Skip to content

Commit

Permalink
added numeric and string validations and comparisons
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Jun 22, 2024
1 parent 848eea7 commit 9bb797e
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

## [0.2.5] - 2024-06-22

### Added

- Numeric and String Validations: Implemented new validation types for numeric and string data, including regex patterns, equality, inequality, range, and length validations. This allows for more granular and specific data validations. [a54a558]

## [0.2.4] - 2024-06-21
- Implemented new type `{type, {:default, default}}`. [a569ecf, 821935f]
- Implemented new type `{type, {:transform, mapper}}`. [785179d]
Expand Down
138 changes: 138 additions & 0 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,16 @@ defmodule Peri do
end
end

defguardp is_numeric(n) when is_integer(n) or is_float(n)
defguardp is_numeric_type(t) when t in [:integer, :float]

@doc false
defp validate_field(nil, nil, _data), do: :ok
defp validate_field(_, :any, _data), do: :ok
defp validate_field(%Date{}, :date, _data), do: :ok
defp validate_field(%Time{}, :time, _data), do: :ok
defp validate_field(%DateTime{}, :datetime, _data), do: :ok
defp validate_field(%NaiveDateTime{}, :naive_datetime, _data), do: :ok
defp validate_field(val, :atom, _data) when is_atom(val), do: :ok
defp validate_field(val, :map, _data) when is_map(val), do: :ok
defp validate_field(val, :string, _data) when is_binary(val), do: :ok
Expand All @@ -307,6 +314,104 @@ defmodule Peri do
defp validate_field([], {:required, {:list, _}}, _data), do: {:error, "cannot be empty", []}
defp validate_field(val, {:required, type}, data), do: validate_field(val, type, data)

defp validate_field(val, {:string, {:regex, regex}}, _data) when is_binary(val) do
if Regex.match?(regex, val) do
:ok
else
{:error, "should match the %{regex} pattern", [regex: regex]}
end
end

defp validate_field(val, {:string, {:eq, eq}}, _data) when is_binary(val) do
if val === eq do
:ok
else
{:error, "should be equal to literal %{literal}", [literal: eq]}
end
end

defp validate_field(val, {:string, {:min, min}}, _data) when is_binary(val) do
if String.length(val) >= min do
:ok
else
{:error, "should have the minimum length of %{length}", [length: min]}
end
end

defp validate_field(val, {:string, {:max, max}}, _data) when is_binary(val) do
if String.length(val) <= max do
:ok
else
{:error, "should have the maximum length of %{length}", [length: max]}
end
end

defp validate_field(val, {type, {:eq, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val == value do
:ok
else
{:error, "should be equal to %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:neq, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val != value do
:ok
else
{:error, "should be not equal to %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:gt, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val > value do
:ok
else
{:error, "should be greater then %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:gte, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val >= value do
:ok
else
{:error, "should be greater then or equal to %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:lte, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val <= value do
:ok
else
{:error, "should be less then or equal to %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:lt, value}}, _data)
when is_numeric_type(type) and is_numeric(val) do
if val < value do
:ok
else
{:error, "should be less then %{value}", [value: value]}
end
end

defp validate_field(val, {type, {:range, {min, max}}}, _data)
when is_numeric_type(type) and is_numeric(val) do
info = [min: min, max: max]
template = "should be in the range of %{min}..%{max} (inclusive)"

cond do
val < min -> {:error, template, info}
val > max -> {:error, template, info}
true -> :ok
end
end

defp validate_field(val, {type, {:default, default}}, data) do
val = if is_nil(val), do: default, else: val

Expand Down Expand Up @@ -560,6 +665,39 @@ 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({:string, {:regex, %Regex{}}}, _p), do: :ok
defp validate_type({:string, {:eq, eq}}, _p) when is_binary(eq), do: :ok
defp validate_type({:string, {:min, min}}, _p) when is_integer(min), do: :ok
defp validate_type({:string, {:max, max}}, _p) when is_integer(max), do: :ok

defp validate_type({type, {:eq, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:neq, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:lt, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:lte, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:gt, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:gte, val}}, _parer)
when is_numeric_type(type) and is_numeric(val),
do: :ok

defp validate_type({type, {:range, {min, max}}}, _parer)
when is_numeric_type(type) and is_numeric(min) and is_numeric(max),
do: :ok

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

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.4"
@version "0.2.5"
@source_url "https://github.com/zoedsoupe/peri"

def project do
Expand Down
148 changes: 148 additions & 0 deletions test/peri_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1512,4 +1512,152 @@ defmodule PeriTest do
assert [%Peri.Error{path: [:info], message: _}] = errors
end
end

defschema(:regex_validation, %{
username: {:string, {:regex, ~r/^[a-zA-Z0-9_]+$/}}
})

defschema(:string_eq_validation, %{
exact_name: {:string, {:eq, "Elixir"}}
})

defschema(:string_min_validation, %{
short_text: {:string, {:min, 5}}
})

defschema(:string_max_validation, %{
long_text: {:string, {:max, 20}}
})

defschema(:numeric_eq_validation, %{
exact_number: {:integer, {:eq, 42}}
})

defschema(:numeric_neq_validation, %{
not_this_number: {:integer, {:neq, 42}}
})

defschema(:numeric_gt_validation, %{
greater_than: {:integer, {:gt, 10}}
})

defschema(:numeric_gte_validation, %{
greater_than_or_equal: {:integer, {:gte, 10}}
})

defschema(:numeric_lt_validation, %{
less_than: {:integer, {:lt, 10}}
})

defschema(:numeric_lte_validation, %{
less_than_or_equal: {:integer, {:lte, 10}}
})

defschema(:numeric_range_validation, %{
in_range: {:integer, {:range, {5, 15}}}
})

describe "regex validation" do
test "validates a string against a regex pattern" do
assert {:ok, %{username: "valid_user"}} = regex_validation(%{username: "valid_user"})

assert {:error, [%Peri.Error{message: "should match the ~r/^[a-zA-Z0-9_]+$/ pattern"}]} =
regex_validation(%{username: "invalid user"})
end
end

describe "string equal validation" do
test "validates a string to be exactly equal to a value" do
assert {:ok, %{exact_name: "Elixir"}} = string_eq_validation(%{exact_name: "Elixir"})

assert {:error, [%Peri.Error{message: "should be equal to literal Elixir"}]} =
string_eq_validation(%{exact_name: "Phoenix"})
end
end

describe "string minimum length validation" do
test "validates a string to have a minimum length" do
assert {:ok, %{short_text: "Hello"}} = string_min_validation(%{short_text: "Hello"})

assert {:error, [%Peri.Error{message: "should have the minimum length of 5"}]} =
string_min_validation(%{short_text: "Hi"})
end
end

describe "string maximum length validation" do
test "validates a string to have a maximum length" do
assert {:ok, %{long_text: "This is a test"}} =
string_max_validation(%{long_text: "This is a test"})

assert {:error, [%Peri.Error{message: "should have the maximum length of 20"}]} =
string_max_validation(%{long_text: "This text is too long for validation"})
end
end

describe "numeric equal validation" do
test "validates a number to be exactly equal to a value" do
assert {:ok, %{exact_number: 42}} = numeric_eq_validation(%{exact_number: 42})

assert {:error, [%Peri.Error{message: "should be equal to 42"}]} =
numeric_eq_validation(%{exact_number: 43})
end
end

describe "numeric not equal validation" do
test "validates a number to not be equal to a value" do
assert {:ok, %{not_this_number: 43}} = numeric_neq_validation(%{not_this_number: 43})

assert {:error, [%Peri.Error{message: "should be not equal to 42"}]} =
numeric_neq_validation(%{not_this_number: 42})
end
end

describe "numeric greater than validation" do
test "validates a number to be greater than a value" do
assert {:ok, %{greater_than: 11}} = numeric_gt_validation(%{greater_than: 11})

assert {:error, [%Peri.Error{message: "should be greater then 10"}]} =
numeric_gt_validation(%{greater_than: 10})
end
end

describe "numeric greater than or equal validation" do
test "validates a number to be greater than or equal to a value" do
assert {:ok, %{greater_than_or_equal: 10}} =
numeric_gte_validation(%{greater_than_or_equal: 10})

assert {:error, [%Peri.Error{message: "should be greater then or equal to 10"}]} =
numeric_gte_validation(%{greater_than_or_equal: 9})
end
end

describe "numeric less than validation" do
test "validates a number to be less than a value" do
assert {:ok, %{less_than: 9}} = numeric_lt_validation(%{less_than: 9})

assert {:error, [%Peri.Error{message: "should be less then 10"}]} =
numeric_lt_validation(%{less_than: 10})
end
end

describe "numeric less than or equal validation" do
test "validates a number to be less than or equal to a value" do
assert {:ok, %{less_than_or_equal: 10}} = numeric_lte_validation(%{less_than_or_equal: 10})

assert {:error, [%Peri.Error{message: "should be less then or equal to 10"}]} =
numeric_lte_validation(%{less_than_or_equal: 11})
end
end

describe "numeric range validation" do
test "validates a number to be within a range" do
assert {:ok, %{in_range: 10}} = numeric_range_validation(%{in_range: 10})

assert {:error, [%Peri.Error{message: "should be in the range of 5..15 (inclusive)"}]} =
numeric_range_validation(%{in_range: 4})

assert {:error, [%Peri.Error{message: "should be in the range of 5..15 (inclusive)"}]} =
numeric_range_validation(%{in_range: 16})
end
end
end

0 comments on commit 9bb797e

Please sign in to comment.