From e43298372dfc64c254652df3a8a3a68793521199 Mon Sep 17 00:00:00 2001 From: Tim Wickstrom Date: Mon, 28 Oct 2024 14:31:09 -0500 Subject: [PATCH] Fixes knuckleswtf/scribe/issues#907 --- config/scribe.php | 9 +- config/scribe_new.php | 5 + src/Config/Output.php | 30 +-- src/Writing/OpenAPISpecWriter.php | 14 +- src/Writing/Writer.php | 10 +- tests/Fixtures/openapi.yaml | 426 ++++++++++++++++-------------- 6 files changed, 265 insertions(+), 229 deletions(-) diff --git a/config/scribe.php b/config/scribe.php index 66dbb831..f2af51ce 100644 --- a/config/scribe.php +++ b/config/scribe.php @@ -9,6 +9,12 @@ // A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec. 'description' => '', + 'contact' => [ + 'name' => '', + 'url' => '', + 'email' => '', + ], + // The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time. // If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL. 'base_url' => null, @@ -127,8 +133,7 @@ -INTRO - , +INTRO, // Example requests for each endpoint will be shown in each of these languages. // Supported options are: bash, javascript, php, python diff --git a/config/scribe_new.php b/config/scribe_new.php index 243013de..3188fcc5 100644 --- a/config/scribe_new.php +++ b/config/scribe_new.php @@ -58,6 +58,11 @@ theme: 'default', title: null, description: '', + contact: [ + "name" => '', + "url" => '', + "email" => '', + ], baseUrls: [ "production" => config("app.base_url"), ], diff --git a/src/Config/Output.php b/src/Config/Output.php index e90b53b1..8159bd05 100644 --- a/src/Config/Output.php +++ b/src/Config/Output.php @@ -8,6 +8,7 @@ public static function with( string $theme = 'default', string $title = null, string $description = '', + array $contact = [], array $baseUrls = [], array $exampleLanguages = ['bash', 'javascript'], bool $logo = false, @@ -18,8 +19,7 @@ public static function with( array $postman = ['enabled' => true], array $openApi = ['enabled' => true], array $tryItOut = ['enabled' => true], - ): static - { + ): static { return new static(...get_defined_vars()); } @@ -27,6 +27,7 @@ public function __construct( public string $theme = 'default', public ?string $title = null, public string $description = '', + public array $contact = [], public array $baseUrls = [], /* If empty, Scribe will use config('app.url') */ public array $groupsOrder = [], public string $introText = "", @@ -38,31 +39,26 @@ public function __construct( public array $postman = ['enabled' => true], public array $openApi = ['enabled' => true], public array $tryItOut = ['enabled' => true], - ) - { - } + ) {} public static function laravelType( bool $addRoutes = true, string $docsUrl = '/docs', string $assetsDirectory = null, array $middleware = [], - ): array - { + ): array { return ['laravel', get_defined_vars()]; } public static function staticType( string $outputPath = 'public/docs', - ): array - { + ): array { return ['static', get_defined_vars()]; } public static function externalStaticType( string $outputPath = 'public/docs', - ): array - { + ): array { return ['external_static', get_defined_vars()]; } @@ -70,24 +66,21 @@ public static function externalLaravelType( bool $addRoutes = true, string $docsUrl = '/docs', array $middleware = [], - ): array - { + ): array { return ['external_laravel', get_defined_vars()]; } public static function postman( bool $enabled = true, array $overrides = [], - ): array - { + ): array { return get_defined_vars(); } public static function openApi( bool $enabled = true, array $overrides = [], - ): array - { + ): array { return get_defined_vars(); } @@ -96,8 +89,7 @@ public static function tryItOut( string $baseUrl = null, bool $useCsrf = false, string $csrfUrl = '/sanctum/csrf-cookie', - ): array - { + ): array { return get_defined_vars(); } } diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 0356f5e0..581271fe 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -41,7 +41,12 @@ public function generateSpecContent(array $groupedEndpoints): array 'info' => [ 'title' => $this->config->get('title') ?: config('app.name', ''), 'description' => $this->config->get('description', ''), - 'version' => '1.0.0', + 'version' => config('app.version', ''), + 'contact' => [ + 'name' => $this->config->get('contact.name', ''), + 'url' => $this->config->get('contact.url', ''), + 'email' => $this->config->get('contact.email', ''), + ], ], 'servers' => [ [ @@ -249,7 +254,6 @@ protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint) } $body['content'][$contentType]['schema'] = $schema; - } // return object rather than empty array, so can get properly serialised as object @@ -400,7 +404,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], 'example' => $decoded, ], - ], + ], ]; case 'object': @@ -547,7 +551,7 @@ public function generateFieldData($field): array 'type' => 'object', 'description' => $field->description ?: '', 'example' => $field->example, - 'nullable'=> $field->nullable, + 'nullable' => $field->nullable, 'properties' => $this->objectIfEmpty(collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) { return [$subfieldName => $this->generateFieldData($subfield)]; })->all()), @@ -656,7 +660,7 @@ public function filterRequiredFields(OutputEndpointData $endpoint, array $proper return $required; } - + /* * Set the description for the schema. If the field has a description, it is set in the schema. */ diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php index 69db61cc..8719b2c8 100644 --- a/src/Writing/Writer.php +++ b/src/Writing/Writer.php @@ -148,7 +148,7 @@ public function generateOpenAPISpec(array $groupedEndpoints): string data_set($spec, $key, $value); } } - return Yaml::dump($spec, 20, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP); + return Yaml::dump($spec, 20, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_NUMERIC_KEY_AS_STRING); } protected function performFinalTasksForLaravelType(): void @@ -220,9 +220,9 @@ public function writeExternalHtmlDocs(): void $writer = app()->makeWith(ExternalHtmlWriter::class, ['config' => $this->config]); $writer->generate([], $this->paths->intermediateOutputPath(), $this->staticTypeOutputPath); - if (!$this->isStatic) { - $this->performFinalTasksForLaravelType(); - } + if (!$this->isStatic) { + $this->performFinalTasksForLaravelType(); + } if ($this->isStatic) { $outputPath = rtrim($this->staticTypeOutputPath, '/') . '/'; @@ -252,7 +252,7 @@ protected function getLaravelTypeOutputPath(): ?string return config( 'view.paths.0', function_exists('base_path') ? base_path("resources/views") : "resources/views" - ). "/" . $this->paths->outputPath(); + ) . "/" . $this->paths->outputPath(); } /** diff --git a/tests/Fixtures/openapi.yaml b/tests/Fixtures/openapi.yaml index 97e8b4ca..275ea8a3 100644 --- a/tests/Fixtures/openapi.yaml +++ b/tests/Fixtures/openapi.yaml @@ -1,28 +1,26 @@ openapi: 3.0.3 info: title: Laravel - description: '' + description: "" version: 3.9.9 servers: - - - url: 'http://localhost' + - url: "http://localhost" paths: /api/withFormDataParams: post: - summary: 'Endpoint with body form data parameters.' - description: '' + summary: "Endpoint with body form data parameters." + description: "" operationId: endpointWithBodyFormDataParameters parameters: - - - in: header - name: Custom-Header - description: '' - example: NotSoCustom - schema: - type: string - responses: { } + - in: header + name: Custom-Header + description: "" + example: NotSoCustom + schema: + type: string + responses: {} tags: - - 'Group A' + - "Group A" requestBody: required: true content: @@ -32,13 +30,13 @@ paths: properties: name: type: string - description: 'Name of image.' + description: "Name of image." example: cat.jpg nullable: false image: type: string format: binary - description: 'The image.' + description: "The image." nullable: false required: - name @@ -46,19 +44,18 @@ paths: security: [] /api/withResponseTag: get: - summary: '' - description: '' + summary: "" + description: "" operationId: getApiWithResponseTag parameters: - - - in: header - name: Custom-Header - description: '' - example: NotSoCustom - schema: - type: string + - in: header + name: Custom-Header + description: "" + example: NotSoCustom + schema: + type: string responses: - 200: + "200": description: "" content: application/json: @@ -68,216 +65,206 @@ paths: id: 4 name: banana color: red - weight: '1 kg' + weight: "1 kg" delicious: true responseTag: true properties: id: { type: integer, example: 4 } name: { type: string, example: banana } color: { type: string, example: red } - weight: { type: string, example: '1 kg' } + weight: { type: string, example: "1 kg" } delicious: { type: boolean, example: true } - responseTag: { type: boolean, example: true } + responseTag: + { type: boolean, example: true } tags: - - 'Group A' + - "Group A" security: [] /api/withQueryParameters: get: - summary: '' - description: '' + summary: "" + description: "" operationId: getApiWithQueryParameters parameters: - - - in: query - name: location_id - description: 'The id of the location.' - example: consequatur - required: true - schema: - type: string - description: 'The id of the location.' - example: consequatur - nullable: false - - - in: query - name: user_id - description: 'The id of the user.' - example: me - required: true - schema: - type: string - description: 'The id of the user.' - example: me - nullable: false - - - in: query - name: page - description: 'The page number.' - example: '4' - required: true - schema: - type: string - description: 'The page number.' - example: '4' - nullable: false - - - in: query - name: filters - description: 'The filters.' - example: consequatur - required: false - schema: - type: string - description: 'The filters.' - example: consequatur - nullable: false - - - in: query - name: url_encoded - description: 'Used for testing that URL parameters will be URL-encoded where needed.' - example: '+ []&=' - required: false - schema: - type: string - description: 'Used for testing that URL parameters will be URL-encoded where needed.' - example: '+ []&=' - nullable: false - - - in: header - name: Custom-Header - description: '' - example: NotSoCustom - schema: - type: string + - in: query + name: location_id + description: "The id of the location." + example: consequatur + required: true + schema: + type: string + description: "The id of the location." + example: consequatur + nullable: false + - in: query + name: user_id + description: "The id of the user." + example: me + required: true + schema: + type: string + description: "The id of the user." + example: me + nullable: false + - in: query + name: page + description: "The page number." + example: "4" + required: true + schema: + type: string + description: "The page number." + example: "4" + nullable: false + - in: query + name: filters + description: "The filters." + example: consequatur + required: false + schema: + type: string + description: "The filters." + example: consequatur + nullable: false + - in: query + name: url_encoded + description: "Used for testing that URL parameters will be URL-encoded where needed." + example: "+ []&=" + required: false + schema: + type: string + description: "Used for testing that URL parameters will be URL-encoded where needed." + example: "+ []&=" + nullable: false + - in: header + name: Custom-Header + description: "" + example: NotSoCustom + schema: + type: string responses: - 200: + "200": description: "" content: - 'text/plain': + "text/plain": schema: type: "string" example: "" tags: - - 'Group A' + - "Group A" security: [] /api/withAuthTag: get: - summary: '' - description: '' + summary: "" + description: "" operationId: getApiWithAuthTag parameters: - - - in: header - name: Custom-Header - description: '' - example: NotSoCustom - schema: - type: string + - in: header + name: Custom-Header + description: "" + example: NotSoCustom + schema: + type: string responses: - 200: + "200": description: "" content: - 'text/plain': + "text/plain": schema: type: "string" example: "" tags: - - 'Group A' - '/api/echoesUrlParameters/{param}/{param2}/{param3}/{param4}': + - "Group A" + "/api/echoesUrlParameters/{param}/{param2}/{param3}/{param4}": get: - summary: '' - description: '' + summary: "" + description: "" operationId: getApiEchoesUrlParametersParamParam2Param3Param4 parameters: - - - in: query - name: something - description: '' - example: consequatur - required: false - schema: - type: string - description: '' - example: consequatur - nullable: false - - - in: header - name: Custom-Header - description: '' - example: NotSoCustom - schema: - type: string + - in: query + name: something + description: "" + example: consequatur + required: false + schema: + type: string + description: "" + example: consequatur + nullable: false + - in: header + name: Custom-Header + description: "" + example: NotSoCustom + schema: + type: string responses: - 200: - description: '' + "200": + description: "" content: application/json: schema: type: object example: - param: '4' + param: "4" param2: consequatur param3: consequatur param4: null properties: - param: { type: string, example: '4' } - param2: { type: string, example: consequatur } - param3: { type: string, example: consequatur } + param: { type: string, example: "4" } + param2: + { type: string, example: consequatur } + param3: + { type: string, example: consequatur } param4: { type: string, example: null } tags: - Other😎 security: [] parameters: - - - in: path - name: param - description: '' - example: '4' - required: true - schema: - type: string - - - in: path - name: param2 - description: '' - required: true - schema: - type: string - example: consequatur - - - in: path - name: param3 - description: 'Optional parameter.' - required: true - schema: - type: string - examples: - omitted: - summary: 'When the value is omitted' - value: '' - present: - summary: 'When the value is present' - value: consequatur - - - in: path - name: param4 - description: 'Optional parameter.' - required: true - schema: - type: string - examples: - omitted: - summary: 'When the value is omitted' - value: '' + - in: path + name: param + description: "" + example: "4" + required: true + schema: + type: string + - in: path + name: param2 + description: "" + required: true + schema: + type: string + example: consequatur + - in: path + name: param3 + description: "Optional parameter." + required: true + schema: + type: string + examples: + omitted: + summary: "When the value is omitted" + value: "" + present: + summary: "When the value is present" + value: consequatur + - in: path + name: param4 + description: "Optional parameter." + required: true + schema: + type: string + examples: + omitted: + summary: "When the value is omitted" + value: "" /api/withBodyParametersAsArray: post: - summary: 'Endpoint with body parameters as array.' - description: '' + summary: "Endpoint with body parameters as array." + description: "" operationId: endpointWithBodyParametersAsArray parameters: - in: header name: Custom-Header - description: '' + description: "" example: NotSoCustom schema: type: string @@ -292,30 +279,73 @@ paths: type: array description: Details. example: - - first_name: 'John' - last_name: 'Doe' - contacts: - - first_name: Janelle - last_name: Monáe - roles: [Admin] + - first_name: "John" + last_name: "Doe" + contacts: + - first_name: Janelle + last_name: Monáe + roles: [Admin] items: type: object properties: - first_name: { type: string, description: 'The first name of the user.', example: John, nullable: false } - last_name: { type: string, description: 'The last name of the user.', example: Doe, nullable: false} - contacts: { type: array, description: 'Contact info', example: [ [ ] ], items: { type: object, properties: { first_name: { type: string, description: 'The first name of the contact.', example: Janelle, nullable: false }, last_name: { type: string, description: 'The last name of the contact.', example: Monáe, nullable: false } }, required: [ first_name, last_name ] } } - roles: { type: array, description: 'The name of the role.', example: [ Admin ], items: { type: string } } + first_name: + { + type: string, + description: "The first name of the user.", + example: John, + nullable: false, + } + last_name: + { + type: string, + description: "The last name of the user.", + example: Doe, + nullable: false, + } + contacts: + { + type: array, + description: "Contact info", + example: [[]], + items: + { + type: object, + properties: + { + first_name: + { + type: string, + description: "The first name of the contact.", + example: Janelle, + nullable: false, + }, + last_name: + { + type: string, + description: "The last name of the contact.", + example: Monáe, + nullable: false, + }, + }, + required: + [first_name, last_name], + }, + } + roles: + { + type: array, + description: "The name of the role.", + example: [Admin], + items: { type: string }, + } required: - first_name - last_name - contacts - roles - security: [ ] + security: [] tags: - - - name: 'Group A' - description: '' - - - name: Other😎 - description: '' - + - name: "Group A" + description: "" + - name: Other😎 + description: ""