RpcElixir.Types.FromSpec (elixir_ts_rpc v0.0.1)

Copy Markdown View Source

Reads classic @spec declarations from a compiled module's BEAM debug info (via Code.Typespec) and translates them into the internal %{kind: ...} representation used by handlers, routers, and codegen.

Users write @spec next to their RPC handlers. FromSpec reads them at runtime — no compile-time macro is required to capture AST.

Note: Code.Typespec is @moduledoc false in Elixir core. It has been stable in practice for many years and is consumed by ExDoc and dialyzer tooling, but the API is not officially committed.

See RpcElixir.Types.FromInferred for an experimental backend that 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.

Reads the spec for module.function/arity and 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 (%{source => target_custom_type}). The aliases are threaded into the Walker.Ctx so that codegen and runtime read the same frozen IR.

Functions

fetch_rpc(module, function)

@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, {:error, :no_spec} if the function has no @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, or {:error, {:invalid_return, return_ast}} if the return type lacks an {:ok, _} variant.

fetch_rpc(module, function, wire_aliases)

@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.

wire_aliases maps a source module to a RpcElixir.CustomType target (e.g. %{DateTime => RpcElixir.UnixMillis}) so the source's .t() crosses the wire as the target custom type. This is the form the router calls.

fetch_spec(module, function, arity)

@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 and returns {:ok, %{args: [input_type, ctx_ast, ...], return_ast: ast, local_types: map}}.

Only the first arg (the RPC input) is translated to the internal type representation — it is the sole arg in the wire contract. The remaining args (the server-side ctx) are returned as raw AST and never walked, so a handler may type ctx as a non-wire type. The return is likewise left as raw AST because handler-style returns ({:ok, T} | {:error, E}) must be decomposed by tag before walking — caller decides what to do with it.

Returns {:error, :no_spec} if no @spec is attached to the function, {:error, :module_not_found} if the module cannot be loaded, or {:error, {:invalid_spec_shape, ast}} if the @spec is not the expected single-clause fun(args) :: return shape (e.g. a multi-clause spec).

fetch_spec(module, function, arity, wire_aliases)

@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 (%{source => target_custom_type}). The aliases are threaded into the Walker.Ctx so that codegen and runtime read the same frozen IR.