This is a routing library for front-end web applications. It provides a Servant-style interface to the router matching and handling functionality, but not the hash-based/push-state driver functionality, in purescript-routing
.
Like purescript-routing-duplex
, this library abstracts away the parsing and printing of URLs to ensure at compile-time they are always in sync. But in this library the user specifies the routes through a typelevel DSL, rather than in an intermediate data type representing the routes and a codec interpreting that type.
This project is inspired by the router used in Miso, a Haskell web framework. The purescript-kushikatsu
router has a similar focus with a different DSL and usage pattern. This library uses type combinators more akin to those in purescript-servant
and purescript-trout
.
Clone, then:
yarn install # also installs bower dependencies
yarn pulp build # builds project
yarn pulp test # runs the tests
yarn run:example:halogen # run the example
yarn run:example:hertz # run the example
Alternatively:
yarn install
yarn spago install # install spago dependencies
yarn spago build # builds project
yarn spago test # runs the tests
Usage starts by defining a typelevel API with named endpoints. Route combinators are single letter abbreviations that follow this mnemonic:
P
is the path segment combinator representing a string.C
is the capture combinator for capturing a value.M
is the capture many combinator for capturing an array of values.Q
is the query string combinator translating a record to a query.V
is the view combinator used to name an endpoint.
For example, from tests/Test/Main.purs
:
type ReadmeApi =
P "profile" :> C "username" String :> V "profile"
:<|> P "article"
:> ( C "id" Int :> V "article_id"
:<|> P "search" :> Q (term :: String) :> V "article_search")
:<|> P "figures" :> M "figures" Int :> V "figures"
Each endpoint must terminate in V name
and must be uniquely specified by name
.
The library uses the type specified in this way to produce the following functions:
This is a smart constructor used to turn the user's API into a Routable api
. A value of type Routable api
is a proxy for a normalized version of the type specified by the user API. In the normalized version, nested alternatives have been removed and all type operators associate to the right.
let api = mkRoutable (RouteProxy :: _ ReadmeApi)
The route
function runs a user-supplied record of handlers for each named endpoint over a uri
. A uri
can be any type with a ToLocation
instance, such as a String
.
Each endpoint name
in the API specifies the field label for the corresponding handler. The endpoint's type fully determines the type of the handler, including capture and query parameter arguments.
Each handler in the record must have the same return type. For example, in an Elm-like framework, the common return type might be the framework's equivalent of an Html Msg
. Here it is just String
:
let handlers =
{ profile : \username -> "Profile for " <> username
, article_id : \(id :: Int) -> "Article #" <> show id
, article_search: \{ term } -> "Searched for " <> term
, figures: \arr -> "Figures: " <> joinWith ", " (show <$> arr)
}
match = route api handlers
assert $ match "/profile/blankhart" === Right "Profile for blankhart"
assert $ match "/article/34" === Right "Article #34"
assert $ match "/article/search?term=ptolemy" === Right "Searched for ptolemy"
assert $ match "/figures/1/2/3/4" === Right "Figures: 1, 2, 3, 4"
The capture and query parameter arguments can be any type with a FromUrlPiece
instance.
The allLinks
function produces a record of link generators accepting capture and query parameters, and returning a Link
to the endpoint's URL. Link
is currently a type alias for String
.
Each endpoint name
in the API specifies the field label for the corresponding link generator. The endpoint's type fully determines the type of the link generator.
The default return type can be modified by passing allLinksWith
a function of type Link -> a
. In an Elm-like framework, this could conveniently create Msg
messages instead of String
objects.
let links = allLinksWith identity api
assert $ links.profile "blankhart" === "/profile/blankhart"
assert $ links.article_id 34 === "/article/34"
assert $ links.article_search { term: "ptolemy" } === "/article/search?term=ptolemy"
assert $ links.figures [1, 2, 3, 4] === "/figures/1/2/3/4"
The capture and query parameters may be any type with a ToUrlPiece
instance.
These functions should satisfy the property that, for each endpoint in the user's API, running route
on the link produced by allLinks
always produces the same value as the corresponding handler.
quickCheck $ \username ->
route api handlers (links.profile username) === Right (handlers.profile username)
The test
file is based on the data type API in the tests for the purescript-routing
library, to facilitate comparison with a well-established router. That setup is extended with quickcheck-style tests verifying the route
/Link
relationship.
The examples/hertz
illustration is based on the router example in the purescript-hertz
framework. This framework was chosen because it has a minimal Purescript dependency footprint and came with a well designed illustration of its routing capabilities.
The examples/halogen
illustration is based on the router example in the purescript-halogen
framework, specifically the example labeled driver-routing
. This example does not have elaborate endpoints.
To avoid re-running the router on each render, the example uses the router to select a rendering function, which is then cached in the application state. The framework render
call just invokes the cached function.
An alternative is to create the router as a separate root component and rely on the framework to limit renders to child components. The Real World Halogen project uses purescript-routing-duplex
to parse the route into an intermediate data type, which is then cached in component state. The render
call dispatches on the intermediate data type.
This approach is a less natural fit with this library because a route
call jumps directly from the URL to the result of invoking the corresponding handler. There is no intermediate data type representing each endpoint.