{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -Wall -Werror -Wno-unticked-promoted-constructors #-}
module Trasa.Core
(
Bodiedness(..)
, Content(..)
, Payload(..)
, Router
, Prepared(..)
, Concealed(..)
, Constructed(..)
, conceal
, concealedToPrepared
, mapConstructed
, Method
, encodeMethod
, decodeMethod
, QueryString(..)
, encodeQuery
, decodeQuery
, Url(..)
, encodeUrl
, decodeUrl
, TrasaErr(..)
, status
, prepareWith
, linkWith
, dispatchWith
, parseWith
, payloadWith
, requestWith
, routerWith
, Path(..)
, match
, capture
, end
, (./)
, mapPath
, appendPath
, Param(..)
, Query(..)
, Parameter(..)
, Rec(..)
, demoteParameter
, flag
, optional
, list
, qend
, (.&)
, mapQuery
, RequestBody(..)
, body
, bodyless
, encodeRequestBody
, decodeRequestBody
, mapRequestBody
, ResponseBody(..)
, resp
, encodeResponseBody
, decodeResponseBody
, mapResponseBody
, Many(..)
, one
, mapMany
, Meta(..)
, MetaBuilder
, metaBuilderToMetaCodec
, MetaCodec
, MetaClient
, metaCodecToMetaClient
, MetaServer
, metaCodecToMetaServer
, mapMetaPath
, mapMetaQuery
, mapMetaRequestBody
, mapMetaResponseBody
, mapMeta
, CaptureEncoding(..)
, HasCaptureEncoding(..)
, CaptureDecoding(..)
, HasCaptureDecoding(..)
, CaptureCodec(..)
, HasCaptureCodec(..)
, BodyEncoding(..)
, HasBodyEncoding(..)
, BodyDecoding(..)
, HasBodyDecoding(..)
, BodyCodec(..)
, HasBodyCodec(..)
, captureCodecToCaptureEncoding
, captureCodecToCaptureDecoding
, bodyCodecToBodyEncoding
, bodyCodecToBodyDecoding
, showReadCaptureCodec
, showReadBodyCodec
, ParamBase
, Arguments
, handler
, prettyRouter
) where
import Data.Kind (Type)
import Data.Functor.Identity (Identity(..))
import Control.Applicative (liftA2)
import Data.Maybe (mapMaybe,listToMaybe,isJust)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import Data.Foldable (toList)
import Data.Bifunctor (first,bimap)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.HTTP.Types.Status as N
import qualified Network.HTTP.Media.MediaType as N
import qualified Network.HTTP.Media.Accept as N
import qualified Data.HashMap.Strict as HM
import qualified Data.Semigroup as SG
import Data.HashMap.Strict (HashMap)
import qualified Topaz.Rec as Topaz
import Topaz.Types (Rec(..), type (++))
import Trasa.Method
import Trasa.Url
import Trasa.Error
import Trasa.Codec
newtype Many f a = Many { getMany :: NonEmpty (f a) }
deriving (Functor)
instance Applicative f => Applicative (Many f) where
pure = Many . pure . pure
Many mf <*> Many mx = Many $ liftA2 (<*>) mf mx
one :: f a -> Many f a
one = Many . pure
mapMany :: (forall x. f x -> g x) -> Many f a -> Many g a
mapMany eta (Many m) = Many (fmap eta m)
data Bodiedness = forall a. Body a | Bodyless
data RequestBody :: (Type -> Type) -> Bodiedness -> Type where
RequestBodyPresent :: !(f a) -> RequestBody f ('Body a)
RequestBodyAbsent :: RequestBody f 'Bodyless
body :: rqf req -> RequestBody rqf ('Body req)
body = RequestBodyPresent
bodyless :: RequestBody rqf 'Bodyless
bodyless = RequestBodyAbsent
mapRequestBody :: (forall x. rqf x -> rqf' x) -> RequestBody rqf request -> RequestBody rqf' request
mapRequestBody _ RequestBodyAbsent = RequestBodyAbsent
mapRequestBody f (RequestBodyPresent reqBod) = RequestBodyPresent (f reqBod)
newtype ResponseBody rpf response = ResponseBody { getResponseBody :: rpf response }
resp :: rpf resp -> ResponseBody rpf resp
resp = ResponseBody
mapResponseBody :: (forall x. rpf x -> rpf' x) -> ResponseBody rpf request -> ResponseBody rpf' request
mapResponseBody f (ResponseBody resBod) = ResponseBody (f resBod)
data Path :: (Type -> Type) -> [Type] -> Type where
PathNil :: Path cap '[]
PathConsCapture :: !(cap a) -> !(Path cap as) -> Path cap (a ': as)
PathConsMatch :: !T.Text -> !(Path cap as) -> Path cap as
infixr 7 ./
(./) :: (a -> b) -> a -> b
(./) f a = f a
match :: T.Text -> Path cpf caps -> Path cpf caps
match = PathConsMatch
capture :: cpf cap -> Path cpf caps -> Path cpf (cap ': caps)
capture = PathConsCapture
end :: Path cpf '[]
end = PathNil
mapPath :: (forall x. cf x -> cf' x) -> Path cf ps -> Path cf' ps
mapPath _ PathNil = PathNil
mapPath f (PathConsMatch s pnext) = PathConsMatch s (mapPath f pnext)
mapPath f (PathConsCapture c pnext) = PathConsCapture (f c) (mapPath f pnext)
appendPath :: Path f as -> Path f bs -> Path f (as ++ bs)
appendPath PathNil bs = bs
appendPath (PathConsMatch a as) bs = PathConsMatch a (appendPath as bs)
appendPath (PathConsCapture cas as) bs = PathConsCapture cas (appendPath as bs)
data Param
= Flag
| forall a. Optional a
| forall a. List a
data Parameter :: Param -> Type where
ParameterFlag :: !Bool -> Parameter Flag
ParameterOptional :: !(Maybe a) -> Parameter (Optional a)
ParameterList :: ![a] -> Parameter (List a)
data Query :: (Type -> Type) -> Param -> Type where
QueryFlag :: !T.Text -> Query cap Flag
QueryOptional :: !T.Text -> !(cap a) -> Query cap (Optional a)
QueryList :: !T.Text -> !(cap a) -> Query cap (List a)
flag :: T.Text -> Query cpf Flag
flag = QueryFlag
optional :: T.Text -> cpf query -> Query cpf (Optional query)
optional = QueryOptional
list :: T.Text -> cpf query -> Query cpf (List query)
list = QueryList
qend :: Rec (Query qpf) '[]
qend = RecNil
infixr 7 .&
(.&) :: Query qpf q -> Rec (Query qpf) qs -> Rec (Query qpf) (q ': qs)
(.&) = RecCons
mapQuery :: (forall x. f x -> g x) -> Rec (Query f) qs -> Rec (Query g) qs
mapQuery eta = Topaz.map $ \case
QueryFlag key -> QueryFlag key
QueryOptional key query -> QueryOptional key (eta query)
QueryList key query -> QueryList key (eta query)
data Meta capCodec qryCodec reqCodec respCodec caps qrys req resp = Meta
{ metaPath :: !(Path capCodec caps)
, metaQuery :: !(Rec (Query qryCodec) qrys)
, metaRequestBody :: !(RequestBody reqCodec req)
, metaResponseBody :: !(ResponseBody respCodec resp)
, metaMethod :: !Method
}
mapMetaPath
:: (forall x. cf x -> cg x)
-> Meta cf qryCodec reqCodec respCodec caps qrys req resp
-> Meta cg qryCodec reqCodec respCodec caps qrys req resp
mapMetaPath eta m = m { metaPath = mapPath eta (metaPath m) }
mapMetaQuery
:: (forall x. qf x -> qg x)
-> Meta capCodec qf reqCodec respCodec caps qrys req resp
-> Meta capCodec qg reqCodec respCodec caps qrys req resp
mapMetaQuery eta m = m { metaQuery = mapQuery eta (metaQuery m) }
mapMetaRequestBody
:: (forall x. rf x -> rg x)
-> Meta capCodec qryCodec rf respCodec caps qrys req resp
-> Meta capCodec qryCodec rg respCodec caps qrys req resp
mapMetaRequestBody eta m = m { metaRequestBody = mapRequestBody eta (metaRequestBody m) }
mapMetaResponseBody
:: (forall x. rf x -> rg x)
-> Meta capCodec qryCodec reqCodec rf caps qrys req resp
-> Meta capCodec qryCodec reqCodec rg caps qrys req resp
mapMetaResponseBody eta m = m { metaResponseBody = mapResponseBody eta (metaResponseBody m)}
mapMeta
:: (forall x. capCodec1 x -> capCodec2 x)
-> (forall x. qryCodec1 x -> qryCodec2 x)
-> (forall x. reqCodec1 x -> reqCodec2 x)
-> (forall x. respCodec1 x -> respCodec2 x)
-> Meta capCodec1 qryCodec1 reqCodec1 respCodec1 caps qrys req resp
-> Meta capCodec2 qryCodec2 reqCodec2 respCodec2 caps qrys req resp
mapMeta mapCaps mapQrys mapReq mapResp (Meta caps qrys req res method) = Meta
(mapPath mapCaps caps)
(mapQuery mapQrys qrys)
(mapRequestBody mapReq req)
(mapResponseBody mapResp res)
method
type MetaBuilder = Meta CaptureCodec CaptureCodec BodyCodec BodyCodec
metaBuilderToMetaCodec
:: Meta capCodec qryCodec reqCodec respCodec caps qrys req resp
-> Meta capCodec qryCodec (Many reqCodec) (Many respCodec) caps qrys req resp
metaBuilderToMetaCodec (Meta path query reqBody respBody method) = Meta
path
query
(mapRequestBody one reqBody)
(mapResponseBody one respBody)
method
type MetaCodec = Meta CaptureCodec CaptureCodec (Many BodyCodec) (Many BodyCodec)
type MetaClient = Meta CaptureEncoding CaptureEncoding (Many BodyEncoding) (Many BodyDecoding)
metaCodecToMetaClient :: MetaCodec caps qrys req resp -> MetaClient caps qrys req resp
metaCodecToMetaClient = mapMeta captureEncoding captureEncoding (mapMany bodyEncoding) (mapMany bodyDecoding)
type MetaServer = Meta CaptureDecoding CaptureDecoding (Many BodyDecoding) (Many BodyEncoding)
metaCodecToMetaServer :: MetaCodec caps qrys req resp -> MetaServer caps qrys req resp
metaCodecToMetaServer = mapMeta captureDecoding captureDecoding (mapMany bodyDecoding) (mapMany bodyEncoding)
linkWith
:: forall route response reqCodec respCodec
. (forall caps qrys req resp. route caps qrys req resp -> Meta CaptureEncoding CaptureEncoding reqCodec respCodec caps qrys req resp)
-> Prepared route response
-> Url
linkWith toMeta (Prepared route captures querys _) =
encodePieces (metaPath m) (metaQuery m) captures querys
where m = toMeta route
data Payload = Payload
{ payloadUrl :: !Url
, payloadContent :: !(Maybe Content)
, payloadAccepts :: !(NonEmpty N.MediaType)
}
payloadWith
:: forall route response
. (forall caps qrys req resp. route caps qrys req resp -> MetaClient caps qrys req resp)
-> Prepared route response
-> Payload
payloadWith toMeta p@(Prepared route _ _ reqBody) =
Payload url content accepts
where
url = linkWith toMeta p
m = toMeta route
content = encodeRequestBody (metaRequestBody m) reqBody
ResponseBody (Many decodings) = metaResponseBody m
accepts = bodyDecodingNames =<< decodings
requestWith
:: Functor m
=> (forall caps qrys req resp. route caps qrys req resp -> MetaClient caps qrys req resp)
-> (Method -> Url -> Maybe Content -> NonEmpty N.MediaType -> m (Either TrasaErr Content))
-> Prepared route response
-> m (Either TrasaErr response)
requestWith toMeta run (Prepared route captures querys reqBody) =
let m = toMeta route
method = metaMethod m
url = encodePieces (metaPath m) (metaQuery m) captures querys
content = encodeRequestBody (metaRequestBody m) reqBody
respBodyDecs@(ResponseBody (Many decodings)) = metaResponseBody m
accepts = bodyDecodingNames =<< decodings
in fmap (\c -> c >>= decodeResponseBody respBodyDecs) (run method url content accepts)
encodeRequestBody :: RequestBody (Many BodyEncoding) request -> RequestBody Identity request -> Maybe Content
encodeRequestBody RequestBodyAbsent RequestBodyAbsent = Nothing
encodeRequestBody (RequestBodyPresent (Many encodings)) (RequestBodyPresent (Identity rq)) =
case NE.head encodings of
BodyEncoding names encoding -> Just (Content (NE.head names) (encoding rq))
decodeRequestBody
:: RequestBody (Many BodyDecoding) req
-> Maybe Content
-> Either TrasaErr (RequestBody Identity req)
decodeRequestBody reqDec mcontent = case reqDec of
RequestBodyPresent decs -> case mcontent of
Nothing -> wrongBody
Just (Content media bod) -> go (toList (getMany decs)) media bod
RequestBodyAbsent -> case mcontent of
Nothing -> Right RequestBodyAbsent
Just _ -> wrongBody
where
wrongBody = Left (status N.status415)
go :: [BodyDecoding a] -> N.MediaType -> LBS.ByteString -> Either TrasaErr (RequestBody Identity (Body a))
go [] _ _ = Left (status N.status415)
go (BodyDecoding medias dec:decs) media bod = case any (flip N.matches media) medias of
True -> bimap (TrasaErr N.status415 . LBS.fromStrict . T.encodeUtf8)
(RequestBodyPresent . Identity)
(dec bod)
False -> go decs media bod
encodeResponseBody
:: forall response
. [N.MediaType]
-> ResponseBody (Many BodyEncoding) response
-> response
-> Either TrasaErr Content
encodeResponseBody medias (ResponseBody encs) res = go (toList (getMany encs))
where
go :: [BodyEncoding response] -> Either TrasaErr Content
go [] = Left (status N.status406)
go (BodyEncoding accepts e:es) = case acceptable (toList accepts) medias of
Just typ -> Right (Content typ (e res))
Nothing -> go es
acceptable :: [N.MediaType] -> [N.MediaType] -> Maybe N.MediaType
acceptable [] _ = Nothing
acceptable (a:as) ms = case any (N.matches a) ms of
True -> Just a
False -> acceptable as ms
decodeResponseBody :: ResponseBody (Many BodyDecoding) response -> Content -> Either TrasaErr response
decodeResponseBody (ResponseBody (Many decodings)) (Content name content) = go (toList decodings)
where
go :: [BodyDecoding response] -> Either TrasaErr response
go [] = Left (status N.status415)
go (BodyDecoding names dec:decs) = case any (N.matches name) names of
True -> first (TrasaErr N.status400 . LBS.fromStrict . T.encodeUtf8) (dec content)
False -> go decs
encodePieces
:: Path CaptureEncoding captures
-> Rec (Query CaptureEncoding) querys
-> Rec Identity captures
-> Rec Parameter querys
-> Url
encodePieces pathEncoding queryEncoding path querys =
Url (encodePath pathEncoding path) (QueryString (encodeQueries queryEncoding querys))
where
encodePath
:: forall caps
. Path CaptureEncoding caps
-> Rec Identity caps
-> [T.Text]
encodePath PathNil RecNil = []
encodePath (PathConsMatch str ps) xs = str : encodePath ps xs
encodePath (PathConsCapture (CaptureEncoding enc) ps) (Identity x `RecCons` xs) = enc x : encodePath ps xs
encodeQueries
:: forall qrys
. Rec (Query CaptureEncoding) qrys
-> Rec Parameter qrys
-> HM.HashMap T.Text QueryParam
encodeQueries RecNil RecNil = HM.empty
encodeQueries (QueryFlag key `RecCons` encs) (ParameterFlag on `RecCons` qs) =
if on then HM.insert key QueryParamFlag rest else rest
where rest = encodeQueries encs qs
encodeQueries (QueryOptional key (CaptureEncoding enc) `RecCons` encs) (ParameterOptional mval `RecCons` qs) =
maybe rest (\val -> HM.insert key (QueryParamSingle (enc val)) rest) mval
where rest = encodeQueries encs qs
encodeQueries (QueryList key (CaptureEncoding enc) `RecCons` encs) (ParameterList vals `RecCons` qs) =
HM.insert key (QueryParamList (fmap enc vals)) (encodeQueries encs qs)
dispatchWith
:: forall route m
. Applicative m
=> (forall caps qrys req resp. route caps qrys req resp -> MetaServer caps qrys req resp)
-> (forall caps qrys req resp. route caps qrys req resp -> Rec Identity caps -> Rec Parameter qrys -> RequestBody Identity req -> m resp)
-> Router route
-> Method
-> [N.MediaType]
-> Url
-> Maybe Content
-> m (Either TrasaErr Content)
dispatchWith toMeta makeResponse madeRouter method accepts url mcontent =
case parseWith toMeta madeRouter method url mcontent of
Left err -> pure (Left err)
Right (Concealed route path querys reqBody) ->
encodeResponseBody accepts (metaResponseBody (toMeta route)) <$>
makeResponse route path querys reqBody
routerWith
:: forall route qryCodec reqCodec respCodec
. (forall caps qrys req resp. route caps qrys req resp -> Meta CaptureDecoding qryCodec reqCodec respCodec caps qrys req resp)
-> [Constructed route]
-> Router route
routerWith toMeta = Router . foldMap buildRouter
where
buildRouter :: Constructed route -> IxedRouter route Z
buildRouter (Constructed route) = singletonIxedRouter route (metaMethod m) (metaPath m)
where m = toMeta route
parseWith
:: forall route capCodec respCodec
. (forall caps qrys req resp. route caps qrys req resp -> Meta capCodec CaptureDecoding (Many BodyDecoding) respCodec caps qrys req resp)
-> Router route
-> Method
-> Url
-> Maybe Content
-> Either TrasaErr (Concealed route)
parseWith toMeta madeRouter method (Url encodedPath encodedQuery) mcontent = do
Pathed route captures <- maybe (Left (status N.status404)) Right
$ parsePathWith madeRouter method encodedPath
let m = toMeta route
querys <- parseQueryWith (metaQuery m) encodedQuery
reqBody <- decodeRequestBody (metaRequestBody m) mcontent
return (Concealed route captures querys reqBody)
parsePathWith :: forall route.
Router route
-> Method
-> [T.Text]
-> Maybe (Pathed route)
parsePathWith (Router r0) method pieces0 =
listToMaybe (go VecNil pieces0 r0)
where
go :: forall n.
Vec n T.Text
-> [T.Text]
-> IxedRouter route n
-> [Pathed route]
go captures ps (IxedRouter matches mcapture responders) = case ps of
[] -> case HM.lookup (encodeMethod method) responders of
Nothing -> []
Just respondersAtMethod ->
mapMaybe (\(IxedResponder route capDecs) ->
fmap (\x -> (Pathed route x)) (decodeCaptureVector capDecs captures)
) respondersAtMethod
p : psNext ->
let res1 = maybe [] id $ fmap (go captures psNext) (HM.lookup p matches)
res2 = maybe [] id $ fmap (go (snocVec p captures) psNext) mcapture
in res1 ++ res2
parseQueryWith :: Rec (Query CaptureDecoding) querys -> QueryString -> Either TrasaErr (Rec Parameter querys)
parseQueryWith decoding (QueryString querys) = Topaz.traverse param decoding
where
param :: forall qry. Query CaptureDecoding qry -> Either TrasaErr (Parameter qry)
param = \case
QueryFlag key -> Right (ParameterFlag (HM.member key querys))
QueryOptional key (CaptureDecoding dec) -> case HM.lookup key querys of
Nothing -> Right (ParameterOptional Nothing)
Just query -> case query of
QueryParamFlag -> Left (TrasaErr N.status400 "query flag given when key-value expected")
QueryParamSingle txt -> Right (ParameterOptional (dec txt))
QueryParamList _ -> Left (TrasaErr N.status400 "query param list given when key-value expected")
QueryList key (CaptureDecoding dec) -> case HM.lookup key querys of
Nothing -> Right (ParameterList [])
Just query -> case query of
QueryParamFlag -> Left (TrasaErr N.status400 "query flag given when list expected")
QueryParamSingle txt -> Right (ParameterList (maybe [] (:[]) (dec txt)))
QueryParamList txts -> Right (ParameterList (mapMaybe dec txts))
decodeCaptureVector ::
IxedRec CaptureDecoding n xs
-> Vec n T.Text
-> Maybe (Rec Identity xs)
decodeCaptureVector IxedRecNil VecNil = Just RecNil
decodeCaptureVector (IxedRecCons (CaptureDecoding decode) rnext) (VecCons piece vnext) = do
val <- decode piece
vals <- decodeCaptureVector rnext vnext
return (Identity val `RecCons` vals)
type family ParamBase (param :: Param) :: Type where
ParamBase Flag = Bool
ParamBase (Optional a) = Maybe a
ParamBase (List a) = [a]
demoteParameter :: Parameter param -> ParamBase param
demoteParameter = \case
ParameterFlag b -> b
ParameterOptional m -> m
ParameterList l -> l
type family Arguments (pieces :: [Type]) (querys :: [Param]) (body :: Bodiedness) (result :: Type) :: Type where
Arguments '[] '[] ('Body b) r = b -> r
Arguments '[] '[] 'Bodyless r = r
Arguments '[] (q ': qs) r b = ParamBase q -> Arguments '[] qs r b
Arguments (c ': cs) qs b r = c -> Arguments cs qs b r
prepareWith
:: (forall caps qrys req resp. route caps qrys req resp -> Meta capCodec qryCodec reqCodec respCodec caps qrys req resp)
-> route captures query request response
-> Arguments captures query request (Prepared route response)
prepareWith toMeta route =
prepareExplicit route (metaPath m) (metaQuery m) (metaRequestBody m)
where m = toMeta route
prepareExplicit :: forall route captures queries request response rqf pf qf.
route captures queries request response
-> Path pf captures
-> Rec (Query qf) queries
-> RequestBody rqf request
-> Arguments captures queries request (Prepared route response)
prepareExplicit route = go (Prepared route)
where
go :: forall caps qrys z.
(Rec Identity caps -> Rec Parameter qrys -> RequestBody Identity request -> z)
-> Path pf caps
-> Rec (Query qf) qrys
-> RequestBody rqf request
-> Arguments caps qrys request z
go k PathNil RecNil RequestBodyAbsent =
k RecNil RecNil RequestBodyAbsent
go k PathNil RecNil (RequestBodyPresent _) =
\reqBod -> k RecNil RecNil (RequestBodyPresent (Identity reqBod))
go k PathNil (q `RecCons` qs) b =
\qt -> go (\caps querys reqBody -> k caps (parameter q qt `RecCons` querys) reqBody) PathNil qs b
go k (PathConsMatch _ pnext) qs b =
go k pnext qs b
go k (PathConsCapture _ pnext) qs b =
\c -> go (\caps querys reqBod -> k (Identity c `RecCons` caps) querys reqBod) pnext qs b
parameter :: forall param. Query qf param -> ParamBase param -> Parameter param
parameter (QueryFlag _) b = ParameterFlag b
parameter (QueryOptional _ _) m = ParameterOptional m
parameter (QueryList _ _) l = ParameterList l
handler :: forall captures querys request x.
Rec Identity captures
-> Rec Parameter querys
-> RequestBody Identity request
-> Arguments captures querys request x
-> x
handler = go
where
go :: forall caps qrys.
Rec Identity caps
-> Rec Parameter qrys
-> RequestBody Identity request
-> Arguments caps qrys request x
-> x
go RecNil RecNil RequestBodyAbsent f = f
go RecNil RecNil (RequestBodyPresent (Identity b)) f = f b
go RecNil (q `RecCons` qs) b f = go RecNil qs b (f (demoteParameter q))
go (Identity c `RecCons` cs) qs b f = go cs qs b (f c)
data Constructed :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Type where
Constructed :: !(route captures querys request response) -> Constructed route
mapConstructed ::
(forall caps qrys req resp. sub caps qrys req resp -> route caps qrys req resp)
-> Constructed sub
-> Constructed route
mapConstructed f (Constructed sub) = Constructed (f sub)
data Pathed :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Type where
Pathed :: !(route captures querys request response) -> !(Rec Identity captures) -> Pathed route
data Prepared :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Type -> Type where
Prepared ::
!(route captures querys request response)
-> !(Rec Identity captures)
-> !(Rec Parameter querys)
-> !(RequestBody Identity request)
-> Prepared route response
data Concealed :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Type where
Concealed ::
!(route captures querys request response)
-> !(Rec Identity captures)
-> !(Rec Parameter querys)
-> !(RequestBody Identity request)
-> Concealed route
conceal :: Prepared route response -> Concealed route
conceal (Prepared route caps querys req) = Concealed route caps querys req
concealedToPrepared
:: forall route a
. Concealed route
-> (forall resp. Prepared route resp -> a)
-> a
concealedToPrepared (Concealed route caps qrys req) f = f (Prepared route caps qrys req)
data Content = Content
{ contentType :: !N.MediaType
, contentData :: !LBS.ByteString
} deriving (Show,Eq,Ord)
data Nat = S !Nat | Z
newtype Router route = Router (IxedRouter route 'Z)
data IxedRouter :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Nat -> Type where
IxedRouter ::
!(HashMap T.Text (IxedRouter route n))
-> !(Maybe (IxedRouter route ('S n)))
-> !(HashMap T.Text [IxedResponder route n])
-> IxedRouter route n
instance Monoid (IxedRouter route n) where
mempty = IxedRouter HM.empty Nothing HM.empty
mappend = (SG.<>)
instance SG.Semigroup (IxedRouter route n) where
(<>) = unionIxedRouter
data IxedResponder :: ([Type] -> [Param] -> Bodiedness -> Type -> Type) -> Nat -> Type where
IxedResponder ::
!(route captures query request response)
-> !(IxedRec CaptureDecoding n captures)
-> IxedResponder route n
data IxedRec :: (k -> Type) -> Nat -> [k] -> Type where
IxedRecNil :: IxedRec f 'Z '[]
IxedRecCons :: !(f r) -> !(IxedRec f n rs) -> IxedRec f ('S n) (r ': rs)
data Vec :: Nat -> Type -> Type where
VecNil :: Vec 'Z a
VecCons :: !a -> !(Vec n a) -> Vec ('S n) a
data IxedPath :: (Type -> Type) -> Nat -> [Type] -> Type where
IxedPathNil :: IxedPath f 'Z '[]
IxedPathCapture :: !(f a) -> !(IxedPath f n as) -> IxedPath f ('S n) (a ': as)
IxedPathMatch :: !T.Text -> !(IxedPath f n a) -> IxedPath f n a
data LenPath :: Nat -> Type where
LenPathNil :: LenPath 'Z
LenPathCapture :: !(LenPath n) -> LenPath ('S n)
LenPathMatch :: !T.Text -> !(LenPath n) -> LenPath n
data HideIx :: (Nat -> k -> Type) -> k -> Type where
HideIx :: !(f n a) -> HideIx f a
snocVec :: a -> Vec n a -> Vec ('S n) a
snocVec a VecNil = VecCons a VecNil
snocVec a (VecCons b vnext) =
VecCons b (snocVec a vnext)
pathToIxedPath :: Path f xs -> HideIx (IxedPath f) xs
pathToIxedPath PathNil = HideIx IxedPathNil
pathToIxedPath (PathConsCapture c pnext) =
case pathToIxedPath pnext of
HideIx ixed -> HideIx (IxedPathCapture c ixed)
pathToIxedPath (PathConsMatch s pnext) =
case pathToIxedPath pnext of
HideIx ixed -> HideIx (IxedPathMatch s ixed)
ixedPathToIxedRec :: IxedPath f n xs -> IxedRec f n xs
ixedPathToIxedRec IxedPathNil = IxedRecNil
ixedPathToIxedRec (IxedPathCapture c pnext) =
IxedRecCons c (ixedPathToIxedRec pnext)
ixedPathToIxedRec (IxedPathMatch _ pnext) =
ixedPathToIxedRec pnext
ixedPathToLenPath :: IxedPath f n xs -> LenPath n
ixedPathToLenPath IxedPathNil = LenPathNil
ixedPathToLenPath (IxedPathCapture _ pnext) =
LenPathCapture (ixedPathToLenPath pnext)
ixedPathToLenPath (IxedPathMatch s pnext) =
LenPathMatch s (ixedPathToLenPath pnext)
snocLenPathMatch :: T.Text -> LenPath n -> LenPath n
snocLenPathMatch s x = case x of
LenPathNil -> LenPathMatch s LenPathNil
LenPathMatch t pnext -> LenPathMatch t (snocLenPathMatch s pnext)
LenPathCapture pnext -> LenPathCapture (snocLenPathMatch s pnext)
snocLenPathCapture :: LenPath n -> LenPath ('S n)
snocLenPathCapture x = case x of
LenPathNil -> LenPathCapture LenPathNil
LenPathMatch t pnext -> LenPathMatch t (snocLenPathCapture pnext)
LenPathCapture pnext -> LenPathCapture (snocLenPathCapture pnext)
reverseLenPathMatch :: LenPath n -> LenPath n
reverseLenPathMatch = go
where
go :: forall n. LenPath n -> LenPath n
go LenPathNil = LenPathNil
go (LenPathMatch s pnext) = snocLenPathMatch s (go pnext)
go (LenPathCapture pnext) = snocLenPathCapture (go pnext)
singletonIxedRouter ::
route captures querys request response -> Method -> Path CaptureDecoding captures -> IxedRouter route 'Z
singletonIxedRouter route method capDecs = case pathToIxedPath capDecs of
HideIx ixedCapDecs ->
let ixedCapDecsRec = ixedPathToIxedRec ixedCapDecs
responder = IxedResponder route ixedCapDecsRec
lenPath = reverseLenPathMatch (ixedPathToLenPath ixedCapDecs)
in singletonIxedRouterHelper responder method lenPath
singletonIxedRouterHelper ::
IxedResponder route n -> Method -> LenPath n -> IxedRouter route 'Z
singletonIxedRouterHelper responder method path =
let r = IxedRouter HM.empty Nothing (HM.singleton (encodeMethod method) [responder])
in singletonIxedRouterGo r path
singletonIxedRouterGo ::
IxedRouter route n -> LenPath n -> IxedRouter route 'Z
singletonIxedRouterGo r lp = case lp of
LenPathNil -> r
LenPathCapture lpNext -> singletonIxedRouterGo (IxedRouter HM.empty (Just r) HM.empty) lpNext
LenPathMatch s lpNext -> singletonIxedRouterGo (IxedRouter (HM.singleton s r) Nothing HM.empty) lpNext
unionIxedRouter :: IxedRouter route n -> IxedRouter route n -> IxedRouter route n
unionIxedRouter = go
where
go :: forall route n. IxedRouter route n -> IxedRouter route n -> IxedRouter route n
go (IxedRouter matchesA captureA respsA) (IxedRouter matchesB captureB respsB) =
IxedRouter
(HM.unionWith go matchesA matchesB)
(unionMaybeWith go captureA captureB)
(HM.unionWith (++) respsA respsB)
unionMaybeWith :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
unionMaybeWith f x y = case x of
Nothing -> y
Just xval -> case y of
Nothing -> x
Just yval -> Just (f xval yval)
prettyRouter :: Router route -> String
prettyRouter (Router r) = L.unlines (prettyIxedRouter 0 (Nothing,r))
prettyIxedRouter ::
Int
-> (Maybe String, IxedRouter route n)
-> [String]
prettyIxedRouter indent (mnode,IxedRouter matches cap responders) =
let spaces = L.replicate indent ' '
nextIndent = if isJust mnode then indent + 2 else indent
children1 = map (first (Just . ('/' : ) . T.unpack)) (HM.toList matches)
children2 = maybe [] (\x -> [(Just "/:capture",x)]) cap
in concat
[ case mnode of
Nothing -> if length responders > 0
then ["/ " ++ showRespondersList responders]
else []
Just _ -> []
, maybe [] (\x -> [x]) $ flip fmap mnode $ \node -> spaces
++ node
++ (if length responders > 0 then " " ++ showRespondersList responders else "")
, prettyIxedRouter nextIndent =<< children1
, prettyIxedRouter nextIndent =<< children2
]
showRespondersList :: HashMap T.Text [a] -> String
showRespondersList = id
. (\x -> "[" ++ x ++ "]")
. L.intercalate ","
. map (\(method,xs) -> T.unpack method ++ (if L.length xs > 1 then "*" else ""))
. HM.toList