Packages

Run WebAssembly from Elixir. Load WASM modules in Rust, Go, C — call them like native functions.

Current section

Files

Jump to
firebird lib firebird phoenix routes.ex
Raw

lib/firebird/phoenix/routes.ex

defmodule Firebird.Phoenix.Routes do
@moduledoc """
WASM-accelerated path and URL helper generation for Phoenix routes.
Generates URL paths and full URLs from route patterns with parameter
substitution, query string building, and URL encoding compiled to
WebAssembly.
Like Phoenix's `Routes.user_path(conn, :show, 42)`, but using WASM
for URL encoding and path generation.
## Features
- Path generation from route patterns with `:param` substitution
- Full URL generation with scheme, host, and port
- Extra parameters become query string (`?key=value`)
- WASM-accelerated URL encoding (RFC 3986 compliant)
- Batch path generation for multiple routes in a single WASM call
- URL encoding utilities for path segments and query values
## Examples
# Generate a path from a pattern
{:ok, path} = Routes.path(instance, "/users/:id", %{"id" => "42"})
# => {:ok, "/users/42"}
# Extra params become query string
{:ok, path} = Routes.path(instance, "/users/:id", %{"id" => "42", "tab" => "posts"})
# => {:ok, "/users/42?tab=posts"}
# Generate a full URL
{:ok, url} = Routes.url(instance, "/users/:id", %{"id" => "42"},
scheme: "https", host: "example.com")
# => {:ok, "https://example.com/users/42"}
# URL-encode a path segment
{:ok, encoded} = Routes.url_encode(instance, "hello world/foo")
# => {:ok, "hello%20world/foo"}
"""
alias Firebird.Phoenix.FastHelper
@doc """
Generate a URL path from a route pattern and parameters.
Path parameters (`:name`) in the pattern are replaced with the
corresponding parameter values. Any parameters not used in the
pattern are appended as a query string.
All parameter values are URL-encoded for safety.
## Parameters
- `instance` - WASM router instance PID
- `pattern` - Route pattern with `:param` segments (e.g., "/users/:id")
- `params` - Map of parameter names to values
## Returns
- `{:ok, path}` - Generated URL path
- `{:error, reason}` - Generation failed
## Examples
{:ok, "/users/42"} = Routes.path(instance, "/users/:id", %{"id" => "42"})
{:ok, "/search?q=elixir"} = Routes.path(instance, "/search", %{"q" => "elixir"})
{:ok, "/users/42?page=1"} = Routes.path(instance, "/users/:id", %{"id" => "42", "page" => "1"})
"""
@spec path(pid() | reference(), String.t(), map()) :: {:ok, String.t()} | {:error, term()}
def path(instance, pattern, params \\ %{})
when is_binary(pattern) and is_map(params) do
input = build_path_input(pattern, params)
case FastHelper.call_string(instance, "generate_path", input) do
{:ok, <<"error|", reason::binary>>} -> {:error, reason}
{:ok, path} -> {:ok, path}
{:error, reason} -> {:error, reason}
end
end
@doc """
Generate a full URL from a route pattern, parameters, and URL options.
Combines scheme, host, optional port, and the generated path into
a complete URL string. Path generation and URL encoding are done
in WASM; URL assembly is done in Elixir using IO lists.
## Parameters
- `instance` - WASM router instance PID
- `pattern` - Route pattern with `:param` segments
- `params` - Map of parameter names to values
- `opts` - URL options:
- `:scheme` - URL scheme (default: `"http"`)
- `:host` - Hostname (default: `"localhost"`)
- `:port` - Port number (default: `nil` — omitted for 80/443)
## Returns
- `{:ok, url}` - Generated full URL
- `{:error, reason}` - Generation failed
## Examples
{:ok, url} = Routes.url(instance, "/users/:id", %{"id" => "42"},
scheme: "https", host: "example.com")
# => {:ok, "https://example.com/users/42"}
{:ok, url} = Routes.url(instance, "/api/items", %{},
scheme: "http", host: "localhost", port: 4000)
# => {:ok, "http://localhost:4000/api/items"}
"""
@spec url(pid() | reference(), String.t(), map(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def url(instance, pattern, params \\ %{}, opts \\ []) do
scheme = Keyword.get(opts, :scheme, "http")
host = Keyword.get(opts, :host, "localhost")
port = Keyword.get(opts, :port)
case path(instance, pattern, params) do
{:ok, generated_path} ->
url_string =
IO.iodata_to_binary([
scheme,
"://",
host,
port_string(scheme, port),
generated_path
])
{:ok, url_string}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Batch generate multiple paths in a single WASM call.
Amortizes the per-call overhead across all path generations.
Each entry is a `{pattern, params}` tuple.
## Parameters
- `instance` - WASM router instance PID
- `entries` - List of `{pattern, params}` tuples
## Returns
- `{:ok, [paths]}` - List of generated paths (same order)
- `{:error, reason}` - Batch generation failed
## Examples
{:ok, paths} = Routes.path_batch(instance, [
{"/users/:id", %{"id" => "1"}},
{"/users/:id", %{"id" => "2"}},
{"/posts/:slug", %{"slug" => "hello-world"}}
])
# => {:ok, ["/users/1", "/users/2", "/posts/hello-world"]}
"""
@spec path_batch(pid() | reference(), list({String.t(), map()})) ::
{:ok, list(String.t())} | {:error, term()}
def path_batch(instance, entries) when is_list(entries) do
input =
entries
|> Enum.map(fn {pattern, params} -> build_path_input(pattern, params) end)
|> Enum.intersperse(<<0>>)
|> IO.iodata_to_binary()
case FastHelper.call_string(instance, "generate_path_batch", input) do
{:ok, <<"error|", reason::binary>>} ->
{:error, reason}
{:ok, result} ->
{:ok, :binary.split(result, <<0>>, [:global])}
{:error, reason} ->
{:error, reason}
end
end
@doc """
URL-encode a string for use in URL path segments.
Uses WASM for RFC 3986 compliant percent-encoding. Preserves
path-safe characters (unreserved + sub-delimiters + `/` + `@`).
## Examples
{:ok, "hello%20world"} = Routes.url_encode(instance, "hello world")
{:ok, "/users/caf%C3%A9"} = Routes.url_encode(instance, "/users/café")
"""
@spec url_encode(pid() | reference(), String.t()) :: {:ok, String.t()} | {:error, term()}
def url_encode(instance, text) when is_binary(text) do
FastHelper.call_string(instance, "url_encode", text)
end
@doc """
URL-encode a string for use in query string values.
Uses stricter encoding than `url_encode/2` — only unreserved
characters are left unencoded. Spaces become `+`.
## Examples
{:ok, "hello+world"} = Routes.url_encode_query(instance, "hello world")
{:ok, "a%26b%3Dc"} = Routes.url_encode_query(instance, "a&b=c")
"""
@spec url_encode_query(pid() | reference(), String.t()) :: {:ok, String.t()} | {:error, term()}
def url_encode_query(instance, text) when is_binary(text) do
FastHelper.call_string(instance, "url_encode_query", text)
end
@doc """
Build a route helper map from a list of named route definitions.
Returns a map of route names to their patterns, enabling lookup
by name for path generation.
## Parameters
- `routes` - List of `{name, method, pattern, handler}` tuples
## Returns
A map of `%{name => %{method: method, pattern: pattern, handler: handler}}`
## Examples
helpers = Routes.build_helpers([
{:user_index, "GET", "/users", "UserController.index"},
{:user_show, "GET", "/users/:id", "UserController.show"},
{:user_create, "POST", "/users", "UserController.create"}
])
helpers.user_show.pattern # => "/users/:id"
"""
@spec build_helpers(list({atom(), String.t(), String.t(), String.t()})) :: map()
def build_helpers(routes) when is_list(routes) do
Map.new(routes, fn {name, method, pattern, handler} ->
{name, %{method: method, pattern: pattern, handler: handler}}
end)
end
@doc """
Generate a path using a named route helper map.
Looks up the route by name, then generates the path with WASM
URL encoding.
## Parameters
- `instance` - WASM router instance PID
- `helpers` - Route helper map from `build_helpers/1`
- `name` - Route name atom
- `params` - Map of parameter names to values
## Returns
- `{:ok, path}` - Generated URL path
- `{:error, :unknown_route}` - Route name not found
- `{:error, reason}` - Generation failed
## Examples
helpers = Routes.build_helpers([
{:user_show, "GET", "/users/:id", "UserController.show"}
])
{:ok, "/users/42"} = Routes.named_path(instance, helpers, :user_show, %{"id" => "42"})
"""
@spec named_path(pid() | reference(), map(), atom(), map()) ::
{:ok, String.t()} | {:error, term()}
def named_path(instance, helpers, name, params \\ %{}) when is_atom(name) and is_map(params) do
case Map.get(helpers, name) do
nil -> {:error, :unknown_route}
%{pattern: pattern} -> path(instance, pattern, params)
end
end
@doc """
Generate a full URL using a named route helper map.
## Examples
{:ok, url} = Routes.named_url(instance, helpers, :user_show, %{"id" => "42"},
scheme: "https", host: "example.com")
"""
@spec named_url(pid() | reference(), map(), atom(), map(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def named_url(instance, helpers, name, params \\ %{}, opts \\ [])
when is_atom(name) and is_map(params) do
case Map.get(helpers, name) do
nil -> {:error, :unknown_route}
%{pattern: pattern} -> url(instance, pattern, params, opts)
end
end
# ── Private ──────────────────────────────────────────────────
# Build the WASM input for generate_path: "pattern\nkey=value\nkey=value"
defp build_path_input(pattern, params) when map_size(params) == 0 do
pattern
end
defp build_path_input(pattern, params) do
param_lines =
Enum.map(params, fn {key, value} ->
["\n", to_string(key), "=", to_string(value)]
end)
IO.iodata_to_binary([pattern | param_lines])
end
# Format port string, omitting default ports
defp port_string("http", nil), do: ""
defp port_string("https", nil), do: ""
defp port_string("http", 80), do: ""
defp port_string("https", 443), do: ""
defp port_string(_, nil), do: ""
defp port_string(_, port) when is_integer(port), do: [?:, Integer.to_string(port)]
end