Skip to main content

guides/ash-integration.md

# Ash Integration

Elex ships an optional Ash resource validation that type-checks expression
strings stored on Ash attributes. The validation runs when the attribute
changes on a changeset.

## Setup

Add both Elex and Ash to your dependencies:

```elixir
def deps do
  [
    {:elex, "~> 0.2.0"},
    {:ash, "~> 3.22"}
  ]
end
```

`Elex.AshValidation` is compiled only when Ash is available. Without Ash in
your dependency tree, the module is not present.

## Basic usage

Validate that a `:formula` attribute contains an expression returning a decimal:

```elixir
defmodule MyApp.Product do
  use Ash.Resource

  attributes do
    attribute :formula, :string do
      allow_nil? false
    end
  end

  validations do
    validate Elex.AshValidation,
      attribute: :formula,
      context: Elex.new_context(),
      expected_type: :decimal
  end
end
```

When a user submits an invalid expression, Ash returns a changeset error with a
human-readable message from Elex.

## Options

| Option | Required | Description |
|--------|----------|-------------|
| `:attribute` | yes | Atom name of the string attribute holding the expression |
| `:context` | yes | `Elex.Context` with allowed variables and functions |
| `:expected_type` | yes | `:decimal`, `:boolean`, or `:string` |
| `:add_value_type_from_attribute` | no | Attribute atom; adds a `"value"` variable typed from that attribute's current value |
| `:description` | no | Custom text included in validation error messages |

## Context with variables

Pass a context that includes every variable your users may reference:

```elixir
context =
  Elex.new_context()
  |> Elex.add_variables(%{
    "price" => 0,
    "quantity" => 0
  })

validations do
  validate Elex.AshValidation,
    attribute: :formula,
    context: context,
    expected_type: :decimal
end
```

Variables in the context need types for validation to succeed, but their values
are not used during validation — only types matter.

## Validating against the current value

Use `:add_value_type_from_attribute` when expressions may reference a `value`
variable representing the attribute being validated:

```elixir
attributes do
  attribute :threshold, :decimal
  attribute :condition, :string
end

validations do
  validate Elex.AshValidation,
    attribute: :condition,
    context: Elex.new_context(),
    expected_type: :boolean,
    add_value_type_from_attribute: :threshold
end
```

This lets users write expressions like `value > 100` where `value` is typed as
`:decimal` from the `:threshold` attribute.

## What gets validated

On change, `Elex.AshValidation`:

1. Reads the new value of the target attribute (skips validation when unchanged)
2. Parses the expression string with `Elex.Parser.parse/3`
3. Checks the result type matches `:expected_type`
4. Returns `:ok` or an `Ash.Error.Changes.InvalidAttribute` error

Parse and type errors surface as attribute errors with the expression string as
the invalid value.

## Further reading

- `Elex.AshValidation` — full API reference
- [Getting Started](getting-started.md) — building contexts and validating expressions
- [Expression Language](expression-language.md) — syntax reference