-module(dream@router). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream/router.gleam"). -export([router/0, build_controller_chain/2, route/5, stream_route/5, find_route/2]). -export_type([middleware/2, route/2, router/2, route_handler/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 uses a radix trie for O(path depth) lookup performance, making it efficient even with\n" " hundreds of routes. The router supports path parameters, wildcards, middleware chains,\n" " and custom context/services types.\n" "\n" " ## Basic Routing\n" "\n" " ```gleam\n" " import dream/router.{route, router}\n" " import dream/http/request.{Get, Post}\n" "\n" " pub fn create_router() {\n" " router()\n" " |> route(method: Get, path: \"/\", controller: controllers.index, middleware: [])\n" " |> route(method: Get, path: \"/users/:id\", controller: controllers.show_user, middleware: [])\n" " |> route(method: Post, path: \"/users\", controller: controllers.create_user, middleware: [])\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 `request.get_param(request, \"id\")`.\n" "\n" " ### Parameter Name Remapping\n" "\n" " Routes with different parameter names at the same position are supported.\n" " Each route extracts parameters using its own declared parameter names:\n" "\n" " ```gleam\n" " router()\n" " |> route(method: Get, path: \"/users/:id\", controller: show_user, middleware: [])\n" " |> route(method: Get, path: \"/users/:user_id/posts\", controller: show_posts, middleware: [])\n" " ```\n" "\n" " - `/users/123` extracts `id = \"123\"` (first route's param name)\n" " - `/users/123/posts` extracts `user_id = \"123\"` (second route's param name)\n" "\n" " The router automatically remaps parameters to match each route's declared names,\n" " even when routes share parameter positions in the trie structure.\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" " |> route(\n" " method: Get,\n" " path: \"/admin/users\",\n" " controller: controllers.admin_users,\n" " middleware: [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 Priority\n" "\n" " When multiple routes could match, the most specific wins:\n" " 1. Literal segments (exact match) - highest priority\n" " 2. Parameters (`:id`)\n" " 3. Single wildcards (`*`)\n" " 4. Extension patterns (`*.jpg`)\n" " 5. Multi-segment wildcards (`**`) - lowest priority\n" "\n" " This means `/users/new` will match before `/users/:id` even if defined in reverse order.\n" "\n" " ## Important: Parameter Validation Trade-off\n" "\n" " Dream validates path parameters at runtime, not compile-time. This means:\n" "\n" " - ✅ Ergonomic API: Simple, clean route definitions\n" " - ✅ Flexible: Easy to add/change routes dynamically\n" " - ❌ No compile-time safety: Typos in parameter names cause runtime errors\n" "\n" " Example of what the compiler WON'T catch:\n" "\n" " ```gleam\n" " // Route definition\n" " router()\n" " |> route(method: Get, path: \"/users/:user_id\", controller: show_user, middleware: [])\n" "\n" " // Controller (WRONG - will fail at runtime)\n" " fn show_user(request: Request, context: Context, services: Services) -> Response {\n" " case get_param(request, \"id\") { // Should be \"user_id\"\n" " Ok(param) -> // This will never execute\n" " Error(_) -> // Always hits this branch\n" " }\n" " }\n" " ```\n" "\n" " This is an intentional design decision favoring ergonomics over compile-time guarantees.\n" " We're exploring more type-safe alternatives that maintain clean APIs in\n" " [GitHub Discussion #15](https://github.com/TrustBound/dream/discussions/15).\n" "\n" " **Mitigation:** Write integration tests that exercise your routes. The runtime\n" " validators (`get_param`, `get_int_param`, etc.) will catch mismatches immediately.\n" ). -type middleware(AGGI, AGGJ) :: {middleware, fun((dream@http@request:request(), AGGI, AGGJ, fun((dream@http@request:request(), AGGI, AGGJ) -> dream@http@response:response())) -> dream@http@response:response())}. -type route(AGGK, AGGL) :: {route, dream@http@request:method(), binary(), fun((dream@http@request:request(), AGGK, AGGL) -> dream@http@response:response()), list(middleware(AGGK, AGGL)), boolean()}. -opaque router(AGGM, AGGN) :: {router, dream@router@trie:radix_trie(route_handler(AGGM, AGGN))}. -type route_handler(AGGO, AGGP) :: {route_handler, fun((dream@http@request:request(), AGGO, AGGP) -> dream@http@response:response()), list(middleware(AGGO, AGGP)), boolean(), list(binary())}. -type empty_services() :: empty_services. -file("src/dream/router.gleam", 200). ?DOC( " Empty router with no routes\n" "\n" " Starting point for building your application's router. Chain this with\n" " `route()` or `stream_route()` calls to add routes.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " router()\n" " |> route(method: Get, path: \"/\", controller: home, middleware: [])\n" " |> route(method: Get, path: \"/users/:id\", controller: show_user, middleware: [])\n" " ```\n" ). -spec router() -> router(any(), any()). router() -> {router, dream@router@trie:new()}. -file("src/dream/router.gleam", 383). ?DOC(" Check if a segment is non-empty\n"). -spec is_non_empty_segment(binary()) -> boolean(). is_non_empty_segment(Seg) -> Seg /= <<""/utf8>>. -file("src/dream/router.gleam", 376). ?DOC(" Split a path into segments for trie lookup\n"). -spec split_path(binary()) -> list(binary()). split_path(Path) -> _pipe = Path, _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), gleam@list:filter(_pipe@1, fun is_non_empty_segment/1). -file("src/dream/router.gleam", 406). ?DOC(" Extract param name from a single segment\n"). -spec extract_param_name_from_segment(dream@router@trie:segment()) -> gleam@option:option(binary()). extract_param_name_from_segment(Segment) -> case Segment of {param, Name} -> {some, Name}; {single_wildcard, {some, Name@1}} -> {some, Name@1}; {multi_wildcard, {some, Name@2}} -> {some, Name@2}; {single_wildcard, none} -> {some, <<"wildcard"/utf8>>}; {multi_wildcard, none} -> {some, <<"path"/utf8>>}; {extension_pattern, _} -> none; {literal_extension, _, _} -> none; {literal, _} -> none end. -file("src/dream/router.gleam", 391). ?DOC( " Extract param names from segments in path order\n" "\n" " Extracts names from Param, SingleWildcard, and MultiWildcard segments.\n" " Used to store param names with the route handler for remapping after lookup.\n" ). -spec extract_param_names_from_segments(list(dream@router@trie:segment())) -> list(binary()). extract_param_names_from_segments(Segments) -> case Segments of [] -> []; [Segment | Rest] -> Rest_names = extract_param_names_from_segments(Rest), Name = extract_param_name_from_segment(Segment), case Name of {some, N} -> [N | Rest_names]; none -> Rest_names end end. -file("src/dream/router.gleam", 426). ?DOC( " Remap params extracted during trie traversal to match route's param names\n" "\n" " Params are extracted using the trie node's param names (which may differ\n" " from the route's param names when routes share param positions). This function\n" " remaps them to the correct names based on the matched route's param_names.\n" "\n" " Both lists should be in path order (first param first).\n" ). -spec remap_params_to_route_names(list({binary(), binary()}), list(binary())) -> list({binary(), binary()}). remap_params_to_route_names(Extracted_params, Route_param_names) -> case {Extracted_params, Route_param_names} of {[], _} -> []; {_, []} -> Extracted_params; {[{_, Value} | Rest_params], [New_name | Rest_names]} -> Remapped_rest = remap_params_to_route_names(Rest_params, Rest_names), [{New_name, Value} | Remapped_rest] end. -file("src/dream/router.gleam", 519). ?DOC(" Apply middleware to a controller, creating a wrapped controller function\n"). -spec apply_middleware_to_controller( fun((dream@http@request:request(), AGIL, AGIM, fun((dream@http@request:request(), AGIL, AGIM) -> dream@http@response:response())) -> dream@http@response:response()), fun((dream@http@request:request(), AGIL, AGIM) -> dream@http@response:response()) ) -> fun((dream@http@request:request(), AGIL, AGIM) -> dream@http@response:response()). apply_middleware_to_controller(Middleware_fn, Controller) -> fun(Request, Context, Services) -> Middleware_fn(Request, Context, Services, Controller) end. -file("src/dream/router.gleam", 505). ?DOC( " Create a wrapped controller that applies middleware\n" "\n" " This is a named function to avoid anonymous functions in production code.\n" ). -spec wrap_controller_with_middleware( fun((dream@http@request:request(), AGIJ, AGIK, fun((dream@http@request:request(), AGIJ, AGIK) -> dream@http@response:response())) -> dream@http@response:response()), fun((dream@http@request:request(), AGIJ, AGIK) -> dream@http@response:response()) ) -> fun((dream@http@request:request(), AGIJ, AGIK) -> dream@http@response:response()). wrap_controller_with_middleware(Middleware_fn, Controller) -> apply_middleware_to_controller(Middleware_fn, Controller). -file("src/dream/router.gleam", 489). ?DOC(" Wrap a controller with a middleware function\n"). -spec create_wrapped_controller( fun((dream@http@request:request(), AGIH, AGII, fun((dream@http@request:request(), AGIH, AGII) -> dream@http@response:response())) -> dream@http@response:response()), fun((dream@http@request:request(), AGIH, AGII) -> dream@http@response:response()) ) -> fun((dream@http@request:request(), AGIH, AGII) -> dream@http@response:response()). create_wrapped_controller(Middleware_fn, Controller) -> wrap_controller_with_middleware(Middleware_fn, Controller). -file("src/dream/router.gleam", 470). ?DOC(" Recursively build the middleware chain\n"). -spec build_chain_recursive( list(middleware(AGIC, AGID)), fun((dream@http@request:request(), AGIC, AGID) -> dream@http@response:response()) ) -> fun((dream@http@request:request(), AGIC, AGID) -> 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", 460). ?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" "\n" " ## Parameters\n" "\n" " - `middleware`: List of middleware to apply\n" " - `final_controller`: The controller that handles the request\n" "\n" " ## Returns\n" "\n" " A composed function that applies all middleware and the controller\n" ). -spec build_controller_chain( list(middleware(AGHX, AGHY)), fun((dream@http@request:request(), AGHX, AGHY) -> dream@http@response:response()) ) -> fun((dream@http@request:request(), AGHX, AGHY) -> dream@http@response:response()). build_controller_chain(Middleware, Final_controller) -> Reversed = lists:reverse(Middleware), build_chain_recursive(Reversed, Final_controller). -file("src/dream/router.gleam", 539). ?DOC(" Wrap a middleware function in the Middleware type\n"). -spec wrap_middleware( fun((dream@http@request:request(), AGIN, AGIO, fun((dream@http@request:request(), AGIN, AGIO) -> dream@http@response:response())) -> dream@http@response:response()) ) -> middleware(AGIN, AGIO). wrap_middleware(Mw) -> {middleware, Mw}. -file("src/dream/router.gleam", 547). ?DOC(" Convert an HTTP method to its string representation\n"). -spec method_to_string(dream@http@request:method()) -> binary(). method_to_string(Method) -> dream@http@request:method_to_string(Method). -file("src/dream/router.gleam", 227). ?DOC( " Add a route to the router\n" "\n" " Registers a route with the given method, path pattern, controller, and middleware.\n" " The path supports parameters (`:id`), wildcards (`*`, `**`), and extensions (`*.jpg`).\n" "\n" " Routes are stored in a radix trie, so they don't need to be defined in any particular\n" " order - the most specific route will always match first.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Simple route\n" " route(router, method: Get, path: \"/\", controller: home_controller, middleware: [])\n" "\n" " // Route with path parameter\n" " route(router, method: Get, path: \"/users/:id\", controller: show_user, middleware: [])\n" "\n" " // Route with middleware\n" " route(router, method: Post, path: \"/admin/users\", controller: create_user, middleware: [auth, logging])\n" "\n" " // Wildcard route for static files\n" " route(router, method: Get, path: \"/assets/**path\", controller: serve_static, middleware: [])\n" " ```\n" ). -spec route( router(AGGU, AGGV), dream@http@request:method(), binary(), fun((dream@http@request:request(), AGGU, AGGV) -> dream@http@response:response()), list(fun((dream@http@request:request(), AGGU, AGGV, fun((dream@http@request:request(), AGGU, AGGV) -> dream@http@response:response())) -> dream@http@response:response())) ) -> router(AGGU, AGGV). route(Router_value, Method_value, Path_pattern, Controller_fn, Middleware_list) -> Segments = dream@router@parser:parse_pattern(Path_pattern), Method_string = method_to_string(Method_value), Middleware_wrappers = gleam@list:map(Middleware_list, fun wrap_middleware/1), Param_names = extract_param_names_from_segments(Segments), Handler = {route_handler, Controller_fn, Middleware_wrappers, false, Param_names}, Updated_trie = dream@router@trie:insert( erlang:element(2, Router_value), Method_string, Segments, Handler ), {router, Updated_trie}. -file("src/dream/router.gleam", 282). ?DOC( " Add a streaming route to the router\n" "\n" " Registers a route that receives the request body as a stream (Yielder(BitArray))\n" " instead of a buffered string. Use this for large file uploads, proxying external\n" " APIs, or any request body > 10MB.\n" "\n" " The controller receives `request.stream` as `Some(Yielder(BitArray))` and\n" " `request.body` as an empty string. Process chunks as they arrive without\n" " buffering the entire body in memory.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " router\n" " |> stream_route(method: Post, path: \"/upload\", controller: handle_upload, middleware: [auth])\n" " |> stream_route(method: Put, path: \"/files/:id\", controller: replace_file, middleware: [])\n" " ```\n" "\n" " ## When to Use\n" "\n" " - File uploads > 10MB\n" " - Proxying external APIs\n" " - Video/audio streaming\n" " - Large form submissions\n" "\n" " For regular requests (JSON APIs, forms < 10MB), use `route()` instead.\n" ). -spec stream_route( router(AGHB, AGHC), dream@http@request:method(), binary(), fun((dream@http@request:request(), AGHB, AGHC) -> dream@http@response:response()), list(fun((dream@http@request:request(), AGHB, AGHC, fun((dream@http@request:request(), AGHB, AGHC) -> dream@http@response:response())) -> dream@http@response:response())) ) -> router(AGHB, AGHC). stream_route( Router_value, Method_value, Path_pattern, Controller_fn, Middleware_list ) -> Segments = dream@router@parser:parse_pattern(Path_pattern), Method_string = method_to_string(Method_value), Middleware_wrappers = gleam@list:map(Middleware_list, fun wrap_middleware/1), Param_names = extract_param_names_from_segments(Segments), Handler = {route_handler, Controller_fn, Middleware_wrappers, true, Param_names}, Updated_trie = dream@router@trie:insert( erlang:element(2, Router_value), Method_string, Segments, Handler ), {router, Updated_trie}. -file("src/dream/router.gleam", 346). ?DOC( " Find the route matching the request\n" "\n" " Searches the router's trie for a route that matches the request's method and path.\n" " Returns the matched route and extracted path parameters, or None if no route matches.\n" "\n" " Uses radix trie lookup for O(path depth) performance, independent of total routes.\n" "\n" " ## Parameters\n" "\n" " - `router_value`: Router with configured routes\n" " - `request`: HTTP request to match against\n" "\n" " ## Returns\n" "\n" " - `Some(#(route, params))`: Matched route and extracted path parameters\n" " - `None`: No matching route found\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let app_router = router\n" " |> route(method: Get, path: \"/users/:id\", controller: show_user, middleware: [])\n" "\n" " case find_route(app_router, request) {\n" " Some(#(route, params)) -> {\n" " // route.controller is show_user\n" " // params is [#(\"id\", \"123\")] if path was \"/users/123\"\n" " }\n" " None -> // No route matched\n" " }\n" " ```\n" ). -spec find_route(router(AGHI, AGHJ), dream@http@request:request()) -> gleam@option:option({route(AGHI, AGHJ), list({binary(), binary()})}). find_route(Router_value, Request) -> Method_string = method_to_string(erlang:element(2, Request)), Path_segments = split_path(erlang:element(5, Request)), case dream@router@trie:lookup( erlang:element(2, Router_value), Method_string, Path_segments ) of {some, {match, Handler, Params}} -> Reversed_params = lists:reverse(Params), Remapped_params = remap_params_to_route_names( Reversed_params, erlang:element(5, Handler) ), Route = {route, erlang:element(2, Request), erlang:element(5, Request), erlang:element(2, Handler), erlang:element(3, Handler), erlang:element(4, Handler)}, {some, {Route, Remapped_params}}; none -> none end.