From 284dc35efd373333a75c722da0d700c7ceed431a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20Schr=C3=B6der?= Date: Fri, 23 Feb 2024 13:47:43 +0100 Subject: [PATCH 1/8] feat: add required to responseField tag and append the required fields to generateResponseContentSpec for object types --- camel/Extraction/ResponseField.php | 3 +++ .../ResponseFields/GetFromResponseFieldTag.php | 14 +++++++++----- src/Writing/OpenAPISpecWriter.php | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/camel/Extraction/ResponseField.php b/camel/Extraction/ResponseField.php index ffc92baf..ebec6db6 100644 --- a/camel/Extraction/ResponseField.php +++ b/camel/Extraction/ResponseField.php @@ -19,5 +19,8 @@ class ResponseField extends BaseDTO /** @var string */ public $type; + /** @var boolean */ + public $required; + public array $enumValues = []; } diff --git a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php index d955132b..03e680ab 100644 --- a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php +++ b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php @@ -16,22 +16,26 @@ class GetFromResponseFieldTag extends GetFieldsFromTagStrategy protected function parseTag(string $tagContent): array { // Format: - // @responseField + // @responseField // Examples: - // @responseField text string The text. + // @responseField text string required The text. // @responseField user_id integer The ID of the user. - preg_match('/(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content); + preg_match('/(.+?)\s+(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content); if (empty($content)) { // This means only name and type were supplied [$name, $type] = preg_split('/\s+/', $tagContent); $description = ''; } else { - [$_, $name, $type, $description] = $content; + [$_, $name, $type, $required, $description] = $content; + if($required !== "required"){ + $description = $required . $description; + $required = false; + } $description = trim($description); } $type = static::normalizeTypeName($type); - $data = compact('name', 'type', 'description'); + $data = compact('name', 'type', 'required', 'description'); // Support optional type in annotation // The type can also be a union or nullable type (eg ?string or string|null) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 509c01f0..85276c22 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -381,6 +381,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE $properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) { return [$key => $this->generateSchemaForValue($value, $endpoint, $key)]; })->toArray(); + $required = $this->generateRequired($endpoint); return [ 'application/json' => [ @@ -388,6 +389,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE 'type' => 'object', 'example' => $decoded, 'properties' => $this->objectIfEmpty($properties), + 'required' => $this->objectIfEmpty($required) ], ], ]; @@ -586,4 +588,20 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin return $schema; } + + /** + * Given a enpoint, generate the required fields for it. The $endpoint is used for looking up response + * field custom. + */ + public function generateRequired(OutputEndpointData $endpoint,): array + { + $requiredFields = []; + $responseFields = $endpoint->responseFields; + foreach ($responseFields as $fieldName => $fieldValue) { + if(isset($fieldValue->required) && $fieldValue->required ){ + $requiredFields[] = $fieldName; + } + } + return $requiredFields; + } } From 91e86b966cc172a5aa768a925fad124bdbfca12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20Schr=C3=B6der?= Date: Fri, 23 Feb 2024 14:02:53 +0100 Subject: [PATCH 2/8] fix: set required key only if there are required fields in the response type --- src/Writing/OpenAPISpecWriter.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 85276c22..14c04880 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -381,18 +381,24 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE $properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) { return [$key => $this->generateSchemaForValue($value, $endpoint, $key)]; })->toArray(); - $required = $this->generateRequired($endpoint); - return [ + $data = [ 'application/json' => [ 'schema' => [ 'type' => 'object', 'example' => $decoded, - 'properties' => $this->objectIfEmpty($properties), - 'required' => $this->objectIfEmpty($required) + 'properties' => $this->objectIfEmpty($properties) ], ], ]; + + $required = $this->generateRequired($endpoint); + + if(! empty( $required)){ + $data['application/json']['schema']['required'] = $required; + } + + return $data; } } From f82f75e124fa8370eb7ca0e10e94eb51ccb39771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20Schr=C3=B6der?= Date: Fri, 23 Feb 2024 14:21:05 +0100 Subject: [PATCH 3/8] fix: add space when readding required to description after recognizing required is part of the description --- .../Strategies/ResponseFields/GetFromResponseFieldTag.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php index 03e680ab..36ce95c3 100644 --- a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php +++ b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php @@ -25,12 +25,14 @@ protected function parseTag(string $tagContent): array // This means only name and type were supplied [$name, $type] = preg_split('/\s+/', $tagContent); $description = ''; + $required = false; } else { [$_, $name, $type, $required, $description] = $content; if($required !== "required"){ - $description = $required . $description; - $required = false; + $description = $required . " " . $description; } + + $required = $required === "required"; $description = trim($description); } From e669ceb5cc4914b5c1d85d4fd3dbb092acbf5089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20Schr=C3=B6der?= Date: Fri, 23 Feb 2024 14:29:16 +0100 Subject: [PATCH 4/8] refactor: make linter happy --- src/Writing/OpenAPISpecWriter.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 14c04880..62ae9e9e 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -374,7 +374,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], 'example' => $decoded, ], - ], + ], ]; case 'object': @@ -387,14 +387,14 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE 'schema' => [ 'type' => 'object', 'example' => $decoded, - 'properties' => $this->objectIfEmpty($properties) + 'properties' => $this->objectIfEmpty($properties), ], ], ]; $required = $this->generateRequired($endpoint); - if(! empty( $required)){ + if(!empty($required)){ $data['application/json']['schema']['required'] = $required; } @@ -599,12 +599,12 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin * Given a enpoint, generate the required fields for it. The $endpoint is used for looking up response * field custom. */ - public function generateRequired(OutputEndpointData $endpoint,): array + public function generateRequired(OutputEndpointData $endpoint): array { $requiredFields = []; $responseFields = $endpoint->responseFields; foreach ($responseFields as $fieldName => $fieldValue) { - if(isset($fieldValue->required) && $fieldValue->required ){ + if(isset($fieldValue->required) && $fieldValue->required){ $requiredFields[] = $fieldName; } } From 768da9dcda8289cfc10532554b2926fedf1e4fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20Schr=C3=B6der?= Date: Fri, 23 Feb 2024 14:30:58 +0100 Subject: [PATCH 5/8] fix: mark required field as optional inside the format --- .../Strategies/ResponseFields/GetFromResponseFieldTag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php index 36ce95c3..395d9d22 100644 --- a/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php +++ b/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php @@ -16,7 +16,7 @@ class GetFromResponseFieldTag extends GetFieldsFromTagStrategy protected function parseTag(string $tagContent): array { // Format: - // @responseField + // @responseField <"required" (optional)> // Examples: // @responseField text string required The text. // @responseField user_id integer The ID of the user. From ff45ff8c22277f323afc4cc1b4233f51a8b9a274 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 8 Mar 2024 14:09:47 +0100 Subject: [PATCH 6/8] fix: only add top-level properties of object as required --- src/Writing/OpenAPISpecWriter.php | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 4a6ffcd6..eba224a2 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -381,6 +381,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE $properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) { return [$key => $this->generateSchemaForValue($value, $endpoint, $key)]; })->toArray(); + $required = $this->filterRequiredFields($endpoint, array_keys($properties)); $data = [ 'application/json' => [ @@ -391,10 +392,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], ], ]; - - $required = $this->generateRequired($endpoint); - - if(!empty($required)){ + if ($required) { $data['application/json']['schema']['required'] = $required; } @@ -567,11 +565,16 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin $subFieldPath = sprintf('%s.%s', $path, $subField); $properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath); } + $required = $this->filterRequiredFields($endpoint, array_keys($properties)); - return [ + $schema = [ 'type' => 'object', 'properties' => $this->objectIfEmpty($properties), ]; + if ($required) { + $schema['required'] = $required; + } + return $schema; } $schema = [ @@ -600,18 +603,17 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin } /** - * Given a enpoint, generate the required fields for it. The $endpoint is used for looking up response - * field custom. + * Given an enpoint and a set of properties, return the properties that are specified as required. */ - public function generateRequired(OutputEndpointData $endpoint): array - { - $requiredFields = []; - $responseFields = $endpoint->responseFields; - foreach ($responseFields as $fieldName => $fieldValue) { - if(isset($fieldValue->required) && $fieldValue->required){ - $requiredFields[] = $fieldName; + public function filterRequiredFields(OutputEndpointData $endpoint, array $properties): array + { + $required = []; + foreach ($properties as $property) { + if (isset($endpoint->responseFields[$property]) && $endpoint->responseFields[$property]->required) { + $required[] = $property; } } - return $requiredFields; + + return $required; } } From 141554aaef90c42bc074fa6005eb198fde07c117 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 26 Sep 2024 16:59:58 +0200 Subject: [PATCH 7/8] fix: add fields as required given nested path and add test --- src/Writing/OpenAPISpecWriter.php | 9 ++++--- tests/Unit/OpenAPISpecWriterTest.php | 37 ++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index eba224a2..08adc858 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -565,7 +565,7 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin $subFieldPath = sprintf('%s.%s', $path, $subField); $properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath); } - $required = $this->filterRequiredFields($endpoint, array_keys($properties)); + $required = $this->filterRequiredFields($endpoint, array_keys($properties), $path); $schema = [ 'type' => 'object', @@ -603,13 +603,14 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin } /** - * Given an enpoint and a set of properties, return the properties that are specified as required. + * Given an enpoint and a set of object keys at a path, return the properties that are specified as required. */ - public function filterRequiredFields(OutputEndpointData $endpoint, array $properties): array + public function filterRequiredFields(OutputEndpointData $endpoint, array $properties, string $path = ''): array { $required = []; foreach ($properties as $property) { - if (isset($endpoint->responseFields[$property]) && $endpoint->responseFields[$property]->required) { + $responseField = $endpoint->responseFields["$path.$property"] ?? $endpoint->responseFields[$property] ?? null; + if ($responseField && $responseField->required) { $required[] = $property; } } diff --git a/tests/Unit/OpenAPISpecWriterTest.php b/tests/Unit/OpenAPISpecWriterTest.php index 6d2c94a5..0a1660c3 100644 --- a/tests/Unit/OpenAPISpecWriterTest.php +++ b/tests/Unit/OpenAPISpecWriterTest.php @@ -436,7 +436,7 @@ public function adds_responses_correctly_as_responses_on_operation_object() [ 'status' => 201, 'description' => '', - 'content' => '{"this": "shouldn\'t be ignored", "and this": "too", "sub level 0": { "sub level 1 key 1": "sl0_sl1k1", "sub level 1 key 2": [ { "sub level 2 key 1": "sl0_sl1k2_sl2k1", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k2_sl2k2_sl3k1" } } ], "sub level 1 key 3": { "sub level 2 key 1": "sl0_sl1k3_sl2k2", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k3_sl2k2_sl3k1", "sub level 3 key null": null, "sub level 3 key integer": 99 } } } }', + 'content' => '{"this": "shouldn\'t be ignored", "and this": "too", "also this": "too", "sub level 0": { "sub level 1 key 1": "sl0_sl1k1", "sub level 1 key 2": [ { "sub level 2 key 1": "sl0_sl1k2_sl2k1", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k2_sl2k2_sl3k1" } } ], "sub level 1 key 3": { "sub level 2 key 1": "sl0_sl1k3_sl2k2", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k3_sl2k2_sl3k1", "sub level 3 key null": null, "sub level 3 key integer": 99 }, "sub level 2 key 3 required" : "sl0_sl1k3_sl2k3" } } }', ], ], 'responseFields' => [ @@ -445,9 +445,19 @@ public function adds_responses_correctly_as_responses_on_operation_object() 'type' => 'string', 'description' => 'Parameter description, ha!', ], + 'also this' => [ + 'name' => 'also this', + 'type' => 'string', + 'description' => 'This response parameter is required.', + 'required' => true, + ], 'sub level 0.sub level 1 key 3.sub level 2 key 1'=> [ - 'description' => 'This is description of nested object', - ] + 'description' => 'This is a description of a nested object', + ], + 'sub level 0.sub level 1 key 3.sub level 2 key 3 required'=> [ + 'description' => 'This is a description of a required nested object', + 'required' => true, + ], ], ]); $endpointData2 = $this->createMockEndpointData([ @@ -485,6 +495,11 @@ public function adds_responses_correctly_as_responses_on_operation_object() 'example' => "too", 'type' => 'string', ], + 'also this' => [ + 'description' => 'This response parameter is required.', + 'example' => "too", + 'type' => 'string', + ], 'sub level 0' => [ 'type' => 'object', 'properties' => [ @@ -512,7 +527,7 @@ public function adds_responses_correctly_as_responses_on_operation_object() 'sub level 2 key 1' => [ 'type' => 'string', 'example' => 'sl0_sl1k3_sl2k2', - 'description' => 'This is description of nested object' + 'description' => 'This is a description of a nested object' ], 'sub level 2 key 2' => [ 'type' => 'object', @@ -530,12 +545,24 @@ public function adds_responses_correctly_as_responses_on_operation_object() 'example' => 99 ] ] - ] + ], + 'sub level 2 key 3 required' => [ + 'type' => 'string', + 'example' => 'sl0_sl1k3_sl2k3', + 'description' => 'This is a description of a required nested object' + ], + + ], + 'required' => [ + 'sub level 2 key 3 required' ] ] ] ] ], + 'required' => [ + 'also this' + ] ], ], ], From ac6d9c5b74f78a7a68bc6094ee79a7b11220b051 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 4 Oct 2024 10:49:01 +0200 Subject: [PATCH 8/8] test: add test for required parameter of response field attribute --- .../GetFromResponseFieldAttributesTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Strategies/ResponseFields/GetFromResponseFieldAttributesTest.php b/tests/Strategies/ResponseFields/GetFromResponseFieldAttributesTest.php index 490e8936..f9c44947 100644 --- a/tests/Strategies/ResponseFields/GetFromResponseFieldAttributesTest.php +++ b/tests/Strategies/ResponseFields/GetFromResponseFieldAttributesTest.php @@ -46,11 +46,19 @@ public function can_fetch_from_responsefield_attribute() 'id' => [ 'type' => 'integer', 'description' => 'The id of the newly created user.', + 'required' => true, ], 'other' => [ 'type' => 'string', 'description' => '', + 'required' => true, ], + 'required_attribute' => [ + 'required' => true, + ], + 'not_required_attribute' => [ + 'required' => false, + ] ], $results); } @@ -98,6 +106,8 @@ class ResponseFieldAttributeTestController { #[ResponseField('id', description: 'The id of the newly created user.')] #[ResponseField('other', 'string')] + #[ResponseField('required_attribute', required: true)] + #[ResponseField('not_required_attribute', required: false)] public function methodWithAttributes() { }