I’m trying to find out if there’s a way to reuse argument constraints rather than defining them over and over again. For example, say you have a User
resource in an Accounts
domain. On that resource there is an attribute for the password_hash
. However, the user is inputting their password
when interacting with the application in various ways, such as registering or updating their password. In both of those scenarios, I would need to run the password
argument against a set of constraints to ensure it meets the password requirements. As a super basic example, both actions I would have something like this:
actions do
create :register do
argument :password, :string do
allow_nil? false
sensitive? true
constraints min_length: 8, max_length: 72
end
end
update :update_password do
argument :password, :string do
allow_nil? false
sensitive? true
constraints min_length: 8, max_length: 72
end
end
end
Is there a way to share the constraints in this situation so that when I make a password argument it will always point towards the same set of constraints? I would hate to have to duplicate this code and keep them in sync manually if there were any changes.
1 Like
Yep! This is the purpose of Ash.Type.NewType
defmodule MyApp.Types.Password do
use Ash.Type.NewType, subtype_of: :string, constraints: [min_length: 8, max_length: 72]
end
argument :password, MyApp.Types.Password do
allow_nil? false
sensitive? true
end
You can also give it a short name like so:
config :ash, :custom_types, [password: MyApp.Types.Password]
And then its
argument :password, :password do
allow_nil? false
sensitive? true
end
2 Likes
Thanks for the quick response! That’s perfect. Follow up question, is there a way to give this NewType
custom error messages for each constraint type? For example, if the password
has an invalid format, saying something along the lines of "must contain special character"
?
Not just by setting or modifying constraints, you’d need to validate the value yourself.
defmodule MyApp.Types.Password do
use Ash.Type.NewType, subtype_of: :string
def cast_input(nil, _constraints) do
{:ok, nil}
end
def cast_input(value, _constraints) do
# do your own logic here, returning things like `{:error, "must contain a special character"}` or `{:ok, value}`
end
end
1 Like
Thanks! To ensure I’m doing this correctly, here’s a rough copy with a regex placeholder:
defmodule MyApp.Accounts.User.Types.Password do
@moduledoc """
A custom type for password validation
"""
use Ash.Type.NewType, subtype_of: :string
def cast_input(nil, _constraints) do
{:ok, nil}
end
def cast_input(value, _constraints) when not is_binary(value) do
{:error, "must be a string"}
end
def cast_input(value, _constraints) do
cond do
String.length(value) < 8 ->
{:error, "must be at least 8 characters"}
String.length(value) > 72 ->
{:error, "must not exceed 72 characters"}
not String.match?(value, ~r/.../) ->
{:error, "must contain a special character"}
true ->
{:ok, value}
end
end
end
Is that about right? When I run it, it works as expected. When giving it non-string input, such as 123
, it returns the error as expected. Should I instead be expected to cast the input into a string seeing as the name of the function is cast_input
?
Its up to you
The builtin string type itself does not attempt to cast arbitrary values to strings, there is likely no need for you to do so either.
EDIT:
if you want to ensure parity with the builtin type, you could wrap your logic in something like:
with {:ok, value} <- Ash.Type.cast_input(:string, value, constraints) do
...
end
1 Like
Excellent! I truly appreciate all the help 