2022-05-23 21:52:38 +03:00
Test/Api.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Test.Api where
import qualified Control.Applicative
import qualified Control.Exception
import qualified Control.Monad
import qualified Control.Monad.IO.Class
import qualified Data.Aeson
import qualified Data.Aeson.Encoding
import qualified Data.Aeson.Parser
import qualified Data.Aeson.Types
import qualified Data.Attoparsec.ByteString
import qualified Data.List
2022-10-03 21:10:44 +03:00
import qualified Data.List.NonEmpty
2022-07-01 14:54:51 +03:00
import qualified Data.Map
2022-05-23 21:52:38 +03:00
import qualified Data.Maybe
import qualified Data.Text
import qualified Data.Time
import qualified Data.Text.Encoding
import qualified GHC.Float
import qualified GHC.Int
import qualified GHC.Types
import qualified Network.HTTP.Types
import qualified Network.Wai
import qualified Web.HttpApiData
import Test.Request
import Test.Response
2022-05-23 22:13:30 +03:00
2022-05-23 21:52:38 +03:00
import Test.Response.Test
2022-05-23 22:13:30 +03:00
import Test.Response.Test2
2022-05-23 21:52:38 +03:00
data Api m = Api {
-- | test
test ::
2022-05-23 22:13:30 +03:00
m TestResponse,
-- | test
test2 ::
m Test2Response
2022-05-23 21:52:38 +03:00
}
2022-06-13 00:06:47 +03:00
application :: (Control.Monad.IO.Class.MonadIO m) => (forall a . Network.Wai.Request -> m a -> IO a) -> Api m -> Network.Wai.Application -> Network.Wai.Application
2022-05-23 21:52:38 +03:00
application run api notFound request respond =
case Network.Wai.pathInfo request of
["test"] ->
case Network.Wai.requestMethod request of
"GET" ->
run request (do
2022-06-13 00:06:47 +03:00
response <- test api
2022-05-23 21:52:38 +03:00
Control.Monad.IO.Class.liftIO (respond $! (toResponse response))
)
x ->
unsupportedMethod x
2022-05-23 22:13:30 +03:00
["test2"] ->
case Network.Wai.requestMethod request of
"GET" ->
run request (do
2022-06-13 00:06:47 +03:00
response <- test2 api
2022-05-23 22:13:30 +03:00
Control.Monad.IO.Class.liftIO (respond $! (toResponse response))
)
x ->
unsupportedMethod x
2022-05-23 21:52:38 +03:00
_ ->
notFound request respond
where
unsupportedMethod _ =
2022-06-20 16:54:38 +03:00
respond (Network.Wai.responseBuilder Network.HTTP.Types.status405 [] mempty)
2022-06-13 00:06:47 +03:00
{-# INLINABLE application #-}
2022-05-23 21:52:38 +03:00
---------------------
Test/Request.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Request
2022-10-03 21:10:44 +03:00
( -- * Parameters
Style (..),
pathVariable,
2022-05-23 21:52:38 +03:00
requiredQueryParameter,
2022-10-03 21:10:44 +03:00
requiredQueryParameters,
2022-05-23 21:52:38 +03:00
optionalQueryParameter,
2022-10-03 21:10:44 +03:00
optionalQueryParameters,
2022-05-23 21:52:38 +03:00
requiredHeader,
optionalHeader,
2022-10-03 21:10:44 +03:00
-- * Request body
2022-05-23 21:52:38 +03:00
parseRequestBody,
jsonBodyParser,
formBodyParser,
)
where
import Data.Aeson (FromJSON, parseJSON)
import qualified Data.Aeson.Parser
import qualified Data.Aeson.Types
import Data.Attoparsec.ByteString (eitherResult, parseWith)
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as LBS
2022-10-03 21:10:44 +03:00
import Data.Coerce (coerce)
import qualified Data.HashMap.Strict as HashMap
2022-05-23 21:52:38 +03:00
import qualified Data.List as List
2022-10-03 21:10:44 +03:00
import qualified Data.List.NonEmpty as NonEmpty
2022-05-23 21:52:38 +03:00
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
2022-09-03 12:40:52 +03:00
import qualified Network.HTTP.Media
2022-05-23 21:52:38 +03:00
import Network.HTTP.Types (HeaderName, hContentType)
import qualified Network.Wai as Wai
import System.IO.Unsafe (unsafeInterleaveIO)
2022-10-03 21:10:44 +03:00
import Web.FormUrlEncoded
( FromForm,
parseAll,
urlDecodeAsForm,
urlDecodeForm,
)
2022-05-23 21:52:38 +03:00
import Web.HttpApiData
( FromHttpApiData,
parseHeader,
parseQueryParam,
parseUrlPiece,
2022-10-03 21:10:44 +03:00
parseUrlPieces,
2022-05-23 21:52:38 +03:00
)
pathVariable ::
FromHttpApiData a =>
-- | Path variable value
Text ->
(a -> Wai.Application) ->
Wai.Application
pathVariable value withVariable = \request respond ->
case parseUrlPiece value of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right x ->
withVariable x request respond
{-# INLINEABLE pathVariable #-}
2022-10-03 21:10:44 +03:00
data Style
= FormStyle
| CommaDelimitedStyle
| SpaceDelimitedStyle
| PipeDelimitedStyle
newtype CommaDelimitedValue a = CommaDelimitedValue {unCommaDelimitedValue :: [a]}
instance FromHttpApiData a => FromHttpApiData (CommaDelimitedValue a) where
parseUrlPiece input = do
xs <- parseUrlPieces (Text.splitOn "," input)
pure (CommaDelimitedValue xs)
newtype SpaceDelimitedValue a = SpaceDelimitedValue {unSpaceDelimitedValue :: [a]}
instance FromHttpApiData a => FromHttpApiData (SpaceDelimitedValue a) where
parseUrlPiece input = do
xs <- parseUrlPieces (Text.splitOn " " input)
pure (SpaceDelimitedValue xs)
newtype PipeDelimitedValue a = PipeDelimitedValue {unPipeDelimitedValue :: [a]}
instance FromHttpApiData a => FromHttpApiData (PipeDelimitedValue a) where
parseUrlPiece input = do
xs <- parseUrlPieces (Text.splitOn "|" input)
pure (PipeDelimitedValue xs)
requiredQueryParameters ::
FromHttpApiData a =>
Style ->
ByteString ->
(NonEmpty.NonEmpty a -> Wai.Application) ->
Wai.Application
requiredQueryParameters style name withParam =
case style of
FormStyle -> \request respond ->
case urlDecodeForm (LBS.fromStrict (ByteString.drop 1 (Wai.rawQueryString request))) of
Left error ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right form ->
case parseAll (Text.decodeUtf8 name) form of
Left _ ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right [] ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right (x : xs) ->
withParam (x NonEmpty.:| xs) request respond
SpaceDelimitedStyle ->
requiredQueryParameter
name
( \xs -> \request respond ->
case NonEmpty.nonEmpty (unSpaceDelimitedValue xs) of
Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just xs ->
withParam xs request respond
)
PipeDelimitedStyle ->
requiredQueryParameter
name
( \xs -> \request respond ->
case NonEmpty.nonEmpty (unPipeDelimitedValue xs) of
Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just xs ->
withParam xs request respond
)
CommaDelimitedStyle ->
requiredQueryParameter
name
( \xs -> \request respond ->
case NonEmpty.nonEmpty (unCommaDelimitedValue xs) of
Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just xs ->
withParam xs request respond
)
optionalQueryParameters ::
FromHttpApiData a =>
Style ->
ByteString ->
(Maybe (NonEmpty.NonEmpty a) -> Wai.Application) ->
Wai.Application
optionalQueryParameters style name withParam =
case style of
FormStyle -> \request respond ->
case urlDecodeForm (LBS.fromStrict (ByteString.drop 1 (Wai.rawQueryString request))) of
Left error ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right form ->
case parseAll (Text.decodeUtf8 name) form of
Left _ ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right [] ->
withParam Nothing request respond
Right (x : xs) ->
withParam (Just (x NonEmpty.:| xs)) request respond
SpaceDelimitedStyle ->
optionalQueryParameter
name
False
( \xs ->
withParam (xs >>= NonEmpty.nonEmpty . unSpaceDelimitedValue)
)
PipeDelimitedStyle ->
optionalQueryParameter
name
False
( \xs ->
withParam (xs >>= NonEmpty.nonEmpty . unPipeDelimitedValue)
)
CommaDelimitedStyle ->
optionalQueryParameter
name
False
( \xs ->
withParam (xs >>= NonEmpty.nonEmpty . unCommaDelimitedValue)
)
2022-05-23 21:52:38 +03:00
requiredQueryParameter ::
FromHttpApiData a =>
ByteString ->
(a -> Wai.Application) ->
Wai.Application
requiredQueryParameter name withParam = \request respond ->
case List.lookup name (Wai.queryString request) of
Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just (Just value) ->
case parseQueryParam (Text.decodeUtf8 value) of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right x ->
withParam x request respond
{-# INLINEABLE requiredQueryParameter #-}
optionalQueryParameter ::
FromHttpApiData a =>
ByteString ->
-- | Allow empty, e.g. "x="
Bool ->
(Maybe a -> Wai.Application) ->
Wai.Application
optionalQueryParameter name allowEmpty withParam = \request respond ->
case List.lookup name (Wai.queryString request) of
Nothing ->
withParam Nothing request respond
Just Nothing
| allowEmpty ->
2022-10-03 21:10:44 +03:00
withParam Nothing request respond
2022-05-23 21:52:38 +03:00
| otherwise ->
2022-10-03 21:10:44 +03:00
respond (Wai.responseBuilder (toEnum 400) [] mempty)
2022-05-23 21:52:38 +03:00
Just (Just value) ->
case parseQueryParam (Text.decodeUtf8 value) of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right x ->
withParam (Just x) request respond
{-# INLINEABLE optionalQueryParameter #-}
optionalHeader ::
FromHttpApiData a =>
HeaderName ->
(Maybe a -> Wai.Application) ->
Wai.Application
optionalHeader name withHeader = \request respond ->
case List.lookup name (Wai.requestHeaders request) of
Nothing ->
withHeader Nothing request respond
Just value ->
case parseHeader value of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right x ->
withHeader (Just x) request respond
{-# INLINEABLE optionalHeader #-}
requiredHeader ::
FromHttpApiData a =>
HeaderName ->
(a -> Wai.Application) ->
Wai.Application
requiredHeader name withHeader = \request respond ->
case List.lookup name (Wai.requestHeaders request) of
Nothing ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Just value ->
case parseHeader value of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right x ->
withHeader x request respond
{-# INLINEABLE requiredHeader #-}
2022-09-03 12:40:52 +03:00
data BodyParser a
= BodyParser
Network.HTTP.Media.MediaType
((a -> Wai.Application) -> Wai.Application)
2022-05-23 21:52:38 +03:00
jsonBodyParser :: FromJSON a => BodyParser a
jsonBodyParser = BodyParser "application/json" parseRequestBodyJSON
{-# INLINE jsonBodyParser #-}
formBodyParser :: FromForm a => BodyParser a
formBodyParser = BodyParser "application/xxx-form-urlencoded" parseRequestBodyForm
{-# INLINE formBodyParser #-}
parseRequestBody :: [BodyParser a] -> (a -> Wai.Application) -> Wai.Application
parseRequestBody parsers withBody = \request respond -> do
let contentType =
fromMaybe
2022-09-03 12:40:52 +03:00
"application/octet-stream"
2022-05-23 21:52:38 +03:00
(List.lookup hContentType (Wai.requestHeaders request))
bodyParser =
2022-09-03 12:40:52 +03:00
Network.HTTP.Media.mapAccept
[(mediaType, parser) | BodyParser mediaType parser <- parsers]
contentType
2022-05-23 21:52:38 +03:00
case bodyParser of
2022-09-03 12:40:52 +03:00
Just parseBody ->
2022-05-23 21:52:38 +03:00
parseBody withBody request respond
Nothing ->
respond (Wai.responseBuilder (toEnum 415) [] mempty)
{-# INLINE parseRequestBody #-}
parseRequestBodyJSON :: FromJSON a => (a -> Wai.Application) -> Wai.Application
parseRequestBodyJSON withBody = \request respond -> do
result <- parseWith (Wai.getRequestBodyChunk request) Data.Aeson.Parser.json' mempty
case eitherResult result of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right value ->
case Data.Aeson.Types.parseEither Data.Aeson.parseJSON value of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right body ->
withBody body request respond
{-# INLINEABLE parseRequestBodyJSON #-}
parseRequestBodyForm :: FromForm a => (a -> Wai.Application) -> Wai.Application
parseRequestBodyForm withBody = \request respond -> do
-- Reads the body using lazy IO. Not great but it gets us
-- going and is pretty local.
let getBodyBytes :: IO [ByteString]
getBodyBytes = do
chunk <- Wai.getRequestBodyChunk request
case chunk of
"" -> pure []
_ -> do
rest <- unsafeInterleaveIO getBodyBytes
pure (chunk : rest)
bytes <- getBodyBytes
case urlDecodeAsForm (LBS.fromChunks bytes) of
Left _err ->
respond (Wai.responseBuilder (toEnum 400) [] mempty)
Right form ->
withBody form request respond
{-# INLINEABLE parseRequestBodyForm #-}
---------------------
Test/Response.hs
2022-06-12 23:55:51 +03:00
module Test.Response
( ToResponse (..),
2022-12-20 11:19:42 +03:00
-- * NDJSON support
NDJSON,
responseNDJSON,
2022-06-12 23:55:51 +03:00
)
where
2022-05-23 21:52:38 +03:00
2022-12-20 11:19:42 +03:00
import qualified Data.Aeson
import qualified Data.Aeson.Encoding
import qualified Data.ByteString.Builder
import qualified Network.HTTP.Types
2022-05-23 21:52:38 +03:00
import qualified Network.Wai
2022-12-20 11:19:42 +03:00
type NDJSON element = ((element -> IO ()) -> IO () -> IO ())
responseNDJSON :: Data.Aeson.ToJSON element => Network.HTTP.Types.Status -> Network.HTTP.Types.ResponseHeaders -> NDJSON element -> Network.Wai.Response
responseNDJSON status responseHeaders stream =
Network.Wai.responseStream status responseHeaders $ \emit flush ->
stream
( \element ->
emit
( Data.Aeson.Encoding.fromEncoding (Data.Aeson.toEncoding element)
<> Data.ByteString.Builder.char7 '\n'
)
)
flush
2022-05-23 21:52:38 +03:00
class ToResponse a where
2022-06-12 23:55:51 +03:00
toResponse :: a -> Network.Wai.Response
2022-05-23 21:52:38 +03:00
---------------------
Test/Response/Test.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Test.Response.Test where
import qualified Control.Applicative
import qualified Control.Exception
import qualified Control.Monad
import qualified Control.Monad.IO.Class
import qualified Data.Aeson
import qualified Data.Aeson.Encoding
import qualified Data.Aeson.Parser
import qualified Data.Aeson.Types
import qualified Data.Attoparsec.ByteString
import qualified Data.List
2022-10-03 21:10:44 +03:00
import qualified Data.List.NonEmpty
2022-07-01 14:54:51 +03:00
import qualified Data.Map
2022-05-23 21:52:38 +03:00
import qualified Data.Maybe
import qualified Data.Text
import qualified Data.Time
import qualified Data.Text.Encoding
import qualified GHC.Float
import qualified GHC.Int
import qualified GHC.Types
import qualified Network.HTTP.Types
import qualified Network.Wai
import qualified Web.HttpApiData
2022-05-23 22:13:30 +03:00
2022-05-23 21:52:38 +03:00
import Test.Response
data TestResponse
= TestResponse200 Network.Wai.StreamingBody
instance ToResponse TestResponse where
toResponse (TestResponse200 x) =
2022-06-20 16:54:38 +03:00
Network.Wai.responseStream Network.HTTP.Types.status200 ([(Network.HTTP.Types.hContentType, "text/csv")]) x
2022-05-23 22:13:30 +03:00
instance Show TestResponse where
show _ = "TestResponse {}"
2022-05-23 21:52:38 +03:00
---------------------
2022-05-23 22:13:30 +03:00
Test/Response/Test2.hs
2022-05-23 21:52:38 +03:00
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
2022-05-23 22:13:30 +03:00
module Test.Response.Test2 where
2022-05-23 21:52:38 +03:00
import qualified Control.Applicative
import qualified Control.Exception
import qualified Control.Monad
import qualified Control.Monad.IO.Class
import qualified Data.Aeson
import qualified Data.Aeson.Encoding
import qualified Data.Aeson.Parser
import qualified Data.Aeson.Types
import qualified Data.Attoparsec.ByteString
import qualified Data.List
2022-10-03 21:10:44 +03:00
import qualified Data.List.NonEmpty
2022-07-01 14:54:51 +03:00
import qualified Data.Map
2022-05-23 21:52:38 +03:00
import qualified Data.Maybe
import qualified Data.Text
import qualified Data.Time
import qualified Data.Text.Encoding
import qualified GHC.Float
import qualified GHC.Int
import qualified GHC.Types
import qualified Network.HTTP.Types
import qualified Network.Wai
import qualified Web.HttpApiData
2022-05-23 22:13:30 +03:00
import Test.Response
2022-05-23 21:52:38 +03:00
2022-05-23 22:13:30 +03:00
data Test2Response
= Test2Response200 Data.Aeson.Value
2022-05-23 21:52:38 +03:00
deriving (Show)
2022-05-23 22:13:30 +03:00
instance ToResponse Test2Response where
toResponse (Test2Response200 x) =
2022-06-20 16:54:38 +03:00
Network.Wai.responseBuilder Network.HTTP.Types.status200 ([(Network.HTTP.Types.hContentType, "application/json")]) (Data.Aeson.fromEncoding (Data.Aeson.toEncoding x))
2022-05-23 21:52:38 +03:00
---------------------
test.cabal
cabal-version: 3.0
name: test
version: 0.1.0.0
library
build-depends:
, aeson
, attoparsec
, base
, bytestring
2022-07-01 14:54:51 +03:00
, containers
2022-05-23 21:52:38 +03:00
, ghc-prim
, http-api-data
2022-09-03 12:40:52 +03:00
, http-media
2022-05-23 21:52:38 +03:00
, http-types
, text
, time
2022-10-03 21:10:44 +03:00
, unordered-containers
2022-05-23 21:52:38 +03:00
, wai
exposed-modules:
Test.Api
Test.Request
Test.Response
Test.Response.Test
2022-05-23 22:13:30 +03:00
Test.Response.Test2