From 1f94ac5b6c3a13b67bed17eabe4933b3ee2b8ce4 Mon Sep 17 00:00:00 2001 From: Ronan McCarter <63772591+rpmccarter@users.noreply.github.com> Date: Sat, 19 Aug 2023 03:10:24 -0700 Subject: [PATCH] Improve OpenAPI spec empty checks (#712) * add empty check for properties * Update src/Writing/OpenAPISpecWriter.php --------- Co-authored-by: Shalvah --- src/Writing/OpenAPISpecWriter.php | 47 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 4d593bb8..4c9a6cd0 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -22,17 +22,9 @@ class OpenAPISpecWriter private DocumentationConfig $config; - /** - * Object to represent empty values, since empty arrays get serialised as objects. - * Can't use a constant because of initialisation expression. - * - */ - public \stdClass $EMPTY; - public function __construct(DocumentationConfig $config = null) { $this->config = $config ?: new DocumentationConfig(config('scribe', [])); - $this->EMPTY = new \stdClass(); } /** @@ -241,6 +233,10 @@ protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint) $schema['properties'][$name] = $fieldData; } + // We remove 'properties' if the request body is an array, so we need to check if it's still there + if (array_key_exists('properties', $schema)) { + $schema['properties'] = $this->objectIfEmpty($schema['properties']); + } $body['required'] = $hasRequiredParameter; if ($hasFileParameter) { @@ -257,7 +253,7 @@ protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint) } // return object rather than empty array, so can get properly serialised as object - return count($body) > 0 ? $body : $this->EMPTY; + return $this->objectIfEmpty($body); } protected function generateEndpointResponsesSpec(OutputEndpointData $endpoint) @@ -282,7 +278,7 @@ protected function generateEndpointResponsesSpec(OutputEndpointData $endpoint) } // return object rather than empty array, so can get properly serialised as object - return count($responses) > 0 ? $responses : $this->EMPTY; + return $this->objectIfEmpty($responses); } protected function getResponseDescription(Response $response): string @@ -386,16 +382,12 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE return [$key => $this->generateSchemaForValue($value, $endpoint, $key)]; })->toArray(); - if (!count($properties)) { - $properties = $this->EMPTY; - } - return [ 'application/json' => [ 'schema' => [ 'type' => 'object', 'example' => $decoded, - 'properties' => $properties, + 'properties' => $this->objectIfEmpty($properties), ], ], ]; @@ -513,9 +505,9 @@ public function generateFieldData($field): array 'type' => 'object', 'description' => $field->description ?: '', 'example' => $field->example, - 'properties' => collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) { + 'properties' => $this->objectIfEmpty(collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) { return [$subfieldName => $this->generateFieldData($subfield)]; - })->all(), + })->all()), ]; } else { return [ @@ -534,6 +526,15 @@ protected function operationId(OutputEndpointData $endpoint): string return Str::lower($endpoint->httpMethods[0]) . join('', array_map(fn ($part) => ucfirst($part), $parts)); } + /** + * Given an array, return an object if the array is empty. To be used with fields that are + * required by OpenAPI spec to be objects, since empty arrays get serialised as []. + */ + protected function objectIfEmpty(array $field): array | \stdClass + { + return count($field) > 0 ? $field : new \stdClass(); + } + /** * Given a value, generate the schema for it. The schema consists of: {type:, example:, properties: (if value is an object)}, * and possibly a description for each property. @@ -543,17 +544,17 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin { if ($value instanceof \stdClass) { $value = (array) $value; - $schema = [ - 'type' => 'object', - 'properties' => [], - ]; + $properties = []; // Recurse into the object foreach($value as $subField => $subValue){ $subFieldPath = sprintf('%s.%s', $path, $subField); - $schema['properties'][$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath); + $properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath); } - return $schema; + return [ + 'type' => 'object', + 'properties' => $this->objectIfEmpty($properties), + ]; } $schema = [