Current section
Files
Jump to
Current section
Files
src/dream@router.erl
-module(dream@router).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream/router.gleam").
-export([default_404_controller/3, method/2, path/2, controller/2, middleware/2, route/5, match_path/2, find_route/2, build_controller_chain/2]).
-export_type([middleware/2, route/2, router/2, empty_services/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Route configuration and request matching\n"
"\n"
" The router matches incoming requests to controllers based on HTTP method and path patterns.\n"
" It supports path parameters, wildcards, middleware chains, and custom context/services types.\n"
"\n"
" ## Basic Routing\n"
"\n"
" ```gleam\n"
" import dream/router.{router}\n"
" import dream/http/transaction.{Get, Post}\n"
"\n"
" pub fn create_router() {\n"
" router\n"
" |> router.route(Get, \"/\", controllers.index, [])\n"
" |> router.route(Get, \"/users/:id\", controllers.show_user, [])\n"
" |> router.route(Post, \"/users\", controllers.create_user, [])\n"
" }\n"
" ```\n"
"\n"
" ## Path Parameters\n"
"\n"
" Use `:name` to capture path segments as parameters:\n"
" - `/users/:id` matches `/users/123` and extracts `id = \"123\"`\n"
" - `/posts/:post_id/comments/:id` extracts both parameters\n"
"\n"
" Access parameters in your controller with `get_param(request, \"id\")`.\n"
"\n"
" ## Wildcards\n"
"\n"
" Wildcards match one or more path segments:\n"
" - `*` or `*name` - Matches exactly one segment\n"
" - `**` or `**path` - Matches zero or more segments (greedy)\n"
" - `*.jpg` - Matches any path ending in `.jpg`\n"
" - `*.{jpg,png,gif}` - Matches multiple extensions\n"
"\n"
" ## Middleware\n"
"\n"
" Middleware run before (and optionally after) your controller:\n"
"\n"
" ```gleam\n"
" router\n"
" |> router.route(\n"
" Get,\n"
" \"/admin/users\",\n"
" controllers.admin_users,\n"
" [auth_middleware, logging_middleware]\n"
" )\n"
" ```\n"
"\n"
" Middleware are executed in order: `auth` → `logging` → controller → `logging` → `auth`.\n"
" Each middleware can modify the request on the way in or the response on the way out.\n"
"\n"
" ## Route Matching\n"
"\n"
" Routes are matched in the order they're defined. First match wins.\n"
" More specific routes should come before general ones:\n"
"\n"
" ```gleam\n"
" router\n"
" |> router.route(Get, \"/users/new\", controllers.new_user, []) // Specific\n"
" |> router.route(Get, \"/users/:id\", controllers.show_user, []) // General\n"
" ```\n"
).
-type middleware(ACUF, ACUG) :: {middleware,
fun((dream@http@request:request(), ACUF, ACUG, fun((dream@http@request:request(), ACUF, ACUG) -> dream@http@response:response())) -> dream@http@response:response())}.
-type route(ACUH, ACUI) :: {route,
dream@http@request:method(),
binary(),
fun((dream@http@request:request(), ACUH, ACUI) -> dream@http@response:response()),
list(middleware(ACUH, ACUI))}.
-type router(ACUJ, ACUK) :: {router, list(route(ACUJ, ACUK))}.
-type empty_services() :: empty_services.
-file("src/dream/router.gleam", 117).
?DOC(" Default 404 controller for AppContext\n").
-spec default_404_controller(
dream@http@request:request(),
dream@context:app_context(),
empty_services()
) -> dream@http@response:response().
default_404_controller(_, _, _) ->
{response,
404,
{text, <<"Not Found"/utf8>>},
[{header, <<"Content-Type"/utf8>>, <<"text/plain; charset=utf-8"/utf8>>}],
[],
{some, <<"text/plain; charset=utf-8"/utf8>>}}.
-file("src/dream/router.gleam", 143).
?DOC(" Set the HTTP method for the route\n").
-spec method(route(ACUL, ACUM), dream@http@request:method()) -> route(ACUL, ACUM).
method(Route, Method_value) ->
{route,
Method_value,
erlang:element(3, Route),
erlang:element(4, Route),
erlang:element(5, Route)}.
-file("src/dream/router.gleam", 151).
?DOC(" Set the path for the route\n").
-spec path(route(ACUR, ACUS), binary()) -> route(ACUR, ACUS).
path(Route, Path_value) ->
{route,
erlang:element(2, Route),
Path_value,
erlang:element(4, Route),
erlang:element(5, Route)}.
-file("src/dream/router.gleam", 159).
?DOC(" Set the controller function for the route\n").
-spec controller(
route(ACUX, ACUY),
fun((dream@http@request:request(), ACUX, ACUY) -> dream@http@response:response())
) -> route(ACUX, ACUY).
controller(Route, Controller_function) ->
{route,
erlang:element(2, Route),
erlang:element(3, Route),
Controller_function,
erlang:element(5, Route)}.
-file("src/dream/router.gleam", 178).
-spec wrap_middleware(
fun((dream@http@request:request(), ACVK, ACVL, fun((dream@http@request:request(), ACVK, ACVL) -> dream@http@response:response())) -> dream@http@response:response())
) -> middleware(ACVK, ACVL).
wrap_middleware(Mw) ->
{middleware, Mw}.
-file("src/dream/router.gleam", 167).
?DOC(" Add middleware to the route (accepts a list for convenience)\n").
-spec middleware(
route(ACVD, ACVE),
list(fun((dream@http@request:request(), ACVD, ACVE, fun((dream@http@request:request(), ACVD, ACVE) -> dream@http@response:response())) -> dream@http@response:response()))
) -> route(ACVD, ACVE).
middleware(Route, Middleware_list) ->
Middleware_wrappers = gleam@list:map(Middleware_list, fun wrap_middleware/1),
{route,
erlang:element(2, Route),
erlang:element(3, Route),
erlang:element(4, Route),
lists:append(Middleware_wrappers, erlang:element(5, Route))}.
-file("src/dream/router.gleam", 205).
?DOC(
" Add a route to the router\n"
"\n"
" Routes are matched in the order they're added, so put more specific routes first.\n"
" The path supports parameters (`:id`), wildcards (`*`, `**`), and extensions (`*.jpg`).\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Simple route\n"
" router.route(router, Get, \"/\", home_controller, [])\n"
"\n"
" // Route with path parameter\n"
" router.route(router, Get, \"/users/:id\", show_user, [])\n"
"\n"
" // Route with middleware\n"
" router.route(router, Post, \"/admin/users\", create_user, [auth, logging])\n"
"\n"
" // Wildcard route for static files\n"
" router.route(router, Get, \"/assets/**path\", serve_static, [])\n"
" ```\n"
).
-spec route(
router(ACVO, ACVP),
dream@http@request:method(),
binary(),
fun((dream@http@request:request(), ACVO, ACVP) -> dream@http@response:response()),
list(fun((dream@http@request:request(), ACVO, ACVP, fun((dream@http@request:request(), ACVO, ACVP) -> dream@http@response:response())) -> dream@http@response:response()))
) -> router(ACVO, ACVP).
route(Router, Method_value, Path_value, Controller_function, Middleware_list) ->
Middleware_wrappers = gleam@list:map(Middleware_list, fun wrap_middleware/1),
Route = {route,
Method_value,
Path_value,
Controller_function,
Middleware_wrappers},
{router, [Route | erlang:element(2, Router)]}.
-file("src/dream/router.gleam", 270).
-spec non_empty_segment(binary()) -> boolean().
non_empty_segment(Segment) ->
Segment /= <<""/utf8>>.
-file("src/dream/router.gleam", 259).
?DOC(
" Match a path against a pattern and extract parameters\n"
"\n"
" Returns the extracted parameters if the path matches the pattern, or `None` if it doesn't.\n"
"\n"
" ## Path Parameters\n"
"\n"
" ```gleam\n"
" match_path(\"/users/:id\", \"/users/123\")\n"
" // -> Some([#(\"id\", \"123\")])\n"
"\n"
" match_path(\"/users/:user_id/posts/:id\", \"/users/123/posts/456\")\n"
" // -> Some([#(\"user_id\", \"123\"), #(\"id\", \"456\")])\n"
" ```\n"
"\n"
" ## Wildcards\n"
"\n"
" ```gleam\n"
" // Single-segment wildcard\n"
" match_path(\"/assets/*file\", \"/assets/logo.png\")\n"
" // -> Some([#(\"file\", \"logo.png\")])\n"
"\n"
" // Multi-segment wildcard\n"
" match_path(\"/files/**path\", \"/files/docs/guide.pdf\")\n"
" // -> Some([#(\"path\", \"docs/guide.pdf\")])\n"
"\n"
" // Extension matching\n"
" match_path(\"/images/*.jpg\", \"/images/photo.jpg\")\n"
" // -> Some([])\n"
"\n"
" // Multiple extensions\n"
" match_path(\"/images/*.{jpg,png}\", \"/images/photo.jpg\")\n"
" // -> Some([])\n"
" ```\n"
).
-spec match_path(binary(), binary()) -> gleam@option:option(list({binary(),
binary()})).
match_path(Pattern_string, Path) ->
Pattern_segments = begin
_pipe = gleam@string:split(Pattern_string, <<"/"/utf8>>),
gleam@list:filter(_pipe, fun non_empty_segment/1)
end,
Path_segments = begin
_pipe@1 = gleam@string:split(Path, <<"/"/utf8>>),
gleam@list:filter(_pipe@1, fun non_empty_segment/1)
end,
dream@router@pattern:match_segments(Pattern_segments, Path_segments).
-file("src/dream/router.gleam", 309).
-spec check_path_match(route(ACWX, ACWY), binary()) -> {ok,
{route(ACWX, ACWY), list({binary(), binary()})}} |
{error, nil}.
check_path_match(Route, Path) ->
case match_path(erlang:element(3, Route), Path) of
{some, Params} ->
{ok, {Route, Params}};
none ->
{error, nil}
end.
-file("src/dream/router.gleam", 297).
-spec check_route_match(dream@http@request:request(), route(ACWO, ACWP)) -> {ok,
{route(ACWO, ACWP), list({binary(), binary()})}} |
{error, nil}.
check_route_match(Request, Route) ->
Method_matches = erlang:element(2, Route) =:= erlang:element(2, Request),
case Method_matches of
false ->
{error, nil};
true ->
check_path_match(Route, erlang:element(5, Request))
end.
-file("src/dream/router.gleam", 282).
-spec find_route_recursive(
list(route(ACWF, ACWG)),
dream@http@request:request()
) -> gleam@option:option({route(ACWF, ACWG), list({binary(), binary()})}).
find_route_recursive(Routes, Request) ->
case Routes of
[] ->
none;
[Route | Rest] ->
case check_route_match(Request, Route) of
{ok, Result} ->
{some, Result};
{error, _} ->
find_route_recursive(Rest, Request)
end
end.
-file("src/dream/router.gleam", 275).
?DOC(" Find matching route and extract params\n").
-spec find_route(router(ACVX, ACVY), dream@http@request:request()) -> gleam@option:option({route(ACVX, ACVY),
list({binary(), binary()})}).
find_route(Router, Request) ->
find_route_recursive(erlang:element(2, Router), Request).
-file("src/dream/router.gleam", 353).
-spec create_wrapped_controller(
fun((dream@http@request:request(), ACXQ, ACXR, fun((dream@http@request:request(), ACXQ, ACXR) -> dream@http@response:response())) -> dream@http@response:response()),
fun((dream@http@request:request(), ACXQ, ACXR) -> dream@http@response:response())
) -> fun((dream@http@request:request(), ACXQ, ACXR) -> dream@http@response:response()).
create_wrapped_controller(Middleware_fn, Controller) ->
fun(Request, Context, Services) ->
Middleware_fn(Request, Context, Services, Controller)
end.
-file("src/dream/router.gleam", 335).
-spec build_chain_recursive(
list(middleware(ACXL, ACXM)),
fun((dream@http@request:request(), ACXL, ACXM) -> dream@http@response:response())
) -> fun((dream@http@request:request(), ACXL, ACXM) -> dream@http@response:response()).
build_chain_recursive(Middleware, Controller) ->
case Middleware of
[] ->
Controller;
[Mw | Rest] ->
case Mw of
{middleware, Middleware_fn} ->
Wrapped_controller = create_wrapped_controller(
Middleware_fn,
Controller
),
build_chain_recursive(Rest, Wrapped_controller)
end
end.
-file("src/dream/router.gleam", 326).
?DOC(
" Build a controller chain from middleware and final controller\n"
"\n"
" Composes middleware with the controller to create a single function. Middleware\n"
" execute in order on the way in, then in reverse order on the way out.\n"
"\n"
" For middleware `[auth, logging]` with controller `handle`:\n"
" Request → auth → logging → handle → logging → auth → Response\n"
).
-spec build_controller_chain(
list(middleware(ACXG, ACXH)),
fun((dream@http@request:request(), ACXG, ACXH) -> dream@http@response:response())
) -> fun((dream@http@request:request(), ACXG, ACXH) -> dream@http@response:response()).
build_controller_chain(Middleware, Final_controller) ->
Reversed = lists:reverse(Middleware),
build_chain_recursive(Reversed, Final_controller).