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.

We start with a clean slate. A fresh Ash app. Please move or delete an already existing Ash app if you have one under the directory name app. Feel free to copy and paste the following lines in your terminal or do it step by step following relationship setup.

mix new --sup app && cd app
awk '/defp deps do/,/\[/ {
       if ($0 ~ /\[/) {
           print $0;
           print "{:ash, \"~> 2.15.8\"}";
           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_apis, [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