diff --git a/README.md b/README.md index 7b2b071..125d09b 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ end - `{type, {:default, default}}` - Provides a default value if the field is missing or `nil`. - `{type, {:default, &some_fun/0}}` - The default values is retrieved from callinf `some_fun/0` if the field is missing. - `{type, {:default, {mod, fun}}}` - The default values is retrieved from callinf `mod.fun/0` if the field is missing. -- `{type, {:transform, mapper}}` - Transforms the field value using the specified mapper function. - - `{type, {:transform, {mod, fun}}}` - Transforms the field value using the specified `mod.fun/1` function. +- `{type, {:transform, mapper}}` - Transforms the field value using the specified mapper function. It can be a 1 or 2 arity function: when is a single arity the mapper function will only receive the defined field value, while with 2 arity will receive the current defined field value and the whole data as the second argument. + - `{type, {:transform, {mod, fun}}}` - Transforms the field value using the specified `mod.fun/1` function. Notice that `fun` can be a 2 arity so it can receive the whole data being validated, in case on dependent fields transformations. + - `{type, {:transform, {mod, fun, args}}}` - Transforms the field value using the specified MFA. Notice that `fun` will be at least a 2 arity one so it can receive the whole data being validated, in case on dependent fields transformations and the maximum arity allowed will be 2 + `length(args)`. - `{:either, {type1, type2}}` - Validates that the field is either of the two specified types. - `{:oneof, types}` - Validates that the field is one of the specified types. - `{:custom, callback}` - Validates that the field passes the custom validation function. diff --git a/lib/peri.ex b/lib/peri.ex index a5e82a4..9cdb99e 100644 --- a/lib/peri.ex +++ b/lib/peri.ex @@ -675,14 +675,34 @@ defmodule Peri do defp validate_field(val, {type, {:transform, {mod, fun}}}, data) when is_atom(mod) and is_atom(fun) do with :ok <- validate_field(val, type, data) do - {:ok, apply(mod, fun, [val])} + cond do + function_exported?(mod, fun, 1) -> + {:ok, apply(mod, fun, [val])} + + function_exported?(mod, fun, 2) -> + {:ok, apply(mod, fun, [val, maybe_get_root_data(data)])} + + true -> + template = "expected %{mod} to export %{fun}/1 or %{fun}/2" + {:error, template, mod: mod, fun: fun} + end end end defp validate_field(val, {type, {:transform, {mod, fun, args}}}, data) when is_atom(mod) and is_atom(fun) and is_list(args) do with :ok <- validate_field(val, type, data) do - {:ok, apply(mod, fun, [val | args])} + cond do + function_exported?(mod, fun, length(args) + 2) -> + {:ok, apply(mod, fun, [val, maybe_get_root_data(data) | args])} + + function_exported?(mod, fun, length(args) + 1) -> + {:ok, apply(mod, fun, [val | args])} + + true -> + template = "expected %{mod} to export %{fun} with arity from %{base} to %{arity}" + {:error, template, mod: mod, fun: fun, arity: length(args), base: length(args) + 1} + end end end diff --git a/test/peri_test.exs b/test/peri_test.exs index 55e8410..91bf203 100644 --- a/test/peri_test.exs +++ b/test/peri_test.exs @@ -1492,6 +1492,53 @@ defmodule PeriTest do end end + describe "transform with MFA" do + test "it should apply the mapper function without additional argument" do + s = {:string, {:transform, {String, :to_integer}}} + assert {:ok, 10} = Peri.validate(s, "10") + end + + test "it should apply the mapper function without additional argument but with dependent field" do + s = %{id: {:string, {:transform, {__MODULE__, :integer_by_name}}}, name: :string} + data = %{id: "10", name: "john"} + assert {:ok, %{id: 20, name: "john"}} = Peri.validate(s, data) + + data = %{id: "10", name: "maria"} + assert {:ok, %{id: 10, name: "maria"}} = Peri.validate(s, data) + end + + test "it should apply mapper function with additional arguments" do + s = {:string, {:transform, {String, :split, [~r/\D/, [trim: true]]}}} + assert {:ok, ["10"]} = Peri.validate(s, "omw 10") + end + + test "it should apply mapper function with additional arguments with dependent field" do + s = %{ + id: {:string, {:transform, {__MODULE__, :integer_by_name, [[make_sense?: false]]}}}, + name: :string + } + + data = %{id: "10", name: "john"} + assert {:ok, %{id: 10, name: "john"}} = Peri.validate(s, data) + end + end + + def integer_by_name(id, %{name: name}) do + if name != "john" do + String.to_integer(id) + else + String.to_integer(id) + 10 + end + end + + def integer_by_name(id, %{name: name}, make_sense?: sense) do + cond do + sense && name != "john" -> String.to_integer(id) - 10 + not sense && name == "john" -> String.to_integer(id) + true -> 42 + end + end + defschema(:either_transform, %{ value: {:either, {{:integer, {:transform, &double/1}}, {:string, {:transform, &upcase/1}}}} })