Display capture hints in router layout

This commit introduces a `CaptureHint` type, which is passed as an extra
argument to the `CaptureRouter` and `CaptureAllRouter` constructors for
the `Router'` type.
`CaptureHint` values are then used in `routerLayout`, to display the
name and "type" of captured values (single or list), instead of just
"<capture>" previously.

N.B.:
Because the `choice` smart constructor for routers can aggregate
`Capture` combinators with different capture hints, the `Capture*Router`
constructors actually take a *list* of `CaptureHint`, instead of a
single one.
This commit is contained in:
Nicolas BACQUEY 2022-03-08 14:55:03 +01:00
parent de923fc887
commit 77b92d0d7d
4 changed files with 51 additions and 26 deletions

View file

@ -235,7 +235,7 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
-- > │ └─ e/ -- > │ └─ e/
-- > │ └─• -- > │ └─•
-- > ├─ b/ -- > ├─ b/
-- > │ └─ <capture>/ -- > │ └─ <x::Int>/
-- > │ ├─• -- > │ ├─•
-- > │ ┆ -- > │ ┆
-- > │ └─• -- > │ └─•
@ -252,7 +252,8 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
-- --
-- [@─•@] Leaves reflect endpoints. -- [@─•@] Leaves reflect endpoints.
-- --
-- [@\<capture\>/@] This is a delayed capture of a path component. -- [@\<x::Int\>/@] This is a delayed capture of a single
-- path component named @x@, of expected type @Int@.
-- --
-- [@\<raw\>@] This is a part of the API we do not know anything about. -- [@\<raw\>@] This is a part of the API we do not know anything about.
-- --

View file

@ -173,7 +173,7 @@ instance (HasServer a context, HasServer b context) => HasServer (a :<|> b) cont
-- > server = getBook -- > server = getBook
-- > where getBook :: Text -> Handler Book -- > where getBook :: Text -> Handler Book
-- > getBook isbn = ... -- > getBook isbn = ...
instance (KnownSymbol capture, FromHttpApiData a instance (KnownSymbol capture, FromHttpApiData a, Typeable a
, HasServer api context, SBoolI (FoldLenient mods) , HasServer api context, SBoolI (FoldLenient mods)
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters , HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
) )
@ -185,7 +185,7 @@ instance (KnownSymbol capture, FromHttpApiData a
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
route Proxy context d = route Proxy context d =
CaptureRouter $ CaptureRouter [hint] $
route (Proxy :: Proxy api) route (Proxy :: Proxy api)
context context
(addCapture d $ \ txt -> withRequest $ \ request -> (addCapture d $ \ txt -> withRequest $ \ request ->
@ -197,6 +197,7 @@ instance (KnownSymbol capture, FromHttpApiData a
where where
rep = typeRep (Proxy :: Proxy Capture') rep = typeRep (Proxy :: Proxy Capture')
formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep (Proxy :: Proxy a))
-- | If you use 'CaptureAll' in one of the endpoints for your API, -- | If you use 'CaptureAll' in one of the endpoints for your API,
-- this automatically requires your server-side handler to be a -- this automatically requires your server-side handler to be a
@ -215,7 +216,7 @@ instance (KnownSymbol capture, FromHttpApiData a
-- > server = getSourceFile -- > server = getSourceFile
-- > where getSourceFile :: [Text] -> Handler Book -- > where getSourceFile :: [Text] -> Handler Book
-- > getSourceFile pathSegments = ... -- > getSourceFile pathSegments = ...
instance (KnownSymbol capture, FromHttpApiData a instance (KnownSymbol capture, FromHttpApiData a, Typeable a
, HasServer api context , HasServer api context
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters , HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
) )
@ -227,7 +228,7 @@ instance (KnownSymbol capture, FromHttpApiData a
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
route Proxy context d = route Proxy context d =
CaptureAllRouter $ CaptureAllRouter [hint] $
route (Proxy :: Proxy api) route (Proxy :: Proxy api)
context context
(addCapture d $ \ txts -> withRequest $ \ request -> (addCapture d $ \ txts -> withRequest $ \ request ->
@ -238,6 +239,7 @@ instance (KnownSymbol capture, FromHttpApiData a
where where
rep = typeRep (Proxy :: Proxy CaptureAll) rep = typeRep (Proxy :: Proxy CaptureAll)
formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep (Proxy :: Proxy [a]))
allowedMethodHead :: Method -> Request -> Bool allowedMethodHead :: Method -> Request -> Bool
allowedMethodHead method request = method == methodGet && requestMethod request == methodHead allowedMethodHead method request = method == methodGet && requestMethod request == methodHead

View file

@ -9,12 +9,16 @@ import Prelude.Compat
import Data.Function import Data.Function
(on) (on)
import Data.List
(nub)
import Data.Map import Data.Map
(Map) (Map)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Text import Data.Text
(Text) (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Typeable
(TypeRep)
import Network.Wai import Network.Wai
(Response, pathInfo) (Response, pathInfo)
import Servant.Server.Internal.ErrorFormatter import Servant.Server.Internal.ErrorFormatter
@ -24,6 +28,18 @@ import Servant.Server.Internal.ServerError
type Router env = Router' env RoutingApplication type Router env = Router' env RoutingApplication
data CaptureHint = CaptureHint
{ captureName :: Text
, captureType :: TypeRep
}
deriving (Show, Eq)
toCaptureTag :: CaptureHint -> Text
toCaptureTag hint = captureName hint <> "::" <> (T.pack . show) (captureType hint)
toCaptureTags :: [CaptureHint] -> Text
toCaptureTags hints = "<" <> T.intercalate "|" (map toCaptureTag hints) <> ">"
-- | Internal representation of a router. -- | Internal representation of a router.
-- --
-- The first argument describes an environment type that is -- The first argument describes an environment type that is
@ -36,10 +52,10 @@ data Router' env a =
-- ^ the map contains routers for subpaths (first path component used -- ^ the map contains routers for subpaths (first path component used
-- for lookup and removed afterwards), the list contains handlers -- for lookup and removed afterwards), the list contains handlers
-- for the empty path, to be tried in order -- for the empty path, to be tried in order
| CaptureRouter (Router' (Text, env) a) | CaptureRouter [CaptureHint] (Router' (Text, env) a)
-- ^ first path component is passed to the child router in its -- ^ first path component is passed to the child router in its
-- environment and removed afterwards -- environment and removed afterwards
| CaptureAllRouter (Router' ([Text], env) a) | CaptureAllRouter [CaptureHint] (Router' ([Text], env) a)
-- ^ all path components are passed to the child router in its -- ^ all path components are passed to the child router in its
-- environment and are removed afterwards -- environment and are removed afterwards
| RawRouter (env -> a) | RawRouter (env -> a)
@ -69,8 +85,8 @@ leafRouter l = StaticRouter M.empty [l]
choice :: Router' env a -> Router' env a -> Router' env a choice :: Router' env a -> Router' env a -> Router' env a
choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) = choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2) StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
choice (CaptureRouter router1) (CaptureRouter router2) = choice (CaptureRouter hints1 router1) (CaptureRouter hints2 router2) =
CaptureRouter (choice router1 router2) CaptureRouter (nub $ hints1 ++ hints2) (choice router1 router2)
choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3 choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
choice router1 router2 = Choice router1 router2 choice router1 router2 = Choice router1 router2
@ -84,7 +100,7 @@ choice router1 router2 = Choice router1 router2
-- --
data RouterStructure = data RouterStructure =
StaticRouterStructure (Map Text RouterStructure) Int StaticRouterStructure (Map Text RouterStructure) Int
| CaptureRouterStructure RouterStructure | CaptureRouterStructure [CaptureHint] RouterStructure
| RawRouterStructure | RawRouterStructure
| ChoiceStructure RouterStructure RouterStructure | ChoiceStructure RouterStructure RouterStructure
deriving (Eq, Show) deriving (Eq, Show)
@ -98,11 +114,11 @@ data RouterStructure =
routerStructure :: Router' env a -> RouterStructure routerStructure :: Router' env a -> RouterStructure
routerStructure (StaticRouter m ls) = routerStructure (StaticRouter m ls) =
StaticRouterStructure (fmap routerStructure m) (length ls) StaticRouterStructure (fmap routerStructure m) (length ls)
routerStructure (CaptureRouter router) = routerStructure (CaptureRouter hints router) =
CaptureRouterStructure $ CaptureRouterStructure hints $
routerStructure router routerStructure router
routerStructure (CaptureAllRouter router) = routerStructure (CaptureAllRouter hints router) =
CaptureRouterStructure $ CaptureRouterStructure hints $
routerStructure router routerStructure router
routerStructure (RawRouter _) = routerStructure (RawRouter _) =
RawRouterStructure RawRouterStructure
@ -114,8 +130,8 @@ routerStructure (Choice r1 r2) =
-- | Compare the structure of two routers. -- | Compare the structure of two routers.
-- --
sameStructure :: Router' env a -> Router' env b -> Bool sameStructure :: Router' env a -> Router' env b -> Bool
sameStructure r1 r2 = sameStructure router1 router2 =
routerStructure r1 == routerStructure r2 routerStructure router1 == routerStructure router2
-- | Provide a textual representation of the -- | Provide a textual representation of the
-- structure of a router. -- structure of a router.
@ -126,7 +142,8 @@ routerLayout router =
where where
mkRouterLayout :: Bool -> RouterStructure -> [Text] mkRouterLayout :: Bool -> RouterStructure -> [Text]
mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
mkRouterLayout c (CaptureRouterStructure r) = mkSubTree c "<capture>" (mkRouterLayout False r) mkRouterLayout c (CaptureRouterStructure hints r) =
mkSubTree c (toCaptureTags hints) (mkRouterLayout False r)
mkRouterLayout c RawRouterStructure = mkRouterLayout c RawRouterStructure =
if c then ["├─ <raw>"] else ["└─ <raw>"] if c then ["├─ <raw>"] else ["└─ <raw>"]
mkRouterLayout c (ChoiceStructure r1 r2) = mkRouterLayout c (ChoiceStructure r1 r2) =
@ -169,7 +186,7 @@ runRouterEnv fmt router env request respond =
-> let request' = request { pathInfo = rest } -> let request' = request { pathInfo = rest }
in runRouterEnv fmt router' env request' respond in runRouterEnv fmt router' env request' respond
_ -> respond $ Fail $ fmt request _ -> respond $ Fail $ fmt request
CaptureRouter router' -> CaptureRouter _ router' ->
case pathInfo request of case pathInfo request of
[] -> respond $ Fail $ fmt request [] -> respond $ Fail $ fmt request
-- This case is to handle trailing slashes. -- This case is to handle trailing slashes.
@ -177,7 +194,7 @@ runRouterEnv fmt router env request respond =
first : rest first : rest
-> let request' = request { pathInfo = rest } -> let request' = request { pathInfo = rest }
in runRouterEnv fmt router' (first, env) request' respond in runRouterEnv fmt router' (first, env) request' respond
CaptureAllRouter router' -> CaptureAllRouter _ router' ->
let segments = pathInfo request let segments = pathInfo request
request' = request { pathInfo = [] } request' = request { pathInfo = [] }
in runRouterEnv fmt router' (segments, env) request' respond in runRouterEnv fmt router' (segments, env) request' respond

View file

@ -9,7 +9,9 @@ import Control.Monad
import Data.Proxy import Data.Proxy
(Proxy (..)) (Proxy (..))
import Data.Text import Data.Text
(unpack) (Text, unpack)
import Data.Typeable
(typeRep)
import Network.HTTP.Types import Network.HTTP.Types
(Status (..)) (Status (..))
import Network.Wai import Network.Wai
@ -51,7 +53,7 @@ routerSpec = do
toApp = toApplication . runRouter (const err404) toApp = toApplication . runRouter (const err404)
cap :: Router () cap :: Router ()
cap = CaptureRouter $ cap = CaptureRouter [hint] $
let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400) let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400)
in leafRouter in leafRouter
$ \env req res -> $ \env req res ->
@ -59,6 +61,9 @@ routerSpec = do
. const . const
$ Route success $ Route success
hint :: CaptureHint
hint = CaptureHint "anything" $ typeRep (Proxy :: Proxy ())
router :: Router () router :: Router ()
router = leafRouter (\_ _ res -> res $ Route success) router = leafRouter (\_ _ res -> res $ Route success)
`Choice` cap `Choice` cap
@ -144,12 +149,12 @@ staticRef = Proxy
-- structure: -- structure:
type Dynamic = type Dynamic =
"a" :> Capture "foo" Int :> "b" :> End "a" :> Capture "foo" Int :> "b" :> End
:<|> "a" :> Capture "bar" Bool :> "c" :> End :<|> "a" :> Capture "foo" Int :> "c" :> End
:<|> "a" :> Capture "baz" Char :> "d" :> End :<|> "a" :> Capture "foo" Int :> "d" :> End
type DynamicRef = type DynamicRef =
"a" :> Capture "anything" () :> "a" :> Capture "foo" Int :>
("b" :> End :<|> "c" :> End :<|> "d" :> End) ("b" :> End :<|> "c" :> End :<|> "d" :> End)
dynamic :: Proxy Dynamic dynamic :: Proxy Dynamic