Type-safe router for Literium web-framework
Why
The ordinary routers is not type-safe, so when you change route or corresponding interface you need track compliance between it manually.
The compiler cannot help you in cases of breaking that compliance.
For example, your app may expect arguments which actual router don't provide.
Or viceversa the router may require some arguments which missing in your application.
This leads us to a question: what can we do to make type system to keep the routes and corresponding interfaces in compliance?
Many frameworks in other languages with type inference, like Haskell and PureScript, solves this question. So why TypeScript cannot do same too.
How
Each route have corresponds object with typed fields i.e. arguments.
The routes without arguments corresponds to empty object.
The type of this arguments object is inferred automatically by constructing the route.
You can construct complex routes using simple routing algebra.
Routing algebra
String route
In simplest case you can use path string to create routes.
import { route_str, route_match, route_build } from 'literium-router';
const blog = route_str('/blog');
route_match(blog, '/blog')
route_match(blog, '/blog/1')
route_match(blog, '/other')
route_build(blog, {})
route_build(blog, {id: 1})
This route corresponds to path /blog and empty object.
Route with argument
When you want handle some data in the path string you can create route with the typed argument.
import { route_arg, route_match, route_build, baseTypes } from 'literium-router';
const blog_id = route_arg({id: 'num'}, baseTypes);
route_match(blog_id, '1')
route_match(blog_id, 'other')
route_match(blog_id, 'blog/1')
route_build(blog_id, {})
route_build(blog_id, {id: 1})
The first argument of route_arg()
is the object with property name as key and with property type-tag as value.
The available type-tags and corresponding value types is defined by the second argument of route_arg()
.
Complex routes
To build complex routes you can use route_and()
to sequentially combine sub-routes.
import { route_str, route_arg, route_and, route_match, route_build, baseTypes } from 'literium-router';
const blog_by_id = route_and(route_str('/blog/'), route_arg({id: 'num'}, baseTypes);
route_match(blog_by_id, '/blog/1')
route_match(blog_by_id, '/blog/1/')
route_match(blog_by_id, '/other')
route_match(blog_by_id, '1')
route_build(blog_by_id, {})
route_build(blog_by_id, {id: 1})
Route variants
And of course you can combine some number of routes which can be used alternatively.
import { route_str, route_arg, route_and, route_match, route_build, baseTypes } from 'literium-router';
const blogs_by_tag = route_and(route_str('/blog/tag-'), route_arg(baseTypes, {id: 'str'}));
const by_author = route_and(route_str('/author-'), route_arg(baseTypes, {user: 'num'}));
const blogs_by_tag_opt_by_author = route_and(blogs_by_tag, route_or(by_author, route_str('')));
route_match(blogs_by_tag_opt_by_author, '/blog/tag-js')
route_match(blogs_by_tag_opt_by_author, '/blog/tag-js/author-3')
route_match(blogs_by_tag_opt_by_author, '/blog/tag-js/')
route_match(blogs_by_tag_opt_by_author, '/blog/tag-js/author-')
route_match(blogs_by_tag_opt_by_author, '/other')
route_build(blogs_by_tag_opt_by_author, {})
route_build(blogs_by_tag_opt_by_author, {tag:"git"})
route_build(blogs_by_tag_opt_by_author, {user:3})
route_build(blogs_by_tag_opt_by_author, {tag:"git",user:3})
NOTE: Keep in mind the more complex variant must precede simple because in else case the simple routes may become unreachable to match.
This assumption is accepted in order to simplify and speedup route matching algorithm.
Argument types
One of the more helpful feature of this router is the possibility to define your own argument types.
For example, suppose we have enum type Order which has two values: Asc and Desc. We can simply use it in our routes as arguments when we defined corresponding TypeApi.
import { route_str, route_arg, route_and, route_match, route_build, TypeApi } from 'literium-router';
export const enum Order { Asc, Desc }
export interface OrderType {
ord: Order;
}
export const orderType: TypeApi<OrderType> = {
ord: {
parse: path => {
const m = path.match(/^(asc|desc)(.*)$/);
if (m) return [m[1] == 'asc' ? Order.Asc : Order.Desc, m[2]];
},
build: arg => arg === Order.Asc || arg === Order.Desc ?
`${arg == Order.Asc ? 'asc' : 'desc'}` : undefined
},
};
const blogs_by_date = route_and(route_str('/blog/date-'), route_arg(orderType, {sort: 'ord'}));
route_match(blogs_by_date, '/blog/date-asc')
route_match(blogs_by_date, '/blog/date-desc')
route_match(blogs_by_date, '/blog/date-')
route_match(blogs_by_date, '/blog/date-other')
route_build(blogs_by_date, {sort:Order.Asc})
route_build(blogs_by_date, {sort:Order.Desc})
route_build(blogs_by_date, {})
route_build(blogs_by_date, {sort:'asc'})
Router
Usually in real-life applications we don't like to operate with routes separately.
One of ways to works with all defined routes together is using of the methods router_match()
and router_build()
.
This methods gets the object with routes names as keys and routes itself as values.
It operates with the complex state which represents object with routes names as keys and corresponding route arguments objects as values.
import { route_str, route_arg, route_and, router_match, router_build, baseTypes } from 'literium-router';
const root = route_str('/');
const blog_by_id = route_and(root, route_str('blog/'), route_arg({id:'num'}, baseTypes));
const blogs_by_tag = route_and(root, route_str('blog/tag-'), route_arg({tag:'str'}, baseTypes));
const routes = { root, blogs_by_tag, blog_by_id };
router_match(routes, '/')
router_match(routes, '/blog/2')
router_match(routes, '/blog/tag-es6')
router_build(routes, {root:{}})
router_build(routes, {blog_by_id:{id:2}})
router_build(routes, {blogs_by_tag:{tag:"es6"}})
TypeScript 2.7 issue
The TypeScript 2.7.x have an issue which breaks the type inference of object values, so I strongly recommend to use TypeScript 2.6.x until TypeScript 2.8.x has been released.
Todo
- Handling search parameters