Reads the data AshIntrospection.Manifest.Decorator wrote under
custom.<namespace>.
Ash.Info.Manifest leaves a custom map on every struct it builds so a
client library can hang its own precomputed data off the manifest. This
module is the only place in lib/ that reads it, and
AshIntrospection.Manifest.Decorator is the only place that writes it.
Nothing else pattern-matches :custom directly.
The namespace is a parameter, not a constant
Every function takes a trailing namespace defaulting to
:ash_introspection. Whether one shared decoration serves every generator
built on this core, or each generator decorates under its own key, is an
open question on issue #23 that this stage does not settle — so the code
answers both. See docs/decisions.md.
Absent decoration is not an empty decoration
decorated?/2 is the question every caller asks first.
AshIntrospection.ResourceInfo reads a decorated resource and falls back to
live Ash.Resource.Info for an undecorated one, so the two cases have to be
distinguishable: a resource whose decoration is missing is not a resource
with no attributes. Singular readers below return nil for "no such field"
only after decorated?/2 has answered true.
Summary
Types
The decoration map written on a %Ash.Info.Manifest.Entrypoint{}.
The decoration map written on the %Ash.Info.Manifest{} itself.
The decoration map written on a %Ash.Info.Manifest.Relationship{}.
The decoration map written on a %Ash.Info.Manifest.Resource{}.
The decoration map written on a %Ash.Info.Manifest.Type{}.
Functions
The action named name, or nil.
Every action, in Ash.Resource.Info.actions/1 order.
The aggregate named name, or nil.
The resolved type of aggregate, precomputed.
The %{argument_atom => client_name} map for one action, or %{}.
The attribute named name, or nil.
Every attribute, public and private, in Ash.Resource.Info.attributes/1 order.
The precomputed bulk-authorization strategy, :error or :filter.
The calculation named name, or nil.
Has struct been decorated under namespace?
The namespace used when a caller names none.
The entrypoint a client asked for by name, or nil.
The client-facing name decorated onto entrypoint, or nil.
The %{client_name => %Ash.Info.Manifest.Entrypoint{}} lookup, or %{}.
The %{field_atom => client_name} map for a decorated resource, or %{}.
The precomputed client-facing name for one argument under formatter, or
nil.
The precomputed client-facing name for field under formatter, or nil.
The client-facing name for one argument of one action, or nil.
The client-facing name for field, or nil when it has no mapping.
The argument atom behind a client-facing name on one action, or nil.
The field atom behind a client-facing name, or nil.
The decoration map on any struct that carries a custom field, or nil.
The aggregate named name if it is public, or nil.
The public aggregates, in Ash.Resource.Info.public_aggregates/1 order.
The attribute named name if it is public, or nil.
The public attributes, in Ash.Resource.Info.public_attributes/1 order.
The calculation named name if it is public, or nil.
The public calculations, in Ash.Resource.Info.public_calculations/1 order.
How the read behind a decorated :many relationship paginates.
The read action a decorated :many relationship loads through, or nil.
The precomputed return classification of action, or :undecorated.
The %{client_name => argument_atom} map for one action, or %{}.
The %{client_name => field_atom} map for a decorated resource, or %{}.
Types
@type entrypoint_payload() :: %{client_name: String.t() | nil}
The decoration map written on a %Ash.Info.Manifest.Entrypoint{}.
@type manifest_payload() :: %{ entrypoint_lookup: %{required(String.t()) => Ash.Info.Manifest.Entrypoint.t()} }
The decoration map written on the %Ash.Info.Manifest{} itself.
@type relationship_payload() :: %{ pagination: :offset | :keyset | :mixed | :none, read_action: atom() | nil }
The decoration map written on a %Ash.Info.Manifest.Relationship{}.
@type resource_payload() :: %{ attributes: [Ash.Resource.Attribute.t()], public_attributes: [Ash.Resource.Attribute.t()], public_calculations: [Ash.Resource.Calculation.t()], public_aggregates: [Ash.Resource.Aggregate.t()], actions: [Ash.Resource.Actions.action()], by_name: %{required(atom()) => %{required(atom() | String.t()) => struct()}}, aggregate_types: %{required(atom()) => term()}, return_classifications: %{required(atom()) => term()}, authorize_bulk_strategy: :error | :filter, field_name_mappings: %{required(atom()) => String.t()}, reverse_field_name_mappings: %{required(String.t()) => atom()}, formatted_field_names: %{required({atom(), atom()}) => String.t()}, argument_name_mappings: %{ required(atom()) => %{required(atom()) => String.t()} }, reverse_argument_name_mappings: %{ required(atom()) => %{required(String.t()) => atom()} }, formatted_argument_names: %{ required(atom()) => %{required({atom(), atom()}) => String.t()} } }
The decoration map written on a %Ash.Info.Manifest.Resource{}.
Attribute, calculation, aggregate and action entries are the live Ash
structs, captured at decoration time. See the decorator's moduledoc for why
they are not rebuilt from %Ash.Info.Manifest.Field{}.
@type type_payload() :: %{ field_name_mappings: %{required(atom()) => String.t()}, reverse_field_name_mappings: %{required(String.t()) => atom()} }
The decoration map written on a %Ash.Info.Manifest.Type{}.
Functions
@spec action(Ash.Info.Manifest.Resource.t() | nil, atom(), atom()) :: Ash.Resource.Actions.action() | nil
The action named name, or nil.
@spec actions(Ash.Info.Manifest.Resource.t() | nil, atom()) :: [ Ash.Resource.Actions.action() ]
Every action, in Ash.Resource.Info.actions/1 order.
@spec aggregate(Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom()) :: Ash.Resource.Aggregate.t() | nil
The aggregate named name, or nil.
@spec aggregate_type( Ash.Info.Manifest.Resource.t() | nil, Ash.Resource.Aggregate.t() | atom(), atom() ) :: term() | :undecorated
The resolved type of aggregate, precomputed.
Ash.Resource.Info.aggregate_type/2 resolves the aggregate's field type
through the relationship path on every call; the decorator does it once. The
return shape is that function's — an {:ok, type} or {:error, reason}
tuple — because every caller here passes it straight through.
Returns :undecorated when the aggregate has no precomputed entry, which is
the caller's signal to resolve live. nil is not usable as that signal: it
is a legitimate resolved value.
@spec argument_name_mappings(Ash.Info.Manifest.Resource.t() | nil, atom(), atom()) :: %{ required(atom()) => String.t() }
The %{argument_atom => client_name} map for one action, or %{}.
@spec attribute(Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom()) :: Ash.Resource.Attribute.t() | nil
The attribute named name, or nil.
name may be an atom or a string, matching Ash.Resource.Info.attribute/2,
which reads a persisted map carrying both key forms.
@spec attributes(Ash.Info.Manifest.Resource.t() | nil, atom()) :: [ Ash.Resource.Attribute.t() ]
Every attribute, public and private, in Ash.Resource.Info.attributes/1 order.
@spec authorize_bulk_strategy(Ash.Info.Manifest.Resource.t() | nil, atom()) :: :error | :filter | nil
The precomputed bulk-authorization strategy, :error or :filter.
Ash.DataLayer.data_layer_can?(resource, :expr_error) is fixed at compile
time, so the pipeline need not ask per request. nil when undecorated.
@spec calculation(Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom()) :: Ash.Resource.Calculation.t() | nil
The calculation named name, or nil.
Has struct been decorated under namespace?
Ask this before reading anything else. A false answer means the caller must
read live, not that the decorated value is empty.
@spec default_namespace() :: atom()
The namespace used when a caller names none.
@spec entrypoint(Ash.Info.Manifest.t() | nil, String.t() | atom(), atom()) :: Ash.Info.Manifest.Entrypoint.t() | nil
The entrypoint a client asked for by name, or nil.
@spec entrypoint_client_name(Ash.Info.Manifest.Entrypoint.t() | nil, atom()) :: String.t() | nil
The client-facing name decorated onto entrypoint, or nil.
@spec entrypoint_lookup(Ash.Info.Manifest.t() | nil, atom()) :: %{ required(String.t()) => Ash.Info.Manifest.Entrypoint.t() }
The %{client_name => %Ash.Info.Manifest.Entrypoint{}} lookup, or %{}.
One map read replaces a scan over every entrypoint in the application. The
key is the client-facing name the consumer's :entrypoint_name callback
returned, as a string.
@spec field_name_mappings( Ash.Info.Manifest.Resource.t() | Ash.Info.Manifest.Type.t() | nil, atom() ) :: %{required(atom()) => String.t()}
The %{field_atom => client_name} map for a decorated resource, or %{}.
@spec formatted_argument_name( Ash.Info.Manifest.Resource.t() | nil, atom(), atom(), atom(), atom() ) :: String.t() | nil
The precomputed client-facing name for one argument under formatter, or
nil.
Only the built-in formatters are precomputed, for the reason
formatted_field_name/4 gives.
@spec formatted_field_name( Ash.Info.Manifest.Resource.t() | nil, atom(), atom(), atom() ) :: String.t() | nil
The precomputed client-facing name for field under formatter, or nil.
Only the built-in formatters are precomputed — :camel_case,
:pascal_case, :snake_case. A {module, function} formatter is
consumer-supplied and runtime-configurable, so it is computed live.
@spec mapped_argument_name( Ash.Info.Manifest.Resource.t() | nil, atom(), atom(), atom() ) :: String.t() | nil
The client-facing name for one argument of one action, or nil.
@spec mapped_field_name( Ash.Info.Manifest.Resource.t() | Ash.Info.Manifest.Type.t() | nil, atom(), atom() ) :: String.t() | nil
The client-facing name for field, or nil when it has no mapping.
@spec original_argument_name( Ash.Info.Manifest.Resource.t() | nil, atom(), String.t() | atom(), atom() ) :: atom() | nil
The argument atom behind a client-facing name on one action, or nil.
@spec original_field_name( Ash.Info.Manifest.Resource.t() | Ash.Info.Manifest.Type.t() | nil, String.t() | atom(), atom() ) :: atom() | nil
The field atom behind a client-facing name, or nil.
This is the reverse of the consumer's :format_field_for_client callback,
computed once per field at decoration time instead of per request.
The decoration map on any struct that carries a custom field, or nil.
Works for %Ash.Info.Manifest{}, %Manifest.Resource{}, %Manifest.Type{},
%Manifest.Entrypoint{} and %Manifest.Action{} alike — they all carry the
same custom: %{} field.
@spec public_aggregate( Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom() ) :: Ash.Resource.Aggregate.t() | nil
The aggregate named name if it is public, or nil.
@spec public_aggregates(Ash.Info.Manifest.Resource.t() | nil, atom()) :: [ Ash.Resource.Aggregate.t() ]
The public aggregates, in Ash.Resource.Info.public_aggregates/1 order.
@spec public_attribute( Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom() ) :: Ash.Resource.Attribute.t() | nil
The attribute named name if it is public, or nil.
@spec public_attributes(Ash.Info.Manifest.Resource.t() | nil, atom()) :: [ Ash.Resource.Attribute.t() ]
The public attributes, in Ash.Resource.Info.public_attributes/1 order.
@spec public_calculation( Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom() ) :: Ash.Resource.Calculation.t() | nil
The calculation named name if it is public, or nil.
@spec public_calculations(Ash.Info.Manifest.Resource.t() | nil, atom()) :: [ Ash.Resource.Calculation.t() ]
The public calculations, in Ash.Resource.Info.public_calculations/1 order.
@spec relationship_pagination(Ash.Info.Manifest.Relationship.t() | nil, atom()) :: :offset | :keyset | :mixed | :none
How the read behind a decorated :many relationship paginates.
:none for an undecorated relationship, for every to-one relationship, and
for a read that offers neither pagination kind. Those three collapse on
purpose: a caller asking how to page a relationship it cannot page gets the
same answer either way.
@spec relationship_read_action(Ash.Info.Manifest.Relationship.t() | nil, atom()) :: atom() | nil
The read action a decorated :many relationship loads through, or nil.
nil for an undecorated relationship and for a to-one one.
@spec return_classification( Ash.Info.Manifest.Resource.t() | nil, Ash.Resource.Actions.action() | atom(), atom() ) :: term() | :undecorated
The precomputed return classification of action, or :undecorated.
The shape is
AshIntrospection.Codegen.ActionIntrospection.action_returns_field_selectable_type?/2's
— an {:ok, kind, data} or {:error, reason} tuple — because every
caller passes it through untouched. :undecorated is the signal to compute
live; {:error, _} cannot double as it, being a legitimate classification.
The classification is scoped by the manifest that was decorated:
classify_return_type/3 asks whether the struct an action returns is a
declared resource, and a manifest is what declares one. Decorating settles
that question at compile time rather than changing it.
@spec reverse_argument_name_mappings( Ash.Info.Manifest.Resource.t() | nil, atom(), atom() ) :: %{ required(String.t()) => atom() }
The %{client_name => argument_atom} map for one action, or %{}.
@spec reverse_field_name_mappings( Ash.Info.Manifest.Resource.t() | Ash.Info.Manifest.Type.t() | nil, atom() ) :: %{required(String.t()) => atom()}
The %{client_name => field_atom} map for a decorated resource, or %{}.