Walks a freshly generated %Ash.Info.Manifest{} once, at compile time, and
writes everything this library reads under custom.<namespace>.
This is the only module in lib/ that is meant to call Ash.Resource.Info
during decoration, and the only one that writes :custom.
AshIntrospection.Manifest.Custom reads it back. The point of the split is
that the request path stops introspecting: live introspection is not deleted
by adopting a manifest, it is relocated to one compile-time pass.
Why the Ash structs are carried, not rebuilt
A %Ash.Info.Manifest.Field{} is a client-facing description. It carries a
resolved %Ash.Info.Manifest.Type{} and has_default?, where this library's
readers return %Ash.Resource.Attribute{} and its callers read .type,
.constraints and .default — AshIntrospection.Codegen.ActionIntrospection
pattern-matches the struct itself
(lib/ash_introspection/codegen/action_introspection.ex:225), and
AshIntrospection.Codegen.ValidationErrorTypes.classify_action_input_errors/3
hands it back to the caller. Rebuilding one from a manifest field would be
lossy — default is not on the manifest at all — and would change a return
type consumers already depend on.
So the decorator captures the live struct. What it computes is the data
that is genuinely derived and genuinely repeated per request: resolved
aggregate types, each action's return classification, the
bulk-authorization strategy, client-facing field and argument names under
each built-in formatter and their reverse, how each :many relationship
paginates, and an entrypoint lookup keyed by client-facing name.
Changing those return types is a separate, breaking decision. It belongs to
the stage that deletes AshIntrospection.Codegen.TypeDiscovery, not to this
additive one. See docs/decisions.md.
What is written where
| Struct | Payload |
|---|---|
%Ash.Info.Manifest{} | entrypoint_lookup |
each %Manifest.Resource{} | fields, actions, aggregate types, return classifications, bulk strategy, field and argument names |
each %Manifest.Relationship{} on it | the read action it loads through, and how that action paginates |
each embedded %Manifest.Type{}'s nested resource | the same resource payload |
each %Manifest.Type{} with a field-names callback | field-name mappings |
each %Manifest.Entrypoint{} | client_name, from the :entrypoint_name callback |
Decoration depends on compile order, and says so
Every payload is read off a module atom the manifest names, so that module
has to be compiled when decorate/3 runs. Under parallel compilation it may
not be. Rather than write wrong data, this module skips a module it
cannot load — Code.ensure_loaded?/1 guards every such read, the repo-wide
rule from #49 — and the reader falls back to live Ash.Resource.Info for
anything undecorated. That is safe but silent, which is exactly the staleness
trap issue #23 records against a naive port of upstream's 8c07331.
The caller owns the compile edges. The transformer that calls this
function must force every referenced module to compile first, and must give
the manifest module a compile-time dependency on the domains it was built
from. Neither belongs here; both are stage 3 of #23. Nothing in this module
makes them harder — decorate/3 is a pure function of its arguments.
Namespace
decorate/3 takes the namespace as an argument rather than hard-coding one,
because whether generators share :ash_introspection or each decorate under
their own key is still open on #23. AshIntrospection.Manifest.Custom
defaults to :ash_introspection and so does this module.
Summary
Functions
Returns manifest with custom[namespace] populated on every struct this
library reads.
Types
@type config() :: %{ optional(:format_field_for_client) => (atom(), module() | nil, atom() -> String.t()), optional(:get_original_field_name) => (module(), String.t() -> atom() | nil), optional(:entrypoint_name) => (module(), atom() -> String.t() | nil), optional(:field_names_callback) => atom(), optional(:output_field_formatter) => atom() }
The slice of the pipeline/codegen config map the decorator reads.
These are the callbacks the consumer already threads through
AshIntrospection.Rpc.Pipeline; the decorator calls each once per field at
compile time instead of once per field per request.
Functions
@spec decorate(Ash.Info.Manifest.t(), atom(), config()) :: Ash.Info.Manifest.t()
Returns manifest with custom[namespace] populated on every struct this
library reads.
Pure: same manifest and config in, same manifest out. Call it once, at
compile time, on the result of Ash.Info.Manifest.generate/1.
config is the pipeline config map. Every key is optional; with an empty map
the decorator computes client names with
AshIntrospection.FieldFormatter.format_field_name/2, which is what the
pipeline does when the consumer supplies no callback.