fix: properly compare against decimal values

improvement: support floats & decimals in the `compare` validation
This commit is contained in:
Zach Daniel 2022-10-21 10:46:46 -04:00
parent 7540b31244
commit 986e08e0c2
8 changed files with 135 additions and 7 deletions

View file

@ -95,7 +95,7 @@ defmodule Ash.CiString do
def to_comparable_string(nil), do: nil
end
use Comp
import Ash.Type.Comparable
defcomparable left :: Ash.CiString, right :: BitString do
Ash.CiString.compare(left, right)

View file

@ -12,22 +12,22 @@ defmodule Ash.Resource.Validation.Compare do
doc: "The attribute to check"
],
greater_than: [
type: {:or, [:integer, :atom, {:fun, 0}]},
type: {:or, [:integer, :atom, :float, {:struct, Decimal}, {:fun, 0}]},
required: false,
doc: "The value that the attribute should be greater than."
],
greater_than_or_equal_to: [
type: {:or, [:integer, :atom, {:fun, 0}]},
type: {:or, [:integer, :atom, :float, {:struct, Decimal}, {:fun, 0}]},
required: false,
doc: "The value that the attribute should be greater than or equal to"
],
less_than: [
type: {:or, [:integer, :atom, {:fun, 0}]},
type: {:or, [:integer, :atom, :float, {:struct, Decimal}, {:fun, 0}]},
required: false,
doc: "The value that the attribute should be less than"
],
less_than_or_equal_to: [
type: {:or, [:integer, :atom, {:fun, 0}]},
type: {:or, [:integer, :atom, :float, {:struct, Decimal}, {:fun, 0}]},
required: false,
doc: "The value that the attribute should be less than or equal to"
]

View file

@ -0,0 +1,65 @@
defmodule Ash.Type.Comparable do
@moduledoc "Helpers for working with `Comparable`"
defmacro defcomparable(
{:"::", _, [left_expression, quoted_left_type]},
{:"::", _, [right_expression, quoted_right_type]},
do: code
) do
{left_type, []} = Code.eval_quoted(quoted_left_type, [], __CALLER__)
{right_type, []} = Code.eval_quoted(quoted_right_type, [], __CALLER__)
lr_type =
[Comparable, Type, left_type, To, right_type]
|> Module.concat()
rl_type =
[Comparable, Type, right_type, To, left_type]
|> Module.concat()
lr_impl =
quote do
defmodule unquote(lr_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(lr_type) do
def compare(%unquote(lr_type){
left: unquote(left_expression),
right: unquote(right_expression)
}) do
unquote(code)
end
end
end
if lr_type == rl_type do
lr_impl
else
quote do
unquote(lr_impl)
defmodule unquote(rl_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(rl_type) do
def compare(%unquote(rl_type){
left: unquote(right_expression),
right: unquote(left_expression)
}) do
case unquote(code) do
:gt -> :lt
:lt -> :gt
:eq -> :eq
end
end
end
end
end
end
end

View file

@ -112,4 +112,8 @@ defmodule Ash.Type.Decimal do
def dump_to_native(value, _) do
Ecto.Type.dump(:decimal, value)
end
@doc false
def new(%Decimal{} = v), do: v
def new(v), do: Decimal.new(v)
end

View file

@ -0,0 +1,17 @@
import Ash.Type.Comparable
defcomparable left :: Decimal, right :: Integer do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end
defcomparable left :: Decimal, right :: Decimal do
Decimal.compare(left, right)
end
defcomparable left :: Decimal, right :: Float do
Decimal.compare(Ash.Type.Decimal.new(left), right)
end
defcomparable left :: Decimal, right :: BitString do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end

View file

@ -217,7 +217,7 @@ defmodule Ash.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:spark, "~> 0.1 and >= 0.1.28"},
{:spark, "~> 0.1 and >= 0.1.29"},
{:ecto, "~> 3.7"},
{:ets, "~> 0.8.0"},
{:decimal, "~> 2.0"},

View file

@ -36,7 +36,7 @@
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
"spark": {:hex, :spark, "0.1.28", "8ce732daa56ad0dc11190b28461f85e71b67c5b61ce4818841bc8fcdbf799676", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "11b2d52b473345e2ecb4fe70c76ca8400b2fa9417acb629a6bd92db9d3ff953b"},
"spark": {:hex, :spark, "0.1.29", "36f29894fdf8b30aa866a677134654db72807cf02a998aee948a0c5e98a48018", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "97ed044974cd47e9286d9fa0fd033620bee6b3569bee27e79d1b9bdb4605371e"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},

View file

@ -11,6 +11,8 @@ defmodule Ash.Test.Resource.Validation.CompareTest do
uuid_primary_key :id
attribute :number_one, :integer
attribute :number_two, :integer
attribute :number_three, :decimal
attribute :number_four, :float
end
end
@ -43,6 +45,46 @@ defmodule Ash.Test.Resource.Validation.CompareTest do
assert :ok = Compare.validate(changeset, opts)
end
test "decimals can be compared against" do
{:ok, opts} = Compare.init(attribute: :number_three, greater_than: 0)
changeset =
Post
|> Ash.Changeset.new(%{number_three: Decimal.new(1)})
assert :ok = Compare.validate(changeset, opts)
end
test "floats can be compared against" do
{:ok, opts} = Compare.init(attribute: :number_four, greater_than: 0)
changeset =
Post
|> Ash.Changeset.new(%{number_four: 1.0})
assert :ok = Compare.validate(changeset, opts)
end
test "decimals can be compared with" do
{:ok, opts} = Compare.init(attribute: :number_one, greater_than: Decimal.new(0))
changeset =
Post
|> Ash.Changeset.new(%{number_one: 1})
assert :ok = Compare.validate(changeset, opts)
end
test "floats can be compared with" do
{:ok, opts} = Compare.init(attribute: :number_one, greater_than: 0.0)
changeset =
Post
|> Ash.Changeset.new(%{number_one: 1})
assert :ok = Compare.validate(changeset, opts)
end
test "validate failure against number" do
{:ok, opts} = Compare.init(attribute: :number_one, greater_than: 100)
changeset = Post |> Ash.Changeset.new(%{number_one: 1})