Skip to content

Commit

Permalink
feat: transform type
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Jun 21, 2024
1 parent 821935f commit 848eea7
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## [0.2.4] - 2024-06-21
- Implemented new type `{type, {:default, default}}`. [a569ecf, 821935f]
- Implemented new type `{type, {:transform, mapper}}`. [785179d]

## [0.2.3] - 2024-06-18

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Peri supports a variety of types to ensure your data is validated accurately. Be
| `{:cond, condition, true_type, else_type}` | Conditionally validates a field based on the result of a condition function. |
| `{:dependent, field, condition, type}` | Validates a field based on the value of another field. |
| `{type, {:default, default}}` | Validates a field exists based on `type`, if not, return the `default` value |
| `{type, {:transform, mapper}}` | Validates a field have valid `type`, if yes, return the return of the `mapper/1` function passing the value |

These types provide flexibility and control over how data is validated, enabling robust and precise schema definitions.

Expand Down
30 changes: 25 additions & 5 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,13 @@ defmodule Peri do

defp validate_field(nil, _, _data), do: :ok

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)}
end
end

defp validate_field(val, {:custom, callback}, _data) when is_function(callback, 1) do
callback.(val)
end
Expand Down Expand Up @@ -361,15 +368,19 @@ defmodule Peri do
|> Enum.reduce_while(:error, fn type, :error ->
case validate_field(val, type, data) do
:ok -> {:halt, :ok}
{:ok, val} -> {:halt, {:ok, val}}
{:error, _reason, _info} -> {:cont, :error}
end
end)
|> then(fn
:ok ->
:ok

{:ok, val} ->
{:ok, val}

:error ->
expected = Enum.map_join(types, " or ", &to_string/1)
expected = Enum.map_join(types, " or ", &inspect/1)
info = [oneof: expected, actual: inspect(val)]
template = "expected one of %{oneof}, got: %{actual}"

Expand Down Expand Up @@ -417,20 +428,26 @@ defmodule Peri do
end

defp validate_field(data, {:list, type}, source) when is_list(data) do
Enum.reduce_while(data, {:ok, nil}, fn el, {:ok, val} ->
Enum.reduce_while(data, {:ok, []}, fn el, {:ok, vals} ->
case validate_field(el, type, source) do
:ok -> {:cont, {:ok, val}}
{:ok, val} -> {:cont, {:ok, val}}
:ok -> {:cont, {:ok, vals}}
{:ok, val} -> {:cont, {:ok, [val | vals]}}
{:error, errors} -> {:halt, {:error, errors}}
{:error, reason, info} -> {:halt, {:error, reason, info}}
end
end)
|> then(fn
{:ok, _} -> :ok
{:ok, []} -> :ok
{:ok, val} -> {:ok, Enum.reverse(val)}
err -> err
end)
end

defp validate_field(data, schema, _data)
when is_enumerable(data) and not is_enumerable(schema) do
{:error, "expected a nested schema but received schema: %{type}", [type: schema]}
end

defp validate_field(data, schema, _data) when is_enumerable(data) do
case traverse_schema(schema, Peri.Parser.new(data)) do
%Peri.Parser{errors: []} = parser -> {:ok, parser.data}
Expand Down Expand Up @@ -543,6 +560,9 @@ defmodule Peri do
defp validate_type({type, {:default, _val}}, p), do: validate_type(type, p)
defp validate_type({:enum, choices}, _) when is_list(choices), do: :ok

defp validate_type({type, {:transform, mapper}}, p) when is_function(mapper, 1),
do: validate_type(type, p)

defp validate_type({:required, {type, {:default, val}}}, _) do
template = "cannot set default value of %{value} for required field of type %{type}"
{:error, template, [value: val, type: type]}
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Peri.MixProject do
use Mix.Project

@version "0.2.3"
@version "0.2.4"
@source_url "https://github.com/zoedsoupe/peri"

def project do
Expand Down
170 changes: 170 additions & 0 deletions test/peri_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1342,4 +1342,174 @@ defmodule PeriTest do
assert {:ok, ^data} = simple_tuple(data)
end
end

defp double(x), do: x * 2
defp upcase(str), do: String.upcase(str)

defschema(:basic_transform, %{
number: {:integer, {:transform, &double/1}},
name: {:string, {:transform, &upcase/1}}
})

defschema(:nested_transform, %{
user: %{
age: {:integer, {:transform, &double/1}},
profile: %{
nickname: {:string, {:transform, &upcase/1}}
}
}
})

defschema(:list_transform, %{
scores: {:list, {:integer, {:transform, &double/1}}}
})

describe "basic transform schema" do
test "applies transform function correctly" do
data = %{number: 5, name: "john"}
expected = %{number: 10, name: "JOHN"}
assert {:ok, ^expected} = basic_transform(data)
end
end

describe "nested transform schema" do
test "applies transform function correctly in nested schema" do
data = %{user: %{age: 5, profile: %{nickname: "john"}}}
expected = %{user: %{age: 10, profile: %{nickname: "JOHN"}}}
assert {:ok, ^expected} = nested_transform(data)
end
end

describe "list transform schema" do
test "applies transform function correctly in list schema" do
data = %{scores: [1, 2, 3]}
expected = %{scores: [2, 4, 6]}
assert {:ok, ^expected} = list_transform(data)
end
end

defschema(:either_transform, %{
value: {:either, {{:integer, {:transform, &double/1}}, {:string, {:transform, &upcase/1}}}}
})

defschema(:either_transform_mixed, %{
value: {:either, {{:integer, {:transform, &double/1}}, :string}}
})

defschema(:oneof_transform, %{
value: {:oneof, [{:integer, {:transform, &double/1}}, {:string, {:transform, &upcase/1}}]}
})

describe "either transform schema" do
test "applies transform function correctly for integer type" do
data = %{value: 5}
expected = %{value: 10}
assert {:ok, ^expected} = either_transform(data)
end

test "applies transform function correctly for string type" do
data = %{value: "john"}
expected = %{value: "JOHN"}
assert {:ok, ^expected} = either_transform(data)
end
end

describe "either transform mixed schema" do
test "applies transform function correctly for integer type" do
data = %{value: 5}
expected = %{value: 10}
assert {:ok, ^expected} = either_transform_mixed(data)
end

test "applies transform function correctly for string type" do
data = %{value: "john"}
assert {:ok, ^data} = either_transform_mixed(data)
end
end

describe "oneof transform schema" do
test "applies transform function correctly for integer type" do
data = %{value: 5}
expected = %{value: 10}
assert {:ok, ^expected} = oneof_transform(data)
end

test "applies transform function correctly for string type" do
data = %{value: "john"}
expected = %{value: "JOHN"}
assert {:ok, ^expected} = oneof_transform(data)
end
end

defschema(:mixed, %{
id: {:integer, {:transform, &(&1 * 2)}},
name:
{:either,
{{:string, {:transform, &String.upcase/1}}, {:atom, {:transform, &Atom.to_string/1}}}},
tags: {:list, :string},
info:
{:oneof,
[
{:string, {:transform, &String.upcase/1}},
%{
age: {:integer, {:transform, &(&1 + 1)}},
address: %{
street: :string,
number: :integer
}
}
]}
})

describe "massive mixed schema validation" do
test "validates and transforms mixed schema with integer id and string info" do
data = %{id: 5, name: :john, tags: ["elixir", "programming"], info: "some info"}

expected = %{
id: 10,
name: "john",
tags: ["elixir", "programming"],
info: "SOME INFO"
}

assert {:ok, ^expected} = mixed(data)
end

test "validates and transforms mixed schema with integer id and map info" do
data = %{
id: 7,
name: "jane",
tags: ["elixir", "programming"],
info: %{age: 25, address: %{street: "Main St", number: 123}}
}

expected = %{
id: 14,
name: "JANE",
tags: ["elixir", "programming"],
info: %{age: 26, address: %{street: "Main St", number: 123}}
}

assert {:ok, ^expected} = mixed(data)
end

test "validates and transforms mixed schema with atom name" do
data = %{id: 8, name: :doe, tags: ["elixir"], info: "details"}

expected = %{
id: 16,
name: "doe",
tags: ["elixir"],
info: "DETAILS"
}

assert {:ok, ^expected} = mixed(data)
end

test "returns error for invalid info type in mixed schema" do
data = %{id: 8, name: "doe", tags: ["elixir"], info: 123}
assert {:error, errors} = mixed(data)
assert [%Peri.Error{path: [:info], message: _}] = errors
end
end
end

0 comments on commit 848eea7

Please sign in to comment.