
Roundabout
A route generator for Gleam.
Why
Gleam gives us a great way of matching paths via pattern matching:
pub type Route {
Users
User(id: String)
NotFound
}
pub fn get_route(segments: List(String)) {
case segments {
["users"] -> Users
["users", id] -> User(id)
_ -> NotFound
}
}
However, this doesn’t provide a type safe way of constructing a path from type e.g.
User("12") -> "/users/12"
See https://www.kurz.net/posts/gleam-routing for a more detailed explanation.
This packages provides a generator which gives you:
- A function for converting paths to Route types (
segments_to_route) - A function for converting Route types to paths (
route_to_path) - Helpers for generating route types and paths for each route
This generator can be used in frontend or backend applications, at it only generates the route types and helper functions. You still need to write your own router using these types.
Install
gleam add roundabout@1
Generating routes
Create a module in your project which defines the route definitions and calls the generator. e.g. in src/gen_routes.gleam
import roundabout.{Int, Lit, Route, Str}
const routes = [
Route(name: "home", path: [], sub: []),
// Will match an individual order e.g. /orders/123
Route(name: "order", path: [Lit("orders"), Int("id")], sub: []),
Route(
name: "user",
path: [Lit("users"), Int("id")],
sub: [
// Will match /users/123
Route(name: "show", path: [], sub: []),
// Will match /users/123/delete
Route(name: "delete", path: [Lit("delete")], sub: []),
],
),
]
pub fn main() -> Nil {
roundabout.main(routes, "src/generated/routes")
}
Call this using:
gleam run -m gen_routes
See example output at examples/src/generated/routes.gleam
Using this in your application
After the routes have been generated, you can use them in your router or views like:
import generated/routes
pub fn handle(segments: List(String)) {
let maybe_route = routes.segments_to_route(segments)
case maybe_route {
Ok(routes.Home) -> handle_home()
Ok(routes.Order(id)) -> handle_order(id)
...
Error(_) -> handle_not_found()
}
}
Notes
The order is important
If you have routes like:
[
Route(name: "show", path: [Str("id")], sub: []),
Route(name: "invite", path: [Lit("invite")], sub: []),
]
The first one will always match over the second one, make sure that literal routes are first.
Structure your routes to support your middleware
If you want to use different middlewares at different levels of your application, you can structure your routes to support this.
For example, having:
const routes = [
Route(name: "home", path: [], sub: []),
Route(
name: "app",
path: [Lit("app")],
sub: [
// Will match /app/
Route(name: "dashboard", path: [], sub: []),
],
),
]
Allows to apply some middleware for authentication like:
import generated/routes
import middleware
import wisp
pub fn handle(req: Request,, ctx: Context) {
let segments = wisp.path_segments(req)
let maybe_route = routes.segments_to_route(segments)
case maybe_route {
Ok(routes.Home) -> handle_home()
Ok(routes.App(sub)) -> {
use authenticated_context <- middleware.require_session(req, ctx)
case sub {
routes.Dashboard -> handle_dashboard(authenticated_context)
}
}
...
Error(_) -> handle_not_found()
}
}
Further documentation can be found at https://hexdocs.pm/roundabout.