From c199343470d1e82082b2b4c552650a79932d9764 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 17:11:14 +0100 Subject: [PATCH 1/9] align function naming with json schema --- README.md | 6 +- composer.json | 2 +- doc/ErrorHandling.md | 8 +- doc/Schema/ArraySchema.md | 20 +- doc/Schema/AssocSchema.md | 2 +- doc/Schema/BackedEnumSchema.md | 12 +- .../{LiteralSchema.md => ConstSchema.md} | 86 ++-- doc/Schema/DiscriminatedUnionSchema.md | 50 +- doc/Schema/FloatSchema.md | 25 +- doc/Schema/IntSchema.md | 24 +- doc/Schema/LazySchema.md | 12 +- doc/Schema/ObjectSchema.md | 8 +- doc/Schema/RecordSchema.md | 4 +- doc/Schema/StringSchema.md | 18 +- doc/Schema/TupleSchema.md | 24 +- doc/Schema/UnionSchema.md | 6 +- src/Parser.php | 11 + src/ParserInterface.php | 7 + src/Schema/ArraySchema.php | 150 +++++- src/Schema/BoolSchema.php | 18 +- src/Schema/ConstSchema.php | 44 ++ src/Schema/DateTimeSchema.php | 54 +-- src/Schema/FloatSchema.php | 216 ++++++--- src/Schema/IntSchema.php | 198 ++++++-- src/Schema/LiteralSchema.php | 21 +- src/Schema/StringSchema.php | 452 +++++++++++------- tests/Integration/ParserTest.php | 22 +- tests/Unit/ErrorsTest.php | 8 +- tests/Unit/ParserTest.php | 22 +- tests/Unit/Schema/ArraySchemaTest.php | 247 ++++++++++ tests/Unit/Schema/BoolSchemaTest.php | 4 +- tests/Unit/Schema/ConstSchemaTest.php | 275 +++++++++++ .../Schema/DiscriminatedUnionSchemaTest.php | 66 +-- tests/Unit/Schema/FloatSchemaTest.php | 284 ++++++++++- tests/Unit/Schema/IntSchemaTest.php | 286 ++++++++++- tests/Unit/Schema/StringSchemaTest.php | 207 +++++++- 36 files changed, 2311 insertions(+), 588 deletions(-) rename doc/Schema/{LiteralSchema.md => ConstSchema.md} (55%) create mode 100644 src/Schema/ConstSchema.php create mode 100644 tests/Unit/Schema/ConstSchemaTest.php diff --git a/README.md b/README.md index 081b7c4..dcbba39 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Heavily inspired by the well-known TypeScript library [zod](https://github.com/c Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-parsing][1]. ```sh -composer require chubbyphp/chubbyphp-parsing "^2.4" +composer require chubbyphp/chubbyphp-parsing "^2.5" ``` ## Quick Start @@ -49,7 +49,7 @@ $p = new Parser(); $userSchema = $p->object([ 'name' => $p->string()->minLength(1)->maxLength(100), 'email' => $p->string()->email(), - 'age' => $p->int()->gte(0)->lte(150), + 'age' => $p->int()->minimum(0)->maximum(150), ]); // Parse and validate data @@ -93,7 +93,7 @@ $user = $userSchema->parse([ | Schema | Description | Documentation | |--------|-------------|---------------| -| `literal()` | Exact value matching | [LiteralSchema](doc/Schema/LiteralSchema.md) | +| `const()` | Exact value matching | [ConstSchema](doc/Schema/ConstSchema.md) | | `backedEnum()` | PHP BackedEnum validation | [BackedEnumSchema](doc/Schema/BackedEnumSchema.md) | | `lazy()` | Recursive/self-referencing schemas | [LazySchema](doc/Schema/LazySchema.md) | | `respectValidation()` | Integration with Respect/Validation | [RespectValidationSchema](doc/Schema/RespectValidationSchema.md) | diff --git a/composer.json b/composer.json index bad89db..4d84ce3 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "scripts": { diff --git a/doc/ErrorHandling.md b/doc/ErrorHandling.md index 502fcd7..c1c0e0c 100644 --- a/doc/ErrorHandling.md +++ b/doc/ErrorHandling.md @@ -226,7 +226,7 @@ $p = new Parser(); $requestSchema = $p->object([ 'name' => $p->string()->trim()->minLength(1)->maxLength(100), 'email' => $p->string()->trim()->toLowerCase()->email(), - 'age' => $p->int()->gte(0)->lte(150), + 'age' => $p->int()->minimum(0)->maximum(150), ])->strict(); function handleRequest(array $input): array @@ -267,9 +267,10 @@ Each schema type uses a consistent error code prefix: | Schema | Prefix | Example Codes | |--------|--------|---------------| | string | `string.` | `string.type`, `string.minLength`, `string.email` | -| int | `int.` | `int.type`, `int.gt`, `int.positive` | -| float | `float.` | `float.type`, `float.gte`, `float.negative` | +| int | `int.` | `int.type`, `int.exclusiveMinimum`, `int.positive` | +| float | `float.` | `float.type`, `float.minimum`, `float.negative` | | bool | `bool.` | `bool.type` | +| const | `const.` | `const.type` | | array | `array.` | `array.type`, `array.minLength` | | assoc | `assoc.` | `assoc.type`, `assoc.unknownField` | | object | `object.` | `object.type`, `object.unknownField` | @@ -278,7 +279,6 @@ Each schema type uses a consistent error code prefix: | record | `record.` | `record.type` | | union | `union.` | `union.type` | | discriminatedUnion | `discriminatedUnion.` | `discriminatedUnion.type`, `discriminatedUnion.discriminator` | -| literal | `literal.` | `literal.type` | | backedEnum | `backedEnum.` | `backedEnum.type` | | lazy | (delegates to inner schema) | | | respectValidation | `respectValidation.` | `respectValidation.assert` | diff --git a/doc/Schema/ArraySchema.md b/doc/Schema/ArraySchema.md index ba8a344..1255889 100644 --- a/doc/Schema/ArraySchema.md +++ b/doc/Schema/ArraySchema.md @@ -19,15 +19,15 @@ $data = $schema->parse([1, 2, 3, 4, 5]); // Returns: [1, 2, 3, 4, 5] ### Length Constraints ```php -$schema->length(5); // Exact length of 5 items -$schema->minLength(1); // At least 1 item -$schema->maxLength(10); // At most 10 items +$schema->exactItems(5); // Exact count of 5 items +$schema->minItems(1); // At least 1 item +$schema->maxItems(10); // At most 10 items ``` ### Content Check ```php -$schema->includes(5); // Array must contain value 5 +$schema->contains(5); // Array must contain value 5 ``` ## Transformations @@ -89,14 +89,14 @@ $sumSchema->parse([1, 2, 3, 4, 5]); // Returns: 15 ### Non-Empty Array ```php -$nonEmptySchema = $p->array($p->string())->minLength(1); +$nonEmptySchema = $p->array($p->string())->minItems(1); ``` ### Unique Tags with Limit ```php $tagsSchema = $p->array($p->string()->trim()->minLength(1)) - ->maxLength(10) + ->maxItems(10) ->map(static fn (string $tag) => strtolower($tag)); ``` @@ -147,9 +147,9 @@ $matrixSchema->parse([ | Code | Description | |------|-------------| | `array.type` | Value is not an array | -| `array.length` | Array length doesn't match exact length | -| `array.minLength` | Array has fewer items than minimum | -| `array.maxLength` | Array has more items than maximum | -| `array.includes` | Array doesn't contain required value | +| `array.exactItems` | Array items count doesn't match exact count | +| `array.minItems` | Array has fewer items than minimum | +| `array.maxItems` | Array has more items than maximum | +| `array.contains` | Array doesn't contain required value | Item-level errors will include the array index in the error path (e.g., `items.0`, `items.1`). diff --git a/doc/Schema/AssocSchema.md b/doc/Schema/AssocSchema.md index 087fea8..61d4bbb 100644 --- a/doc/Schema/AssocSchema.md +++ b/doc/Schema/AssocSchema.md @@ -158,7 +158,7 @@ echo $config['database']['host']; // 'localhost' $addressSchema = $p->assoc([ 'street' => $p->string(), 'city' => $p->string(), - 'zipCode' => $p->string()->regexp('/^\d{5}$/'), + 'zipCode' => $p->string()->pattern('/^\d{5}$/'), ]); $personSchema = $p->assoc([ diff --git a/doc/Schema/BackedEnumSchema.md b/doc/Schema/BackedEnumSchema.md index 247a956..88a794b 100644 --- a/doc/Schema/BackedEnumSchema.md +++ b/doc/Schema/BackedEnumSchema.md @@ -170,7 +170,7 @@ enum Rank: int { case Two = 2; case Three = 3; - // ... + // ... case Jack = 11; case Queen = 12; case King = 13; @@ -182,7 +182,7 @@ $cardSchema = $p->object([ 'rank' => $p->backedEnum(Rank::class), ]); -$cardSchema->parse(['suit' => 'H', 'rank' => 14]); +$cardSchema->parse(['suit' => 'H', 'rank' => 14]); // Returns object with Suit::Hearts and Rank::Ace ``` @@ -204,14 +204,14 @@ $tagsSchema->parse(['featured', 'new', 'sale']); // Returns: [Tag::Featured, Tag::New, Tag::Sale] ``` -## BackedEnum vs Literal Union +## BackedEnum vs Const Union Use **BackedEnumSchema** when: - You already have a PHP enum defined - You want type safety with enum instances - You need enum methods/functionality -Use **Literal union** when: +Use **Const union** when: - Values are ad-hoc or temporary - You don't need enum type safety - You're matching simple string/int values @@ -222,8 +222,8 @@ enum Status: string { case Active = 'active'; case Inactive = 'inactive'; } $schema = $p->backedEnum(Status::class); // Returns Status enum instances -// Literal union: simple string values -$schema = $p->union([$p->literal('active'), $p->literal('inactive')]); +// Const union: simple string values +$schema = $p->union([$p->const('active'), $p->const('inactive')]); // Returns strings ``` diff --git a/doc/Schema/LiteralSchema.md b/doc/Schema/ConstSchema.md similarity index 55% rename from doc/Schema/LiteralSchema.md rename to doc/Schema/ConstSchema.md index 3a5b242..1b1e1e9 100644 --- a/doc/Schema/LiteralSchema.md +++ b/doc/Schema/ConstSchema.md @@ -1,6 +1,8 @@ -# LiteralSchema +# ConstSchema -The `LiteralSchema` validates that a value matches an exact literal value. It supports string, integer, float, and boolean literals. +> **Deprecated**: Use [ConstSchema](ConstSchema.md) instead. The `ConstSchema` and `const()` method are deprecated and will be removed in a future version. + +The `ConstSchema` validates that a value matches an exact const value. It supports string, integer, float, and boolean consts. ## Basic Usage @@ -9,27 +11,27 @@ use Chubbyphp\Parsing\Parser; $p = new Parser(); -// String literal -$schema = $p->literal('email'); +// String const +$schema = $p->const('email'); $data = $schema->parse('email'); // Returns: 'email' -// Integer literal -$schema = $p->literal(42); +// Integer const +$schema = $p->const(42); $data = $schema->parse(42); // Returns: 42 -// Boolean literal -$schema = $p->literal(true); +// Boolean const +$schema = $p->const(true); $data = $schema->parse(true); // Returns: true ``` ## Supported Types ```php -$p->literal('string'); // String literal -$p->literal(42); // Integer literal -$p->literal(3.14); // Float literal -$p->literal(true); // Boolean literal -$p->literal(false); // Boolean literal +$p->const('string'); // String const +$p->const(42); // Integer const +$p->const(3.14); // Float const +$p->const(true); // Boolean const +$p->const(false); // Boolean const ``` ## Common Patterns @@ -41,11 +43,11 @@ Used with `DiscriminatedUnionSchema` to identify object types: ```php $contactSchema = $p->discriminatedUnion([ $p->object([ - 'type' => $p->literal('email'), + 'type' => $p->const('email'), 'address' => $p->string()->email(), ]), $p->object([ - 'type' => $p->literal('phone'), + 'type' => $p->const('phone'), 'number' => $p->string(), ]), ], 'type'); @@ -55,9 +57,9 @@ $contactSchema = $p->discriminatedUnion([ ```php $statusSchema = $p->union([ - $p->literal('pending'), - $p->literal('approved'), - $p->literal('rejected'), + $p->const('pending'), + $p->const('approved'), + $p->const('rejected'), ]); $statusSchema->parse('approved'); // Returns: 'approved' @@ -67,16 +69,16 @@ $statusSchema->parse('invalid'); // Throws error ### Magic Numbers ```php -$httpOkSchema = $p->literal(200); -$httpNotFoundSchema = $p->literal(404); +$httpOkSchema = $p->const(200); +$httpNotFoundSchema = $p->const(404); $responseSchema = $p->object([ 'status' => $p->union([ - $p->literal(200), - $p->literal(201), - $p->literal(400), - $p->literal(404), - $p->literal(500), + $p->const(200), + $p->const(201), + $p->const(400), + $p->const(404), + $p->const(500), ]), 'body' => $p->string(), ]); @@ -85,12 +87,12 @@ $responseSchema = $p->object([ ### Boolean Flags ```php -$enabledSchema = $p->literal(true); -$disabledSchema = $p->literal(false); +$enabledSchema = $p->const(true); +$disabledSchema = $p->const(false); // Only accept explicitly true $mustBeEnabled = $p->object([ - 'feature' => $p->literal(true), + 'feature' => $p->const(true), ]); ``` @@ -99,8 +101,8 @@ $mustBeEnabled = $p->object([ ```php $sortSchema = $p->record( $p->union([ - $p->literal('asc'), - $p->literal('desc'), + $p->const('asc'), + $p->const('desc'), ]) ); @@ -114,29 +116,29 @@ $sortSchema->parse([ ```php $requestSchema = $p->object([ - 'version' => $p->literal('2.0'), + 'version' => $p->const('2.0'), 'method' => $p->string(), 'params' => $p->record($p->string()), ]); ``` -### Null Literal +### Null Const -While `nullable()` is preferred for optional null values, you can use literal for explicit null: +While `nullable()` is preferred for optional null values, you can use const for explicit null: ```php -$nullSchema = $p->literal(null); +$nullSchema = $p->const(null); // Useful in unions where null has specific meaning $valueOrNotSet = $p->union([ $p->string(), - $p->literal(null), + $p->const(null), ]); ``` -## Literal vs Enum +## Const vs Enum -Use **LiteralSchema** when: +Use **ConstSchema** when: - You have a single specific value - You're building discriminators for unions - You need to match magic numbers or specific strings @@ -147,9 +149,9 @@ Use **BackedEnumSchema** when: - The values represent a closed set of options ```php -// Literal: single value or ad-hoc unions -$type = $p->literal('user'); -$direction = $p->union([$p->literal('asc'), $p->literal('desc')]); +// Const: single value or ad-hoc unions +$type = $p->const('user'); +$direction = $p->union([$p->const('asc'), $p->const('desc')]); // BackedEnum: related values with type safety enum Direction: string { @@ -163,6 +165,6 @@ $direction = $p->backedEnum(Direction::class); | Code | Description | |------|-------------| -| `literal.type` | Value doesn't match the expected literal | +| `const.type` | Value doesn't match the expected const | -The error will include the expected literal value and the actual value received. +The error will include the expected const value and the actual value received. diff --git a/doc/Schema/DiscriminatedUnionSchema.md b/doc/Schema/DiscriminatedUnionSchema.md index 3b76b28..133757e 100644 --- a/doc/Schema/DiscriminatedUnionSchema.md +++ b/doc/Schema/DiscriminatedUnionSchema.md @@ -13,8 +13,8 @@ $p = new Parser(); // Using ObjectSchema (returns stdClass) $schema = $p->discriminatedUnion([ - $p->object(['_type' => $p->literal('email'), 'address' => $p->string()->email()]), - $p->object(['_type' => $p->literal('phone'), 'number' => $p->string()]), + $p->object(['_type' => $p->const('email'), 'address' => $p->string()->email()]), + $p->object(['_type' => $p->const('phone'), 'number' => $p->string()]), ], '_type'); $email = $schema->parse(['_type' => 'email', 'address' => 'user@example.com']); @@ -22,8 +22,8 @@ $phone = $schema->parse(['_type' => 'phone', 'number' => '+41790000000']); // Using AssocSchema (returns array) $schemaAssoc = $p->discriminatedUnion([ - $p->assoc(['_type' => $p->literal('email'), 'address' => $p->string()->email()]), - $p->assoc(['_type' => $p->literal('phone'), 'number' => $p->string()]), + $p->assoc(['_type' => $p->const('email'), 'address' => $p->string()->email()]), + $p->assoc(['_type' => $p->const('phone'), 'number' => $p->string()]), ], '_type'); $emailArr = $schemaAssoc->parse(['_type' => 'email', 'address' => 'user@example.com']); @@ -33,7 +33,7 @@ $emailArr = $schemaAssoc->parse(['_type' => 'email', 'address' => 'user@example. ## How It Works 1. The schema reads the discriminator field from the input -2. It finds the matching object schema based on the literal value +2. It finds the matching object schema based on the const value 3. The input is validated against that specific schema This is O(1) lookup vs O(n) sequential trying in regular unions. @@ -45,8 +45,8 @@ The default discriminator field is `_type`, but you can customize it: ```php // Using 'kind' as discriminator $schema = $p->discriminatedUnion([ - $p->object(['kind' => $p->literal('circle'), 'radius' => $p->float()]), - $p->object(['kind' => $p->literal('rectangle'), 'width' => $p->float(), 'height' => $p->float()]), + $p->object(['kind' => $p->const('circle'), 'radius' => $p->float()]), + $p->object(['kind' => $p->const('rectangle'), 'width' => $p->float(), 'height' => $p->float()]), ], 'kind'); ``` @@ -57,17 +57,17 @@ $schema = $p->discriminatedUnion([ ```php $contactSchema = $p->discriminatedUnion([ $p->object([ - '_type' => $p->literal('email'), + '_type' => $p->const('email'), 'address' => $p->string()->email(), 'verified' => $p->bool(), ]), $p->object([ - '_type' => $p->literal('phone'), - 'number' => $p->string()->regexp('/^\+\d{10,15}$/'), + '_type' => $p->const('phone'), + 'number' => $p->string()->pattern('/^\+\d{10,15}$/'), 'country' => $p->string()->length(2), ]), $p->object([ - '_type' => $p->literal('address'), + '_type' => $p->const('address'), 'street' => $p->string(), 'city' => $p->string(), 'zipCode' => $p->string(), @@ -80,16 +80,16 @@ $contactSchema = $p->discriminatedUnion([ ```php $shapeSchema = $p->discriminatedUnion([ $p->object([ - 'type' => $p->literal('circle'), + 'type' => $p->const('circle'), 'radius' => $p->float()->positive(), ]), $p->object([ - 'type' => $p->literal('rectangle'), + 'type' => $p->const('rectangle'), 'width' => $p->float()->positive(), 'height' => $p->float()->positive(), ]), $p->object([ - 'type' => $p->literal('triangle'), + 'type' => $p->const('triangle'), 'base' => $p->float()->positive(), 'height' => $p->float()->positive(), ]), @@ -101,17 +101,17 @@ $shapeSchema = $p->discriminatedUnion([ ```php $eventSchema = $p->discriminatedUnion([ $p->object([ - 'event' => $p->literal('user.created'), + 'event' => $p->const('user.created'), 'userId' => $p->string()->uuidV4(), 'email' => $p->string()->email(), ]), $p->object([ - 'event' => $p->literal('user.updated'), + 'event' => $p->const('user.updated'), 'userId' => $p->string()->uuidV4(), 'changes' => $p->record($p->string()), ]), $p->object([ - 'event' => $p->literal('user.deleted'), + 'event' => $p->const('user.deleted'), 'userId' => $p->string()->uuidV4(), 'deletedAt' => $p->string()->toDateTime(), ]), @@ -123,18 +123,18 @@ $eventSchema = $p->discriminatedUnion([ ```php $paymentSchema = $p->discriminatedUnion([ $p->object([ - 'method' => $p->literal('credit_card'), - 'cardNumber' => $p->string()->regexp('/^\d{16}$/'), - 'expiry' => $p->string()->regexp('/^\d{2}\/\d{2}$/'), - 'cvv' => $p->string()->regexp('/^\d{3,4}$/'), + 'method' => $p->const('credit_card'), + 'cardNumber' => $p->string()->pattern('/^\d{16}$/'), + 'expiry' => $p->string()->pattern('/^\d{2}\/\d{2}$/'), + 'cvv' => $p->string()->pattern('/^\d{3,4}$/'), ]), $p->object([ - 'method' => $p->literal('bank_transfer'), + 'method' => $p->const('bank_transfer'), 'iban' => $p->string(), 'bic' => $p->string(), ]), $p->object([ - 'method' => $p->literal('paypal'), + 'method' => $p->const('paypal'), 'email' => $p->string()->email(), ]), ], 'method'); @@ -147,8 +147,8 @@ class EmailContact { public string $address; } class PhoneContact { public string $number; } $schema = $p->discriminatedUnion([ - $p->object(['_type' => $p->literal('email'), 'address' => $p->string()], EmailContact::class), - $p->object(['_type' => $p->literal('phone'), 'number' => $p->string()], PhoneContact::class), + $p->object(['_type' => $p->const('email'), 'address' => $p->string()], EmailContact::class), + $p->object(['_type' => $p->const('phone'), 'number' => $p->string()], PhoneContact::class), ]); $contact = $schema->parse(['_type' => 'email', 'address' => 'test@example.com']); diff --git a/doc/Schema/FloatSchema.md b/doc/Schema/FloatSchema.md index bb1b2c3..9358ac3 100644 --- a/doc/Schema/FloatSchema.md +++ b/doc/Schema/FloatSchema.md @@ -19,10 +19,10 @@ $data = $schema->parse(4.2); // Returns: 4.2 ### Comparison Constraints ```php -$schema->gt(5.0); // Greater than 5.0 -$schema->gte(5.0); // Greater than or equal to 5.0 -$schema->lt(10.0); // Less than 10.0 -$schema->lte(10.0); // Less than or equal to 10.0 +$schema->minimum(5.0); // Greater than or equal to 5.0 +$schema->exclusiveMinimum(5.0); // Greater than 5.0 +$schema->exclusiveMaximum(10.0); // Less than 10.0 +$schema->maximum(10.0); // Less than or equal to 10.0 ``` ### Sign Constraints @@ -56,17 +56,18 @@ $priceSchema->parse(-5.0); // Throws: nonNegative validation error ### Percentage ```php -$percentageSchema = $p->float()->gte(0.0)->lte(100.0); +$percentageSchema = $p->float()->minimum(0.0)->maximum(100.0); $percentageSchema->parse(75.5); // Returns: 75.5 -$percentageSchema->parse(150.0); // Throws: lte validation error +$percentageSchema->parse(-1.0); // Throws: minimum validation error +$percentageSchema->parse(150.0); // Throws: maximum validation error ``` ### Coordinates ```php -$latitudeSchema = $p->float()->gte(-90.0)->lte(90.0); -$longitudeSchema = $p->float()->gte(-180.0)->lte(180.0); +$latitudeSchema = $p->float()->minimum(-90.0)->maximum(90.0); +$longitudeSchema = $p->float()->minimum(-180.0)->maximum(180.0); $coordinatesSchema = $p->object([ 'lat' => $latitudeSchema, @@ -81,8 +82,8 @@ $coordinatesSchema->parse(['lat' => 47.1, 'lng' => 8.2]); | Code | Description | |------|-------------| | `float.type` | Value is not a float | -| `float.gt` | Value is not greater than threshold (used by `gt()` and `positive()`) | -| `float.gte` | Value is not greater than or equal to threshold (used by `gte()` and `nonNegative()`) | -| `float.lt` | Value is not less than threshold (used by `lt()` and `negative()`) | -| `float.lte` | Value is not less than or equal to threshold (used by `lte()` and `nonPositive()`) | +| `float.minimum` | Value is not greater than or equal to threshold (used by `minimum` and `nonNegative()`) | +| `float.exclusiveMinimum` | Value is not greater than threshold (used by `exclusiveMinimum()` and `positive()`) | +| `float.exclusiveMaximum` | Value is not less than threshold (used by `exclusiveMaximum()` and `negative()`) | +| `float.maximum` | Value is not less than or equal to threshold (used by `maximum()` and `nonPositive()`) | | `float.int` | Cannot convert float to int without precision loss (for `toInt()`) | diff --git a/doc/Schema/IntSchema.md b/doc/Schema/IntSchema.md index 4e2a12f..f2b3c26 100644 --- a/doc/Schema/IntSchema.md +++ b/doc/Schema/IntSchema.md @@ -19,10 +19,10 @@ $data = $schema->parse(1337); // Returns: 1337 ### Comparison Constraints ```php -$schema->gt(5); // Greater than 5 -$schema->gte(5); // Greater than or equal to 5 -$schema->lt(10); // Less than 10 -$schema->lte(10); // Less than or equal to 10 +$schema->minimum(5); // Greater than or equal to 5 +$schema->exclusiveMinimum(5); // Greater than 5 +$schema->exclusiveMaximum(10); // Less than 10 +$schema->maximum(10); // Less than or equal to 10 ``` ### Sign Constraints @@ -57,18 +57,18 @@ $positiveSchema->parse(-1); // Throws: positive validation error ### Range Validation ```php -$ageSchema = $p->int()->gte(0)->lte(150); +$ageSchema = $p->int()->minimum(0)->maximum(150); $ageSchema->parse(25); // Returns: 25 -$ageSchema->parse(-1); // Throws: gte validation error -$ageSchema->parse(200); // Throws: lte validation error +$ageSchema->parse(-1); // Throws: minimum validation error +$ageSchema->parse(200); // Throws: maximum validation error ``` ### Pagination ```php $offsetSchema = $p->int()->nonNegative(); -$limitSchema = $p->int()->positive()->lte(100); +$limitSchema = $p->int()->positive()->maximum(100); $paginationSchema = $p->object([ 'offset' => $offsetSchema, @@ -90,7 +90,7 @@ $date = $timestampSchema->parse(1705744500); | Code | Description | |------|-------------| | `int.type` | Value is not an integer | -| `int.gt` | Value is not greater than threshold (used by `gt()` and `positive()`) | -| `int.gte` | Value is not greater than or equal to threshold (used by `gte()` and `nonNegative()`) | -| `int.lt` | Value is not less than threshold (used by `lt()` and `negative()`) | -| `int.lte` | Value is not less than or equal to threshold (used by `lte()` and `nonPositive()`) | +| `int.minimum` | Value is not greater than or equal to threshold (used by `minimum` and `nonNegative()`) | +| `int.exclusiveMinimum` | Value is not greater than threshold (used by `exclusiveMinimum()` and `positive()`) | +| `int.exclusiveMaximum` | Value is not less than threshold (used by `exclusiveMaximum()` and `negative()`) | +| `int.maximum` | Value is not less than or equal to threshold (used by `maximum()` and `nonPositive()`) | diff --git a/doc/Schema/LazySchema.md b/doc/Schema/LazySchema.md index 7c38e7d..1b80383 100644 --- a/doc/Schema/LazySchema.md +++ b/doc/Schema/LazySchema.md @@ -123,14 +123,14 @@ $commentSchema->parse([ ```php $fileSchema = $p->object([ - 'type' => $p->literal('file'), + 'type' => $p->const('file'), 'name' => $p->string(), 'size' => $p->int()->nonNegative(), ]); $folderSchema = $p->lazy(static function () use ($p, &$folderSchema, $fileSchema) { return $p->object([ - 'type' => $p->literal('folder'), + 'type' => $p->const('folder'), 'name' => $p->string(), 'children' => $p->array( $p->union([$fileSchema, $folderSchema]) @@ -192,19 +192,19 @@ $employeeSchema->parse([ $jsonSchemaType = $p->lazy(static function () use ($p, &$jsonSchemaType) { return $p->discriminatedUnion([ $p->object([ - 'type' => $p->literal('string'), + 'type' => $p->const('string'), 'minLength' => $p->int()->nonNegative(), ])->optional(['minLength']), $p->object([ - 'type' => $p->literal('number'), + 'type' => $p->const('number'), 'minimum' => $p->float(), ])->optional(['minimum']), $p->object([ - 'type' => $p->literal('object'), + 'type' => $p->const('object'), 'properties' => $p->record($jsonSchemaType), ]), $p->object([ - 'type' => $p->literal('array'), + 'type' => $p->const('array'), 'items' => $jsonSchemaType, ]), ], 'type'); diff --git a/doc/Schema/ObjectSchema.md b/doc/Schema/ObjectSchema.md index 3a9f1a6..62d9464 100644 --- a/doc/Schema/ObjectSchema.md +++ b/doc/Schema/ObjectSchema.md @@ -123,7 +123,7 @@ $userSchema = $p->object([ $createUserSchema = $p->object([ 'name' => $p->string()->trim()->minLength(1)->maxLength(100), 'email' => $p->string()->trim()->toLowerCase()->email(), - 'age' => $p->int()->gte(0)->lte(150), + 'age' => $p->int()->minimum(0)->maximum(150), ])->strict(); ``` @@ -133,7 +133,7 @@ $createUserSchema = $p->object([ $addressSchema = $p->object([ 'street' => $p->string(), 'city' => $p->string(), - 'zipCode' => $p->string()->regexp('/^\d{5}$/'), + 'zipCode' => $p->string()->pattern('/^\d{5}$/'), ]); $personSchema = $p->object([ @@ -182,8 +182,8 @@ $petSchema = $p->object([ $listRequestSchema = $p->object([ 'offset' => $p->int()->nonNegative(), - 'limit' => $p->int()->positive()->lte(100), - 'sort' => $p->record($p->literal('asc')), + 'limit' => $p->int()->positive()->maximum(100), + 'sort' => $p->record($p->const('asc')), 'items' => $p->array($petSchema), ]); ``` diff --git a/doc/Schema/RecordSchema.md b/doc/Schema/RecordSchema.md index a245d9a..50bd00b 100644 --- a/doc/Schema/RecordSchema.md +++ b/doc/Schema/RecordSchema.md @@ -71,8 +71,8 @@ $configSchema->parse([ ```php $sortSchema = $p->record( $p->union([ - $p->literal('asc'), - $p->literal('desc'), + $p->const('asc'), + $p->const('desc'), ]) ); diff --git a/doc/Schema/StringSchema.md b/doc/Schema/StringSchema.md index cd0f554..3a4c72b 100644 --- a/doc/Schema/StringSchema.md +++ b/doc/Schema/StringSchema.md @@ -27,10 +27,10 @@ $schema->maxLength(100); // Maximum 100 characters ### Content Checks ```php -$schema->includes('amp'); // Must contain 'amp' +$schema->contains('amp'); // Must contain 'amp' $schema->startsWith('exa'); // Must start with 'exa' $schema->endsWith('ple'); // Must end with 'ple' -$schema->regexp('/^[a-z]+$/i'); // Must match regex pattern +$schema->pattern('/^[a-z]+$/i'); // Must match regex pattern ``` ### Format Validations @@ -38,12 +38,12 @@ $schema->regexp('/^[a-z]+$/i'); // Must match regex pattern ```php use Chubbyphp\Parsing\Enum\Uuid; -$schema->domain(); // Valid domain +$schema->hostname(); // Valid hostname $schema->email(); // Valid email address $schema->ipV4(); // Valid IPv4 address $schema->ipV6(); // Valid IPv6 address $schema->mac(); // Valid mac address -$schema->url(); // Valid URL +$schema->uri(); // Valid URI $schema->uuid(); // Valid UUID v4 $schema->uuid(Uuid::v5); // Valid UUID v5 ``` @@ -110,7 +110,7 @@ $usernameSchema = $p->string() ->toLowerCase() ->minLength(3) ->maxLength(20) - ->regexp('/^[a-z0-9_]+$/'); + ->pattern('/^[a-z0-9_]+$/'); $usernameSchema->parse(' John_Doe123 '); // Returns: 'john_doe123' ``` @@ -123,16 +123,16 @@ $usernameSchema->parse(' John_Doe123 '); // Returns: 'john_doe123' | `string.length` | String length doesn't match exact length | | `string.minLength` | String is shorter than minimum | | `string.maxLength` | String is longer than maximum | -| `string.includes` | String doesn't contain required substring | +| `string.contains` | String doesn't contain required substring | | `string.startsWith` | String doesn't start with required prefix | | `string.endsWith` | String doesn't end with required suffix | -| `string.domain` | Invalid domain format | +| `string.hostname` | Invalid hostname format | | `string.email` | Invalid email format | | `string.ipV4` | Invalid IPv4 format | | `string.ipV6` | Invalid IPv6 format | | `string.mac` | Invalid mac format | -| `string.regexp` | String doesn't match regex pattern | -| `string.url` | Invalid URL format | +| `string.pattern` | String doesn't match the pattern | +| `string.uri` | Invalid URI format | | `string.uuidV4` | Invalid UUID v4 format | | `string.uuidV5` | Invalid UUID v5 format | | `string.bool` | Cannot convert string to bool (for `toBool()`) | diff --git a/doc/Schema/TupleSchema.md b/doc/Schema/TupleSchema.md index ee74599..9631acd 100644 --- a/doc/Schema/TupleSchema.md +++ b/doc/Schema/TupleSchema.md @@ -30,8 +30,8 @@ A tuple schema: ```php $coordinatesSchema = $p->tuple([ - $p->float()->gte(-90)->lte(90), // Latitude - $p->float()->gte(-180)->lte(180), // Longitude + $p->float()->minimum(-90)->maximum(90), // Latitude + $p->float()->minimum(-180)->maximum(180), // Longitude ]); $coordinatesSchema->parse([47.3769, 8.5417]); // Zurich coordinates @@ -41,9 +41,9 @@ $coordinatesSchema->parse([47.3769, 8.5417]); // Zurich coordinates ```php $rgbSchema = $p->tuple([ - $p->int()->gte(0)->lte(255), // Red - $p->int()->gte(0)->lte(255), // Green - $p->int()->gte(0)->lte(255), // Blue + $p->int()->minimum(0)->maximum(255), // Red + $p->int()->minimum(0)->maximum(255), // Green + $p->int()->minimum(0)->maximum(255), // Blue ]); $rgbSchema->parse([255, 128, 0]); // Orange color @@ -53,10 +53,10 @@ $rgbSchema->parse([255, 128, 0]); // Orange color ```php $rgbaSchema = $p->tuple([ - $p->int()->gte(0)->lte(255), // Red - $p->int()->gte(0)->lte(255), // Green - $p->int()->gte(0)->lte(255), // Blue - $p->float()->gte(0)->lte(1), // Alpha + $p->int()->minimum(0)->maximum(255), // Red + $p->int()->minimum(0)->maximum(255), // Green + $p->int()->minimum(0)->maximum(255), // Blue + $p->float()->minimum(0)->maximum(1), // Alpha ]); $rgbaSchema->parse([255, 128, 0, 0.5]); // Semi-transparent orange @@ -107,9 +107,9 @@ $point3dSchema->parse([1.5, 2.5, 3.5]); ```php $datePartsSchema = $p->tuple([ - $p->int()->gte(1)->lte(9999), // Year - $p->int()->gte(1)->lte(12), // Month - $p->int()->gte(1)->lte(31), // Day + $p->int()->minimum(1)->maximum(9999), // Year + $p->int()->minimum(1)->maximum(12), // Month + $p->int()->minimum(1)->maximum(31), // Day ]); $datePartsSchema->parse([2024, 1, 20]); diff --git a/doc/Schema/UnionSchema.md b/doc/Schema/UnionSchema.md index eb1d474..304058b 100644 --- a/doc/Schema/UnionSchema.md +++ b/doc/Schema/UnionSchema.md @@ -46,7 +46,7 @@ While `nullable()` is preferred for simple null handling, unions can express mor ```php $schema = $p->union([ $p->string()->minLength(1), - $p->literal(null), + $p->const(null), ]); $schema->parse('hello'); // Returns: 'hello' @@ -70,12 +70,12 @@ $idSchema->parse('550e8400-e29b-41d4-a716-446655440000'); // Returns: UUID strin ```php $successSchema = $p->object([ - 'status' => $p->literal('success'), + 'status' => $p->const('success'), 'data' => $p->object([...]), ]); $errorSchema = $p->object([ - 'status' => $p->literal('error'), + 'status' => $p->const('error'), 'message' => $p->string(), ]); diff --git a/src/Parser.php b/src/Parser.php index b5613e3..c3192a9 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -8,6 +8,7 @@ use Chubbyphp\Parsing\Schema\AssocSchema; use Chubbyphp\Parsing\Schema\BackedEnumSchema; use Chubbyphp\Parsing\Schema\BoolSchema; +use Chubbyphp\Parsing\Schema\ConstSchema; use Chubbyphp\Parsing\Schema\DateTimeSchema; use Chubbyphp\Parsing\Schema\DiscriminatedUnionSchema; use Chubbyphp\Parsing\Schema\FloatSchema; @@ -57,6 +58,11 @@ public function dateTime(): DateTimeSchema return new DateTimeSchema(); } + public function const(bool|float|int|string $const): ConstSchema + { + return new ConstSchema($const); + } + /** * @param array $objectSchemas */ @@ -83,8 +89,13 @@ public function lazy(\Closure $lazy): SchemaInterface return new LazySchema($lazy); } + /** + * @deprecated use const + */ public function literal(bool|float|int|string $literal): LiteralSchema { + @trigger_error('Use const instead', E_USER_DEPRECATED); + return new LiteralSchema($literal); } diff --git a/src/ParserInterface.php b/src/ParserInterface.php index d9c75fa..6231da5 100644 --- a/src/ParserInterface.php +++ b/src/ParserInterface.php @@ -8,6 +8,7 @@ use Chubbyphp\Parsing\Schema\AssocSchema; use Chubbyphp\Parsing\Schema\BackedEnumSchema; use Chubbyphp\Parsing\Schema\BoolSchema; +use Chubbyphp\Parsing\Schema\ConstSchema; use Chubbyphp\Parsing\Schema\DateTimeSchema; use Chubbyphp\Parsing\Schema\DiscriminatedUnionSchema; use Chubbyphp\Parsing\Schema\FloatSchema; @@ -23,6 +24,7 @@ /** * @method AssocSchema assoc(array $fieldNameToSchema) + * @method ConstSchema const(bool|float|int|string $const) */ interface ParserInterface { @@ -40,6 +42,8 @@ public function backedEnum(string $backedEnumClass): BackedEnumSchema; public function bool(): BoolSchema; + // public function const(bool|float|int|string $const): ConstSchema; + public function dateTime(): DateTimeSchema; /** @@ -56,6 +60,9 @@ public function int(): IntSchema; */ public function lazy(\Closure $lazy): SchemaInterface; + /** + * @deprecated use const() instead + */ public function literal(bool|float|int|string $literal): LiteralSchema; /** diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php index 03f4ad0..0d01d71 100644 --- a/src/Schema/ArraySchema.php +++ b/src/Schema/ArraySchema.php @@ -13,6 +13,15 @@ final class ArraySchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_TYPE_CODE = 'array.type'; public const string ERROR_TYPE_TEMPLATE = 'Type should be "array", {{given}} given'; + public const string ERROR_EXACT_ITEMS_CODE = 'array.exactItems'; + public const string ERROR_EXACT_ITEMS_TEMPLATE = 'Items count {{exactItems}}, {{given}} given'; + + public const string ERROR_MIN_ITEMS_CODE = 'array.minItems'; + public const string ERROR_MIN_ITEMS_TEMPLATE = 'Min items {{minItems}}, {{given}} given'; + + public const string ERROR_MAX_ITEMS_CODE = 'array.maxItems'; + public const string ERROR_MAX_ITEMS_TEMPLATE = 'Max items {{maxItems}}, {{given}} given'; + public const string ERROR_LENGTH_CODE = 'array.length'; public const string ERROR_LENGTH_TEMPLATE = 'Length {{length}}, {{given}} given'; @@ -22,60 +31,152 @@ final class ArraySchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_MAX_LENGTH_CODE = 'array.maxLength'; public const string ERROR_MAX_LENGTH_TEMPLATE = 'Max length {{max}}, {{given}} given'; + public const string ERROR_CONTAINS_CODE = 'array.contains'; + public const string ERROR_CONTAINS_TEMPLATE = '{{given}} does not contain {{contains}}'; + public const string ERROR_INCLUDES_CODE = 'array.includes'; public const string ERROR_INCLUDES_TEMPLATE = '{{given}} does not include {{includes}}'; public function __construct(private SchemaInterface $itemSchema) {} + public function exactItems(int $exactItems): static + { + return $this->postParse(static function (array $array) use ($exactItems) { + $arrayLength = \count($array); + + if ($arrayLength === $exactItems) { + return $array; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXACT_ITEMS_CODE, + self::ERROR_EXACT_ITEMS_TEMPLATE, + ['exactItems' => $exactItems, 'given' => $arrayLength] + ) + ); + }); + } + + public function minItems(int $minItems): static + { + return $this->postParse(static function (array $array) use ($minItems) { + $arrayLength = \count($array); + + if ($arrayLength >= $minItems) { + return $array; + } + + throw new ErrorsException( + new Error( + self::ERROR_MIN_ITEMS_CODE, + self::ERROR_MIN_ITEMS_TEMPLATE, + ['minItems' => $minItems, 'given' => $arrayLength] + ) + ); + }); + } + + public function maxItems(int $maxItems): static + { + return $this->postParse(static function (array $array) use ($maxItems) { + $arrayLength = \count($array); + + if ($arrayLength <= $maxItems) { + return $array; + } + + throw new ErrorsException( + new Error( + self::ERROR_MAX_ITEMS_CODE, + self::ERROR_MAX_ITEMS_TEMPLATE, + ['maxItems' => $maxItems, 'given' => $arrayLength] + ) + ); + }); + } + + /** + * @deprecated use exactItems + */ public function length(int $length): static { + @trigger_error('Use exactItems instead', E_USER_DEPRECATED); + return $this->postParse(static function (array $array) use ($length) { $arrayLength = \count($array); - if ($arrayLength !== $length) { - throw new ErrorsException( - new Error( - self::ERROR_LENGTH_CODE, - self::ERROR_LENGTH_TEMPLATE, - ['length' => $length, 'given' => $arrayLength] - ) - ); + if ($arrayLength === $length) { + return $array; } - return $array; + throw new ErrorsException( + new Error( + self::ERROR_LENGTH_CODE, + self::ERROR_LENGTH_TEMPLATE, + ['length' => $length, 'given' => $arrayLength] + ) + ); }); } + /** + * @deprecated use minItems + */ public function minLength(int $minLength): static { + @trigger_error('Use minItems instead', E_USER_DEPRECATED); + return $this->postParse(static function (array $array) use ($minLength) { $arrayLength = \count($array); - if ($arrayLength < $minLength) { - throw new ErrorsException( - new Error( - self::ERROR_MIN_LENGTH_CODE, - self::ERROR_MIN_LENGTH_TEMPLATE, - ['minLength' => $minLength, 'given' => $arrayLength] - ) - ); + if ($arrayLength >= $minLength) { + return $array; } - return $array; + throw new ErrorsException( + new Error( + self::ERROR_MIN_LENGTH_CODE, + self::ERROR_MIN_LENGTH_TEMPLATE, + ['minLength' => $minLength, 'given' => $arrayLength] + ) + ); }); } + /** + * @deprecated use maxItems + */ public function maxLength(int $maxLength): static { + @trigger_error('Use maxItems instead', E_USER_DEPRECATED); + return $this->postParse(static function (array $array) use ($maxLength) { $arrayLength = \count($array); - if ($arrayLength > $maxLength) { + if ($arrayLength <= $maxLength) { + return $array; + } + + throw new ErrorsException( + new Error( + self::ERROR_MAX_LENGTH_CODE, + self::ERROR_MAX_LENGTH_TEMPLATE, + ['maxLength' => $maxLength, 'given' => $arrayLength] + ) + ); + }); + } + + public function contains(mixed $contains, bool $strict = true): static + { + return $this->postParse(static function (array $array) use ($contains, $strict) { + if (!\in_array($contains, $array, $strict)) { throw new ErrorsException( new Error( - self::ERROR_MAX_LENGTH_CODE, - self::ERROR_MAX_LENGTH_TEMPLATE, - ['maxLength' => $maxLength, 'given' => $arrayLength] + self::ERROR_CONTAINS_CODE, + self::ERROR_CONTAINS_TEMPLATE, + ['contains' => $contains, 'given' => $array] ) ); } @@ -84,8 +185,13 @@ public function maxLength(int $maxLength): static }); } + /** + * @deprecated use contains + */ public function includes(mixed $includes, bool $strict = true): static { + @trigger_error('Use contains instead', E_USER_DEPRECATED); + return $this->postParse(static function (array $array) use ($includes, $strict) { if (!\in_array($includes, $array, $strict)) { throw new ErrorsException( diff --git a/src/Schema/BoolSchema.php b/src/Schema/BoolSchema.php index 92c945b..f2fd7e2 100644 --- a/src/Schema/BoolSchema.php +++ b/src/Schema/BoolSchema.php @@ -44,16 +44,16 @@ public function toString(): StringSchema protected function innerParse(mixed $input): mixed { - if (!\is_bool($input)) { - throw new ErrorsException( - new Error( - self::ERROR_TYPE_CODE, - self::ERROR_TYPE_TEMPLATE, - ['given' => $this->getDataType($input)] - ) - ); + if (\is_bool($input)) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_TYPE_CODE, + self::ERROR_TYPE_TEMPLATE, + ['given' => $this->getDataType($input)] + ) + ); } } diff --git a/src/Schema/ConstSchema.php b/src/Schema/ConstSchema.php new file mode 100644 index 0000000..ad1cca4 --- /dev/null +++ b/src/Schema/ConstSchema.php @@ -0,0 +1,44 @@ + $this->getDataType($input)] + ) + ); + } + + if ($input === $this->const) { + return $input; + } + + throw new ErrorsException( + new Error( + self::ERROR_EQUALS_CODE, + self::ERROR_EQUALS_TEMPLATE, + ['expected' => $this->const, 'given' => $input] + ) + ); + } +} diff --git a/src/Schema/DateTimeSchema.php b/src/Schema/DateTimeSchema.php index 917011c..5c26d05 100644 --- a/src/Schema/DateTimeSchema.php +++ b/src/Schema/DateTimeSchema.php @@ -21,34 +21,34 @@ final class DateTimeSchema extends AbstractSchemaInnerParse implements SchemaInt public function from(\DateTimeImmutable $from): static { return $this->postParse(static function (\DateTimeImmutable $datetime) use ($from) { - if ($datetime < $from) { - throw new ErrorsException( - new Error( - self::ERROR_FROM_CODE, - self::ERROR_FROM_TEMPLATE, - ['from' => $from->format('c'), 'given' => $datetime->format('c')] - ) - ); + if ($datetime >= $from) { + return $datetime; } - return $datetime; + throw new ErrorsException( + new Error( + self::ERROR_FROM_CODE, + self::ERROR_FROM_TEMPLATE, + ['from' => $from->format('c'), 'given' => $datetime->format('c')] + ) + ); }); } public function to(\DateTimeImmutable $to): static { return $this->postParse(static function (\DateTimeImmutable $datetime) use ($to) { - if ($datetime > $to) { - throw new ErrorsException( - new Error( - self::ERROR_TO_CODE, - self::ERROR_TO_TEMPLATE, - ['to' => $to->format('c'), 'given' => $datetime->format('c')] - ) - ); + if ($datetime <= $to) { + return $datetime; } - return $datetime; + throw new ErrorsException( + new Error( + self::ERROR_TO_CODE, + self::ERROR_TO_TEMPLATE, + ['to' => $to->format('c'), 'given' => $datetime->format('c')] + ) + ); }); } @@ -74,16 +74,16 @@ public function toString(): StringSchema protected function innerParse(mixed $input): mixed { - if (!$input instanceof \DateTimeInterface) { - throw new ErrorsException( - new Error( - self::ERROR_TYPE_CODE, - self::ERROR_TYPE_TEMPLATE, - ['given' => $this->getDataType($input)] - ) - ); + if ($input instanceof \DateTimeInterface) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_TYPE_CODE, + self::ERROR_TYPE_TEMPLATE, + ['given' => $this->getDataType($input)] + ) + ); } } diff --git a/src/Schema/FloatSchema.php b/src/Schema/FloatSchema.php index bedca94..3041290 100644 --- a/src/Schema/FloatSchema.php +++ b/src/Schema/FloatSchema.php @@ -12,12 +12,24 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_TYPE_CODE = 'float.type'; public const string ERROR_TYPE_TEMPLATE = 'Type should be "float", {{given}} given'; - public const string ERROR_GT_CODE = 'float.gt'; - public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + public const string ERROR_MINIMUM_CODE = 'float.minimum'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be greater than or equal {{minimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'float.exclusiveMinimum'; + public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'float.exclusiveMaximum'; + public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; + + public const string ERROR_MAXIMUM_CODE = 'float.maximum'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be lesser than or equal {{maximum}}, {{given}} given'; public const string ERROR_GTE_CODE = 'float.gte'; public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; + public const string ERROR_GT_CODE = 'float.gt'; + public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + public const string ERROR_LT_CODE = 'float.lt'; public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; @@ -27,71 +39,159 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_INT_CODE = 'float.int'; public const string ERROR_INT_TEMPLATE = 'Cannot convert {{given}} to int'; - public function gt(float $gt): static + public function minimum(float $minimum): static { - return $this->postParse(static function (float $float) use ($gt) { - if ($float <= $gt) { - throw new ErrorsException( - new Error( - self::ERROR_GT_CODE, - self::ERROR_GT_TEMPLATE, - ['gt' => $gt, 'given' => $float] - ) - ); + return $this->postParse(static function (float $float) use ($minimum) { + if ($float >= $minimum) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_MINIMUM_CODE, + self::ERROR_MINIMUM_TEMPLATE, + ['minimum' => $minimum, 'given' => $float] + ) + ); + }); + } + + public function exclusiveMinimum(float $exclusiveMinimum): static + { + return $this->postParse(static function (float $float) use ($exclusiveMinimum) { + if ($float > $exclusiveMinimum) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MINIMUM_CODE, + self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, + ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $float] + ) + ); + }); + } + + public function exclusiveMaximum(float $exclusiveMaximum): static + { + return $this->postParse(static function (float $float) use ($exclusiveMaximum) { + if ($float < $exclusiveMaximum) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MAXIMUM_CODE, + self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, + ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $float] + ) + ); + }); + } + + public function maximum(float $maximum): static + { + return $this->postParse(static function (float $float) use ($maximum) { + if ($float <= $maximum) { + return $float; } - return $float; + throw new ErrorsException( + new Error( + self::ERROR_MAXIMUM_CODE, + self::ERROR_MAXIMUM_TEMPLATE, + ['maximum' => $maximum, 'given' => $float] + ) + ); }); } + /** + * @deprecated use minimum + */ public function gte(float $gte): static { + @trigger_error('Use minimum instead', E_USER_DEPRECATED); + return $this->postParse(static function (float $float) use ($gte) { - if ($float < $gte) { - throw new ErrorsException( - new Error( - self::ERROR_GTE_CODE, - self::ERROR_GTE_TEMPLATE, - ['gte' => $gte, 'given' => $float] - ) - ); + if ($float >= $gte) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_GTE_CODE, + self::ERROR_GTE_TEMPLATE, + ['gte' => $gte, 'given' => $float] + ) + ); + }); + } + + /** + * @deprecated use exclusiveMinimum + */ + public function gt(float $gt): static + { + @trigger_error('Use exclusiveMinimum instead', E_USER_DEPRECATED); + + return $this->postParse(static function (float $float) use ($gt) { + if ($float > $gt) { + return $float; } - return $float; + throw new ErrorsException( + new Error( + self::ERROR_GT_CODE, + self::ERROR_GT_TEMPLATE, + ['gt' => $gt, 'given' => $float] + ) + ); }); } + /** + * @deprecated use exclusiveMaximum + */ public function lt(float $lt): static { + @trigger_error('Use exclusiveMaximum instead', E_USER_DEPRECATED); + return $this->postParse(static function (float $float) use ($lt) { - if ($float >= $lt) { - throw new ErrorsException( - new Error( - self::ERROR_LT_CODE, - self::ERROR_LT_TEMPLATE, - ['lt' => $lt, 'given' => $float] - ) - ); + if ($float < $lt) { + return $float; } - return $float; + throw new ErrorsException( + new Error( + self::ERROR_LT_CODE, + self::ERROR_LT_TEMPLATE, + ['lt' => $lt, 'given' => $float] + ) + ); }); } + /** + * @deprecated use maximum + */ public function lte(float $lte): static { + @trigger_error('Use maximum instead', E_USER_DEPRECATED); + return $this->postParse(static function (float $float) use ($lte) { - if ($float > $lte) { - throw new ErrorsException( - new Error( - self::ERROR_LTE_CODE, - self::ERROR_LTE_TEMPLATE, - ['lte' => $lte, 'given' => $float] - ) - ); + if ($float <= $lte) { + return $float; } - return $float; + throw new ErrorsException( + new Error( + self::ERROR_LTE_CODE, + self::ERROR_LTE_TEMPLATE, + ['lte' => $lte, 'given' => $float] + ) + ); }); } @@ -127,17 +227,17 @@ public function toInt(): IntSchema $intInput = (int) $input; - if ((float) $intInput !== $input) { - throw new ErrorsException( - new Error( - self::ERROR_INT_CODE, - self::ERROR_INT_TEMPLATE, - ['given' => $input] - ) - ); + if ((float) $intInput === $input) { + return $intInput; } - return $intInput; + throw new ErrorsException( + new Error( + self::ERROR_INT_CODE, + self::ERROR_INT_TEMPLATE, + ['given' => $input] + ) + ); })->nullable($this->nullable); } @@ -153,16 +253,16 @@ public function toString(): StringSchema protected function innerParse(mixed $input): mixed { - if (!\is_float($input)) { - throw new ErrorsException( - new Error( - self::ERROR_TYPE_CODE, - self::ERROR_TYPE_TEMPLATE, - ['given' => $this->getDataType($input)] - ) - ); + if (\is_float($input)) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_TYPE_CODE, + self::ERROR_TYPE_TEMPLATE, + ['given' => $this->getDataType($input)] + ) + ); } } diff --git a/src/Schema/IntSchema.php b/src/Schema/IntSchema.php index 58d0705..a57d935 100644 --- a/src/Schema/IntSchema.php +++ b/src/Schema/IntSchema.php @@ -12,83 +12,183 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac public const string ERROR_TYPE_CODE = 'int.type'; public const string ERROR_TYPE_TEMPLATE = 'Type should be "int", {{given}} given'; - public const string ERROR_GT_CODE = 'int.gt'; - public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + public const string ERROR_MINIMUM_CODE = 'int.minimum'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be greater than or equal {{minimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'int.exclusiveMinimum'; + public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'int.exclusiveMaximum'; + public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; + + public const string ERROR_MAXIMUM_CODE = 'int.maximum'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be lesser than or equal {{maximum}}, {{given}} given'; public const string ERROR_GTE_CODE = 'int.gte'; public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; + public const string ERROR_GT_CODE = 'int.gt'; + public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + public const string ERROR_LT_CODE = 'int.lt'; public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; public const string ERROR_LTE_CODE = 'int.lte'; public const string ERROR_LTE_TEMPLATE = 'Value should be lesser than or equal {{lte}}, {{given}} given'; - public function gt(int $gt): static + public function minimum(int $minimum): static { - return $this->postParse(static function (int $int) use ($gt) { - if ($int <= $gt) { - throw new ErrorsException( - new Error( - self::ERROR_GT_CODE, - self::ERROR_GT_TEMPLATE, - ['gt' => $gt, 'given' => $int] - ) - ); + return $this->postParse(static function (int $int) use ($minimum) { + if ($int >= $minimum) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_MINIMUM_CODE, + self::ERROR_MINIMUM_TEMPLATE, + ['minimum' => $minimum, 'given' => $int] + ) + ); + }); + } + + public function exclusiveMinimum(int $exclusiveMinimum): static + { + return $this->postParse(static function (int $int) use ($exclusiveMinimum) { + if ($int > $exclusiveMinimum) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MINIMUM_CODE, + self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, + ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $int] + ) + ); + }); + } + + public function exclusiveMaximum(int $exclusiveMaximum): static + { + return $this->postParse(static function (int $int) use ($exclusiveMaximum) { + if ($int < $exclusiveMaximum) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MAXIMUM_CODE, + self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, + ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $int] + ) + ); + }); + } + + public function maximum(int $maximum): static + { + return $this->postParse(static function (int $int) use ($maximum) { + if ($int <= $maximum) { + return $int; } - return $int; + throw new ErrorsException( + new Error( + self::ERROR_MAXIMUM_CODE, + self::ERROR_MAXIMUM_TEMPLATE, + ['maximum' => $maximum, 'given' => $int] + ) + ); }); } + /** + * @deprecated use minimum + */ public function gte(int $gte): static { + @trigger_error('Use minimum instead', E_USER_DEPRECATED); + return $this->postParse(static function (int $int) use ($gte) { - if ($int < $gte) { - throw new ErrorsException( - new Error( - self::ERROR_GTE_CODE, - self::ERROR_GTE_TEMPLATE, - ['gte' => $gte, 'given' => $int] - ) - ); + if ($int >= $gte) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_GTE_CODE, + self::ERROR_GTE_TEMPLATE, + ['gte' => $gte, 'given' => $int] + ) + ); + }); + } + + /** + * @deprecated use exclusiveMinimum + */ + public function gt(int $gt): static + { + @trigger_error('Use exclusiveMinimum instead', E_USER_DEPRECATED); + + return $this->postParse(static function (int $int) use ($gt) { + if ($int > $gt) { + return $int; } - return $int; + throw new ErrorsException( + new Error( + self::ERROR_GT_CODE, + self::ERROR_GT_TEMPLATE, + ['gt' => $gt, 'given' => $int] + ) + ); }); } + /** + * @deprecated use exclusiveMaximum + */ public function lt(int $lt): static { + @trigger_error('Use exclusiveMaximum instead', E_USER_DEPRECATED); + return $this->postParse(static function (int $int) use ($lt) { - if ($int >= $lt) { - throw new ErrorsException( - new Error( - self::ERROR_LT_CODE, - self::ERROR_LT_TEMPLATE, - ['lt' => $lt, 'given' => $int] - ) - ); + if ($int < $lt) { + return $int; } - return $int; + throw new ErrorsException( + new Error( + self::ERROR_LT_CODE, + self::ERROR_LT_TEMPLATE, + ['lt' => $lt, 'given' => $int] + ) + ); }); } + /** + * @deprecated use maximum + */ public function lte(int $lte): static { + @trigger_error('Use maximum instead', E_USER_DEPRECATED); + return $this->postParse(static function (int $int) use ($lte) { - if ($int > $lte) { - throw new ErrorsException( - new Error( - self::ERROR_LTE_CODE, - self::ERROR_LTE_TEMPLATE, - ['lte' => $lte, 'given' => $int] - ) - ); + if ($int <= $lte) { + return $int; } - return $int; + throw new ErrorsException( + new Error( + self::ERROR_LTE_CODE, + self::ERROR_LTE_TEMPLATE, + ['lte' => $lte, 'given' => $int] + ) + ); }); } @@ -145,16 +245,16 @@ public function toDateTime(): DateTimeSchema protected function innerParse(mixed $input): mixed { - if (!\is_int($input)) { - throw new ErrorsException( - new Error( - self::ERROR_TYPE_CODE, - self::ERROR_TYPE_TEMPLATE, - ['given' => $this->getDataType($input)] - ) - ); + if (\is_int($input)) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_TYPE_CODE, + self::ERROR_TYPE_TEMPLATE, + ['given' => $this->getDataType($input)] + ) + ); } } diff --git a/src/Schema/LiteralSchema.php b/src/Schema/LiteralSchema.php index 22327f3..01bb736 100644 --- a/src/Schema/LiteralSchema.php +++ b/src/Schema/LiteralSchema.php @@ -7,6 +7,9 @@ use Chubbyphp\Parsing\Error; use Chubbyphp\Parsing\ErrorsException; +/** + * @deprecated use ConstSchema instead + */ final class LiteralSchema extends AbstractSchemaInnerParse { public const string ERROR_TYPE_CODE = 'literal.type'; @@ -29,16 +32,16 @@ protected function innerParse(mixed $input): mixed ); } - if ($input !== $this->literal) { - throw new ErrorsException( - new Error( - self::ERROR_EQUALS_CODE, - self::ERROR_EQUALS_TEMPLATE, - ['expected' => $this->literal, 'given' => $input] - ) - ); + if ($input === $this->literal) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_EQUALS_CODE, + self::ERROR_EQUALS_TEMPLATE, + ['expected' => $this->literal, 'given' => $input] + ) + ); } } diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php index d0325a7..1e5a0e5 100644 --- a/src/Schema/StringSchema.php +++ b/src/Schema/StringSchema.php @@ -22,6 +22,9 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_MAX_LENGTH_CODE = 'string.maxLength'; public const string ERROR_MAX_LENGTH_TEMPLATE = 'Max length {{max}}, {{given}} given'; + public const string ERROR_CONTAINS_CODE = 'string.contains'; + public const string ERROR_CONTAINS_TEMPLATE = '{{given}} does not contain {{contains}}'; + public const string ERROR_INCLUDES_CODE = 'string.includes'; public const string ERROR_INCLUDES_TEMPLATE = '{{given}} does not include {{includes}}'; @@ -31,6 +34,9 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_ENDSWITH_CODE = 'string.endsWith'; public const string ERROR_ENDSWITH_TEMPLATE = '{{given}} does not ends with {{endsWith}}'; + public const string ERROR_HOSTNAME_CODE = 'string.hostname'; + public const string ERROR_HOSTNAME_TEMPLATE = 'Invalid hostname {{given}}'; + public const string ERROR_DOMAIN_CODE = 'string.domain'; public const string ERROR_DOMAIN_TEMPLATE = 'Invalid domain {{given}}'; @@ -46,9 +52,15 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_MATCH_CODE = 'string.match'; public const string ERROR_MATCH_TEMPLATE = '{{given}} does not match {{match}}'; + public const string ERROR_PATTERN_CODE = 'string.pattern'; + public const string ERROR_PATTERN_TEMPLATE = '{{given}} does not pattern {{pattern}}'; + public const string ERROR_REGEXP_CODE = 'string.regexp'; public const string ERROR_REGEXP_TEMPLATE = '{{given}} does not regexp {{regexp}}'; + public const string ERROR_URI_CODE = 'string.uri'; + public const string ERROR_URI_TEMPLATE = 'Invalid uri {{given}}'; + public const string ERROR_URL_CODE = 'string.url'; public const string ERROR_URL_TEMPLATE = 'Invalid url {{given}}'; @@ -74,17 +86,17 @@ public function length(int $length): static return $this->postParse(static function (string $string) use ($length) { $stringLength = \strlen($string); - if ($stringLength !== $length) { - throw new ErrorsException( - new Error( - self::ERROR_LENGTH_CODE, - self::ERROR_LENGTH_TEMPLATE, - ['length' => $length, 'given' => $stringLength] - ) - ); + if ($stringLength === $length) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_LENGTH_CODE, + self::ERROR_LENGTH_TEMPLATE, + ['length' => $length, 'given' => $stringLength] + ) + ); }); } @@ -93,17 +105,17 @@ public function minLength(int $minLength): static return $this->postParse(static function (string $string) use ($minLength) { $stringLength = \strlen($string); - if ($stringLength < $minLength) { - throw new ErrorsException( - new Error( - self::ERROR_MIN_LENGTH_CODE, - self::ERROR_MIN_LENGTH_TEMPLATE, - ['minLength' => $minLength, 'given' => $stringLength] - ) - ); + if ($stringLength >= $minLength) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_MIN_LENGTH_CODE, + self::ERROR_MIN_LENGTH_TEMPLATE, + ['minLength' => $minLength, 'given' => $stringLength] + ) + ); }); } @@ -112,162 +124,206 @@ public function maxLength(int $maxLength): static return $this->postParse(static function (string $string) use ($maxLength) { $stringLength = \strlen($string); - if ($stringLength > $maxLength) { - throw new ErrorsException( - new Error( - self::ERROR_MAX_LENGTH_CODE, - self::ERROR_MAX_LENGTH_TEMPLATE, - ['maxLength' => $maxLength, 'given' => $stringLength] - ) - ); + if ($stringLength <= $maxLength) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_MAX_LENGTH_CODE, + self::ERROR_MAX_LENGTH_TEMPLATE, + ['maxLength' => $maxLength, 'given' => $stringLength] + ) + ); }); } + public function contains(string $contains): static + { + return $this->postParse(static function (string $string) use ($contains) { + if (str_contains($string, $contains)) { + return $string; + } + + throw new ErrorsException( + new Error( + self::ERROR_CONTAINS_CODE, + self::ERROR_CONTAINS_TEMPLATE, + ['contains' => $contains, 'given' => $string] + ) + ); + }); + } + + /** + * @deprecated use contains + */ public function includes(string $includes): static { + @trigger_error('Use contains instead', E_USER_DEPRECATED); + return $this->postParse(static function (string $string) use ($includes) { - if (!str_contains($string, $includes)) { - throw new ErrorsException( - new Error( - self::ERROR_INCLUDES_CODE, - self::ERROR_INCLUDES_TEMPLATE, - ['includes' => $includes, 'given' => $string] - ) - ); + if (str_contains($string, $includes)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_INCLUDES_CODE, + self::ERROR_INCLUDES_TEMPLATE, + ['includes' => $includes, 'given' => $string] + ) + ); }); } public function startsWith(string $startsWith): static { return $this->postParse(static function (string $string) use ($startsWith) { - if (!str_starts_with($string, $startsWith)) { - throw new ErrorsException( - new Error( - self::ERROR_STARTSWITH_CODE, - self::ERROR_STARTSWITH_TEMPLATE, - ['startsWith' => $startsWith, 'given' => $string] - ) - ); + if (str_starts_with($string, $startsWith)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_STARTSWITH_CODE, + self::ERROR_STARTSWITH_TEMPLATE, + ['startsWith' => $startsWith, 'given' => $string] + ) + ); }); } public function endsWith(string $endsWith): static { return $this->postParse(static function (string $string) use ($endsWith) { - if (!str_ends_with($string, $endsWith)) { - throw new ErrorsException( - new Error( - self::ERROR_ENDSWITH_CODE, - self::ERROR_ENDSWITH_TEMPLATE, - ['endsWith' => $endsWith, 'given' => $string] - ) - ); + if (str_ends_with($string, $endsWith)) { + return $string; + } + + throw new ErrorsException( + new Error( + self::ERROR_ENDSWITH_CODE, + self::ERROR_ENDSWITH_TEMPLATE, + ['endsWith' => $endsWith, 'given' => $string] + ) + ); + }); + } + + public function hostname(): static + { + return $this->postParse(static function (string $string) { + if (filter_var($string, FILTER_VALIDATE_DOMAIN)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_HOSTNAME_CODE, + self::ERROR_HOSTNAME_TEMPLATE, + ['given' => $string] + ) + ); }); } + /** + * @deprecated use hostname + */ public function domain(): static { + @trigger_error('Use hostname instead', E_USER_DEPRECATED); + return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_DOMAIN)) { - throw new ErrorsException( - new Error( - self::ERROR_DOMAIN_CODE, - self::ERROR_DOMAIN_TEMPLATE, - ['given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_DOMAIN)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_DOMAIN_CODE, + self::ERROR_DOMAIN_TEMPLATE, + ['given' => $string] + ) + ); }); } public function email(): static { return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_EMAIL)) { - throw new ErrorsException( - new Error( - self::ERROR_EMAIL_CODE, - self::ERROR_EMAIL_TEMPLATE, - ['given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_EMAIL)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_EMAIL_CODE, + self::ERROR_EMAIL_TEMPLATE, + ['given' => $string] + ) + ); }); } public function ipV4(): static { return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new ErrorsException( - new Error( - self::ERROR_IP_CODE, - self::ERROR_IP_TEMPLATE, - ['version' => 'v4', 'given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_IP_CODE, + self::ERROR_IP_TEMPLATE, + ['version' => 'v4', 'given' => $string] + ) + ); }); } public function ipV6(): static { return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - throw new ErrorsException( - new Error( - self::ERROR_IP_CODE, - self::ERROR_IP_TEMPLATE, - ['version' => 'v6', 'given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_IP_CODE, + self::ERROR_IP_TEMPLATE, + ['version' => 'v6', 'given' => $string] + ) + ); }); } public function mac(): static { return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_MAC)) { - throw new ErrorsException( - new Error( - self::ERROR_MAC_CODE, - self::ERROR_MAC_TEMPLATE, - ['given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_MAC)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_MAC_CODE, + self::ERROR_MAC_TEMPLATE, + ['given' => $string] + ) + ); }); } /** - * @deprecated: use regexp + * @deprecated: use pattern */ public function match(string $match): static { - @trigger_error('Use regexp instead', E_USER_DEPRECATED); + @trigger_error('Use pattern instead', E_USER_DEPRECATED); if (false === @preg_match($match, '')) { throw new \InvalidArgumentException(\sprintf('Invalid match "%s" given', $match)); @@ -276,22 +332,50 @@ public function match(string $match): static return $this->postParse(static function (string $string) use ($match) { $doesMatch = filter_var($string, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => $match]]); - if (false === $doesMatch) { - throw new ErrorsException( - new Error( - self::ERROR_MATCH_CODE, - self::ERROR_MATCH_TEMPLATE, - ['match' => $match, 'given' => $string] - ) - ); + if ($doesMatch) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_MATCH_CODE, + self::ERROR_MATCH_TEMPLATE, + ['match' => $match, 'given' => $string] + ) + ); }); } + public function pattern(string $pattern): static + { + if (false === @preg_match($pattern, '')) { + throw new \InvalidArgumentException(\sprintf('Invalid pattern "%s" given', $pattern)); + } + + return $this->postParse(static function (string $string) use ($pattern) { + $doesMatch = filter_var($string, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => $pattern]]); + + if ($doesMatch) { + return $string; + } + + throw new ErrorsException( + new Error( + self::ERROR_PATTERN_CODE, + self::ERROR_PATTERN_TEMPLATE, + ['pattern' => $pattern, 'given' => $string] + ) + ); + }); + } + + /** + * @deprecated: use pattern + */ public function regexp(string $regexp): static { + @trigger_error('Use pattern instead', E_USER_DEPRECATED); + if (false === @preg_match($regexp, '')) { throw new \InvalidArgumentException(\sprintf('Invalid regexp "%s" given', $regexp)); } @@ -299,34 +383,56 @@ public function regexp(string $regexp): static return $this->postParse(static function (string $string) use ($regexp) { $doesMatch = filter_var($string, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => $regexp]]); - if (false === $doesMatch) { - throw new ErrorsException( - new Error( - self::ERROR_REGEXP_CODE, - self::ERROR_REGEXP_TEMPLATE, - ['regexp' => $regexp, 'given' => $string] - ) - ); + if ($doesMatch) { + return $string; + } + + throw new ErrorsException( + new Error( + self::ERROR_REGEXP_CODE, + self::ERROR_REGEXP_TEMPLATE, + ['regexp' => $regexp, 'given' => $string] + ) + ); + }); + } + + public function uri(): static + { + return $this->postParse(static function (string $string) { + if (filter_var($string, FILTER_VALIDATE_URL)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_URI_CODE, + self::ERROR_URI_TEMPLATE, + ['given' => $string] + ) + ); }); } + /** + * @deprecated use uri + */ public function url(): static { + @trigger_error('Use uri instead', E_USER_DEPRECATED); + return $this->postParse(static function (string $string) { - if (!filter_var($string, FILTER_VALIDATE_URL)) { - throw new ErrorsException( - new Error( - self::ERROR_URL_CODE, - self::ERROR_URL_TEMPLATE, - ['given' => $string] - ) - ); + if (filter_var($string, FILTER_VALIDATE_URL)) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_URL_CODE, + self::ERROR_URL_TEMPLATE, + ['given' => $string] + ) + ); }); } @@ -336,17 +442,17 @@ public function uuid(Uuid $version = Uuid::v4): static $matches = []; preg_match(self::UUID_PATTERN, $string, $matches); - if ((int) ($matches[1] ?? '-1') !== $version->value) { - throw new ErrorsException( - new Error( - self::ERROR_UUID_CODE, - self::ERROR_UUID_TEMPLATE, - ['version' => 'v'.$version->value, 'given' => $string] - ) - ); + if ((int) ($matches[1] ?? '-1') === $version->value) { + return $string; } - return $string; + throw new ErrorsException( + new Error( + self::ERROR_UUID_CODE, + self::ERROR_UUID_TEMPLATE, + ['version' => 'v'.$version->value, 'given' => $string] + ) + ); }); } @@ -435,17 +541,17 @@ public function toBool(): BoolSchema $boolInput = filter_var($input, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE); - if (null === $boolInput) { - throw new ErrorsException( - new Error( - self::ERROR_BOOL_CODE, - self::ERROR_BOOL_TEMPLATE, - ['given' => $input] - ) - ); + if (null !== $boolInput) { + return $boolInput; } - return $boolInput; + throw new ErrorsException( + new Error( + self::ERROR_BOOL_CODE, + self::ERROR_BOOL_TEMPLATE, + ['given' => $input] + ) + ); })->nullable($this->nullable); } @@ -461,17 +567,17 @@ public function toFloat(): FloatSchema $floatInput = filter_var($input, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); - if (null === $floatInput) { - throw new ErrorsException( - new Error( - self::ERROR_FLOAT_CODE, - self::ERROR_FLOAT_TEMPLATE, - ['given' => $input] - ) - ); + if (null !== $floatInput) { + return $floatInput; } - return $floatInput; + throw new ErrorsException( + new Error( + self::ERROR_FLOAT_CODE, + self::ERROR_FLOAT_TEMPLATE, + ['given' => $input] + ) + ); })->nullable($this->nullable); } @@ -487,32 +593,32 @@ public function toInt(): IntSchema $intInput = filter_var($input, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); - if (null === $intInput) { - throw new ErrorsException( - new Error( - self::ERROR_INT_CODE, - self::ERROR_INT_TEMPLATE, - ['given' => $input] - ) - ); + if (null !== $intInput) { + return $intInput; } - return $intInput; + throw new ErrorsException( + new Error( + self::ERROR_INT_CODE, + self::ERROR_INT_TEMPLATE, + ['given' => $input] + ) + ); })->nullable($this->nullable); } protected function innerParse(mixed $input): mixed { - if (!\is_string($input)) { - throw new ErrorsException( - new Error( - self::ERROR_TYPE_CODE, - self::ERROR_TYPE_TEMPLATE, - ['given' => $this->getDataType($input)] - ) - ); + if (\is_string($input)) { + return $input; } - return $input; + throw new ErrorsException( + new Error( + self::ERROR_TYPE_CODE, + self::ERROR_TYPE_TEMPLATE, + ['given' => $this->getDataType($input)] + ) + ); } } diff --git a/tests/Integration/ParserTest.php b/tests/Integration/ParserTest.php index acf3857..4be9b24 100644 --- a/tests/Integration/ParserTest.php +++ b/tests/Integration/ParserTest.php @@ -42,13 +42,13 @@ public function testSuccess(): void "timezone": "+00:00" }, "discriminatedUnion": { - "literal": "type1", + "const": "type1", "string": "test", "default": "defaultOnClass" }, "float": 1.5, "int": 5, - "literal": 1337, + "const": 1337, "object": { "string": "test" }, @@ -70,10 +70,10 @@ public function testSuccess(): void 'backedEnum' => 'D', 'bool' => true, 'dateTime' => new \DateTimeImmutable('2024-01-20T09:15:00+00:00'), - 'discriminatedUnion' => ['literal' => 'type1', 'string' => 'test'], + 'discriminatedUnion' => ['const' => 'type1', 'string' => 'test'], 'float' => 1.5, 'int' => 5, - 'literal' => 1337, + 'const' => 1337, 'object' => ['string' => 'test'], 'record' => ['key1' => 'value1', 'key2' => 'value2'], 'string' => 'test', @@ -163,9 +163,9 @@ public function testFailureWithEmptyArray(): void ], ], [ - 'path' => 'literal', + 'path' => 'const', 'error' => [ - 'code' => 'literal.type', + 'code' => 'const.type', 'template' => 'Type should be "bool|float|int|string", {{given}} given', 'variables' => [ 'given' => 'NULL', @@ -239,7 +239,7 @@ public function testFailureWithEmptyArray(): void protected function getSchema(): SchemaInterface { $discriminatedUnion = new class { - public bool|float|int|string $literal; + public bool|float|int|string $const; public string $string; public string $default = 'defaultOnClass'; }; @@ -252,7 +252,7 @@ protected function getSchema(): SchemaInterface public object $discriminatedUnion; public float $float; public int $int; - public bool|float|int|string $literal; + public bool|float|int|string $const; public object $object; public array $record; public string $string; @@ -268,11 +268,11 @@ protected function getSchema(): SchemaInterface 'bool' => $p->bool(), 'dateTime' => $p->dateTime(), 'discriminatedUnion' => $p->discriminatedUnion([ - $p->object(['literal' => $p->literal('type1'), 'string' => $p->string()], $discriminatedUnion::class), - ], 'literal'), + $p->object(['const' => $p->const('type1'), 'string' => $p->string()], $discriminatedUnion::class), + ], 'const'), 'float' => $p->float(), 'int' => $p->int(), - 'literal' => $p->literal(1337), + 'const' => $p->const(1337), 'object' => $p->object(['string' => $p->string()]), 'record' => $p->record($p->string()), 'string' => $p->string(), diff --git a/tests/Unit/ErrorsTest.php b/tests/Unit/ErrorsTest.php index db6c0bc..05cfc5e 100644 --- a/tests/Unit/ErrorsTest.php +++ b/tests/Unit/ErrorsTest.php @@ -88,7 +88,7 @@ public function testJsonSerialize(): void 4 => [ 'path' => 'sort.name', 'error' => [ - 'code' => 'literal.type', + 'code' => 'const.type', 'template' => 'Type should be "bool|float|int|string", {{given}} given', 'variables' => [ 'given' => 'null', @@ -168,7 +168,7 @@ public function testJsonSerialize(): void 12 => [ 'path' => '_type', 'error' => [ - 'code' => 'literal.type', + 'code' => 'const.type', 'template' => 'Type should be "bool|float|int|string", {{given}} given', 'variables' => [ 'given' => 'null', @@ -358,7 +358,7 @@ private function provideErrors(): Errors ) ->add( (new Errors()) - ->add(new Error('literal.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']), 'name'), + ->add(new Error('const.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']), 'name'), 'sort' ) ->add( @@ -388,7 +388,7 @@ private function provideErrors(): Errors ), 'items' ) - ->add(new Error('literal.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']), '_type') + ->add(new Error('const.type', 'Type should be "bool|float|int|string", {{given}} given', ['given' => 'null']), '_type') ; } } diff --git a/tests/Unit/ParserTest.php b/tests/Unit/ParserTest.php index b78ee5c..b23254c 100644 --- a/tests/Unit/ParserTest.php +++ b/tests/Unit/ParserTest.php @@ -9,6 +9,7 @@ use Chubbyphp\Parsing\Schema\AssocSchema; use Chubbyphp\Parsing\Schema\BackedEnumSchema; use Chubbyphp\Parsing\Schema\BoolSchema; +use Chubbyphp\Parsing\Schema\ConstSchema; use Chubbyphp\Parsing\Schema\DateTimeSchema; use Chubbyphp\Parsing\Schema\DiscriminatedUnionSchema; use Chubbyphp\Parsing\Schema\FloatSchema; @@ -77,6 +78,15 @@ public function testBool(): void self::assertInstanceOf(BoolSchema::class, $BoolSchema); } + public function testConst(): void + { + $p = new Parser(); + + $constSchema = $p->const('person'); + + self::assertInstanceOf(ConstSchema::class, $constSchema); + } + public function testDateTime(): void { $p = new Parser(); @@ -92,7 +102,7 @@ public function testDiscriminatedUnion(): void $discriminatedUnionSchema = $p->discriminatedUnion([ $p->object([ - '_type' => $p->literal('person'), + '_type' => $p->const('person'), ]), ], '_type'); @@ -134,8 +144,18 @@ public function testLiteral(): void { $p = new Parser(); + error_clear_last(); + $literalSchema = $p->literal('person'); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use const instead', $lastError['message']); + self::assertInstanceOf(LiteralSchema::class, $literalSchema); } diff --git a/tests/Unit/Schema/ArraySchemaTest.php b/tests/Unit/Schema/ArraySchemaTest.php index a2a6903..26f3d1e 100644 --- a/tests/Unit/Schema/ArraySchemaTest.php +++ b/tests/Unit/Schema/ArraySchemaTest.php @@ -28,6 +28,10 @@ public function testImmutability(): void self::assertNotSame($schema, $schema->preParse(static fn (mixed $input) => $input)); self::assertNotSame($schema, $schema->postParse(static fn (array $output) => $output)); self::assertNotSame($schema, $schema->catch(static fn (array $output, ErrorsException $e) => $output)); + self::assertNotSame($schema, $schema->exactItems(1)); + self::assertNotSame($schema, $schema->minItems(1)); + self::assertNotSame($schema, $schema->maxItems(1)); + self::assertNotSame($schema, $schema->contains('test')); } public function testParseSuccess(): void @@ -204,8 +208,18 @@ public function testParseWithValidLength(): void { $input = ['test', 'test', 'test', 'test']; + error_clear_last(); + $schema = (new ArraySchema(new StringSchema()))->length(4); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use exactItems instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -236,12 +250,58 @@ public function testParseWithInvalidLength(): void } } + public function testParseWithValidExactItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->exactItems(4); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidExactItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->exactItems(5); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'array.exactItems', + 'template' => 'Items count {{exactItems}}, {{given}} given', + 'variables' => [ + 'exactItems' => 5, + 'given' => \count($input), + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidMinLength(): void { $input = ['test', 'test', 'test', 'test']; + error_clear_last(); + $schema = (new ArraySchema(new StringSchema()))->minLength(4); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use minItems instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -272,12 +332,58 @@ public function testParseWithInvalidMinLength(): void } } + public function testParseWithValidMinItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->minItems(4); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMinItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->minItems(5); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'array.minItems', + 'template' => 'Min items {{minItems}}, {{given}} given', + 'variables' => [ + 'minItems' => 5, + 'given' => \count($input), + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidMaxLength(): void { $input = ['test', 'test', 'test', 'test']; + error_clear_last(); + $schema = (new ArraySchema(new StringSchema()))->maxLength(4); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use maxItems instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -308,6 +414,137 @@ public function testParseWithInvalidMaxLength(): void } } + public function testParseWithValidMaxItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->maxItems(4); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMaxItems(): void + { + $input = ['test', 'test', 'test', 'test']; + + $schema = (new ArraySchema(new StringSchema()))->maxItems(3); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'array.maxItems', + 'template' => 'Max items {{maxItems}}, {{given}} given', + 'variables' => [ + 'maxItems' => 3, + 'given' => \count($input), + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithValidContains(): void + { + $dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00'); + $dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + + $input = [$dateTime1, $dateTime2]; + + $schema = (new ArraySchema(new DateTimeSchema()))->contains($dateTime2); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithValidContainsWithEqualButNotSame(): void + { + $dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00'); + $dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + + $dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + + $input = [$dateTime1, $dateTime2]; + + $schema = (new ArraySchema(new DateTimeSchema()))->contains($dateTime2Equal, false); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidContainsWithEqualButNotSame(): void + { + $dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00'); + $dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + + $dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + + $input = [$dateTime1, $dateTime2]; + + $schema = (new ArraySchema(new DateTimeSchema()))->contains($dateTime2Equal); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'array.contains', + 'template' => '{{given}} does not contain {{contains}}', + 'variables' => [ + 'contains' => json_decode(json_encode($dateTime2Equal), true), + 'given' => json_decode(json_encode($input), true), + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + + public function testParseWithInvalidContains(): void + { + $dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00'); + $dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00'); + $dateTime3 = new \DateTimeImmutable('2024-01-22T09:15:00+00:00'); + + $input = [$dateTime1, $dateTime2]; + + $schema = (new ArraySchema(new DateTimeSchema()))->contains($dateTime3); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'array.contains', + 'template' => '{{given}} does not contain {{contains}}', + 'variables' => [ + 'contains' => json_decode(json_encode($dateTime3), true), + 'given' => json_decode(json_encode($input), true), + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + public function testParseWithValidIncludes(): void { $dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00'); @@ -315,8 +552,18 @@ public function testParseWithValidIncludes(): void $input = [$dateTime1, $dateTime2]; + error_clear_last(); + $schema = (new ArraySchema(new DateTimeSchema()))->includes($dateTime2); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use contains instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/BoolSchemaTest.php b/tests/Unit/Schema/BoolSchemaTest.php index 3d001df..e697107 100644 --- a/tests/Unit/Schema/BoolSchemaTest.php +++ b/tests/Unit/Schema/BoolSchemaTest.php @@ -153,7 +153,7 @@ public function testParseWithToFloat(): void { $input = true; - $schema = (new BoolSchema())->toFloat()->gte(1.0); + $schema = (new BoolSchema())->toFloat()->minimum(1.0); self::assertSame((float) $input, $schema->parse($input)); } @@ -169,7 +169,7 @@ public function testParseWithToInt(): void { $input = true; - $schema = (new BoolSchema())->toInt()->gte(1); + $schema = (new BoolSchema())->toInt()->minimum(1); self::assertSame((int) $input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/ConstSchemaTest.php b/tests/Unit/Schema/ConstSchemaTest.php new file mode 100644 index 0000000..1b4e695 --- /dev/null +++ b/tests/Unit/Schema/ConstSchemaTest.php @@ -0,0 +1,275 @@ +nullable()); + self::assertNotSame($schema, $schema->nullable(false)); + self::assertNotSame($schema, $schema->default('test')); + self::assertNotSame($schema, $schema->preParse(static fn (mixed $input) => $input)); + self::assertNotSame($schema, $schema->postParse(static fn (string $output) => $output)); + self::assertNotSame($schema, $schema->catch(static fn (string $output, ErrorsException $e) => $output)); + } + + public function testParseSuccessWithBool(): void + { + $input = true; + + $schema = new ConstSchema($input); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseSuccessWithInt(): void + { + $input = 1; + + $schema = new ConstSchema($input); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseSuccessWithFloat(): void + { + $input = 1.5; + + $schema = new ConstSchema($input); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseSuccessWithString(): void + { + $input = 'test'; + + $schema = new ConstSchema($input); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseSuccessWithDefault(): void + { + $input = 'test'; + + $schema = (new ConstSchema($input))->default($input); + + self::assertSame($input, $schema->parse(null)); + } + + public function testParseSuccessWithNullAndNullable(): void + { + $schema = (new ConstSchema('test'))->nullable(); + + self::assertNull($schema->parse(null)); + } + + public function testParseFailedWithNull(): void + { + $schema = new ConstSchema('test'); + + try { + $schema->parse(null); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.type', + 'template' => 'Type should be "bool|float|int|string", {{given}} given', + 'variables' => [ + 'given' => 'NULL', + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseFailedWithDifferentStringConstValue(): void + { + $schema = new ConstSchema('test1'); + + try { + $schema->parse('test2'); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.equals', + 'template' => 'Input should be {{expected}}, {{given}} given', + 'variables' => [ + 'expected' => 'test1', + 'given' => 'test2', + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseFailedWithDifferentFloatConstValue(): void + { + $schema = new ConstSchema(4.2); + + try { + $schema->parse(4.1); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.equals', + 'template' => 'Input should be {{expected}}, {{given}} given', + 'variables' => [ + 'expected' => 4.2, + 'given' => 4.1, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseFailedWithDifferentIntConstValue(): void + { + $schema = new ConstSchema(1337); + + try { + $schema->parse(1336); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.equals', + 'template' => 'Input should be {{expected}}, {{given}} given', + 'variables' => [ + 'expected' => 1337, + 'given' => 1336, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseFailedWithDifferentBoolConstValue(): void + { + $schema = new ConstSchema(true); + + try { + $schema->parse(false); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.equals', + 'template' => 'Input should be {{expected}}, {{given}} given', + 'variables' => [ + 'expected' => true, + 'given' => false, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseSuccessWithPreParse(): void + { + $input = 'test1'; + + $schema = (new ConstSchema($input))->preParse(static fn () => $input); + + self::assertSame($input, $schema->parse(null)); + } + + public function testParseSuccessWithPostParse(): void + { + $input = 'test1'; + + $schema = (new ConstSchema($input))->postParse(static fn (string $output) => $output.'1'); + + self::assertSame('test11', $schema->parse($input)); + } + + public function testParseFailedWithCatch(): void + { + $schema = (new ConstSchema('test')) + ->catch(static function (mixed $input, ErrorsException $errorsException) { + self::assertNull($input); + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.type', + 'template' => 'Type should be "bool|float|int|string", {{given}} given', + 'variables' => [ + 'given' => 'NULL', + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + + return 'catched'; + }) + ; + + self::assertSame('catched', $schema->parse(null)); + } + + public function testSafeParseSuccess(): void + { + $input = 'test'; + + $schema = new ConstSchema($input); + + self::assertSame($input, $schema->safeParse($input)->data); + } + + public function testSafeParseFailed(): void + { + $schema = new ConstSchema('test'); + + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'const.type', + 'template' => 'Type should be "bool|float|int|string", {{given}} given', + 'variables' => [ + 'given' => 'NULL', + ], + ], + ], + ], $schema->safeParse(null)->exception->errors->jsonSerialize()); + } +} diff --git a/tests/Unit/Schema/DiscriminatedUnionSchemaTest.php b/tests/Unit/Schema/DiscriminatedUnionSchemaTest.php index 7849da0..19d26b9 100644 --- a/tests/Unit/Schema/DiscriminatedUnionSchemaTest.php +++ b/tests/Unit/Schema/DiscriminatedUnionSchemaTest.php @@ -6,9 +6,9 @@ use Chubbyphp\Parsing\ErrorsException; use Chubbyphp\Parsing\Schema\ArraySchema; +use Chubbyphp\Parsing\Schema\ConstSchema; use Chubbyphp\Parsing\Schema\DiscriminatedUnionSchema; use Chubbyphp\Parsing\Schema\IntSchema; -use Chubbyphp\Parsing\Schema\LiteralSchema; use Chubbyphp\Parsing\Schema\ObjectSchema; use Chubbyphp\Parsing\Schema\StringSchema; use PHPUnit\Framework\TestCase; @@ -37,8 +37,8 @@ final class DiscriminatedUnionSchemaTest extends TestCase public function testImmutability(): void { $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); self::assertNotSame($schema, $schema->nullable()); @@ -82,8 +82,8 @@ public function testParseSuccess(): void $input = ['field1' => 'type2', 'field2' => 'test']; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); $output = $schema->parse($input); @@ -100,8 +100,8 @@ public function testParseSuccessWithStdClassInput(): void $input->field2 = 'test'; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); $output = $schema->parse($input); @@ -116,8 +116,8 @@ public function testParseSuccessWithIteratorInput(): void $input = new \ArrayIterator(['field1' => 'type2', 'field2' => 'test']); $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); $output = $schema->parse($input); @@ -134,8 +134,8 @@ public function testParseSuccessWithJsonSerialzableObject(): void $input->field2 = 'test'; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); $output = $schema->parse($input); @@ -167,8 +167,8 @@ public function testParseSuccessWithDefault(): void $input2 = ['field1' => 'type2', 'field2' => 'test2']; $schema = (new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'))->default($input1); self::assertSame($input1, (array) $schema->parse(null)); @@ -178,8 +178,8 @@ public function testParseSuccessWithDefault(): void public function testParseSuccessWithNullAndNullable(): void { $schema = (new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'))->nullable(); self::assertNull($schema->parse(null)); @@ -188,8 +188,8 @@ public function testParseSuccessWithNullAndNullable(): void public function testParseFailedWithNull(): void { $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); try { @@ -217,8 +217,8 @@ public function testParseFailedWithMissingDiscriminatorField(): void $input = ['field2' => 'test']; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); try { @@ -246,8 +246,8 @@ public function testParseFailedWithNoMatchingDiscriminator(): void $input = ['field1' => 'type3', 'field2' => 'test']; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); try { @@ -259,7 +259,7 @@ public function testParseFailedWithNoMatchingDiscriminator(): void [ 'path' => '', 'error' => [ - 'code' => 'literal.equals', + 'code' => 'const.equals', 'template' => 'Input should be {{expected}}, {{given}} given', 'variables' => [ 'expected' => 'type1', @@ -270,7 +270,7 @@ public function testParseFailedWithNoMatchingDiscriminator(): void [ 'path' => '', 'error' => [ - 'code' => 'literal.equals', + 'code' => 'const.equals', 'template' => 'Input should be {{expected}}, {{given}} given', 'variables' => [ 'expected' => 'type2', @@ -287,8 +287,8 @@ public function testParseSuccessWithPreParse(): void $input = ['field1' => 'type2', 'field2' => 'test']; $schema = (new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'))->preParse(static fn () => $input); self::assertSame($input, (array) $schema->parse(null)); @@ -299,8 +299,8 @@ public function testParseSuccessWithPostParse(): void $input = ['field1' => 'type2', 'field2' => 'test']; $schema = (new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'))->postParse(static function (\stdClass $output) { $output->field3 = 'test'; @@ -313,8 +313,8 @@ public function testParseSuccessWithPostParse(): void public function testParseFailedWithCatch(): void { $schema = (new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1')) ->catch(static function (mixed $input, ErrorsException $errorsException) { self::assertNull($input); @@ -343,8 +343,8 @@ public function testSafeParseSuccess(): void $input = ['field1' => 'type2', 'field2' => 'test']; $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); self::assertSame($input, (array) $schema->safeParse($input)->data); @@ -353,8 +353,8 @@ public function testSafeParseSuccess(): void public function testSafeParseFailed(): void { $schema = new DiscriminatedUnionSchema([ - new ObjectSchema(['field1' => new LiteralSchema('type1')]), - new ObjectSchema(['field1' => new LiteralSchema('type2'), 'field2' => new StringSchema()]), + new ObjectSchema(['field1' => new ConstSchema('type1')]), + new ObjectSchema(['field1' => new ConstSchema('type2'), 'field2' => new StringSchema()]), ], 'field1'); self::assertSame([ diff --git a/tests/Unit/Schema/FloatSchemaTest.php b/tests/Unit/Schema/FloatSchemaTest.php index c4888d7..4f8936d 100644 --- a/tests/Unit/Schema/FloatSchemaTest.php +++ b/tests/Unit/Schema/FloatSchemaTest.php @@ -149,22 +149,60 @@ public function testSafeParseFailed(): void ], $schema->safeParse(null)->exception->errors->jsonSerialize()); } - public function testParseWithValidGt(): void + public function testParseWithValidMinimum(): void + { + $input = 4.1; + $minimum = 4.1; + + $schema = (new FloatSchema())->minimum($minimum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMinimum(): void + { + $input = 4.1; + $minimum = 4.2; + + $schema = (new FloatSchema())->minimum($minimum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.minimum', + 'template' => 'Value should be greater than or equal {{minimum}}, {{given}} given', + 'variables' => [ + 'minimum' => $minimum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithValidExclusiveMinimum(): void { $input = 4.2; - $gt = 4.1; + $exclusiveMinimum = 4.1; - $schema = (new FloatSchema())->gt($gt); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); self::assertSame($input, $schema->parse($input)); } - public function testParseWithInvalidGtEqual(): void + public function testParseWithInvalidExclusiveMinimumEqual(): void { $input = 4.1; - $gt = 4.1; + $exclusiveMinimum = 4.1; - $schema = (new FloatSchema())->gt($gt); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -176,10 +214,10 @@ public function testParseWithInvalidGtEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'float.gt', - 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'code' => 'float.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'gt' => $gt, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -190,12 +228,12 @@ public function testParseWithInvalidGtEqual(): void } } - public function testParseWithInvalidGtLesser(): void + public function testParseWithInvalidExclusiveMinimumLesser(): void { $input = 4.1; - $gt = 4.2; + $exclusiveMinimum = 4.2; - $schema = (new FloatSchema())->gt($gt); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -207,10 +245,10 @@ public function testParseWithInvalidGtLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'float.gt', - 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'code' => 'float.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'gt' => $gt, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -221,13 +259,127 @@ public function testParseWithInvalidGtLesser(): void } } + public function testParseWithValidExclusiveMaximum(): void + { + $input = 4.1; + $exclusiveMaximum = 4.2; + + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidExclusiveMaximumEqual(): void + { + $input = 4.1; + $exclusiveMaximum = 4.1; + + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'variables' => [ + 'exclusiveMaximum' => $exclusiveMaximum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithInvalidExclusiveMaximumLesser(): void + { + $input = 4.2; + $exclusiveMaximum = 4.1; + + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'variables' => [ + 'exclusiveMaximum' => $exclusiveMaximum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithValidMaximum(): void + { + $input = 4.1; + $maximum = 4.1; + + $schema = (new FloatSchema())->maximum($maximum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMaximum(): void + { + $input = 4.2; + $maximum = 4.1; + + $schema = (new FloatSchema())->maximum($maximum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.maximum', + 'template' => 'Value should be lesser than or equal {{maximum}}, {{given}} given', + 'variables' => [ + 'maximum' => $maximum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidGte(): void { $input = 4.1; $gte = 4.1; + error_clear_last(); + $schema = (new FloatSchema())->gte($gte); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use minimum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -259,13 +411,105 @@ public function testParseWithInvalidGte(): void } } + public function testParseWithValidGt(): void + { + $input = 4.2; + $gt = 4.1; + + error_clear_last(); + + $schema = (new FloatSchema())->gt($gt); + + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use exclusiveMinimum instead', $lastError['message']); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidGtEqual(): void + { + $input = 4.1; + $gt = 4.1; + + $schema = (new FloatSchema())->gt($gt); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.gt', + 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'variables' => [ + 'gt' => $gt, + 'given' => $input, + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + + public function testParseWithInvalidGtLesser(): void + { + $input = 4.1; + $gt = 4.2; + + $schema = (new FloatSchema())->gt($gt); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'float.gt', + 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'variables' => [ + 'gt' => $gt, + 'given' => $input, + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + public function testParseWithValidLt(): void { $input = 4.1; $lt = 4.2; + error_clear_last(); + $schema = (new FloatSchema())->lt($lt); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use exclusiveMaximum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -330,8 +574,18 @@ public function testParseWithValidLte(): void $input = 4.1; $lte = 4.1; + error_clear_last(); + $schema = (new FloatSchema())->lte($lte); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use maximum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/IntSchemaTest.php b/tests/Unit/Schema/IntSchemaTest.php index 594b998..dd57acd 100644 --- a/tests/Unit/Schema/IntSchemaTest.php +++ b/tests/Unit/Schema/IntSchemaTest.php @@ -148,22 +148,132 @@ public function testSafeParseFailed(): void ], $schema->safeParse(null)->exception->errors->jsonSerialize()); } - public function testParseWithValidGt(): void + public function testParseWithValidMinimum(): void + { + $input = 4; + $minimum = 4; + + $schema = (new IntSchema())->minimum($minimum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMinimum(): void + { + $input = 4; + $minimum = 5; + + $schema = (new IntSchema())->minimum($minimum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.minimum', + 'template' => 'Value should be greater than or equal {{minimum}}, {{given}} given', + 'variables' => [ + 'minimum' => $minimum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithValidExclusiveMinimum(): void { $input = 5; - $gt = 4; + $exclusiveMinimum = 4; - $schema = (new IntSchema())->gt($gt); + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); self::assertSame($input, $schema->parse($input)); } - public function testParseWithInvalidGtEqual(): void + public function testParseWithInvalidExclusiveMinimumEqual(): void + { + $input = 4; + $exclusiveMinimum = 4; + + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'variables' => [ + 'exclusiveMinimum' => $exclusiveMinimum, + 'given' => $input, + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + + public function testParseWithInvalidExclusiveMinimumLesser(): void + { + $input = 4; + $exclusiveMinimum = 5; + + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame( + [ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'variables' => [ + 'exclusiveMinimum' => $exclusiveMinimum, + 'given' => $input, + ], + ], + ], + ], + $errorsException->errors->jsonSerialize() + ); + } + } + + public function testParseWithValidExclusiveMaximum(): void + { + $input = 4; + $exclusiveMaximum = 5; + + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidExclusiveMaximumEqual(): void { $input = 5; - $gt = 5; + $exclusiveMaximum = 5; - $schema = (new IntSchema())->gt($gt); + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -174,10 +284,10 @@ public function testParseWithInvalidGtEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'int.gt', - 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'code' => 'int.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'gt' => $gt, + 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $input, ], ], @@ -186,12 +296,12 @@ public function testParseWithInvalidGtEqual(): void } } - public function testParseWithInvalidGtLesser(): void + public function testParseWithInvalidExclusiveMaximumLesser(): void { - $input = 4; - $gt = 5; + $input = 5; + $exclusiveMaximum = 4; - $schema = (new IntSchema())->gt($gt); + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -202,10 +312,48 @@ public function testParseWithInvalidGtLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'int.gt', - 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'code' => 'int.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'gt' => $gt, + 'exclusiveMaximum' => $exclusiveMaximum, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithValidMaximum(): void + { + $input = 5; + $maximum = 5; + + $schema = (new IntSchema())->maximum($maximum); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidMaximum(): void + { + $input = 5; + $maximum = 4; + + $schema = (new IntSchema())->maximum($maximum); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.maximum', + 'template' => 'Value should be lesser than or equal {{maximum}}, {{given}} given', + 'variables' => [ + 'maximum' => $maximum, 'given' => $input, ], ], @@ -219,8 +367,18 @@ public function testParseWithValidGte(): void $input = 5; $gte = 5; + error_clear_last(); + $schema = (new IntSchema())->gte($gte); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use minimum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -252,13 +410,99 @@ public function testParseWithInvalidGte(): void } } + public function testParseWithValidGt(): void + { + $input = 5; + $gt = 4; + + error_clear_last(); + + $schema = (new IntSchema())->gt($gt); + + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use exclusiveMinimum instead', $lastError['message']); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidGtEqual(): void + { + $input = 5; + $gt = 5; + + $schema = (new IntSchema())->gt($gt); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.gt', + 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'variables' => [ + 'gt' => $gt, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + + public function testParseWithInvalidGtLesser(): void + { + $input = 4; + $gt = 5; + + $schema = (new IntSchema())->gt($gt); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'int.gt', + 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'variables' => [ + 'gt' => $gt, + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidLt(): void { $input = 4; $lt = 5; + error_clear_last(); + $schema = (new IntSchema())->lt($lt); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use exclusiveMaximum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -323,8 +567,18 @@ public function testParseWithValidLte(): void $input = 5; $lte = 5; + error_clear_last(); + $schema = (new IntSchema())->lte($lte); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use maximum instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/StringSchemaTest.php b/tests/Unit/Schema/StringSchemaTest.php index aac44a2..03804de 100644 --- a/tests/Unit/Schema/StringSchemaTest.php +++ b/tests/Unit/Schema/StringSchemaTest.php @@ -258,12 +258,58 @@ public function testParseWithInvalidMaxLength(): void } } + public function testParseWithValidContains(): void + { + $input = 'example'; + + $schema = (new StringSchema())->contains('amp'); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidContains(): void + { + $input = 'example'; + + $schema = (new StringSchema())->contains('lee'); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'string.contains', + 'template' => '{{given}} does not contain {{contains}}', + 'variables' => [ + 'contains' => 'lee', + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidIncludes(): void { $input = 'example'; + error_clear_last(); + $schema = (new StringSchema())->includes('amp'); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use contains instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -366,12 +412,57 @@ public function testParseWithInvalidEndsWith(): void } } + public function testParseWithValidHostname(): void + { + $input = 'example.com'; + + $schema = (new StringSchema())->hostname(); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidHostname(): void + { + $input = 'example..com'; + + $schema = (new StringSchema())->hostname(); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'string.hostname', + 'template' => 'Invalid hostname {{given}}', + 'variables' => [ + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidDomain(): void { $input = 'example.com'; + error_clear_last(); + $schema = (new StringSchema())->domain(); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use hostname instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -556,13 +647,11 @@ public function testParseWithMatchWithInvalidPattern(): void public function testParseWithValidMatch(): void { - error_clear_last(); - $input = 'aBcDeFg'; - $schema = (new StringSchema())->match('/^[a-z]+$/i'); + error_clear_last(); - self::assertSame($input, $schema->parse($input)); + $schema = (new StringSchema())->match('/^[a-z]+$/i'); $lastError = error_get_last(); @@ -570,7 +659,9 @@ public function testParseWithValidMatch(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use regexp instead', $lastError['message']); + self::assertSame('Use pattern instead', $lastError['message']); + + self::assertSame($input, $schema->parse($input)); } public function testParseWithInvalidMatch(): void @@ -600,6 +691,53 @@ public function testParseWithInvalidMatch(): void } } + public function testParseWithPatternWithInvalidPattern(): void + { + try { + (new StringSchema())->pattern('test'); + + throw new \Exception('code should not be reached'); + } catch (\InvalidArgumentException $e) { + self::assertSame('Invalid pattern "test" given', $e->getMessage()); + } + } + + public function testParseWithValidPattern(): void + { + $input = 'aBcDeFg'; + + $schema = (new StringSchema())->pattern('/^[a-z]+$/i'); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidPattern(): void + { + $input = 'a1B2C3d4'; + + $schema = (new StringSchema())->pattern('/^[a-z]+$/i'); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'string.pattern', + 'template' => '{{given}} does not pattern {{pattern}}', + 'variables' => [ + 'pattern' => '/^[a-z]+$/i', + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithRegexpWithInvalidPattern(): void { try { @@ -615,8 +753,18 @@ public function testParseWithValidRegexp(): void { $input = 'aBcDeFg'; + error_clear_last(); + $schema = (new StringSchema())->regexp('/^[a-z]+$/i'); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use pattern instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -647,12 +795,57 @@ public function testParseWithInvalidRegexp(): void } } + public function testParseWithValidUri(): void + { + $input = 'https://localhost'; + + $schema = (new StringSchema())->uri(); + + self::assertSame($input, $schema->parse($input)); + } + + public function testParseWithInvalidUri(): void + { + $input = 'test'; + + $schema = (new StringSchema())->uri(); + + try { + $schema->parse($input); + + throw new \Exception('code should not be reached'); + } catch (ErrorsException $errorsException) { + self::assertSame([ + [ + 'path' => '', + 'error' => [ + 'code' => 'string.uri', + 'template' => 'Invalid uri {{given}}', + 'variables' => [ + 'given' => $input, + ], + ], + ], + ], $errorsException->errors->jsonSerialize()); + } + } + public function testParseWithValidUrl(): void { $input = 'https://localhost'; + error_clear_last(); + $schema = (new StringSchema())->url(); + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use uri instead', $lastError['message']); + self::assertSame($input, $schema->parse($input)); } @@ -1064,7 +1257,7 @@ public function testParseWithValidtoFloat(): void { $input = '4.2'; - $schema = (new StringSchema())->toFloat()->gt(4.0); + $schema = (new StringSchema())->toFloat()->exclusiveMinimum(4.0); self::assertSame((float) $input, $schema->parse($input)); } @@ -1106,7 +1299,7 @@ public function testParseWithValidtoInt(): void { $input = '42'; - $schema = (new StringSchema())->toInt()->gt(40); + $schema = (new StringSchema())->toInt()->exclusiveMinimum(40); self::assertSame((int) $input, $schema->parse($input)); } From cae3a17ba4ac7b87972b90e8eb976dcbe1382c04 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 21:33:07 +0100 Subject: [PATCH 2/9] exclusiveMinimum and exclusiveMaximums are boolean now --- doc/ErrorHandling.md | 2 +- doc/Schema/FloatSchema.md | 10 ++-- doc/Schema/IntSchema.md | 10 ++-- src/Schema/FloatSchema.php | 76 ++++++-------------------- src/Schema/IntSchema.php | 76 ++++++-------------------- tests/Unit/Schema/FloatSchemaTest.php | 66 ++++++++++++---------- tests/Unit/Schema/IntSchemaTest.php | 66 ++++++++++++---------- tests/Unit/Schema/StringSchemaTest.php | 4 +- 8 files changed, 119 insertions(+), 191 deletions(-) diff --git a/doc/ErrorHandling.md b/doc/ErrorHandling.md index c1c0e0c..3011a58 100644 --- a/doc/ErrorHandling.md +++ b/doc/ErrorHandling.md @@ -267,7 +267,7 @@ Each schema type uses a consistent error code prefix: | Schema | Prefix | Example Codes | |--------|--------|---------------| | string | `string.` | `string.type`, `string.minLength`, `string.email` | -| int | `int.` | `int.type`, `int.exclusiveMinimum`, `int.positive` | +| int | `int.` | `int.type`, `int.minimum`, `int.positive` | | float | `float.` | `float.type`, `float.minimum`, `float.negative` | | bool | `bool.` | `bool.type` | | const | `const.` | `const.type` | diff --git a/doc/Schema/FloatSchema.md b/doc/Schema/FloatSchema.md index 9358ac3..6110881 100644 --- a/doc/Schema/FloatSchema.md +++ b/doc/Schema/FloatSchema.md @@ -20,8 +20,8 @@ $data = $schema->parse(4.2); // Returns: 4.2 ```php $schema->minimum(5.0); // Greater than or equal to 5.0 -$schema->exclusiveMinimum(5.0); // Greater than 5.0 -$schema->exclusiveMaximum(10.0); // Less than 10.0 +$schema->minimum(5.0, true); // Greater than 5.0 +$schema->maximum(10.0, true); // Less than 10.0 $schema->maximum(10.0); // Less than or equal to 10.0 ``` @@ -82,8 +82,6 @@ $coordinatesSchema->parse(['lat' => 47.1, 'lng' => 8.2]); | Code | Description | |------|-------------| | `float.type` | Value is not a float | -| `float.minimum` | Value is not greater than or equal to threshold (used by `minimum` and `nonNegative()`) | -| `float.exclusiveMinimum` | Value is not greater than threshold (used by `exclusiveMinimum()` and `positive()`) | -| `float.exclusiveMaximum` | Value is not less than threshold (used by `exclusiveMaximum()` and `negative()`) | -| `float.maximum` | Value is not less than or equal to threshold (used by `maximum()` and `nonPositive()`) | +| `float.minimum` | Value is not greater than or equal to threshold | +| `float.maximum` | Value is not less than or equal to threshold | | `float.int` | Cannot convert float to int without precision loss (for `toInt()`) | diff --git a/doc/Schema/IntSchema.md b/doc/Schema/IntSchema.md index f2b3c26..30046b6 100644 --- a/doc/Schema/IntSchema.md +++ b/doc/Schema/IntSchema.md @@ -20,8 +20,8 @@ $data = $schema->parse(1337); // Returns: 1337 ```php $schema->minimum(5); // Greater than or equal to 5 -$schema->exclusiveMinimum(5); // Greater than 5 -$schema->exclusiveMaximum(10); // Less than 10 +$schema->minimum(5, true); // Greater than 5 +$schema->maximum(10, true); // Less than 10 $schema->maximum(10); // Less than or equal to 10 ``` @@ -90,7 +90,5 @@ $date = $timestampSchema->parse(1705744500); | Code | Description | |------|-------------| | `int.type` | Value is not an integer | -| `int.minimum` | Value is not greater than or equal to threshold (used by `minimum` and `nonNegative()`) | -| `int.exclusiveMinimum` | Value is not greater than threshold (used by `exclusiveMinimum()` and `positive()`) | -| `int.exclusiveMaximum` | Value is not less than threshold (used by `exclusiveMaximum()` and `negative()`) | -| `int.maximum` | Value is not less than or equal to threshold (used by `maximum()` and `nonPositive()`) | +| `int.minimum` | Value is not greater than or equal to threshold | +| `int.maximum` | Value is not less than or equal to threshold | \ No newline at end of file diff --git a/src/Schema/FloatSchema.php b/src/Schema/FloatSchema.php index 3041290..67ed1cd 100644 --- a/src/Schema/FloatSchema.php +++ b/src/Schema/FloatSchema.php @@ -13,16 +13,10 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_TYPE_TEMPLATE = 'Type should be "float", {{given}} given'; public const string ERROR_MINIMUM_CODE = 'float.minimum'; - public const string ERROR_MINIMUM_TEMPLATE = 'Value should be greater than or equal {{minimum}}, {{given}} given'; - - public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'float.exclusiveMinimum'; - public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; - - public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'float.exclusiveMaximum'; - public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given'; public const string ERROR_MAXIMUM_CODE = 'float.maximum'; - public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be lesser than or equal {{maximum}}, {{given}} given'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; public const string ERROR_GTE_CODE = 'float.gte'; public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; @@ -39,10 +33,10 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_INT_CODE = 'float.int'; public const string ERROR_INT_TEMPLATE = 'Cannot convert {{given}} to int'; - public function minimum(float $minimum): static + public function minimum(float $minimum, bool $exclusiveMinimum = false): static { - return $this->postParse(static function (float $float) use ($minimum) { - if ($float >= $minimum) { + return $this->postParse(static function (float $float) use ($minimum, $exclusiveMinimum) { + if ((!$exclusiveMinimum && $float >= $minimum) || ($exclusiveMinimum && $float > $minimum)) { return $float; } @@ -50,50 +44,16 @@ public function minimum(float $minimum): static new Error( self::ERROR_MINIMUM_CODE, self::ERROR_MINIMUM_TEMPLATE, - ['minimum' => $minimum, 'given' => $float] - ) - ); - }); - } - - public function exclusiveMinimum(float $exclusiveMinimum): static - { - return $this->postParse(static function (float $float) use ($exclusiveMinimum) { - if ($float > $exclusiveMinimum) { - return $float; - } - - throw new ErrorsException( - new Error( - self::ERROR_EXCLUSIVE_MINIMUM_CODE, - self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, - ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $float] - ) - ); - }); - } - - public function exclusiveMaximum(float $exclusiveMaximum): static - { - return $this->postParse(static function (float $float) use ($exclusiveMaximum) { - if ($float < $exclusiveMaximum) { - return $float; - } - - throw new ErrorsException( - new Error( - self::ERROR_EXCLUSIVE_MAXIMUM_CODE, - self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, - ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $float] + ['minimum' => $minimum, 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $float] ) ); }); } - public function maximum(float $maximum): static + public function maximum(float $maximum, bool $exclusiveMaximum = false): static { - return $this->postParse(static function (float $float) use ($maximum) { - if ($float <= $maximum) { + return $this->postParse(static function (float $float) use ($maximum, $exclusiveMaximum) { + if ((!$exclusiveMaximum && $float <= $maximum) || ($exclusiveMaximum && $float < $maximum)) { return $float; } @@ -101,18 +61,18 @@ public function maximum(float $maximum): static new Error( self::ERROR_MAXIMUM_CODE, self::ERROR_MAXIMUM_TEMPLATE, - ['maximum' => $maximum, 'given' => $float] + ['maximum' => $maximum, 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $float] ) ); }); } /** - * @deprecated use minimum + * @deprecated Use minimum($gte) instead */ public function gte(float $gte): static { - @trigger_error('Use minimum instead', E_USER_DEPRECATED); + @trigger_error('Use minimum($gte) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($gte) { if ($float >= $gte) { @@ -130,11 +90,11 @@ public function gte(float $gte): static } /** - * @deprecated use exclusiveMinimum + * @deprecated Use minimum($gt, true) instead */ public function gt(float $gt): static { - @trigger_error('Use exclusiveMinimum instead', E_USER_DEPRECATED); + @trigger_error('Use minimum($gt, true) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($gt) { if ($float > $gt) { @@ -152,11 +112,11 @@ public function gt(float $gt): static } /** - * @deprecated use exclusiveMaximum + * @deprecated Use maximum($lt, true) instead */ public function lt(float $lt): static { - @trigger_error('Use exclusiveMaximum instead', E_USER_DEPRECATED); + @trigger_error('Use maximum($lt, true) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($lt) { if ($float < $lt) { @@ -174,11 +134,11 @@ public function lt(float $lt): static } /** - * @deprecated use maximum + * @deprecated Use maximum($lte) instead */ public function lte(float $lte): static { - @trigger_error('Use maximum instead', E_USER_DEPRECATED); + @trigger_error('Use maximum($lte) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($lte) { if ($float <= $lte) { diff --git a/src/Schema/IntSchema.php b/src/Schema/IntSchema.php index a57d935..cc40824 100644 --- a/src/Schema/IntSchema.php +++ b/src/Schema/IntSchema.php @@ -13,16 +13,10 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac public const string ERROR_TYPE_TEMPLATE = 'Type should be "int", {{given}} given'; public const string ERROR_MINIMUM_CODE = 'int.minimum'; - public const string ERROR_MINIMUM_TEMPLATE = 'Value should be greater than or equal {{minimum}}, {{given}} given'; - - public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'int.exclusiveMinimum'; - public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; - - public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'int.exclusiveMaximum'; - public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given'; public const string ERROR_MAXIMUM_CODE = 'int.maximum'; - public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be lesser than or equal {{maximum}}, {{given}} given'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; public const string ERROR_GTE_CODE = 'int.gte'; public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; @@ -36,10 +30,10 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac public const string ERROR_LTE_CODE = 'int.lte'; public const string ERROR_LTE_TEMPLATE = 'Value should be lesser than or equal {{lte}}, {{given}} given'; - public function minimum(int $minimum): static + public function minimum(int $minimum, bool $exclusiveMinimum = false): static { - return $this->postParse(static function (int $int) use ($minimum) { - if ($int >= $minimum) { + return $this->postParse(static function (int $int) use ($minimum, $exclusiveMinimum) { + if ((!$exclusiveMinimum && $int >= $minimum) || ($exclusiveMinimum && $int > $minimum)) { return $int; } @@ -47,50 +41,16 @@ public function minimum(int $minimum): static new Error( self::ERROR_MINIMUM_CODE, self::ERROR_MINIMUM_TEMPLATE, - ['minimum' => $minimum, 'given' => $int] - ) - ); - }); - } - - public function exclusiveMinimum(int $exclusiveMinimum): static - { - return $this->postParse(static function (int $int) use ($exclusiveMinimum) { - if ($int > $exclusiveMinimum) { - return $int; - } - - throw new ErrorsException( - new Error( - self::ERROR_EXCLUSIVE_MINIMUM_CODE, - self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, - ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $int] - ) - ); - }); - } - - public function exclusiveMaximum(int $exclusiveMaximum): static - { - return $this->postParse(static function (int $int) use ($exclusiveMaximum) { - if ($int < $exclusiveMaximum) { - return $int; - } - - throw new ErrorsException( - new Error( - self::ERROR_EXCLUSIVE_MAXIMUM_CODE, - self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, - ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $int] + ['minimum' => $minimum, 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $int] ) ); }); } - public function maximum(int $maximum): static + public function maximum(int $maximum, bool $exclusiveMaximum = false): static { - return $this->postParse(static function (int $int) use ($maximum) { - if ($int <= $maximum) { + return $this->postParse(static function (int $int) use ($maximum, $exclusiveMaximum) { + if ((!$exclusiveMaximum && $int <= $maximum) || ($exclusiveMaximum && $int < $maximum)) { return $int; } @@ -98,18 +58,18 @@ public function maximum(int $maximum): static new Error( self::ERROR_MAXIMUM_CODE, self::ERROR_MAXIMUM_TEMPLATE, - ['maximum' => $maximum, 'given' => $int] + ['maximum' => $maximum, 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $int] ) ); }); } /** - * @deprecated use minimum + * @deprecated Use minimum($gte) instead */ public function gte(int $gte): static { - @trigger_error('Use minimum instead', E_USER_DEPRECATED); + @trigger_error('Use minimum($gte) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($gte) { if ($int >= $gte) { @@ -127,11 +87,11 @@ public function gte(int $gte): static } /** - * @deprecated use exclusiveMinimum + * @deprecated Use minimum($gt, true) instead */ public function gt(int $gt): static { - @trigger_error('Use exclusiveMinimum instead', E_USER_DEPRECATED); + @trigger_error('Use minimum($gt, true) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($gt) { if ($int > $gt) { @@ -149,11 +109,11 @@ public function gt(int $gt): static } /** - * @deprecated use exclusiveMaximum + * @deprecated Use maximum($lt, true) instead */ public function lt(int $lt): static { - @trigger_error('Use exclusiveMaximum instead', E_USER_DEPRECATED); + @trigger_error('Use maximum($lt, true) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($lt) { if ($int < $lt) { @@ -171,11 +131,11 @@ public function lt(int $lt): static } /** - * @deprecated use maximum + * @deprecated Use maximum($lte) instead */ public function lte(int $lte): static { - @trigger_error('Use maximum instead', E_USER_DEPRECATED); + @trigger_error('Use maximum($lte) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($lte) { if ($int <= $lte) { diff --git a/tests/Unit/Schema/FloatSchemaTest.php b/tests/Unit/Schema/FloatSchemaTest.php index 4f8936d..f192d8a 100644 --- a/tests/Unit/Schema/FloatSchemaTest.php +++ b/tests/Unit/Schema/FloatSchemaTest.php @@ -176,9 +176,10 @@ public function testParseWithInvalidMinimum(): void 'path' => '', 'error' => [ 'code' => 'float.minimum', - 'template' => 'Value should be greater than or equal {{minimum}}, {{given}} given', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ 'minimum' => $minimum, + 'exclusiveMinimum' => false, 'given' => $input, ], ], @@ -190,9 +191,9 @@ public function testParseWithInvalidMinimum(): void public function testParseWithValidExclusiveMinimum(): void { $input = 4.2; - $exclusiveMinimum = 4.1; + $minimum = 4.1; - $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new FloatSchema())->minimum($minimum, true); self::assertSame($input, $schema->parse($input)); } @@ -200,9 +201,9 @@ public function testParseWithValidExclusiveMinimum(): void public function testParseWithInvalidExclusiveMinimumEqual(): void { $input = 4.1; - $exclusiveMinimum = 4.1; + $minimum = 4.1; - $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new FloatSchema())->minimum($minimum, true); try { $schema->parse($input); @@ -214,10 +215,11 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'float.exclusiveMinimum', - 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'code' => 'float.minimum', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'exclusiveMinimum' => $exclusiveMinimum, + 'minimum' => $minimum, + 'exclusiveMinimum' => true, 'given' => $input, ], ], @@ -231,9 +233,9 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void public function testParseWithInvalidExclusiveMinimumLesser(): void { $input = 4.1; - $exclusiveMinimum = 4.2; + $minimum = 4.2; - $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new FloatSchema())->minimum($minimum, true); try { $schema->parse($input); @@ -245,10 +247,11 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'float.exclusiveMinimum', - 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'code' => 'float.minimum', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'exclusiveMinimum' => $exclusiveMinimum, + 'minimum' => $minimum, + 'exclusiveMinimum' => true, 'given' => $input, ], ], @@ -262,9 +265,9 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void public function testParseWithValidExclusiveMaximum(): void { $input = 4.1; - $exclusiveMaximum = 4.2; + $maximum = 4.2; - $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new FloatSchema())->maximum($maximum, true); self::assertSame($input, $schema->parse($input)); } @@ -272,9 +275,9 @@ public function testParseWithValidExclusiveMaximum(): void public function testParseWithInvalidExclusiveMaximumEqual(): void { $input = 4.1; - $exclusiveMaximum = 4.1; + $maximum = 4.1; - $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new FloatSchema())->maximum($maximum, true); try { $schema->parse($input); @@ -285,10 +288,11 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'float.exclusiveMaximum', - 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'code' => 'float.maximum', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'exclusiveMaximum' => $exclusiveMaximum, + 'maximum' => $maximum, + 'exclusiveMaximum' => true, 'given' => $input, ], ], @@ -300,9 +304,9 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void public function testParseWithInvalidExclusiveMaximumLesser(): void { $input = 4.2; - $exclusiveMaximum = 4.1; + $maximum = 4.1; - $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new FloatSchema())->maximum($maximum, true); try { $schema->parse($input); @@ -313,10 +317,11 @@ public function testParseWithInvalidExclusiveMaximumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'float.exclusiveMaximum', - 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'code' => 'float.maximum', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'exclusiveMaximum' => $exclusiveMaximum, + 'maximum' => $maximum, + 'exclusiveMaximum' => true, 'given' => $input, ], ], @@ -352,9 +357,10 @@ public function testParseWithInvalidMaximum(): void 'path' => '', 'error' => [ 'code' => 'float.maximum', - 'template' => 'Value should be lesser than or equal {{maximum}}, {{given}} given', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ 'maximum' => $maximum, + 'exclusiveMaximum' => false, 'given' => $input, ], ], @@ -378,7 +384,7 @@ public function testParseWithValidGte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum instead', $lastError['message']); + self::assertSame('Use minimum($gte) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -426,7 +432,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exclusiveMinimum instead', $lastError['message']); + self::assertSame('Use minimum($gt, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -508,7 +514,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exclusiveMaximum instead', $lastError['message']); + self::assertSame('Use maximum($lt, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -584,7 +590,7 @@ public function testParseWithValidLte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum instead', $lastError['message']); + self::assertSame('Use maximum($lte) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/IntSchemaTest.php b/tests/Unit/Schema/IntSchemaTest.php index dd57acd..34397c9 100644 --- a/tests/Unit/Schema/IntSchemaTest.php +++ b/tests/Unit/Schema/IntSchemaTest.php @@ -175,9 +175,10 @@ public function testParseWithInvalidMinimum(): void 'path' => '', 'error' => [ 'code' => 'int.minimum', - 'template' => 'Value should be greater than or equal {{minimum}}, {{given}} given', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ 'minimum' => $minimum, + 'exclusiveMinimum' => false, 'given' => $input, ], ], @@ -189,9 +190,9 @@ public function testParseWithInvalidMinimum(): void public function testParseWithValidExclusiveMinimum(): void { $input = 5; - $exclusiveMinimum = 4; + $minimum = 4; - $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new IntSchema())->minimum($minimum, true); self::assertSame($input, $schema->parse($input)); } @@ -199,9 +200,9 @@ public function testParseWithValidExclusiveMinimum(): void public function testParseWithInvalidExclusiveMinimumEqual(): void { $input = 4; - $exclusiveMinimum = 4; + $minimum = 4; - $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new IntSchema())->minimum($minimum, true); try { $schema->parse($input); @@ -213,10 +214,11 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'int.exclusiveMinimum', - 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'code' => 'int.minimum', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'exclusiveMinimum' => $exclusiveMinimum, + 'minimum' => $minimum, + 'exclusiveMinimum' => true, 'given' => $input, ], ], @@ -230,9 +232,9 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void public function testParseWithInvalidExclusiveMinimumLesser(): void { $input = 4; - $exclusiveMinimum = 5; + $minimum = 5; - $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); + $schema = (new IntSchema())->minimum($minimum, true); try { $schema->parse($input); @@ -244,10 +246,11 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'int.exclusiveMinimum', - 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', + 'code' => 'int.minimum', + 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'exclusiveMinimum' => $exclusiveMinimum, + 'minimum' => $minimum, + 'exclusiveMinimum' => true, 'given' => $input, ], ], @@ -261,9 +264,9 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void public function testParseWithValidExclusiveMaximum(): void { $input = 4; - $exclusiveMaximum = 5; + $maximum = 5; - $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new IntSchema())->maximum($maximum, true); self::assertSame($input, $schema->parse($input)); } @@ -271,9 +274,9 @@ public function testParseWithValidExclusiveMaximum(): void public function testParseWithInvalidExclusiveMaximumEqual(): void { $input = 5; - $exclusiveMaximum = 5; + $maximum = 5; - $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new IntSchema())->maximum($maximum, true); try { $schema->parse($input); @@ -284,10 +287,11 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'int.exclusiveMaximum', - 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'code' => 'int.maximum', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'exclusiveMaximum' => $exclusiveMaximum, + 'maximum' => $maximum, + 'exclusiveMaximum' => true, 'given' => $input, ], ], @@ -299,9 +303,9 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void public function testParseWithInvalidExclusiveMaximumLesser(): void { $input = 5; - $exclusiveMaximum = 4; + $maximum = 4; - $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); + $schema = (new IntSchema())->maximum($maximum, true); try { $schema->parse($input); @@ -312,10 +316,11 @@ public function testParseWithInvalidExclusiveMaximumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'int.exclusiveMaximum', - 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', + 'code' => 'int.maximum', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'exclusiveMaximum' => $exclusiveMaximum, + 'maximum' => $maximum, + 'exclusiveMaximum' => true, 'given' => $input, ], ], @@ -351,9 +356,10 @@ public function testParseWithInvalidMaximum(): void 'path' => '', 'error' => [ 'code' => 'int.maximum', - 'template' => 'Value should be lesser than or equal {{maximum}}, {{given}} given', + 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', 'variables' => [ 'maximum' => $maximum, + 'exclusiveMaximum' => false, 'given' => $input, ], ], @@ -377,7 +383,7 @@ public function testParseWithValidGte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum instead', $lastError['message']); + self::assertSame('Use minimum($gte) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -425,7 +431,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exclusiveMinimum instead', $lastError['message']); + self::assertSame('Use minimum($gt, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -501,7 +507,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exclusiveMaximum instead', $lastError['message']); + self::assertSame('Use maximum($lt, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -577,7 +583,7 @@ public function testParseWithValidLte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum instead', $lastError['message']); + self::assertSame('Use maximum($lte) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/StringSchemaTest.php b/tests/Unit/Schema/StringSchemaTest.php index 03804de..98b9976 100644 --- a/tests/Unit/Schema/StringSchemaTest.php +++ b/tests/Unit/Schema/StringSchemaTest.php @@ -1257,7 +1257,7 @@ public function testParseWithValidtoFloat(): void { $input = '4.2'; - $schema = (new StringSchema())->toFloat()->exclusiveMinimum(4.0); + $schema = (new StringSchema())->toFloat()->minimum(4.0, true); self::assertSame((float) $input, $schema->parse($input)); } @@ -1299,7 +1299,7 @@ public function testParseWithValidtoInt(): void { $input = '42'; - $schema = (new StringSchema())->toInt()->exclusiveMinimum(40); + $schema = (new StringSchema())->toInt()->minimum(40, true); self::assertSame((int) $input, $schema->parse($input)); } From 12beebdf6a2e1c45923bf0d55e84a977473df666 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 21:42:00 +0100 Subject: [PATCH 3/9] enhance deprecation messages --- src/Parser.php | 4 ++-- src/Schema/ArraySchema.php | 14 +++++++------- src/Schema/StringSchema.php | 20 ++++++++++---------- tests/Unit/ParserTest.php | 2 +- tests/Unit/Schema/ArraySchemaTest.php | 8 ++++---- tests/Unit/Schema/StringSchemaTest.php | 10 +++++----- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index c3192a9..e76acaa 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -90,11 +90,11 @@ public function lazy(\Closure $lazy): SchemaInterface } /** - * @deprecated use const + * @deprecated Use const($literal) instead */ public function literal(bool|float|int|string $literal): LiteralSchema { - @trigger_error('Use const instead', E_USER_DEPRECATED); + @trigger_error('Use const($literal) instead', E_USER_DEPRECATED); return new LiteralSchema($literal); } diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php index 0d01d71..476dd8f 100644 --- a/src/Schema/ArraySchema.php +++ b/src/Schema/ArraySchema.php @@ -97,11 +97,11 @@ public function maxItems(int $maxItems): static } /** - * @deprecated use exactItems + * @deprecated Use exactItems($length) instead */ public function length(int $length): static { - @trigger_error('Use exactItems instead', E_USER_DEPRECATED); + @trigger_error('Use exactItems($length) instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($length) { $arrayLength = \count($array); @@ -121,11 +121,11 @@ public function length(int $length): static } /** - * @deprecated use minItems + * @deprecated Use minItems($minLength) instead */ public function minLength(int $minLength): static { - @trigger_error('Use minItems instead', E_USER_DEPRECATED); + @trigger_error('Use minItems($minLength) instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($minLength) { $arrayLength = \count($array); @@ -145,11 +145,11 @@ public function minLength(int $minLength): static } /** - * @deprecated use maxItems + * @deprecated Use maxItems($maxLength) instead */ public function maxLength(int $maxLength): static { - @trigger_error('Use maxItems instead', E_USER_DEPRECATED); + @trigger_error('Use maxItems($maxLength) instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($maxLength) { $arrayLength = \count($array); @@ -190,7 +190,7 @@ public function contains(mixed $contains, bool $strict = true): static */ public function includes(mixed $includes, bool $strict = true): static { - @trigger_error('Use contains instead', E_USER_DEPRECATED); + @trigger_error('Use contains($includes, $strict) instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($includes, $strict) { if (!\in_array($includes, $array, $strict)) { diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php index 1e5a0e5..d042ba0 100644 --- a/src/Schema/StringSchema.php +++ b/src/Schema/StringSchema.php @@ -156,11 +156,11 @@ public function contains(string $contains): static } /** - * @deprecated use contains + * @deprecated Use contains($includes) instea */ public function includes(string $includes): static { - @trigger_error('Use contains instead', E_USER_DEPRECATED); + @trigger_error('Use contains($includes) instead', E_USER_DEPRECATED); return $this->postParse(static function (string $string) use ($includes) { if (str_contains($string, $includes)) { @@ -229,11 +229,11 @@ public function hostname(): static } /** - * @deprecated use hostname + * @deprecated Use hostname() instead */ public function domain(): static { - @trigger_error('Use hostname instead', E_USER_DEPRECATED); + @trigger_error('Use hostname() instead', E_USER_DEPRECATED); return $this->postParse(static function (string $string) { if (filter_var($string, FILTER_VALIDATE_DOMAIN)) { @@ -319,11 +319,11 @@ public function mac(): static } /** - * @deprecated: use pattern + * @deprecated: Use pattern($match) instead */ public function match(string $match): static { - @trigger_error('Use pattern instead', E_USER_DEPRECATED); + @trigger_error('Use pattern($match) instead', E_USER_DEPRECATED); if (false === @preg_match($match, '')) { throw new \InvalidArgumentException(\sprintf('Invalid match "%s" given', $match)); @@ -370,11 +370,11 @@ public function pattern(string $pattern): static } /** - * @deprecated: use pattern + * @deprecated: Use pattern($regexp) instead */ public function regexp(string $regexp): static { - @trigger_error('Use pattern instead', E_USER_DEPRECATED); + @trigger_error('Use pattern($regexp) instead', E_USER_DEPRECATED); if (false === @preg_match($regexp, '')) { throw new \InvalidArgumentException(\sprintf('Invalid regexp "%s" given', $regexp)); @@ -415,11 +415,11 @@ public function uri(): static } /** - * @deprecated use uri + * @deprecated Use uri() instead */ public function url(): static { - @trigger_error('Use uri instead', E_USER_DEPRECATED); + @trigger_error('Use uri() instead', E_USER_DEPRECATED); return $this->postParse(static function (string $string) { if (filter_var($string, FILTER_VALIDATE_URL)) { diff --git a/tests/Unit/ParserTest.php b/tests/Unit/ParserTest.php index b23254c..f94fc6b 100644 --- a/tests/Unit/ParserTest.php +++ b/tests/Unit/ParserTest.php @@ -154,7 +154,7 @@ public function testLiteral(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use const instead', $lastError['message']); + self::assertSame('Use const($literal) instead', $lastError['message']); self::assertInstanceOf(LiteralSchema::class, $literalSchema); } diff --git a/tests/Unit/Schema/ArraySchemaTest.php b/tests/Unit/Schema/ArraySchemaTest.php index 26f3d1e..f1b4e09 100644 --- a/tests/Unit/Schema/ArraySchemaTest.php +++ b/tests/Unit/Schema/ArraySchemaTest.php @@ -218,7 +218,7 @@ public function testParseWithValidLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exactItems instead', $lastError['message']); + self::assertSame('Use exactItems($length) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -300,7 +300,7 @@ public function testParseWithValidMinLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minItems instead', $lastError['message']); + self::assertSame('Use minItems($minLength) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -382,7 +382,7 @@ public function testParseWithValidMaxLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maxItems instead', $lastError['message']); + self::assertSame('Use maxItems($maxLength) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -562,7 +562,7 @@ public function testParseWithValidIncludes(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use contains instead', $lastError['message']); + self::assertSame('Use contains($includes, $strict) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/StringSchemaTest.php b/tests/Unit/Schema/StringSchemaTest.php index 98b9976..8504b55 100644 --- a/tests/Unit/Schema/StringSchemaTest.php +++ b/tests/Unit/Schema/StringSchemaTest.php @@ -308,7 +308,7 @@ public function testParseWithValidIncludes(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use contains instead', $lastError['message']); + self::assertSame('Use contains($includes) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -461,7 +461,7 @@ public function testParseWithValidDomain(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use hostname instead', $lastError['message']); + self::assertSame('Use hostname() instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -659,7 +659,7 @@ public function testParseWithValidMatch(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use pattern instead', $lastError['message']); + self::assertSame('Use pattern($match) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -763,7 +763,7 @@ public function testParseWithValidRegexp(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use pattern instead', $lastError['message']); + self::assertSame('Use pattern($regexp) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -844,7 +844,7 @@ public function testParseWithValidUrl(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use uri instead', $lastError['message']); + self::assertSame('Use uri() instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } From 97a15506b8388e7e0324bda4a8f44059ef0e45cd Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 21:48:29 +0100 Subject: [PATCH 4/9] add deprecated to the consts as well --- src/Schema/ArraySchema.php | 12 ++++++++++++ src/Schema/FloatSchema.php | 12 ++++++++++++ src/Schema/IntSchema.php | 12 ++++++++++++ src/Schema/StringSchema.php | 15 +++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php index 476dd8f..0d1705a 100644 --- a/src/Schema/ArraySchema.php +++ b/src/Schema/ArraySchema.php @@ -22,19 +22,31 @@ final class ArraySchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_MAX_ITEMS_CODE = 'array.maxItems'; public const string ERROR_MAX_ITEMS_TEMPLATE = 'Max items {{maxItems}}, {{given}} given'; + /** @deprecated: see ERROR_EXACT_ITEMS_CODE */ public const string ERROR_LENGTH_CODE = 'array.length'; + + /** @deprecated: see ERROR_EXACT_ITEMS_TEMPLATE */ public const string ERROR_LENGTH_TEMPLATE = 'Length {{length}}, {{given}} given'; + /** @deprecated: see ERROR_MIN_ITEMS_CODE */ public const string ERROR_MIN_LENGTH_CODE = 'array.minLength'; + + /** @deprecated: see ERROR_MIN_ITEMS_TEMPLATE */ public const string ERROR_MIN_LENGTH_TEMPLATE = 'Min length {{min}}, {{given}} given'; + /** @deprecated: see ERROR_MAX_ITEMS_CODE */ public const string ERROR_MAX_LENGTH_CODE = 'array.maxLength'; + + /** @deprecated: see ERROR_MAX_ITEMS_TEMPLATE */ public const string ERROR_MAX_LENGTH_TEMPLATE = 'Max length {{max}}, {{given}} given'; public const string ERROR_CONTAINS_CODE = 'array.contains'; public const string ERROR_CONTAINS_TEMPLATE = '{{given}} does not contain {{contains}}'; + /** @deprecated: see ERROR_CONTAINS_CODE */ public const string ERROR_INCLUDES_CODE = 'array.includes'; + + /** @deprecated: see ERROR_CONTAINS_TEMPLATE */ public const string ERROR_INCLUDES_TEMPLATE = '{{given}} does not include {{includes}}'; public function __construct(private SchemaInterface $itemSchema) {} diff --git a/src/Schema/FloatSchema.php b/src/Schema/FloatSchema.php index 67ed1cd..cd1c6be 100644 --- a/src/Schema/FloatSchema.php +++ b/src/Schema/FloatSchema.php @@ -18,16 +18,28 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_MAXIMUM_CODE = 'float.maximum'; public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; + /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GTE_CODE = 'float.gte'; + + /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; + /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GT_CODE = 'float.gt'; + + /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + /** @deprecated: see ERROR_MAXIMUM_CODE */ public const string ERROR_LT_CODE = 'float.lt'; + + /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; + /** @deprecated: see ERROR_MAXIMUM_CODE */ public const string ERROR_LTE_CODE = 'float.lte'; + + /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ public const string ERROR_LTE_TEMPLATE = 'Value should be lesser than or equal {{lte}}, {{given}} given'; public const string ERROR_INT_CODE = 'float.int'; diff --git a/src/Schema/IntSchema.php b/src/Schema/IntSchema.php index cc40824..7a6343b 100644 --- a/src/Schema/IntSchema.php +++ b/src/Schema/IntSchema.php @@ -18,16 +18,28 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac public const string ERROR_MAXIMUM_CODE = 'int.maximum'; public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; + /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GTE_CODE = 'int.gte'; + + /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; + /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GT_CODE = 'int.gt'; + + /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; + /** @deprecated: see ERROR_MAXIMUM_CODE */ public const string ERROR_LT_CODE = 'int.lt'; + + /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; + /** @deprecated: see ERROR_MAXIMUM_CODE */ public const string ERROR_LTE_CODE = 'int.lte'; + + /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ public const string ERROR_LTE_TEMPLATE = 'Value should be lesser than or equal {{lte}}, {{given}} given'; public function minimum(int $minimum, bool $exclusiveMinimum = false): static diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php index d042ba0..05630c9 100644 --- a/src/Schema/StringSchema.php +++ b/src/Schema/StringSchema.php @@ -25,7 +25,10 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_CONTAINS_CODE = 'string.contains'; public const string ERROR_CONTAINS_TEMPLATE = '{{given}} does not contain {{contains}}'; + /** @deprecated: see ERROR_CONTAINS_CODE */ public const string ERROR_INCLUDES_CODE = 'string.includes'; + + /** @deprecated: see ERROR_CONTAINS_TEMPLATE */ public const string ERROR_INCLUDES_TEMPLATE = '{{given}} does not include {{includes}}'; public const string ERROR_STARTSWITH_CODE = 'string.startsWith'; @@ -37,7 +40,10 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_HOSTNAME_CODE = 'string.hostname'; public const string ERROR_HOSTNAME_TEMPLATE = 'Invalid hostname {{given}}'; + /** @deprecated: see ERROR_HOSTNAME_CODE */ public const string ERROR_DOMAIN_CODE = 'string.domain'; + + /** @deprecated: see ERROR_HOSTNAME_TEMPLATE */ public const string ERROR_DOMAIN_TEMPLATE = 'Invalid domain {{given}}'; public const string ERROR_EMAIL_CODE = 'string.email'; @@ -49,19 +55,28 @@ final class StringSchema extends AbstractSchemaInnerParse implements SchemaInter public const string ERROR_MAC_CODE = 'string.mac'; public const string ERROR_MAC_TEMPLATE = 'Invalid mac {{given}}'; + /** @deprecated: see ERROR_PATTERN_CODE */ public const string ERROR_MATCH_CODE = 'string.match'; + + /** @deprecated: see ERROR_PATTERN_TEMPLATE */ public const string ERROR_MATCH_TEMPLATE = '{{given}} does not match {{match}}'; public const string ERROR_PATTERN_CODE = 'string.pattern'; public const string ERROR_PATTERN_TEMPLATE = '{{given}} does not pattern {{pattern}}'; + /** @deprecated: see ERROR_PATTERN_CODE */ public const string ERROR_REGEXP_CODE = 'string.regexp'; + + /** @deprecated: see ERROR_PATTERN_TEMPLATE */ public const string ERROR_REGEXP_TEMPLATE = '{{given}} does not regexp {{regexp}}'; public const string ERROR_URI_CODE = 'string.uri'; public const string ERROR_URI_TEMPLATE = 'Invalid uri {{given}}'; + /** @deprecated: see ERROR_URI_CODE */ public const string ERROR_URL_CODE = 'string.url'; + + /** @deprecated: see ERROR_URI_TEMPLATE */ public const string ERROR_URL_TEMPLATE = 'Invalid url {{given}}'; public const string ERROR_UUID_CODE = 'string.uuid'; From 35b3d1f07f770f5e20f6ba36367c220b2bb88054 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 23:45:24 +0100 Subject: [PATCH 5/9] enhance deprecation message --- src/Parser.php | 2 - src/Schema/AbstractSchemaInnerParse.php | 29 ++++++++ src/Schema/ArraySchema.php | 8 +-- src/Schema/FloatSchema.php | 28 +++++--- src/Schema/IntSchema.php | 28 +++++--- src/Schema/LiteralSchema.php | 5 +- src/Schema/StringSchema.php | 6 +- tests/Unit/ParserTest.php | 2 +- .../Schema/AbstractSchemaInnerParseTest.php | 70 +++++++++++++++++++ tests/Unit/Schema/ArraySchemaTest.php | 8 +-- tests/Unit/Schema/FloatSchemaTest.php | 8 +-- tests/Unit/Schema/IntSchemaTest.php | 54 ++++++++------ tests/Unit/Schema/StringSchemaTest.php | 6 +- 13 files changed, 194 insertions(+), 60 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index e76acaa..839db6b 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -94,8 +94,6 @@ public function lazy(\Closure $lazy): SchemaInterface */ public function literal(bool|float|int|string $literal): LiteralSchema { - @trigger_error('Use const($literal) instead', E_USER_DEPRECATED); - return new LiteralSchema($literal); } diff --git a/src/Schema/AbstractSchemaInnerParse.php b/src/Schema/AbstractSchemaInnerParse.php index 19afe98..fa31f73 100644 --- a/src/Schema/AbstractSchemaInnerParse.php +++ b/src/Schema/AbstractSchemaInnerParse.php @@ -123,4 +123,33 @@ final protected function getDataType(mixed $input): string { return \is_object($input) ? $input::class : \gettype($input); } + + protected function varExport(mixed $input): string + { + if ($input instanceof \DateTimeInterface) { + return 'new \DateTimeImmutable(\''.$input->format('c').'\')'; + } + + if ($input instanceof \BackedEnum) { + return '\\'.$input::class.'::from('.$this->varExport($input->value).')'; + } + + if ($input instanceof \stdClass) { + return '(object) '.$this->varExport((array) $input); + } + + if (\is_array($input)) { + return '['.implode(', ', array_map( + fn (int|string $key, mixed $value) => $this->varExport($key).' => '.$this->varExport($value), + array_keys($input), + $input + )).']'; + } + + if (null === $input) { + return 'null'; + } + + return var_export($input, true); + } } diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php index 0d1705a..18c5342 100644 --- a/src/Schema/ArraySchema.php +++ b/src/Schema/ArraySchema.php @@ -113,7 +113,7 @@ public function maxItems(int $maxItems): static */ public function length(int $length): static { - @trigger_error('Use exactItems($length) instead', E_USER_DEPRECATED); + @trigger_error('Use exactItems('.$this->varExport($length).') instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($length) { $arrayLength = \count($array); @@ -137,7 +137,7 @@ public function length(int $length): static */ public function minLength(int $minLength): static { - @trigger_error('Use minItems($minLength) instead', E_USER_DEPRECATED); + @trigger_error('Use minItems('.$this->varExport($minLength).') instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($minLength) { $arrayLength = \count($array); @@ -161,7 +161,7 @@ public function minLength(int $minLength): static */ public function maxLength(int $maxLength): static { - @trigger_error('Use maxItems($maxLength) instead', E_USER_DEPRECATED); + @trigger_error('Use maxItems('.$this->varExport($maxLength).') instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($maxLength) { $arrayLength = \count($array); @@ -202,7 +202,7 @@ public function contains(mixed $contains, bool $strict = true): static */ public function includes(mixed $includes, bool $strict = true): static { - @trigger_error('Use contains($includes, $strict) instead', E_USER_DEPRECATED); + @trigger_error('Use contains('.$this->varExport($includes).', '.$this->varExport($strict).') instead', E_USER_DEPRECATED); return $this->postParse(static function (array $array) use ($includes, $strict) { if (!\in_array($includes, $array, $strict)) { diff --git a/src/Schema/FloatSchema.php b/src/Schema/FloatSchema.php index cd1c6be..c3c5a7b 100644 --- a/src/Schema/FloatSchema.php +++ b/src/Schema/FloatSchema.php @@ -84,7 +84,7 @@ public function maximum(float $maximum, bool $exclusiveMaximum = false): static */ public function gte(float $gte): static { - @trigger_error('Use minimum($gte) instead', E_USER_DEPRECATED); + @trigger_error('Use minimum('.$this->varExport($gte).') instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($gte) { if ($float >= $gte) { @@ -106,7 +106,7 @@ public function gte(float $gte): static */ public function gt(float $gt): static { - @trigger_error('Use minimum($gt, true) instead', E_USER_DEPRECATED); + @trigger_error('Use minimum('.$this->varExport($gt).', true) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($gt) { if ($float > $gt) { @@ -128,7 +128,7 @@ public function gt(float $gt): static */ public function lt(float $lt): static { - @trigger_error('Use maximum($lt, true) instead', E_USER_DEPRECATED); + @trigger_error('Use maximum('.$this->varExport($lt).', true) instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($lt) { if ($float < $lt) { @@ -150,7 +150,7 @@ public function lt(float $lt): static */ public function lte(float $lte): static { - @trigger_error('Use maximum($lte) instead', E_USER_DEPRECATED); + @trigger_error('Use maximum('.$this->varExport($lte).') instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($lte) { if ($float <= $lte) { @@ -167,21 +167,33 @@ public function lte(float $lte): static }); } - public function positive(): static + /** + * @deprecated Use minimum(0.0) instead + */ + public function nonNegative(): static { - return $this->gt(0.0); + return $this->gte(0.0); } - public function nonNegative(): static + /** + * @deprecated Use minimum(0.0, true) instead + */ + public function positive(): static { - return $this->gte(0.0); + return $this->gt(0.0); } + /** + * @deprecated Use maximum(0.0, true) instead + */ public function negative(): static { return $this->lt(0.0); } + /** + * @deprecated Use maximum(0.0) instead + */ public function nonPositive(): static { return $this->lte(0.0); diff --git a/src/Schema/IntSchema.php b/src/Schema/IntSchema.php index 7a6343b..459bcac 100644 --- a/src/Schema/IntSchema.php +++ b/src/Schema/IntSchema.php @@ -81,7 +81,7 @@ public function maximum(int $maximum, bool $exclusiveMaximum = false): static */ public function gte(int $gte): static { - @trigger_error('Use minimum($gte) instead', E_USER_DEPRECATED); + @trigger_error('Use minimum('.$this->varExport($gte).') instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($gte) { if ($int >= $gte) { @@ -103,7 +103,7 @@ public function gte(int $gte): static */ public function gt(int $gt): static { - @trigger_error('Use minimum($gt, true) instead', E_USER_DEPRECATED); + @trigger_error('Use minimum('.$this->varExport($gt).', true) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($gt) { if ($int > $gt) { @@ -125,7 +125,7 @@ public function gt(int $gt): static */ public function lt(int $lt): static { - @trigger_error('Use maximum($lt, true) instead', E_USER_DEPRECATED); + @trigger_error('Use maximum('.$this->varExport($lt).', true) instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($lt) { if ($int < $lt) { @@ -147,7 +147,7 @@ public function lt(int $lt): static */ public function lte(int $lte): static { - @trigger_error('Use maximum($lte) instead', E_USER_DEPRECATED); + @trigger_error('Use maximum('.$this->varExport($lte).') instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($lte) { if ($int <= $lte) { @@ -164,21 +164,33 @@ public function lte(int $lte): static }); } - public function positive(): static + /** + * @deprecated Use minimum(0) instead + */ + public function nonNegative(): static { - return $this->gt(0); + return $this->gte(0); } - public function nonNegative(): static + /** + * @deprecated Use minimum(0, true) instead + */ + public function positive(): static { - return $this->gte(0); + return $this->gt(0); } + /** + * @deprecated Use maximum(0, true) instead + */ public function negative(): static { return $this->lt(0); } + /** + * @deprecated Use maximum(0) instead + */ public function nonPositive(): static { return $this->lte(0); diff --git a/src/Schema/LiteralSchema.php b/src/Schema/LiteralSchema.php index 01bb736..45e5425 100644 --- a/src/Schema/LiteralSchema.php +++ b/src/Schema/LiteralSchema.php @@ -18,7 +18,10 @@ final class LiteralSchema extends AbstractSchemaInnerParse public const string ERROR_EQUALS_CODE = 'literal.equals'; public const string ERROR_EQUALS_TEMPLATE = 'Input should be {{expected}}, {{given}} given'; - public function __construct(private bool|float|int|string $literal) {} + public function __construct(private bool|float|int|string $literal) + { + @trigger_error('Use '.ConstSchema::class.' instead', E_USER_DEPRECATED); + } protected function innerParse(mixed $input): mixed { diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php index 05630c9..d31ca7c 100644 --- a/src/Schema/StringSchema.php +++ b/src/Schema/StringSchema.php @@ -175,7 +175,7 @@ public function contains(string $contains): static */ public function includes(string $includes): static { - @trigger_error('Use contains($includes) instead', E_USER_DEPRECATED); + @trigger_error('Use contains('.$this->varExport($includes).') instead', E_USER_DEPRECATED); return $this->postParse(static function (string $string) use ($includes) { if (str_contains($string, $includes)) { @@ -338,7 +338,7 @@ public function mac(): static */ public function match(string $match): static { - @trigger_error('Use pattern($match) instead', E_USER_DEPRECATED); + @trigger_error('Use pattern('.$this->varExport($match).') instead', E_USER_DEPRECATED); if (false === @preg_match($match, '')) { throw new \InvalidArgumentException(\sprintf('Invalid match "%s" given', $match)); @@ -389,7 +389,7 @@ public function pattern(string $pattern): static */ public function regexp(string $regexp): static { - @trigger_error('Use pattern($regexp) instead', E_USER_DEPRECATED); + @@trigger_error('Use pattern('.$this->varExport($regexp).') instead', E_USER_DEPRECATED); if (false === @preg_match($regexp, '')) { throw new \InvalidArgumentException(\sprintf('Invalid regexp "%s" given', $regexp)); diff --git a/tests/Unit/ParserTest.php b/tests/Unit/ParserTest.php index f94fc6b..352896b 100644 --- a/tests/Unit/ParserTest.php +++ b/tests/Unit/ParserTest.php @@ -154,7 +154,7 @@ public function testLiteral(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use const($literal) instead', $lastError['message']); + self::assertSame('Use Chubbyphp\Parsing\Schema\ConstSchema instead', $lastError['message']); self::assertInstanceOf(LiteralSchema::class, $literalSchema); } diff --git a/tests/Unit/Schema/AbstractSchemaInnerParseTest.php b/tests/Unit/Schema/AbstractSchemaInnerParseTest.php index d0f0941..4c8274d 100644 --- a/tests/Unit/Schema/AbstractSchemaInnerParseTest.php +++ b/tests/Unit/Schema/AbstractSchemaInnerParseTest.php @@ -9,6 +9,14 @@ use Chubbyphp\Parsing\Schema\AbstractSchemaInnerParse; use PHPUnit\Framework\TestCase; +enum AbstractSchemaInnerParseEnum: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + /** * @covers \Chubbyphp\Parsing\Schema\AbstractSchemaInnerParse * @@ -146,4 +154,66 @@ protected function innerParse(mixed $input): mixed self::assertSame(\stdClass::class, $e->errors->jsonSerialize()[0]['error']['variables']['given']); } } + + public function testVarExport(): void + { + $schema = new class extends AbstractSchemaInnerParse { + protected function innerParse(mixed $input): string + { + return $this->varExport($input); + } + }; + + self::assertSame('null', $schema->parse(null)); + self::assertSame('true', $schema->parse(true)); + self::assertSame('false', $schema->parse(false)); + self::assertSame('1', $schema->parse(1)); + self::assertSame('1.234', $schema->parse(1.234)); + self::assertSame('1200.0', $schema->parse(1.2e3)); + self::assertSame('7.0E-10', $schema->parse(7E-10)); + self::assertSame('1234.567', $schema->parse(1_234.567)); + self::assertSame("'test'", $schema->parse('test')); + self::assertSame( + "new \\DateTimeImmutable('2024-01-20T09:15:00+00:00')", + $schema->parse(new \DateTimeImmutable('2024-01-20T09:15:00+00:00')) + ); + self::assertSame( + '\Chubbyphp\Tests\Parsing\Unit\Schema\AbstractSchemaInnerParseEnum::from(\'H\')', + $schema->parse(AbstractSchemaInnerParseEnum::Hearts) + ); + + self::assertSame( + "['null' => null, 'true' => true, 'false' => 'false', 1 => 1, '1.234' => 1.234, '1.2e3' => 1200.0, '7E-10' => 7.0E-10, '1_234.567' => 1234.567, 'test' => 'test', '2024-01-20T09:15:00+00:00' => new \\DateTimeImmutable('2024-01-20T09:15:00+00:00'), 'AbstractSchemaInnerParseEnum::Hearts' => \\Chubbyphp\\Tests\\Parsing\\Unit\\Schema\\AbstractSchemaInnerParseEnum::from('H')]", + $schema->parse([ + 'null' => null, + 'true' => true, + 'false' => 'false', + '1' => 1, + '1.234' => 1.234, + '1.2e3' => 1.2e3, + '7E-10' => 7E-10, + '1_234.567' => 1_234.567, + 'test' => 'test', + '2024-01-20T09:15:00+00:00' => new \DateTimeImmutable('2024-01-20T09:15:00+00:00'), + 'AbstractSchemaInnerParseEnum::Hearts' => AbstractSchemaInnerParseEnum::Hearts, + ]) + ); + + self::assertSame( + "(object) ['null' => null, 'true' => true, 'false' => 'false', 1 => 1, '1.234' => 1.234, '1.2e3' => 1200.0, '7E-10' => 7.0E-10, '1_234.567' => 1234.567, 'test' => 'test', '2024-01-20T09:15:00+00:00' => new \\DateTimeImmutable('2024-01-20T09:15:00+00:00'), 'AbstractSchemaInnerParseEnum::Hearts' => \\Chubbyphp\\Tests\\Parsing\\Unit\\Schema\\AbstractSchemaInnerParseEnum::from('H')]", + $schema->parse((object) [ + 'null' => null, + 'true' => true, + 'false' => 'false', + '1' => 1, + '1.234' => 1.234, + '1.2e3' => 1.2e3, + '7E-10' => 7E-10, + '1_234.567' => 1_234.567, + 'test' => 'test', + '2024-01-20T09:15:00+00:00' => new \DateTimeImmutable('2024-01-20T09:15:00+00:00'), + 'AbstractSchemaInnerParseEnum::Hearts' => AbstractSchemaInnerParseEnum::Hearts, + ]) + ); + } } diff --git a/tests/Unit/Schema/ArraySchemaTest.php b/tests/Unit/Schema/ArraySchemaTest.php index f1b4e09..c8f0c1a 100644 --- a/tests/Unit/Schema/ArraySchemaTest.php +++ b/tests/Unit/Schema/ArraySchemaTest.php @@ -218,7 +218,7 @@ public function testParseWithValidLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use exactItems($length) instead', $lastError['message']); + self::assertSame('Use exactItems(4) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -300,7 +300,7 @@ public function testParseWithValidMinLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minItems($minLength) instead', $lastError['message']); + self::assertSame('Use minItems(4) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -382,7 +382,7 @@ public function testParseWithValidMaxLength(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maxItems($maxLength) instead', $lastError['message']); + self::assertSame('Use maxItems(4) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -562,7 +562,7 @@ public function testParseWithValidIncludes(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use contains($includes, $strict) instead', $lastError['message']); + self::assertSame("Use contains(new \\DateTimeImmutable('2024-01-21T09:15:00+00:00'), true) instead", $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/FloatSchemaTest.php b/tests/Unit/Schema/FloatSchemaTest.php index f192d8a..79187ef 100644 --- a/tests/Unit/Schema/FloatSchemaTest.php +++ b/tests/Unit/Schema/FloatSchemaTest.php @@ -384,7 +384,7 @@ public function testParseWithValidGte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum($gte) instead', $lastError['message']); + self::assertSame('Use minimum(4.1) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -432,7 +432,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum($gt, true) instead', $lastError['message']); + self::assertSame('Use minimum(4.1, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -514,7 +514,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum($lt, true) instead', $lastError['message']); + self::assertSame('Use maximum(4.2, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -590,7 +590,7 @@ public function testParseWithValidLte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum($lte) instead', $lastError['message']); + self::assertSame('Use maximum(4.1) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/IntSchemaTest.php b/tests/Unit/Schema/IntSchemaTest.php index 34397c9..038e3e4 100644 --- a/tests/Unit/Schema/IntSchemaTest.php +++ b/tests/Unit/Schema/IntSchemaTest.php @@ -383,7 +383,7 @@ public function testParseWithValidGte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum($gte) instead', $lastError['message']); + self::assertSame('Use minimum(5) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -431,7 +431,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum($gt, true) instead', $lastError['message']); + self::assertSame('Use minimum(4, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -507,7 +507,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum($lt, true) instead', $lastError['message']); + self::assertSame('Use maximum(5, true) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -583,7 +583,7 @@ public function testParseWithValidLte(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum($lte) instead', $lastError['message']); + self::assertSame('Use maximum(5) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -616,20 +616,30 @@ public function testParseWithInvalidLte(): void } } - public function testParseWithValidPositive(): void + public function testParseWithValidNonNegative(): void { - $input = 1; + $input = 0; - $schema = (new IntSchema())->positive(); + error_clear_last(); + + $schema = (new IntSchema())->nonNegative(); + + $lastError = error_get_last(); + + self::assertNotNull($lastError); + self::assertArrayHasKey('type', $lastError); + self::assertSame(E_USER_DEPRECATED, $lastError['type']); + self::assertArrayHasKey('message', $lastError); + self::assertSame('Use minimum(0) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } - public function testParseWithInvalidPositive(): void + public function testParseWithInvalidNonNegative(): void { - $input = 0; + $input = -1; - $schema = (new IntSchema())->positive(); + $schema = (new IntSchema())->nonNegative(); try { $schema->parse($input); @@ -640,10 +650,10 @@ public function testParseWithInvalidPositive(): void [ 'path' => '', 'error' => [ - 'code' => 'int.gt', - 'template' => 'Value should be greater than {{gt}}, {{given}} given', + 'code' => 'int.gte', + 'template' => 'Value should be greater than or equal {{gte}}, {{given}} given', 'variables' => [ - 'gt' => 0, + 'gte' => 0, 'given' => $input, ], ], @@ -652,20 +662,20 @@ public function testParseWithInvalidPositive(): void } } - public function testParseWithValidNonNegative(): void + public function testParseWithValidPositive(): void { - $input = 0; + $input = 1; - $schema = (new IntSchema())->nonNegative(); + $schema = (new IntSchema())->positive(); self::assertSame($input, $schema->parse($input)); } - public function testParseWithInvalidNonNegative(): void + public function testParseWithInvalidPositive(): void { - $input = -1; + $input = 0; - $schema = (new IntSchema())->nonNegative(); + $schema = (new IntSchema())->positive(); try { $schema->parse($input); @@ -676,10 +686,10 @@ public function testParseWithInvalidNonNegative(): void [ 'path' => '', 'error' => [ - 'code' => 'int.gte', - 'template' => 'Value should be greater than or equal {{gte}}, {{given}} given', + 'code' => 'int.gt', + 'template' => 'Value should be greater than {{gt}}, {{given}} given', 'variables' => [ - 'gte' => 0, + 'gt' => 0, 'given' => $input, ], ], diff --git a/tests/Unit/Schema/StringSchemaTest.php b/tests/Unit/Schema/StringSchemaTest.php index 8504b55..ba5721f 100644 --- a/tests/Unit/Schema/StringSchemaTest.php +++ b/tests/Unit/Schema/StringSchemaTest.php @@ -308,7 +308,7 @@ public function testParseWithValidIncludes(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use contains($includes) instead', $lastError['message']); + self::assertSame('Use contains(\'amp\') instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -659,7 +659,7 @@ public function testParseWithValidMatch(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use pattern($match) instead', $lastError['message']); + self::assertSame('Use pattern(\'/^[a-z]+$/i\') instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -763,7 +763,7 @@ public function testParseWithValidRegexp(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use pattern($regexp) instead', $lastError['message']); + self::assertSame('Use pattern(\'/^[a-z]+$/i\') instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } From 0d6c8317e8fa7e020885e732f93f5b4ce8876a9c Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Mon, 16 Feb 2026 21:27:25 +0100 Subject: [PATCH 6/9] make exclusive*( their own function --- doc/Schema/FloatSchema.md | 6 +- doc/Schema/IntSchema.md | 8 ++- src/Schema/FloatSchema.php | 76 ++++++++++++++++++++------ src/Schema/IntSchema.php | 76 ++++++++++++++++++++------ tests/Unit/Schema/FloatSchemaTest.php | 66 +++++++++++----------- tests/Unit/Schema/IntSchemaTest.php | 66 +++++++++++----------- tests/Unit/Schema/StringSchemaTest.php | 4 +- 7 files changed, 191 insertions(+), 111 deletions(-) diff --git a/doc/Schema/FloatSchema.md b/doc/Schema/FloatSchema.md index 6110881..7450971 100644 --- a/doc/Schema/FloatSchema.md +++ b/doc/Schema/FloatSchema.md @@ -20,8 +20,8 @@ $data = $schema->parse(4.2); // Returns: 4.2 ```php $schema->minimum(5.0); // Greater than or equal to 5.0 -$schema->minimum(5.0, true); // Greater than 5.0 -$schema->maximum(10.0, true); // Less than 10.0 +$schema->exclusiveMinimum(5.0); // Greater than 5.0 +$schema->exclusiveMaximum(10.0); // Less than 10.0 $schema->maximum(10.0); // Less than or equal to 10.0 ``` @@ -83,5 +83,7 @@ $coordinatesSchema->parse(['lat' => 47.1, 'lng' => 8.2]); |------|-------------| | `float.type` | Value is not a float | | `float.minimum` | Value is not greater than or equal to threshold | +| `float.exclusiveMinimum` | Value is not greater than to threshold | +| `float.exclusiveMaximum` | Value is not less than to threshold | | `float.maximum` | Value is not less than or equal to threshold | | `float.int` | Cannot convert float to int without precision loss (for `toInt()`) | diff --git a/doc/Schema/IntSchema.md b/doc/Schema/IntSchema.md index 30046b6..1808bb0 100644 --- a/doc/Schema/IntSchema.md +++ b/doc/Schema/IntSchema.md @@ -20,8 +20,8 @@ $data = $schema->parse(1337); // Returns: 1337 ```php $schema->minimum(5); // Greater than or equal to 5 -$schema->minimum(5, true); // Greater than 5 -$schema->maximum(10, true); // Less than 10 +$schema->exclusiveMinimum(5); // Greater than 5 +$schema->exclusiveMaximum(10); // Less than 10 $schema->maximum(10); // Less than or equal to 10 ``` @@ -91,4 +91,6 @@ $date = $timestampSchema->parse(1705744500); |------|-------------| | `int.type` | Value is not an integer | | `int.minimum` | Value is not greater than or equal to threshold | -| `int.maximum` | Value is not less than or equal to threshold | \ No newline at end of file +| `int.exclusiveMinimum` | Value is not greater than to threshold | +| `int.exclusiveMaximum` | Value is not less than to threshold | +| `int.maximum` | Value is not less than or equal to threshold | diff --git a/src/Schema/FloatSchema.php b/src/Schema/FloatSchema.php index c3c5a7b..682d0ac 100644 --- a/src/Schema/FloatSchema.php +++ b/src/Schema/FloatSchema.php @@ -13,10 +13,16 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_TYPE_TEMPLATE = 'Type should be "float", {{given}} given'; public const string ERROR_MINIMUM_CODE = 'float.minimum'; - public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'float.exclusiveMinimum'; + public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'float.exclusiveMaximum'; + public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; public const string ERROR_MAXIMUM_CODE = 'float.maximum'; - public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}}, {{given}} given'; /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GTE_CODE = 'float.gte'; @@ -24,16 +30,16 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; - /** @deprecated: see ERROR_MINIMUM_CODE */ + /** @deprecated: see ERROR_EXCLUSIVE_MINIMUM_CODE */ public const string ERROR_GT_CODE = 'float.gt'; - /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ + /** @deprecated: see ERROR_EXCLUSIVE_MINIMUM_TEMPLATE */ public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; - /** @deprecated: see ERROR_MAXIMUM_CODE */ + /** @deprecated: see ERROR_EXCLUSIVE_MAXIMUM_CODE */ public const string ERROR_LT_CODE = 'float.lt'; - /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ + /** @deprecated: see ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE */ public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; /** @deprecated: see ERROR_MAXIMUM_CODE */ @@ -45,10 +51,10 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf public const string ERROR_INT_CODE = 'float.int'; public const string ERROR_INT_TEMPLATE = 'Cannot convert {{given}} to int'; - public function minimum(float $minimum, bool $exclusiveMinimum = false): static + public function minimum(float $minimum): static { - return $this->postParse(static function (float $float) use ($minimum, $exclusiveMinimum) { - if ((!$exclusiveMinimum && $float >= $minimum) || ($exclusiveMinimum && $float > $minimum)) { + return $this->postParse(static function (float $float) use ($minimum) { + if ($float >= $minimum) { return $float; } @@ -56,16 +62,50 @@ public function minimum(float $minimum, bool $exclusiveMinimum = false): static new Error( self::ERROR_MINIMUM_CODE, self::ERROR_MINIMUM_TEMPLATE, - ['minimum' => $minimum, 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $float] + ['minimum' => $minimum, 'given' => $float] + ) + ); + }); + } + + public function exclusiveMinimum(float $exclusiveMinimum): static + { + return $this->postParse(static function (float $float) use ($exclusiveMinimum) { + if ($float > $exclusiveMinimum) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MINIMUM_CODE, + self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, + ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $float] + ) + ); + }); + } + + public function exclusiveMaximum(float $exclusiveMaximum): static + { + return $this->postParse(static function (float $float) use ($exclusiveMaximum) { + if ($float < $exclusiveMaximum) { + return $float; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MAXIMUM_CODE, + self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, + ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $float] ) ); }); } - public function maximum(float $maximum, bool $exclusiveMaximum = false): static + public function maximum(float $maximum): static { - return $this->postParse(static function (float $float) use ($maximum, $exclusiveMaximum) { - if ((!$exclusiveMaximum && $float <= $maximum) || ($exclusiveMaximum && $float < $maximum)) { + return $this->postParse(static function (float $float) use ($maximum) { + if ($float <= $maximum) { return $float; } @@ -73,7 +113,7 @@ public function maximum(float $maximum, bool $exclusiveMaximum = false): static new Error( self::ERROR_MAXIMUM_CODE, self::ERROR_MAXIMUM_TEMPLATE, - ['maximum' => $maximum, 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $float] + ['maximum' => $maximum, 'given' => $float] ) ); }); @@ -102,11 +142,11 @@ public function gte(float $gte): static } /** - * @deprecated Use minimum($gt, true) instead + * @deprecated Use exclusiveMinimum($gt) instead */ public function gt(float $gt): static { - @trigger_error('Use minimum('.$this->varExport($gt).', true) instead', E_USER_DEPRECATED); + @trigger_error('Use exclusiveMinimum('.$this->varExport($gt).') instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($gt) { if ($float > $gt) { @@ -124,11 +164,11 @@ public function gt(float $gt): static } /** - * @deprecated Use maximum($lt, true) instead + * @deprecated Use exclusiveMaximum($lt) instead */ public function lt(float $lt): static { - @trigger_error('Use maximum('.$this->varExport($lt).', true) instead', E_USER_DEPRECATED); + @trigger_error('Use exclusiveMaximum('.$this->varExport($lt).') instead', E_USER_DEPRECATED); return $this->postParse(static function (float $float) use ($lt) { if ($float < $lt) { diff --git a/src/Schema/IntSchema.php b/src/Schema/IntSchema.php index 459bcac..6373eac 100644 --- a/src/Schema/IntSchema.php +++ b/src/Schema/IntSchema.php @@ -13,10 +13,16 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac public const string ERROR_TYPE_TEMPLATE = 'Type should be "int", {{given}} given'; public const string ERROR_MINIMUM_CODE = 'int.minimum'; - public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given'; + public const string ERROR_MINIMUM_TEMPLATE = 'Value should be minimum {{minimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MINIMUM_CODE = 'int.exclusiveMinimum'; + public const string ERROR_EXCLUSIVE_MINIMUM_TEMPLATE = 'Value should be greater than {{exclusiveMinimum}}, {{given}} given'; + + public const string ERROR_EXCLUSIVE_MAXIMUM_CODE = 'int.exclusiveMaximum'; + public const string ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE = 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given'; public const string ERROR_MAXIMUM_CODE = 'int.maximum'; - public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given'; + public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}}, {{given}} given'; /** @deprecated: see ERROR_MINIMUM_CODE */ public const string ERROR_GTE_CODE = 'int.gte'; @@ -24,16 +30,16 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ public const string ERROR_GTE_TEMPLATE = 'Value should be greater than or equal {{gte}}, {{given}} given'; - /** @deprecated: see ERROR_MINIMUM_CODE */ + /** @deprecated: see ERROR_EXCLUSIVE_MINIMUM_CODE */ public const string ERROR_GT_CODE = 'int.gt'; - /** @deprecated: see ERROR_MINIMUM_TEMPLATE */ + /** @deprecated: see ERROR_EXCLUSIVE_MINIMUM_TEMPLATE */ public const string ERROR_GT_TEMPLATE = 'Value should be greater than {{gt}}, {{given}} given'; - /** @deprecated: see ERROR_MAXIMUM_CODE */ + /** @deprecated: see ERROR_EXCLUSIVE_MAXIMUM_CODE */ public const string ERROR_LT_CODE = 'int.lt'; - /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ + /** @deprecated: see ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE */ public const string ERROR_LT_TEMPLATE = 'Value should be lesser than {{lt}}, {{given}} given'; /** @deprecated: see ERROR_MAXIMUM_CODE */ @@ -42,10 +48,10 @@ final class IntSchema extends AbstractSchemaInnerParse implements SchemaInterfac /** @deprecated: see ERROR_MAXIMUM_TEMPLATE */ public const string ERROR_LTE_TEMPLATE = 'Value should be lesser than or equal {{lte}}, {{given}} given'; - public function minimum(int $minimum, bool $exclusiveMinimum = false): static + public function minimum(int $minimum): static { - return $this->postParse(static function (int $int) use ($minimum, $exclusiveMinimum) { - if ((!$exclusiveMinimum && $int >= $minimum) || ($exclusiveMinimum && $int > $minimum)) { + return $this->postParse(static function (int $int) use ($minimum) { + if ($int >= $minimum) { return $int; } @@ -53,16 +59,50 @@ public function minimum(int $minimum, bool $exclusiveMinimum = false): static new Error( self::ERROR_MINIMUM_CODE, self::ERROR_MINIMUM_TEMPLATE, - ['minimum' => $minimum, 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $int] + ['minimum' => $minimum, 'given' => $int] + ) + ); + }); + } + + public function exclusiveMinimum(int $exclusiveMinimum): static + { + return $this->postParse(static function (int $int) use ($exclusiveMinimum) { + if ($int > $exclusiveMinimum) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MINIMUM_CODE, + self::ERROR_EXCLUSIVE_MINIMUM_TEMPLATE, + ['exclusiveMinimum' => $exclusiveMinimum, 'given' => $int] + ) + ); + }); + } + + public function exclusiveMaximum(int $exclusiveMaximum): static + { + return $this->postParse(static function (int $int) use ($exclusiveMaximum) { + if ($int < $exclusiveMaximum) { + return $int; + } + + throw new ErrorsException( + new Error( + self::ERROR_EXCLUSIVE_MAXIMUM_CODE, + self::ERROR_EXCLUSIVE_MAXIMUM_TEMPLATE, + ['exclusiveMaximum' => $exclusiveMaximum, 'given' => $int] ) ); }); } - public function maximum(int $maximum, bool $exclusiveMaximum = false): static + public function maximum(int $maximum): static { - return $this->postParse(static function (int $int) use ($maximum, $exclusiveMaximum) { - if ((!$exclusiveMaximum && $int <= $maximum) || ($exclusiveMaximum && $int < $maximum)) { + return $this->postParse(static function (int $int) use ($maximum) { + if ($int <= $maximum) { return $int; } @@ -70,7 +110,7 @@ public function maximum(int $maximum, bool $exclusiveMaximum = false): static new Error( self::ERROR_MAXIMUM_CODE, self::ERROR_MAXIMUM_TEMPLATE, - ['maximum' => $maximum, 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $int] + ['maximum' => $maximum, 'given' => $int] ) ); }); @@ -99,11 +139,11 @@ public function gte(int $gte): static } /** - * @deprecated Use minimum($gt, true) instead + * @deprecated Use exclusiveMinimum($gt) instead */ public function gt(int $gt): static { - @trigger_error('Use minimum('.$this->varExport($gt).', true) instead', E_USER_DEPRECATED); + @trigger_error('Use exclusiveMinimum('.$this->varExport($gt).') instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($gt) { if ($int > $gt) { @@ -121,11 +161,11 @@ public function gt(int $gt): static } /** - * @deprecated Use maximum($lt, true) instead + * @deprecated Use exclusiveMaximum($lt) instead */ public function lt(int $lt): static { - @trigger_error('Use maximum('.$this->varExport($lt).', true) instead', E_USER_DEPRECATED); + @trigger_error('Use exclusiveMaximum('.$this->varExport($lt).') instead', E_USER_DEPRECATED); return $this->postParse(static function (int $int) use ($lt) { if ($int < $lt) { diff --git a/tests/Unit/Schema/FloatSchemaTest.php b/tests/Unit/Schema/FloatSchemaTest.php index 79187ef..22dcd23 100644 --- a/tests/Unit/Schema/FloatSchemaTest.php +++ b/tests/Unit/Schema/FloatSchemaTest.php @@ -25,6 +25,10 @@ public function testImmutability(): void self::assertNotSame($schema, $schema->preParse(static fn (mixed $input) => $input)); self::assertNotSame($schema, $schema->postParse(static fn (float $output) => $output)); self::assertNotSame($schema, $schema->catch(static fn (float $output, ErrorsException $e) => $output)); + self::assertNotSame($schema, $schema->minimum(0.0)); + self::assertNotSame($schema, $schema->exclusiveMinimum(0.0)); + self::assertNotSame($schema, $schema->exclusiveMaximum(0.0)); + self::assertNotSame($schema, $schema->maximum(0.0)); } public function testParseSuccess(): void @@ -176,10 +180,9 @@ public function testParseWithInvalidMinimum(): void 'path' => '', 'error' => [ 'code' => 'float.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'template' => 'Value should be minimum {{minimum}}, {{given}} given', 'variables' => [ 'minimum' => $minimum, - 'exclusiveMinimum' => false, 'given' => $input, ], ], @@ -191,9 +194,9 @@ public function testParseWithInvalidMinimum(): void public function testParseWithValidExclusiveMinimum(): void { $input = 4.2; - $minimum = 4.1; + $exclusiveMinimum = 4.1; - $schema = (new FloatSchema())->minimum($minimum, true); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); self::assertSame($input, $schema->parse($input)); } @@ -201,9 +204,9 @@ public function testParseWithValidExclusiveMinimum(): void public function testParseWithInvalidExclusiveMinimumEqual(): void { $input = 4.1; - $minimum = 4.1; + $exclusiveMinimum = 4.1; - $schema = (new FloatSchema())->minimum($minimum, true); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -215,11 +218,10 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'float.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'code' => 'float.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'minimum' => $minimum, - 'exclusiveMinimum' => true, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -233,9 +235,9 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void public function testParseWithInvalidExclusiveMinimumLesser(): void { $input = 4.1; - $minimum = 4.2; + $exclusiveMinimum = 4.2; - $schema = (new FloatSchema())->minimum($minimum, true); + $schema = (new FloatSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -247,11 +249,10 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'float.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'code' => 'float.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'minimum' => $minimum, - 'exclusiveMinimum' => true, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -265,9 +266,9 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void public function testParseWithValidExclusiveMaximum(): void { $input = 4.1; - $maximum = 4.2; + $exclusiveMaximum = 4.2; - $schema = (new FloatSchema())->maximum($maximum, true); + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); self::assertSame($input, $schema->parse($input)); } @@ -275,9 +276,9 @@ public function testParseWithValidExclusiveMaximum(): void public function testParseWithInvalidExclusiveMaximumEqual(): void { $input = 4.1; - $maximum = 4.1; + $exclusiveMaximum = 4.1; - $schema = (new FloatSchema())->maximum($maximum, true); + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -288,11 +289,10 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'float.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'code' => 'float.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'maximum' => $maximum, - 'exclusiveMaximum' => true, + 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $input, ], ], @@ -304,9 +304,9 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void public function testParseWithInvalidExclusiveMaximumLesser(): void { $input = 4.2; - $maximum = 4.1; + $exclusiveMaximum = 4.1; - $schema = (new FloatSchema())->maximum($maximum, true); + $schema = (new FloatSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -317,11 +317,10 @@ public function testParseWithInvalidExclusiveMaximumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'float.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'code' => 'float.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'maximum' => $maximum, - 'exclusiveMaximum' => true, + 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $input, ], ], @@ -357,10 +356,9 @@ public function testParseWithInvalidMaximum(): void 'path' => '', 'error' => [ 'code' => 'float.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'template' => 'Value should be maximum {{maximum}}, {{given}} given', 'variables' => [ 'maximum' => $maximum, - 'exclusiveMaximum' => false, 'given' => $input, ], ], @@ -432,7 +430,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum(4.1, true) instead', $lastError['message']); + self::assertSame('Use exclusiveMinimum(4.1) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -514,7 +512,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum(4.2, true) instead', $lastError['message']); + self::assertSame('Use exclusiveMaximum(4.2) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/IntSchemaTest.php b/tests/Unit/Schema/IntSchemaTest.php index 038e3e4..0baeb6f 100644 --- a/tests/Unit/Schema/IntSchemaTest.php +++ b/tests/Unit/Schema/IntSchemaTest.php @@ -25,6 +25,10 @@ public function testImmutability(): void self::assertNotSame($schema, $schema->preParse(static fn (mixed $input) => $input)); self::assertNotSame($schema, $schema->postParse(static fn (int $output) => $output)); self::assertNotSame($schema, $schema->catch(static fn (int $output, ErrorsException $e) => $output)); + self::assertNotSame($schema, $schema->minimum(0)); + self::assertNotSame($schema, $schema->exclusiveMinimum(0)); + self::assertNotSame($schema, $schema->exclusiveMaximum(0)); + self::assertNotSame($schema, $schema->maximum(0)); } public function testParseSuccess(): void @@ -175,10 +179,9 @@ public function testParseWithInvalidMinimum(): void 'path' => '', 'error' => [ 'code' => 'int.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'template' => 'Value should be minimum {{minimum}}, {{given}} given', 'variables' => [ 'minimum' => $minimum, - 'exclusiveMinimum' => false, 'given' => $input, ], ], @@ -190,9 +193,9 @@ public function testParseWithInvalidMinimum(): void public function testParseWithValidExclusiveMinimum(): void { $input = 5; - $minimum = 4; + $exclusiveMinimum = 4; - $schema = (new IntSchema())->minimum($minimum, true); + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); self::assertSame($input, $schema->parse($input)); } @@ -200,9 +203,9 @@ public function testParseWithValidExclusiveMinimum(): void public function testParseWithInvalidExclusiveMinimumEqual(): void { $input = 4; - $minimum = 4; + $exclusiveMinimum = 4; - $schema = (new IntSchema())->minimum($minimum, true); + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -214,11 +217,10 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'int.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'code' => 'int.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'minimum' => $minimum, - 'exclusiveMinimum' => true, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -232,9 +234,9 @@ public function testParseWithInvalidExclusiveMinimumEqual(): void public function testParseWithInvalidExclusiveMinimumLesser(): void { $input = 4; - $minimum = 5; + $exclusiveMinimum = 5; - $schema = (new IntSchema())->minimum($minimum, true); + $schema = (new IntSchema())->exclusiveMinimum($exclusiveMinimum); try { $schema->parse($input); @@ -246,11 +248,10 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'int.minimum', - 'template' => 'Value should be minimum {{minimum}} {{exclusiveMinimum}}, {{given}} given', + 'code' => 'int.exclusiveMinimum', + 'template' => 'Value should be greater than {{exclusiveMinimum}}, {{given}} given', 'variables' => [ - 'minimum' => $minimum, - 'exclusiveMinimum' => true, + 'exclusiveMinimum' => $exclusiveMinimum, 'given' => $input, ], ], @@ -264,9 +265,9 @@ public function testParseWithInvalidExclusiveMinimumLesser(): void public function testParseWithValidExclusiveMaximum(): void { $input = 4; - $maximum = 5; + $exclusiveMaximum = 5; - $schema = (new IntSchema())->maximum($maximum, true); + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); self::assertSame($input, $schema->parse($input)); } @@ -274,9 +275,9 @@ public function testParseWithValidExclusiveMaximum(): void public function testParseWithInvalidExclusiveMaximumEqual(): void { $input = 5; - $maximum = 5; + $exclusiveMaximum = 5; - $schema = (new IntSchema())->maximum($maximum, true); + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -287,11 +288,10 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void [ 'path' => '', 'error' => [ - 'code' => 'int.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'code' => 'int.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'maximum' => $maximum, - 'exclusiveMaximum' => true, + 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $input, ], ], @@ -303,9 +303,9 @@ public function testParseWithInvalidExclusiveMaximumEqual(): void public function testParseWithInvalidExclusiveMaximumLesser(): void { $input = 5; - $maximum = 4; + $exclusiveMaximum = 4; - $schema = (new IntSchema())->maximum($maximum, true); + $schema = (new IntSchema())->exclusiveMaximum($exclusiveMaximum); try { $schema->parse($input); @@ -316,11 +316,10 @@ public function testParseWithInvalidExclusiveMaximumLesser(): void [ 'path' => '', 'error' => [ - 'code' => 'int.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'code' => 'int.exclusiveMaximum', + 'template' => 'Value should be lesser than {{exclusiveMaximum}}, {{given}} given', 'variables' => [ - 'maximum' => $maximum, - 'exclusiveMaximum' => true, + 'exclusiveMaximum' => $exclusiveMaximum, 'given' => $input, ], ], @@ -356,10 +355,9 @@ public function testParseWithInvalidMaximum(): void 'path' => '', 'error' => [ 'code' => 'int.maximum', - 'template' => 'Value should be maximum {{maximum}} {{exclusiveMaximum}}, {{given}} given', + 'template' => 'Value should be maximum {{maximum}}, {{given}} given', 'variables' => [ 'maximum' => $maximum, - 'exclusiveMaximum' => false, 'given' => $input, ], ], @@ -431,7 +429,7 @@ public function testParseWithValidGt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use minimum(4, true) instead', $lastError['message']); + self::assertSame('Use exclusiveMinimum(4) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } @@ -507,7 +505,7 @@ public function testParseWithValidLt(): void self::assertArrayHasKey('type', $lastError); self::assertSame(E_USER_DEPRECATED, $lastError['type']); self::assertArrayHasKey('message', $lastError); - self::assertSame('Use maximum(5, true) instead', $lastError['message']); + self::assertSame('Use exclusiveMaximum(5) instead', $lastError['message']); self::assertSame($input, $schema->parse($input)); } diff --git a/tests/Unit/Schema/StringSchemaTest.php b/tests/Unit/Schema/StringSchemaTest.php index ba5721f..a205be2 100644 --- a/tests/Unit/Schema/StringSchemaTest.php +++ b/tests/Unit/Schema/StringSchemaTest.php @@ -1257,7 +1257,7 @@ public function testParseWithValidtoFloat(): void { $input = '4.2'; - $schema = (new StringSchema())->toFloat()->minimum(4.0, true); + $schema = (new StringSchema())->toFloat()->exclusiveMinimum(4.0); self::assertSame((float) $input, $schema->parse($input)); } @@ -1299,7 +1299,7 @@ public function testParseWithValidtoInt(): void { $input = '42'; - $schema = (new StringSchema())->toInt()->minimum(40, true); + $schema = (new StringSchema())->toInt()->exclusiveMinimum(40); self::assertSame((int) $input, $schema->parse($input)); } From 38b4cf9260a9b4c87f00092ba542bc4ff4b0b9e1 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Thu, 12 Feb 2026 17:51:30 +0100 Subject: [PATCH 7/9] json-schema-code-generator --- src/JsonSchemaCodeGenerator.php | 631 +++++++++++++++ tests/Unit/JsonSchemaCodeGeneratorTest.php | 855 +++++++++++++++++++++ 2 files changed, 1486 insertions(+) create mode 100644 src/JsonSchemaCodeGenerator.php create mode 100644 tests/Unit/JsonSchemaCodeGeneratorTest.php diff --git a/src/JsonSchemaCodeGenerator.php b/src/JsonSchemaCodeGenerator.php new file mode 100644 index 0000000..d6fd254 --- /dev/null +++ b/src/JsonSchemaCodeGenerator.php @@ -0,0 +1,631 @@ +|string $jsonSchema JSON Schema as an associative array or JSON string + * + * @throws \InvalidArgumentException if the schema is invalid + */ + public function generate(array|string $jsonSchema): string + { + if (\is_string($jsonSchema)) { + $decoded = json_decode($jsonSchema, true); + + if (!\is_array($decoded)) { + throw new \InvalidArgumentException('Invalid JSON string provided'); + } + + /** @var array $decoded */ + $jsonSchema = $decoded; + } + + return $this->generateSchema($jsonSchema); + } + + /** + * @param array $schema + */ + private function generateSchema(array $schema): string + { + if (isset($schema['$ref'])) { + throw new \InvalidArgumentException('$ref is not supported. Please dereference the schema first.'); + } + + // Handle const/enum as literal + if (\array_key_exists('const', $schema)) { + return $this->generateConst($schema); + } + + if (isset($schema['enum'])) { + return $this->generateEnum($schema); + } + + // Handle oneOf / anyOf as union + if (isset($schema['oneOf']) && \is_array($schema['oneOf'])) { + /** @var array> $oneOf */ + $oneOf = $schema['oneOf']; + + return $this->generateUnion($oneOf, $schema); + } + + if (isset($schema['anyOf']) && \is_array($schema['anyOf'])) { + /** @var array> $anyOf */ + $anyOf = $schema['anyOf']; + + return $this->generateUnion($anyOf, $schema); + } + + // Handle allOf by merging into a single object schema + if (isset($schema['allOf']) && \is_array($schema['allOf'])) { + /** @var array> $allOf */ + $allOf = $schema['allOf']; + + return $this->generateAllOf($allOf, $schema); + } + + $type = $schema['type'] ?? null; + + // Handle type as array (e.g., ["string", "null"]) + if (\is_array($type)) { + /** @var array $type */ + return $this->generateMultiType($type, $schema); + } + + if (null !== $type && !\is_string($type)) { + throw new \InvalidArgumentException('Invalid type value in schema'); + } + + return match ($type) { + 'string' => $this->generateString($schema), + 'integer' => $this->generateInteger($schema), + 'number' => $this->generateNumber($schema), + 'boolean' => $this->generateBoolean($schema), + 'array' => $this->generateArray($schema), + 'object' => $this->generateObject($schema), + 'null' => $this->generateNull($schema), + null => $this->generateFallback($schema), + default => throw new \InvalidArgumentException(\sprintf('Unsupported JSON Schema type: %s', $type)), + }; + } + + /** + * @param array $schema + */ + private function generateConst(array $schema): string + { + return '$p->literal('.$this->exportValue($schema['const']).')'; + } + + /** + * @param array $schema + */ + private function generateEnum(array $schema): string + { + $values = $schema['enum']; + + if (!\is_array($values) || 0 === \count($values)) { + throw new \InvalidArgumentException('Enum must be a non-empty array'); + } + + $hasNull = \in_array(null, $values, true); + + /** @var array $nonNullValues */ + $nonNullValues = array_values(array_filter($values, static fn (mixed $v): bool => null !== $v)); + + if (0 === \count($nonNullValues)) { + throw new \InvalidArgumentException('Enum must have at leas one non null value'); + } + + if (1 === \count($nonNullValues)) { + $code = '$p->literal('.$this->exportValue($nonNullValues[0]).')'; + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + // Multiple values: union of literals + $literals = array_map( + fn (mixed $v): string => '$p->literal('.$this->exportValue($v).')', + $nonNullValues, + ); + + $code = '$p->union(['.implode(', ', $literals).'])'; + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + /** + * @param array> $schemas + * @param array $parentSchema + */ + private function generateUnion(array $schemas, array $parentSchema): string + { + // Check if this is a discriminated union (all objects with a common discriminator field) + $discriminator = $this->detectDiscriminator($schemas); + + if (null !== $discriminator) { + return $this->generateDiscriminatedUnion($schemas, $discriminator, $parentSchema); + } + + // Filter out null type schemas + $hasNull = false; + + /** @var array> $nonNullSchemas */ + $nonNullSchemas = []; + + foreach ($schemas as $subSchema) { + if (isset($subSchema['type']) && 'null' === $subSchema['type']) { + $hasNull = true; + } else { + $nonNullSchemas[] = $subSchema; + } + } + + if (1 === \count($nonNullSchemas)) { + $code = $this->generateSchema($nonNullSchemas[0]); + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + $subCodes = array_map(fn (array $s): string => $this->generateSchema($s), $nonNullSchemas); + + $code = '$p->union(['.implode(', ', $subCodes).'])'; + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + /** + * @param array> $schemas + * + * @return null|string the discriminator field name, or null + */ + private function detectDiscriminator(array $schemas): ?string + { + if (\count($schemas) < 2) { + return null; + } + + // All schemas must be objects + foreach ($schemas as $schema) { + $type = $schema['type'] ?? null; + + if ('object' !== $type) { + return null; + } + } + + // Find common required fields with const or enum of one value + /** @var array $candidates */ + $candidates = []; + + foreach ($schemas as $schema) { + /** @var array> $properties */ + $properties = isset($schema['properties']) && \is_array($schema['properties']) + ? $schema['properties'] + : []; + + /** @var array $required */ + $required = isset($schema['required']) && \is_array($schema['required']) + ? $schema['required'] + : []; + + foreach ($required as $fieldName) { + if (!isset($properties[$fieldName])) { + continue; + } + + /** @var array $fieldSchema */ + $fieldSchema = $properties[$fieldName]; + $isDiscriminator = false; + + if (\array_key_exists('const', $fieldSchema)) { + $isDiscriminator = true; + } elseif (isset($fieldSchema['enum']) && \is_array($fieldSchema['enum']) && 1 === \count($fieldSchema['enum'])) { + $isDiscriminator = true; + } + + if ($isDiscriminator) { + if (!isset($candidates[$fieldName])) { + $candidates[$fieldName] = 0; + } + + ++$candidates[$fieldName]; + } + } + } + + // The discriminator field must appear in ALL schemas + foreach ($candidates as $fieldName => $count) { + if ($count === \count($schemas)) { + return (string) $fieldName; + } + } + + return null; + } + + /** + * @param array> $schemas + * @param array $parentSchema + */ + private function generateDiscriminatedUnion(array $schemas, string $discriminator, array $parentSchema): string + { + $objectCodes = array_map(fn (array $s): string => $this->generateObject($s), $schemas); + + return '$p->discriminatedUnion(['.implode(', ', $objectCodes).'], ' + .$this->exportValue($discriminator).')'; + } + + /** + * @param array> $schemas + * @param array $parentSchema + */ + private function generateAllOf(array $schemas, array $parentSchema): string + { + // Merge all schemas into one object schema + /** @var array> $mergedProperties */ + $mergedProperties = []; + + /** @var array $mergedRequired */ + $mergedRequired = []; + + foreach ($schemas as $subSchema) { + if (isset($subSchema['properties']) && \is_array($subSchema['properties'])) { + /** @var array> $props */ + $props = $subSchema['properties']; + $mergedProperties = array_merge($mergedProperties, $props); + } + + if (isset($subSchema['required']) && \is_array($subSchema['required'])) { + /** @var array $req */ + $req = $subSchema['required']; + $mergedRequired = array_merge($mergedRequired, $req); + } + } + + /** @var array $merged */ + $merged = [ + 'type' => 'object', + 'properties' => $mergedProperties, + 'required' => array_values(array_unique($mergedRequired)), + ]; + + // Carry over additionalProperties if set on parent + if (\array_key_exists('additionalProperties', $parentSchema)) { + $merged['additionalProperties'] = $parentSchema['additionalProperties']; + } + + return $this->generateObject($merged); + } + + /** + * @param array $types + * @param array $schema + */ + private function generateMultiType(array $types, array $schema): string + { + $hasNull = \in_array('null', $types, true); + $nonNullTypes = array_values(array_filter($types, static fn (string $t): bool => 'null' !== $t)); + + if (1 === \count($nonNullTypes)) { + $singleSchema = $schema; + $singleSchema['type'] = $nonNullTypes[0]; + $code = $this->generateSchema($singleSchema); + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + // Multiple non-null types: union + $subCodes = []; + + foreach ($nonNullTypes as $t) { + $subSchema = $schema; + $subSchema['type'] = $t; + $subCodes[] = $this->generateSchema($subSchema); + } + + $code = '$p->union(['.implode(', ', $subCodes).'])'; + + if ($hasNull) { + $code .= '->nullable()'; + } + + return $code; + } + + /** + * @param array $schema + */ + private function generateString(array $schema): string + { + $code = '$p->string()'; + + if (isset($schema['minLength']) && (\is_int($schema['minLength']) || \is_float($schema['minLength']))) { + $code .= '->minLength('.(int) $schema['minLength'].')'; + } + + if (isset($schema['maxLength']) && (\is_int($schema['maxLength']) || \is_float($schema['maxLength']))) { + $code .= '->maxLength('.(int) $schema['maxLength'].')'; + } + + if (isset($schema['pattern']) && \is_string($schema['pattern'])) { + $code .= '->regexp('.$this->exportValue('/'.$schema['pattern'].'/').')'; + } + + if (isset($schema['format']) && \is_string($schema['format'])) { + $code = $this->applyStringFormat($code, $schema['format']); + } + + return $code; + } + + private function applyStringFormat(string $code, string $format): string + { + return match ($format) { + 'email' => $code.'->email()', + 'uri', 'url' => $code.'->url()', + 'ipv4' => $code.'->ipV4()', + 'ipv6' => $code.'->ipV6()', + 'uuid' => $code.'->uuid()', + 'hostname' => $code.'->domain()', + 'date-time' => $code.'->toDateTime()', + default => $code.' /* unsupported format: '.$format.' */', + }; + } + + /** + * @param array $schema + */ + private function generateInteger(array $schema): string + { + $code = '$p->int()'; + + return $this->applyNumericConstraints($code, $schema, true); + } + + /** + * @param array $schema + */ + private function generateNumber(array $schema): string + { + $code = '$p->float()'; + + return $this->applyNumericConstraints($code, $schema, false); + } + + /** + * @param array $schema + */ + private function applyNumericConstraints(string $code, array $schema, bool $isInt): string + { + if (isset($schema['minimum']) && (\is_int($schema['minimum']) || \is_float($schema['minimum']))) { + $val = $isInt ? (int) $schema['minimum'] : (float) $schema['minimum']; + $code .= '->gte('.$this->exportValue($val).')'; + } + + if (isset($schema['maximum']) && (\is_int($schema['maximum']) || \is_float($schema['maximum']))) { + $val = $isInt ? (int) $schema['maximum'] : (float) $schema['maximum']; + $code .= '->lte('.$this->exportValue($val).')'; + } + + if (isset($schema['exclusiveMinimum']) && (\is_int($schema['exclusiveMinimum']) || \is_float($schema['exclusiveMinimum']))) { + $val = $isInt ? (int) $schema['exclusiveMinimum'] : (float) $schema['exclusiveMinimum']; + $code .= '->gt('.$this->exportValue($val).')'; + } + + if (isset($schema['exclusiveMaximum']) && (\is_int($schema['exclusiveMaximum']) || \is_float($schema['exclusiveMaximum']))) { + $val = $isInt ? (int) $schema['exclusiveMaximum'] : (float) $schema['exclusiveMaximum']; + $code .= '->lt('.$this->exportValue($val).')'; + } + + return $code; + } + + /** + * @param array $schema + */ + private function generateBoolean(array $schema): string + { + return '$p->bool()'; + } + + /** + * @param array $schema + */ + private function generateArray(array $schema): string + { + if (isset($schema['prefixItems']) && \is_array($schema['prefixItems'])) { + return $this->generateTuple($schema); + } + + if (isset($schema['items']) && \is_array($schema['items']) && [] !== $schema['items'] && array_is_list($schema['items'])) { + return $this->generateTuple($schema); + } + + /** @var array $itemSchema */ + $itemSchema = isset($schema['items']) && \is_array($schema['items']) ? $schema['items'] : []; + + if ([] === $itemSchema) { + // No items schema defined: default to string + $itemCode = '$p->string()'; + } else { + $itemCode = $this->generateSchema($itemSchema); + } + + $code = '$p->array('.$itemCode.')'; + + if (isset($schema['minItems']) && (\is_int($schema['minItems']) || \is_float($schema['minItems']))) { + $code .= '->minLength('.(int) $schema['minItems'].')'; + } + + if (isset($schema['maxItems']) && (\is_int($schema['maxItems']) || \is_float($schema['maxItems']))) { + $code .= '->maxLength('.(int) $schema['maxItems'].')'; + } + + return $code; + } + + /** + * @param array $schema + */ + private function generateTuple(array $schema): string + { + /** @var array> $items */ + $items = isset($schema['prefixItems']) && \is_array($schema['prefixItems']) + ? $schema['prefixItems'] + : (\is_array($schema['items'] ?? null) ? $schema['items'] : []); + + $itemCodes = array_map(fn (array $item): string => $this->generateSchema($item), $items); + + return '$p->tuple(['.implode(', ', $itemCodes).'])'; + } + + /** + * @param array $schema + */ + private function generateObject(array $schema): string + { + /** @var array> $properties */ + $properties = isset($schema['properties']) && \is_array($schema['properties']) + ? $schema['properties'] + : []; + + /** @var array $required */ + $required = isset($schema['required']) && \is_array($schema['required']) + ? $schema['required'] + : []; + + $additionalProperties = $schema['additionalProperties'] ?? null; + + // If no properties defined but additionalProperties has a schema, use record + if ([] === $properties && \is_array($additionalProperties)) { + /** @var array $additionalPropertiesSchema */ + $additionalPropertiesSchema = $additionalProperties; + + return '$p->record('.$this->generateSchema($additionalPropertiesSchema).')'; + } + + if ([] === $properties && (true === $additionalProperties || null === $additionalProperties)) { + return '$p->record($p->string())'; + } + + $fieldCodes = []; + + foreach ($properties as $fieldName => $fieldSchema) { + $fieldCode = $this->generateSchema($fieldSchema); + + if (!\in_array($fieldName, $required, true)) { + $fieldCode .= '->nullable()'; + } + + $fieldCodes[] = $this->exportValue($fieldName).' => '.$fieldCode; + } + + $code = '$p->object(['.implode(', ', $fieldCodes).'])'; + + // Build list of optional (non-required) fields + /** @var array $optionalFields */ + $optionalFields = []; + + foreach (array_keys($properties) as $fieldName) { + if (!\in_array($fieldName, $required, true)) { + $optionalFields[] = $fieldName; + } + } + + if ([] !== $optionalFields) { + $exportedOptionals = array_map(fn (string $f): string => $this->exportValue($f), $optionalFields); + $code .= '->optional(['.implode(', ', $exportedOptionals).'])'; + } + + if (false === $additionalProperties) { + $code .= '->strict()'; + } + + return $code; + } + + /** + * @param array $schema + */ + private function generateNull(array $schema): string + { + return '$p->string()->nullable()->default(null)'; + } + + /** + * Fallback when no type is specified and no composition keywords are found. + * + * @param array $schema + */ + private function generateFallback(array $schema): string + { + // If there are properties, treat as object + if (isset($schema['properties'])) { + $schema['type'] = 'object'; + + return $this->generateObject($schema); + } + + // Default: accept any string + return '$p->string()'; + } + + private function exportValue(mixed $value): string + { + if (\is_string($value)) { + return "'".addcslashes($value, "'\\")."'"; + } + + if (\is_int($value)) { + return (string) $value; + } + + if (\is_float($value)) { + $str = (string) $value; + + // Ensure float representation + if (!str_contains($str, '.') && !str_contains($str, 'E') && !str_contains($str, 'e')) { + $str .= '.0'; + } + + return $str; + } + + if (\is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (null === $value) { + return 'null'; + } + + return var_export($value, true); + } +} diff --git a/tests/Unit/JsonSchemaCodeGeneratorTest.php b/tests/Unit/JsonSchemaCodeGeneratorTest.php new file mode 100644 index 0000000..78a892f --- /dev/null +++ b/tests/Unit/JsonSchemaCodeGeneratorTest.php @@ -0,0 +1,855 @@ +generate(['type' => 'string']); + + self::assertSame('$p->string()', $code); + } + + public function testStringWithMinLength(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'minLength' => 3]); + + self::assertSame('$p->string()->minLength(3)', $code); + } + + public function testStringWithMaxLength(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'maxLength' => 100]); + + self::assertSame('$p->string()->maxLength(100)', $code); + } + + public function testStringWithMinAndMaxLength(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'minLength' => 1, 'maxLength' => 255]); + + self::assertSame('$p->string()->minLength(1)->maxLength(255)', $code); + } + + public function testStringWithPattern(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'pattern' => '^[a-z]+$']); + + self::assertSame("\$p->string()->regexp('/^[a-z]+$/')", $code); + } + + public function testStringWithEmailFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'email']); + + self::assertSame('$p->string()->email()', $code); + } + + public function testStringWithUriFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'uri']); + + self::assertSame('$p->string()->url()', $code); + } + + public function testStringWithIpv4Format(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'ipv4']); + + self::assertSame('$p->string()->ipV4()', $code); + } + + public function testStringWithIpv6Format(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'ipv6']); + + self::assertSame('$p->string()->ipV6()', $code); + } + + public function testStringWithUuidFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'uuid']); + + self::assertSame('$p->string()->uuid()', $code); + } + + public function testStringWithHostnameFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'hostname']); + + self::assertSame('$p->string()->domain()', $code); + } + + public function testStringWithDateTimeFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'date-time']); + + self::assertSame('$p->string()->toDateTime()', $code); + } + + public function testStringWithUnsupportedFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'binary']); + + self::assertSame('$p->string() /* unsupported format: binary */', $code); + } + + public function testInteger(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer']); + + self::assertSame('$p->int()', $code); + } + + public function testIntegerWithMinimum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'minimum' => 0]); + + self::assertSame('$p->int()->gte(0)', $code); + } + + public function testIntegerWithMaximum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'maximum' => 100]); + + self::assertSame('$p->int()->lte(100)', $code); + } + + public function testIntegerWithExclusiveMinimum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'exclusiveMinimum' => 0]); + + self::assertSame('$p->int()->gt(0)', $code); + } + + public function testIntegerWithExclusiveMaximum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'exclusiveMaximum' => 100]); + + self::assertSame('$p->int()->lt(100)', $code); + } + + public function testIntegerWithAllConstraints(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'integer', + 'minimum' => 1, + 'maximum' => 99, + ]); + + self::assertSame('$p->int()->gte(1)->lte(99)', $code); + } + + public function testNumber(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number']); + + self::assertSame('$p->float()', $code); + } + + public function testNumberWithMinimum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'minimum' => 0.5]); + + self::assertSame('$p->float()->gte(0.5)', $code); + } + + public function testNumberWithMaximum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'maximum' => 99.9]); + + self::assertSame('$p->float()->lte(99.9)', $code); + } + + public function testNumberWithExclusiveMinimum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'exclusiveMinimum' => 0.0]); + + self::assertSame('$p->float()->gt(0.0)', $code); + } + + public function testNumberWithExclusiveMaximum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'exclusiveMaximum' => 100.0]); + + self::assertSame('$p->float()->lt(100.0)', $code); + } + + public function testBoolean(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'boolean']); + + self::assertSame('$p->bool()', $code); + } + + public function testNull(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'null']); + + self::assertSame('$p->string()->nullable()->default(null)', $code); + } + + public function testArrayBasic(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => ['type' => 'string'], + ]); + + self::assertSame('$p->array($p->string())', $code); + } + + public function testArrayOfIntegers(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => ['type' => 'integer'], + ]); + + self::assertSame('$p->array($p->int())', $code); + } + + public function testArrayWithMinItems(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'minItems' => 1, + ]); + + self::assertSame('$p->array($p->string())->minLength(1)', $code); + } + + public function testArrayWithMaxItems(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'maxItems' => 10, + ]); + + self::assertSame('$p->array($p->string())->maxLength(10)', $code); + } + + public function testArrayWithMinAndMaxItems(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'minItems' => 1, + 'maxItems' => 10, + ]); + + self::assertSame('$p->array($p->string())->minLength(1)->maxLength(10)', $code); + } + + public function testArrayWithNoItems(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'array']); + + self::assertSame('$p->array($p->string())', $code); + } + + public function testArrayOfObjects(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ], + ]); + + self::assertSame("\$p->array(\$p->object(['name' => \$p->string()]))", $code); + } + + public function testTupleWithPrefixItems(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'prefixItems' => [ + ['type' => 'number'], + ['type' => 'number'], + ], + ]); + + self::assertSame('$p->tuple([$p->float(), $p->float()])', $code); + } + + public function testObjectBasic(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'age' => ['type' => 'integer'], + ], + 'required' => ['name', 'age'], + ]); + + self::assertSame("\$p->object(['name' => \$p->string(), 'age' => \$p->int()])", $code); + } + + public function testObjectWithOptionalFields(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'nickname' => ['type' => 'string'], + ], + 'required' => ['name'], + ]); + + self::assertSame( + "\$p->object(['name' => \$p->string(), 'nickname' => \$p->string()->nullable()])->optional(['nickname'])", + $code, + ); + } + + public function testObjectStrict(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + 'additionalProperties' => false, + ]); + + self::assertSame("\$p->object(['name' => \$p->string()])->strict()", $code); + } + + public function testObjectAsRecord(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'additionalProperties' => ['type' => 'string'], + ]); + + self::assertSame('$p->record($p->string())', $code); + } + + public function testObjectAsRecordWithIntValues(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'additionalProperties' => ['type' => 'integer'], + ]); + + self::assertSame('$p->record($p->int())', $code); + } + + public function testObjectNested(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'address' => [ + 'type' => 'object', + 'properties' => [ + 'street' => ['type' => 'string'], + 'city' => ['type' => 'string'], + ], + 'required' => ['street', 'city'], + ], + ], + 'required' => ['address'], + ]); + + self::assertSame( + "\$p->object(['address' => \$p->object(['street' => \$p->string(), 'city' => \$p->string()])])", + $code, + ); + } + + public function testConst(): void + { + $generator = new JsonSchemaCodeGenerator(); + + self::assertSame("\$p->literal('active')", $generator->generate(['const' => 'active'])); + self::assertSame('$p->literal(42)', $generator->generate(['const' => 42])); + self::assertSame('$p->literal(true)', $generator->generate(['const' => true])); + } + + public function testEnumStrings(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['enum' => ['active', 'inactive', 'pending']]); + + self::assertSame( + "\$p->union([\$p->literal('active'), \$p->literal('inactive'), \$p->literal('pending')])", + $code, + ); + } + + public function testEnumSingleValue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['enum' => ['only']]); + + self::assertSame("\$p->literal('only')", $code); + } + + public function testEnumWithNull(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['enum' => ['active', 'inactive', null]]); + + self::assertSame( + "\$p->union([\$p->literal('active'), \$p->literal('inactive')])->nullable()", + $code, + ); + } + + public function testEnumIntegers(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['enum' => [1, 2, 3]]); + + self::assertSame('$p->union([$p->literal(1), $p->literal(2), $p->literal(3)])', $code); + } + + public function testOneOfSimple(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'oneOf' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + + self::assertSame('$p->union([$p->string(), $p->int()])', $code); + } + + public function testOneOfWithNull(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'oneOf' => [ + ['type' => 'string'], + ['type' => 'null'], + ], + ]); + + self::assertSame('$p->string()->nullable()', $code); + } + + public function testAnyOfSimple(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'anyOf' => [ + ['type' => 'string'], + ['type' => 'integer'], + ], + ]); + + self::assertSame('$p->union([$p->string(), $p->int()])', $code); + } + + public function testNullableTypeArray(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => ['string', 'null']]); + + self::assertSame('$p->string()->nullable()', $code); + } + + public function testMultiType(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => ['string', 'integer']]); + + self::assertSame('$p->union([$p->string(), $p->int()])', $code); + } + + public function testMultiTypeWithNull(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => ['string', 'integer', 'null']]); + + self::assertSame('$p->union([$p->string(), $p->int()])->nullable()', $code); + } + + public function testAllOfMerge(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'allOf' => [ + [ + 'type' => 'object', + 'properties' => ['id' => ['type' => 'integer']], + 'required' => ['id'], + ], + [ + 'type' => 'object', + 'properties' => ['name' => ['type' => 'string']], + 'required' => ['name'], + ], + ], + ]); + + self::assertSame("\$p->object(['id' => \$p->int(), 'name' => \$p->string()])", $code); + } + + public function testDiscriminatedUnion(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'oneOf' => [ + [ + 'type' => 'object', + 'properties' => [ + 'type' => ['const' => 'email'], + 'address' => ['type' => 'string'], + ], + 'required' => ['type', 'address'], + ], + [ + 'type' => 'object', + 'properties' => [ + 'type' => ['const' => 'phone'], + 'number' => ['type' => 'string'], + ], + 'required' => ['type', 'number'], + ], + ], + ]); + + self::assertSame( + "\$p->discriminatedUnion([\$p->object(['type' => \$p->literal('email'), 'address' => \$p->string()]), " + ."\$p->object(['type' => \$p->literal('phone'), 'number' => \$p->string()])], 'type')", + $code, + ); + } + + public function testFromJsonString(): void + { + $generator = new JsonSchemaCodeGenerator(); + $json = '{"type": "string", "minLength": 1}'; + $code = $generator->generate($json); + + self::assertSame('$p->string()->minLength(1)', $code); + } + + public function testInvalidJsonString(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid JSON string provided'); + + $generator->generate('not valid json'); + } + + public function testUnsupportedType(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported JSON Schema type: foobar'); + + $generator->generate(['type' => 'foobar']); + } + + public function testRefThrowsException(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$ref is not supported'); + + $generator->generate(['$ref' => '#/definitions/Foo']); + } + + public function testEmptyEnumThrowsException(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Enum must be a non-empty array'); + + $generator->generate(['enum' => []]); + } + + public function testComplexSchema(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer', 'minimum' => 1], + 'name' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 100], + 'email' => ['type' => 'string', 'format' => 'email'], + 'age' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 150], + 'tags' => [ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'minItems' => 1, + ], + 'metadata' => [ + 'type' => 'object', + 'additionalProperties' => ['type' => 'string'], + ], + ], + 'required' => ['id', 'name', 'email'], + 'additionalProperties' => false, + ]); + + self::assertSame( + "\$p->object(['id' => \$p->int()->gte(1), 'name' => \$p->string()->minLength(1)->maxLength(100), " + ."'email' => \$p->string()->email(), 'age' => \$p->int()->gte(0)->lte(150)->nullable(), " + ."'tags' => \$p->array(\$p->string())->minLength(1)->nullable(), 'metadata' => \$p->record(\$p->string())->nullable()])" + ."->optional(['age', 'tags', 'metadata'])->strict()", + $code, + ); + } + + public function testSchemaWithNoType(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([]); + + self::assertSame('$p->string()', $code); + } + + public function testSchemaWithNoTypeButProperties(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'properties' => [ + 'name' => ['type' => 'string'], + ], + 'required' => ['name'], + ]); + + self::assertSame("\$p->object(['name' => \$p->string()])", $code); + } + + public function testObjectEmptyPropertiesWithAdditionalPropertiesTrue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'additionalProperties' => true, + ]); + + self::assertSame('$p->record($p->string())', $code); + } + + public function testStringWithMultipleConstraints(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'string', + 'minLength' => 3, + 'maxLength' => 50, + 'format' => 'email', + ]); + + self::assertSame('$p->string()->minLength(3)->maxLength(50)->email()', $code); + } + + public function testNestedArraysOfObjects(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'array', + 'items' => [ + 'type' => 'array', + 'items' => ['type' => 'integer'], + ], + ]); + + self::assertSame('$p->array($p->array($p->int()))', $code); + } + + public function testDiscriminatedUnionWithEnum(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'oneOf' => [ + [ + 'type' => 'object', + 'properties' => [ + 'kind' => ['enum' => ['circle']], + 'radius' => ['type' => 'number'], + ], + 'required' => ['kind', 'radius'], + ], + [ + 'type' => 'object', + 'properties' => [ + 'kind' => ['enum' => ['square']], + 'side' => ['type' => 'number'], + ], + 'required' => ['kind', 'side'], + ], + ], + ]); + + self::assertSame( + "\$p->discriminatedUnion([\$p->object(['kind' => \$p->literal('circle'), 'radius' => \$p->float()]), " + ."\$p->object(['kind' => \$p->literal('square'), 'side' => \$p->float()])], 'kind')", + $code, + ); + } + + public function testNullableTypeArrayWithConstraints(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => ['string', 'null'], + 'minLength' => 1, + ]); + + self::assertSame('$p->string()->minLength(1)->nullable()', $code); + } + + public function testStringWithUrlFormat(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'string', 'format' => 'url']); + + self::assertSame('$p->string()->url()', $code); + } + + public function testAllOfWithOptionalFields(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'allOf' => [ + [ + 'type' => 'object', + 'properties' => ['id' => ['type' => 'integer']], + 'required' => ['id'], + ], + [ + 'type' => 'object', + 'properties' => ['nickname' => ['type' => 'string']], + ], + ], + ]); + + self::assertSame( + "\$p->object(['id' => \$p->int(), 'nickname' => \$p->string()->nullable()])->optional(['nickname'])", + $code, + ); + } + + public function testObjectAllFieldsOptional(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'a' => ['type' => 'string'], + 'b' => ['type' => 'integer'], + ], + ]); + + self::assertSame( + "\$p->object(['a' => \$p->string()->nullable(), 'b' => \$p->int()->nullable()])->optional(['a', 'b'])", + $code, + ); + } + + public function testRefInsideNestedSchemaThrows(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$ref is not supported'); + + $generator->generate([ + 'type' => 'object', + 'properties' => [ + 'child' => ['$ref' => '#/definitions/Child'], + ], + 'required' => ['child'], + ]); + } + + public function testEnumOnlyNull(): void + { + $generator = new JsonSchemaCodeGenerator(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Enum must have at leas one non null value'); + + $generator->generate(['enum' => [null]]); + } + + public function testObjectWithNoProperties(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'object']); + + self::assertSame('$p->record($p->string())', $code); + } + + public function testNumberIntegerWholeValue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'minimum' => 0]); + + self::assertSame('$p->float()->gte(0.0)', $code); + } +} From 165e39fe36e2a79056131c312e78422c53ba19c6 Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Sat, 14 Feb 2026 20:49:56 +0100 Subject: [PATCH 8/9] update to use the new method names for code generation --- src/JsonSchemaCodeGenerator.php | 29 +++++----- tests/Unit/JsonSchemaCodeGeneratorTest.php | 62 +++++++++++----------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/JsonSchemaCodeGenerator.php b/src/JsonSchemaCodeGenerator.php index d6fd254..833c2d0 100644 --- a/src/JsonSchemaCodeGenerator.php +++ b/src/JsonSchemaCodeGenerator.php @@ -38,8 +38,7 @@ private function generateSchema(array $schema): string throw new \InvalidArgumentException('$ref is not supported. Please dereference the schema first.'); } - // Handle const/enum as literal - if (\array_key_exists('const', $schema)) { + if (isset($schema['const'])) { return $this->generateConst($schema); } @@ -100,7 +99,7 @@ private function generateSchema(array $schema): string */ private function generateConst(array $schema): string { - return '$p->literal('.$this->exportValue($schema['const']).')'; + return '$p->const('.$this->exportValue($schema['const']).')'; } /** @@ -120,11 +119,11 @@ private function generateEnum(array $schema): string $nonNullValues = array_values(array_filter($values, static fn (mixed $v): bool => null !== $v)); if (0 === \count($nonNullValues)) { - throw new \InvalidArgumentException('Enum must have at leas one non null value'); + throw new \InvalidArgumentException('Enum must have at least one non null value'); } if (1 === \count($nonNullValues)) { - $code = '$p->literal('.$this->exportValue($nonNullValues[0]).')'; + $code = '$p->const('.$this->exportValue($nonNullValues[0]).')'; if ($hasNull) { $code .= '->nullable()'; @@ -135,7 +134,7 @@ private function generateEnum(array $schema): string // Multiple values: union of literals $literals = array_map( - fn (mixed $v): string => '$p->literal('.$this->exportValue($v).')', + fn (mixed $v): string => '$p->const('.$this->exportValue($v).')', $nonNullValues, ); @@ -375,7 +374,7 @@ private function generateString(array $schema): string } if (isset($schema['pattern']) && \is_string($schema['pattern'])) { - $code .= '->regexp('.$this->exportValue('/'.$schema['pattern'].'/').')'; + $code .= '->pattern('.$this->exportValue('/'.$schema['pattern'].'/').')'; } if (isset($schema['format']) && \is_string($schema['format'])) { @@ -389,11 +388,11 @@ private function applyStringFormat(string $code, string $format): string { return match ($format) { 'email' => $code.'->email()', - 'uri', 'url' => $code.'->url()', + 'uri', 'url' => $code.'->uri()', 'ipv4' => $code.'->ipV4()', 'ipv6' => $code.'->ipV6()', 'uuid' => $code.'->uuid()', - 'hostname' => $code.'->domain()', + 'hostname' => $code.'->hostname()', 'date-time' => $code.'->toDateTime()', default => $code.' /* unsupported format: '.$format.' */', }; @@ -426,22 +425,22 @@ private function applyNumericConstraints(string $code, array $schema, bool $isIn { if (isset($schema['minimum']) && (\is_int($schema['minimum']) || \is_float($schema['minimum']))) { $val = $isInt ? (int) $schema['minimum'] : (float) $schema['minimum']; - $code .= '->gte('.$this->exportValue($val).')'; + $code .= '->minimum('.$this->exportValue($val).')'; } if (isset($schema['maximum']) && (\is_int($schema['maximum']) || \is_float($schema['maximum']))) { $val = $isInt ? (int) $schema['maximum'] : (float) $schema['maximum']; - $code .= '->lte('.$this->exportValue($val).')'; + $code .= '->maximum('.$this->exportValue($val).')'; } if (isset($schema['exclusiveMinimum']) && (\is_int($schema['exclusiveMinimum']) || \is_float($schema['exclusiveMinimum']))) { $val = $isInt ? (int) $schema['exclusiveMinimum'] : (float) $schema['exclusiveMinimum']; - $code .= '->gt('.$this->exportValue($val).')'; + $code .= '->exclusiveMinimum('.$this->exportValue($val).')'; } if (isset($schema['exclusiveMaximum']) && (\is_int($schema['exclusiveMaximum']) || \is_float($schema['exclusiveMaximum']))) { $val = $isInt ? (int) $schema['exclusiveMaximum'] : (float) $schema['exclusiveMaximum']; - $code .= '->lt('.$this->exportValue($val).')'; + $code .= '->exclusiveMaximum('.$this->exportValue($val).')'; } return $code; @@ -481,11 +480,11 @@ private function generateArray(array $schema): string $code = '$p->array('.$itemCode.')'; if (isset($schema['minItems']) && (\is_int($schema['minItems']) || \is_float($schema['minItems']))) { - $code .= '->minLength('.(int) $schema['minItems'].')'; + $code .= '->minItems('.(int) $schema['minItems'].')'; } if (isset($schema['maxItems']) && (\is_int($schema['maxItems']) || \is_float($schema['maxItems']))) { - $code .= '->maxLength('.(int) $schema['maxItems'].')'; + $code .= '->maxItems('.(int) $schema['maxItems'].')'; } return $code; diff --git a/tests/Unit/JsonSchemaCodeGeneratorTest.php b/tests/Unit/JsonSchemaCodeGeneratorTest.php index 78a892f..31890dd 100644 --- a/tests/Unit/JsonSchemaCodeGeneratorTest.php +++ b/tests/Unit/JsonSchemaCodeGeneratorTest.php @@ -51,7 +51,7 @@ public function testStringWithPattern(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'string', 'pattern' => '^[a-z]+$']); - self::assertSame("\$p->string()->regexp('/^[a-z]+$/')", $code); + self::assertSame("\$p->string()->pattern('/^[a-z]+$/')", $code); } public function testStringWithEmailFormat(): void @@ -67,7 +67,7 @@ public function testStringWithUriFormat(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'string', 'format' => 'uri']); - self::assertSame('$p->string()->url()', $code); + self::assertSame('$p->string()->uri()', $code); } public function testStringWithIpv4Format(): void @@ -99,7 +99,7 @@ public function testStringWithHostnameFormat(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'string', 'format' => 'hostname']); - self::assertSame('$p->string()->domain()', $code); + self::assertSame('$p->string()->hostname()', $code); } public function testStringWithDateTimeFormat(): void @@ -131,7 +131,7 @@ public function testIntegerWithMinimum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'integer', 'minimum' => 0]); - self::assertSame('$p->int()->gte(0)', $code); + self::assertSame('$p->int()->minimum(0)', $code); } public function testIntegerWithMaximum(): void @@ -139,7 +139,7 @@ public function testIntegerWithMaximum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'integer', 'maximum' => 100]); - self::assertSame('$p->int()->lte(100)', $code); + self::assertSame('$p->int()->maximum(100)', $code); } public function testIntegerWithExclusiveMinimum(): void @@ -147,7 +147,7 @@ public function testIntegerWithExclusiveMinimum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'integer', 'exclusiveMinimum' => 0]); - self::assertSame('$p->int()->gt(0)', $code); + self::assertSame('$p->int()->exclusiveMinimum(0)', $code); } public function testIntegerWithExclusiveMaximum(): void @@ -155,7 +155,7 @@ public function testIntegerWithExclusiveMaximum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'integer', 'exclusiveMaximum' => 100]); - self::assertSame('$p->int()->lt(100)', $code); + self::assertSame('$p->int()->exclusiveMaximum(100)', $code); } public function testIntegerWithAllConstraints(): void @@ -167,7 +167,7 @@ public function testIntegerWithAllConstraints(): void 'maximum' => 99, ]); - self::assertSame('$p->int()->gte(1)->lte(99)', $code); + self::assertSame('$p->int()->minimum(1)->maximum(99)', $code); } public function testNumber(): void @@ -183,7 +183,7 @@ public function testNumberWithMinimum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'number', 'minimum' => 0.5]); - self::assertSame('$p->float()->gte(0.5)', $code); + self::assertSame('$p->float()->minimum(0.5)', $code); } public function testNumberWithMaximum(): void @@ -191,7 +191,7 @@ public function testNumberWithMaximum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'number', 'maximum' => 99.9]); - self::assertSame('$p->float()->lte(99.9)', $code); + self::assertSame('$p->float()->maximum(99.9)', $code); } public function testNumberWithExclusiveMinimum(): void @@ -199,7 +199,7 @@ public function testNumberWithExclusiveMinimum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'number', 'exclusiveMinimum' => 0.0]); - self::assertSame('$p->float()->gt(0.0)', $code); + self::assertSame('$p->float()->exclusiveMinimum(0.0)', $code); } public function testNumberWithExclusiveMaximum(): void @@ -207,7 +207,7 @@ public function testNumberWithExclusiveMaximum(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'number', 'exclusiveMaximum' => 100.0]); - self::assertSame('$p->float()->lt(100.0)', $code); + self::assertSame('$p->float()->exclusiveMaximum(100.0)', $code); } public function testBoolean(): void @@ -257,7 +257,7 @@ public function testArrayWithMinItems(): void 'minItems' => 1, ]); - self::assertSame('$p->array($p->string())->minLength(1)', $code); + self::assertSame('$p->array($p->string())->minItems(1)', $code); } public function testArrayWithMaxItems(): void @@ -269,7 +269,7 @@ public function testArrayWithMaxItems(): void 'maxItems' => 10, ]); - self::assertSame('$p->array($p->string())->maxLength(10)', $code); + self::assertSame('$p->array($p->string())->maxItems(10)', $code); } public function testArrayWithMinAndMaxItems(): void @@ -282,7 +282,7 @@ public function testArrayWithMinAndMaxItems(): void 'maxItems' => 10, ]); - self::assertSame('$p->array($p->string())->minLength(1)->maxLength(10)', $code); + self::assertSame('$p->array($p->string())->minItems(1)->maxItems(10)', $code); } public function testArrayWithNoItems(): void @@ -422,9 +422,9 @@ public function testConst(): void { $generator = new JsonSchemaCodeGenerator(); - self::assertSame("\$p->literal('active')", $generator->generate(['const' => 'active'])); - self::assertSame('$p->literal(42)', $generator->generate(['const' => 42])); - self::assertSame('$p->literal(true)', $generator->generate(['const' => true])); + self::assertSame("\$p->const('active')", $generator->generate(['const' => 'active'])); + self::assertSame('$p->const(42)', $generator->generate(['const' => 42])); + self::assertSame('$p->const(true)', $generator->generate(['const' => true])); } public function testEnumStrings(): void @@ -433,7 +433,7 @@ public function testEnumStrings(): void $code = $generator->generate(['enum' => ['active', 'inactive', 'pending']]); self::assertSame( - "\$p->union([\$p->literal('active'), \$p->literal('inactive'), \$p->literal('pending')])", + "\$p->union([\$p->const('active'), \$p->const('inactive'), \$p->const('pending')])", $code, ); } @@ -443,7 +443,7 @@ public function testEnumSingleValue(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['enum' => ['only']]); - self::assertSame("\$p->literal('only')", $code); + self::assertSame("\$p->const('only')", $code); } public function testEnumWithNull(): void @@ -452,7 +452,7 @@ public function testEnumWithNull(): void $code = $generator->generate(['enum' => ['active', 'inactive', null]]); self::assertSame( - "\$p->union([\$p->literal('active'), \$p->literal('inactive')])->nullable()", + "\$p->union([\$p->const('active'), \$p->const('inactive')])->nullable()", $code, ); } @@ -462,7 +462,7 @@ public function testEnumIntegers(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['enum' => [1, 2, 3]]); - self::assertSame('$p->union([$p->literal(1), $p->literal(2), $p->literal(3)])', $code); + self::assertSame('$p->union([$p->const(1), $p->const(2), $p->const(3)])', $code); } public function testOneOfSimple(): void @@ -574,8 +574,8 @@ public function testDiscriminatedUnion(): void ]); self::assertSame( - "\$p->discriminatedUnion([\$p->object(['type' => \$p->literal('email'), 'address' => \$p->string()]), " - ."\$p->object(['type' => \$p->literal('phone'), 'number' => \$p->string()])], 'type')", + "\$p->discriminatedUnion([\$p->object(['type' => \$p->const('email'), 'address' => \$p->string()]), " + ."\$p->object(['type' => \$p->const('phone'), 'number' => \$p->string()])], 'type')", $code, ); } @@ -654,9 +654,9 @@ public function testComplexSchema(): void ]); self::assertSame( - "\$p->object(['id' => \$p->int()->gte(1), 'name' => \$p->string()->minLength(1)->maxLength(100), " - ."'email' => \$p->string()->email(), 'age' => \$p->int()->gte(0)->lte(150)->nullable(), " - ."'tags' => \$p->array(\$p->string())->minLength(1)->nullable(), 'metadata' => \$p->record(\$p->string())->nullable()])" + "\$p->object(['id' => \$p->int()->minimum(1), 'name' => \$p->string()->minLength(1)->maxLength(100), " + ."'email' => \$p->string()->email(), 'age' => \$p->int()->minimum(0)->maximum(150)->nullable(), " + ."'tags' => \$p->array(\$p->string())->minItems(1)->nullable(), 'metadata' => \$p->record(\$p->string())->nullable()])" ."->optional(['age', 'tags', 'metadata'])->strict()", $code, ); @@ -746,8 +746,8 @@ public function testDiscriminatedUnionWithEnum(): void ]); self::assertSame( - "\$p->discriminatedUnion([\$p->object(['kind' => \$p->literal('circle'), 'radius' => \$p->float()]), " - ."\$p->object(['kind' => \$p->literal('square'), 'side' => \$p->float()])], 'kind')", + "\$p->discriminatedUnion([\$p->object(['kind' => \$p->const('circle'), 'radius' => \$p->float()]), " + ."\$p->object(['kind' => \$p->const('square'), 'side' => \$p->float()])], 'kind')", $code, ); } @@ -768,7 +768,7 @@ public function testStringWithUrlFormat(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'string', 'format' => 'url']); - self::assertSame('$p->string()->url()', $code); + self::assertSame('$p->string()->uri()', $code); } public function testAllOfWithOptionalFields(): void @@ -850,6 +850,6 @@ public function testNumberIntegerWholeValue(): void $generator = new JsonSchemaCodeGenerator(); $code = $generator->generate(['type' => 'number', 'minimum' => 0]); - self::assertSame('$p->float()->gte(0.0)', $code); + self::assertSame('$p->float()->minimum(0.0)', $code); } } From 309a1751b21fe054b0b4519e36ef2ce3f1ae351c Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Mon, 16 Feb 2026 21:34:16 +0100 Subject: [PATCH 9/9] update minimum/exclusiveMinimum and maximum/exclusiveMaximum --- src/JsonSchemaCodeGenerator.php | 24 ++++---- tests/Unit/JsonSchemaCodeGeneratorTest.php | 64 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/JsonSchemaCodeGenerator.php b/src/JsonSchemaCodeGenerator.php index 833c2d0..cf1ff1b 100644 --- a/src/JsonSchemaCodeGenerator.php +++ b/src/JsonSchemaCodeGenerator.php @@ -425,20 +425,24 @@ private function applyNumericConstraints(string $code, array $schema, bool $isIn { if (isset($schema['minimum']) && (\is_int($schema['minimum']) || \is_float($schema['minimum']))) { $val = $isInt ? (int) $schema['minimum'] : (float) $schema['minimum']; - $code .= '->minimum('.$this->exportValue($val).')'; - } - - if (isset($schema['maximum']) && (\is_int($schema['maximum']) || \is_float($schema['maximum']))) { - $val = $isInt ? (int) $schema['maximum'] : (float) $schema['maximum']; - $code .= '->maximum('.$this->exportValue($val).')'; - } - - if (isset($schema['exclusiveMinimum']) && (\is_int($schema['exclusiveMinimum']) || \is_float($schema['exclusiveMinimum']))) { + if (isset($schema['exclusiveMinimum']) && true === $schema['exclusiveMinimum']) { + $code .= '->exclusiveMinimum('.$this->exportValue($val).')'; + } else { + $code .= '->minimum('.$this->exportValue($val).')'; + } + } elseif (isset($schema['exclusiveMinimum']) && (\is_int($schema['exclusiveMinimum']) || \is_float($schema['exclusiveMinimum']))) { $val = $isInt ? (int) $schema['exclusiveMinimum'] : (float) $schema['exclusiveMinimum']; $code .= '->exclusiveMinimum('.$this->exportValue($val).')'; } - if (isset($schema['exclusiveMaximum']) && (\is_int($schema['exclusiveMaximum']) || \is_float($schema['exclusiveMaximum']))) { + if (isset($schema['maximum']) && (\is_int($schema['maximum']) || \is_float($schema['maximum']))) { + $val = $isInt ? (int) $schema['maximum'] : (float) $schema['maximum']; + if (isset($schema['exclusiveMaximum']) && true === $schema['exclusiveMaximum']) { + $code .= '->exclusiveMaximum('.$this->exportValue($val).')'; + } else { + $code .= '->maximum('.$this->exportValue($val).')'; + } + } elseif (isset($schema['exclusiveMaximum']) && (\is_int($schema['exclusiveMaximum']) || \is_float($schema['exclusiveMaximum']))) { $val = $isInt ? (int) $schema['exclusiveMaximum'] : (float) $schema['exclusiveMaximum']; $code .= '->exclusiveMaximum('.$this->exportValue($val).')'; } diff --git a/tests/Unit/JsonSchemaCodeGeneratorTest.php b/tests/Unit/JsonSchemaCodeGeneratorTest.php index 31890dd..4bcb6f4 100644 --- a/tests/Unit/JsonSchemaCodeGeneratorTest.php +++ b/tests/Unit/JsonSchemaCodeGeneratorTest.php @@ -170,6 +170,38 @@ public function testIntegerWithAllConstraints(): void self::assertSame('$p->int()->minimum(1)->maximum(99)', $code); } + public function testIntegerWithMinimumAndExclusiveMinimumTrue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'minimum' => 0, 'exclusiveMinimum' => true]); + + self::assertSame('$p->int()->exclusiveMinimum(0)', $code); + } + + public function testIntegerWithMinimumAndExclusiveMinimumFalse(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'minimum' => 0, 'exclusiveMinimum' => false]); + + self::assertSame('$p->int()->minimum(0)', $code); + } + + public function testIntegerWithMaximumAndExclusiveMaximumTrue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'maximum' => 100, 'exclusiveMaximum' => true]); + + self::assertSame('$p->int()->exclusiveMaximum(100)', $code); + } + + public function testIntegerWithMaximumAndExclusiveMaximumFalse(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'integer', 'maximum' => 100, 'exclusiveMaximum' => false]); + + self::assertSame('$p->int()->maximum(100)', $code); + } + public function testNumber(): void { $generator = new JsonSchemaCodeGenerator(); @@ -210,6 +242,38 @@ public function testNumberWithExclusiveMaximum(): void self::assertSame('$p->float()->exclusiveMaximum(100.0)', $code); } + public function testNumberWithMinimumAndExclusiveMinimumTrue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'minimum' => 0.5, 'exclusiveMinimum' => true]); + + self::assertSame('$p->float()->exclusiveMinimum(0.5)', $code); + } + + public function testNumberWithMinimumAndExclusiveMinimumFalse(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'minimum' => 0.5, 'exclusiveMinimum' => false]); + + self::assertSame('$p->float()->minimum(0.5)', $code); + } + + public function testNumberWithMaximumAndExclusiveMaximumTrue(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'maximum' => 99.9, 'exclusiveMaximum' => true]); + + self::assertSame('$p->float()->exclusiveMaximum(99.9)', $code); + } + + public function testNumberWithMaximumAndExclusiveMaximumFalse(): void + { + $generator = new JsonSchemaCodeGenerator(); + $code = $generator->generate(['type' => 'number', 'maximum' => 99.9, 'exclusiveMaximum' => false]); + + self::assertSame('$p->float()->maximum(99.9)', $code); + } + public function testBoolean(): void { $generator = new JsonSchemaCodeGenerator();