From 2d4a5e929c92e2394f2a1302a944342adf2fb4e4 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Mon, 21 Oct 2024 09:54:07 +0200 Subject: [PATCH] fix: add OpenAPI fields (#644) --- .editorconfig | 8 + .../Model/Elements/BasicStructureElement.php | 9 +- .../Model/Elements/ObjectStructureElement.php | 3 +- .../Tests/ArrayStructureElementTest.php | 6 +- .../Tests/BasicStructureElementTest.php | 2 +- .../Tests/EnumStructureElementTest.php | 6 +- .../Tests/ObjectStructureElementTest.php | 4 +- .../Elements/Tests/RequestBodyElementTest.php | 4 +- src/PHPDraft/Model/HierarchyElement.php | 4 +- .../Model/Tests/ObjectElementTest.php | 2 +- src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php | 310 ++++++++++++++---- .../Out/OpenAPI/Tests/OpenApiRendererTest.php | 2 +- tests/statics/openapi/empty.json | 4 +- 13 files changed, 280 insertions(+), 84 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e94d260e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*.html] +indent_size = 2 +indent_style = space + +[*.php] +indent_size = 2 +tab_width = 2 +indent_style = space diff --git a/src/PHPDraft/Model/Elements/BasicStructureElement.php b/src/PHPDraft/Model/Elements/BasicStructureElement.php index 8dc886f9..ac7952d0 100644 --- a/src/PHPDraft/Model/Elements/BasicStructureElement.php +++ b/src/PHPDraft/Model/Elements/BasicStructureElement.php @@ -45,9 +45,9 @@ abstract class BasicStructureElement implements StructureElement /** * Object status (required|optional). * - * @var string|null + * @var string[] */ - public ?string $status = ''; + public array $status = []; /** * Parent structure. * @@ -130,14 +130,13 @@ protected function parse_common(object $object, array &$dependencies): void $this->is_variable = $object->attributes->variable->content ?? false; - $this->status = null; if (isset($object->attributes->typeAttributes->content)) { $data = array_map(function ($item) { return $item->content; }, $object->attributes->typeAttributes->content); - $this->status = join(', ', $data); + $this->status = $data; } elseif (isset($object->attributes->typeAttributes)) { - $this->status = join(', ', $object->attributes->typeAttributes); + $this->status = $object->attributes->typeAttributes; } if (!in_array($this->type, self::DEFAULTS, true) && $this->type !== null) { diff --git a/src/PHPDraft/Model/Elements/ObjectStructureElement.php b/src/PHPDraft/Model/Elements/ObjectStructureElement.php index abc14ab9..cb540f78 100644 --- a/src/PHPDraft/Model/Elements/ObjectStructureElement.php +++ b/src/PHPDraft/Model/Elements/ObjectStructureElement.php @@ -214,6 +214,7 @@ protected function construct_string_return(string $value): string $desc = MarkdownExtra::defaultTransform($this->description); } - return "{$this->key->value}{$variable}{$type} {$this->status}{$desc}{$value}"; + $status_string = join(', ', $this->status); + return "{$this->key->value}{$variable}{$type} {$status_string}{$desc}{$value}"; } } diff --git a/src/PHPDraft/Model/Elements/Tests/ArrayStructureElementTest.php b/src/PHPDraft/Model/Elements/Tests/ArrayStructureElementTest.php index 8ca65b54..0153b442 100644 --- a/src/PHPDraft/Model/Elements/Tests/ArrayStructureElementTest.php +++ b/src/PHPDraft/Model/Elements/Tests/ArrayStructureElementTest.php @@ -65,7 +65,7 @@ public static function parseObjectProvider(): array $val2->value = 'Objective-C'; $val2->type = 'string'; $base1->value = [$val1, $val2]; - $base1->status = null; + $base1->status = []; $base1->element = 'array'; $base1->type = null; $base1->is_variable = false; @@ -82,7 +82,7 @@ public static function parseObjectProvider(): array $val2->value = 'another item'; $val2->type = 'string'; $base2->value = [$val1, $val2]; - $base2->status = null; + $base2->status = []; $base2->element = 'array'; $base2->type = 'Some simple array'; $base2->is_variable = false; @@ -101,7 +101,7 @@ public static function parseObjectProvider(): array $val2->value = null; $val2->type = 'array'; $base3->value = [$val1, $val2]; - $base3->status = 'optional'; + $base3->status = ['optional']; $base3->element = 'member'; $base3->type = 'array'; $base3->is_variable = false; diff --git a/src/PHPDraft/Model/Elements/Tests/BasicStructureElementTest.php b/src/PHPDraft/Model/Elements/Tests/BasicStructureElementTest.php index ad9b6223..4fd7806f 100644 --- a/src/PHPDraft/Model/Elements/Tests/BasicStructureElementTest.php +++ b/src/PHPDraft/Model/Elements/Tests/BasicStructureElementTest.php @@ -150,7 +150,7 @@ public static function parseValueProvider(): array $obj2 = clone $obj; $obj2->attributes->typeAttributes = [1, 2]; - $answer->status = '1, 2'; + $answer->status = [1, 2]; $return[] = [$obj2, $answer]; diff --git a/src/PHPDraft/Model/Elements/Tests/EnumStructureElementTest.php b/src/PHPDraft/Model/Elements/Tests/EnumStructureElementTest.php index dcb96800..537a4b07 100644 --- a/src/PHPDraft/Model/Elements/Tests/EnumStructureElementTest.php +++ b/src/PHPDraft/Model/Elements/Tests/EnumStructureElementTest.php @@ -151,7 +151,7 @@ public static function parseObjectProvider(): array $base1 = new EnumStructureElement(); $base1->key = null; $base1->value = [ $value1, $value2 ]; - $base1->status = null; + $base1->status = []; $base1->element = 'enum'; $base1->type = 'Some simple enum'; $base1->is_variable = false; @@ -164,7 +164,7 @@ public static function parseObjectProvider(): array $base2->key->type = 'string'; $base2->key->value = 'car_id_list'; $base2->value = 'world'; - $base2->status = null; + $base2->status = []; $base2->element = 'enum'; $base2->type = 'string'; $base2->description = null; @@ -177,7 +177,7 @@ public static function parseObjectProvider(): array $base3->key->type = 'number'; $base3->key->value = '5'; $base3->value = '5'; - $base3->status = 'optional'; + $base3->status = ['optional']; $base3->element = 'member'; $base3->type = 'number'; $base3->description = "List of car identifiers to retrieve"; diff --git a/src/PHPDraft/Model/Elements/Tests/ObjectStructureElementTest.php b/src/PHPDraft/Model/Elements/Tests/ObjectStructureElementTest.php index 03821d86..193072cc 100644 --- a/src/PHPDraft/Model/Elements/Tests/ObjectStructureElementTest.php +++ b/src/PHPDraft/Model/Elements/Tests/ObjectStructureElementTest.php @@ -82,7 +82,7 @@ public static function parseObjectProvider(): array $base1->key->type = 'string'; $base1->key->value = 'name'; $base1->value = 'P10'; - $base1->status = 'optional'; + $base1->status = ['optional']; $base1->element = 'member'; $base1->type = 'string'; $base1->is_variable = false; @@ -95,7 +95,7 @@ public static function parseObjectProvider(): array $base2->key->type = 'string'; $base2->key->value = 'Auth2'; $base2->value = 'something'; - $base2->status = 'required'; + $base2->status = ['required']; $base2->element = 'member'; $base2->type = 'string'; $base2->is_variable = false; diff --git a/src/PHPDraft/Model/Elements/Tests/RequestBodyElementTest.php b/src/PHPDraft/Model/Elements/Tests/RequestBodyElementTest.php index e62e4ad8..334fe1d8 100644 --- a/src/PHPDraft/Model/Elements/Tests/RequestBodyElementTest.php +++ b/src/PHPDraft/Model/Elements/Tests/RequestBodyElementTest.php @@ -142,7 +142,7 @@ public static function parseObjectProvider(): array $base1->key->value = 'name'; $base1->key->description = null; $base1->value = 'P10'; - $base1->status = 'optional'; + $base1->status = ['optional']; $base1->element = 'member'; $base1->type = 'string'; $base1->is_variable = false; @@ -156,7 +156,7 @@ public static function parseObjectProvider(): array $base2->key->value = 'Auth2'; $base2->key->description = null; $base2->value = 'something'; - $base2->status = 'required'; + $base2->status = ['required']; $base2->element = 'member'; $base2->type = 'string'; $base2->is_variable = false; diff --git a/src/PHPDraft/Model/HierarchyElement.php b/src/PHPDraft/Model/HierarchyElement.php index 04453639..a2d27900 100644 --- a/src/PHPDraft/Model/HierarchyElement.php +++ b/src/PHPDraft/Model/HierarchyElement.php @@ -27,9 +27,9 @@ abstract class HierarchyElement /** * Description of the element. * - * @var string + * @var string|null */ - public string $description; + public ?string $description = NULL; /** * Child elements. diff --git a/src/PHPDraft/Model/Tests/ObjectElementTest.php b/src/PHPDraft/Model/Tests/ObjectElementTest.php index 817a172f..bea4b244 100644 --- a/src/PHPDraft/Model/Tests/ObjectElementTest.php +++ b/src/PHPDraft/Model/Tests/ObjectElementTest.php @@ -83,7 +83,7 @@ public function testValueSetup(): void */ public function testStatusSetup(): void { - $this->assertSame('', $this->class->status); + $this->assertSame([], $this->class->status); } /** diff --git a/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php index 920687cc..36ac7684 100644 --- a/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php +++ b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php @@ -2,10 +2,14 @@ namespace PHPDraft\Out\OpenAPI; +use PHPDraft\Model\Elements\BasicStructureElement; +use PHPDraft\Model\Elements\ElementStructureElement; +use PHPDraft\Model\Elements\StructureElement; use PHPDraft\Model\HTTPRequest; use PHPDraft\Model\HTTPResponse; +use PHPDraft\Model\Resource; +use PHPDraft\Model\Transition; use PHPDraft\Out\BaseTemplateRenderer; -use stdClass; class OpenApiRenderer extends BaseTemplateRenderer { @@ -18,7 +22,7 @@ public function init(object $json): self public function write(string $filename): void { - $output = json_encode($this->toOpenApiObject(), JSON_PRETTY_PRINT); + $output = json_encode($this->toOpenApiObject(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); file_put_contents($filename, $output); } @@ -92,21 +96,43 @@ private function getServers(): array { private function getPaths(): object { $return = []; foreach ($this->categories as $category) { + /** @var Resource $resource */ foreach ($category->children ?? [] as $resource) { + /** @var Transition $transition */ foreach ($resource->children ?? [] as $transition) { $transition_return = []; - $transition_return['parameters'] = []; + $parameters = []; if ($transition->url_variables !== []) { - $transition_return['parameters'] = $this->toParameters($transition->url_variables, $transition->href); + $parameters += $this->toParameters($transition->url_variables, $transition->href); } + if ($transition->data_variables !== NULL) + { + $parameters += $this->toParameters([$transition->data_variables], $transition->href); + } + $transition_return['parameters'] = $parameters; - foreach ($transition->requests as $request) { + /** @var HTTPRequest $request */ + foreach ($transition->requests as $request) + { $request_return = [ 'operationId' => $request->get_id(), 'responses' => $this->toResponses($transition->responses), + 'summary' => $request->title ?? $transition->title, + 'description' => $request->description ?? $transition->description, ]; + + $parameters = []; + if ($request->struct !== NULL) { + if (is_array($request->struct)) + { + $parameters += $this->toParameters($request->struct, $transition->href); + } else { + $parameters += $this->toParameters([$request->struct], $transition->href); + } + } + $request_return['tags'] = [$category->title]; if (isset($transition_return['parameters']) && $transition_return['parameters'] !== []) { - $request_return['parameters'] = $transition_return['parameters']; + $request_return['parameters'] = array_merge($transition_return['parameters'], $parameters); } if ($request->body !== NULL) { $request_return['requestBody'] = $this->toBody($request); @@ -141,14 +167,22 @@ private function toParameters(array $objects, string $href): array { $return = []; foreach ($objects as $variable) { + if ($variable->key === NULL) + { + continue; + } + $return_tmp = [ 'name' => $variable->key->value, 'in' => str_contains($href, '{' . $variable->key->value . '}') ? 'path' : 'query', 'required' => $variable->status === 'required', - 'schema' => [ - 'type' => $variable->type, - ], + 'schema' => [], ]; + if ($this->isRef($variable->type)) { + $return_tmp['schema']['$ref'] = '#/components/schemas/' . $variable->type; + } else { + $return_tmp['schema']['type'] = $variable->type; + } if (isset($variable->value)) { @@ -165,47 +199,9 @@ private function toParameters(array $objects, string $href): array { return $return; } - /** - * Convert a HTTP Request into an OpenAPI body - * - * @param HTTPRequest $request Request to convert - * - * @return array> OpenAPI style body - */ - private function toBody(HTTPRequest $request): array + private function isRef(string $type): bool { - $return = []; - - if (!is_array($request->struct)) { - $return['description'] = $request->struct->description; - } - - $content_type = $request->headers['Content-Type'] ?? 'text/plain'; - if (isset($request->struct) && $request->struct !== []) - { - $return['content'] = [ - $content_type => [ - 'schema' => [ - 'type' => $request->struct->element, - 'properties' => array_map(fn($value) => [$value->key->value => ['type' => $value->type]], $request->struct->value), - ], - ], - ]; - } else { - $return['content'] = [ - $content_type => [ - 'schema' => [ - 'type' => 'string', - ], - ], - ]; - } - - if ($request->body !== NULL && $request->body !== []) { - $return['content'][$content_type]['example'] = $request->body[0]; - } - - return $return; + return !in_array($type, ["array", "boolean", "integer", "null", "number", "object", "string"], TRUE); } /** @@ -227,17 +223,21 @@ private function toResponses(array $responses): array 'schema' => [ 'type' => 'string', 'example' => $value, - ] + ], ]; } $content = []; foreach ($response->content as $key => $contents) { $content[$key] = [ - "schema"=> [ - "type"=> "string", - "example"=> $contents - ] + 'schema' => [ + 'type' => "string", + ], + 'examples' => [ + 'base' => [ + 'value' => $contents, + ], + ], ]; } foreach ($response->structure as $structure) { @@ -248,10 +248,12 @@ private function toResponses(array $responses): array "properties"=> [ $structure->key->value => [ "type" => $structure->type, - 'example' => $structure->value, - ] - ] - ] + ], + ], + 'example' => [ + $structure->key->value => $structure->value, + ], + ], ]; } $return[$response->statuscode] = [ @@ -264,6 +266,53 @@ private function toResponses(array $responses): array return $return; } + /** + * Convert a HTTP Request into an OpenAPI body + * + * @param HTTPRequest $request Request to convert + * + * @return array> OpenAPI style body + */ + private function toBody(HTTPRequest $request): array + { + $return = []; + + if (!is_array($request->struct) && $request->struct->description !== NULL) { + $return['description'] = $request->struct->description; + } + + $content_type = $request->headers['Content-Type'] ?? 'text/plain'; + if (isset($request->struct) && $request->struct !== []) + { + $properties = []; + foreach ($request->struct->value as $value) { + $properties[$value->key->value] = ['type' => $value->type]; + } + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => $request->struct->element, + 'properties' => $properties, + ], + ], + ]; + } else { + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => 'string', + ], + ], + ]; + } + + if ($request->body !== NULL && $request->body !== []) { + $return['content'][$content_type]['examples']['base']['value'] = $request->body[0]; + } + + return $return; + } + /** * Get webhook information for the API. * @return object @@ -274,7 +323,130 @@ private function getWebhooks(): object { return (object) []; } * Get component information for the API. * @return object */ - private function getComponents(): object { return (object) []; } + private function getComponents(): object { + $return = []; + foreach ($this->base_structures as $structure) + { + $object = $this->getComponent($structure); + + if ($structure->ref !== NULL) { + $return[$structure->type] = [ + 'allOf' => [ + ['$ref' => "#/components/schemas/$structure->ref"], + $object, + ], + ]; + } else { + $return[$structure->type] = $object; + } + } + return (object) ['schemas' => $return ]; + } + + /** + * Get a component + * + * @param BasicStructureElement $structure + * + * @return array + */ + private function getComponent(BasicStructureElement $structure): array + { + $required = []; + $properties = []; + if ($structure->value !== NULL) + { + /** @var BasicStructureElement $value */ + foreach ($structure->value as $value) + { + $propery_data = $this->getSchemaProperty($value); + if ($propery_data === NULL) { continue; } + if (in_array('required', $value->status, TRUE)) { $required[] = $value->key->value;} + + $properties[$value->key->value] = $propery_data; + } + } + + $object = [ + 'type' => $structure->element, + ]; + switch ($structure->element) { + case 'enum': + case 'array': + $object['items'] = $properties; + break; + case 'object': + $object['properties'] = $properties; + $object['required'] = $required; + break; + default: + break; + } + + if ($structure->description !== NULL) { + $object['description'] = $structure->description; + } + + return $object; + } + + /** + * Get property in a schema + * + * @param BasicStructureElement|ElementStructureElement $value Data to convert + * + * @return array|null + */ + private function getSchemaProperty(BasicStructureElement|ElementStructureElement $value): ?array + { + //TODO: Check this case + if ($value instanceof ElementStructureElement || $value->key === NULL) + { + return NULL; + } + + $propery_data = []; + if ($value->description !== NULL) { + $propery_data['description'] = $value->description; + } + + if ($this->isRef($value->type) && $value->type !== 'enum') + { + $propery_data['$ref'] = '#/components/schemas/' . $value->type; + return $propery_data; + } + + if ($value->type === 'enum') { + $propery_data['type'] = in_array('nullable', $value->status, TRUE) ? [ $value->type, 'null' ] : $value->type; + $options = []; + foreach ($value->value->value as $option) { + if ($option instanceof ElementStructureElement) { + $options[] = ['const' => $option->value, 'title' => $option->value]; + } + } + $propery_data['oneOf'] = $options; + + return $propery_data; + } + + if ($value->type === 'array') { + $propery_data['type'] = array_unique(array_map(fn($item) => $item->type,$value->value->value)); + $propery_data['example'] = array_merge(array_filter(array_map(fn($item) => $item->value,$value->value->value))); + + return $propery_data; + } + + if ($value->type === 'object') { + $propery_data['type'] = $value->type; + $propery_data['properties'] = $this->getComponent($value->value)['properties']; + + return $propery_data; + } + + $propery_data['type'] = in_array('nullable', $value->status, TRUE) ? [ $value->type, 'null' ] : $value->type; + + return $propery_data; + } /** * Get security information for the API @@ -282,12 +454,26 @@ private function getComponents(): object { return (object) []; } */ private function getSecurity(): array { return []; } +// private function getDocs(): object { return (object) []; } + /** * Get tags for the API - * @return string[] + * @return array> */ - private function getTags(): array { return []; } + private function getTags(): array { + $return = []; + foreach ($this->categories as $category) { + $data = [ + 'name' => $category->title, + ]; + if ($category->description !== NULL) { + $data['description'] = $category->description; + } -// private function getDocs(): object { return (object) []; } + $return[] = $data; + } + + return $return; + } } \ No newline at end of file diff --git a/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php b/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php index ddcaef94..774b0437 100644 --- a/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php +++ b/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php @@ -56,7 +56,7 @@ public function testGetComponents(): void $method = $this->get_reflection_method('getComponents'); $result = $method->invokeArgs($this->class, []); - $this->assertEquals((object)[],$result); + $this->assertEquals((object)['schemas' => []],$result); } public function testGetDocs(): void diff --git a/tests/statics/openapi/empty.json b/tests/statics/openapi/empty.json index b31cd314..14497bfa 100644 --- a/tests/statics/openapi/empty.json +++ b/tests/statics/openapi/empty.json @@ -17,7 +17,9 @@ ], "paths": {}, "webhooks": {}, - "components": {}, + "components": { + "schemas": [] + }, "security": [], "tags": [] } \ No newline at end of file