AshIntrospection.Manifest.Custom (AshIntrospection v0.4.1)

Copy Markdown View Source

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 resolved type of aggregate, precomputed.

The %{argument_atom => client_name} map for one action, or %{}.

The precomputed bulk-authorization strategy, :error or :filter.

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 attribute named name if it is public, or nil.

The calculation named name if it is public, or nil.

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

entrypoint_payload()

@type entrypoint_payload() :: %{client_name: String.t() | nil}

The decoration map written on a %Ash.Info.Manifest.Entrypoint{}.

manifest_payload()

@type manifest_payload() :: %{
  entrypoint_lookup: %{required(String.t()) => Ash.Info.Manifest.Entrypoint.t()}
}

The decoration map written on the %Ash.Info.Manifest{} itself.

relationship_payload()

@type relationship_payload() :: %{
  pagination: :offset | :keyset | :mixed | :none,
  read_action: atom() | nil
}

The decoration map written on a %Ash.Info.Manifest.Relationship{}.

resource_payload()

@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_payload()

@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

action(resource, name, namespace \\ :ash_introspection)

@spec action(Ash.Info.Manifest.Resource.t() | nil, atom(), atom()) ::
  Ash.Resource.Actions.action() | nil

The action named name, or nil.

actions(resource, namespace \\ :ash_introspection)

Every action, in Ash.Resource.Info.actions/1 order.

aggregate(resource, name, namespace \\ :ash_introspection)

@spec aggregate(Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom()) ::
  Ash.Resource.Aggregate.t() | nil

The aggregate named name, or nil.

aggregate_type(resource, aggregate, namespace \\ :ash_introspection)

@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.

argument_name_mappings(resource, action_name, namespace \\ :ash_introspection)

@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 %{}.

attribute(resource, name, namespace \\ :ash_introspection)

@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.

attributes(resource, namespace \\ :ash_introspection)

@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.

authorize_bulk_strategy(resource, namespace \\ :ash_introspection)

@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.

calculation(resource, name, namespace \\ :ash_introspection)

@spec calculation(Ash.Info.Manifest.Resource.t() | nil, atom() | String.t(), atom()) ::
  Ash.Resource.Calculation.t() | nil

The calculation named name, or nil.

decorated?(struct, namespace \\ :ash_introspection)

@spec decorated?(struct() | nil, atom()) :: boolean()

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.

default_namespace()

@spec default_namespace() :: atom()

The namespace used when a caller names none.

entrypoint(manifest, client_name, namespace \\ :ash_introspection)

@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.

entrypoint_client_name(entrypoint, namespace \\ :ash_introspection)

@spec entrypoint_client_name(Ash.Info.Manifest.Entrypoint.t() | nil, atom()) ::
  String.t() | nil

The client-facing name decorated onto entrypoint, or nil.

entrypoint_lookup(manifest, namespace \\ :ash_introspection)

@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.

field_name_mappings(struct, namespace \\ :ash_introspection)

@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 %{}.

formatted_argument_name(resource, action_name, argument, formatter, namespace \\ :ash_introspection)

@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.

formatted_field_name(resource, field, formatter, namespace \\ :ash_introspection)

@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.

mapped_argument_name(resource, action_name, argument, namespace \\ :ash_introspection)

@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.

mapped_field_name(struct, field, namespace \\ :ash_introspection)

@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.

original_argument_name(resource, action_name, client_name, namespace \\ :ash_introspection)

@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.

original_field_name(struct, client_name, namespace \\ :ash_introspection)

@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.

payload(struct, namespace \\ :ash_introspection)

@spec payload(struct() | nil, atom()) :: map() | nil

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.

public_aggregate(resource, name, namespace \\ :ash_introspection)

@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.

public_aggregates(resource, namespace \\ :ash_introspection)

@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.

public_attribute(resource, name, namespace \\ :ash_introspection)

@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.

public_attributes(resource, namespace \\ :ash_introspection)

@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.

public_calculation(resource, name, namespace \\ :ash_introspection)

@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.

public_calculations(resource, namespace \\ :ash_introspection)

@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.

relationship_pagination(relationship, namespace \\ :ash_introspection)

@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.

relationship_read_action(relationship, namespace \\ :ash_introspection)

@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.

return_classification(resource, action, namespace \\ :ash_introspection)

@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.

reverse_argument_name_mappings(resource, action_name, namespace \\ :ash_introspection)

@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 %{}.

reverse_field_name_mappings(struct, namespace \\ :ash_introspection)

@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 %{}.