The primary key field names of resource.
Returns [] for a module the manifest does not carry, matching
Ash.Info.Manifest.primary_key/2.
The one place this library asks what a resource looks like.
Every Ash.Resource.Info call in lib/ goes through this module. That is
the whole point of it: issue #23 replaces live introspection with the
precomputed Ash.Info.Manifest upstream ash_typescript adopted, and a
migration spread across 64 call sites in nine modules is not reviewable. One
reader makes each later stage a small diff.
Reads take the config map the pipeline and codegen already thread, and look
for an optional :manifest key — the same shape :load_restrictions and
:is_interop_resource? use.
| Config | Source |
|---|---|
no :manifest key, or nil | live Ash.Resource.Info, exactly as before |
%Ash.Info.Manifest{} | the manifest, lookups rebuilt per read |
AshIntrospection.ResourceInfo.Source | the manifest, lookups built once |
Omitting the key preserves current behaviour exactly. That is the
compatibility guarantee of this stage and the reason it is reversible: no
caller in this repo passes :manifest yet, so every read is still live.
test/ash_introspection/resource_info_test.exs proves it against
Ash.Resource.Info directly rather than asserting it.
resource?/1 is two questions, not oneAsh.Resource.Info.resource?/1 answers a fact about a module.
Ash.Info.Manifest.has_resource?/2 answers a fact about the declared API
surface. They are not the same question, and 18 of the 64 sites ask it, so
this module refuses to guess which one a caller meant:
runtime_resource?/2 — manifest first, live Ash.Resource.Info when
the module is absent from it. For the request path, where the argument
can be a runtime value.__struct__ that Ash handed back. A module
nobody declared must still serialize as a resource; answering false
would silently drop it into the generic Map.from_struct/1 branch.
declared_resource?/2 — the manifest is the whole answer when one is
present; absence means false. For codegen, where scoping is the
point: a resource nobody exposed should not appear in generated output.
Both count embedded resources. Ash.Info.Manifest.Generator splits them out
of resources and re-enters them under types with
kind: :embedded_resource, so a bare has_resource?/2 would answer false
for every embedded resource — a divergence from live introspection that has
nothing to do with scoping. The only difference between the two functions is
what a missing module means.
Everything else defaults to the fallback reading. A site is given
declared_resource?/2 only when it is unambiguously codegen.
Every reader is manifest-backed, and they divide into two groups by what the manifest has to carry for them to be:
| Function | Answered from |
|---|---|
runtime_resource?/2, declared_resource?/2, embedded?/2 | the manifest as ash generates it |
primary_key/2, identity_keys/3 | the manifest as ash generates it |
relationship/3, public_relationship/3 | the manifest as ash generates it |
public_field_names/2 | the manifest as ash generates it |
every attribute, calculation, aggregate, action reader | custom.<namespace>, written by AshIntrospection.Manifest.Decorator |
aggregate_type/3, authorize_bulk_strategy/2 | custom.<namespace> |
The second group needs decoration because a generated manifest cannot answer
it. %Ash.Info.Manifest.Field{} is a client-facing description: it carries a
resolved %Ash.Info.Manifest.Type{} where callers here read
{type, constraints}, and has_default? where they read default. The
decorator captures the live struct once, at compile time, so the answer is
identical rather than approximated. test/ash_introspection/manifest/
proves that field for field.
A manifest is not a complete list of relationships.
Ash.Info.Manifest.Generator.generate/1 defaults
:include_private_relationships? to false, so relationship/3 falls back
to live on a miss — a private belongs_to is absent from the manifest and
present in Ash.Resource.Info. public_relationship/3 does not fall back:
every public relationship is carried, so a miss there is the answer.
An undecorated manifest still reads live for the second group. A resource the decorator skipped — a module it could not load at decoration time — is present in the manifest and bare, and is indistinguishable to these readers from a resource the manifest never carried. Both fall back.
Where the native return values differ, this module returns a narrow map with
only the keys its call sites read, and both sources build it. relationship/3
is the case: live returns %Ash.Resource.Relationships.HasOne{} and friends,
the manifest returns %Ash.Info.Manifest.Relationship{}. Callers here read
:destination and :cardinality and nothing else, so that is what comes
back. identity_keys/3 is the same narrowing over %Ash.Resource.Identity{}
versus the manifest's %{keys: [...]}.
:resource_info_moduleThe :resource_info_module config key names the consumer's generated
Info module, used for interop_resource?/1 and get_original_field_name/2.
It is unrelated to this module and to :manifest.
The slice of the pipeline/codegen config map this module reads.
A relationship narrowed to the keys this library reads.
Whether a bulk update or destroy on resource should authorize with
:error or :filter.
Is module part of the declared API surface?
The decorated %Ash.Info.Manifest.Resource{} for resource and the
namespace it was decorated under, or nil to read live.
Is module an embedded resource?
The key names of the identity identity_name on resource, or nil.
Normalizes the :manifest key on a config map with prepare/2.
Builds the lookup maps for a manifest once, so reads do not rebuild them.
The primary key field names of resource.
The names of every public field on resource — attributes, calculations and
aggregates together.
The public relationship name on resource, narrowed like relationship/3.
The relationship name on resource, narrowed to :name, :destination
and :cardinality, or nil.
How the read behind the :many relationship name paginates.
The read action the :many relationship name loads through, or nil.
Is module a resource, falling back to live introspection when the manifest
does not know it?
Returns the prepared manifest source on config, or nil for live reads.
@type config() :: %{ optional(:manifest) => Ash.Info.Manifest.t() | AshIntrospection.ResourceInfo.Source.t() | nil }
The slice of the pipeline/codegen config map this module reads.
Every other key is ignored.
A relationship narrowed to the keys this library reads.
@spec action(module(), atom(), config()) :: Ash.Resource.Actions.action() | nil
@spec actions(module(), config()) :: [Ash.Resource.Actions.action()]
@spec aggregate(module(), atom() | String.t(), config()) :: Ash.Resource.Aggregate.t() | nil
@spec aggregate_type(module(), Ash.Resource.Aggregate.t(), config()) :: term()
See Ash.Resource.Info.aggregate_type/2.
The decorator resolves this once per aggregate at compile time; live, it
walks the relationship path to the aggregated field on every call. The
{:ok, type} / {:error, reason} return shape is that function's and is
passed through unchanged.
@spec attribute(module(), atom() | String.t(), config()) :: Ash.Resource.Attribute.t() | nil
@spec attributes(module(), config()) :: [Ash.Resource.Attribute.t()]
Whether a bulk update or destroy on resource should authorize with
:error or :filter.
:error when the data layer can express errors in expressions, :filter
otherwise — Ash.DataLayer.data_layer_can?(resource, :expr_error). A data
layer is fixed at compile time, so the decorator answers this once.
@spec calculation(module(), atom() | String.t(), config()) :: Ash.Resource.Calculation.t() | nil
Is module part of the declared API surface?
With a manifest, absence is the answer: a module nobody exposed is not a
resource here. Without one, this is Ash.Resource.Info.resource?/1. For
codegen only.
@spec decoration(module(), config()) :: {Ash.Info.Manifest.Resource.t(), atom()} | nil
The decorated %Ash.Info.Manifest.Resource{} for resource and the
namespace it was decorated under, or nil to read live.
For the modules that read decorated data this one does not wrap —
AshIntrospection.Codegen.ActionIntrospection and its return
classification. They still go through AshIntrospection.Manifest.Custom to
read it; this only finds the struct.
nil covers three cases a caller treats identically: no manifest, a manifest
that does not carry the module, and a manifest that carries it undecorated.
Is module an embedded resource?
Manifest first, live when the module is absent from it — embedded resources
live in manifest.types with kind: :embedded_resource, not in
manifest.resources.
The key names of the identity identity_name on resource, or nil.
A narrowing: live introspection returns %Ash.Resource.Identity{} and the
manifest returns %{keys: [...]}. Every caller here reads :keys.
Normalizes the :manifest key on a config map with prepare/2.
Call it once at an entry point rather than preparing on every read. A config
with no :manifest key is returned untouched. An optional
:manifest_namespace key names the decoration namespace.
@spec prepare( Ash.Info.Manifest.t() | AshIntrospection.ResourceInfo.Source.t() | nil, atom() | nil ) :: AshIntrospection.ResourceInfo.Source.t() | nil
Builds the lookup maps for a manifest once, so reads do not rebuild them.
Accepts a %Ash.Info.Manifest{}, an already prepared
AshIntrospection.ResourceInfo.Source, or nil. Returns nil unchanged, so
it is safe to call on a config value that may be absent.
namespace names the custom key AshIntrospection.Manifest.Decorator
decorated under. nil keeps a prepared source's own namespace and gives a
bare manifest the default.
The primary key field names of resource.
Returns [] for a module the manifest does not carry, matching
Ash.Info.Manifest.primary_key/2.
@spec public_aggregate(module(), atom() | String.t(), config()) :: Ash.Resource.Aggregate.t() | nil
@spec public_aggregates(module(), config()) :: [Ash.Resource.Aggregate.t()]
@spec public_attribute(module(), atom() | String.t(), config()) :: Ash.Resource.Attribute.t() | nil
@spec public_attributes(module(), config()) :: [Ash.Resource.Attribute.t()]
@spec public_calculation(module(), atom() | String.t(), config()) :: Ash.Resource.Calculation.t() | nil
@spec public_calculations(module(), config()) :: [Ash.Resource.Calculation.t()]
The names of every public field on resource — attributes, calculations and
aggregates together.
One Map.keys/1 on the manifest path; three list walks on the live one.
@spec public_relationship(module(), atom(), config()) :: relationship() | nil
The public relationship name on resource, narrowed like relationship/3.
The manifest carries only public relationships, so both sources agree.
@spec relationship(module(), atom(), config()) :: relationship() | nil
The relationship name on resource, narrowed to :name, :destination
and :cardinality, or nil.
A miss on the manifest falls back to live introspection, because a manifest
is not a complete list of relationships:
Ash.Info.Manifest.Generator.generate/1 defaults
:include_private_relationships? to false
(deps/ash/lib/ash/info/manifest/generator.ex:50), so a private belongs_to
is absent from a manifest built with the defaults while
Ash.Resource.Info.relationship/2 still answers for it.
How the read behind the :many relationship name paginates.
:offset, :keyset, :mixed when the action offers both, and :none when
it offers neither — which is also the answer for every to-one relationship
and for a relationship nobody declared.
Live, this walks to the destination's read action on every call. The decorator resolves it once per relationship at compile time. Precomputed for issue #24, the relationship query envelopes, which need the answer per relationship per request.
The read action the :many relationship name loads through, or nil.
The relationship's own read_action when it names one, the destination's
primary read otherwise. nil for a to-one relationship, for a destination
with no primary read, and for a relationship nobody declared.
Is module a resource, falling back to live introspection when the manifest
does not know it?
For the request path. See the module doc for why this is not the same
question as declared_resource?/2.
@spec source(config()) :: AshIntrospection.ResourceInfo.Source.t() | nil
Returns the prepared manifest source on config, or nil for live reads.