Validations

In Ash there are two areas where validations are used:

  • Attributes Those are used to validate the input data before it is saved to the database.

  • Actions Those are used to validate the input data before an action is executed.

Attributes Validations

Validations are used when constraints are not powerful enough.

Let me show you how to use validations with an online shop product example.

Setting Up a Fresh Ash App

The fastest way to get started with Ash is using Igniter, a powerful code generator for Ash applications. Igniter handles all the boilerplate setup for you, making it easy to get up and running quickly.

First, install the Igniter archive if you haven’t already:

$ mix archive.install hex igniter_new

Then create a fresh Ash application:

$ mix igniter.new app --install ash
$ cd app

For a Phoenix application with Ash and PostgreSQL support:

$ mix igniter.new app --with phx.new --install ash,ash_postgres
$ cd app

Igniter will create all the necessary files and folder structure, install dependencies, and set up your configuration automatically.

Using the Interactive Web Installer

You can also use the interactive web installer at https://ash-hq.org/#get-started to create a custom setup command tailored to your specific needs.

Manual Setup Alternative

If you prefer the manual approach, you can follow the steps in the manual approach or use this bash script that performs the same setup:

mix new --sup app && cd app
awk '/defp deps do/,/\[/ {
       if ($0 ~ /\[/) {
           print $0;
           print "{:ash, \"~> 3.0\"}";
           next;
       }
   } 1' mix.exs > mix.exs.tmp
mv mix.exs.tmp mix.exs
mix deps.get
echo '[
  import_deps: [:ash],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]' > .formatter.exs
mkdir config
echo 'import Config
config :app, :ash_domains, [App.Shop]' > config/config.exs
mix format

Please create the following files:

lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string do
      allow_nil? false
    end
    attribute :price, :decimal do
      allow_nil? false
    end
    attribute :use_by_date, :date do
      allow_nil? false
    end
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
lib/app/shop.ex
defmodule App.Shop do
  use Ash.Api

  resources do
    resource App.Shop.Product
  end
end

Custom Validations

We only add products to our shop that are not expired. For that we add a custom validation for the use_by_date attribute. It must not be in the past. We need to write a custom validation for that.

lib/app/validations/in_the_future_or_today.ex
defmodule App.Validations.InTheFutureOrToday do
  use Ash.Resource.Validation

  def validate(changeset, opts) do
    case Ash.Changeset.fetch_argument_or_change(changeset, opts[:field]) do
      :error ->
        # in this case, they aren't changing the field
        :ok

      {:ok, value} ->
        case Date.compare(Date.utc_today(), value) do
          :gt ->
            {:error, field: opts[:field], message: "must be in the future or today"}

          _ ->
            :ok
        end
    end
  end
end

That validation can be used like this:

lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string do
      allow_nil? false
    end
    attribute :price, :decimal do
      allow_nil? false
    end
    attribute :use_by_date, :date do
      allow_nil? false
    end
  end

  validations do
    validate {App.Validations.InTheFutureOrToday, field: :use_by_date} (1)
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1Here we are using the validation App.Validations.InTheFutureOrToday.

Let’s try it out:

$ iex -S mix
iex(1)> App.Shop.Product.create!(%{
                                   name: "Apple",
                                   price: 0.1,
                                   use_by_date: ~D[2008-11-10]
                                 })
** (Ash.Error.Invalid) Input Invalid

* Invalid value provided for use_by_date: must be in the future or today.

nil

    (ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3

With custom validations you can solve pretty much any validation problem.

Action Validations

WIP. This is just a placeholder which will be filled with content in the next couple of days.

actions do
  create :create do
    validate compare(:age, greater_than_or_equal_to: 18)
  end
end

# or

validations do
  validate present([:foo, :bar], at_least: 1) do
    on [:create, :update]
    where present(:baz)
  end
end