RpcElixir.Router (elixir_ts_rpc v0.0.2)

Copy Markdown View Source

Macro-based DSL for defining RPC procedures in a router module. See Getting started for the surrounding setup.

Usage

expose/2 publishes a whole handler module: every public, spec'd, arity-2 function becomes a procedure named after the function. That is the default way to register an API surface.

defmodule MyApp.Router do
  use RpcElixir.Router

  scope "users" do
    expose Hello.Users   # → "users.get", "users.update", ...
  end

  scope "accounts", middleware: [MyApp.Middleware.RequireUser] do
    expose Hello.Accounts
  end
end

procedure/2 and procedure/3 are the manual alternative. They name one procedure at a time from a remote arity-2 capture, for when the wire name must differ from the function name, when only part of a module should be reachable, or when one function needs its own middleware.

defmodule MyApp.Router do
  use RpcElixir.Router

  procedure "users.get",    &Hello.Users.get/2
  procedure "users.update", &Hello.Users.update/2, middleware: [MyApp.Middleware.RequireUser]
end

Scoping

scope/2 and scope/3 share a prefix, middleware, or both across a group. Both registration forms nest inside them and mix freely, as long as no two procedures claim the same name.

scope "users", middleware: [MyApp.Middleware.RequireUser] do
  expose Hello.Users                    # → "users.get", "users.list", ...
  procedure "search", &Hello.Search.users/2   # → "users.search"
end

Wire aliases

use RpcElixir.Router, wire_aliases: [{DateTime, RpcElixir.UnixMillis}]

The wire_aliases option maps a source type's .t() to a RpcElixir.CustomType module project-wide. See Custom types.

Generated functions

  • __procedures__/0 — a list of maps with keys name, handler_mod, handler_fun, input, output, error, middleware, doc, schema_base. input and output are internal IR maps from RpcElixir.Types.FromSpec. error is one too, or nil when the spec declares no {:error, _} arm.

  • __manifest__/0 — the same list with the :middleware key removed, for exporting a router shape without server-internal detail. Codegen does not use it: it needs :middleware to fold in middleware error codes, so it reads __procedures__/0.

  • __procedures_index__/0 — a %{name => procedure} map for O(1) dispatch lookup. Same procedure maps as __procedures__/0, keyed by :name.

Compile-time guarantees

Every registration, whether from expose or procedure, is validated at compile time:

  • A procedure capture must be a remote function capture of arity 2.
  • An exposed module must use RpcElixir.Handler and must have at least one public, spec'd, arity-2 function.
  • The handler module must be compilable via Code.ensure_compiled!/1.
  • Every registered function must carry a @spec in the RPC convention (input, ctx) :: {:ok, output} | {:error, error}.
  • Procedure names must be unique within a router.

Violations raise CompileError at the offending call site.

Summary

Functions

Registers every public, @spec'd, arity-2 function of a handler module as a procedure. Each procedure takes the name of its function. This is the default way to register an API surface.

Registers one procedure from a wire name and a remote arity-2 capture.

Groups the procedure calls in the block. They share a prefix, middleware, or both. Declare a cross-cutting concern once, not per procedure.

Returns the absolute paths of the router's own source file and every handler module's source file, deduplicated.

Functions

expose(handler_ast, opts \\ [])

(macro)

Registers every public, @spec'd, arity-2 function of a handler module as a procedure. Each procedure takes the name of its function. This is the default way to register an API surface.

scope "counter", middleware: [RequireUser] do
  expose Hello.Counter   # → "counter.get", "counter.adjust", ...
end

The module is the API surface. Adding a spec'd arity-2 function to it publishes a procedure, with no router edit. Helpers left as defp, or without a @spec, stay unpublished.

The module must use RpcElixir.Handler, or expose raises. A module with no public, spec'd, arity-2 function raises too. The wire name joins any enclosing scope/2 prefix. Scope middleware and the :middleware option apply as for procedure/3, to every function in the module. Every exposed function must follow the RPC contract (input, ctx) :: {:ok, _} | {:error, _}. A function that does not raises CompileError, the same as a hand-written procedure.

See procedure/3 for the manual alternative, and for the cases that call for it. The two mix freely in one router.

procedure(name, capture_ast, opts \\ [])

(macro)

Registers one procedure from a wire name and a remote arity-2 capture.

procedure "users.get", &Hello.Users.get/2

This is the manual alternative to expose/2. Reach for it when the wire name must differ from the function name, when only part of a module should be reachable, when one function needs its own middleware, or when the published surface has to be auditable line by line. Otherwise prefer expose/2 and let the handler module define the surface.

The capture gives editors go-to-definition and pins the arity at the call site. A local capture, another arity, or a non-capture raises CompileError.

The :middleware option adds middleware for this procedure alone. Each entry is a module or a {module, opts} tuple. See RpcElixir.Middleware.

procedure "users.update", &Hello.Users.update/2, middleware: [RequireUser]

Inside scope/2 the scope prefix and middleware also apply. Every call is checked at compile time. See "Compile-time guarantees".

scope(prefix_or_opts, list)

(macro)

Groups the procedure calls in the block. They share a prefix, middleware, or both. Declare a cross-cutting concern once, not per procedure.

A string prefix prepends a dotted segment to each inner wire name. Scope middleware runs before procedure-specific middleware. Outer-most auth therefore runs first. Scopes nest: prefixes concatenate and middleware accumulates outer-to-inner.

scope "users", middleware: [RequireUser] do
  procedure "list", &Users.list/2   # → "users.list", middleware: [RequireUser]
  procedure "get", &Users.get/2     # → "users.get",  middleware: [RequireUser]
end

The prefix is optional. scope middleware: [RequireUser] do ... end shares middleware without renaming. Procedures outside any scope are unaffected.

scope(prefix, opts, list)

(macro)

source_files(router)

@spec source_files(module()) :: [String.t()]

Returns the absolute paths of the router's own source file and every handler module's source file, deduplicated.