# nine
A library providing a data driven router compiler.
The goal of `nine` is to allow developers to compose middleware and handlers.
This should significantly decrease boiler plate and give a web development experience competitive with other
languages and ecosystems.
## Build
rebar3 compile
## Usage
`nine` functions as a router compiler library. Meaning you give it a router config and it compiles a router module.
`nine` does not have it's own web server, it is meant to be paired with a backend.
This is a list of implemented backends:
- [nine_cowboy](https://git.sr.ht/~fancycade/nine_cowboy)
- [nine_elli](https://git.sr.ht/~fancycade/nine_elli)
`nine` provides one single function to be used: `compile`. Compile can be given one map argument or 3 arguments.
nine:compile(#{routes => Routes, router => Router, generator => Generator})
OR
nine:compile(Routes, Router, Generator).
The generator is provided by the backend, and is a module required to have at least one function `generate/2`.
The name of the `Router` module and the normalized routes after nine compiles the routes config given.
`nine:compile` takes a router config and compiles it into an Erlang module at runtime using [forms](https://www.erlang.org/doc/apps/erts/absform).
## Routes
A route is a map with these keys:
#{<<"path">> => ...,
<<"method">> => ...,
<<"pre">> => ...,
<<"post">> => ...,
<<"handle">> => ...}
Using atoms for the keys is an option as well:
#{path => ...,
method => ...,
pre => ...,
post => ...,
handle => ...}
path: The URL path for the request to be handled. Example: <<"/todo">>.
method: The request method to be handled. Example: <<"GET">>.
pre: The middleware to be called before the request handler. Example: [{todo_mid, json_request}]
post: The middleware to be called after the request handler. Example: [{todo_mid, json_response}]
handle: The request handler function.
Because of how some of nine's defaults work this is the most minimal config possible:
#{handle => {index_handler, index}}
This config will route requests with the root path ('/') with any method type to the function `index_handler:index/1`.
The `handle` key is the only required key in a route config.
Use a list to compose multiple requests. The order in the list is respected as the order that the requests will be matched.
A full config for a todo application would look like this:
[#{<<"path">> => <<"/">> =>
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, index}},
#{<<"path">> => <<"/api">>,
<<"post">> => [{todo_mid, json_response}],
<<"handle">> => [
#{<<"path">> => <<"/todo">>,
<<"pre">> => [{todo_mid, json_request}],
<<"handle">> => [
#{<<"method">> => <<"POST">>,
<<"handle">> => {todo_handler, post_todo}},
#{<<"method">> => <<"DELETE">>,
<<"handle">> => {todo_handler, delete_todo}}
],
#{<<"path">> => <<"/todos">>,
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, get_todos}}
]
},
#{<<"path">> => <<"*">>,
<<"handle">> => {todo_handler, not_found}}
]
There is a lot going on here with this config which we will break down in the next sections.
### Handler
A handler is specified as {module, function}. Example:
{todo_handler, get_todos}
`todo_handler` being the module, and `get_todos` is the function.
The handler function is expected to look like this:
get_todos(Context) ->
Resp = cowboy_req:reply(
200,
#{<<"Content-Type">> => <<"application/json">>},
thoas:encode(#{hello => <<"world">>})
),
Context#{resp => Resp}.
#### Nested Handlers
It is possible to compose complex router configs by nesting handlers. The `handle` key can either take a
tuple or a list of route configs.
For example an `api` handler can branch off with multiple other request paths.
[{<<"path">> => <<"/api">>,
<<"handle">> => [
#{<<"path">> => <<"todo">>,
<<"method">> => <<"POST">>,
<<"handle">> => {todo_handler, post_todo}},
#{<<"path">> => <<"todos">>,
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, get_todos}}
]}]
This means when a POST request goes to `/api/todo` the function `todo_handler:post_todo/1` will be called. While
a request going to `/api/todos` will trigger the function `todo_handler:get_todos/1`.
#### URL Path Params
`nine` builds in a way to have named parameters in the URL.
A path like `/todo/:id` will result in the context map including the params key.
The value of the params will be `#{id => <<"id1">>}`.
In case you are worried about atoms coming from user data, it is okay for `id` to be an atom because it is a static value set at compile time.
nine also supports partial path params like `/person/num:ber` will result in a params map looking like `#{ber => <<"2">>}` for example. This is similar to how Phoenix works with routing.
#### Wildcard
`nine` supports catch all routes with `*` in the path. For example: `<<"/*">>` will match any route.
We can also put a wildcard at the suffix of a path: `<<"/foo/*">>` will match a route like `<<"/foo/bar">>`.
Wildcards can only be at the end of a path. This is similar to how Phoenix works with catch all routes.
The reason for this is the routing uses Erlang pattern matching, so it must follow the same rules.
### Middleware
Middleware are specified just like handlers, in fact they are the same thing! An example middleware might look like:
{nine_cowboy_mid, json_request}
Middleware are functions that take a Context as input and output a Context or an elli response. One could write a logging middleware like this:
logging_middleware(Context) ->
logger:debug(#{context => Context}),
Context.
Or we could make a middleware that adds some data to the Context:
message_middleware(Context) ->
Context#{message => <<"Hello, World!">>}.
Middleware are helpful in all sorts of situations and allow developers to write web apps in a DRY way.
#### Middleware Chains
`nine` specifies middleware chaining with the `pre` and `post` keys. Nesting routes will also concatenate the `pre` and `post` keys
in the expected order.
For example:
#{<<"pre">> => [{todo_mid, json_request}],
<<"handle">> => {todo_handler, post_todo}}
Will generate a sequence of function calls where `todo_mid:json_request` is called first, and the result is passed to
`todo_handler:post_todo`.
Allowing `post_todo` to be implemented like so:
post_todo(Context=#{json := #{<<"body">> := Body}}) ->
todo_db:insert(Body),
nine_cowboy_util:redirect(Context, <<"/">>).
`post_todo` can expect the `json` key to be filled with data because `json_request` is called first.
#### Halting
There are situations where we want to return a response immediately without finishing the middleware chain. This is known as halting.
`nine` makes this possible because each middleware and handler call is wrapped in a case statement checking for the `resp` key.
If a handler or middleware returns a Context map with the `resp` key it will immediately be sent without triggering further middleware.
## Inspirations
`nine` was inspired by other composable middleware tools.
- [ring](https://github.com/ring-clojure/ring/wiki/Concepts) - Standard Clojure HTTP abstraction for web servers
- [ataraxy](https://github.com/weavejester/ataraxy) - Data driven routing library for Clojure
- [Plug.Router](https://hexdocs.pm/plug/readme.html#plug-router) - Ecosystem defining Elixir HTTP middleware
- [golang http middleware](https://dev.to/theghostmac/understanding-and-crafting-http-middlewares-in-go-3183) - Standard Library Golang Middleware Pattern
- [Cowboy Router](https://ninenines.eu/docs/en/cowboy/2.10/guide/routing/) - Cowboy router is compiled into a lookup table
## Fun Facts
- The name `nine` comes from "nine nines".
- Middleware was originally intended to look like Ring's, but wasn't compatible with Erlang's pattern matching lookups.