Last active
April 10, 2024 14:20
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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