README
superouter
Quick Start
npm install superouter
const superouter = require('superouter')
const Route =
superouter.type('Route', {
Home: '/',
Post: '/posts/:post',
Settings: '/settings/...rest'
})
Route.Home()
//=> { type: 'Route', tag: 'Home' }
Route.Post({ post })
//=> { type: 'Route', tag: 'Post', value: { post }}
Route.Settings({ rest: '/a/b/c' })
// => { type: 'Route', tag: 'Settings', value: { rest: '/a/b/c' } }
const view =
Route.fold(
{ Home: () => <Home/>
, Post: ({ post }) => <Post post={post} />
, Settings: ({ rest }) => <Settings subpath={rest} />
}
)
view ( Route.Home() ) //=> <Home />
Route.matchOr( () => Route.Home(), '/settings/1/2/3' )
//=> Route.Settings({ rest: '1/2/3' })
const renderRoute = () => {
const url = window.location.pathname
React.render(
view( Route.matchOr( () => Route.Home(), url ) )
, document.body
)
}
// Respond to history
history.onpopstate( () => renderRoute() )
// Render initial route
renderRoute()
What?
- Data Driven
- Pure
- Predictable
- Simple
- Composeable
- Server + Client
- Serializable
Predictable
In the land of SPA's, routing bugs are probably the most frustrating to debug, because you are often losing context as the page switches.
The standard approach to routing is to generate a regex and pick the first match that works.
What this library does differently is handle the logic of routing with a parser that can justify and rank its decisions which can lead to better matches that don't rely on definition order.
E.g. the following patterns can match the same URL, but one is more specific.
Type | Pattern |
---|---|
Less Specific | /accounts/:account_id |
More Specific | /accounts/create |
If we had a url /accounts/create
both patterns will match, but clearly a particular pattern is the intended match. Most routers rely on definition order but this library will rank matches by specificity and return the best match.
You can then take that stream of known structures and very easily connect it to the history API or server side routing. It's exactly the kind of thing you don't realise you need until you do.
Data Driven
This library uses a specification for its data structures called stags. You can take the output and generate your own framework or application behaviour in a standard structured way. You can also persist / serialize and transfer these data structures because reference equality is never relied upon.
This means you can do fun things like have your API define its endpoints with superouter and then send type information over the wire to generate constructors for a client side SDK for better validation.
Or you can store route information as analytics and replay them later without losing accompanying state in the deserialization process.
Routes are the heart of our applications but by compressing them into strings and regular expressions we are throwing away valuable information that can be used in various parts of the application.
Pure
This library exposes pure functions, it's up to you to connect the routing data structures to your own framework or native browser API. That at first may sound like a chore, but it's exactly what you need when you decide you want to do some fancy hydration, SSR or custom routing transitions.
When a library manages the routing side effects, you may find yourself hacking around the edges to do the thing you actually want to do.
Simple
All this library does is handle conversions between different types of data, it doesn't perform any side effects directly so it's easy to explore and test and therefore reliable to build upon.
Composeable
Because the data structures used by this library are part of the exposed API, you can compose this library's functions with your own to create something new and interesting without having to submit a PR.
API
superouter.type
superouter.type(typename: String, { [tagName]: patternString })
Defines a Route
type for your application.
const Route =
superouter.type('Route', {
Home: '/',
Settings: '/settings/:page',
Messages: '/messages/:user/:thread'
})
The patternString
can include the following forms.
Type | Format | Explanation |
---|---|---|
Path | text |
A static segment of a route path. |
Part | :text |
A dynamic segment of a route path |
Variadic | ...text |
1 or more dynamic segments of a route path |
patternString
types cannot be mixed. So :...text
is not valid.
Route
Route
is a type that represents the various pages in your application.
You can create more than 1 Route
type for different sections or layers of your app. You can define all your Routes centrally or cascade your Route
matching in layers in a typed manner.
The Route
type will have a constructor function for each tag of Route
you specified in its definition under the namespace of
. These Route
constructor functions will throw
if a property specified in the pattern was not passed in at initialization.
To safely create a Route
instance from a url
, use Route.matchOr
.
const Route =
superouter.type('Route', {
Home: '/',
Settings: '/settings/:section/:setting'
})
Route.Settings({ section: 'ci', settings: 'access' })
// Route.Settings({ section: 'ci', settings: 'access' })
Route.Settings({ missing: 1, things: 2 })
//=> TypeError: ...
Route.fold
({ [routetagNames]: ({ ...routeArgs }) => a }) => Route => a
const view =
Route.fold({
Home: () => <Home/>,
Post: ({ name }) => <Post postName={name} />
})
view ( Route.Post({ name: 'A Perfect API' }) )
// => <Post postname="A Perfect API"/>
Used to define functions that handle all the potential routes in your application.
For some more advanced error checking try stags's fold instead.
stags
will throw if you have missed tags or specified too many tags.
Route.fold
is especially useful to map Route
to views in your application in a manner similar to <Switch>
in react-router
.
Route.matchOr
( (Error[]) => Route, url ) => Route
matchOr
accepts a callback to handle url's that can't be matched and a url
to try to match.
If no match can be found the callback is executed to allow the user to return from unexpected url's.
const Route = type('Route', {
Home: '/',
Settings: '/settings/:settings',
Album: '/album/:album_id',
AlbumPhoto: '/album/:album_id/photo/:file_id',
Tag: '/tag/:tag',
TagList: '/tag',
TagFile: '/tag/:tag/photo/:file_id'
})
Route.matchOr(
() => Route.Home(),
'/album/abc123/photo/123'
)
//=> Route.AlbumPhoto({ album_id: 'abc123', file_id: '123' })
Route.matchOr(
() => Route.Home(),
'/unknown/route'
)
//=> Route.Home()
matchOr
also passes in all the errors keyed by the tag name of the routes
to the callback, so you can have custom logic that returns different default branches depending on the matching errors.
Route.matchOr(
errs => errs.TagFile.find( x => x.tag === 'ExcessPattern' )
? Route.TagList()
: Route.Home()
, url
)
Route.matches
string => Valid.Y( Route[] ) | Valid.N ( StrMap( tagName, Error[] ) )
Route.matches
is a lower level alternative to Route.matchOr
which either returns all the valid matching routes (if there are any) or all the errors
that prevented matches keyed by the name of the route tags.
Route.toURL
Route => string
const Route = type('Route', {
Home: '/',
Settings: '/settings/:settings',
Album: '/album/:album_id',
AlbumPhoto: '/album/:album_id/photo/:file_id',
Tag: '/tag/:tag',
TagFile: '/tag/:tag/photo/:file_id'
})
Route.toURL( Route.Tag({ tag: 'beach' }) )
//=> /tag/beach
Advanced
🚨 Warning 🚨
There is absolutely no need to ever use any of the functionality below. You can very happily and safely only use the above API. Everything below is the primitives used to create the higher level API. It's exposed because there's no danger in doing so, and it's documented because it's exposed.
Valid
This library uses a sum-type Valid
to safely model invalid route matches. The user friendly API traverses this type and throws on errors. But if one
wants to safely analyze all the invalid patterns without using a try {} catch(e){...}
Valid
can be extremely useful.
You'll encounter Valid
if you interact with some more advanced functions exposed by the library including tokenizePattern
, tokenizeURL
, type$safe
, or Route.matches
.
Valid
is an example of stags usage. It's simply an object with 2 constructors Y
and N
.
Valid.Y(x)
will return an object { type: 'Valid', tag: 'Y', value: x }
And Valid.N(x)
will return an object { type: 'Valid', tag: 'N', value: x }
.
It's simply a way to annotate that there's some kind of error branching in a function without throwing an error.
Valid
includes some helper functions like fold
, bifold
and map
.
tokenizePattern
string -> Valid.Y( PatternToken[] ) | Valid.N( PatternToken.Error[] )
Convert a pattern string into an array of tokens or an array of PatternToken errors.
The response is wrapped in a Valid
to model the branching behaviour.
PatternToken.Error
is documented further in the Error Types section.
tokenizeURL
(PatternToken[], string) -> Valid.Y( URLToken[] ) | Valid.N( URLToken.Error[] )
Convert an array of PatternToken
's and a url
into an array of URLToken
's.
URLToken.Error
is documented further in the Error Types section.
The response is wrapped in a Valid
to model the branching behaviour.
PatternToken
A data structure that models the different types of supported patterns used
by the superouter.type
constructor.
There are 3 types of patterns:
data PatternToken
= Path string
| Part string
| Variadic string
URLToken
A data structure that models the matching of segments of a URL to segments of a PatternToken
.
There are 5 types of patterns:
data URLToken
= Unmatched { expected::string, actual::string }
| ExcessSegment string
| Path string
| Part { key::string, value::string }
| Variadic { key::string, value::string }
Each URLToken
has a specificity which helps guide higher level functions to choose the most accurate route match.
Error Types
You will encounter different types of errors when using the more advanced aspects of superouter
. Below is a brief explanation of each type of error and the circumstance that would trigger them.
Type | tag | When |
---|---|---|
PatternToken |
DuplicateDef |
When two patterns in a route defintion are effectively the same. |
PatternToken |
DuplicatePart |
When two bindings within a pattern have the same name. |
PatternToken |
VariadicPosition |
When a variadic pattern is not in the final position. |
PatternToken |
VariadicCount |
When there is more than one variadic in a pattern. |
URLToken |
UnmatchedPaths |
When a path segment was found but it did not match the expected value. |
URLToken |
ExcessSegment |
When a URL had more segments than a pattern had expected and there was no variadic to consume the excess segments. |
URLToken |
ExcessPattern |
When a URL did not have enough segments to satisfy a pattern. |