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.
Stage 1 installs the seam; it does not finish the migration. A function is backed by the manifest here only where both sources return the identical value:
| Function | Manifest-backed |
|---|---|
runtime_resource?/2, declared_resource?/2, embedded?/2 | yes |
primary_key/2, identity_keys/3 | yes |
relationship/3, public_relationship/3 | yes |
public_field_names/2 | yes |
every attribute, calculation, aggregate, action reader | no — live either way |
The rest read live even when a manifest is present, and say so below. The
reason is shape, not effort: Ash.Info.Manifest.Field carries a resolved
%Ash.Info.Manifest.Type{} where Ash.Resource.Attribute carries an Ash
type module plus a constraints keyword list. Translating between them is a
real piece of work with its own failure modes, and it is what stage 2's
decorator exists for. Half-translating it here would put a second, quieter
answer next to the live one.
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.
See Ash.Resource.Info.action/2. Live in this stage.
See Ash.Resource.Info.actions/1. Live in this stage.
See Ash.Resource.Info.aggregate/2. Live in this stage.
See Ash.Resource.Info.aggregate_type/2. Live in this stage.
See Ash.Resource.Info.attribute/2. Live in this stage.
See Ash.Resource.Info.attributes/1. Live in this stage.
See Ash.Resource.Info.calculation/2. Live in this stage.
Is module part of the declared API surface?
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/1.
Builds the lookup maps for a manifest once, so reads do not rebuild them.
The primary key field names of resource.
See Ash.Resource.Info.public_aggregate/2. Live in this stage.
See Ash.Resource.Info.public_aggregates/1. Live in this stage.
See Ash.Resource.Info.public_attribute/2. Live in this stage.
See Ash.Resource.Info.public_attributes/1. Live in this stage.
See Ash.Resource.Info.public_calculation/2. Live in this stage.
See Ash.Resource.Info.public_calculations/1. Live in this stage.
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.
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
See Ash.Resource.Info.action/2. Live in this stage.
@spec actions(module(), config()) :: [Ash.Resource.Actions.action()]
See Ash.Resource.Info.actions/1. Live in this stage.
@spec aggregate(module(), atom() | String.t(), config()) :: Ash.Resource.Aggregate.t() | nil
See Ash.Resource.Info.aggregate/2. Live in this stage.
@spec aggregate_type(module(), Ash.Resource.Aggregate.t(), config()) :: term()
See Ash.Resource.Info.aggregate_type/2. Live in this stage.
@spec attribute(module(), atom() | String.t(), config()) :: Ash.Resource.Attribute.t() | nil
See Ash.Resource.Info.attribute/2. Live in this stage.
@spec attributes(module(), config()) :: [Ash.Resource.Attribute.t()]
See Ash.Resource.Info.attributes/1. Live in this stage.
@spec calculation(module(), atom() | String.t(), config()) :: Ash.Resource.Calculation.t() | nil
See Ash.Resource.Info.calculation/2. Live in this stage.
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.
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/1.
Call it once at an entry point rather than preparing on every read. A config
with no :manifest key is returned untouched.
@spec prepare(Ash.Info.Manifest.t() | AshIntrospection.ResourceInfo.Source.t() | 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.
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
See Ash.Resource.Info.public_aggregate/2. Live in this stage.
@spec public_aggregates(module(), config()) :: [Ash.Resource.Aggregate.t()]
See Ash.Resource.Info.public_aggregates/1. Live in this stage.
@spec public_attribute(module(), atom() | String.t(), config()) :: Ash.Resource.Attribute.t() | nil
See Ash.Resource.Info.public_attribute/2. Live in this stage.
@spec public_attributes(module(), config()) :: [Ash.Resource.Attribute.t()]
See Ash.Resource.Info.public_attributes/1. Live in this stage.
@spec public_calculation(module(), atom() | String.t(), config()) :: Ash.Resource.Calculation.t() | nil
See Ash.Resource.Info.public_calculation/2. Live in this stage.
@spec public_calculations(module(), config()) :: [Ash.Resource.Calculation.t()]
See Ash.Resource.Info.public_calculations/1. Live in this stage.
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.
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.