diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4016206 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +**Setup:** +```bash +mix deps.get # Install dependencies +``` + +**Testing:** +```bash +mix test # Run all tests +mix test test/valdi_test.exs # Run specific test file +mix coveralls # Run tests with coverage +mix coveralls.html # Generate HTML coverage report +``` + +**Code Quality:** +```bash +mix format # Format code according to .formatter.exs +mix docs # Generate documentation +``` + +**Build:** +```bash +mix compile # Compile the project +``` + +## Project Architecture + +**Valdi** is an Elixir data validation library that provides comprehensive validation functions for different data types and structures. + +### Core Module Structure + +The main validation logic is contained in a single module `Valdi` (lib/valdi.ex) with these key functions: + +- **Main validation functions:** + - `validate/2` - Main validation function that accepts value and list of validators + - `validate_list/2` - Validates each item in a list against given validators + - `validate_map/2` - Validates map values against a validation specification + +- **Individual validators:** + - `validate_type/2` - Type checking (supports built-in types, structs, arrays) + - `validate_required/2` - Required field validation + - `validate_number/2` - Number range validation (min/max/equal_to/greater_than/less_than) + - `validate_decimal/2` - Decimal number validation using Decimal library + - `validate_length/2` - Length validation for strings, lists, maps, tuples + - `validate_format/2` - Regex pattern matching for strings (also accessible via `pattern` alias) + - `validate_inclusion/2` & `validate_exclusion/2` - Value inclusion/exclusion in enumerables + - `validate_each_item/2` - Applies validation to each array element + +### Validation Flow + +1. `validate/2` calls `prepare_validator/1` to prioritize validators (required → type → others) +2. `do_validate/3` processes validators sequentially, stopping at first error +3. Individual validator functions return `:ok` or `{:error, message}` +4. For list/map validation, errors include indexes/keys for failed items + +### Supported Types + +Built-in types: `:boolean`, `:integer`, `:float`, `:number`, `:string`/`:binary`, `:tuple`, `:array`/`:list`, `:atom`, `:function`, `:map`, `:keyword`, `:decimal`, `:date`, `:time`, `:datetime`, `:naive_datetime`, `:utc_datetime` + +Extended types: struct modules (e.g., `User`), `{:array, type}` for typed arrays + +### Testing + +The test suite in test/valdi_test.exs provides comprehensive coverage with parameterized tests for different validation scenarios. Tests use ExUnit with doctest for embedded examples. \ No newline at end of file diff --git a/lib/valdi.ex b/lib/valdi.ex index c5540d3..1ff0e8c 100644 --- a/lib/valdi.ex +++ b/lib/valdi.ex @@ -35,6 +35,7 @@ defmodule Valdi do :type, :required, :format, + :pattern, :number, :length, :in, @@ -55,7 +56,7 @@ defmodule Valdi do **All supported validations**: - `type`: validate datatype - - `format`: check if binary value matched given regex + - `format`|`pattern`: check if binary value matched given regex - `number`: validate number value - `length`: validate length of supported types. See `validate_length/2` for more details. - `in`: validate inclusion @@ -173,6 +174,7 @@ defmodule Valdi do defp get_validator(:type), do: &validate_type/2 defp get_validator(:required), do: &validate_required/2 defp get_validator(:format), do: &validate_format/2 + defp get_validator(:pattern), do: &validate_format/2 defp get_validator(:number), do: &validate_number/2 defp get_validator(:length), do: &validate_length/2 defp get_validator(:in), do: &validate_inclusion/2 @@ -439,17 +441,26 @@ defmodule Valdi do defp get_length(_param), do: {:error, :wrong_type} @doc """ - Checks whether a string match the given regex. + Checks whether a string match the given regex pattern. ```elixir iex> Valdi.validate_format("year: 2001", ~r/year:\\s\\d{4}/) :ok iex> Valdi.validate_format("hello", ~r/\d+/) {:error, "does not match format"} + iex> Valdi.validate_format("hello", "h.*o") + :ok ``` """ - @spec validate_format(String.t(), Regex.t()) :: + @spec validate_format(String.t(), Regex.t() | String.t()) :: :ok | error + def validate_format(value, check) when is_binary(value) and is_binary(check) do + case Regex.compile(check) do + {:ok, regex} -> validate_format(value, regex) + {:error, _} -> {:error, "invalid regex pattern"} + end + end + def validate_format(value, check) when is_binary(value) do if Regex.match?(check, value), do: :ok, else: {:error, "does not match format"} end diff --git a/test/valdi_test.exs b/test/valdi_test.exs index c3f5d85..c00dda9 100644 --- a/test/valdi_test.exs +++ b/test/valdi_test.exs @@ -108,6 +108,38 @@ defmodule ValdiTest do Valdi.validate(10, type: :integer, format: ~r/year:\s\d{4}/) end + test "validate pattern with match string should ok" do + assert :ok = Valdi.validate("year: 1999", type: :string, pattern: ~r/year:\s\d{4}/) + end + + test "validate pattern with not match string should error" do + assert {:error, "does not match format"} = + Valdi.validate("", type: :string, pattern: ~r/year:\s\d{4}/) + end + + test "validate pattern with number should error" do + assert {:error, "format check only support string"} = + Valdi.validate(10, type: :integer, pattern: ~r/year:\s\d{4}/) + end + + test "validate format with string pattern should ok" do + assert :ok = Valdi.validate("hello world", type: :string, format: "h.*d") + end + + test "validate format with string pattern not match should error" do + assert {:error, "does not match format"} = + Valdi.validate("hello", type: :string, format: "\\d+") + end + + test "validate format with invalid string pattern should error" do + assert {:error, "invalid regex pattern"} = + Valdi.validate("hello", type: :string, format: "[") + end + + test "validate pattern with string pattern should ok" do + assert :ok = Valdi.validate("test123", type: :string, pattern: "test\\d+") + end + @number_tests [ [:equal_to, 10, 10, :ok], [:equal_to, 10, 11, :error],