Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/pkg/
/spec/reports/
/tmp/
.idea/

# rspec failure tracking
.rspec_status
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `non_empty_string` - combines required, trim, and non_empty
- `trimmed_email` - combines trim, lowercase, and email
- `Result#value` attribute to access transformed values
- **ArrayValidator**:
- Size validators: `min_items`, `max_items`, `length`, `non_empty`
- Content validators: `unique`, `contains` / `includes`
- Element validation: `of(validator)` for nested validation
- Transformations: `compact`, `flatten`
- **Structured Error Object**:
- New `ValidationError` class with `message`, `code`, `path`, and `meta` attributes.
- Specific error codes for all validators (e.g., `:too_short`, `:invalid_email`).
Expand Down
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A fluent, type-safe schema validation library for Ruby inspired by Zod. Build complex validations with chainable methods, custom error messages, and built-in transformations.

[![Ruby](https://img.shields.io/badge/ruby-3.2+-red.svg)](https://www.ruby-lang.org)
[![Tests](https://img.shields.io/badge/tests-143%20passing-success.svg)](spec/)
[![Tests](https://img.shields.io/badge/tests-166%20passing-success.svg)](spec/)
[![Coverage](https://img.shields.io/badge/coverage-100%25-success.svg)](coverage/)

## Features
Expand Down Expand Up @@ -207,13 +207,60 @@ discount_validator = ValidatorRb.integer
.min(0)
.max(100)
.optional

# Minimum items
ValidatorRb.array.min_items(1).validate([1, 2, 3])

# Maximum items
ValidatorRb.array.max_items(5).validate([1, 2, 3])

# Exact length
ValidatorRb.array.length(3).validate([1, 2, 3])

# Not empty
ValidatorRb.array.non_empty.validate([1])
```

### Content Validation

```ruby
# Unique elements
ValidatorRb.array.unique.validate([1, 2, 3])

# Contains specific element
ValidatorRb.array.contains("admin").validate(["user", "admin"])
ValidatorRb.array.includes(1).validate([1, 2, 3])
```

## Transformations
### Element Validation

Transformations modify the value **before** validation runs. The transformed value is available in `result.value`.
Validate each element in the array using another validator:

```ruby
# Array of strings
ValidatorRb.array.of(ValidatorRb.string).validate(["a", "b"])

# Array of positive integers
ValidatorRb.array.of(ValidatorRb.integer.positive).validate([1, 2, 3])

# Nested arrays
inner = ValidatorRb.array.of(ValidatorRb.integer)
ValidatorRb.array.of(inner).validate([[1, 2], [3, 4]])
```

### Transformations

```ruby
# Compact (remove nil)
validator = ValidatorRb.array.compact
result = validator.validate([1, nil, 2])
result.value # => [1, 2]

# Flatten
validator = ValidatorRb.array.flatten
result = validator.validate([[1, 2], [3, 4]])
result.value # => [1, 2, 3, 4]

# Trim whitespace
validator = ValidatorRb.string.trim
result = validator.validate(" hello ")
Expand Down Expand Up @@ -333,8 +380,6 @@ result.error_message # String: errors joined by ", "
result.value # Object: transformed value (or original if no transformations)

### ValidationError Object

```ruby
error = result.errors.first

error.message # String: Human-readable error message
Expand All @@ -343,20 +388,21 @@ error.path # Array: Path to the invalid field (default: [])
error.meta # Hash: Additional context (default: {})
error.to_h # Hash: Serialized error representation
```
```

## Development

After checking out the repo, run:

```bash

bundle install
bundle exec rspec
```

To run tests with coverage:

```bash

bundle exec rspec --format documentation
```

Expand All @@ -374,7 +420,7 @@ Bug reports and pull requests are welcome on GitHub!

## Roadmap

- [ ] Additional validators (float, boolean, array, hash)
- [ ] Additional validators (float, boolean, hash)
- [ ] Async validation support
- [ ] Conditional validations
- [ ] Custom validator registration
Expand Down
68 changes: 68 additions & 0 deletions examples/array_validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

require_relative "../lib/validator_rb"

puts "=== Array Validation Example ==="

# 1. Basic Array Validation
puts "\n1. Basic Array Validation:"
validator = ValidatorRb.array.min_items(2).max_items(5).unique
result = validator.validate([1, 2, 3])
puts "Input: [1, 2, 3]"
puts "Valid? #{result.success?}"

result = validator.validate([1])
puts "Input: [1]"
puts "Valid? #{result.success?}"
puts "Errors: #{result.error_message}"

result = validator.validate([1, 2, 2])
puts "Input: [1, 2, 2]"
puts "Valid? #{result.success?}"
puts "Errors: #{result.error_message}"

# 2. Element Validation (Array of Emails)
puts "\n2. Element Validation (Array of Emails):"
email_validator = ValidatorRb.string.email
array_validator = ValidatorRb.array.of(email_validator)

emails = ["valid@example.com", "invalid-email", "another@example.com"]
result = array_validator.validate(emails)

puts "Input: #{emails.inspect}"
puts "Valid? #{result.success?}"
if result.failure?
result.errors.each do |error|
puts "- Error at index #{error.path[0]}: #{error.message} (Code: #{error.code})"
end
end

# 3. Nested Array Validation
puts "\n3. Nested Array Validation:"
# Matrix of positive integers
inner_validator = ValidatorRb.array.of(ValidatorRb.integer.positive)
matrix_validator = ValidatorRb.array.of(inner_validator)

matrix = [
[1, 2, 3],
[4, -5, 6], # Error here
[7, 8, 9]
]

result = matrix_validator.validate(matrix)
puts "Input: #{matrix.inspect}"
puts "Valid? #{result.success?}"
if result.failure?
result.errors.each do |error|
puts "- Error at path #{error.path}: #{error.message}"
end
end

# 4. Transformations
puts "\n4. Transformations (Compact & Flatten):"
validator = ValidatorRb.array.compact.flatten
input = [1, nil, [2, 3], nil, [4, [5]]]
result = validator.validate(input)

puts "Input: #{input.inspect}"
puts "Output: #{result.value.inspect}"
2 changes: 2 additions & 0 deletions examples/structured_errors.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require_relative "../lib/validator_rb"

puts "=== Structured Error Handling Example ==="
Expand Down
12 changes: 12 additions & 0 deletions lib/validator_rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative "validator_rb/base_validator"
require_relative "validator_rb/string_validator"
require_relative "validator_rb/integer_validator"
require_relative "validator_rb/array_validator"

# Main ValidatorRb module
#
Expand Down Expand Up @@ -47,5 +48,16 @@ def string
def integer
IntegerValidator.new
end

# Creates a new ArrayValidator instance
#
# @return [ArrayValidator] a new array validator instance
#
# @example
# validator = ValidatorRb.array.min_items(1)
# result = validator.validate([1, 2, 3])
def array
ArrayValidator.new
end
end
end
135 changes: 135 additions & 0 deletions lib/validator_rb/array_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# frozen_string_literal: true

module ValidatorRb
# Validator for Array types
class ArrayValidator < BaseValidator
def initialize
super
add_validation(code: :not_array) { |value| value.is_a?(Array) || "must be an array" }
end

# Overrides validate to flatten nested errors returned by array validations
def validate(value)
result = super
result.errors.flatten!
result
end

# Validates that the array has at least the specified number of items
#
# @param count [Integer] minimum number of items
# @param message [String, nil] custom error message
# @return [self]
def min_items(count, message: nil)
add_validation(message: message, code: :min_items) do |value|
value.length >= count || "must have at least #{count} items"
end
end

# Validates that the array has at most the specified number of items
#
# @param count [Integer] maximum number of items
# @param message [String, nil] custom error message
# @return [self]
def max_items(count, message: nil)
add_validation(message: message, code: :max_items) do |value|
value.length <= count || "must have at most #{count} items"
end
end

# Validates that the array has exactly the specified number of items
#
# @param count [Integer] exact number of items
# @param message [String, nil] custom error message
# @return [self]
def length(count, message: nil)
add_validation(message: message, code: :length) do |value|
value.length == count || "must have exactly #{count} items"
end
end

# Validates that the array is not empty
#
# @param message [String, nil] custom error message
# @return [self]
def non_empty(message: nil)
add_validation(message: message, code: :empty) do |value|
!value.empty? || "cannot be empty"
end
end

# Validates that the array contains unique elements
#
# @param message [String, nil] custom error message
# @return [self]
def unique(message: nil)
add_validation(message: message, code: :unique) do |value|
value.uniq.length == value.length || "must contain unique elements"
end
end

# Validates that the array contains a specific element
#
# @param element [Object] element that must be present
# @param message [String, nil] custom error message
# @return [self]
def contains(element, message: nil)
add_validation(message: message, code: :missing_element) do |value|
value.include?(element) || "must contain #{element.inspect}"
end
end
alias includes contains

# Validates each element of the array using another validator
#
# @param validator [BaseValidator] validator to apply to each element
# @return [self]
def of(validator)
validation = lambda do |value|
errors = validate_elements(value, validator)
errors.empty? || errors
end
@validations << validation
self
end

# Transformation: Removes nil values from the array
#
# @return [self]
def compact
add_transformation(&:compact)
end

# Transformation: Flattens nested arrays
#
# @param level [Integer, nil] recursion level (default: nil for all levels)
# @return [self]
def flatten(level = nil)
add_transformation { |value| level ? value.flatten(level) : value.flatten }
end

private

def validate_elements(value, validator)
errors = []
value.each_with_index do |item, index|
result = validator.validate(item)
next if result.success?

result.errors.each do |error|
errors << create_nested_error(error, index)
end
end
errors
end

def create_nested_error(error, index)
ValidationError.new(
error.message,
error.code,
path: [index] + error.path,
meta: error.meta
)
end
end
end
Loading
Loading