Skip to content

Commit

Permalink
Correctly handle nested results on transformations (#15)
Browse files Browse the repository at this point in the history
* fix: correctly handle nested values on transformations

* add test cases for mfa with args and dependent fields too
  • Loading branch information
zoedsoupe authored Nov 26, 2024
1 parent 6520de5 commit 596d849
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 6 deletions.
40 changes: 34 additions & 6 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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])}
Expand All @@ -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])}
Expand All @@ -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

Expand Down
105 changes: 105 additions & 0 deletions test/peri_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}}
})
Expand Down

0 comments on commit 596d849

Please sign in to comment.