Skip to content

Commit

Permalink
feat: multiple field dependencies dependent type
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Jul 6, 2024
1 parent c85b972 commit 1be99ef
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 38 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ end
- `{:custom, {mod, fun}}` - Validates that the field passes the custom validation function.
- `{:custom, {mod, fun, args}}` - Validates that the field passes the custom validation function.
- `{:dependent, field, condition, type}` - Validates the field based on the value of another field.
- `{:dependent, condition}` - Validates the field based on the value of multiple data values.
- `{:cond, condition, type, else_type}` - Conditional validation based on a condition function.

## Defining Schemas
Expand Down
65 changes: 27 additions & 38 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,6 @@ defmodule Peri do
end
```
## Available Types
- `:string` - Validates that the field is a binary (string).
- `:integer` - Validates that the field is an integer.
- `:float` - Validates that the field is a float.
- `:boolean` - Validates that the field is a boolean.
- `:atom` - Validates that the field is an atom.
- `:any` - Allows any datatype.
- `{:required, type}` - Marks the field as required and validates it according to the specified type.
- `:map` - Validates that the field is a map.
- `{:either, {type_1, type_2}}` - Validates that the field is either of `type_1` or `type_2`.
- `{:oneof, types}` - Validates that the field is at least one of the provided types.
- `{:list, type}` - Validates that the field is a list with elements of the specified type.
- `{:tuple, types}` - Validates that the field is a tuple with the specified types in sequence.
- `{:custom, anonymous_fun_arity_1}` - Validates that the field passes the callback function.
- `{:custom, {MyModule, :my_validation}}` - Validates using a function from a specific module.
- `{:custom, {MyModule, :my_validation, [arg1, arg2]}}` - Same as above but allows extra arguments.
- `{:cond, condition, true_type, else_type}` - Validates the field based on a condition.
- `{:dependent, field, condition, type}` - Validates the field based on another field’s value.
- `{type, {:default, default}}` - Sets a default value for a field if it is nil.
## Error Handling
Peri provides detailed error messages that include the path to the invalid data, the expected and actual values, and custom error messages for custom validations.
Expand Down Expand Up @@ -547,7 +526,31 @@ defmodule Peri do
end
end

defp validate_field(nil, _, _data), do: :ok
defp validate_field(val, {:cond, condition, true_type, else_type}, parser) do
if condition.(parser.data) do
validate_field(val, true_type, parser)
else
validate_field(val, else_type, parser)
end
end

defp validate_field(val, {:dependent, callback}, parser)
when is_function(callback, 1) do
with {:ok, type} <- callback.(parser.data),
{:ok, schema} <- validate_schema(type) do
validate_field(val, schema, parser)
end
end

defp validate_field(val, {:dependent, field, condition, type}, data) do
dependent_val = get_enumerable_value(data, field)

with :ok <- condition.(val, dependent_val) do
validate_field(val, type, data)
end
end

defp validate_field(nil, s, _data) when not is_enumerable(s), do: :ok

defp validate_field(val, {type, {:transform, mapper}}, data)
when is_function(mapper, 1) do
Expand All @@ -570,22 +573,6 @@ defmodule Peri do
apply(mod, fun, [val | args])
end

defp validate_field(val, {:cond, condition, true_type, else_type}, data) do
if condition.(data) do
validate_field(val, true_type, data)
else
validate_field(val, else_type, data)
end
end

defp validate_field(val, {:dependent, field, condition, type}, data) do
dependent_val = get_enumerable_value(data, field)

with :ok <- condition.(val, dependent_val) do
validate_field(val, type, data)
end
end

defp validate_field(val, {:either, {type_1, type_2}}, data) do
with {:error, _, _} <- validate_field(val, type_1, data),
{:error, _, _} <- validate_field(val, type_2, data) do
Expand Down Expand Up @@ -848,6 +835,8 @@ defmodule Peri do
end
end

defp validate_type({:dependent, cb}, _) when is_function(cb, 1), do: :ok

defp validate_type({:dependent, _, cb, type}, p) when is_function(cb, 1) do
validate_type(type, p)
end
Expand Down
115 changes: 115 additions & 0 deletions test/peri_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1660,4 +1660,119 @@ defmodule PeriTest do
numeric_range_validation(%{in_range: 16})
end
end

defmodule TypeDependentSchema do
import Peri

defschema(:email_details, %{email: {:required, :string}})
defschema(:country_details, %{country: {:required, :string}})
defschema(:details, Map.merge(get_schema(:email_details), get_schema(:country_details)))

defschema(:info, %{
name: {:required, :string},
provide_email: {:required, :boolean},
provide_country: {:required, :boolean},
details: {:dependent, &verify_details/1}
})

defp verify_details(data) do
%{provide_email: pe, provide_country: pc} = data

provide = {pe, pc}

case provide do
{true, true} -> {:ok, get_schema(:details)}
{true, false} -> {:ok, get_schema(:email_details)}
{false, true} -> {:ok, get_schema(:country_details)}
{false, false} -> {:ok, nil}
end
end
end

describe "TypeDependentSchema.info/1" do
test "validates correctly when both email and country are provided" do
data = %{
name: "John Doe",
provide_email: true,
provide_country: true,
details: %{
email: "[email protected]",
country: "USA"
}
}

assert {:ok, valid_data} = TypeDependentSchema.info(data)
assert valid_data == data
end

test "validates correctly when only email is provided" do
data = %{
name: "Jane Doe",
provide_email: true,
provide_country: false,
details: %{
email: "[email protected]"
}
}

assert {:ok, valid_data} = TypeDependentSchema.info(data)
assert valid_data == data
end

test "validates correctly when only country is provided" do
data = %{
name: "Jake Doe",
provide_email: false,
provide_country: true,
details: %{country: "Canada"}
}

assert {:ok, valid_data} = TypeDependentSchema.info(data)
assert valid_data == data
end

test "validates correctly when neither email nor country is provided" do
data = %{name: "Jenny Doe", provide_email: false, provide_country: false}

assert {:ok, valid_data} = TypeDependentSchema.info(data)
assert valid_data == data
end

test "returns an error when email is required but not provided" do
data = %{name: "John Doe", provide_email: true, provide_country: false}

assert {
:error,
[
%Peri.Error{
path: [:details],
key: :details,
content: %{actual: "nil", expected: %{email: {:required, :string}}},
message: "expected type of %{email: {:required, :string}} received nil value",
errors: nil
}
]
} =
TypeDependentSchema.info(data)
end

test "returns an error when country is required but not provided" do
data = %{name: "John Doe", provide_email: false, provide_country: true}

assert {
:error,
[
%Peri.Error{
path: [:details],
key: :details,
content: %{actual: "nil", expected: %{country: {:required, :string}}},
message:
"expected type of %{country: {:required, :string}} received nil value",
errors: nil
}
]
} =
TypeDependentSchema.info(data)
end
end
end

0 comments on commit 1be99ef

Please sign in to comment.