From 384c0c0210f02475a39f17a54f657b6583c88453 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Oct 2020 16:15:54 +0200 Subject: [PATCH 1/4] Add shorthand support --- composer.json | 3 +- src/Shorthand/Shorthand.php | 203 ++++++++++++++++++++++++++++ tests/Shorthand/ShorthandTest.php | 218 ++++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 src/Shorthand/Shorthand.php create mode 100644 tests/Shorthand/ShorthandTest.php diff --git a/composer.json b/composer.json index 8ce143e..e403229 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ } }, "require": { - "php": "^7.4 || ^8.0" + "php": "^7.4 || ^8.0", + "ext-json": "*" }, "require-dev": { "jangregor/phpstan-prophecy": "^0.8.0", diff --git a/src/Shorthand/Shorthand.php b/src/Shorthand/Shorthand.php new file mode 100644 index 0000000..6f01ecd --- /dev/null +++ b/src/Shorthand/Shorthand.php @@ -0,0 +1,203 @@ + 'object', + 'properties' => [ + + ], + 'required' => [], + 'additionalProperties' => false, + ]; + + foreach ($shorthand as $property => $shorthandDefinition) { + if(!is_string($property) || empty($property)) { + throw new LogicException(sprintf( + 'Shorthand %s contains an empty or non string property. Cannot deal with that!', + json_encode($shorthand) + )); + } + + $schemaProperty = $property; + + if(mb_substr($property, -1) === '?') { + $schemaProperty = mb_substr($property, 0, strlen($property) - 1); + } else if ($schemaProperty === '$ref') { + if(count($shorthand) > 1) { + throw new LogicException(sprintf( + 'Shorthand %s contains a top level ref property "$ref", but it is not the only property! + \nA top level reference cannot have other properties then "$ref".', + json_encode($shorthand) + )); + } + + if(!is_string($shorthandDefinition)) { + throw new LogicException(sprintf( + 'Detected a top level shorthand reference using a "$ref" property, but the value of the property is not a string.', + )); + } + + $shorthandDefinition = str_replace('#/definitions/', '', $shorthandDefinition); + + return [ + '$ref' => "#/definitions/$shorthandDefinition" + ]; + } else if ($schemaProperty === '$items') { + if(count($shorthand) > 1) { + throw new LogicException(sprintf( + 'Shorthand %s contains a top level array property "$items", but it is not the only property! + \nA top level array cannot have other properties then "$items".', + json_encode($shorthand) + )); + } + + if(!is_string($shorthandDefinition)) { + throw new LogicException(sprintf( + 'Detected a top level shorthand array using an "$items" property, but the value of the property is not a string.', + )); + } + + if(mb_substr($shorthandDefinition, -2) !== '[]') { + $shorthandDefinition .= '[]'; + } + + return self::convertShorthandStringToJsonSchema($shorthandDefinition); + } else if ($schemaProperty === '$title') { + $schema['title'] = $shorthandDefinition; + continue; + } else { + $schema['required'][] = $schemaProperty; + } + + if(is_array($shorthandDefinition)) { + $schema['properties'][$schemaProperty] = self::convertToJsonSchema($shorthandDefinition); + } else if (is_string($shorthandDefinition)) { + $schema['properties'][$schemaProperty] = self::convertShorthandStringToJsonSchema($shorthandDefinition); + } else { + throw new LogicException(sprintf( + 'I tried to parse JSONSchema for property: "%s", but it is neither a string nor an object.', + $schemaProperty + )); + } + } + + return $schema; + } + + private static function convertShorthandStringToJsonSchema(string $shorthandStr): array + { + if($shorthandStr === '') { + return ['type' => 'string']; + } + + $parts = explode('|', $shorthandStr); + + if($parts[0] === 'enum') { + return ['enum' => array_slice($parts, 1)]; + } + + if(mb_substr($parts[0], -2) === '[]') { + $itemsParts = [mb_substr($parts[0], 0, mb_strlen($parts[0]) - 2)]; + array_push($itemsParts, ...array_slice($parts, 1)); + + return [ + 'type' => 'array', + 'items' => self::convertShorthandStringToJsonSchema(implode('|', $itemsParts)), + ]; + } + + switch ($parts[0]) { + case 'string': + case 'integer': + case 'number': + case 'boolean': + $type = $parts[0]; + + if(isset($parts[1]) && $parts[1] === 'null') { + $type = [$type, 'null']; + + array_splice($parts, 1, 1); + } + + $schema = ['type' => $type]; + + if(count($parts) > 1) { + $parts = array_slice($parts, 1); + + foreach ($parts as $part) { + [$validationKey, $validationValue] = self::parseShorthandValidation($part); + + $schema[$validationKey] = $validationValue; + } + } + + return $schema; + default: + return [ + '$ref' => '#/definitions/'.$parts[0], + ]; + } + } + + private static function parseShorthandValidation(string $shorthandValidation): array + { + $parts = explode(':', $shorthandValidation); + + if(count($parts) !== 2) { + throw new LogicException(sprintf( + 'Cannot parse shorthand validation: "%s". Expected format "validationKey:value". Please check again!', + $shorthandValidation + )); + } + + [$validationKey, $value] = $parts; + + if($value === 'true') { + return [$validationKey, true]; + } + + if($value === 'false') { + return [$validationKey, false]; + } + + if((string)intval($value) === $value) { + return [$validationKey, (int)$value]; + } + + if((string)floatval($value) === $value) { + return [$validationKey, (float)$value]; + } + + return [$validationKey, $value]; + } +} diff --git a/tests/Shorthand/ShorthandTest.php b/tests/Shorthand/ShorthandTest.php new file mode 100644 index 0000000..021bac5 --- /dev/null +++ b/tests/Shorthand/ShorthandTest.php @@ -0,0 +1,218 @@ + '']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['type' => 'string']], + ['test'] + ), $schema); + } + + /** + * @test + */ + public function it_converts_enum_shorthand_to_json_schema_string() + { + $schema = Shorthand::convertToJsonSchema(['test' => 'enum|available|blocked|bought']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['enum' => ["available", "blocked", "bought"]]], + ['test'] + ), $schema); + } + + /** + * @test + */ + public function it_converts_shorthand_type_to_json_schema() + { + foreach (self::SHORTHAND_TYPES as $type) { + $schema = Shorthand::convertToJsonSchema(['test' => $type]); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['type' => $type]], + ['test'] + ), $schema); + } + } + + /** + * @test + */ + public function it_converts_nullable_shorthand_type_to_nullable_json_schema_type() + { + foreach (self::SHORTHAND_TYPES as $type) { + $schema = Shorthand::convertToJsonSchema(['test' => $type.'|null']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['type' => [$type, 'null']]], + ['test'] + ), $schema); + } + } + + /** + * @test + */ + public function it_converts_unknown_type_to_json_schema_ref() + { + $schema = Shorthand::convertToJsonSchema(['test' => 'User']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['$ref' => '#/definitions/User']], + ['test'] + ), $schema); + } + + /** + * @test + */ + public function it_converts_array_shorthand_type_to_json_schema_array_type() + { + foreach (self::SHORTHAND_TYPES as $type) { + $schema = Shorthand::convertToJsonSchema(['test' => $type.'[]']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['type' => 'array', 'items' => ['type' => $type]]], + ['test'] + ), $schema); + } + } + + /** + * @test + */ + public function it_converts_unknown_shorthand_array_type_to_json_schema_array_with_ref_items() + { + $schema = Shorthand::convertToJsonSchema(['test' => 'User[]']); + + $this->assertEquals($this->jsonSchemaObject( + ['test' => ['type' => 'array', 'items' => ['$ref' => '#/definitions/User']]], + ['test'] + ), $schema); + } + + /** + * @test + */ + public function it_parses_shorthand_validation_and_adds_it_to_json_schema() + { + $schema = Shorthand::convertToJsonSchema([ + 'test1' => 'string|format:email|maxLength:255', + 'test2' => 'number|minimum:0.5|maximum:10', + 'test3' => 'string|null|format:email', + 'test4' => 'boolean|default:false', + 'test5' => 'boolean|null|default:true' + ]); + + $this->assertEquals($this->jsonSchemaObject( + [ + 'test1' => ['type' => 'string', 'format' => 'email', 'maxLength' => 255], + 'test2' => ['type' => 'number', 'minimum' => 0.5, 'maximum' => 10], + 'test3' => ['type' => ['string', 'null'], 'format' => 'email'], + 'test4' => ['type' => 'boolean', 'default' => false], + 'test5' => ['type' => ['boolean', 'null'], 'default' => true], + ], + ['test1', 'test2', 'test3', 'test4', 'test5'] + ), $schema); + } + + /** + * @test + */ + public function it_converts_shorthand_top_level_array_to_json_schema_array() + { + $variants = ['Profile', 'Profile[]']; + + foreach ($variants as $variant) { + $schema = Shorthand::convertToJsonSchema(['$items' => $variant]); + + $this->assertEquals(['type' => 'array', 'items' => ['$ref' => '#/definitions/Profile']], $schema); + } + } + + /** + * @test + */ + public function it_converts_top_level_reference_to_json_schema_reference() + { + $variants = ['Profile', '#/definitions/Profile']; + + foreach ($variants as $variant) { + $schema = Shorthand::convertToJsonSchema(['$ref' => $variant]); + + $this->assertEquals(['$ref' => '#/definitions/Profile'], $schema); + } + } + + /** + * @test + */ + public function it_converts_shorthand_object_to_json_schema_object() + { + $schema = Shorthand::convertToJsonSchema([ + '$title' => 'Prospect', + 'name' => 'string|minLength:1', + 'email' => 'string|format:email', + 'age?' => 'number|minimum:0', + 'address' => [ + 'zip' => 'string|minLength:1', + 'city' => 'string|minLength:1', + ], + 'tags' => 'string[]|minLength:1', + 'searchProfile?' => [ + 'roomsMin' => 'number|null|minimum:0.5', + 'roomsMax' => 'number|null|minimum:0.5', + ] + ]); + + $this->assertEquals( + $this->jsonSchemaObject([ + 'name' => ['type' => 'string', 'minLength' => 1], + 'email' => ['type' => 'string', 'format' => 'email'], + 'age' => ['type' => 'number', 'minimum' => 0], + 'address' => $this->jsonSchemaObject([ + 'zip' => ['type' => 'string', 'minLength' => 1], + 'city' => ['type' => 'string', 'minLength' => 1], + ], ['zip', 'city']), + 'tags' => ['type' => 'array', 'items' => ['type' => 'string', 'minLength' => 1]], + 'searchProfile' => $this->jsonSchemaObject([ + 'roomsMin' => ['type' => ['number', 'null'], 'minimum' => 0.5], + 'roomsMax' => ['type' => ['number', 'null'], 'minimum' => 0.5], + ], ['roomsMin', 'roomsMax']) + ], ['name', 'email', 'address', 'tags'], 'Prospect'), + $schema + ); + } + + private function jsonSchemaObject(array $properties, array $required = [], $title = null): array + { + $obj = [ + 'type' => 'object', + 'properties' => $properties, + 'additionalProperties' => false, + 'required' => $required + ]; + + if($title) { + $obj['title'] = $title; + } + + return $obj; + } +} From 4b8107169e54e657f24ff6416bab48f9054875f7 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Oct 2020 16:37:45 +0200 Subject: [PATCH 2/4] Handle JSON Schema title and derive TypeSet from shorthand --- src/Shorthand/Shorthand.php | 121 +++++++++++++++--------------- src/Type/ArrayType.php | 13 +++- src/Type/ObjectType.php | 13 +++- src/Type/ReferenceType.php | 13 +++- src/Type/ScalarType.php | 13 +++- src/Type/TitleAware.php | 16 ++++ src/Type/Type.php | 12 +++ tests/Shorthand/ShorthandTest.php | 30 +++++--- tests/Type/TypeTest.php | 30 ++++++++ 9 files changed, 183 insertions(+), 78 deletions(-) create mode 100644 src/Type/TitleAware.php create mode 100644 tests/Type/TypeTest.php diff --git a/src/Shorthand/Shorthand.php b/src/Shorthand/Shorthand.php index 6f01ecd..6046a47 100644 --- a/src/Shorthand/Shorthand.php +++ b/src/Shorthand/Shorthand.php @@ -1,5 +1,4 @@ $shorthand + * @return array + */ public static function convertToJsonSchema(array $shorthand): array { $schema = [ @@ -41,70 +30,70 @@ public static function convertToJsonSchema(array $shorthand): array ]; foreach ($shorthand as $property => $shorthandDefinition) { - if(!is_string($property) || empty($property)) { - throw new LogicException(sprintf( + if (! \is_string($property) || empty($property)) { + throw new LogicException(\sprintf( 'Shorthand %s contains an empty or non string property. Cannot deal with that!', - json_encode($shorthand) + \json_encode($shorthand) )); } $schemaProperty = $property; - if(mb_substr($property, -1) === '?') { - $schemaProperty = mb_substr($property, 0, strlen($property) - 1); - } else if ($schemaProperty === '$ref') { - if(count($shorthand) > 1) { - throw new LogicException(sprintf( + if (\mb_substr($property, -1) === '?') { + $schemaProperty = \mb_substr($property, 0, \strlen($property) - 1); + } elseif ($schemaProperty === '$ref') { + if (\count($shorthand) > 1) { + throw new LogicException(\sprintf( 'Shorthand %s contains a top level ref property "$ref", but it is not the only property! \nA top level reference cannot have other properties then "$ref".', - json_encode($shorthand) + \json_encode($shorthand) )); } - if(!is_string($shorthandDefinition)) { - throw new LogicException(sprintf( + if (! \is_string($shorthandDefinition)) { + throw new LogicException(\sprintf( 'Detected a top level shorthand reference using a "$ref" property, but the value of the property is not a string.', )); } - $shorthandDefinition = str_replace('#/definitions/', '', $shorthandDefinition); + $shorthandDefinition = \str_replace('#/definitions/', '', $shorthandDefinition); return [ - '$ref' => "#/definitions/$shorthandDefinition" + '$ref' => "#/definitions/$shorthandDefinition", ]; - } else if ($schemaProperty === '$items') { - if(count($shorthand) > 1) { - throw new LogicException(sprintf( + } elseif ($schemaProperty === '$items') { + if (\count($shorthand) > 1) { + throw new LogicException(\sprintf( 'Shorthand %s contains a top level array property "$items", but it is not the only property! \nA top level array cannot have other properties then "$items".', - json_encode($shorthand) + \json_encode($shorthand) )); } - if(!is_string($shorthandDefinition)) { - throw new LogicException(sprintf( + if (! \is_string($shorthandDefinition)) { + throw new LogicException(\sprintf( 'Detected a top level shorthand array using an "$items" property, but the value of the property is not a string.', )); } - if(mb_substr($shorthandDefinition, -2) !== '[]') { + if (\mb_substr($shorthandDefinition, -2) !== '[]') { $shorthandDefinition .= '[]'; } return self::convertShorthandStringToJsonSchema($shorthandDefinition); - } else if ($schemaProperty === '$title') { + } elseif ($schemaProperty === '$title') { $schema['title'] = $shorthandDefinition; continue; } else { $schema['required'][] = $schemaProperty; } - if(is_array($shorthandDefinition)) { + if (\is_array($shorthandDefinition)) { $schema['properties'][$schemaProperty] = self::convertToJsonSchema($shorthandDefinition); - } else if (is_string($shorthandDefinition)) { + } elseif (\is_string($shorthandDefinition)) { $schema['properties'][$schemaProperty] = self::convertShorthandStringToJsonSchema($shorthandDefinition); } else { - throw new LogicException(sprintf( + throw new LogicException(\sprintf( 'I tried to parse JSONSchema for property: "%s", but it is neither a string nor an object.', $schemaProperty )); @@ -114,25 +103,29 @@ public static function convertToJsonSchema(array $shorthand): array return $schema; } + /** + * @param string $shorthandStr + * @return array + */ private static function convertShorthandStringToJsonSchema(string $shorthandStr): array { - if($shorthandStr === '') { + if ($shorthandStr === '') { return ['type' => 'string']; } - $parts = explode('|', $shorthandStr); + $parts = \explode('|', $shorthandStr); - if($parts[0] === 'enum') { - return ['enum' => array_slice($parts, 1)]; + if ($parts[0] === 'enum') { + return ['enum' => \array_slice($parts, 1)]; } - if(mb_substr($parts[0], -2) === '[]') { - $itemsParts = [mb_substr($parts[0], 0, mb_strlen($parts[0]) - 2)]; - array_push($itemsParts, ...array_slice($parts, 1)); + if (\mb_substr($parts[0], -2) === '[]') { + $itemsParts = [\mb_substr($parts[0], 0, \mb_strlen($parts[0]) - 2)]; + \array_push($itemsParts, ...\array_slice($parts, 1)); return [ 'type' => 'array', - 'items' => self::convertShorthandStringToJsonSchema(implode('|', $itemsParts)), + 'items' => self::convertShorthandStringToJsonSchema(\implode('|', $itemsParts)), ]; } @@ -143,16 +136,16 @@ private static function convertShorthandStringToJsonSchema(string $shorthandStr) case 'boolean': $type = $parts[0]; - if(isset($parts[1]) && $parts[1] === 'null') { + if (isset($parts[1]) && $parts[1] === 'null') { $type = [$type, 'null']; - array_splice($parts, 1, 1); + \array_splice($parts, 1, 1); } $schema = ['type' => $type]; - if(count($parts) > 1) { - $parts = array_slice($parts, 1); + if (\count($parts) > 1) { + $parts = \array_slice($parts, 1); foreach ($parts as $part) { [$validationKey, $validationValue] = self::parseShorthandValidation($part); @@ -169,12 +162,16 @@ private static function convertShorthandStringToJsonSchema(string $shorthandStr) } } + /** + * @param string $shorthandValidation + * @return array + */ private static function parseShorthandValidation(string $shorthandValidation): array { - $parts = explode(':', $shorthandValidation); + $parts = \explode(':', $shorthandValidation); - if(count($parts) !== 2) { - throw new LogicException(sprintf( + if (\count($parts) !== 2) { + throw new LogicException(\sprintf( 'Cannot parse shorthand validation: "%s". Expected format "validationKey:value". Please check again!', $shorthandValidation )); @@ -182,20 +179,20 @@ private static function parseShorthandValidation(string $shorthandValidation): a [$validationKey, $value] = $parts; - if($value === 'true') { + if ($value === 'true') { return [$validationKey, true]; } - if($value === 'false') { + if ($value === 'false') { return [$validationKey, false]; } - if((string)intval($value) === $value) { - return [$validationKey, (int)$value]; + if ((string) (int) $value === $value) { + return [$validationKey, (int) $value]; } - if((string)floatval($value) === $value) { - return [$validationKey, (float)$value]; + if ((string) (float) $value === $value) { + return [$validationKey, (float) $value]; } return [$validationKey, $value]; diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index d472aef..3db65c3 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -10,7 +10,7 @@ namespace OpenCodeModeling\JsonSchemaToPhp\Type; -final class ArrayType implements TypeDefinition +final class ArrayType implements TypeDefinition, TitleAware { use PopulateRequired; @@ -36,6 +36,7 @@ final class ArrayType implements TypeDefinition protected ?string $name = null; protected bool $isRequired = false; protected bool $nullable = false; + protected ?string $title = null; private function __construct() { @@ -231,6 +232,16 @@ public function additionalItems(): ?TypeSet return $this->additionalItems; } + public function title(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + public static function type(): string { return self::TYPE_ARRAY; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 24f8823..88bf5fa 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -10,13 +10,14 @@ namespace OpenCodeModeling\JsonSchemaToPhp\Type; -final class ObjectType implements TypeDefinition, NullableAware, RequiredAware +final class ObjectType implements TypeDefinition, NullableAware, RequiredAware, TitleAware { use PopulateRequired; protected ?string $name = null; protected bool $isRequired = false; protected bool $nullable = false; + protected ?string $title = null; /** * @var null|bool|TypeSet @@ -182,6 +183,16 @@ public function required(): array return $this->required; } + public function title(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + /** * @return array */ diff --git a/src/Type/ReferenceType.php b/src/Type/ReferenceType.php index 8753ee0..04ab708 100644 --- a/src/Type/ReferenceType.php +++ b/src/Type/ReferenceType.php @@ -10,13 +10,14 @@ namespace OpenCodeModeling\JsonSchemaToPhp\Type; -final class ReferenceType implements TypeDefinition, RequiredAware, NullableAware +final class ReferenceType implements TypeDefinition, RequiredAware, NullableAware, TitleAware { protected ?TypeSet $resolvedType = null; protected ?string $name = null; protected ?string $ref = null; protected bool $isRequired = false; protected bool $nullable = false; + protected ?string $title = null; private function __construct() { @@ -109,4 +110,14 @@ public function setNullable(bool $nullable): void } $this->nullable = $nullable; } + + public function title(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } } diff --git a/src/Type/ScalarType.php b/src/Type/ScalarType.php index 886ca94..3207098 100644 --- a/src/Type/ScalarType.php +++ b/src/Type/ScalarType.php @@ -10,12 +10,13 @@ namespace OpenCodeModeling\JsonSchemaToPhp\Type; -abstract class ScalarType implements TypeDefinition, RequiredAware, NullableAware +abstract class ScalarType implements TypeDefinition, RequiredAware, NullableAware, TitleAware { protected ?string $format = null; protected ?string $name = null; protected bool $isRequired = false; protected bool $nullable = false; + protected ?string $title = null; /** * @var mixed @@ -146,4 +147,14 @@ public function setIsRequired(bool $required): void { $this->isRequired = $required; } + + public function title(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } } diff --git a/src/Type/TitleAware.php b/src/Type/TitleAware.php new file mode 100644 index 0000000..4c4fbed --- /dev/null +++ b/src/Type/TitleAware.php @@ -0,0 +1,16 @@ + $shorthand + * @param string|null $name + * @return TypeSet + */ + public static function fromShorthand(array $shorthand, ?string $name = null): TypeSet + { + return self::fromDefinition(Shorthand::convertToJsonSchema($shorthand), $name); + } + /** * @param array $definition * @param string|null $name diff --git a/tests/Shorthand/ShorthandTest.php b/tests/Shorthand/ShorthandTest.php index 021bac5..56f0ea0 100644 --- a/tests/Shorthand/ShorthandTest.php +++ b/tests/Shorthand/ShorthandTest.php @@ -13,7 +13,7 @@ final class ShorthandTest extends TestCase /** * @test */ - public function it_converts_empty_shorthand_string_to_json_schema_string() + public function it_converts_empty_shorthand_string_to_json_schema_string(): void { $schema = Shorthand::convertToJsonSchema(['test' => '']); @@ -26,7 +26,7 @@ public function it_converts_empty_shorthand_string_to_json_schema_string() /** * @test */ - public function it_converts_enum_shorthand_to_json_schema_string() + public function it_converts_enum_shorthand_to_json_schema_string(): void { $schema = Shorthand::convertToJsonSchema(['test' => 'enum|available|blocked|bought']); @@ -39,7 +39,7 @@ public function it_converts_enum_shorthand_to_json_schema_string() /** * @test */ - public function it_converts_shorthand_type_to_json_schema() + public function it_converts_shorthand_type_to_json_schema(): void { foreach (self::SHORTHAND_TYPES as $type) { $schema = Shorthand::convertToJsonSchema(['test' => $type]); @@ -54,7 +54,7 @@ public function it_converts_shorthand_type_to_json_schema() /** * @test */ - public function it_converts_nullable_shorthand_type_to_nullable_json_schema_type() + public function it_converts_nullable_shorthand_type_to_nullable_json_schema_type(): void { foreach (self::SHORTHAND_TYPES as $type) { $schema = Shorthand::convertToJsonSchema(['test' => $type.'|null']); @@ -69,7 +69,7 @@ public function it_converts_nullable_shorthand_type_to_nullable_json_schema_type /** * @test */ - public function it_converts_unknown_type_to_json_schema_ref() + public function it_converts_unknown_type_to_json_schema_ref(): void { $schema = Shorthand::convertToJsonSchema(['test' => 'User']); @@ -82,7 +82,7 @@ public function it_converts_unknown_type_to_json_schema_ref() /** * @test */ - public function it_converts_array_shorthand_type_to_json_schema_array_type() + public function it_converts_array_shorthand_type_to_json_schema_array_type(): void { foreach (self::SHORTHAND_TYPES as $type) { $schema = Shorthand::convertToJsonSchema(['test' => $type.'[]']); @@ -97,7 +97,7 @@ public function it_converts_array_shorthand_type_to_json_schema_array_type() /** * @test */ - public function it_converts_unknown_shorthand_array_type_to_json_schema_array_with_ref_items() + public function it_converts_unknown_shorthand_array_type_to_json_schema_array_with_ref_items(): void { $schema = Shorthand::convertToJsonSchema(['test' => 'User[]']); @@ -110,7 +110,7 @@ public function it_converts_unknown_shorthand_array_type_to_json_schema_array_wi /** * @test */ - public function it_parses_shorthand_validation_and_adds_it_to_json_schema() + public function it_parses_shorthand_validation_and_adds_it_to_json_schema(): void { $schema = Shorthand::convertToJsonSchema([ 'test1' => 'string|format:email|maxLength:255', @@ -135,7 +135,7 @@ public function it_parses_shorthand_validation_and_adds_it_to_json_schema() /** * @test */ - public function it_converts_shorthand_top_level_array_to_json_schema_array() + public function it_converts_shorthand_top_level_array_to_json_schema_array(): void { $variants = ['Profile', 'Profile[]']; @@ -149,7 +149,7 @@ public function it_converts_shorthand_top_level_array_to_json_schema_array() /** * @test */ - public function it_converts_top_level_reference_to_json_schema_reference() + public function it_converts_top_level_reference_to_json_schema_reference(): void { $variants = ['Profile', '#/definitions/Profile']; @@ -163,7 +163,7 @@ public function it_converts_top_level_reference_to_json_schema_reference() /** * @test */ - public function it_converts_shorthand_object_to_json_schema_object() + public function it_converts_shorthand_object_to_json_schema_object(): void { $schema = Shorthand::convertToJsonSchema([ '$title' => 'Prospect', @@ -200,7 +200,13 @@ public function it_converts_shorthand_object_to_json_schema_object() ); } - private function jsonSchemaObject(array $properties, array $required = [], $title = null): array + /** + * @param array $properties + * @param string[] $required + * @param null|string $title + * @return array + */ + private function jsonSchemaObject(array $properties, array $required = [], string $title = null): array { $obj = [ 'type' => 'object', diff --git a/tests/Type/TypeTest.php b/tests/Type/TypeTest.php new file mode 100644 index 0000000..329e84b --- /dev/null +++ b/tests/Type/TypeTest.php @@ -0,0 +1,30 @@ + 'string|minLength:1', '$title' => 'Person'], 'Person'); + + $this->assertCount(1, $typeSet); + + /** @var ObjectType $type */ + $type = $typeSet->first(); + + $properties = $type->properties(); + + $this->assertArrayHasKey('name', $properties); + $this->assertEquals('Person', $type->name()); + $this->assertEquals('Person', $type->title()); + } +} From d3c210a770f694ed2df8ed7705fdb3ab10bfa1a1 Mon Sep 17 00:00:00 2001 From: Sandro Keil Date: Wed, 28 Oct 2020 18:31:38 +0100 Subject: [PATCH 3/4] Introduce exception classes --- composer.json | 3 +- src/Exception/ExceptionInterface.php | 20 +++++ src/Exception/InvalidShorthand.php | 115 +++++++++++++++++++++++++++ src/Exception/LogicException.php | 17 ++++ src/Exception/RuntimeException.php | 22 +++++ src/Shorthand/Shorthand.php | 37 +++------ 6 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidShorthand.php create mode 100644 src/Exception/LogicException.php create mode 100644 src/Exception/RuntimeException.php diff --git a/composer.json b/composer.json index e403229..8ce143e 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,7 @@ } }, "require": { - "php": "^7.4 || ^8.0", - "ext-json": "*" + "php": "^7.4 || ^8.0" }, "require-dev": { "jangregor/phpstan-prophecy": "^0.8.0", diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..ba0ef4c --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,20 @@ + + */ + private array $shorthand; + + /** + * @param string $schemaProperty + * @param array $shorthand + * @return static + */ + public static function cannotParseProperty(string $schemaProperty, array $shorthand): self + { + $self = new static( + \sprintf( + 'I tried to parse JSONSchema for property: "%s", but it is neither a string nor an object.', + $schemaProperty + ) + ); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @param array $shorthand $shorthand + * @return static + */ + public static function emptyString(array $shorthand): self + { + $self = new static('Shorthand contains an empty or non string property. Cannot deal with that!'); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @param array $shorthand $shorthand + * @return static + */ + public static function refNotString(array $shorthand): self + { + $self = new static( + 'Detected a top level shorthand reference using a "$ref" property, but the value of the property is not a string.' + ); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @param array $shorthand $shorthand + * @return static + */ + public static function itemsNotString(array $shorthand): self + { + $self = new static( + 'Detected a top level shorthand array using an "$items" property, but the value of the property is not a string.' + ); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @param array $shorthand $shorthand + * @return static + */ + public static function refWithOtherProperties(array $shorthand): self + { + $self = new static( + 'Shorthand contains a top level ref property "$ref", but it is not the only property!' + . ' A top level reference cannot have other properties then "$ref".' + ); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @param array $shorthand $shorthand + * @return static + */ + public static function itemsWithOtherProperties(array $shorthand): self + { + $self = new static( + 'Shorthand %s contains a top level array property "$items", but it is not the only property!' + . ' A top level array cannot have other properties then "$items".' + ); + $self->shorthand = $shorthand; + + return $self; + } + + /** + * @return array + */ + public function shorthand(): array + { + return $this->shorthand; + } +} diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000..451fa62 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,17 @@ + $shorthandDefinition) { if (! \is_string($property) || empty($property)) { - throw new LogicException(\sprintf( - 'Shorthand %s contains an empty or non string property. Cannot deal with that!', - \json_encode($shorthand) - )); + throw InvalidShorthand::emptyString($shorthand); } $schemaProperty = $property; if (\mb_substr($property, -1) === '?') { - $schemaProperty = \mb_substr($property, 0, \strlen($property) - 1); + $schemaProperty = \mb_substr($property, 0, -1); } elseif ($schemaProperty === '$ref') { if (\count($shorthand) > 1) { - throw new LogicException(\sprintf( - 'Shorthand %s contains a top level ref property "$ref", but it is not the only property! - \nA top level reference cannot have other properties then "$ref".', - \json_encode($shorthand) - )); + throw InvalidShorthand::refWithOtherProperties($shorthand); } if (! \is_string($shorthandDefinition)) { - throw new LogicException(\sprintf( - 'Detected a top level shorthand reference using a "$ref" property, but the value of the property is not a string.', - )); + throw InvalidShorthand::refNotString($shorthand); } $shorthandDefinition = \str_replace('#/definitions/', '', $shorthandDefinition); @@ -63,17 +55,11 @@ public static function convertToJsonSchema(array $shorthand): array ]; } elseif ($schemaProperty === '$items') { if (\count($shorthand) > 1) { - throw new LogicException(\sprintf( - 'Shorthand %s contains a top level array property "$items", but it is not the only property! - \nA top level array cannot have other properties then "$items".', - \json_encode($shorthand) - )); + throw InvalidShorthand::itemsWithOtherProperties($shorthand); } if (! \is_string($shorthandDefinition)) { - throw new LogicException(\sprintf( - 'Detected a top level shorthand array using an "$items" property, but the value of the property is not a string.', - )); + throw InvalidShorthand::itemsNotString($shorthand); } if (\mb_substr($shorthandDefinition, -2) !== '[]') { @@ -93,10 +79,7 @@ public static function convertToJsonSchema(array $shorthand): array } elseif (\is_string($shorthandDefinition)) { $schema['properties'][$schemaProperty] = self::convertShorthandStringToJsonSchema($shorthandDefinition); } else { - throw new LogicException(\sprintf( - 'I tried to parse JSONSchema for property: "%s", but it is neither a string nor an object.', - $schemaProperty - )); + throw InvalidShorthand::cannotParseProperty($schemaProperty, $shorthand); } } @@ -120,7 +103,7 @@ private static function convertShorthandStringToJsonSchema(string $shorthandStr) } if (\mb_substr($parts[0], -2) === '[]') { - $itemsParts = [\mb_substr($parts[0], 0, \mb_strlen($parts[0]) - 2)]; + $itemsParts = [\mb_substr($parts[0], 0, -2)]; \array_push($itemsParts, ...\array_slice($parts, 1)); return [ From fce284bb92b81be41fc71a7947e4e52baf4dbff6 Mon Sep 17 00:00:00 2001 From: Sandro Keil Date: Wed, 28 Oct 2020 18:38:15 +0100 Subject: [PATCH 4/4] Use switch/case --- src/Shorthand/Shorthand.php | 62 +++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/Shorthand/Shorthand.php b/src/Shorthand/Shorthand.php index 52e8c80..2750dd3 100644 --- a/src/Shorthand/Shorthand.php +++ b/src/Shorthand/Shorthand.php @@ -34,44 +34,46 @@ public static function convertToJsonSchema(array $shorthand): array if (! \is_string($property) || empty($property)) { throw InvalidShorthand::emptyString($shorthand); } - $schemaProperty = $property; - if (\mb_substr($property, -1) === '?') { - $schemaProperty = \mb_substr($property, 0, -1); - } elseif ($schemaProperty === '$ref') { - if (\count($shorthand) > 1) { - throw InvalidShorthand::refWithOtherProperties($shorthand); - } + switch (true) { + case \mb_substr($property, -1) === '?': + $schemaProperty = \mb_substr($property, 0, -1); + break; + case $schemaProperty === '$ref': + if (\count($shorthand) > 1) { + throw InvalidShorthand::refWithOtherProperties($shorthand); + } - if (! \is_string($shorthandDefinition)) { - throw InvalidShorthand::refNotString($shorthand); - } + if (! \is_string($shorthandDefinition)) { + throw InvalidShorthand::refNotString($shorthand); + } - $shorthandDefinition = \str_replace('#/definitions/', '', $shorthandDefinition); + $shorthandDefinition = \str_replace('#/definitions/', '', $shorthandDefinition); - return [ - '$ref' => "#/definitions/$shorthandDefinition", - ]; - } elseif ($schemaProperty === '$items') { - if (\count($shorthand) > 1) { - throw InvalidShorthand::itemsWithOtherProperties($shorthand); - } + return [ + '$ref' => "#/definitions/$shorthandDefinition", + ]; + case $schemaProperty === '$items': + if (\count($shorthand) > 1) { + throw InvalidShorthand::itemsWithOtherProperties($shorthand); + } - if (! \is_string($shorthandDefinition)) { - throw InvalidShorthand::itemsNotString($shorthand); - } + if (! \is_string($shorthandDefinition)) { + throw InvalidShorthand::itemsNotString($shorthand); + } - if (\mb_substr($shorthandDefinition, -2) !== '[]') { - $shorthandDefinition .= '[]'; - } + if (\mb_substr($shorthandDefinition, -2) !== '[]') { + $shorthandDefinition .= '[]'; + } - return self::convertShorthandStringToJsonSchema($shorthandDefinition); - } elseif ($schemaProperty === '$title') { - $schema['title'] = $shorthandDefinition; - continue; - } else { - $schema['required'][] = $schemaProperty; + return self::convertShorthandStringToJsonSchema($shorthandDefinition); + case $schemaProperty === '$title': + $schema['title'] = $shorthandDefinition; + continue 2; + default: + $schema['required'][] = $schemaProperty; + break; } if (\is_array($shorthandDefinition)) {