ihp-router-1.0.0: Trie-based routing with a Yesod-style DSL for WAI
Safe HaskellNone
LanguageGHC2021

IHP.Router.DSL.TH

Description

The splice parses the DSL body, reifys the target controller type, and emits IHP-free declarations:

  • instance HasPath Ctrl — pathTo per constructor.
  • <ctrlLower>Trie :: (Ctrl -> Application) -> RouteTrie — top-level binding that builds the trie when applied to a user-supplied dispatch function. Plug it into routeTrieMiddleware for a working WAI dispatcher.

The IHP-flavoured [routes|…|] quoter (which additionally emits an instance CanRoute Ctrl and a webRoutes :: [ControllerRoute app] binding) lives in IHP's IHP.Router.IHP module and calls into parseAndReify / genericEmit here.

Names referenced from inside the splice (HasPath, pathTo, renderCapture, parseCapture) are imported via mkName so they resolve at the splice call site — typically via import IHP.Router.WAI for plain WAI users or import IHP.RouterPrelude for IHP apps.

Names from sibling ihp-router modules (LiteralSeg, CaptureSeg, buildRouteTrie, mkHandler, mkHandlerQ, …) are referenced hygienically via QuoteName syntax.

Synopsis

Splice entry points

routes :: QuasiQuoter Source #

The [routes|...|] quasi-quoter. Captures use RFC 6570 URI-template syntax — {name} for a single segment, {+name} for a splat — and ?name1&name2 for query-string parameters.

[routes|PostsController
GET    /posts                 PostsAction
GET    /posts/{postId}        ShowPostAction
PATCH  /posts/{postId}        UpdatePostAction
GET    /search?q&page         SearchAction
|]

Query parameters are declared explicitly: each ?name names a record field on the action constructor that the URL carries in the query string. The field's Haskell type determines the shape: plain a is required (missing/unparseable ⇒ 404), Maybe a is optional, [a] collects repeated values (?tags=a&tags=b). Every record field of the action constructor must be covered by either a path capture or a query-param entry — unbound fields fail at splice time with a pointer to the exact fields left over.

Renaming. To map a capture or query-param name to a differently named record field, use the { field = #name } syntax after the action. Works for path captures and query params alike:

GET /ShowPost?id     ShowPostAction { postId = #id }
GET /users/{uid}     ShowUserAction { userId = #uid }

This is the ihp-router (plain-WAI) flavour of the quoter — emits HasPath instances and a per-controller <ctrlLower>Trie binding only. IHP apps use the wrapping quoter from IHP.Router.IHP (re-exported through IHP.Router.DSL), which additionally emits the IHP-flavoured CanRoute instance and lowercase-header webRoutes binding.

Three header forms — all top-level declarations:

  1. Single-controller. Uppercase-initial header = controller type.
  2. Binding-named (multi-controller). Lowercase-initial header is the name of a binding the splice emits. In ihp-router it carries the per-controller trie values; in IHP it additionally carries the [ControllerRoute app] list for FrontController.controllers.
  3. No header. Multi-controller, instances + trie values only.

routesDec :: String -> Q [Dec] Source #

The default splice for the [routes|…|] quoter in ihp-router. Emits the IHP-free declarations a plain WAI app needs — an instance HasPath Ctrl per controller and a parameterised <ctrlLower>Trie :: (Ctrl -> Application) -> RouteTrie binding per controller.

IHP apps don't call this directly; they use the IHP-flavoured routes quoter from IHP.Router.IHP, which composes genericEmit with its own IHP-specific CanRoute / webRoutes emission.

genericRoutesDec :: String -> Q [Dec] Source #

The IHP-free entry point: parses the DSL, reifies the controller(s), and emits genericEmit's output. Suitable for plain WAI applications.

Same as routesDec; provided under the more descriptive name so the IHP-side shim can call it explicitly when composing the IHP-flavoured quoter.

Shared types and helpers

Re-exported for the IHP-side IHP.Router.IHP shim, which composes its own quoter on top of parseAndReify / genericEmit. Plain WAI users typically don't need to touch these directly.

parseAndReify :: String -> Q ParsedBlock Source #

Parse the DSL body, reify each referenced controller type, and validate every route against its action constructor. Returns a ParsedBlock both emitters can consume.

WebSocket routes (those whose routeKind is WebSocketRoute) are split out into pbWsRoutes rather than going through groupRoutesByParent / validateRoute — they reference a WSApp type by name (not an action constructor of a controller), so the HTTP-route validation pipeline doesn't apply. For v1 we also restrict WS routes to the lowercase-header form, since the IHP shim only knows how to register them through the webRoutes binding the lowercase form emits.

genericEmit :: ParsedBlock -> Q [Dec] Source #

Emit the generic HasPath instance per controller plus the top-level <ctrlLower>Trie binding per controller. No IHP-specific references — this is the half that moves to ihp-router in the extraction PR.

data ParsedBlock Source #

A parsed [routes|…|] body, ready for emission. Produced by parseAndReify and consumed by genericEmit and ihpEmit.

Constructors

ParsedBlock 

Fields

  • pbHeader :: !HeaderForm

    Which header shape the user wrote.

  • pbGroups :: ![(ControllerInfo, [ValidatedRoute])]

    Routes grouped by parent controller type, in the order those types first appear in the DSL block. For single-controller uppercase blocks this list has length 1.

  • pbWsRoutes :: ![(Name, ByteString)]

    WebSocket routes (kind WebSocketRoute), resolved to (typeName, path) pairs in declaration order. The IHP-side shim (ihpEmit) turns each entry into a webSocketRoute @T "/path" call inside the lowercase-header binding. The generic genericEmit ignores this field — plain ihp-router users have no WSApp typeclass in scope, and v1 only registers WS routes via the IHP-flavoured binding.

data HeaderForm Source #

Which of the three header forms the parser saw.

Constructors

HeaderUppercase !Text

Just name with an uppercase initial — single-controller form. The emitters skip groupRoutesByParent and treat the single entry in pbGroups as the user's one target type.

HeaderLowercase !Text

Just name with a lowercase initial — multi-controller binding-named form. The IHP emitter additionally produces a top-level name :: [ControllerRoute app] binding.

HeaderAbsent

Header-less multi-controller form. Instances only, no binding.

data ConstructorInfo Source #

Constructors

ConstructorInfo 

Fields

data ValidatedRoute Source #

Constructors

ValidatedRoute 

Fields

  • vrMethods :: ![Method]
     
  • vrPath :: ![ValidatedSeg]
     
  • vrCon :: !ConstructorInfo
     
  • vrLine :: !Int
     
  • vrBindingsByCapture :: !(Map Text Text)

    Query-param bindings declared explicitly in the route's ?name&… suffix. Each entry is (urlName, fieldName, kind, innerType):

    • urlName is what the user wrote in the DSL's ?urlName suffix — it becomes the query-string key in both the rendered URL (pathTo) and the lookup at request-handling time.
    • fieldName is the record field the urlName binds to. They usually match; they differ only when the user wrote a { field = #urlName } rebinding in the ActionRef.
    • kind is QFRequired for plain fields (a), QFOptional for Maybe a fields (absent query params decode to Nothing), or QFList for [a] fields (0+ repeated values).
    • innerType is the unwrapped field type (strips one level of Maybe or []) used for parseCapture / renderCapture.

    The order mirrors the order in the DSL so pathTo's rendered query string reads left-to-right as the user wrote it.

  • vrQueryFields :: ![(Text, Text, QueryFieldKind, Type)]
     

data QueryFieldKind Source #

Constructors

QFRequired

plain a; Nothing from decoder → 404

QFOptional

Maybe a; absent → Nothing, present-parseable → Just v

QFList

[a]; 0+ repeated query values

Instances

Instances details
Show QueryFieldKind Source # 
Instance details

Defined in IHP.Router.DSL.TH

Eq QueryFieldKind Source # 
Instance details

Defined in IHP.Router.DSL.TH

trieValueName :: Name -> Name Source #

Top-level binding name for the per-controller trie value: e.g. PostsController becomes postsControllerTrie. Both the emitter (emitTrieValue) and any IHP-side wrapper that wants to call the binding with a specific dispatch function (e.g. IHP.Router.IHP.emitCanRoute) use this helper to agree on the name.