diff --git a/lib/peri.ex b/lib/peri.ex index 9cdb99e..695f8f1 100644 --- a/lib/peri.ex +++ b/lib/peri.ex @@ -660,21 +660,35 @@ defmodule Peri do defp validate_field(val, {type, {:transform, mapper}}, data) when is_function(mapper, 1) do - with :ok <- validate_field(val, type, data) do - {:ok, mapper.(val)} + case validate_field(val, type, data) do + :ok -> {:ok, mapper.(val)} + {:ok, val} -> {:ok, mapper.(val)} + err -> err end end defp validate_field(val, {type, {:transform, mapper}}, data) when is_function(mapper, 2) do - with :ok <- validate_field(val, type, data) do - {:ok, mapper.(val, maybe_get_root_data(data))} + case validate_field(val, type, data) do + :ok -> {:ok, mapper.(val, maybe_get_root_data(data))} + {:ok, val} -> {:ok, mapper.(val, maybe_get_root_data(data))} + err -> err end end 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 + result = validate_field(val, type, data) + ok? = match?(:ok, result) or match?({:ok, _}, result) + + val = + case result do + :ok -> val + {:ok, val} -> val + err -> err + end + + if ok? do cond do function_exported?(mod, fun, 1) -> {:ok, apply(mod, fun, [val])} @@ -686,12 +700,24 @@ defmodule Peri do template = "expected %{mod} to export %{fun}/1 or %{fun}/2" {:error, template, mod: mod, fun: fun} end + else + result 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 + result = validate_field(val, type, data) + ok? = match?(:ok, result) or match?({:ok, _}, result) + + val = + case result do + :ok -> val + {:ok, val} -> val + err -> err + end + + if ok? do cond do function_exported?(mod, fun, length(args) + 2) -> {:ok, apply(mod, fun, [val, maybe_get_root_data(data) | args])} @@ -703,6 +729,8 @@ defmodule Peri do 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 + else + result end end diff --git a/test/peri_test.exs b/test/peri_test.exs index 91bf203..69f6be7 100644 --- a/test/peri_test.exs +++ b/test/peri_test.exs @@ -1539,6 +1539,111 @@ defmodule PeriTest do end end + describe "transform directive on nested schema type" do + test "it should apply the trasnformation on a nested schema type" do + nested = %{foo: :string} + parent = %{bar: {nested, {:transform, fn v -> v end}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # generic map type should pass too + parent = %{bar: {:map, {:transform, fn v -> v end}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # function are indeed being applied? + assert_raise RuntimeError, fn -> + parent = %{bar: {nested, {:transform, fn _ -> raise("boom") end}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + + assert_raise RuntimeError, fn -> + parent = %{bar: {:map, {:transform, fn _ -> raise("boom") end}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + end + + test "it should apply the mapper on nested schemas too by MFA" do + nested = %{foo: :string} + parent = %{bar: {nested, {:transform, {__MODULE__, :id}}}} + data = %{bar: %{foo: "10"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # generic map type should pass too + parent = %{bar: {:map, {:transform, {__MODULE__, :id}}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # function are indeed being applied? + assert_raise RuntimeError, fn -> + parent = %{bar: {nested, {:transform, {__MODULE__, :boom}}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + + assert_raise RuntimeError, fn -> + parent = %{bar: {:map, {:transform, {__MODULE__, :boom}}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + end + + test "it should apply the mapper on nested schemas too by MFA with additional args" do + nested = %{foo: :string} + parent = %{bar: {nested, {:transform, {__MODULE__, :id, []}}}} + data = %{bar: %{foo: "10"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # generic map type should pass too + parent = %{bar: {:map, {:transform, {__MODULE__, :id, []}}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # function are indeed being applied? + assert_raise RuntimeError, fn -> + parent = %{bar: {nested, {:transform, {__MODULE__, :boom, []}}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + + assert_raise RuntimeError, fn -> + parent = %{bar: {:map, {:transform, {__MODULE__, :boom, []}}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + end + + test "it should apply the trasnformation on a nested schema type with dependent fields" do + nested = %{foo: :string} + parent = %{bar: {nested, {:transform, fn v, _ -> v end}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # generic map type should pass too + parent = %{bar: {:map, {:transform, fn v, _ -> v end}}} + data = %{bar: %{foo: "hello"}} + assert {:ok, ^data} = Peri.validate(parent, data) + + # function are indeed being applied? + assert_raise RuntimeError, fn -> + parent = %{bar: {nested, {:transform, fn _, _ -> raise("boom") end}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + + assert_raise RuntimeError, fn -> + parent = %{bar: {:map, {:transform, fn _, _ -> raise("boom") end}}} + data = %{bar: %{foo: "hello"}} + refute {:ok, ^data} = Peri.validate(parent, data) + end + end + end + + def id(v), do: v + def boom(_), do: raise("boom") + defschema(:either_transform, %{ value: {:either, {{:integer, {:transform, &double/1}}, {:string, {:transform, &upcase/1}}}} })