2022-03-02 11:36:08 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
2022-03-09 16:21:59 +03:00
|
|
|
module Tie.Template.Request_
|
2022-10-03 21:10:44 +03:00
|
|
|
( -- * Parameters
|
|
|
|
Style (..),
|
|
|
|
pathVariable,
|
2022-03-02 11:36:08 +03:00
|
|
|
requiredQueryParameter,
|
2022-10-03 21:10:44 +03:00
|
|
|
requiredQueryParameters,
|
2022-03-02 11:36:08 +03:00
|
|
|
optionalQueryParameter,
|
2022-10-03 21:10:44 +03:00
|
|
|
optionalQueryParameters,
|
2022-03-02 17:38:42 +03:00
|
|
|
requiredHeader,
|
|
|
|
optionalHeader,
|
2022-10-03 21:10:44 +03:00
|
|
|
|
|
|
|
-- * Request body
|
2022-03-06 15:11:02 +03:00
|
|
|
parseRequestBody,
|
|
|
|
jsonBodyParser,
|
|
|
|
formBodyParser,
|
2022-03-02 11:36:08 +03:00
|
|
|
)
|
|
|
|
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
|
2022-03-06 15:11:02 +03:00
|
|
|
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-03-02 11:36:08 +03:00
|
|
|
import qualified Data.List as List
|
2022-10-03 21:10:44 +03:00
|
|
|
import qualified Data.List.NonEmpty as NonEmpty
|
2022-03-06 15:11:02 +03:00
|
|
|
import Data.Maybe (fromMaybe)
|
2022-03-02 11:36:08 +03:00
|
|
|
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-03-02 17:38:42 +03:00
|
|
|
import Network.HTTP.Types (HeaderName, hContentType)
|
2022-03-02 11:36:08 +03:00
|
|
|
import qualified Network.Wai as Wai
|
2022-03-06 15:11:02 +03:00
|
|
|
import System.IO.Unsafe (unsafeInterleaveIO)
|
2022-10-03 21:10:44 +03:00
|
|
|
import Web.FormUrlEncoded
|
|
|
|
( FromForm,
|
|
|
|
parseAll,
|
|
|
|
urlDecodeAsForm,
|
|
|
|
urlDecodeForm,
|
|
|
|
)
|
2022-03-02 11:36:08 +03:00
|
|
|
import Web.HttpApiData
|
|
|
|
( FromHttpApiData,
|
2022-03-02 17:38:42 +03:00
|
|
|
parseHeader,
|
2022-03-02 11:36:08 +03:00
|
|
|
parseQueryParam,
|
|
|
|
parseUrlPiece,
|
2022-10-03 21:10:44 +03:00
|
|
|
parseUrlPieces,
|
2022-03-02 11:36:08 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
pathVariable ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-03-02 11:36:08 +03:00
|
|
|
-- | Path variable value
|
|
|
|
Text ->
|
|
|
|
(a -> Wai.Application) ->
|
|
|
|
Wai.Application
|
|
|
|
pathVariable value withVariable = \request respond ->
|
|
|
|
case parseUrlPiece value of
|
2022-03-02 14:55:08 +03:00
|
|
|
Left _err ->
|
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Right x ->
|
|
|
|
withVariable x request respond
|
2022-03-02 14:55:08 +03:00
|
|
|
{-# INLINEABLE pathVariable #-}
|
2022-03-02 11:36:08 +03:00
|
|
|
|
2022-10-03 21:10:44 +03:00
|
|
|
data Style
|
|
|
|
= FormStyle
|
|
|
|
| CommaDelimitedStyle
|
|
|
|
| SpaceDelimitedStyle
|
|
|
|
| PipeDelimitedStyle
|
|
|
|
|
|
|
|
newtype CommaDelimitedValue a = CommaDelimitedValue {unCommaDelimitedValue :: [a]}
|
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
instance (FromHttpApiData a) => FromHttpApiData (CommaDelimitedValue a) where
|
2022-10-03 21:10:44 +03:00
|
|
|
parseUrlPiece input = do
|
|
|
|
xs <- parseUrlPieces (Text.splitOn "," input)
|
|
|
|
pure (CommaDelimitedValue xs)
|
|
|
|
|
|
|
|
newtype SpaceDelimitedValue a = SpaceDelimitedValue {unSpaceDelimitedValue :: [a]}
|
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
instance (FromHttpApiData a) => FromHttpApiData (SpaceDelimitedValue a) where
|
2022-10-03 21:10:44 +03:00
|
|
|
parseUrlPiece input = do
|
|
|
|
xs <- parseUrlPieces (Text.splitOn " " input)
|
|
|
|
pure (SpaceDelimitedValue xs)
|
|
|
|
|
|
|
|
newtype PipeDelimitedValue a = PipeDelimitedValue {unPipeDelimitedValue :: [a]}
|
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
instance (FromHttpApiData a) => FromHttpApiData (PipeDelimitedValue a) where
|
2022-10-03 21:10:44 +03:00
|
|
|
parseUrlPiece input = do
|
|
|
|
xs <- parseUrlPieces (Text.splitOn "|" input)
|
|
|
|
pure (PipeDelimitedValue xs)
|
|
|
|
|
|
|
|
requiredQueryParameters ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-10-03 21:10:44 +03:00
|
|
|
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 ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-10-03 21:10:44 +03:00
|
|
|
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-03-02 11:36:08 +03:00
|
|
|
requiredQueryParameter ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-03-02 11:36:08 +03:00
|
|
|
ByteString ->
|
|
|
|
(a -> Wai.Application) ->
|
|
|
|
Wai.Application
|
|
|
|
requiredQueryParameter name withParam = \request respond ->
|
|
|
|
case List.lookup name (Wai.queryString request) of
|
|
|
|
Nothing ->
|
2022-03-02 14:55:08 +03:00
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Just Nothing ->
|
2022-03-02 14:55:08 +03:00
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Just (Just value) ->
|
|
|
|
case parseQueryParam (Text.decodeUtf8 value) of
|
2022-03-02 14:55:08 +03:00
|
|
|
Left _err ->
|
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Right x ->
|
|
|
|
withParam x request respond
|
2022-03-02 14:55:08 +03:00
|
|
|
{-# INLINEABLE requiredQueryParameter #-}
|
2022-03-02 11:36:08 +03:00
|
|
|
|
|
|
|
optionalQueryParameter ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-03-02 11:36:08 +03:00
|
|
|
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-03-02 11:36:08 +03:00
|
|
|
| otherwise ->
|
2022-10-03 21:10:44 +03:00
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Just (Just value) ->
|
|
|
|
case parseQueryParam (Text.decodeUtf8 value) of
|
2022-03-02 14:55:08 +03:00
|
|
|
Left _err ->
|
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-02 11:36:08 +03:00
|
|
|
Right x ->
|
|
|
|
withParam (Just x) request respond
|
2022-03-02 14:55:08 +03:00
|
|
|
{-# INLINEABLE optionalQueryParameter #-}
|
2022-03-02 11:36:08 +03:00
|
|
|
|
2022-03-02 17:38:42 +03:00
|
|
|
optionalHeader ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-03-02 17:38:42 +03:00
|
|
|
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 ::
|
2023-04-07 11:37:04 +03:00
|
|
|
(FromHttpApiData a) =>
|
2022-03-02 17:38:42 +03:00
|
|
|
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-03-06 15:11:02 +03:00
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
jsonBodyParser :: (FromJSON a) => BodyParser a
|
2022-03-06 15:11:02 +03:00
|
|
|
jsonBodyParser = BodyParser "application/json" parseRequestBodyJSON
|
|
|
|
{-# INLINE jsonBodyParser #-}
|
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
formBodyParser :: (FromForm a) => BodyParser a
|
2022-03-06 15:11:02 +03:00
|
|
|
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-03-06 15:11:02 +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-03-06 15:11:02 +03:00
|
|
|
|
|
|
|
case bodyParser of
|
2022-09-03 12:40:52 +03:00
|
|
|
Just parseBody ->
|
2022-03-06 15:11:02 +03:00
|
|
|
parseBody withBody request respond
|
|
|
|
Nothing ->
|
|
|
|
respond (Wai.responseBuilder (toEnum 415) [] mempty)
|
|
|
|
{-# INLINE parseRequestBody #-}
|
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
parseRequestBodyJSON :: (FromJSON a) => (a -> Wai.Application) -> Wai.Application
|
2022-03-06 15:11:02 +03:00
|
|
|
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
|
2022-03-02 14:55:08 +03:00
|
|
|
Left _err ->
|
|
|
|
respond (Wai.responseBuilder (toEnum 400) [] mempty)
|
2022-03-06 15:11:02 +03:00
|
|
|
Right body ->
|
|
|
|
withBody body request respond
|
2022-03-02 17:38:42 +03:00
|
|
|
{-# INLINEABLE parseRequestBodyJSON #-}
|
2022-03-06 15:11:02 +03:00
|
|
|
|
2023-04-07 11:37:04 +03:00
|
|
|
parseRequestBodyForm :: (FromForm a) => (a -> Wai.Application) -> Wai.Application
|
2022-03-06 15:11:02 +03:00
|
|
|
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 #-}
|