Descripex
Self-describing API declarations for Elixir. Define a function's documentation, machine-readable hints metadata, and runtime introspection with a single api() macro call — no separate @doc blocks needed.
Installation
Add descripex to your dependencies:
def deps do
[
{:descripex, "~> 0.6"}
]
endUsage
defmodule MyLib.Funding do
use Descripex, namespace: "/funding"
api(:annualize, "Annualize a per-period funding rate to APR.",
params: [
rate: [kind: :value, description: "Per-period funding rate as decimal", schema: float()],
period_hours: [kind: :value, default: 8, description: "Hours per period", schema: pos_integer()]
],
returns: %{type: :float, description: "Annualized percentage rate", schema: float()},
returns_example: {:ok, %{apr: 10.95}}
)
@spec annualize(number(), pos_integer()) :: float()
def annualize(rate, period_hours \\ 8) do
rate * (365 * 24 / period_hours) * 100
end
end
The api macro generates:
@doc— human-readable documentation from the description and params@doc hints:— machine-readable metadata for agent consumption__api__/0and__api__/1— runtime introspection functionsschema:— Elixir type syntax compiled to JSON Schema via json_spec at compile time (zero runtime cost)
api/3 option highlights
returnsdefines return shape and human summaryreturns_exampleadds a concrete example rendered in docs and included in@doc hints:schemaon params/opts/returns compiles Elixir type syntax (e.g.,float(),[String.t()],:buy | :sell) to JSON Schema at compile timecomposes_withdeclares intra-module composition relationships (e.g.,[:normalize, :persist])
Manual @doc Coexistence
api() writes to two independent slots in the BEAM docs chunk: doc text (slot 4) and hints metadata (slot 5). You can write a manual @docafterapi() to provide custom prose while keeping the structured metadata:
api(:imbalance!, "Calculate orderbook imbalance (raises on error).",
params: [orderbook: [kind: :exchange_data, description: "Orderbook data"]],
returns: %{type: :float, description: "Imbalance ratio"}
)
@doc "Bang variant of `imbalance/2`. Returns the float directly or raises on error."
def imbalance!(orderbook, depth \\ 10), do: ...
The function gets both the custom @doc text and the full machine-readable hints contract.
Compile-Time Validation
Descripex validates declarations at compile time:
-
Every
api(:name, ...)must have a matchingdef name(...) - Declared param names must match actual function argument names by position
-
Mismatches raise
CompileErrorbefore your code ever runs
ExDoc Compatibility
Descripex automatically escapes { and } in description strings when generating @doc text. This prevents ExDoc's Earmark parser from misinterpreting Elixir-style return types (e.g., {:ok, %{current, history}}) as Inline Attribute Lists.
The raw (unescaped) descriptions are preserved in @doc hints: metadata — only the human-readable @doc text is escaped.
Progressive Disclosure
Discover a library's API incrementally — from overview to function detail:
# Make your library discoverable
defmodule MyLib do
use Descripex.Discoverable, modules: [MyLib.Funding, MyLib.Risk]
end
MyLib.describe() # Level 1: library overview
MyLib.describe(:funding) # Level 2: module functions
MyLib.describe(:funding, :annualize) # Level 3: function detail
Short names are derived from the last module segment (e.g., MyLib.Funding → :funding). Full module atoms also work. Non-Descripex modules are included with basic function listings.
Or use the functional API directly:
modules = [MyLib.Funding, MyLib.Risk]
Descripex.Describe.describe(modules)
Descripex.Describe.describe(modules, :funding, :annualize)Manifest
Build a JSON-serializable manifest from all declared modules:
Descripex.Manifest.build([MyLib.Funding, MyLib.Risk])JSON Schema
Add schema: to params, opts, or returns to compile Elixir type syntax into JSON Schema at compile time:
params: [
side: [kind: :value, description: "Trade side", schema: :buy | :sell],
prices: [kind: :value, description: "Price list", schema: [float()]]
],
returns: %{type: :map, description: "Result", schema: %{score: float(), tags: [String.t()]}}
Conversion is handled by json_spec at compile time; the resulting JSON Schema map lands in hints.params.*.schema and hints.returns.schema. Params without schema: are unaffected.
MCP Tool Generation
Convert annotated modules into MCP tool definitions:
Descripex.MCP.tools([MyLib.Funding, MyLib.Risk])
# => [%{name: "funding__annualize", description: "...", inputSchema: %{...}}, ...]
Each api()-annotated function becomes a tool with name, description, and inputSchema. Params with schema: get typed JSON Schema properties; params without get description-only properties. Pass name_style: :full for fully-qualified tool names.
Static JSON Export
Export the manifest as JSON to disk:
mix descripex.manifest MyApp.Funding MyApp.Risk
mix descripex.manifest --app my_app # auto-discover annotated modules
mix descripex.manifest --pretty --output priv/manifest.json MyApp.Funding
Requires jason ~> 1.4 as a dev dependency in your project. Modules can also be configured via config :descripex, manifest_modules: [...].
Dogfooding
Descripex describes itself. The library's own modules use api() declarations and Discoverable:
Descripex.describe() # Overview of Manifest, Describe, and MCP
Descripex.describe(:mcp) # Functions in MCP
Descripex.describe(:mcp, :tools) # Full detail for tools/1Documentation
Full documentation is available on HexDocs.
Quality Gates
Run mix doctor as part of local/CI checks. This project enforces 100% @doc, @spec, and @moduledoc coverage.
License
MIT