diff --git a/src/ProvidesValidationRules.php b/src/ProvidesValidationRules.php new file mode 100644 index 0000000..16d8720 --- /dev/null +++ b/src/ProvidesValidationRules.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\JsonSchema; + +interface ProvidesValidationRules +{ + public static function validationRules(): array; +} diff --git a/src/RecordLogic/TypeDetector.php b/src/RecordLogic/TypeDetector.php index 315606c..9816d7e 100644 --- a/src/RecordLogic/TypeDetector.php +++ b/src/RecordLogic/TypeDetector.php @@ -28,7 +28,8 @@ public static function getTypeFromClass(string $classOrType, bool $allowNestedSc } if($refObj->implementsInterface(JsonSchemaAwareCollection::class)) { - return JsonSchema::array(\call_user_func([$classOrType, '__itemSchema'])); + $validation = is_callable([$classOrType, 'validationRules'])? \call_user_func([$classOrType, 'validationRules']) : null; + return JsonSchema::array(\call_user_func([$classOrType, '__itemSchema']), $validation); } if($scalarSchemaType = self::determineScalarTypeIfPossible($classOrType)) { @@ -41,15 +42,18 @@ public static function getTypeFromClass(string $classOrType, bool $allowNestedSc private static function determineScalarTypeIfPossible(string $class): ?Type { if(is_callable([$class, 'fromString'])) { - return JsonSchema::string(); + $validation = is_callable([$class, 'validationRules'])? \call_user_func([$class, 'validationRules']) : null; + return JsonSchema::string($validation); } if(is_callable([$class, 'fromInt'])) { - return JsonSchema::integer(); + $validation = is_callable([$class, 'validationRules'])? \call_user_func([$class, 'validationRules']) : null; + return JsonSchema::integer($validation); } if(is_callable([$class, 'fromFloat'])) { - return JsonSchema::float(); + $validation = is_callable([$class, 'validationRules'])? \call_user_func([$class, 'validationRules']) : null; + return JsonSchema::float($validation); } if(is_callable([$class, 'fromBool'])) { diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 03b3b56..39932dd 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -13,6 +13,7 @@ use EventEngine\JsonSchema\AnnotatedType; use EventEngine\JsonSchema\JsonSchema; +use EventEngine\JsonSchema\Type; use EventEngine\Schema\PayloadSchema; use EventEngine\Schema\TypeSchema; @@ -21,6 +22,11 @@ final class ArrayType implements AnnotatedType, PayloadSchema use NullableType, HasAnnotations; + public const MAX_ITEMS = 'maxItems'; + public const MIN_ITEMS = 'minItems'; + public const UNIQUE_ITEMS = 'uniqueItems'; + public const CONTAINS = 'contains'; + /** * @var string|array */ @@ -42,6 +48,58 @@ public function __construct(TypeSchema $itemSchema, array $validation = null) $this->validation = $validation; } + public function withMaxItems(int $maxItems): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::MAX_ITEMS] = $maxItems; + + $cp->validation = $validation; + + return $cp; + } + + public function withMinItems(int $minItems): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::MIN_ITEMS] = $minItems; + + $cp->validation = $validation; + + return $cp; + } + + public function withUniqueItems(): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::UNIQUE_ITEMS] = true; + + $cp->validation = $validation; + + return $cp; + } + + public function withContains(TypeSchema $itemSchema): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::CONTAINS] = $itemSchema->toArray(); + + $cp->validation = $validation; + + return $cp; + } + public function toArray(): array { return \array_merge([ diff --git a/src/Type/BoolType.php b/src/Type/BoolType.php index 4547afa..5ff8003 100644 --- a/src/Type/BoolType.php +++ b/src/Type/BoolType.php @@ -19,6 +19,8 @@ final class BoolType implements AnnotatedType use NullableType, HasAnnotations; + public const CONST = 'const'; + /** * @var string|array */ diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 0955f43..ea1ac47 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -19,6 +19,14 @@ class FloatType implements AnnotatedType use NullableType, HasAnnotations; + public const MINIMUM = 'minimum'; + public const MAXIMUM = 'maximum'; + public const MULTIPLE_OF = 'multipleOf'; + public const EXCLUSIVE_MAXIMUM = 'exclusiveMaximum'; + public const EXCLUSIVE_MINIMUM = 'exclusiveMinimum'; + public const ENUM = 'enum'; + public const CONST = 'const'; + /** * @var string|array */ @@ -45,7 +53,20 @@ public function withMinimum(float $min): self $validation = (array) $this->validation; - $validation['minimum'] = $min; + $validation[self::MINIMUM] = $min; + + $cp->validation = $validation; + + return $cp; + } + + public function withExclusiveMinimum(float $exclusiveMin): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::EXCLUSIVE_MINIMUM] = $exclusiveMin; $cp->validation = $validation; @@ -58,7 +79,20 @@ public function withMaximum(float $max): self $validation = (array) $this->validation; - $validation['maximum'] = $max; + $validation[self::MAXIMUM] = $max; + + $cp->validation = $validation; + + return $cp; + } + + public function withExclusiveMaximum(float $exclusiveMax): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::EXCLUSIVE_MAXIMUM] = $exclusiveMax; $cp->validation = $validation; @@ -71,8 +105,21 @@ public function withRange(float $min, float $max): self $validation = (array) $this->validation; - $validation['minimum'] = $min; - $validation['maximum'] = $max; + $validation[self::MINIMUM] = $min; + $validation[self::MAXIMUM] = $max; + + $cp->validation = $validation; + + return $cp; + } + + public function withMultipleOf(float $multipleOf): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::MULTIPLE_OF] = $multipleOf; $cp->validation = $validation; diff --git a/src/Type/IntType.php b/src/Type/IntType.php index cbc9dc3..7d40b6e 100644 --- a/src/Type/IntType.php +++ b/src/Type/IntType.php @@ -19,6 +19,15 @@ class IntType implements AnnotatedType use NullableType, HasAnnotations; + public const MINIMUM = 'minimum'; + public const MAXIMUM = 'maximum'; + public const MULTIPLE_OF = 'multipleOf'; + public const EXCLUSIVE_MAXIMUM = 'exclusiveMaximum'; + public const EXCLUSIVE_MINIMUM = 'exclusiveMinimum'; + public const ENUM = 'enum'; + public const CONST = 'const'; + + /** * @var string|array */ @@ -45,7 +54,20 @@ public function withMinimum(int $min): self $validation = (array) $this->validation; - $validation['minimum'] = $min; + $validation[self::MINIMUM] = $min; + + $cp->validation = $validation; + + return $cp; + } + + public function withExclusiveMinimum(int $exclusiveMin): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::EXCLUSIVE_MINIMUM] = $exclusiveMin; $cp->validation = $validation; @@ -58,7 +80,20 @@ public function withMaximum(int $max): self $validation = (array) $this->validation; - $validation['maximum'] = $max; + $validation[self::MAXIMUM] = $max; + + $cp->validation = $validation; + + return $cp; + } + + public function withExclusiveMaximum(int $exclusiveMax): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::EXCLUSIVE_MAXIMUM] = $exclusiveMax; $cp->validation = $validation; @@ -71,8 +106,21 @@ public function withRange(int $min, int $max): self $validation = (array) $this->validation; - $validation['minimum'] = $min; - $validation['maximum'] = $max; + $validation[self::MINIMUM] = $min; + $validation[self::MAXIMUM] = $max; + + $cp->validation = $validation; + + return $cp; + } + + public function withMultipleOf(float $multipleOf): self + { + $cp = clone $this; + + $validation = (array) $this->validation; + + $validation[self::MULTIPLE_OF] = $multipleOf; $cp->validation = $validation; diff --git a/src/Type/StringType.php b/src/Type/StringType.php index caf6a3b..340841c 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -19,6 +19,14 @@ class StringType implements AnnotatedType use NullableType, HasAnnotations; + public const PATTERN = 'pattern'; + public const FORMAT = 'format'; + public const MIN_LENGTH = 'minLength'; + public const MAX_LENGTH = 'maxLength'; + public const ENUM = 'enum'; + public const CONST = 'const'; + + private $type = JsonSchema::TYPE_STRING; /** @@ -34,7 +42,15 @@ public function __construct(array $validation = null) public function withMinLength(int $minLength): self { $cp = clone $this; - $cp->validation['minLength'] = $minLength; + $cp->validation[self::MIN_LENGTH] = $minLength; + + return $cp; + } + + public function withMaxLength(int $maxLength): self + { + $cp = clone $this; + $cp->validation[self::MAX_LENGTH] = $maxLength; return $cp; } @@ -42,7 +58,7 @@ public function withMinLength(int $minLength): self public function withPattern(string $pattern): self { $cp = clone $this; - $cp->validation['pattern'] = $pattern; + $cp->validation[self::PATTERN] = $pattern; return $cp; } diff --git a/tests/JsonSchemaAwareRecordLogicTest.php b/tests/JsonSchemaAwareRecordLogicTest.php index fb12c90..7d490d8 100644 --- a/tests/JsonSchemaAwareRecordLogicTest.php +++ b/tests/JsonSchemaAwareRecordLogicTest.php @@ -4,7 +4,6 @@ namespace EventEngineTest\JsonSchema; use EventEngine\JsonSchema\JsonSchema; -use EventEngine\JsonSchema\Type\TypeRef; use EventEngineTest\JsonSchema\Stub\ArrayItemRecord; use EventEngineTest\JsonSchema\Stub\CollectionItemAllowNestedRecord; use EventEngineTest\JsonSchema\Stub\CollectionItemRecord; @@ -13,6 +12,7 @@ use EventEngineTest\JsonSchema\Stub\NullableScalarPropsRecord; use EventEngineTest\JsonSchema\Stub\ScalarPropsRecord; use EventEngineTest\JsonSchema\Stub\VoOptionalPropsRecord; +use EventEngineTest\JsonSchema\Stub\VoProp\UserId; use EventEngineTest\JsonSchema\Stub\VoPropsRecord; final class JsonSchemaAwareRecordLogicTest extends BasicTestCase @@ -100,7 +100,7 @@ public function it_uses_item_schema_from_collection() $schema = CollectionItemRecord::__schema(); $expected = JsonSchema::object([ - 'friends' => JsonSchema::array(JsonSchema::typeRef(ScalarPropsRecord::__type())) + 'friends' => JsonSchema::array(JsonSchema::typeRef(ScalarPropsRecord::__type()))->withMaxItems(10) ]); $this->assertEquals($expected->toArray(), $schema->toArray()); @@ -127,10 +127,10 @@ public function it_detects_scalar_types_through_method_analysis_of_vo_classes() $schema = VoPropsRecord::__schema(); $expected = JsonSchema::object([ - 'userId' => JsonSchema::string(), - 'age' => JsonSchema::integer(), + 'userId' => JsonSchema::string()->withPattern(UserId::PATTERN), + 'age' => JsonSchema::integer()->withRange(0, 150), 'member' => JsonSchema::boolean(), - 'score' => JsonSchema::float() + 'score' => JsonSchema::float()->withRange(0.1, 1), ]); $this->assertEquals($expected->toArray(), $schema->toArray()); @@ -144,11 +144,11 @@ public function it_respects_optional_properties() $schema = VoOptionalPropsRecord::__schema(); $expected = JsonSchema::object([ - 'userId' => JsonSchema::string(), - 'age' => JsonSchema::integer(), + 'userId' => JsonSchema::string()->withPattern(UserId::PATTERN), + 'age' => JsonSchema::integer()->withRange(0, 150), 'member' => JsonSchema::boolean(), ], [ - 'score' => JsonSchema::float() + 'score' => JsonSchema::float()->withRange(0.1, 1), ]); $this->assertEquals($expected->toArray(), $schema->toArray()); diff --git a/tests/Stub/ScalarPropsRecordCollection.php b/tests/Stub/ScalarPropsRecordCollection.php index 85a698b..cc11e3b 100644 --- a/tests/Stub/ScalarPropsRecordCollection.php +++ b/tests/Stub/ScalarPropsRecordCollection.php @@ -5,8 +5,10 @@ use EventEngine\JsonSchema\JsonSchemaAwareCollection; use EventEngine\JsonSchema\JsonSchemaAwareCollectionLogic; +use EventEngine\JsonSchema\ProvidesValidationRules; +use EventEngine\JsonSchema\Type\ArrayType; -final class ScalarPropsRecordCollection implements JsonSchemaAwareCollection +final class ScalarPropsRecordCollection implements JsonSchemaAwareCollection, ProvidesValidationRules { use JsonSchemaAwareCollectionLogic; @@ -15,6 +17,11 @@ private static function __itemType(): ?string return ScalarPropsRecord::class; } + public static function validationRules(): array + { + return [ArrayType::MAX_ITEMS => 10]; + } + /** * @var ScalarPropsRecord[] */ diff --git a/tests/Stub/VoProp/Age.php b/tests/Stub/VoProp/Age.php index acb9229..4e16b0c 100644 --- a/tests/Stub/VoProp/Age.php +++ b/tests/Stub/VoProp/Age.php @@ -3,10 +3,18 @@ namespace EventEngineTest\JsonSchema\Stub\VoProp; -final class Age +use EventEngine\JsonSchema\ProvidesValidationRules; +use EventEngine\JsonSchema\Type\IntType; + +final class Age implements ProvidesValidationRules { private $age; + public static function validationRules(): array + { + return [IntType::MINIMUM => 0, IntType::MAXIMUM => 150]; + } + public static function fromInt(int $age): self { return new self($age); @@ -35,5 +43,4 @@ public function __toString(): string { return (string)$this->age; } - } diff --git a/tests/Stub/VoProp/Score.php b/tests/Stub/VoProp/Score.php index 3badd7c..c8dbe6a 100644 --- a/tests/Stub/VoProp/Score.php +++ b/tests/Stub/VoProp/Score.php @@ -3,10 +3,18 @@ namespace EventEngineTest\JsonSchema\Stub\VoProp; -final class Score +use EventEngine\JsonSchema\ProvidesValidationRules; +use EventEngine\JsonSchema\Type\FloatType; + +final class Score implements ProvidesValidationRules { private $score; + public static function validationRules(): array + { + return [FloatType::MINIMUM => 0.1, FloatType::MAXIMUM => 1]; + } + public static function fromFloat(float $score): self { return new self($score); @@ -35,5 +43,4 @@ public function __toString(): string { return (string)$this->score; } - } diff --git a/tests/Stub/VoProp/UserId.php b/tests/Stub/VoProp/UserId.php index 6a7b2b5..2b3e84d 100644 --- a/tests/Stub/VoProp/UserId.php +++ b/tests/Stub/VoProp/UserId.php @@ -3,10 +3,19 @@ namespace EventEngineTest\JsonSchema\Stub\VoProp; -final class UserId +use EventEngine\JsonSchema\ProvidesValidationRules; + +final class UserId implements ProvidesValidationRules { + public const PATTERN = '^[0-9]+$'; + private $userId; + public static function validationRules(): array + { + return ['pattern' => self::PATTERN]; + } + public static function fromString(string $userId): self { return new self($userId);