diff --git a/camel/Extraction/Example.php b/camel/Extraction/Example.php new file mode 100644 index 00000000..e0d414bf --- /dev/null +++ b/camel/Extraction/Example.php @@ -0,0 +1,37 @@ +description; + } +} diff --git a/camel/Extraction/ExampleCollection.php b/camel/Extraction/ExampleCollection.php new file mode 100644 index 00000000..336ed2a5 --- /dev/null +++ b/camel/Extraction/ExampleCollection.php @@ -0,0 +1,13 @@ + + */ +class ExampleCollection extends BaseDTOCollection +{ + public static string $base = Example::class; +} \ No newline at end of file diff --git a/camel/Extraction/ExtractedEndpointData.php b/camel/Extraction/ExtractedEndpointData.php index 3f6b295a..8f8d9ad2 100644 --- a/camel/Extraction/ExtractedEndpointData.php +++ b/camel/Extraction/ExtractedEndpointData.php @@ -68,6 +68,8 @@ class ExtractedEndpointData extends BaseDTO */ public array $responseFields = []; + public ExampleCollection $examples; + /** * Authentication info for this endpoint. In the form [{where}, {name}, {sample}] * Example: ["queryParameters", "api_key", "njiuyiw97865rfyvgfvb1"] @@ -84,6 +86,7 @@ public function __construct(array $parameters = []) { $parameters['metadata'] = $parameters['metadata'] ?? new Metadata([]); $parameters['responses'] = $parameters['responses'] ?? new ResponseCollection([]); + $parameters['examples'] = $parameters['examples'] ?? new ExampleCollection([]); parent::__construct($parameters); diff --git a/camel/Output/OutputEndpointData.php b/camel/Output/OutputEndpointData.php index ca8a360e..0fc6d4f7 100644 --- a/camel/Output/OutputEndpointData.php +++ b/camel/Output/OutputEndpointData.php @@ -7,6 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Knuckles\Camel\BaseDTO; +use Knuckles\Camel\Extraction\ExampleCollection; use Knuckles\Camel\Extraction\Metadata; use Knuckles\Camel\Extraction\ResponseCollection; use Knuckles\Camel\Extraction\ResponseField; @@ -77,6 +78,8 @@ class OutputEndpointData extends BaseDTO */ public array $responseFields = []; + public ExampleCollection $examples; + /** * The same as bodyParameters, but organized in a hierarchy. * So, top-level items first, with a __fields property containing their children, and so on. @@ -100,6 +103,7 @@ public function __construct(array $parameters = []) $parameters['queryParameters'] = array_map(fn($param) => new Parameter($param), $parameters['queryParameters'] ?? []); $parameters['urlParameters'] = array_map(fn($param) => new Parameter($param), $parameters['urlParameters'] ?? []); $parameters['responseFields'] = array_map(fn($param) => new ResponseField($param), $parameters['responseFields'] ?? []); + $parameters['examples'] = new ExampleCollection($parameters['examples'] ?? []); parent::__construct($parameters); diff --git a/config/scribe.php b/config/scribe.php index 17e6d70a..36735d38 100644 --- a/config/scribe.php +++ b/config/scribe.php @@ -248,6 +248,9 @@ Strategies\ResponseFields\GetFromResponseFieldAttribute::class, Strategies\ResponseFields\GetFromResponseFieldTag::class, ], + 'examples' => [ + Strategies\Examples\UseExampleTag::class, + ], ], // For response calls, API resource responses and transformer responses, diff --git a/src/Config/Defaults.php b/src/Config/Defaults.php index 4483c3d3..7454f2bb 100644 --- a/src/Config/Defaults.php +++ b/src/Config/Defaults.php @@ -73,4 +73,10 @@ public static function responseFieldsStrategies(): StrategyListWrapper ]); } + public static function examplesStrategies(): StrategyListWrapper + { + return new StrategyListWrapper([ + Strategies\Examples\UseExampleTag::class, + ]); + } } diff --git a/src/Exceptions/ExampleResponseStatusCodeNotFound.php b/src/Exceptions/ExampleResponseStatusCodeNotFound.php new file mode 100644 index 00000000..b7653f4d --- /dev/null +++ b/src/Exceptions/ExampleResponseStatusCodeNotFound.php @@ -0,0 +1,17 @@ +fetchResponseFields($endpointData, $routeRules); $this->mergeInheritedMethodsData('responseFields', $endpointData, $inheritedDocsOverrides); + $this->fetchExampleFields($endpointData, $routeRules); + $this->mergeInheritedMethodsData('examples', $endpointData, $inheritedDocsOverrides); + self::$routeBeingProcessed = null; return $endpointData; @@ -188,6 +193,15 @@ protected function fetchRequestHeaders(ExtractedEndpointData $endpointData, arra }); } + protected function fetchExampleFields(ExtractedEndpointData $endpointData, array $rulesToApply): void + { + $this->iterateThroughStrategies('examples', $endpointData, $rulesToApply, function ($results) use ($endpointData) { + $endpointData->examples->concat($results); + }); + + $endpointData->examples = new ExampleCollection($endpointData->examples->values()); + } + /** * Iterate through all defined strategies for this stage. * A strategy may return an array of attributes diff --git a/src/Extracting/Strategies/Examples/UseExampleTag.php b/src/Extracting/Strategies/Examples/UseExampleTag.php new file mode 100644 index 00000000..65d386ef --- /dev/null +++ b/src/Extracting/Strategies/Examples/UseExampleTag.php @@ -0,0 +1,81 @@ +route); + return $this->getDocBlockExamples($docBlocks['method']->getTags()); + } + + /** + * @param Tag[] $tags + */ + public function getDocBlockExamples(array $tags): ?array + { + $exampleTags = Utils::filterDocBlockTags($tags, 'example'); + + if (empty($exampleTags)) return null; + + $examples = array_map(function (Tag $exampleTag) { + $content = $exampleTag->getContent(); + + ['fields' => $fields, 'content' => $content] = a::parseIntoContentAndFields($content, ['type', 'scenario', 'file']); + + $description = $fields['scenario'] ?: ""; + $type = $fields['type'] ?: ""; + $file = $fields['file'] ?: ""; + $meta = []; + + if (empty($type) || ! in_array($type, self::TYPES)) { + // Type is required + throw new ExampleTypeNotFound(); + } + + if ($type === 'response') { + // Extract the status code + preg_match('/status=(\d+)/', $content, $statusMatches); + + if (isset($statusMatches[1])) { + $meta['status'] = (int) $statusMatches[1]; + } else { + // Status code is required for type response + throw new ExampleResponseStatusCodeNotFound(); + } + + // Remove the status code + $content = preg_replace('/status=(\d+)/', '', $content); + } + + if (! empty($file)) { + $json = json_decode($content, true) ?? null; + + $content = ResponseFileTools::getResponseContents($file, $json); + } + + return ['type' => $type, 'meta' => $meta, 'content' => $content, 'description' => $description]; + }, $exampleTags); + + return $examples; + } +} diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 3a0d6299..9476663f 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Knuckles\Camel\Camel; +use Knuckles\Camel\Extraction\Example; use Knuckles\Camel\Extraction\Response; use Knuckles\Camel\Output\OutputEndpointData; use Knuckles\Camel\Output\Parameter; @@ -247,9 +248,11 @@ protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint) } else { $contentType = 'application/json'; } - - $body['content'][$contentType]['schema'] = $schema; - + + $body['content'][$contentType] = $this->mergeMediaTypeObjectExamples( + ['schema' => $schema], + $endpoint + ); } // return object rather than empty array, so can get properly serialised as object @@ -263,20 +266,42 @@ protected function generateEndpointResponsesSpec(OutputEndpointData $endpoint) foreach ($endpoint->responses as $response) { // OpenAPI groups responses by status code - // Only one response type per status code, so only the last one will be used + // Only one response type per status code, so only the first one will be used if (intval($response->status) === 204) { // Must not add content for 204 $responses[204] = [ 'description' => $this->getResponseDescription($response), ]; } else { - $responses[$response->status] = [ - 'description' => $this->getResponseDescription($response), - 'content' => $this->generateResponseContentSpec($response->content, $endpoint), - ]; + if (! isset($responses[$response->status])) { + $responses[$response->status] = [ + 'description' => $this->getResponseDescription($response), + 'content' => $this->generateResponseContentSpec($response->status, $response->content, $endpoint), + ]; + } } } + // Parse any examples + $endpoint['examples']->where('type', 'response') + ->each(function (Example $item, int $key) use (&$responses, $endpoint) { + $statusCode = $item['meta']['status']; + + if (! isset($responses[$statusCode])) { + if (intval($statusCode) === 204) { + // Must not add content for 204 + $responses[204] = [ + 'description' => $item['description'], + ]; + } else { + $responses[$statusCode] = [ + 'description' => $item['description'], + 'content' => $this->generateResponseContentSpec($statusCode, $item['content'], $endpoint), + ]; + } + } + }); + // return object rather than empty array, so can get properly serialised as object return $this->objectIfEmpty($responses); } @@ -297,7 +322,7 @@ protected function getResponseDescription(Response $response): string return $description; } - protected function generateResponseContentSpec(?string $responseContent, OutputEndpointData $endpoint) + protected function generateResponseContentSpec(int $responseStatus, ?string $responseContent, OutputEndpointData $endpoint) { if (Str::startsWith($responseContent, '<>')) { return [ @@ -325,12 +350,12 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE $decoded = json_decode($responseContent); if ($decoded === null) { // Decoding failed, so we return the content string as is return [ - 'text/plain' => [ + 'text/plain' => $this->mergeMediaTypeObjectExamples([ 'schema' => [ 'type' => 'string', 'example' => $responseContent, ], - ], + ], $endpoint, $responseStatus, $responseContent), ]; } @@ -340,19 +365,19 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE case 'integer': case 'double': return [ - 'application/json' => [ + 'application/json' => $this->mergeMediaTypeObjectExamples([ 'schema' => [ 'type' => $type === 'double' ? 'number' : $type, 'example' => $decoded, ], - ], + ], $endpoint, $responseStatus, $responseContent), ]; case 'array': if (!count($decoded)) { // empty array return [ - 'application/json' => [ + 'application/json' => $this->mergeMediaTypeObjectExamples([ 'schema' => [ 'type' => 'array', 'items' => [ @@ -360,13 +385,13 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], 'example' => $decoded, ], - ], + ], $endpoint, $responseStatus, $responseContent), ]; } // Non-empty array return [ - 'application/json' => [ + 'application/json' => $this->mergeMediaTypeObjectExamples([ 'schema' => [ 'type' => 'array', 'items' => [ @@ -374,7 +399,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], 'example' => $decoded, ], - ], + ], $endpoint, $responseStatus, $responseContent), ]; case 'object': @@ -383,13 +408,13 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE })->toArray(); return [ - 'application/json' => [ + 'application/json' => $this->mergeMediaTypeObjectExamples([ 'schema' => [ 'type' => 'object', 'example' => $decoded, 'properties' => $this->objectIfEmpty($properties), ], - ], + ], $endpoint, $responseStatus, $responseContent), ]; } } @@ -590,4 +615,47 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin return $schema; } + + protected function mergeMediaTypeObjectExamples(array $merge, OutputEndpointData $endpoint, ?int $responseStatus = null, ?string $responseContent = null) + { + $type = ($responseStatus) ? "response" : "request"; + + $decoded = json_decode($responseContent ?? '{}', true); + + $i = 0; + + $examples = []; + + $endpoint['examples']->where('type', $type) + ->reject(function (Example $item, int $key) use ($type, $responseStatus) { + return $type === 'response' && $item['meta']['status'] != $responseStatus; + }) + ->map(function (Example $item, int $key) use (&$examples, &$i, $type) { + // Extract the JSON part of the string + $json = json_decode($item['content'], true); + + $summary = ! empty($item['description']) ? $item['description'] : sprintf("%s %d", Str::ucfirst($type), $i+1); + + $examples[sprintf("%s-example-%d", $type, $i+1)] = [ + 'summary' => $summary, + 'value' => $json ?? $item['content'] + ]; + + $i++; + }); + + if (count($examples) > 1) { + if ($merge['schema']['example'] ?? false) { + unset($merge['schema']['example']); + } + + return data_set($merge, 'examples', $examples); + } + + if ($responseStatus) { + return data_set($merge, 'schema.example', $decoded ?? $responseContent); + } + + return $merge; + } } diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 43b0992e..a17f8705 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -527,7 +527,6 @@ public function withInlineValidatorMake(Request $request) // Do stuff if ($validator->fails()) { - } } @@ -585,7 +584,7 @@ public function withInjectedModel(TestUser $user) { return null; } - + public function withInjectedModelFullParamName(TestPost $testPost) { return null; @@ -608,6 +607,116 @@ public function withInjectedEnumAndModel(Category $category, TestUser $user) return null; } */ + + /** + * @example + * type="response" + * status=200 + * { + * "id": 4, + * "name": "banana", + * "color": "red", + * "weight": "1 kg", + * "delicious": true, + * "responseTag": true + * } + */ + public function withExampleTagTypeResponse() + { + return ''; + } + + /** + * @example + * type="response" + * status=200 + * { + * "id": 4, + * "name": "banana", + * "color": "red", + * "weight": "1 kg", + * "delicious": true, + * "responseTag": true + * } + * + * @example + * type="response" + * status=200 + * { + * "id": 5, + * "name": "banana", + * "color": "green", + * "weight": "1 kg", + * "delicious": true, + * "responseTag": true + * } + */ + public function withMultipleExampleTagTypeResponse() + { + return ''; + } + + /** + * @example + * type="response" + * { + * "id": 4, + * "name": "banana", + * "color": "red", + * "weight": "1 kg", + * "delicious": true, + * "responseTag": true + * } + */ + public function withExampleTagTypeResponseWithoutStatusCode() + { + return ''; + } + + /** + * @example + * type="request" + * { + * "id": 4, + * "name": "banana", + * "color": "red", + * "weight": "1 kg", + * "delicious": true, + * "requestTag": true + * } + */ + public function withExampleTagTypeRequest() + { + return ''; + } + + /** + * @example + * type="request" + * { + * "id": 4, + * "name": "banana", + * "color": "red", + * "weight": "1 kg", + * "delicious": true, + * "requestTag": true + * } + * + * @example + * type="request" + * { + * "id": 5, + * "name": "banana", + * "color": "green", + * "weight": "1 kg", + * "delicious": true, + * "requestTag": true + * } + */ + public function withMultipleExampleTagTypeRequest() + { + return ''; + } } /** @@ -616,4 +725,4 @@ enum Category: string case Fruits = 'fruits'; case People = 'people'; } -*/ + */ diff --git a/tests/Fixtures/openapi.yaml b/tests/Fixtures/openapi.yaml index 9ea3a135..22bcd5ce 100644 --- a/tests/Fixtures/openapi.yaml +++ b/tests/Fixtures/openapi.yaml @@ -176,6 +176,54 @@ paths: example: "" tags: - 'Group A' + '/api/withExampleTagTypeResponse': + get: + summary: '' + operationId: getApiWithExampleTagTypeResponse + description: '' + parameters: + - + in: header + name: Custom-Header + description: '' + example: NotSoCustom + schema: + type: string + responses: + 200: + description: '' + content: + text/plain: + schema: + type: string + example: '' + tags: + - 'Group A' + security: [] + '/api/withExampleTagTypeRequest': + get: + summary: '' + operationId: getApiWithExampleTagTypeRequest + description: '' + parameters: + - + in: header + name: Custom-Header + description: '' + example: NotSoCustom + schema: + type: string + responses: + 200: + description: '' + content: + text/plain: + schema: + type: string + example: '' + tags: + - 'Group A' + security: [] '/api/echoesUrlParameters/{param}/{param2}/{param3}/{param4}': get: summary: '' diff --git a/tests/GenerateDocumentation/OutputTest.php b/tests/GenerateDocumentation/OutputTest.php index 763d789e..8f25a5ec 100644 --- a/tests/GenerateDocumentation/OutputTest.php +++ b/tests/GenerateDocumentation/OutputTest.php @@ -47,6 +47,7 @@ protected function setUp(): void )) ->toArray(), 'responseFields' => Defaults::responseFieldsStrategies()->toArray(), + 'examples' => Defaults::examplesStrategies()->toArray(), ], ]); $this->setConfig(['database_connections_to_transact' => []]); @@ -211,6 +212,8 @@ public function generated_openapi_spec_file_is_correct() RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']); RouteFacade::get('/api/withAuthTag', [TestController::class, 'withAuthenticatedTag']); RouteFacade::get('/api/echoesUrlParameters/{param}/{param2}/{param3?}/{param4?}', [TestController::class, 'echoesUrlParameters']); + RouteFacade::get('/api/withExampleTagTypeResponse', [TestController::class, 'withExampleTagTypeResponse']); + RouteFacade::get('/api/withExampleTagTypeRequest', [TestController::class, 'withExampleTagTypeRequest']); $this->setConfig([ 'openapi.enabled' => true, diff --git a/tests/Strategies/Examples/UseExampleTagTest.php b/tests/Strategies/Examples/UseExampleTagTest.php new file mode 100644 index 00000000..74e9ebec --- /dev/null +++ b/tests/Strategies/Examples/UseExampleTagTest.php @@ -0,0 +1,220 @@ +getDocBlockExamples($tags); + + $this->assertEquals($expected[0]['status'], $results[0]['meta']['status']); + $this->assertEquals($expected[1]['status'], $results[1]['meta']['status']); + $this->assertEquals($expected[0]['description'], $results[0]['description']); + $this->assertEquals($expected[1]['description'], $results[1]['description']); + $this->assertEquals($expected[0]['content'], json_decode($results[0]['content'], true)); + $this->assertEquals($expected[1]['content'], json_decode($results[1]['content'], true)); + } + + /** + * @test + * @dataProvider exampleRequestTags + */ + public function allows_multiple_example_tags_with_type_request_for_multiple_scenarios(array $tags, array $expected) + { + $strategy = new UseExampleTag(new DocumentationConfig([])); + $results = $strategy->getDocBlockExamples($tags); + + $this->assertEquals('request', $results[0]['type']); + $this->assertEquals('request', $results[1]['type']); + $this->assertEquals($expected[0]['description'], $results[0]['description']); + $this->assertEquals($expected[1]['description'], $results[1]['description']); + $this->assertEquals($expected[0]['content'], json_decode($results[0]['content'], true)); + $this->assertEquals($expected[1]['content'], json_decode($results[1]['content'], true)); + } + + /** + * @test + * @dataProvider exampleFileTags + */ + public function allows_multiple_examples_with_files_for_multiple_statuses_and_scenarios(array $tags, array $expected) + { + $filePath = __DIR__ . '/../../Fixtures/response_test.json'; + $filePath2 = __DIR__ . '/../../Fixtures/response_error_test.json'; + + $strategy = new UseExampleTag(new DocumentationConfig([])); + $results = $strategy->getDocBlockExamples($tags); + + $this->assertArraySubset([ + [ + 'type' => 'request', + 'meta' => [], + 'description' => $expected[0]['description'], + 'content' => file_get_contents($filePath), + ], + [ + 'type' => 'response', + 'meta' => [ + 'status' => 401, + ], + 'description' => $expected[1]['description'], + 'content' => file_get_contents($filePath2), + ], + ], $results); + } + + /** + * @test + * @dataProvider invalidExampleTags + */ + public function will_throw_an_exception_when_missing_or_invalid_type_attribue(array $tags) + { + $this->expectException(ExampleTypeNotFound::class); + + $strategy = new UseExampleTag(new DocumentationConfig([])); + $strategy->getDocBlockExamples($tags); + } + + /** + * @test + * @dataProvider invalidExampleResponseTags + */ + public function will_throw_an_exception_when_missing_reponse_status_code_attribue(array $tags) + { + $this->expectException(ExampleResponseStatusCodeNotFound::class); + + $strategy = new UseExampleTag(new DocumentationConfig([])); + $strategy->getDocBlockExamples($tags); + } + + public static function exampleResponseTags() + { + $response1 = '{ + "id": 4, + "name": "banana" + }'; + $response2 = '{ + "message": "Unauthorized" + }'; + return [ + "with fields" => [ + [ + new Tag('example', "type=response status=200 scenario=\"success\" $response1"), + new Tag('example', "type=response status=401 scenario='auth problem' $response2"), + ], + [ + [ + 'status' => 200, + 'description' => 'success', + 'content' => [ + 'id' => 4, + 'name' => 'banana', + ], + ], + [ + 'status' => 401, + 'description' => 'auth problem', + 'content' => [ + 'message' => 'Unauthorized', + ], + ], + ], + ], + ]; + } + + public static function exampleRequestTags() + { + $request1 = '{ + "id": 4, + "name": "banana" + }'; + $request2 = '{ + "message": "Unauthorized" + }'; + return [ + "with fields" => [ + [ + new Tag('example', "type=request scenario=\"with customer filters\" $request1"), + new Tag('example', "type=request scenario='with company filters' $request2"), + ], + [ + [ + 'description' => 'with customer filters', + 'content' => [ + 'id' => 4, + 'name' => 'banana', + ], + ], + [ + 'description' => 'with company filters', + 'content' => [ + 'message' => 'Unauthorized', + ], + ], + ], + ], + ]; + } + + public static function exampleFileTags() + { + return [ + "with fields" => [ + [ + new Tag('example', 'type="request" scenario="user update" file="tests/Fixtures/response_test.json"'), + new Tag('example', 'type="response" status=401 scenario=\'auth problem\' file="tests/Fixtures/response_error_test.json"'), + ], + [ + [ + 'description' => 'user update', + ], + [ + 'status' => 401, + 'description' => 'auth problem', + ], + ], + ], + ]; + } + + public static function invalidExampleTags() + { + return [ + "with fields" => [ + [ + new Tag('example', "scenario=\"with customer filters\""), + new Tag('example', "type=invalid scenario='with company filters'"), + ], + ], + ]; + } + + public static function invalidExampleResponseTags() + { + return [ + "with fields" => [ + [ + new Tag('example', "type=response"), + new Tag('example', "type=response scenario='Successful response'"), + ], + ], + ]; + } +} diff --git a/tests/Unit/ExtractorTest.php b/tests/Unit/ExtractorTest.php index 6e355041..c89e2393 100644 --- a/tests/Unit/ExtractorTest.php +++ b/tests/Unit/ExtractorTest.php @@ -42,6 +42,9 @@ class ExtractorTest extends BaseUnitTest 'responseFields' => [ Strategies\ResponseFields\GetFromResponseFieldTag::class, ], + 'examples' => [ + Strategies\Examples\UseExampleTag::class, + ], ], ]; diff --git a/tests/Unit/OpenAPISpecWriterTest.php b/tests/Unit/OpenAPISpecWriterTest.php index 6d2c94a5..d6628caa 100644 --- a/tests/Unit/OpenAPISpecWriterTest.php +++ b/tests/Unit/OpenAPISpecWriterTest.php @@ -557,6 +557,265 @@ public function adds_responses_correctly_as_responses_on_operation_object() ], $results['paths']['/path2']['put']['responses']); } + /** @test */ + public function adds_examples_correctly_as_examples_on_media_type_object() + { + $endpointData1 = $this->createMockEndpointData([ + 'httpMethods' => ['POST'], + 'uri' => '/path1', + 'examples' => [ + [ + 'type' => 'response', + 'meta' => [ + 'status' => 204, + ], + 'description' => 'Successfully updated.', + 'content' => '{"this": "should be ignored"}', + ], + [ + 'type' => 'response', + 'meta' => [ + '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 } } } }', + ], + [ + 'type' => 'response', + 'meta' => [ + 'status' => 201, + ], + 'description' => '', + 'content' => '{"this": "also"}', + ], + [ + 'type' => 'response', + 'meta' => [ + 'status' => 200, + ], + 'description' => 'Successfully retrieved.', + 'content' => '{"this": "shouldn\'t be ignored"}', + ], + [ + 'type' => 'response', + 'meta' => [ + 'status' => 200, + ], + 'description' => 'Successfully completed.', + 'content' => '{"this": "also shouldn\'t be ignored"}', + ], + ], + ]); + $endpointData2 = $this->createMockEndpointData([ + 'httpMethods' => ['PUT'], + 'uri' => '/path2', + 'bodyParameters' => [ + 'stringParam' => [ + 'name' => 'stringParam', + 'description' => 'String param', + 'required' => false, + 'example' => 'hahoho', + 'type' => 'string', + ], + ], + 'examples' => [ + [ + 'type' => 'request', + 'meta' => [], + 'description' => 'Task 1', + 'content' => '{"this": "shouldn\'t be ignored"}', + ], + [ + 'type' => 'request', + 'meta' => [], + 'description' => 'Task 2', + 'content' => '{"this": "also shouldn\'t be ignored"}', + ], + ], + ]); + $groups = [$this->createGroup([$endpointData1, $endpointData2])]; + + $results = $this->generate($groups); + + $this->assertCount(3, $results['paths']['/path1']['post']['responses']); + $this->assertCount(2, $results['paths']['/path1']['post']['responses'][200]['content']['application/json']['examples']); + $this->assertArraySubset([ + '200' => [ + "description" => "Okayy", + "content" => [ + "application/json" => [ + "schema" => [ + "type" => "object", + "properties" => [ + "random" => [ + "type" => "string", + "example" => "json", + ] + ] + ], + "examples" => [ + "response-example-1" => [ + "summary" => "Successfully retrieved.", + "value" => [ + "this" => "shouldn't be ignored", + ], + ], + "response-example-2" => [ + "summary" => "Successfully completed.", + "value" => [ + "this" => "also shouldn't be ignored", + ] + ] + ] + ] + ] + ], + '204' => [ + 'description' => 'Successfully updated.', + ], + '201' => [ + "description" => "", + "content" => [ + "application/json" => [ + "schema" => [ + "type" => "object", + "properties" => [ + "this" => [ + "type" => "string", + "example" => "shouldn't be ignored", + ], + "and this" => [ + "type" => "string", + "example" => "too", + ], + "sub level 0" => [ + "type" => "object", + "properties" => [ + "sub level 1 key 1" => [ + "type" => "string", + "example" => "sl0_sl1k1", + ], + "sub level 1 key 2" => [ + "type" => "array", + "example" => [ + [ + "sub level 2 key 1" => "sl0_sl1k2_sl2k1", + "sub level 2 key 2" => [ + "sub level 3 key 1" => "sl0_sl1k2_sl2k2_sl3k1", + ], + ], + ], + "items" => [ + "type" => "object", + "properties" => [ + "sub level 2 key 1" => [ + "type" => "string", + "example" => "sl0_sl1k2_sl2k1", + ], + "sub level 2 key 2" => [ + "type" => "object", + "properties" => [ + "sub level 3 key 1" => [ + "type" => "string", + "example" => "sl0_sl1k2_sl2k2_sl3k1", + ], + ] + ] + ] + ] + ], + "sub level 1 key 3" => [ + "type" => "object", + "properties" => [ + "sub level 2 key 1" => [ + "type" => "string", + "example" => "sl0_sl1k3_sl2k2", + ], + "sub level 2 key 2" => [ + "type" => "object", + "properties" => [ + "sub level 3 key 1" => [ + "type" => "string", + "example" => "sl0_sl1k3_sl2k2_sl3k1", + ], + "sub level 3 key null" => [ + "type" => "string", + "example" => null, + ], + "sub level 3 key integer" => [ + "type" => "integer", + "example" => 99, + ], + ] + ] + ] + ] + ] + ] + ] + ], + "examples" => [ + "response-example-1" => [ + "summary" => "Response 1", + "value" => [ + "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, + ] + ] + ] + ] + ], + "response-example-2" => [ + "summary" => "Response 2", + "value" => [ + "this" => "also" + ] + ] + ] + ] + ] + ] + ], $results['paths']['/path1']['post']['responses']); + $this->assertCount(1, $results['paths']['/path2']['put']['responses']); + $this->assertEquals([ + '200' => [ + "description" => "Okayy", + "content" => [ + "application/json" => [ + "schema" => [ + "type" => "object", + "example" => [ + "random" => "json", + ], + "properties" => [ + "random" => [ + "type" => "string", + "example" => "json" + ] + ] + ] + ] + ] + ] + ], $results['paths']['/path2']['put']['responses']); + } + protected function createMockEndpointData(array $custom = []): OutputEndpointData { $faker = Factory::create(); @@ -583,6 +842,7 @@ protected function createMockEndpointData(array $custom = []): OutputEndpointDat ], ], 'responseFields' => [], + 'examples' => [], ]; foreach ($custom as $key => $value) {