Skip to content

Commit 42bdadd

Browse files
committed
feat(validators): introduce between() method for range validation
Added a new `between()` method to both StringValidator and NumericConstraintsTrait, allowing for concise validation of string lengths and numeric ranges. This enhancement simplifies the validation process by providing a shorthand for existing methods. Updated documentation and tests to reflect these changes.
1 parent 2420cda commit 42bdadd

11 files changed

Lines changed: 107 additions & 5 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88
- `notEmpty()` convenience method for `StringValidator` and `ArrayValidator` to reject empty strings/arrays
9+
- `between()` shorthand for string length and numeric range constraints
910

1011
### Documentation
1112
- Clarified fail-fast behavior per field and schema-level error aggregation in guides and examples

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ $stringValidator = Validator::isString()
309309
### Real-World Validation Gaps (For Consideration)
310310
- [x] **`notEmpty()` method** - Explicit validation that value is not empty string/array (clearer than custom validation)
311311
- [ ] **`in()` alias for `oneOf()`** - More intuitive method name (`->in(['active', 'inactive'])`)
312-
- [ ] **`between(min, max)` for strings** - Length validation shorthand (`->between(3, 50)` instead of `->minLength(3)->maxLength(50)`)
313-
- [ ] **`between(min, max)` for numerics** - Range validation shorthand (`->between(1, 100)` instead of `->min(1)->max(100)`)
312+
- [x] **`between(min, max)` for strings** - Length validation shorthand (`->between(3, 50)` instead of `->minLength(3)->maxLength(50)`)
313+
- [x] **`between(min, max)` for numerics** - Range validation shorthand (`->between(1, 100)` instead of `->min(1)->max(100)`)
314314
- [ ] **`filled()` method** - Requires non-null AND non-empty (stricter than required())
315315
- [ ] **`when()` conditional validation** - Apply validation only when condition is met (`->when($userRole === 'admin', fn($v) => $v->required())`)
316316

TASKS.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ Public repo; keep this list short (about 7) and up to date. Rules: no numbering
1111
- `in()` alias for `oneOf()`
1212
- Add `in()` method as an alias for `oneOf()` for more intuitive API (`->in(['active', 'inactive'])` reads better than `->oneOf()`).
1313

14-
- `between()` convenience methods
15-
- Add `between(min, max)` shorthand for strings (length) and numerics (range) to reduce boilerplate (`->between(3, 50)` vs `->minLength(3)->maxLength(50)`).
16-
1714
- Mutation testing pilot
1815
- Wire Infection (or similar) to the test suite, add baseline config, and document how to run it locally for enhanced test quality verification.
1916

docs/guides/numeric-validation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ $rangeValidator = Validator::isInt()->min(1)->max(10);
107107
$rating = $rangeValidator->validate(8); // Valid
108108
```
109109

110+
Use `between(min, max)` as a shorthand for the inclusive range:
111+
112+
```php
113+
$rangeValidator = Validator::isInt()->between(1, 10);
114+
$rating = $rangeValidator->validate(8); // Valid
115+
```
116+
110117
### Sign Constraints
111118

112119
```php

docs/guides/string-validation.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,17 @@ $result = $exactLengthValidator->validate('Hello'); // Valid (exactly 5 chars)
262262
// $exactLengthValidator->validate('Too Long'); // ❌ ValidationException (8 chars)
263263
```
264264

265+
### Length Between Bounds
266+
267+
Use `between(min, max)` as a shorthand for `minLength()` + `maxLength()`:
268+
269+
```php
270+
$validator = Validator::isString()->between(3, 8);
271+
$validator->validate('Hello'); // Valid
272+
// $validator->validate('Hi'); // ❌ ValidationException (too short)
273+
// $validator->validate('Too long'); // ❌ ValidationException (too long)
274+
```
275+
265276
### Non-Empty Strings
266277

267278
Use `notEmpty()` as a clearer alternative to `minLength(1)` when you only want to reject empty strings.

llms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ date(string $format = 'Y-m-d', ?string $message = null): static
8282
minLength(int $min, ?string $message = null): static
8383
maxLength(int $max, ?string $message = null): static
8484
length(int $exact, ?string $message = null): static
85+
between(int $min, int $max, ?string $message = null): static
8586
notEmpty(?string $message = null): static
8687
oneOf(array $values, ?string $message = null): static
8788
```
@@ -91,6 +92,7 @@ oneOf(array $values, ?string $message = null): static
9192
```php
9293
min(int|float $min, ?string $message = null): static
9394
max(int|float $max, ?string $message = null): static
95+
between(int|float $min, int|float $max, ?string $message = null): static
9496
gt(int|float $threshold, ?string $message = null): static
9597
gte(int|float $threshold, ?string $message = null): static
9698
lt(int|float $threshold, ?string $message = null): static

src/Lemmon/Validator/NumericConstraintsTrait.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ public function max(int|float $max, null|string $message = null): static
4242
);
4343
}
4444

45+
/**
46+
* Validates that the value falls within the provided inclusive range.
47+
*
48+
* @param int|float $min The minimum value.
49+
* @param int|float $max The maximum value.
50+
* @param ?string $message Custom error message.
51+
* @return static
52+
*/
53+
public function between(int|float $min, int|float $max, null|string $message = null): static
54+
{
55+
if ($message === null) {
56+
return $this->min($min)->max($max);
57+
}
58+
59+
return $this->min($min, $message)->max($max, $message);
60+
}
61+
4562
/**
4663
* Validates that the value is greater than the given threshold.
4764
*

src/Lemmon/Validator/StringValidator.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ public function length(int $exact, null|string $message = null): static
144144
);
145145
}
146146

147+
public function between(int $min, int $max, null|string $message = null): static
148+
{
149+
if ($message === null) {
150+
return $this->minLength($min)->maxLength($max);
151+
}
152+
153+
return $this->minLength($min, $message)->maxLength($max, $message);
154+
}
155+
147156
public function notEmpty(null|string $message = null): static
148157
{
149158
return $this->minLength(1, $message ?? 'Value must not be empty');

tests/FloatValidatorTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@
4646
$rangeValidator->validate(5);
4747
})->throws(ValidationException::class);
4848

49+
it('should validate floats between bounds', function () {
50+
$validator = Validator::isFloat()->between(1.5, 2.5);
51+
52+
expect($validator->validate(1.5))->toBe(1.5);
53+
expect($validator->validate(2.0))->toBe(2.0);
54+
expect($validator->validate(2.5))->toBe(2.5);
55+
56+
$validator->validate(1.4);
57+
})->throws(ValidationException::class, 'Value must be at least 1.5');
58+
59+
it('should reject floats above the between range', function () {
60+
$validator = Validator::isFloat()->between(1.5, 2.5);
61+
$validator->validate(2.6);
62+
})->throws(ValidationException::class, 'Value must be at most 2.5');
63+
64+
it('should use custom error message for float between validation', function () {
65+
$validator = Validator::isFloat()->between(1.5, 2.5, 'Out of range');
66+
$validator->validate(3.0);
67+
})->throws(ValidationException::class, 'Out of range');
68+
4969
it('should validate float multiples', function () {
5070
$multipleValidator = Validator::isFloat()->multipleOf(5);
5171

tests/IntValidatorTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@
3535
$rangeValidator->validate(5);
3636
})->throws(ValidationException::class, 'Value must be at least 10');
3737

38+
it('should validate integers between bounds', function () {
39+
$validator = Validator::isInt()->between(10, 20);
40+
41+
expect($validator->validate(10))->toBe(10);
42+
expect($validator->validate(15))->toBe(15);
43+
expect($validator->validate(20))->toBe(20);
44+
45+
$validator->validate(9);
46+
})->throws(ValidationException::class, 'Value must be at least 10');
47+
48+
it('should reject integers above the between range', function () {
49+
$validator = Validator::isInt()->between(10, 20);
50+
$validator->validate(21);
51+
})->throws(ValidationException::class, 'Value must be at most 20');
52+
53+
it('should use custom error message for integer between validation', function () {
54+
$validator = Validator::isInt()->between(1, 5, 'Out of range');
55+
$validator->validate(0);
56+
})->throws(ValidationException::class, 'Out of range');
57+
3858
it('should validate integer multiples', function () {
3959
$multipleValidator = Validator::isInt()->multipleOf(5);
4060

0 commit comments

Comments
 (0)