Constraints
Contraints can be used to validate input data. This can be a bit misleading for newbies because in addition validations are a thing too. Contraints work for attributes and arguments.
Different datatypes have different constraints. You can use :allow_empty? for string but not for integer . |
Need more information about contraints? Have a look at the official Ash documentation at Constraints.
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.
Using Igniter (Recommended)
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:
defmodule App.Shop.Product do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :name, :string
attribute :description, :string
attribute :price, :decimal
attribute :stock_quantity, :integer
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
defmodule App.Shop do
use Ash.Api
resources do
resource App.Shop.Product
end
end
allow_nil? (Required Attributes)
The simplest validation is a check that an attribute is not nil
. This is done with the allow_nil?/1
function. We want to be sure that name
, price
and stock_quantity
are always set. Please adjust the attributes
block in lib/app/shop/resources/product.ex
:
[...]
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
attribute :description, :string
attribute :price, :decimal do
allow_nil? false
end
attribute :stock_quantity, :integer do
allow_nil? false
end
end
[...]
Now let’s try to create a product without a name:
$ iex -S mix
iex(1)> App.Shop.Product.create!(%{price: 10.0,
stock_quantity: 3})
** (Ash.Error.Invalid) Input Invalid
* attribute name is required
(ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
Perfect. The validation works.
In a written tutorial I prefer to use the But while programming I prefer to use the non-! version of the create function:
|
allow_empty?
Sometimes we want to allow empty strings.
[...]
attribute :description, :string do
allow_nil? false (1)
constraints allow_empty?: true (2)
end
[...]
1 | The description attribute is not allowed to be nil . The tricky part is the syntax here. You have to put allow_nil? in an extra code line. |
2 | But it is allowed to be an empty string. |
Now let’s try to create a product. First with a nil description and than with an empty description:
$ iex -S mix
iex(1)> App.Shop.Product.create!(%{name: "Banana", price: 0.1, stock_quantity: 5})
** (Ash.Error.Invalid) Input Invalid
* attribute description is required
(ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
iex(1)> App.Shop.Product.create!(%{name: "Banana", description: "", price: 0.1, stock_quantity: 5})
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "8334b52f-1ba0-4adb-b790-705f8e9e1291",
name: "Banana",
description: "",
price: Decimal.new("0.1"),
stock_quantity: 5,
...
>
Perfect. The validation works.
min, max, min_length and max_length
Sometimes we want to make sure that an attribute has a minimal or maximal length. Let’s add a minimal length of 3 characters and a maximal length of 255 characters for the name
attribute. And while we are at it let us add a maximum of 512 characters for the description
attribute.
But what about the numbers? We want to make sure that the price
is always greater than 0 and the stock_quantity
is always greater than or equal to 0. For that we can use the constraints/1
function with the min
and max
options.
[...]
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
constraints min_length: 3, max_length: 255
end
attribute :description, :string do
constraints max_length: 512
end
attribute :price, :decimal do
allow_nil? false
constraints min: 0.01
end
attribute :stock_quantity, :integer do
allow_nil? false
constraints min: 0
end
end
[...]
Testing the validation:
iex(7)> App.Shop.Product.create!(%{name: "Y",
price: 0,
stock_quantitiy: -1})
** (Ash.Error.Invalid) Input Invalid
* attribute stock_quantity is required (1)
* Invalid value provided for price: must be more than or equal to 0.01. (2)
0
* Invalid value provided for name: length must be greater than or equal to 3. (3)
"Y"
(ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
1 | stock_quantity can not be nil and has to be greater than or equal to 0. |
2 | name can not be nil and has to have at least 3 characters. |
3 | price can not be nil and has to be greater than or equal to 0.01. |
Pattern Matching
Assuming that we only want to have characters an the -
in the name of a product we can use match?/1
to check if the name matches a regular expression. Let’s add this to the name
attribute:
[...]
attribute :name, :string do
allow_nil? false
constraints min_length: 3,
max_length: 255,
match: ~r/^[a-zA-Z-]*$/
end
[...]
Let’s test it:
$ iex -S mix
iex(1)> App.Shop.Product.create!(%{name: "Banana2023",
price: 0.1,
stock_quantity: 20}) (1)
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for name: must match the pattern ~r/^[a-zA-Z-]*$/.
"Banana2023"
(ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
iex(2)> App.Shop.Product.create!(%{name: "Banana",
price: 0.1,
stock_quantity: 20})
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "c29444dc-7da2-4849-b251-b851a745112a",
name: "Banana",
description: nil,
price: Decimal.new("0.1"),
stock_quantity: 20,
...
>
iex(2)>
1 | The name "Banana2023" does not match the pattern. |
Trim
What happens if you add a could of spaces at the end of a name? Let’s try it:
$ iex -S mix
iex(1)> App.Shop.Product.create!(%{name: "Banana ",
price: 0.1,
stock_quantity: 12})
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "5b9b53f4-6109-4757-a8b7-9aaf1acda1f3",
name: "Banana",
...
>
Those spaces get trimmed automatically. This is the default behavior and normaly what you want because humans and auto fill browsers sometimes add spaces at the end of a form field on a webpage.
In case you want to keep those spaces you can use trim: false
:
[...]
attribute :name, :string do
allow_nil? false
constraints min_length: 3,
max_length: 255,
match: ~r/^[a-zA-Z- ]*$/,
trim?: false
end
[...]
I did sneek in a space in the regular expression. Because otherwise the validation for |
iex(4)> App.Shop.Product.create!(%{name: "Banana ",
price: 0.1,
stock_quantity: 12})
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "b1793ac1-4bfb-4f4f-9b3a-42a64c30378b",
name: "Banana ",
description: nil,
price: Decimal.new("0.1"),
stock_quantity: 12,
...
>