From 04fafdbc2c7b8da96a6714f7e848a76d99f8f429 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Tue, 9 Jun 2020 14:48:33 +0800 Subject: [PATCH 01/14] Typehint for ApiInterface --- src/ApiInterface.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ApiInterface.php b/src/ApiInterface.php index dfc22e3..cc62577 100644 --- a/src/ApiInterface.php +++ b/src/ApiInterface.php @@ -9,11 +9,17 @@ */ interface ApiInterface { + /** + * @return mixed + */ public static function parameters(); + /** + * @return mixed + */ public static function requestBody(); - public static function responses(); + public static function responses(): array; - public static function permissions(); + public static function permissions(): array; } From 65213a2bf07de95a64beb5efcc11e7361b2ee82c Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Sat, 18 Jul 2020 16:44:22 +0800 Subject: [PATCH 02/14] Update to generate OpenAPI 3.1 docs --- src/TypeParser.php | 15 +-- tests/TypeTest.php | 251 +-------------------------------------------- 2 files changed, 8 insertions(+), 258 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index c0184c6..badeccc 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -148,9 +148,7 @@ protected function parseInputField($definition) 'in' => $fetcher, 'schema' => $schema, ]; - if ($required) { - $result['required'] = $required; - } + $result['required'] = $required; return $result; } @@ -236,17 +234,12 @@ protected function parseEnum(string $definition) protected function makeNullableSchema(array $schema, $nullable) { - if ($this->mode & self::MODE_OPEN_API) { - // OpenAPI specficition does not support this, just ingore the nullable setting. - return $schema; - } - if (! $nullable) { return $schema; } return [ - 'anyOf' => [ + 'oneOf' => [ ['type' => 'null'], $schema, ], @@ -264,10 +257,8 @@ protected function parseScalar($definition) $typeClass = $this->getValidTypeClass($definition); $schema = $typeClass::toArray(); - if (($this->mode & self::MODE_JSON_SCHEMA) && $nullable) { + if ($nullable) { $schema['type'] = [$schema['type'], 'null']; - } elseif (($this->mode & self::MODE_OPEN_API) && $nullable) { - $schema['nullable'] = $nullable; } return $schema; diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 95ea84c..1d6b86a 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -28,7 +28,6 @@ public function typeToArrayCases() 'type' => 'object', 'example' => ['id' => 1, 'name' => 'INFO'], ], - null, ], [ Map002Type::class, @@ -39,7 +38,6 @@ public function typeToArrayCases() ], 'example' => ['优' => 90, '良' => 80, '中' => 60], ], - null, ], [ Map003Type::class, @@ -54,14 +52,12 @@ public function typeToArrayCases() ], ], ], - null, ], [ 'string', [ 'type' => 'string', ], - null, ], [ ['string'], @@ -71,7 +67,6 @@ public function typeToArrayCases() 'type' => 'string', ], ], - null, ], [ @@ -79,10 +74,6 @@ public function typeToArrayCases() [ 'type' => ['string', 'null'], ], - [ - 'type' => 'string', - 'nullable' => true, - ], ], [ @@ -93,13 +84,6 @@ public function typeToArrayCases() 'type' => ['string', 'null'], ], ], - [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'nullable' => true, - ], - ], ], [ @@ -111,13 +95,6 @@ public function typeToArrayCases() 'bar', ], ], - [ - 'type' => 'string', - 'enum' => [ - 'foo', - 'bar', - ], - ], ], [ @@ -142,27 +119,6 @@ public function typeToArrayCases() ], 'required' => ['id', 'file'], ], - [ - 'type' => 'object', - 'properties' => [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string'], - 'is_admin' => ['type' => 'boolean'], - 'file' => ['type' => 'string', 'format' => 'binary',], - 'nullable_field' => ['type' => 'string', 'nullable' => true], - 'date' => [ - 'type' => 'string', - 'format' => 'date', - 'nullable' => true, - 'pattern' => '^\d{4}-\d{2}-\d{2}$', - ], - 'time' => [ - 'type' => 'string', - 'pattern' => '^\d{2}:\d{2}:\d{2}$', - ], - ], - 'required' => ['id', 'file'], - ], ], [ @@ -184,24 +140,6 @@ public function typeToArrayCases() ], ], ], - [ - 'type' => 'object', - 'properties' => [ - 'field1' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'field2' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'nullable' => true, - ], - ], - ], - ], ], [ Product005Type::class, @@ -231,33 +169,6 @@ public function typeToArrayCases() ], ], ], - [ - 'required' => ['related1'], - 'type' => 'object', - 'properties' => [ - 'related1' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'field1' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'field2' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'nullable' => true, - ], - ], - ], - ], - ], - ], - ], ], [ Product003Type::class, @@ -304,51 +215,6 @@ public function typeToArrayCases() ], ], ], - [ - 'type' => 'object', - 'properties' => [ - 'related1' => [ - 'type' => 'object', - 'properties' => [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string'], - 'is_admin' => ['type' => 'boolean'], - 'file' => ['type' => 'string', 'format' => 'binary',], - 'nullable_field' => ['type' => 'string', 'nullable' => true], - 'date' => [ - 'type' => 'string', - 'format' => 'date', - 'nullable' => true, - 'pattern' => '^\d{4}-\d{2}-\d{2}$', - ], - 'time' => [ - 'type' => 'string', - 'pattern' => '^\d{2}:\d{2}:\d{2}$', - ], - ], - 'required' => ['id', 'file'], - - ], - 'related2' => [ - 'type' => 'object', - 'properties' => [ - 'field1' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'field2' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'nullable' => true, - ], - ], - ], - ], - ], - ], ], [ @@ -378,7 +244,7 @@ public function typeToArrayCases() ], 'related2' => [ - 'anyOf' => [ + 'oneOf' => [ [ 'type' => 'null', ], @@ -404,52 +270,6 @@ public function typeToArrayCases() ], 'required' => ['related1'], ], - [ - 'type' => 'object', - 'properties' => [ - 'related1' => [ - 'type' => 'object', - 'properties' => [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string'], - 'is_admin' => ['type' => 'boolean'], - 'file' => ['type' => 'string', 'format' => 'binary'], - 'nullable_field' => ['type' => 'string', 'nullable' => true], - 'date' => [ - 'type' => 'string', - 'format' => 'date', - 'nullable' => true, - 'pattern' => '^\d{4}-\d{2}-\d{2}$', - ], - 'time' => [ - 'type' => 'string', - 'pattern' => '^\d{2}:\d{2}:\d{2}$', - ], - ], - 'required' => ['id', 'file'], - - ], - 'related2' => [ - 'type' => 'object', - 'properties' => [ - 'field1' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'field2' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'nullable' => true, - ], - ], - ], - ], - ], - 'required' => ['related1'], - ], ], [ @@ -478,29 +298,7 @@ public function typeToArrayCases() 'schema' => ['type' => 'number'], ], ], - - [ - [ - 'name' => 'limit', - 'in' => 'query', - 'required' => true, - 'schema' => ['type' => 'number'], - ], - [ - 'name' => 'offset', - 'in' => 'query', - 'required' => false, - 'schema' => ['type' => 'number', 'nullable' => true], - ], - [ - 'name' => 'default_in', - 'in' => 'query', - 'required' => false, - 'schema' => ['type' => 'number'], - ], - ], ], - [ [Product001Type::class], [ @@ -537,42 +335,6 @@ public function typeToArrayCases() 'required' => ['id', 'file'], ], ], - [ - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'id' => [ - 'type' => 'integer', - ], - 'name' => [ - 'type' => 'string', - ], - 'is_admin' => [ - 'type' => 'boolean', - ], - 'file' => [ - 'type' => 'string', - 'format' => 'binary', - ], - 'nullable_field' => [ - 'type' => 'string', - 'nullable' => true, - ], - 'date' => [ - 'type' => 'string', - 'format' => 'date', - 'nullable' => true, - 'pattern' => '^\d{4}-\d{2}-\d{2}$', - ], - 'time' => [ - 'type' => 'string', - 'pattern' => '^\d{2}:\d{2}:\d{2}$', - ], - ], - 'required' => ['id', 'file'], - ], - ], ], ]; } @@ -581,16 +343,14 @@ public function typeToArrayCases() * @dataProvider typeToArrayCases * @param $type * @param $expect1 - * @param $expect2 */ - public function testTypeToArray($type, $expect1, $expect2) + public function testTypeToArray($type, $expect1) { $parser = new TypeParser(TypeParser::MODE_JSON_SCHEMA); - var_dump($parser->parse($type)); $this->assertEquals($expect1, $parser->parse($type)); $parser = new TypeParser(TypeParser::MODE_OPEN_API); - $this->assertEquals($expect2 ?? $expect1, $parser->parse($type)); + $this->assertEquals($expect1, $parser->parse($type)); } public function typeToArrayWithRefCases() @@ -614,8 +374,7 @@ public function typeToArrayWithRefCases() 'field2' => [ 'type' => 'array', 'items' => [ - 'type' => 'string', - 'nullable' => true, + 'type' => ['string', 'null'], ], ], ], @@ -797,7 +556,7 @@ public function dataCases() 'type' => 'object', 'properties' => [ 'foo' => [ - 'anyOf' => [ + 'oneOf' => [ ['type' => 'null'], ], ], From 24281517c0dc3ef00c07285f023617e874097a76 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Tue, 2 Mar 2021 15:17:32 +0800 Subject: [PATCH 03/14] Upgraded phpdocumentor/reflection-docblock --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99f8872..5fb8891 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "require": { "php": ">=7.0", - "phpdocumentor/reflection-docblock": "^4.3", + "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "justinrainbow/json-schema": "^5.2" }, "require-dev": { From 265fd6b5500bbeca545583f37f11cdb3432fbb46 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 1 Apr 2021 18:17:14 +0800 Subject: [PATCH 04/14] Improved InputValidator to convert int to boolean --- src/InputValidator.php | 7 ++- src/constraints/TypeConstraint.php | 21 ++++++++ tests/TypeTest.php | 77 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/constraints/TypeConstraint.php diff --git a/src/InputValidator.php b/src/InputValidator.php index 1614dc4..664f121 100644 --- a/src/InputValidator.php +++ b/src/InputValidator.php @@ -3,7 +3,9 @@ namespace rethink\typedphp; use JsonSchema\Constraints\Constraint; +use JsonSchema\Constraints\Factory; use JsonSchema\Validator; +use rethink\typedphp\constraints\TypeConstraint; /** * Class InputValidator @@ -37,8 +39,11 @@ protected function validateInternal($definition, $data, &$result) } $schema = $definition['schema']; + + $factory = new Factory(); + $factory->setConstraintClass('type', TypeConstraint::class); - $validator = new Validator(); + $validator = new Validator($factory); $result = $data[$definition['name']]; diff --git a/src/constraints/TypeConstraint.php b/src/constraints/TypeConstraint.php new file mode 100644 index 0000000..baf8a66 --- /dev/null +++ b/src/constraints/TypeConstraint.php @@ -0,0 +1,21 @@ + ['a' => '1'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => '0'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], + [ + [ + 'query' => ['a' => 1], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => 0], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], // missing required input field [ [ From 918bbc8f728d9e15d748e794c1b2c928c792329b Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 1 Apr 2021 20:28:18 +0800 Subject: [PATCH 05/14] Fixed break string bools --- src/constraints/TypeConstraint.php | 4 ++-- tests/TypeTest.php | 38 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/constraints/TypeConstraint.php b/src/constraints/TypeConstraint.php index baf8a66..45cd833 100644 --- a/src/constraints/TypeConstraint.php +++ b/src/constraints/TypeConstraint.php @@ -10,9 +10,9 @@ class TypeConstraint extends \JsonSchema\Constraints\TypeConstraint { protected function toBoolean($value) { - if ($value == 1) { + if ($value === 1 || $value === '1') { return true; - } elseif ($value == 0) { + } elseif ($value === 0 || $value === '0') { return false; } else { return parent::toBoolean($value); diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 24f82c6..682f6c2 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -718,6 +718,44 @@ public function inputDataCases() 'query' => ['a' => false], ], ], + [ + [ + 'query' => ['a' => 'true'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => 'false'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], // missing required input field [ [ From 361a3524d259cfddaca1f96e419908d658f4efd2 Mon Sep 17 00:00:00 2001 From: Devon Liu Date: Wed, 21 Dec 2022 15:43:44 +0800 Subject: [PATCH 06/14] Speed up parser by cache parsed result --- src/TypeParser.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index a745916..65aefae 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -314,16 +314,28 @@ protected function parseMap(string $definition): array protected function parseString($definition) { + static $cached = []; $newDefinition = trim($definition, '?'); + + $key = $definition; + if (is_subclass_of($newDefinition, MapType::class)) { + $key = $newDefinition; + } + + if (isset($cached[$key])) { + return $cached[$key]; + } + if (is_subclass_of($newDefinition, ProductType::class)) { - return $this->parseObject($definition); + $cached[$key] = $this->parseObject($definition); } elseif (is_subclass_of($newDefinition, SumType::class)) { - return $this->parseEnum($definition); + $cached[$key]= $this->parseEnum($definition); } elseif (is_subclass_of($newDefinition, MapType::class)) { - return $this->parseMap($newDefinition); + $cached[$key] = $this->parseMap($newDefinition); } else { - return $this->parseScalar($definition); + $cached[$key] = $this->parseScalar($definition); } + return $cached[$key]; } /** From 96d3d44e65b14a9ae6e0d9aa32d86c0cee679ce5 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Tue, 21 Mar 2023 12:21:33 +0800 Subject: [PATCH 07/14] Added union type --- src/TypeParser.php | 29 +++++++++- src/types/ProductType.php | 4 +- src/types/UnionType.php | 24 ++++++++ tests/TypeTest.php | 114 +++++++++++++++++++++++++++++++++++++- 4 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 src/types/UnionType.php diff --git a/src/TypeParser.php b/src/TypeParser.php index 65aefae..2959488 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -18,6 +18,7 @@ use rethink\typedphp\types\TimestampType; use rethink\typedphp\types\TimeType; use rethink\typedphp\types\Type; +use rethink\typedphp\types\UnionType; /** * Class TypeParser @@ -312,6 +313,30 @@ protected function parseMap(string $definition): array return $this->makeNullableSchema($schema, $nullable); } + protected function parseUnion(string $definition): array + { + $nullable = false; + if ($this->isNullable($definition)) { + $nullable = true; + $definition = trim($definition, '?'); + } + + assert(is_subclass_of($definition, UnionType::class)); + + $types = []; + foreach ($definition::allowedTypes() as $allowedType) { + $types[] = $this->parse($allowedType); + } + + if ($nullable) { + $types[] = ['type' => 'null']; + } + + return [ + 'oneOf' => $types, + ]; + } + protected function parseString($definition) { static $cached = []; @@ -331,7 +356,9 @@ protected function parseString($definition) } elseif (is_subclass_of($newDefinition, SumType::class)) { $cached[$key]= $this->parseEnum($definition); } elseif (is_subclass_of($newDefinition, MapType::class)) { - $cached[$key] = $this->parseMap($newDefinition); + $cached[$key] = $this->parseMap($definition); + } elseif (is_subclass_of($newDefinition, UnionType::class)) { + $cached[$key] = $this->parseUnion($definition); } else { $cached[$key] = $this->parseScalar($definition); } diff --git a/src/types/ProductType.php b/src/types/ProductType.php index e5b8f1c..6de5ea6 100644 --- a/src/types/ProductType.php +++ b/src/types/ProductType.php @@ -22,8 +22,6 @@ public static function name() public static function toArray() { - $parser = new TypeParser(static::class); - - return $parser->parse(); + throw new \Exception('Should not be called'); } } diff --git a/src/types/UnionType.php b/src/types/UnionType.php new file mode 100644 index 0000000..97e6802 --- /dev/null +++ b/src/types/UnionType.php @@ -0,0 +1,24 @@ + 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'is_admin' => ['type' => 'boolean'], + 'file' => ['type' => 'string', 'format' => 'binary',], + 'nullable_field' => ['type' => ['string', 'null']], + 'date' => [ + 'type' => ['string', 'null'], + 'format' => 'date', + 'pattern' => '^\d{4}-\d{2}-\d{2}$', + ], + 'time' => [ + 'type' => 'string', + 'pattern' => '^\d{2}:\d{2}:\d{2}$', + ], + ], + 'required' => ['id', 'file'], + ]; + } + + protected function product001SchemaWithNull(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'is_admin' => ['type' => 'boolean'], + 'file' => ['type' => 'string', 'format' => 'binary',], + 'nullable_field' => ['type' => 'string', 'nullable' => true], + 'date' => [ + 'type' => 'string', + 'format' => 'date', + 'nullable' => true, + 'pattern' => '^\d{4}-\d{2}-\d{2}$', + ], + 'time' => [ + 'type' => 'string', + 'pattern' => '^\d{2}:\d{2}:\d{2}$', + ], + ], + 'required' => ['id', 'file'], + ]; + } + public function typeToArrayCases() { + $product001Schema = $this->product001Schema(); + + $product001SchemaWithNull = $this->product001SchemaWithNull(); + return [ [ Map001Type::class, @@ -574,6 +629,31 @@ public function typeToArrayCases() ], ], ], + [ + Union001Type::class, + [ + 'oneOf' => [ + [ + 'type' => 'string', + ], + [ + 'type' => 'integer', + ], + $product001Schema, + ], + ], + [ + 'oneOf' => [ + [ + 'type' => 'string', + ], + [ + 'type' => 'integer', + ], + $product001SchemaWithNull, + ], + ] + ], ]; } @@ -624,6 +704,28 @@ public function typeToArrayWithRefCases() ], ], ], + [ + Union001Type::class . '?', + [ + 'oneOf' => [ + [ + 'type' => 'string', + ], + [ + 'type' => 'integer', + ], + [ + '$ref' => '#/components/schemas/Product001', + ], + [ + 'type' => 'null', + ], + ], + ], + [ + 'Product001' => $this->product001SchemaWithNull(), + ] + ], ]; } @@ -1034,7 +1136,6 @@ class Dict003ItemType extends ProductType class Map003Type extends MapType { - public static function valueType(): string { return Dict003ItemType::class; @@ -1044,5 +1145,16 @@ public static function example(): array { return []; } +} +class Union001Type extends UnionType +{ + public static function allowedTypes(): array + { + return [ + 'string', + 'integer', + Product001Type::class, + ]; + } } From cc9f891af8dc31413a48b7276f1bc7c4e73d1cd5 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Sat, 15 Apr 2023 19:32:21 +0800 Subject: [PATCH 08/14] Update deps --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5fb8891..f665291 100644 --- a/composer.json +++ b/composer.json @@ -9,12 +9,12 @@ } ], "require": { - "php": ">=7.0", - "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", + "php": ">=7.4", + "phpdocumentor/reflection-docblock": "^5.3", "justinrainbow/json-schema": "^5.2" }, "require-dev": { - "phpunit/phpunit": "^7.5" + "phpunit/phpunit": "^9.6" }, "autoload": { "psr-4": { From dbf8557638f28ce07aace6725db0bb91347d58af Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Sat, 15 Apr 2023 19:36:16 +0800 Subject: [PATCH 09/14] Fixed errors on php8 --- src/InputValidator.php | 4 ++-- src/TypeValidator.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/InputValidator.php b/src/InputValidator.php index 664f121..8b8b916 100644 --- a/src/InputValidator.php +++ b/src/InputValidator.php @@ -32,7 +32,7 @@ protected function fetchData($fetcher) protected function validateInternal($definition, $data, &$result) { if (($definition['required'] ?? false) && !array_key_exists($definition['name'], $data)) { - $this->errors[] = "The required ${definition['in']} parameter: '${definition['name']}' is required"; + $this->errors[] = "The required {$definition['in']} parameter: '{$definition['name']}' is required"; return false; } else if (!array_key_exists($definition['name'], $data)) { return false; @@ -51,7 +51,7 @@ protected function validateInternal($definition, $data, &$result) if (!$validator->isValid()) { foreach ($validator->getErrors() as $error) { - $this->errors[] = "The type of ${definition['in']} parameter \"${definition['name']}\" is invalid, " . lcfirst($error['message']); + $this->errors[] = "The type of {$definition['in']} parameter \"{$definition['name']}\" is invalid, " . lcfirst($error['message']); } return false; } diff --git a/src/TypeValidator.php b/src/TypeValidator.php index cb96cf3..a4a5170 100644 --- a/src/TypeValidator.php +++ b/src/TypeValidator.php @@ -38,7 +38,7 @@ protected function formatError(array $error) if (empty($error['property'])) { return $error['message']; } else { - return "The data of \"${error['property']}\" is invalid, " . lcfirst($error['message']); + return "The data of \"{$error['property']}\" is invalid, " . lcfirst($error['message']); } } From 6dcad2c4bc34d71ac806bfee8404234bc9b6cc34 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 20 Apr 2023 18:45:57 +0800 Subject: [PATCH 10/14] Supported recursive data type --- .gitignore | 1 + src/TypeParser.php | 78 +++++++++++++++++++++++++++++++--------------- tests/TypeTest.php | 69 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index d1502b0..ff7f293 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor/ composer.lock +.idea/ diff --git a/src/TypeParser.php b/src/TypeParser.php index ee5c314..018288a 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -37,6 +37,8 @@ class TypeParser protected $schemas = []; + protected $object_chains = []; + public function __construct($mode) { $this->mode = $mode; @@ -172,47 +174,73 @@ protected function parseObject($definition) $definitionName = $definition::name(); + $isNestedType = in_array($definitionName, $this->object_chains); + if (($this->mode & self::MODE_REF_SCHEMA) && isset($this->schemas[$definition])) { return $this->makeNullableSchema([ '$ref' => '#/components/schemas/' . $definitionName, ], $nullable); } - $reflection = new \ReflectionClass($definition); - $properties = []; - $requiredFields = []; + $this->object_chains[] = $definitionName; - foreach ($reflection->getStaticProperties() as $property => $tmpDefinition) { - list($required, $schema) = $this->parseField($tmpDefinition); - $comment = $reflection->getProperty($property)->getDocComment(); - if ($comment) { - $docblock = DocBlockFactory::createInstance()->create($comment); - $schema['title'] = trim($docblock->getSummary()); - $schema['description'] = trim($docblock->getDescription()->render()); + if ($isNestedType) { + $result = $this->makeNullableSchema([ + '$ref' => '#/components/schemas/' . $definitionName, + ], $nullable); + } else { + $result = $this->parseObjectSchema($definitionName, $definition, $nullable); + } + + array_pop($this->object_chains); + + return $result; + } + + protected function parseObjectSchema(string $name, string $definition, bool $nullable): array + { + if (!isset($this->schemas[$name])) { + $reflection = new \ReflectionClass($definition); + $properties = []; + $requiredFields = []; + + foreach ($reflection->getStaticProperties() as $property => $tmpDefinition) { + list($required, $schema) = $this->parseField($tmpDefinition); + $comment = $reflection->getProperty($property)->getDocComment(); + if ($comment) { + $docblock = DocBlockFactory::createInstance()->create($comment); + $schema['title'] = trim($docblock->getSummary()); + $schema['description'] = trim($docblock->getDescription()->render()); + } + $properties[$property] = $schema; + + if ($required) { + $requiredFields[] = $property; + } } - $properties[$property] = $schema; - if ($required) { - $requiredFields[] = $property; + $schema = [ + 'type' => 'object', + 'properties' => $properties, + ]; + if ($requiredFields) { + $schema['required'] = $requiredFields; } - } - $schema = [ - 'type' => 'object', - 'properties' => $properties, - ]; - if ($requiredFields) { - $schema['required'] = $requiredFields; + $this->schemas[$name] = $schema; + } else { + $schema = $this->schemas[$name]; } - if ($this->mode & self::MODE_REF_SCHEMA) { - $this->schemas[$definitionName] = $schema; - return $this->makeNullableSchema([ - '$ref' => '#/components/schemas/' . $definitionName, + if (($this->mode & self::MODE_REF_SCHEMA)) { + $result = $this->makeNullableSchema([ + '$ref' => '#/components/schemas/' . $name, ], $nullable); } else { - return $this->makeNullableSchema($schema, $nullable); + $result = $this->makeNullableSchema($schema, $nullable); } + + return $result; } protected function parseEnum(string $definition) diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 3149331..c20f3a4 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -378,6 +378,27 @@ public function typeToArrayCases() ], ], ], + [ + RecursiveType::class, + [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + 'sub' => [ + 'oneOf' => [ + [ + 'type' => 'null' + ], + [ + '$ref' => '#/components/schemas/Recursive', + ] + ], + ], + ], + ], + ] ]; } @@ -688,6 +709,26 @@ public function testValidateInputData($inputs, $definition, $errors, $data) public function dataCases() { + $nestedSchema = [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + 'sub' => [ + 'oneOf' => [ + [ + 'type' => 'null' + ], + [ + '$ref' => '#/components/schemas/Recursive', + ] + ], + ], + ], + 'required' => ['name', 'sub'], + ]; + return [ [ '1', @@ -743,6 +784,28 @@ public function dataCases() ], [], ], + + # validate nested data type + [ + [ + 'name' => 'foobar', + 'sub' => [ + 'name' => 'sub foobar', + 'sub' => [ + 'name' => 'sub sub foobar', + 'sub' => null, + ], + ] + ], + $nestedSchema + [ + 'components' => [ + 'schemas' => [ + 'Recursive' => $nestedSchema, + ], + ] + ], + [], + ] ]; } @@ -877,3 +940,9 @@ public static function allowedTypes(): array ]; } } + +class RecursiveType extends ProductType +{ + public static $name = 'string'; + public static $sub = RecursiveType::class . '?'; +} From bcd4af192aff86b88aeaba2e8003d1e844983a5e Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 20 Apr 2023 22:45:48 +0800 Subject: [PATCH 11/14] Made union type respect MODE_REF_SCHEMA --- src/TypeParser.php | 11 +++++++++++ tests/TypeTest.php | 23 +++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index 018288a..042d7a0 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -347,6 +347,17 @@ protected function parseUnion(string $definition): array $types[] = $this->parse($allowedType); } + if ($this->mode & self::MODE_REF_SCHEMA) { + $definitionName = $definition::name(); + $this->schemas[$definitionName] = [ + 'oneOf' => $types, + ]; + + return $this->makeNullableSchema([ + '$ref' => '#/components/schemas/' . $definitionName, + ], $nullable); + } + if ($nullable) { $types[] = ['type' => 'null']; } diff --git a/tests/TypeTest.php b/tests/TypeTest.php index c20f3a4..4da3b1c 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -449,20 +449,27 @@ public function typeToArrayWithRefCases() [ 'oneOf' => [ [ - 'type' => 'string', - ], - [ - 'type' => 'integer', - ], - [ - '$ref' => '#/components/schemas/Product001', + 'type' => 'null', ], [ - 'type' => 'null', + '$ref' => '#/components/schemas/Union001', ], ], ], [ + 'Union001' => [ + 'oneOf' => [ + [ + 'type' => 'string', + ], + [ + 'type' => 'integer', + ], + [ + '$ref' => '#/components/schemas/Product001', + ], + ], + ], 'Product001' => $this->product001Schema(), ] ], From bba986205b00cf9b97493bb2fbb9e58e0dbd8ccf Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 20 Apr 2023 23:56:28 +0800 Subject: [PATCH 12/14] Supported discriminated union type --- src/TypeParser.php | 15 +++++++++++++-- tests/TypeTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index 042d7a0..78fc1a2 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -209,8 +209,19 @@ protected function parseObjectSchema(string $name, string $definition, bool $nul $comment = $reflection->getProperty($property)->getDocComment(); if ($comment) { $docblock = DocBlockFactory::createInstance()->create($comment); - $schema['title'] = trim($docblock->getSummary()); - $schema['description'] = trim($docblock->getDescription()->render()); + $title = trim($docblock->getSummary()); + if ($title) { + $schema['title'] = $title; + } + $description = trim($docblock->getDescription()->render()); + if ($description) { + $schema['description'] = $description; + } + + $tag = $docblock->getTagsByName('discriminated')[0] ?? null; + if ($tag) { + $schema['x-discriminated'] = $tag->getDescription()->render(); + } } $properties[$property] = $schema; diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 4da3b1c..cc011a9 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -378,6 +378,21 @@ public function typeToArrayCases() ], ], ], + [ + Discriminated001Type::class, + [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + 'x-discriminated' => 'foobar', + ], + 'field1' => [ + 'type' => 'string', + ], + ] + ], + ], [ RecursiveType::class, [ @@ -948,6 +963,15 @@ public static function allowedTypes(): array } } +class Discriminated001Type extends ProductType +{ + /** + * @discriminated foobar + */ + public static string $type = 'string'; + public static string $field1 = 'string'; +} + class RecursiveType extends ProductType { public static $name = 'string'; From 760d30a5c944cc1016b688255f1a6f41be83e01d Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Fri, 21 Apr 2023 10:13:23 +0800 Subject: [PATCH 13/14] Switched to enum to implement discriminated union --- src/TypeParser.php | 6 ++++-- tests/TypeTest.php | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index 78fc1a2..1bac709 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -218,9 +218,11 @@ protected function parseObjectSchema(string $name, string $definition, bool $nul $schema['description'] = $description; } - $tag = $docblock->getTagsByName('discriminated')[0] ?? null; + $tag = $docblock->getTagsByName('enum')[0] ?? null; if ($tag) { - $schema['x-discriminated'] = $tag->getDescription()->render(); + $schema['type'] = 'string'; + $schema['enum'] = preg_split("/\s*[,]\s*/u", $tag->getDescription()->render(), -1, PREG_SPLIT_NO_EMPTY); + unset($schema['$ref']); } } $properties[$property] = $schema; diff --git a/tests/TypeTest.php b/tests/TypeTest.php index cc011a9..26f4a98 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -385,7 +385,7 @@ public function typeToArrayCases() 'properties' => [ 'type' => [ 'type' => 'string', - 'x-discriminated' => 'foobar', + 'enum' => ['foobar'], ], 'field1' => [ 'type' => 'string', @@ -966,7 +966,7 @@ public static function allowedTypes(): array class Discriminated001Type extends ProductType { /** - * @discriminated foobar + * @enum foobar */ public static string $type = 'string'; public static string $field1 = 'string'; From 95d31bd02650b7625f18e76a16721ce845d13b1e Mon Sep 17 00:00:00 2001 From: devonliu02 Date: Wed, 10 Apr 2024 14:52:01 +0800 Subject: [PATCH 14/14] Support scope type security --- composer.json | 2 +- src/DocGenerator.php | 121 ++++++++++++++++++++++++++++++-- src/security/ScopeInterface.php | 14 ++++ src/security/SecurityScheme.php | 19 +++++ 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/security/ScopeInterface.php create mode 100644 src/security/SecurityScheme.php diff --git a/composer.json b/composer.json index 5fb8891..0d90f2a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.1", "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "justinrainbow/json-schema": "^5.2" }, diff --git a/src/DocGenerator.php b/src/DocGenerator.php index c10610f..01d0904 100644 --- a/src/DocGenerator.php +++ b/src/DocGenerator.php @@ -2,10 +2,12 @@ namespace rethink\typedphp; -use ReflectionClass; use InvalidArgumentException; -use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlockFactory; +use ReflectionClass; +use rethink\typedphp\security\ScopeInterface; +use const PREG_SPLIT_NO_EMPTY; /** * Class DocGenerator @@ -63,6 +65,7 @@ public function buildApiObject($apiClass) 'operationId' => $this->getStaticProperty($class, 'op'), 'parameters' => $parameters ? $this->parser->parse($parameters) : [], 'responses' => (object)$this->buildResponses($apiClass, $class), + 'security' => $this->buildSecurity($apiClass), ]; if ($bodyDefinition = $this->buildRequestBody($apiClass, $class)) { @@ -120,17 +123,75 @@ private function parseContentType(\ReflectionClass $class) $docblock = DocBlockFactory::createInstance()->create($comment); $tags = $docblock->getTagsByName('content-type'); if (count($tags)) { - return trim((string) $tags[0]->getDescription()); + return trim((string)$tags[0]->getDescription()); } } return null; } + /** + * @param $apiClass + * @return array + * @link https://spec.openapis.org/oas/v3.0.1#security-requirement-object + */ + protected function buildSecurity($apiClass): array + { + if (method_exists($apiClass, 'scopes') === false) { + return []; + } + + $scopes = $apiClass::scopes(); + + // supported format: + // 1. Scope + // 2. [Scope1, Scope2] AND relation + // 3. [[Scope1, Scope2], [Scope3, Scope4]] OR relation + + // only 1 scope + if ($scopes instanceof ScopeInterface) { + return [ + [ + $scopes->schemeName() => [ + $scopes->name(), + ], + ] + ]; + } + + if (is_array($scopes)) { + // multiple scopes with AND relation + if (current($scopes) instanceof ScopeInterface) { + return [ + $this->buildForAndScopes(...$scopes) + ]; + } + // multiple scopes with OR relation + if (is_array(current($scopes))) { + $security = []; + foreach ($scopes as $scope) { + $security[] = $this->buildForAndScopes(...$scope); + } + return $security; + } + } + + throw new InvalidArgumentException('Invalid scopes definition.'); + } + + protected function buildForAndScopes(ScopeInterface ...$scopes): array + { + $result = []; + foreach ($scopes as $scope) { + $result[$scope->schemeName()][] = $scope->name(); + } + return $result; + } + protected function buildResponses($apiClass, \ReflectionClass $class) { $responses = []; - foreach ($apiClass::responses() as $code => $responseDefinition) { + foreach ($apiClass::responses() as $code => $responseDefinition) { if ($responseDefinition !== null) { $responses[$code] = [ @@ -186,6 +247,58 @@ public function generate() return [ 'paths' => (object)$this->buildPathsObject(), 'schemas' => (object)$this->parser->getSchemas(), + 'securitySchemes' => (object)$this->buildSecuritySchemes(), ]; } + + /** + * @return array + * @link https://spec.openapis.org/oas/v3.0.1#security-scheme-object + */ + protected function buildSecuritySchemes(): array + { + $scopes = []; + foreach ($this->apiClasses as $apiClass) { + if (method_exists($apiClass, 'scopes')) { + $scopes[] = $this->collectScopes($apiClass::scopes()); + } + } + $scopes = array_merge([], ...$scopes); + + $securitySchemes = []; + /** @var ScopeInterface $scope */ + foreach ($scopes as $scope) { + if (isset($securitySchemes[$scope->schemeName()])) { + $securitySchemes[$scope->schemeName()]['flows']['authorizationCode']['scopes'][$scope->name()] = $scope->description(); + } else { + $securitySchemes[$scope->schemeName()] = [ + 'type' => 'oauth2', + 'flows' => [ + 'clientCredentials' => [ + 'tokenUrl' => '', + 'scopes' => [ + $scope->name() => $scope->description(), + ], + ], + ], + ]; + } + } + return $securitySchemes; + } + + protected function collectScopes($scopes): array + { + if ($scopes instanceof ScopeInterface) { + return [$scopes]; + } + if (is_array($scopes)) { + $result = []; + foreach ($scopes as $scope) { + $result[] = $this->collectScopes($scope); + } + return array_merge([], ...$result); + } + throw new InvalidArgumentException('Invalid scopes definition.'); + } } diff --git a/src/security/ScopeInterface.php b/src/security/ScopeInterface.php new file mode 100644 index 0000000..baa3670 --- /dev/null +++ b/src/security/ScopeInterface.php @@ -0,0 +1,14 @@ +