RpcElixir.Types.FromSpec (elixir_ts_rpc v0.0.2)

Copy Markdown View Source

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

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

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

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

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 — a %{source => target_custom_type} map. Aliases are threaded into the Walker.Ctx, so codegen and runtime read the same frozen IR.