Skip to content

Instantly share code, notes, and snippets.

@sheerlox
Last active April 10, 2024 14:20
Show Gist options
  • Save sheerlox/c0ea94f967475a6ffd5b97d416584e45 to your computer and use it in GitHub Desktop.
Save sheerlox/c0ea94f967475a6ffd5b97d416584e45 to your computer and use it in GitHub Desktop.
A custom "compare" validator for Elixir's Ash framework that works with Date strings, structs, attributes and arguments.
# Adapted from Ash.Resource.Validation.Compare
defmodule DateCompare do
use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute
@compare_opts [
attribute: [
type: :atom,
required: true,
doc: "The attribute to validate."
],
greater_than: [
type: {:or, [:atom, :string, {:struct, Date}]},
required: false,
doc: "The value that the attribute should be greater than."
],
greater_than_or_equal_to: [
type: {:or, [:atom, :string, {:struct, Date}]},
required: false,
doc: "The value that the attribute should be greater than or equal to"
],
less_than: [
type: {:or, [:atom, :string, {:struct, Date}]},
required: false,
doc: "The value that the attribute should be less than"
],
less_than_or_equal_to: [
type: {:or, [:atom, :string, {:struct, Date}]},
required: false,
doc: "The value that the attribute should be less than or equal to"
]
]
@moduledoc """
Validates that a Date attribute or argument meets the given comparison criteria.
The values provided for each option may be an attribute, argument, Date struct or a date string representation.
## Options
#{Spark.OptionsHelpers.docs(@compare_opts)}
## Examples
validate {DateCompare,
attribute: :position_end_date, greater_than: &Date.utc_today/0},
message: "cannot be in the past"
"""
@impl true
def init(opts) do
case Spark.OptionsHelpers.validate(
opts,
@compare_opts
) do
{:ok, opts} ->
{:ok, opts}
{:error, error} ->
{:error, Exception.message(error)}
end
end
@impl true
def validate(changeset, opts, _context) do
value =
if Enum.any?(changeset.action.arguments, &(&1.name == opts[:attribute])) do
Ash.Changeset.fetch_argument(changeset, opts[:attribute])
else
{:ok, Ash.Changeset.get_attribute(changeset, opts[:attribute])}
end
case value do
{:ok, value} ->
Enum.reduce(
Keyword.take(opts, [
:greater_than,
:less_than,
:greater_than_or_equal_to,
:less_than_or_equal_to
]),
:ok,
fn validation, _ ->
case validation do
{:greater_than, attribute} ->
if Date.compare(value, attribute_value(changeset, attribute)) == :gt,
do: :ok,
else: invalid_attribute_error(opts, value)
{:greater_than_or_equal_to, attribute} ->
if Enum.member?(
[:gt, :eq],
Date.compare(value, attribute_value(changeset, attribute))
),
do: :ok,
else: invalid_attribute_error(opts, value)
{:less_than, attribute} ->
if Date.compare(value, attribute_value(changeset, attribute)) == :lt,
do: :ok,
else: invalid_attribute_error(opts, value)
{:less_than_or_equal_to, attribute} ->
if Enum.member?(
[:lt, :eq],
Date.compare(value, attribute_value(changeset, attribute))
),
do: :ok,
else: invalid_attribute_error(opts, value)
true ->
:ok
end
end
)
_ ->
:ok
end
end
@impl true
def describe(opts) do
[
vars: [
value:
case opts[:value] do
fun when is_function(fun, 0) -> fun.()
v -> v
end,
greater_than: opts[:greater_than],
less_than: opts[:less_than],
greater_than_or_equal_to: opts[:greater_than_or_equal_to],
less_than_or_equal_to: opts[:less_than_or_equal_to]
],
message: opts[:message] || message(opts)
]
end
defp attribute_value(_changeset, attribute) when is_function(attribute, 0) do
attribute.()
end
defp attribute_value(changeset, attribute) when is_atom(attribute),
do: Ash.Changeset.get_argument_or_attribute(changeset, attribute)
defp attribute_value(_, attribute), do: attribute
defp invalid_attribute_error(opts, attribute_value) do
{:error,
[
field: opts[:attribute],
value: attribute_value
]
|> with_description(opts)
|> InvalidAttribute.exception()}
end
defp message(opts) do
opts
|> Keyword.take([
:greater_than,
:less_than,
:greater_than_or_equal_to,
:less_than_or_equal_to
])
|> Enum.map_join(" and ", fn {key, _value} ->
case key do
:greater_than ->
"must be greater than %{greater_than}"
:less_than ->
"must be less than %{less_than}"
:greater_than_or_equal_to ->
"must be greater than or equal to %{greater_than_or_equal_to}"
:less_than_or_equal_to ->
"must be less than or equal to %{less_than_or_equal_to}"
end
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment