Reads classic @spec declarations from a compiled module's BEAM debug info,
via Code.Typespec. Translates them into the internal %{kind: ...} IR used
by handlers, routers, and codegen.
You write @spec next to your RPC handlers. FromSpec reads them at runtime.
No compile-time macro is needed to capture AST.
Note: Code.Typespec is @moduledoc false in Elixir core, so the API is not
officially committed. In practice it has been stable for many years. ExDoc
and dialyzer tooling consume it.
RpcElixir.Types.FromInferred is an experimental backend. It reads Elixir's
set-theoretic inferred signatures instead.
Summary
Functions
Convenience for the RPC convention call(input, context) :: {:ok, output} | {:error, error}.
Same as fetch_rpc/2, applying wire_aliases while resolving types. The map
is %{source => target_custom_type}, e.g. %{DateTime => RpcElixir.UnixMillis}.
The source's .t() then crosses the wire as that custom type. This is the
form the router calls.
Reads the spec for module.function/arity. Returns
{:ok, %{args: [input_type, ctx_ast, ...], return_ast: ast, local_types: map}}.
Like fetch_spec/3, but resolves source-module .t() calls through
wire_aliases — a %{source => target_custom_type} map. Aliases are threaded
into the Walker.Ctx, so codegen and runtime read the same frozen IR.
Functions
@spec fetch_rpc(module(), atom()) :: {:ok, %{ input: RpcElixir.Types.internal_spec(), output: RpcElixir.Types.internal_spec(), error: RpcElixir.Types.internal_spec() | nil }} | {:error, :no_spec} | {:error, :module_not_found} | {:error, {:invalid_spec_shape, term()}} | {:error, {:invalid_return, term()}}
Convenience for the RPC convention call(input, context) :: {:ok, output} | {:error, error}.
Returns {:ok, %{input: t, output: t, error: t | nil}} on success. Otherwise:
{:error, :no_spec} without a @spec. {:error, :module_not_found} if the
module cannot be loaded. {:error, {:invalid_spec_shape, ast}} if the @spec
is not a single-clause fun(args) :: return.
{:error, {:invalid_return, return_ast}} if the return type lacks an
{:ok, _} variant.
@spec fetch_rpc(module(), atom(), map()) :: {:ok, %{ input: RpcElixir.Types.internal_spec(), output: RpcElixir.Types.internal_spec(), error: RpcElixir.Types.internal_spec() | nil }} | {:error, :no_spec} | {:error, :module_not_found} | {:error, {:invalid_spec_shape, term()}} | {:error, {:invalid_return, term()}}
Same as fetch_rpc/2, applying wire_aliases while resolving types. The map
is %{source => target_custom_type}, e.g. %{DateTime => RpcElixir.UnixMillis}.
The source's .t() then crosses the wire as that custom type. This is the
form the router calls.
@spec fetch_spec(module(), atom(), non_neg_integer()) :: {:ok, %{ args: [RpcElixir.Types.internal_spec() | Macro.t()], return_ast: term(), local_types: map() }} | {:error, :no_spec} | {:error, :module_not_found} | {:error, {:invalid_spec_shape, term()}}
Reads the spec for module.function/arity. Returns
{:ok, %{args: [input_type, ctx_ast, ...], return_ast: ast, local_types: map}}.
Only the first arg is translated: the RPC input, the sole arg in the wire
contract. The remaining ctx args come back as raw AST. They are never
walked, so a handler may type ctx as a non-wire type. The return is raw AST
too. Handler-style returns ({:ok, T} | {:error, E}) must be decomposed by
tag before walking, so the caller decides.
Returns {:error, :no_spec} without a @spec. Returns
{:error, :module_not_found} if the module cannot be loaded. Returns
{:error, {:invalid_spec_shape, ast}} if the @spec is not the expected
single-clause fun(args) :: return shape or its when-bounded form.
A multi-clause @spec is not rejected; only one clause is kept.
@spec fetch_spec(module(), atom(), non_neg_integer(), map()) :: {:ok, %{ args: [RpcElixir.Types.internal_spec() | Macro.t()], return_ast: term(), local_types: map() }} | {:error, :no_spec} | {:error, :module_not_found} | {:error, {:invalid_spec_shape, term()}}
Like fetch_spec/3, but resolves source-module .t() calls through
wire_aliases — a %{source => target_custom_type} map. Aliases are threaded
into the Walker.Ctx, so codegen and runtime read the same frozen IR.