{-# LANGUAGE
BangPatterns
, DataKinds
, FlexibleContexts
, GADTs
, MagicHash
, OverloadedStrings
, ScopedTypeVariables
, StandaloneDeriving
, StrictData
, TypeApplications
, TypeFamilies
, TypeInType
, TypeOperators
, UndecidableInstances
#-}
module Network.Wai.Route
(
App
, Handler
, RoutingTrie
, route
, routePrefix
, Route (..)
, defRoute
, compileRoute
, compileRoutes
, Params (..)
, ParamName
, InvalidParam (..)
, Path, Vars, Var, Some
, (=~=)
, str, var, some, (./), end
, pathVarsLen
, pathPattern
, parsePath
, PathError (..)
, SomePath (..)
, Query
, qreq
, qdef
, qopt
, (.&.)
, withQuery
, parseQuery
, QueryError (..)
, VarsLen
, knownVarsLen
, getMethod
, byMethod
, withMethod
, getQueryParam
, getQueryParam'
, getHeader
, InvalidHeader (..)
, appInvalidParam
, appMissingParam
, appQueryError
, app400
, app404
, app405
, Trie
, Pattern
, Matcher (..)
, Capture (..)
, FromHttpApiData
) where
import GHC.Exts (Proxy#, proxy#)
import GHC.TypeLits
import Data.ByteString (ByteString)
import Data.Kind
import Data.Sequence (Seq (..), (<|))
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Data.Text.Lazy.Builder (fromString, fromText, toLazyText)
import Data.Trie.Pattern
import Network.HTTP.Types.Header
import Network.HTTP.Types.Method
import Network.HTTP.Types.Status
import Network.HTTP.Types (QueryItem)
import Network.Wai
import Prelude
import Web.HttpApiData (FromHttpApiData (..))
import qualified Data.ByteString.Char8 as C8
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import qualified Data.Text.Lazy.Encoding as LazyText
import qualified Data.Trie.Pattern as Trie
type App m
= Request
-> (Response -> m ResponseReceived)
-> m ResponseReceived
type Handler m
= Seq (Capture Text)
-> App m
type RoutingTrie m = Trie Text (Handler m)
route :: Monad m => RoutingTrie m -> App m -> App m
route rt app rq k = case Trie.match (pathInfo rq) rt of
Just (h, cs) -> h cs rq k
Nothing -> app rq k
routePrefix :: Monad m => RoutingTrie m -> App m -> App m
routePrefix rt app rq k = case Trie.matchPrefix (pathInfo rq) rt of
Just (h, cs, str') -> h cs (rq { pathInfo = str' }) k
Nothing -> app rq k
data Route m = forall vars. Route
{ routePath :: Path vars
, routeHandler :: Params vars -> App m
, routeInvalidParam :: InvalidParam -> App m
}
instance Eq (Route m) where
(Route p1 _ _) == (Route p2 _ _) = p1 =~= p2
instance Show (Route m) where
show (Route p _ _) = show p
defRoute :: Monad m => Path vars -> (Params vars -> App m) -> Route m
defRoute p h = Route p h appInvalidParam
compileRoutes :: Monad m => [Route m] -> RoutingTrie m
compileRoutes = Trie.fromAssocList . map compileRoute
compileRoute :: Monad m => Route m -> (Pattern Text, Handler m)
compileRoute (Route p h f) = (pathPattern p, handler)
where
handler cs = case parsePath p cs of
Right args -> h args
Left (PathInvalidParam x) -> f x
Left PathMissingParams ->
error "wai-route: incomplete parse: missing captures"
type Vars = [(Symbol,Type)]
type Var s a = '(s,a)
type Some a = Var "" a
data Path :: Vars -> Type where
Val :: Text
-> Path vars
-> Path vars
Var :: (KnownSymbol s, Eq a, FromHttpApiData a)
=> Proxy# s
-> Proxy# a
-> Path vars
-> Path (Var s a ': vars)
End :: Path '[]
instance Eq (Path vars) where
p1 == p2 = p1 =~= p2
instance Show (Path vars) where
show End = ""
show (Val s p) = showString "/" . showString (Text.unpack s) . shows p $ ""
show (Var s _ p) = showString "/" . (
let s' = symbolVal' s
in if null s'
then showString "*"
else showString ":" . showString s'
) . shows p $ ""
(=~=) :: Path vars -> Path vars' -> Bool
(=~=) End End = True
(=~=) (Val s p) (Val s' p') = s == s' && p =~= p'
(=~=) (Var _ _ p) (Var _ _ p') = p =~= p'
(=~=) _ _ = False
var :: (KnownSymbol s, Eq a, FromHttpApiData a)
=> Path vars
-> Path (Var s a ': vars)
var = Var proxy# proxy#
some :: (Eq a, FromHttpApiData a)
=> Path vars
-> Path (Some a ': vars)
some = var @""
str :: Text -> Path vars -> Path vars
str = Val
(./) :: (Path vars -> Path vars') -> Path vars -> Path vars'
(./) f p = f p
infixr 5 ./
end :: Path '[]
end = End
pathPattern :: Path vars -> Pattern Text
pathPattern = go Seq.empty
where
go :: Pattern Text -> Path vars -> Pattern Text
go pat (Val s p) = EqStr s <| go pat p
go pat (Var _ _ p) = AnyStr <| go pat p
go pat End = pat
pathVarsLen :: Path vars -> Int
pathVarsLen = go 0
where
go :: Int -> Path vars -> Int
go !n End = n
go !n (Var _ _ p) = go (n + 1) p
go !n (Val _ p) = go n p
data PathError
= PathMissingParams
| PathInvalidParam InvalidParam
deriving (Eq, Show, Read)
parsePath :: Path vars -> Seq (Capture Text) -> Either PathError (Params vars)
parsePath End _ = Right Nil
parsePath (Val _ p) cs = parsePath p cs
parsePath _ Empty = Left PathMissingParams
parsePath (Var s (_ :: Proxy# a) p) (Capture c :<| cs) =
case parseUrlPiece @a c of
Right a -> (a :::) <$> parsePath p cs
Left e -> Left $! PathInvalidParam (InvalidParam (symbolVal' s) c e)
data SomePath = forall vars. SomePath (Path vars)
deriving instance Show SomePath
instance Eq SomePath where
(SomePath p1) == (SomePath p2) = p1 =~= p2
data Query :: Vars -> Type where
QDef :: (Eq a, KnownSymbol s, FromHttpApiData a)
=> Proxy# s
-> Proxy# a
-> a
-> Query '[Var s a]
QOpt :: (Eq a, KnownSymbol s, FromHttpApiData a)
=> Proxy# s
-> Proxy# a
-> Query '[Var s (Maybe a)]
QReq :: (Eq a, KnownSymbol s, FromHttpApiData a)
=> Proxy# s
-> Proxy# a
-> Query '[Var s a]
QAnd :: (Eq a, KnownSymbol s, FromHttpApiData a)
=> Query '[Var s a]
-> Query (Var s' a' ': vars)
-> Query (Var s a ': Var s' a' ': vars)
qreq :: (KnownSymbol s, FromHttpApiData a, Eq a) => Query '[Var s a]
qreq = QReq proxy# proxy#
qdef :: (KnownSymbol s, FromHttpApiData a, Eq a) => a -> Query '[Var s a]
qdef = QDef proxy# proxy#
qopt :: (KnownSymbol s, FromHttpApiData a, Eq a) => Query '[Var s (Maybe a)]
qopt = QOpt proxy# proxy#
(.&.) :: (Eq a, KnownSymbol s, FromHttpApiData a)
=> Query '[Var s a]
-> Query (Var s' a' ': vars)
-> Query (Var s a ': Var s' a' ': vars)
(.&.) = QAnd
infixr 5 .&.
instance Show (Query vars) where
showsPrec _ (QReq s _ ) = showString (symbolVal' s) . showString "=?[req]"
showsPrec _ (QDef s _ _) = showString (symbolVal' s) . showString "=?[def]"
showsPrec _ (QOpt s _ ) = showString (symbolVal' s) . showString "=?[opt]"
showsPrec _ (QAnd q qs) = shows q . showString "&" . shows qs
data QueryError
= QueryMissingParam ParamName
| QueryInvalidParam InvalidParam
deriving (Eq, Show, Read)
parseQuery :: Query vars -> [QueryItem] -> Either QueryError (Params vars)
parseQuery qry items = case qry of
q@QReq{} -> (::: Nil) <$> parseQ q
q@QDef{} -> (::: Nil) <$> parseQ q
q@QOpt{} -> (::: Nil) <$> parseQ q
QAnd q qs -> (:::) <$> parseQ q <*> parseQuery qs items
where
parseQ :: Query '[Var s a] -> Either QueryError a
parseQ q = case q of
QReq s (_ :: Proxy# a) ->
parse s (parseQParam @a) (Left . QueryMissingParam)
QDef s (_ :: Proxy# a) def ->
parse s (parseQParam @a) (const (Right def))
QOpt s (_ :: Proxy# a) ->
parse s (\n -> fmap Just . parseQParam @a n) (const (Right Nothing))
parse :: KnownSymbol s
=> Proxy# s
-> (ParamName -> ByteString -> Either InvalidParam a)
-> (ParamName -> Either QueryError a)
-> Either QueryError a
parse s f g = let name = symbolVal' s in
case Prelude.lookup (C8.pack name) items of
Just (Just val) -> case f name val of
Left e -> Left $! QueryInvalidParam e
Right a -> Right a
_ -> g name
withQuery
:: Query vars
-> (QueryError -> App m)
-> (Params vars -> App m)
-> App m
withQuery q onE onP rq = case parseQuery q (queryString rq) of
Left e -> onE e rq
Right p -> onP p rq
parseQParam :: forall a. FromHttpApiData a
=> ParamName
-> ByteString
-> Either InvalidParam a
parseQParam name val =
let val' = decodeUtf8With lenientDecode val
in case parseQueryParam @a val' of
Right a -> Right a
Left e -> Left $! InvalidParam name val' e
type ParamName = String
data Params :: Vars -> Type where
Nil :: Params '[]
(:::) :: Eq a => a -> Params vars -> Params (Var s a ': vars)
infixr 5 :::
deriving instance Eq (Params vars)
data InvalidParam = InvalidParam
{ invalidParamName :: ParamName
, invalidParamValue :: Text
, invalidParamMsg :: Text
} deriving (Eq, Show, Read)
type family VarsLen vars :: Nat where
VarsLen '[] = 0
VarsLen (_ ': vars) = 1 + VarsLen vars
knownVarsLen :: forall proxy vars. KnownNat (VarsLen vars) => proxy vars -> Integer
knownVarsLen _ = natVal' (proxy# :: Proxy# (VarsLen vars))
getMethod :: Request -> Either ByteString StdMethod
getMethod = parseMethod . requestMethod
byMethod :: (StdMethod -> App m) -> App m
byMethod f rq k = case getMethod rq of
Left _ -> app405 rq k
Right m -> f m rq k
withMethod :: Monad m => StdMethod -> App m -> App m
withMethod m app = byMethod $ \m' ->
if m == m' then app
else app405
getQueryParam :: FromHttpApiData a => Request -> ByteString -> Maybe (Either InvalidParam a)
getQueryParam rq key = case Prelude.lookup key (queryString rq) of
Nothing -> Nothing
Just Nothing -> Nothing
Just (Just bs) -> Just $! parseQParam (C8.unpack key) bs
getQueryParam' :: FromHttpApiData a => Request -> Text -> Maybe (Either InvalidParam a)
getQueryParam' rq = getQueryParam rq . encodeUtf8
data InvalidHeader = InvalidHeader
{ invalidHeaderName :: HeaderName
, invalidHeaderValue :: ByteString
, invalidHeaderMsg :: Text
} deriving (Eq, Show, Read)
getHeader :: FromHttpApiData a => Request -> HeaderName -> Maybe (Either InvalidHeader a)
getHeader rq h = case Prelude.lookup h (requestHeaders rq) of
Nothing -> Nothing
Just bs -> case parseHeader bs of
Right a -> Just (Right a)
Left e -> Just (Left (InvalidHeader h bs e))
appInvalidParam :: InvalidParam -> App m
appInvalidParam (InvalidParam n v e) _rq k =
k $ responseLBS code hdrs body
where
code = status400
hdrs = [(hContentType, "text/plain; charset=utf-8")]
body = LazyText.encodeUtf8 . toLazyText $
fromString "Invalid parameter "
<> fromString "["
<> (if not (null n)
then fromString n <> fromString "="
else mempty)
<> fromText v
<> fromString "], "
<> fromText e
appMissingParam :: ParamName -> App m
appMissingParam name _rq k =
k $ responseLBS code hdrs body
where
code = status400
hdrs = [(hContentType, "text/plain; charset=utf-8")]
body = LazyText.encodeUtf8 . toLazyText $
fromString "Missing parameter "
<> fromString "["
<> fromString name
<> fromString "]"
appQueryError :: QueryError -> App m
appQueryError (QueryInvalidParam e) = appInvalidParam e
appQueryError (QueryMissingParam n) = appMissingParam n
app404 :: App m
app404 _rq k = k $ responseLBS status404 [] mempty
app405 :: App m
app405 _rq k = k $ responseLBS status405 [] mempty
app400 :: App m
app400 _rq k = k $ responseLBS status400 [] mempty