diff --git a/docs/validators.md b/docs/validators.md index 958731a6..6413105f 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -49,7 +49,7 @@ In this page you will find a list of validators by their category. **Objects**: [Attributes][] - [Instance][] - [ObjectType][] - [Property][] - [PropertyExists][] - [PropertyOptional][] -**Strings**: [Alnum][] - [Alpha][] - [Base64][] - [Charset][] - [Consonant][] - [Contains][] - [ContainsAny][] - [ContainsCount][] - [Control][] - [Digit][] - [Emoji][] - [EndsWith][] - [Format][] - [Graph][] - [HexRgbColor][] - [In][] - [Json][] - [Lowercase][] - [Phone][] - [PostalCode][] - [Printable][] - [Punct][] - [Regex][] - [Slug][] - [Sorted][] - [Space][] - [Spaced][] - [StartsWith][] - [StringType][] - [StringVal][] - [Uppercase][] - [Uuid][] - [Version][] - [Vowel][] - [Xdigit][] +**Strings**: [Alnum][] - [Alpha][] - [Base64][] - [Charset][] - [Consonant][] - [Contains][] - [ContainsAny][] - [ContainsCount][] - [Control][] - [Digit][] - [Emoji][] - [EndsWith][] - [Format][] - [Graph][] - [HexRgbColor][] - [In][] - [Json][] - [Lowercase][] - [Phone][] - [PostalCode][] - [Printable][] - [Punct][] - [Regex][] - [Slug][] - [Sorted][] - [Space][] - [Spaced][] - [StartsWith][] - [StringType][] - [StringVal][] - [Trimmed][] - [Uppercase][] - [Uuid][] - [Version][] - [Vowel][] - [Xdigit][] **Structures**: [Attributes][] - [Key][] - [KeyExists][] - [KeyOptional][] - [KeySet][] - [Property][] - [PropertyExists][] - [PropertyOptional][] @@ -203,6 +203,7 @@ In this page you will find a list of validators by their category. - [Templated][] - `v::templated('You must provide a valid email', v::email())->assert('foo@bar.com');` - [Time][] - `v::time()->assert('00:00:00');` - [Tld][] - `v::tld()->assert('com');` +- [Trimmed][] - `v::trimmed()->assert('lorem ipsum');` - [TrueVal][] - `v::trueVal()->assert(true);` - [Undef][] - `v::undef()->assert('');` - [UndefOr][] - `v::undefOr(v::alpha())->assert('');` @@ -351,7 +352,7 @@ In this page you will find a list of validators by their category. [Sorted]: validators/Sorted.md "Validates whether the input is sorted in a certain order or not." [Space]: validators/Space.md "Validates whether the input contains only whitespaces characters." [Spaced]: validators/Spaced.md "Validates if a string contains at least one whitespace (spaces, tabs, or line breaks);" -[StartsWith]: validators/StartsWith.md "Validates whether the input starts with a given value." +[StartsWith]: validators/StartsWith.md "Validates whether the input starts with one of the given values." [StringType]: validators/StringType.md "Validates whether the type of an input is string or not." [StringVal]: validators/StringVal.md "Validates whether the input can be used as a string." [SubdivisionCode]: validators/SubdivisionCode.md "Validates subdivision country codes according to ISO 3166-2." @@ -360,6 +361,7 @@ In this page you will find a list of validators by their category. [Templated]: validators/Templated.md "Defines a validator with a custom message template." [Time]: validators/Time.md "Validates whether an input is a time or not. The `$format` argument should be in" [Tld]: validators/Tld.md "Validates whether the input is a top-level domain." +[Trimmed]: validators/Trimmed.md "Validates whether the input string does not start or end with the given values." [TrueVal]: validators/TrueVal.md "Validates if a value is considered as `true`." [Undef]: validators/Undef.md "Validates if the given input is undefined. By _undefined_ we consider `null` or an empty string (`''`)." [UndefOr]: validators/UndefOr.md "Validates the input using a defined validator when the input is not `null` or an empty string (`''`)." diff --git a/docs/validators/EndsWith.md b/docs/validators/EndsWith.md index a6010780..ac4d889d 100644 --- a/docs/validators/EndsWith.md +++ b/docs/validators/EndsWith.md @@ -8,15 +8,22 @@ SPDX-FileContributor: Henrique Moody # EndsWith - `EndsWith(mixed $endValue)` +- `EndsWith(mixed $endValue, mixed ...$endValues)` This validator is similar to `Contains()`, but validates -only if the value is at the end of the input. +only if one of the values is at the end of the input. Only +string inputs and string end values are checked; non‑string +values are considered invalid but will not produce PHP errors +thanks to internal type guards. -For strings: +For strings (non-string inputs are always rejected): ```php v::endsWith('ipsum')->assert('lorem ipsum'); // Validation passes successfully + +v::endsWith(', PhD', ', doctor')->assert('Jane Doe, PhD'); +// Validation passes successfully ``` For arrays: @@ -24,9 +31,15 @@ For arrays: ```php v::endsWith('ipsum')->assert(['lorem', 'ipsum']); // Validation passes successfully + +v::endsWith('.', ';')->assert(['this', 'is', 'a', 'tokenized', 'phrase', '.']); +// Validation passes successfully + +v::endsWith('.', ';')->assert(['this', 'is', 'a', 'tokenized', 'phrase']); +// → `["this", "is", "a", "tokenized", "phrase"]` must end with "." or ";" ``` -Message template for this validator includes `{{endValue}}`. +Message template for this validator includes `{{endValue}}` and `{{endValues}}`. ## Templates @@ -37,12 +50,20 @@ Message template for this validator includes `{{endValue}}`. | `default` | {{subject}} must end with {{endValue}} | | `inverted` | {{subject}} must not end with {{endValue}} | +### `EndsWith::TEMPLATE_MULTIPLE_VALUES` + +| Mode | Template | +| ---------: | :------------------------------------------------------- | +| `default` | {{subject}} must end with {{endValues|list:or}} | +| `inverted` | {{subject}} must not end with {{endValues|list:or}} | + ## Template placeholders | Placeholder | Description | | ----------- | ---------------------------------------------------------------- | -| `endValue` | | | `subject` | The validated input or the custom validator name (if specified). | +| `endValue` | The value that will be checked to be at the end of the input. | +| `endValues` | Additional values to check. | ## Categorization @@ -53,6 +74,7 @@ Message template for this validator includes `{{endValue}}`. | Version | Description | | ------: | :---------------------------------- | +| 3.1.0 | Added support for multiple values | | 3.0.0 | Case-insensitive comparison removed | | 0.3.9 | Created | @@ -62,3 +84,4 @@ Message template for this validator includes `{{endValue}}`. - [In](In.md) - [Regex](Regex.md) - [StartsWith](StartsWith.md) +- [Trimmed](Trimmed.md) diff --git a/docs/validators/StartsWith.md b/docs/validators/StartsWith.md index 51fff00e..9ce2aa00 100644 --- a/docs/validators/StartsWith.md +++ b/docs/validators/StartsWith.md @@ -9,8 +9,9 @@ SPDX-FileContributor: Henrique Moody # StartsWith - `StartsWith(mixed $startValue)` +- `StartsWith(mixed $startValue, mixed ...$startValues)` -Validates whether the input starts with a given value. +Validates whether the input starts with one of the given values. This validator is similar to [Contains](Contains.md), but validates only if the value is at the beginning of the input. @@ -20,6 +21,9 @@ For strings: ```php v::startsWith('lorem')->assert('lorem ipsum'); // Validation passes successfully + +v::startsWith('Dr.', 'Mr.')->assert('Dr. Jane Doe'); +// Validation passes successfully ``` For arrays: @@ -27,9 +31,15 @@ For arrays: ```php v::startsWith('lorem')->assert(['lorem', 'ipsum']); // Validation passes successfully + +v::startsWith(0, 1)->assert([0, 1, 2, 3]); +// Validation passes successfully + +v::startsWith(0, 1)->assert([1, 2, 3]); +// Validation passes successfully ``` -Message template for this validator includes `{{startValue}}`. +Message template for this validator includes `{{startValue}}` and `{{startValues}}`. ## Templates @@ -40,12 +50,20 @@ Message template for this validator includes `{{startValue}}`. | `default` | {{subject}} must start with {{startValue}} | | `inverted` | {{subject}} must not start with {{startValue}} | +### `StartsWith::TEMPLATE_MULTIPLE_VALUES` + +| Mode | Template | +| ---------: | :----------------------------------------------------------- | +| `default` | {{subject}} must start with {{startValues|list:or}} | +| `inverted` | {{subject}} must not start with {{startValues|list:or}} | + ## Template placeholders -| Placeholder | Description | -| ------------ | ---------------------------------------------------------------- | -| `subject` | The validated input or the custom validator name (if specified). | -| `startValue` | | +| Placeholder | Description | +| ------------- | ---------------------------------------------------------------- | +| `subject` | The validated input or the custom validator name (if specified). | +| `startValue` | The value that will be checked to be at the start of the input. | +| `startValues` | Additional values to check. | ## Categorization @@ -56,6 +74,7 @@ Message template for this validator includes `{{startValue}}`. | Version | Description | | ------: | :---------------------------------- | +| 3.1.0 | Added support for multiple values | | 3.0.0 | Case-insensitive comparison removed | | 0.3.9 | Created | @@ -65,3 +84,4 @@ Message template for this validator includes `{{startValue}}`. - [EndsWith](EndsWith.md) - [In](In.md) - [Regex](Regex.md) +- [Trimmed](Trimmed.md) diff --git a/docs/validators/Trimmed.md b/docs/validators/Trimmed.md new file mode 100644 index 00000000..9a41c33c --- /dev/null +++ b/docs/validators/Trimmed.md @@ -0,0 +1,78 @@ + + +# Trimmed + +- `Trimmed()` +- `Trimmed(string ...$trimValues)` + +Validates whether the input string does not start or end with the given values. + +When no values are provided, this validator uses a default list of Unicode invisible characters (including regular whitespace, non-breaking spaces, and zero-width characters). + +With the default values: + +```php +v::trimmed()->assert('lorem ipsum'); +// Validation passes successfully + +v::trimmed()->assert("\u{200B}lorem"); +// → "​lorem" must not contain leading or trailing whitespace +``` + +With custom values: + +```php +v::trimmed('Dr.', 'Mr.', 'PhD.')->assert('John'); +// Validation passes successfully + +v::trimmed('Dr.', 'Mr.', 'PhD.')->assert('Dr. John'); +// → "Dr. John" must not contain leading or trailing "Dr.", "Mr.", or "PhD." + +v::trimmed('Dr.', 'Mr.', ', PhD')->assert('John Doe, PhD'); +// → "John Doe, PhD" must not contain leading or trailing "Dr.", "Mr.", or ", PhD" +``` + +This validator composes [StartsWith](StartsWith.md) and [EndsWith](EndsWith.md). + +## Templates + +### `Trimmed::TEMPLATE_STANDARD` + +| Mode | Template | +| ---------: | :---------------------------------------------------------- | +| `default` | {{subject}} must not contain leading or trailing whitespace | +| `inverted` | {{subject}} must contain leading or trailing whitespace | + +### `Trimmed::TEMPLATE_CUSTOM` + +| Mode | Template | +| ---------: | :--------------------------------------------------------------------------- | +| `default` | {{subject}} must not contain leading or trailing {{trimValues|list:or}} | +| `inverted` | {{subject}} must contain leading or trailing {{trimValues|list:or}} | + +## Template placeholders + +| Placeholder | Description | +| ------------ | ---------------------------------------------------------------- | +| `subject` | The validated input or the custom validator name (if specified). | +| `trimValues` | The values that will be checked at start end end of input. | + +## Categorization + +- Strings + +## Changelog + +| Version | Description | +| ------: | :---------- | +| 3.1.0 | Created | + +## See Also + +- [EndsWith](EndsWith.md) +- [Space](Space.md) +- [Spaced](Spaced.md) +- [StartsWith](StartsWith.md) diff --git a/src/Mixins/AllBuilder.php b/src/Mixins/AllBuilder.php index a455969a..1a288c4f 100644 --- a/src/Mixins/AllBuilder.php +++ b/src/Mixins/AllBuilder.php @@ -103,7 +103,7 @@ interface AllBuilder public static function allEmoji(): Chain; - public static function allEndsWith(mixed $endValue): Chain; + public static function allEndsWith(mixed $endValue, mixed ...$endValues): Chain; public static function allEquals(mixed $compareTo): Chain; @@ -269,7 +269,7 @@ interface AllBuilder public static function allSpaced(): Chain; - public static function allStartsWith(mixed $startValue): Chain; + public static function allStartsWith(mixed $startValue, mixed ...$startValues): Chain; public static function allStringType(): Chain; @@ -286,6 +286,8 @@ interface AllBuilder public static function allTld(): Chain; + public static function allTrimmed(string ...$trimValues): Chain; + public static function allTrueVal(): Chain; public static function allUndef(): Chain; diff --git a/src/Mixins/AllChain.php b/src/Mixins/AllChain.php index aef20164..e8b90c01 100644 --- a/src/Mixins/AllChain.php +++ b/src/Mixins/AllChain.php @@ -103,7 +103,7 @@ interface AllChain public function allEmoji(): Chain; - public function allEndsWith(mixed $endValue): Chain; + public function allEndsWith(mixed $endValue, mixed ...$endValues): Chain; public function allEquals(mixed $compareTo): Chain; @@ -269,7 +269,7 @@ interface AllChain public function allSpaced(): Chain; - public function allStartsWith(mixed $startValue): Chain; + public function allStartsWith(mixed $startValue, mixed ...$startValues): Chain; public function allStringType(): Chain; @@ -286,6 +286,8 @@ interface AllChain public function allTld(): Chain; + public function allTrimmed(string ...$trimValues): Chain; + public function allTrueVal(): Chain; public function allUndef(): Chain; diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index 354c339f..d4042eda 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -108,7 +108,7 @@ interface Builder extends AllBuilder, KeyBuilder, LengthBuilder, MaxBuilder, Min public static function emoji(): Chain; - public static function endsWith(mixed $endValue): Chain; + public static function endsWith(mixed $endValue, mixed ...$endValues): Chain; public static function equals(mixed $compareTo): Chain; @@ -296,7 +296,7 @@ interface Builder extends AllBuilder, KeyBuilder, LengthBuilder, MaxBuilder, Min public static function spaced(): Chain; - public static function startsWith(mixed $startValue): Chain; + public static function startsWith(mixed $startValue, mixed ...$startValues): Chain; public static function stringType(): Chain; @@ -316,6 +316,8 @@ interface Builder extends AllBuilder, KeyBuilder, LengthBuilder, MaxBuilder, Min public static function tld(): Chain; + public static function trimmed(string ...$trimValues): Chain; + public static function trueVal(): Chain; public static function undef(): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index 0a486889..acc75aef 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -110,7 +110,7 @@ interface Chain extends Validator, AllChain, KeyChain, LengthChain, MaxChain, Mi public function emoji(): Chain; - public function endsWith(mixed $endValue): Chain; + public function endsWith(mixed $endValue, mixed ...$endValues): Chain; public function equals(mixed $compareTo): Chain; @@ -298,7 +298,7 @@ interface Chain extends Validator, AllChain, KeyChain, LengthChain, MaxChain, Mi public function spaced(): Chain; - public function startsWith(mixed $startValue): Chain; + public function startsWith(mixed $startValue, mixed ...$startValues): Chain; public function stringType(): Chain; @@ -318,6 +318,8 @@ interface Chain extends Validator, AllChain, KeyChain, LengthChain, MaxChain, Mi public function tld(): Chain; + public function trimmed(string ...$trimValues): Chain; + public function trueVal(): Chain; public function undef(): Chain; diff --git a/src/Mixins/KeyBuilder.php b/src/Mixins/KeyBuilder.php index bb6f7187..a8c85ec4 100644 --- a/src/Mixins/KeyBuilder.php +++ b/src/Mixins/KeyBuilder.php @@ -105,7 +105,7 @@ interface KeyBuilder public static function keyEmoji(int|string $key): Chain; - public static function keyEndsWith(int|string $key, mixed $endValue): Chain; + public static function keyEndsWith(int|string $key, mixed $endValue, mixed ...$endValues): Chain; public static function keyEquals(int|string $key, mixed $compareTo): Chain; @@ -271,7 +271,7 @@ interface KeyBuilder public static function keySpaced(int|string $key): Chain; - public static function keyStartsWith(int|string $key, mixed $startValue): Chain; + public static function keyStartsWith(int|string $key, mixed $startValue, mixed ...$startValues): Chain; public static function keyStringType(int|string $key): Chain; @@ -288,6 +288,8 @@ interface KeyBuilder public static function keyTld(int|string $key): Chain; + public static function keyTrimmed(int|string $key, string ...$trimValues): Chain; + public static function keyTrueVal(int|string $key): Chain; public static function keyUndef(int|string $key): Chain; diff --git a/src/Mixins/KeyChain.php b/src/Mixins/KeyChain.php index f29ea57f..e0c62514 100644 --- a/src/Mixins/KeyChain.php +++ b/src/Mixins/KeyChain.php @@ -105,7 +105,7 @@ interface KeyChain public function keyEmoji(int|string $key): Chain; - public function keyEndsWith(int|string $key, mixed $endValue): Chain; + public function keyEndsWith(int|string $key, mixed $endValue, mixed ...$endValues): Chain; public function keyEquals(int|string $key, mixed $compareTo): Chain; @@ -271,7 +271,7 @@ interface KeyChain public function keySpaced(int|string $key): Chain; - public function keyStartsWith(int|string $key, mixed $startValue): Chain; + public function keyStartsWith(int|string $key, mixed $startValue, mixed ...$startValues): Chain; public function keyStringType(int|string $key): Chain; @@ -288,6 +288,8 @@ interface KeyChain public function keyTld(int|string $key): Chain; + public function keyTrimmed(int|string $key, string ...$trimValues): Chain; + public function keyTrueVal(int|string $key): Chain; public function keyUndef(int|string $key): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index bcf83744..41c6c44a 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -105,7 +105,7 @@ interface NotBuilder public static function notEmoji(): Chain; - public static function notEndsWith(mixed $endValue): Chain; + public static function notEndsWith(mixed $endValue, mixed ...$endValues): Chain; public static function notEquals(mixed $compareTo): Chain; @@ -287,7 +287,7 @@ interface NotBuilder public static function notSpaced(): Chain; - public static function notStartsWith(mixed $startValue): Chain; + public static function notStartsWith(mixed $startValue, mixed ...$startValues): Chain; public static function notStringType(): Chain; @@ -304,6 +304,8 @@ interface NotBuilder public static function notTld(): Chain; + public static function notTrimmed(string ...$trimValues): Chain; + public static function notTrueVal(): Chain; public static function notUndef(): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 9a550413..113737fa 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -105,7 +105,7 @@ interface NotChain public function notEmoji(): Chain; - public function notEndsWith(mixed $endValue): Chain; + public function notEndsWith(mixed $endValue, mixed ...$endValues): Chain; public function notEquals(mixed $compareTo): Chain; @@ -287,7 +287,7 @@ interface NotChain public function notSpaced(): Chain; - public function notStartsWith(mixed $startValue): Chain; + public function notStartsWith(mixed $startValue, mixed ...$startValues): Chain; public function notStringType(): Chain; @@ -304,6 +304,8 @@ interface NotChain public function notTld(): Chain; + public function notTrimmed(string ...$trimValues): Chain; + public function notTrueVal(): Chain; public function notUndef(): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 3ac1b0b7..12f54b0b 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -105,7 +105,7 @@ interface NullOrBuilder public static function nullOrEmoji(): Chain; - public static function nullOrEndsWith(mixed $endValue): Chain; + public static function nullOrEndsWith(mixed $endValue, mixed ...$endValues): Chain; public static function nullOrEquals(mixed $compareTo): Chain; @@ -289,7 +289,7 @@ interface NullOrBuilder public static function nullOrSpaced(): Chain; - public static function nullOrStartsWith(mixed $startValue): Chain; + public static function nullOrStartsWith(mixed $startValue, mixed ...$startValues): Chain; public static function nullOrStringType(): Chain; @@ -306,6 +306,8 @@ interface NullOrBuilder public static function nullOrTld(): Chain; + public static function nullOrTrimmed(string ...$trimValues): Chain; + public static function nullOrTrueVal(): Chain; public static function nullOrUnique(): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 1c5e4ac4..32965991 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -105,7 +105,7 @@ interface NullOrChain public function nullOrEmoji(): Chain; - public function nullOrEndsWith(mixed $endValue): Chain; + public function nullOrEndsWith(mixed $endValue, mixed ...$endValues): Chain; public function nullOrEquals(mixed $compareTo): Chain; @@ -289,7 +289,7 @@ interface NullOrChain public function nullOrSpaced(): Chain; - public function nullOrStartsWith(mixed $startValue): Chain; + public function nullOrStartsWith(mixed $startValue, mixed ...$startValues): Chain; public function nullOrStringType(): Chain; @@ -306,6 +306,8 @@ interface NullOrChain public function nullOrTld(): Chain; + public function nullOrTrimmed(string ...$trimValues): Chain; + public function nullOrTrueVal(): Chain; public function nullOrUnique(): Chain; diff --git a/src/Mixins/PropertyBuilder.php b/src/Mixins/PropertyBuilder.php index ff523c57..1ba349a0 100644 --- a/src/Mixins/PropertyBuilder.php +++ b/src/Mixins/PropertyBuilder.php @@ -105,7 +105,7 @@ interface PropertyBuilder public static function propertyEmoji(string $propertyName): Chain; - public static function propertyEndsWith(string $propertyName, mixed $endValue): Chain; + public static function propertyEndsWith(string $propertyName, mixed $endValue, mixed ...$endValues): Chain; public static function propertyEquals(string $propertyName, mixed $compareTo): Chain; @@ -271,7 +271,7 @@ interface PropertyBuilder public static function propertySpaced(string $propertyName): Chain; - public static function propertyStartsWith(string $propertyName, mixed $startValue): Chain; + public static function propertyStartsWith(string $propertyName, mixed $startValue, mixed ...$startValues): Chain; public static function propertyStringType(string $propertyName): Chain; @@ -288,6 +288,8 @@ interface PropertyBuilder public static function propertyTld(string $propertyName): Chain; + public static function propertyTrimmed(string $propertyName, string ...$trimValues): Chain; + public static function propertyTrueVal(string $propertyName): Chain; public static function propertyUndef(string $propertyName): Chain; diff --git a/src/Mixins/PropertyChain.php b/src/Mixins/PropertyChain.php index e06549f8..6b8f198e 100644 --- a/src/Mixins/PropertyChain.php +++ b/src/Mixins/PropertyChain.php @@ -105,7 +105,7 @@ interface PropertyChain public function propertyEmoji(string $propertyName): Chain; - public function propertyEndsWith(string $propertyName, mixed $endValue): Chain; + public function propertyEndsWith(string $propertyName, mixed $endValue, mixed ...$endValues): Chain; public function propertyEquals(string $propertyName, mixed $compareTo): Chain; @@ -271,7 +271,7 @@ interface PropertyChain public function propertySpaced(string $propertyName): Chain; - public function propertyStartsWith(string $propertyName, mixed $startValue): Chain; + public function propertyStartsWith(string $propertyName, mixed $startValue, mixed ...$startValues): Chain; public function propertyStringType(string $propertyName): Chain; @@ -288,6 +288,8 @@ interface PropertyChain public function propertyTld(string $propertyName): Chain; + public function propertyTrimmed(string $propertyName, string ...$trimValues): Chain; + public function propertyTrueVal(string $propertyName): Chain; public function propertyUndef(string $propertyName): Chain; diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index 769ae53f..c8c6273e 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -103,7 +103,7 @@ interface UndefOrBuilder public static function undefOrEmoji(): Chain; - public static function undefOrEndsWith(mixed $endValue): Chain; + public static function undefOrEndsWith(mixed $endValue, mixed ...$endValues): Chain; public static function undefOrEquals(mixed $compareTo): Chain; @@ -287,7 +287,7 @@ interface UndefOrBuilder public static function undefOrSpaced(): Chain; - public static function undefOrStartsWith(mixed $startValue): Chain; + public static function undefOrStartsWith(mixed $startValue, mixed ...$startValues): Chain; public static function undefOrStringType(): Chain; @@ -304,6 +304,8 @@ interface UndefOrBuilder public static function undefOrTld(): Chain; + public static function undefOrTrimmed(string ...$trimValues): Chain; + public static function undefOrTrueVal(): Chain; public static function undefOrUnique(): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index fd9ceb96..aee52fca 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -103,7 +103,7 @@ interface UndefOrChain public function undefOrEmoji(): Chain; - public function undefOrEndsWith(mixed $endValue): Chain; + public function undefOrEndsWith(mixed $endValue, mixed ...$endValues): Chain; public function undefOrEquals(mixed $compareTo): Chain; @@ -287,7 +287,7 @@ interface UndefOrChain public function undefOrSpaced(): Chain; - public function undefOrStartsWith(mixed $startValue): Chain; + public function undefOrStartsWith(mixed $startValue, mixed ...$startValues): Chain; public function undefOrStringType(): Chain; @@ -304,6 +304,8 @@ interface UndefOrChain public function undefOrTld(): Chain; + public function undefOrTrimmed(string ...$trimValues): Chain; + public function undefOrTrueVal(): Chain; public function undefOrUnique(): Chain; diff --git a/src/Validators/Core/Envelope.php b/src/Validators/Core/Envelope.php index d034cf69..b1f401aa 100644 --- a/src/Validators/Core/Envelope.php +++ b/src/Validators/Core/Envelope.php @@ -21,11 +21,18 @@ abstract class Envelope implements Validator public function __construct( private readonly Validator $validator, private readonly array $parameters = [], + private readonly string $template = Validator::TEMPLATE_STANDARD, ) { } public function evaluate(mixed $input): Result { - return Result::of($this->validator->evaluate($input)->hasPassed, $input, $this, $this->parameters); + return Result::of( + $this->validator->evaluate($input)->hasPassed, + $input, + $this, + $this->parameters, + $this->template, + ); } } diff --git a/src/Validators/EndsWith.php b/src/Validators/EndsWith.php index 5a5279e6..5056a446 100644 --- a/src/Validators/EndsWith.php +++ b/src/Validators/EndsWith.php @@ -20,8 +20,10 @@ use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +use function count; use function end; use function is_array; +use function is_string; use function mb_strlen; use function mb_strrpos; @@ -30,26 +32,56 @@ use function mb_strrpos; '{{subject}} must end with {{endValue}}', '{{subject}} must not end with {{endValue}}', )] +#[Template( + '{{subject}} must end with {{endValues|list:or}}', + '{{subject}} must not end with {{endValues|list:or}}', + self::TEMPLATE_MULTIPLE_VALUES, +)] final readonly class EndsWith implements Validator { + public const string TEMPLATE_MULTIPLE_VALUES = '__multiple_values__'; + + /** @var non-empty-array */ + private array $endValues; + public function __construct( - private mixed $endValue, + mixed $endValue, + mixed ...$endValues, ) { + $this->endValues = [$endValue, ...$endValues]; } public function evaluate(mixed $input): Result { - $parameters = ['endValue' => $this->endValue]; + $template = self::TEMPLATE_STANDARD; + $parameters = [ + 'endValue' => $this->endValues[0], + 'endValues' => $this->endValues, + ]; - return Result::of($this->validateIdentical($input), $input, $this, $parameters); + if (count($this->endValues) > 1) { + $template = self::TEMPLATE_MULTIPLE_VALUES; + } + + return Result::of($this->validateIdentical($input), $input, $this, $parameters, $template); } private function validateIdentical(mixed $input): bool { - if (is_array($input)) { - return end($input) === $this->endValue; + foreach ($this->endValues as $endValue) { + if (is_array($input) && end($input) === $endValue) { + return true; + } + + // ensure both operands are strings before using mb_ functions + if ( + is_string($input) && is_string($endValue) + && mb_strrpos($input, $endValue) === mb_strlen($input) - mb_strlen($endValue) + ) { + return true; + } } - return mb_strrpos($input, $this->endValue) === mb_strlen($input) - mb_strlen($this->endValue); + return false; } } diff --git a/src/Validators/StartsWith.php b/src/Validators/StartsWith.php index a9548148..6b9d48cd 100644 --- a/src/Validators/StartsWith.php +++ b/src/Validators/StartsWith.php @@ -20,6 +20,7 @@ use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +use function count; use function is_array; use function is_string; use function mb_strpos; @@ -30,28 +31,50 @@ use function reset; '{{subject}} must start with {{startValue}}', '{{subject}} must not start with {{startValue}}', )] +#[Template( + '{{subject}} must start with {{startValues|list:or}}', + '{{subject}} must not start with {{startValues|list:or}}', + self::TEMPLATE_MULTIPLE_VALUES, +)] final readonly class StartsWith implements Validator { + public const string TEMPLATE_MULTIPLE_VALUES = '__multiple_values__'; + + /** @var non-empty-array */ + private array $startValues; + public function __construct( - private mixed $startValue, + mixed $startValue, + mixed ...$startValues, ) { + $this->startValues = [$startValue, ...$startValues]; } public function evaluate(mixed $input): Result { - $parameters = ['startValue' => $this->startValue]; + $template = self::TEMPLATE_STANDARD; + $parameters = [ + 'startValue' => $this->startValues[0], + 'startValues' => $this->startValues, + ]; - return Result::of($this->validateIdentical($input), $input, $this, $parameters); + if (count($this->startValues) > 1) { + $template = self::TEMPLATE_MULTIPLE_VALUES; + } + + return Result::of($this->validateIdentical($input), $input, $this, $parameters, $template); } protected function validateIdentical(mixed $input): bool { - if (is_array($input)) { - return reset($input) === $this->startValue; - } + foreach ($this->startValues as $startValue) { + if (is_array($input) && reset($input) === $startValue) { + return true; + } - if (is_string($input) && is_string($this->startValue)) { - return mb_strpos($input, $this->startValue) === 0; + if (is_string($input) && is_string($startValue) && mb_strpos($input, $startValue) === 0) { + return true; + } } return false; diff --git a/src/Validators/Trimmed.php b/src/Validators/Trimmed.php new file mode 100644 index 00000000..88588aa1 --- /dev/null +++ b/src/Validators/Trimmed.php @@ -0,0 +1,87 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators; + +use Attribute; +use Respect\Validation\Message\Template; +use Respect\Validation\Validator; +use Respect\Validation\Validators\Core\Envelope; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +#[Template( + '{{subject}} must not contain leading or trailing whitespace', + '{{subject}} must contain leading or trailing whitespace', + Validator::TEMPLATE_STANDARD, +)] +#[Template( + '{{subject}} must not contain leading or trailing {{trimValues|list:or}}', + '{{subject}} must contain leading or trailing {{trimValues|list:or}}', + self::TEMPLATE_CUSTOM, +)] +final class Trimmed extends Envelope +{ + public const string TEMPLATE_CUSTOM = '__custom__'; + + /** Unicode whitespace and zero-width characters. */ + private const array DEFAULT_TRIM_VALUES = [ + "\u{0009}", // CHARACTER TABULATION + "\u{000A}", // LINE FEED + "\u{000B}", // LINE TABULATION + "\u{000C}", // FORM FEED + "\u{000D}", // CARRIAGE RETURN + "\u{0020}", // SPACE + "\u{0085}", // NEXT LINE + "\u{00A0}", // NO-BREAK SPACE + "\u{1680}", // OGHAM SPACE MARK + "\u{180E}", // MONGOLIAN VOWEL SEPARATOR + "\u{2000}", // EN QUAD + "\u{2001}", // EM QUAD + "\u{2002}", // EN SPACE + "\u{2003}", // EM SPACE + "\u{2004}", // THREE-PER-EM SPACE + "\u{2005}", // FOUR-PER-EM SPACE + "\u{2006}", // SIX-PER-EM SPACE + "\u{2007}", // FIGURE SPACE + "\u{2008}", // PUNCTUATION SPACE + "\u{2009}", // THIN SPACE + "\u{200A}", // HAIR SPACE + "\u{200B}", // ZERO WIDTH SPACE + "\u{200C}", // ZERO WIDTH NON-JOINER + "\u{200D}", // ZERO WIDTH JOINER + "\u{2028}", // LINE SEPARATOR + "\u{2029}", // PARAGRAPH SEPARATOR + "\u{202F}", // NARROW NO-BREAK SPACE + "\u{205F}", // MEDIUM MATHEMATICAL SPACE + "\u{2060}", // WORD JOINER + "\u{3000}", // IDEOGRAPHIC SPACE + "\u{FEFF}", // ZERO WIDTH NO-BREAK SPACE + ]; + + public function __construct(string ...$trimValues) + { + $hasCustomTrimValues = $trimValues !== []; + $trimValues = $hasCustomTrimValues ? $trimValues : self::DEFAULT_TRIM_VALUES; + + parent::__construct( + new ShortCircuit( + new StringType(), + new Not( + new AnyOf( + new StartsWith(...$trimValues), + new EndsWith(...$trimValues), + ), + ), + ), + $hasCustomTrimValues ? ['trimValues' => $trimValues] : [], + $hasCustomTrimValues ? self::TEMPLATE_CUSTOM : Validator::TEMPLATE_STANDARD, + ); + } +} diff --git a/tests/feature/Validators/EndsWithTest.php b/tests/feature/Validators/EndsWithTest.php index d93af327..6bc9ee3a 100644 --- a/tests/feature/Validators/EndsWithTest.php +++ b/tests/feature/Validators/EndsWithTest.php @@ -9,6 +9,8 @@ declare(strict_types=1); +use Respect\Validation\Exceptions\ValidationException; + test('Scenario #1', catchMessage( fn() => v::endsWith('foo')->assert('bar'), fn(string $message) => expect($message)->toBe('"bar" must end with "foo"'), @@ -28,3 +30,21 @@ test('Scenario #4', catchFullMessage( fn() => v::not(v::endsWith('foo'))->assert(['bar', 'foo']), fn(string $fullMessage) => expect($fullMessage)->toBe('- `["bar", "foo"]` must not end with "foo"'), )); + +test('Scenario #5', catchMessage( + fn() => v::endsWith('Mr.', 'Dr.')->assert('John Doe'), + fn(string $message) => expect($message)->toBe('"John Doe" must end with "Mr." or "Dr."'), +)); + +test('Scenario #6', catchFullMessage( + fn() => v::not(v::endsWith('divorced.', 'PhD.'))->assert('John Doe, PhD.'), + fn(string $fullMessage) => expect($fullMessage)->toBe('- "John Doe, PhD." must not end with "divorced." or "PhD."'), +)); + +// ensure non-string values do not throw errors and are considered invalid +test('non-string input or end value are invalid', function (): void { + expect(fn() => v::endsWith('foo')->assert(123)) + ->toThrow(ValidationException::class); + expect(fn() => v::endsWith(123)->assert('foo')) + ->toThrow(ValidationException::class); +}); diff --git a/tests/feature/Validators/StartsWithTest.php b/tests/feature/Validators/StartsWithTest.php index aa4cef08..59e45145 100644 --- a/tests/feature/Validators/StartsWithTest.php +++ b/tests/feature/Validators/StartsWithTest.php @@ -28,3 +28,13 @@ test('Scenario #4', catchFullMessage( fn() => v::not(v::startsWith('c'))->assert(['c', 'd']), fn(string $fullMessage) => expect($fullMessage)->toBe('- `["c", "d"]` must not start with "c"'), )); + +test('Scenario #5', catchMessage( + fn() => v::startsWith('Mr.', 'Dr.')->assert('John Doe'), + fn(string $message) => expect($message)->toBe('"John Doe" must start with "Mr." or "Dr."'), +)); + +test('Scenario #6', catchFullMessage( + fn() => v::not(v::startsWith('Mr.', 'Dr.'))->assert('Dr. John Doe'), + fn(string $fullMessage) => expect($fullMessage)->toBe('- "Dr. John Doe" must not start with "Mr." or "Dr."'), +)); diff --git a/tests/feature/Validators/TrimmedTest.php b/tests/feature/Validators/TrimmedTest.php new file mode 100644 index 00000000..f209c0c5 --- /dev/null +++ b/tests/feature/Validators/TrimmedTest.php @@ -0,0 +1,35 @@ + + */ + +declare(strict_types=1); + +test('default template', catchAll( + fn() => v::trimmed()->assert(' word'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('" word" must not contain leading or trailing whitespace') + ->and($fullMessage)->toBe('- " word" must not contain leading or trailing whitespace') + ->and($messages)->toBe(['trimmed' => '" word" must not contain leading or trailing whitespace']), +)); + +test('inverted template', catchAll( + fn() => v::not(v::trimmed())->assert('word'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"word" must contain leading or trailing whitespace') + ->and($fullMessage)->toBe('- "word" must contain leading or trailing whitespace') + ->and($messages)->toBe(['notTrimmed' => '"word" must contain leading or trailing whitespace']), +)); + +test('custom alternatives', catchMessage( + fn() => v::trimmed('foo', 'bar')->assert('foobaz'), + fn(string $message) => expect($message)->toBe('"foobaz" must not contain leading or trailing "foo" or "bar"'), +)); + +test('custom alternatives inverted template', catchMessage( + fn() => v::not(v::trimmed('foo', 'bar'))->assert('bazqux'), + fn(string $message) => expect($message)->toBe('"bazqux" must contain leading or trailing "foo" or "bar"'), +)); diff --git a/tests/src/SmokeTestProvider.php b/tests/src/SmokeTestProvider.php index dbf438f0..c3438160 100644 --- a/tests/src/SmokeTestProvider.php +++ b/tests/src/SmokeTestProvider.php @@ -169,6 +169,7 @@ trait SmokeTestProvider yield 'Templated' => [new vs\Templated('Foo', new vs\StringVal()), 'foo']; yield 'Time' => [new vs\Time(), '12:34:56']; yield 'Tld' => [new vs\Tld(), 'com']; + yield 'Trimmed' => [new vs\Trimmed(), 'example']; yield 'TrueVal' => [new vs\TrueVal(), true]; yield 'Undef' => [new vs\Undef(), null]; yield 'UndefOr' => [new vs\UndefOr(new vs\IntVal()), null]; diff --git a/tests/unit/Validators/EndsWithTest.php b/tests/unit/Validators/EndsWithTest.php index 04394d7d..d2d79821 100644 --- a/tests/unit/Validators/EndsWithTest.php +++ b/tests/unit/Validators/EndsWithTest.php @@ -28,6 +28,8 @@ final class EndsWithTest extends RuleTestCase [new EndsWith('foo'), ['bar', 'foo']], [new EndsWith('foo'), 'barbazfoo'], [new EndsWith('foo'), 'foobazfoo'], + [new EndsWith('foo', 'bar'), 'bazbar'], + [new EndsWith('foo', 'bar'), ['baz', 'bar']], [new EndsWith(1), [2, 3, 1]], [new EndsWith('1'), [2, 3, '1']], ]; @@ -44,8 +46,13 @@ final class EndsWithTest extends RuleTestCase [new EndsWith('foo'), 'faabarbaz'], [new EndsWith('foo'), 'baabazfaa'], [new EndsWith('foo'), 'baafoofaa'], + [new EndsWith('foo', 'bar'), 'foobaz'], + [new EndsWith('foo', 'bar'), ['foo', 'baz']], [new EndsWith('1'), [1, '1', 3]], [new EndsWith('1'), [2, 3, 1]], + // non-string inputs/values should not trigger warnings + [new EndsWith('foo'), 123], + [new EndsWith(123), 'foo'], ]; } } diff --git a/tests/unit/Validators/StartsWithTest.php b/tests/unit/Validators/StartsWithTest.php index 0c1c0f5d..eabcfaa8 100644 --- a/tests/unit/Validators/StartsWithTest.php +++ b/tests/unit/Validators/StartsWithTest.php @@ -28,6 +28,8 @@ final class StartsWithTest extends RuleTestCase [new StartsWith('foo'), ['foo', 'bar']], [new StartsWith('foo'), 'foobarbaz'], [new StartsWith('foo'), 'foobazfoo'], + [new StartsWith('foo', 'bar'), 'barbaz'], + [new StartsWith('foo', 'bar'), ['bar', 'baz']], [new StartsWith('1'), ['1', 2, 3]], ]; } @@ -44,6 +46,8 @@ final class StartsWithTest extends RuleTestCase [new StartsWith('foo'), 'faabarbaz'], [new StartsWith('foo'), 'baabazfaa'], [new StartsWith('foo'), 'baafoofaa'], + [new StartsWith('foo', 'bar'), 'bazfoo'], + [new StartsWith('foo', 'bar'), ['baz', 'foo']], [new StartsWith('1'), [1, '1', 3]], [new StartsWith('1'), [1, 2, 3]], ]; diff --git a/tests/unit/Validators/TrimmedTest.php b/tests/unit/Validators/TrimmedTest.php new file mode 100644 index 00000000..466fad3f --- /dev/null +++ b/tests/unit/Validators/TrimmedTest.php @@ -0,0 +1,48 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Respect\Validation\Test\RuleTestCase; + +#[Group('validator')] +#[CoversClass(Trimmed::class)] +final class TrimmedTest extends RuleTestCase +{ + /** @return iterable */ + public static function providerForValidInput(): iterable + { + return [ + [new Trimmed(), 'foo'], + [new Trimmed(), 'foo bar'], + [new Trimmed(), "foo\tbar"], + [new Trimmed(), ''], + [new Trimmed('foo', 'bar'), 'bazqux'], + [new Trimmed('foo', 'bar'), 'oofbarf'], + ]; + } + + /** @return iterable */ + public static function providerForInvalidInput(): iterable + { + return [ + [new Trimmed(), ' foo'], + [new Trimmed(), "foo\t"], + [new Trimmed(), "\u{200B}foo"], + [new Trimmed(), "foo\u{FEFF}"], + [new Trimmed(), 123], + [new Trimmed('foo', 'bar'), 'foobaz'], + [new Trimmed('foo', 'bar'), 'bazbar'], + [new Trimmed('foo', 'bar'), 'barbazfoo'], + ]; + } +}