tie/Request.template.hs

331 lines
10 KiB
Haskell
Raw Normal View History

{-# 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,
requiredQueryParameter,
2022-10-03 21:10:44 +03:00
requiredQueryParameters,
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,
)
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
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)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Network.HTTP.Media
2022-03-02 17:38:42 +03:00
import Network.HTTP.Types (HeaderName, hContentType)
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,
)
import Web.HttpApiData
( FromHttpApiData,
2022-03-02 17:38:42 +03:00
parseHeader,
parseQueryParam,
parseUrlPiece,
2022-10-03 21:10:44 +03:00
parseUrlPieces,
)
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)
)
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
| otherwise ->
2022-10-03 21:10:44 +03:00
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 (Just x) request respond
{-# INLINEABLE optionalQueryParameter #-}
2022-03-02 17:38:42 +03:00
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 #-}
data BodyParser a
= BodyParser
Network.HTTP.Media.MediaType
((a -> Wai.Application) -> Wai.Application)
2022-03-06 15:11:02 +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
"application/octet-stream"
2022-03-06 15:11:02 +03:00
(List.lookup hContentType (Wai.requestHeaders request))
bodyParser =
Network.HTTP.Media.mapAccept
[(mediaType, parser) | BodyParser mediaType parser <- parsers]
contentType
2022-03-06 15:11:02 +03:00
case bodyParser of
Just parseBody ->
2022-03-06 15:11:02 +03:00
parseBody withBody request respond
Nothing ->
respond (Wai.responseBuilder (toEnum 415) [] mempty)
{-# INLINE parseRequestBody #-}
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
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
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 #-}