diff --git a/CHANGELOG.md b/CHANGELOG.md index fdde020..3bc7722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/lib/peri.ex b/lib/peri.ex index 8fa750a..3e7dbd8 100644 --- a/lib/peri.ex +++ b/lib/peri.ex @@ -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 @@ -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 @@ -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) diff --git a/mix.exs b/mix.exs index fcf4738..15a94f3 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/test/peri_test.exs b/test/peri_test.exs index 08f64d0..f4390e8 100644 --- a/test/peri_test.exs +++ b/test/peri_test.exs @@ -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