Wymcp.CallbackSurface (Wymcp v0.9.1)

View Source

The callback-surface invariant, and the one check that holds its required half for every module a mount wires in.

Every callback a wymcp behaviour declares optional is one the framework probes before calling, so its absence is a value; every other declared callback is required and verified when the module is wired in. That rule is the callback-surface invariant, and it holds across every wymcp behaviour: Wymcp.Tool, whose optional callbacks are probed where they are called, and Wymcp.Auth, which satisfies it by declaring nothing optional.

Three clauses, each with its own enforcement:

  • optional implies guarded — Wymcp.CallbackSurfaceInvariantTest's optional column sweeps the framework's entry points with a tool that defines no optional callback at all, and the same test holds Wymcp.Auth's optional set empty.
  • required implies verified — validate!/3 runs at Wymcp.Router.init/1 for every module a mount wires in, :tools and :auth alike, and at a use Wymcp.Tool module's own compile. The check verifies what a declaration promises, not who declared it: a module exporting every required callback passes whether or not it declares the behaviour, and the compiler separately warns a module that declares the behaviour and misses one.
  • called implies declared — enforced by nothing automatic. Both the check and the invariant test derive from behaviour_info/1, so a call site whose callback was never declared is invisible to them. Closing that would need static analysis of lib/ for module.<fun>() call sites, out of proportion to the risk; the gap is recorded here rather than hidden.

The check

The required set is derived — behaviour_info(:callbacks) -- behaviour_info(:optional_callbacks) — never listed, so a callback added to a behaviour joins the check by being declared.

It runs ahead of everything else that reads a wired-in module, because everything else calls into it: Wymcp.Tool.validate_callback_shape!/1 calls name/0 and actions/0, so a module missing either would otherwise raise UndefinedFunctionError instead of a configuration refusal naming the fix.

Reaching the module uses Code.ensure_compiled/1, which waits: at a mount module's compile the modules it wires in are in the same parallel-compiler run and have no .beam yet, so a non-waiting load would refuse every real mount. The compiler's {:error, _} reply is not diagnostic — it reports the same :unavailable for a misspelled name, for a module that depends on the mount module naming it, and for one waiting on something it cannot get: a struct defined nowhere, or a compile-time cycle the mount module is not part of. In that last case the load refusal is the only error the build prints, so it masks the compiler's own report. That is why the load refusal asserts no cause outright and hands the author a step instead — take the module out of the option for one compile — whose outcome tells the causes apart: a failing compile prints the error the load refusal masked. Do not split it by reason.

Both configuration refusals name the module, the behaviour and the wiring site the caller passes, because their reader is the author who wired the module in, and the place they wrote it is the next thing to open. Neither claims what a behaviour's macro supplies: a check every behaviour shares cannot know.

Summary

Functions

Verifies that module loads and exports every required callback of behaviour, returning :ok. Raises Wymcp.ConfigurationError otherwise, naming module, behaviour and site — the wiring site, worded by the caller — and carrying module, with no callback: neither configuration refusal has a definition to point at.

Functions

validate!(module, behaviour, site)

Verifies that module loads and exports every required callback of behaviour, returning :ok. Raises Wymcp.ConfigurationError otherwise, naming module, behaviour and site — the wiring site, worded by the caller — and carrying module, with no callback: neither configuration refusal has a definition to point at.

Examples

iex> site = "the :auth option of Wymcp.Router"
iex> Wymcp.CallbackSurface.validate!(Wymcp.Auth.Noop, Wymcp.Auth, site)
:ok