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

IHP.Router.WAI

Description

Re-exports the surface a plain WAI app needs to declare routes via the [routes|…|] quasi-quoter and dispatch incoming requests through the trie:

Typical use:

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where

import IHP.Router.WAI
import Network.Wai (Application, responseLBS)
import Network.Wai.Handler.Warp (run)
import Network.HTTP.Types (status200, status404)

data Routes = ListPosts | ShowPost { postId :: Int }
    deriving (Eq, Show)

[routes|Routes
GET /posts             ListPosts
GET posts{postId}    ShowPost
|]

dispatch :: Routes -> Application
dispatch ListPosts _ respond = respond (responseLBS status200 [] "list")
dispatch (ShowPost { postId }) _ respond =
    respond (responseLBS status200 [] (cs (show postId)))

main :: IO ()
main = run 3000 (routeTrieMiddleware (routesTrie dispatch) notFound)
  where
    notFound _ respond = respond (responseLBS status404 [] "Not Found")

The routesTrie binding is emitted by the splice (one per controller type) — its name is the controller's <lowercaseFirst>Trie.

Synopsis

The DSL

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.

Trie + middleware

data RouteTrie Source #

Path-indexed trie node.

Lookup priority at each node is: literal segment > typed capture > splat. Splat captures consume the remaining path and land on the bundled HandlersByMethod.

routeTrieMiddleware :: RouteTrie -> Middleware Source #

Wrap a RouteTrie as a WAI Middleware.

Semantics:

  • If the trie matches, the matched handler (a Captures -> Application) runs with the captured path segments and the incoming request.
  • If the path matches but the HTTP method does not, a 405 Method Not Allowed response is returned with an Allow header listing the supported methods.
  • If neither the path nor a splat matches, the request is handed to the fallback application — which is the standard WAI behaviour when a middleware declines a request.

The middleware is pure with respect to the trie: construction happens at application startup, lookup on every request.

URL captures

class Typeable a => UrlCapture a where Source #

A type that can appear as a URL path segment.

Instances provide a parseCapture for decoding a raw segment bytestring (post-URL-decoding) into a typed value, and renderCapture for the reverse direction when generating URLs via pathTo.

Example:

>>> parseCapture @Int "42"
Just 42
>>> renderCapture (42 :: Int)
"42"

Methods

parseCapture :: ByteString -> Maybe a Source #

Parse a single URL segment (already URL-decoded) into a typed value. Returns Nothing if the segment cannot be interpreted as this type.

renderCapture :: a -> Text Source #

Render a typed value as URL-ready text. The caller is responsible for URL-encoding if needed.

newtype Segment Source #

A URL path segment guaranteed to be non-empty.

Useful when a capture must not match an empty string. Plain Text captures happily match "" (the segment between two consecutive slashes); Segment rejects that case, which is often what you want for splat captures or required path pieces.

Constructors

Segment 

Fields

Instances

Instances details
Show Segment Source # 
Instance details

Defined in IHP.Router.Capture

Eq Segment Source # 
Instance details

Defined in IHP.Router.Capture

Methods

(==) :: Segment -> Segment -> Bool #

(/=) :: Segment -> Segment -> Bool #

Ord Segment Source # 
Instance details

Defined in IHP.Router.Capture

UrlCapture Segment Source # 
Instance details

Defined in IHP.Router.Capture

URL generation

class HasPath controller where Source #

Type class for types that can be converted to URL paths.

This is used by IHP's routing system to generate URLs for controller actions.

Example:

>>> pathTo UsersAction
"/Users"
>>> pathTo ShowUserAction { userId = "a32913dd-ef80-4f3e-9a91-7879e17b2ece" }
"/ShowUser?userId=a32913dd-ef80-4f3e-9a91-7879e17b2ece"

Methods

pathTo :: controller -> Text Source #