AshIntrospection.Codegen.TypeDiscovery (AshIntrospection v0.4.0)

Copy Markdown View Source

Language-agnostic type discovery for Ash resources and types.

This module provides the core type discovery logic that recursively traverses the type dependency tree to find all Ash resources and types that need code generation.

Configuration

Type discovery is configured via a config map:

%{
  # Required: function to get RPC resources from otp_app
  get_rpc_resources: fn otp_app -> [...] end,

  # Optional: function returning the declared action entrypoints, as
  # %{resource: module, action: atom} maps or {module, atom} tuples.
  # Supplying it scopes discovery to those actions; omitting it keeps every
  # public action and field of every RPC resource in scope.
  get_rpc_action_entrypoints: fn otp_app -> [...] end,

  # Optional: callback name for field names (default: :interop_field_names)
  field_names_callback: :interop_field_names,

  # Optional: function to check if a module has the language extension
  has_language_extension?: fn resource -> true/false end,

  # Optional: language name for warnings (default: "Interop")
  language_name: "TypeScript"
}

Type Discovery

The discovery process handles:

  • Ash resources (both embedded and non-embedded)
  • TypedStruct modules
  • Complex nested types (unions, maps, arrays, etc.)
  • Recursive type references with cycle detection
  • Path tracking for diagnostic purposes

Path Tracking

During traversal, paths are tracked as lists of segments:

  • {:root, ResourceModule} - Starting point
  • {:attribute, :field_name} - Attribute field
  • {:calculation, :calc_name} - Calculation
  • {:aggregate, :agg_name} - Aggregate
  • {:union_member, :type_name} - Union member
  • {:array_items} - Array items
  • {:map_field, :field_name} - Map field
  • {:action, :action_name} - Action
  • {:argument, :argument_name} - Action or calculation argument
  • {:metadata, :metadata_name} - Action metadata
  • :returns - Generic action return type

No :manifest here, deliberately

Every introspection read goes through AshIntrospection.ResourceInfo, like the rest of lib/, but none of them is given the config map — they take the reader's default, which is live Ash.Resource.Info. Threading a config through this module's ten private traversal helpers would be work with a known expiry date: issue #23's stage 4 deletes this module outright, because Ash.Info.Manifest.Generator.Reachability and Generator.TypeResolver already do its job and reach more than it does — accepted attributes, action metadata and transitive relationship depth. Routing the calls keeps the "one reader" invariant that makes the later stages greppable; threading the config would not survive to use it.

Summary

Functions

Builds a formatted warning message for resources missing from RPC config.

Builds a warning message for non-RPC resources referenced by RPC resources.

Discovers embedded resources from RPC resources by scanning and filtering.

Discovers all types with field constraints referenced by the given resources.

Finds all non-RPC resources that are referenced by RPC resources.

Finds all non-RPC resources referenced by RPC resources, with paths showing where they're referenced.

Finds all embedded resources referenced by a single resource.

Finds all non-embedded resources referenced by a single resource.

Finds all Ash resources referenced by a single resource's public attributes, calculations, and aggregates.

Finds resources with a language extension that are not configured in any RPC block.

Finds all Ash resources used as struct arguments in RPC actions.

Formats a path (list of path segments) into a human-readable string.

Scans a single resource to find all referenced resources.

Finds all Ash resources referenced by RPC resources.

Traverses a fields keyword list (from Map/Keyword/Tuple/custom type constraints) to find any Ash resource references in the nested field types.

Recursively traverses a type and its constraints to find all Ash resource references.

Types

config()

@type config() :: %{
  optional(:get_rpc_resources) => (atom() -> [module()]),
  optional(:get_rpc_action_entrypoints) => (atom() ->
                                              [map() | {module(), atom()}]),
  optional(:field_names_callback) => atom(),
  optional(:has_language_extension?) => (module() -> boolean()),
  optional(:language_name) => String.t()
}

Functions

build_missing_config_warning(otp_app, missing_resources, config \\ %{})

Builds a formatted warning message for resources missing from RPC config.

Parameters

  • otp_app - The OTP application name
  • missing_resources - List of resource modules
  • config - Configuration map with language_name

Returns

A formatted warning string.

build_non_rpc_references_warning(referenced_non_rpc_with_paths, config \\ %{})

Builds a warning message for non-RPC resources referenced by RPC resources.

Parameters

  • referenced_non_rpc_with_paths - Map of resource => [paths]
  • config - Configuration map with language_name

Returns

A formatted warning string.

find_embedded_resources(otp_app, config)

Discovers embedded resources from RPC resources by scanning and filtering.

When config carries get_rpc_action_entrypoints, only the declared actions are treated as entrypoints: a resource exposing nothing but a generic action contributes only what that action names, not every embedded type hanging off its attributes. Without that key every public action and field of every RPC resource is in scope.

Parameters

  • otp_app - The OTP application name
  • config - Configuration map

Returns

A list of embedded resource modules.

find_field_constrained_types(resources, config \\ %{})

Discovers all types with field constraints referenced by the given resources.

Scans public attributes of resources to find types with field constraints (Map with fields, Keyword with fields, Tuple with fields, Struct with fields, TypedStruct) in direct types, arrays, and union types.

Parameters

  • resources - A list of Ash resource modules to scan
  • config - Configuration map

Returns

A list of unique type info maps containing:

  • :instance_of - The module (if available)
  • :constraints - The type constraints
  • :field_name_mappings - Field name mappings (if available)

find_non_rpc_referenced_resources(otp_app, config)

Finds all non-RPC resources that are referenced by RPC resources.

These are resources that appear in attributes, calculations, or aggregates of RPC resources but are not themselves configured as RPC resources.

Parameters

  • otp_app - The OTP application name
  • config - Configuration map

Returns

A list of non-RPC resource modules that are referenced by RPC resources.

find_non_rpc_referenced_resources_with_paths(otp_app, config)

Finds all non-RPC resources referenced by RPC resources, with paths showing where they're referenced.

Parameters

  • otp_app - The OTP application name
  • config - Configuration map

Returns

A map where keys are non-RPC resource modules and values are lists of formatted path strings showing where each resource is referenced.

find_referenced_embedded_resources(resource)

Finds all embedded resources referenced by a single resource.

Parameters

  • resource - An Ash resource module to scan

Returns

A list of embedded resource modules.

find_referenced_non_embedded_resources(resource)

Finds all non-embedded resources referenced by a single resource.

Parameters

  • resource - An Ash resource module to scan

Returns

A list of non-embedded resource modules.

find_referenced_resources(resource)

Finds all Ash resources referenced by a single resource's public attributes, calculations, and aggregates.

Parameters

  • resource - An Ash resource module to scan

Returns

A list of Ash resource modules referenced by the given resource.

find_resources_missing_from_rpc_config(otp_app, config)

Finds resources with a language extension that are not configured in any RPC block.

Parameters

  • otp_app - The OTP application name
  • config - Configuration map with get_rpc_resources and has_language_extension?

Returns

A list of non-embedded resource modules with the extension but not configured for RPC.

find_struct_argument_resources(actions)

Finds all Ash resources used as struct arguments in RPC actions.

Scans the given actions' public arguments for:

  • :struct or Ash.Type.Struct with an instance_of constraint pointing at an Ash resource,
  • an embedded resource named directly as the argument's type,
  • either of the above behind a NewType wrapper or an array.

Embedded resources are included. A generator that skipped them produced no type for an argument a client has to construct.

Parameters

  • actions - A list of action structs to scan

Returns

A list of unique Ash resource modules used as struct arguments.

format_path(path)

Formats a path (list of path segments) into a human-readable string.

Parameters

  • path - A list of path segments

Returns

A formatted string representing the path.

Examples

iex> path = [{:root, MyApp.Todo}, {:attribute, :metadata}, {:union_member, :text}]
iex> TypeDiscovery.format_path(path)
"Todo -> metadata -> (union: text)"

scan_rpc_resource(resource, visited \\ MapSet.new())

Scans a single resource to find all referenced resources.

Covers the resource's public attributes, calculations (including their arguments) and aggregates, plus every public action's arguments, :returns type and metadata. An action is part of a resource's public surface, so a type it names is a type the client has to be able to build or read.

Parameters

  • resource - An Ash resource module
  • visited - A MapSet of already-visited resources (defaults to empty)

Returns

A tuple of {found_resources, updated_visited} where:

  • found_resources - List of {resource, path} tuples
  • updated_visited - Updated MapSet of visited resources

scan_rpc_resources(otp_app, config)

Finds all Ash resources referenced by RPC resources.

Recursively scans the public attributes, calculations (with their arguments) and aggregates of each entrypoint resource, plus each entrypoint action's arguments, :returns type and metadata, traversing complex types like maps with fields, unions and typed structs to find any Ash resource references.

Entrypoints come from get_rpc_action_entrypoints when the config supplies it, and otherwise from get_rpc_resources with every public action in scope.

Parameters

  • otp_app - The OTP application name to scan for domains and RPC resources
  • config - Configuration map with get_rpc_resources callback

Returns

A list of unique Ash resource modules that are referenced by RPC resources.

traverse_fields(fields)

Traverses a fields keyword list (from Map/Keyword/Tuple/custom type constraints) to find any Ash resource references in the nested field types.

Parameters

  • fields - A keyword list where keys are field names and values are field configs

Returns

A list of Ash resource modules found in the field definitions.

traverse_type(type, constraints)

Recursively traverses a type and its constraints to find all Ash resource references.

This function handles:

  • Direct Ash resource module references
  • Ash.Type.Struct with instance_of constraint
  • Ash.Type.Union with multiple type members
  • Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Tuple with fields constraints
  • Custom types with fields constraints
  • Arrays of any of the above

Parameters

  • type - The type to traverse (module or type atom)
  • constraints - The constraints keyword list for the type

Returns

A list of Ash resource modules found in the type tree.