AshFormBuilder.Infer (AshFormBuilder v0.2.0)

View Source

Zero-Config Auto-Inference Engine for AshFormBuilder v0.2.0.

Dynamically infers complete form field definitions from Ash resource metadata. Reads action definitions, attributes, arguments, and relationships via Ash.Resource.Info.

Key Capabilities

  • Complete Type Mapping - Maps all Ash 3.0 types to appropriate UI components
  • Smart Defaults - Infers labels, required status, placeholders from constraints
  • Relationship Detection - Auto-detects and configures many_to_many, has_many, belongs_to
  • Constraint Awareness - Detects one_of, enums, and other constraints for UI selection
  • Configurable Ignoring - Exclude fields without writing full field blocks
  • Manage Relationship Support - Infers UI based on manage_relationship configuration

Usage

# Zero-config - infer everything from action.accept
fields = AshFormBuilder.Infer.infer_fields(MyApp.Task, :create)

# Custom ignore list
fields = AshFormBuilder.Infer.infer_fields(MyApp.Task, :create,
  ignore_fields: [:id, :tenant_id, :deleted_at]
)

# Include timestamps
fields = AshFormBuilder.Infer.infer_fields(MyApp.Task, :create,
  include_timestamps: true
)

# Customize relationship UI
fields = AshFormBuilder.Infer.infer_fields(MyApp.Task, :create,
  many_to_many_as: :select,
  belongs_to_as: :combobox
)

Type Inference Table

Ash TypeConstraintInferred UIExample
:string-:text_inputStandard text
:ci_string-:text_inputCase-insensitive
:text-:textareaMulti-line
:boolean-:checkboxToggle
:integer-:numberWhole numbers
:float / :decimal-:numberDecimals
:date-:dateDate picker
:datetime-:datetimeDateTime picker
:atomone_of::selectDropdown
:enum module-:selectEnum values
:email-:emailEmail input
:url-:urlURL input
:phone-:telTelephone
many_to_many-:multiselect_comboboxSearchable multi-select
has_many-:nested_formDynamic nested forms
belongs_to-:selectForeign key selection

Relationship Handling

When a relationship is detected in the action's accept list:

  1. many_to_many:multiselect_combobox with searchable selection
  2. has_many → Nested form configuration (not a direct field)
  3. belongs_to:select or :combobox based on destination

For many_to_many relationships, the following field properties are populated:

  • type: :multiselect_combobox
  • relationship: :relationship_name
  • relationship_type: :many_to_many
  • destination_resource: DestinationResource
  • opts: [label_key: :name, value_key: :id, search_event: "...", ...]

This enables seamless UI rendering with searchable multi-select for related records.

Summary

Functions

Detects field type (attribute or relationship) for a given field name.

Detects required preloads for update/destroy actions.

Infers Field structs for the given resource action with zero-config operation.

Infers complete form schema including nested forms and preload requirements.

Functions

detect_field_type(resource, field_name)

@spec detect_field_type(module(), atom()) ::
  :attribute | {:relationship, struct()} | :ignore

Detects field type (attribute or relationship) for a given field name.

Returns

  • :attribute - Field is a resource attribute
  • {:relationship, relationship} - Field is a relationship
  • :ignore - Field should be ignored

Examples

iex> Infer.detect_field_type(MyApp.Task, :title)
:attribute

iex> Infer.detect_field_type(MyApp.Task, :tags)
{:relationship, %Ash.Resource.Relationships.ManyToMany{...}}

detect_required_preloads(fields, resource, action_name)

@spec detect_required_preloads([AshFormBuilder.Field.t()], module(), atom()) :: [
  atom()
]

Detects required preloads for update/destroy actions.

Identifies which relationships need preloading for form rendering (e.g., many_to_many relationships that appear in the form).

Examples

iex> Infer.detect_required_preloads(fields, MyApp.Task, :update)
[:tags, :assignees]

infer_fields(resource, action_name, opts \\ [])

@spec infer_fields(module(), atom(), keyword()) :: [AshFormBuilder.Field.t()]

Infers Field structs for the given resource action with zero-config operation.

Options

  • :ignore_fields - List of field names to skip (default: [:id, :inserted_at, :updated_at])
  • :include_timestamps - Include timestamp fields (default: false)
  • :many_to_many_as - UI type for many_to_many (default: :multiselect_combobox)
  • :has_many_as - UI type for has_many (default: :nested_form)
  • :belongs_to_as - UI type for belongs_to (default: :select)
  • :creatable - Enable creatable combobox for many_to_many (default: false)
  • :create_action - Action for creating new items (default: :create)

Returns

List of %AshFormBuilder.Field{} structs in rendering order.

Examples

# Basic zero-config inference
iex> fields = Infer.infer_fields(MyApp.Task, :create)
[%Field{name: :title, type: :text_input, ...}, ...]

# Custom ignore list
iex> fields = Infer.infer_fields(MyApp.Task, :create, ignore_fields: [:tenant_id])
[%Field{...}]

# Include timestamps
iex> fields = Infer.infer_fields(MyApp.Task, :create, include_timestamps: true)
[%Field{name: :inserted_at, ...}, %Field{name: :updated_at, ...}]

# Creatable combobox for relationships
iex> fields = Infer.infer_fields(MyApp.Task, :create, creatable: true)
[%Field{name: :tags, type: :multiselect_combobox, opts: [creatable: true]}]

infer_schema(resource, action_name, opts \\ [])

@spec infer_schema(module(), atom(), keyword()) :: %{
  fields: [AshFormBuilder.Field.t()],
  nested_forms: keyword(),
  action: atom(),
  resource: module(),
  required_preloads: [atom()]
}

Infers complete form schema including nested forms and preload requirements.

Returns a map suitable for passing to AshPhoenix.Form.for_create/3.

Returns

%{ fields: [Field.t()], nested_forms: keyword(), action: atom(), resource: module(), required_preloads: [atom()] }

Examples

iex> schema = Infer.infer_schema(MyApp.Task, :create)
%{
  fields: [%Field{...}],
  nested_forms: [subtasks: [type: :list, resource: MyApp.Subtask]],
  action: :create,
  resource: MyApp.Task,
  required_preloads: [:tags, :assignees]
}