瀏覽代碼

Support request body as array

shalvah 4 年之前
父節點
當前提交
3f49c83927

+ 4 - 1
CHANGELOG.md

@@ -20,4 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - moved body-parameters to components
 - index renamed to intro.blade.php
 - views moved to markdown/
-- Removed continue_without_database_transactions
+- Removed continue_without_database_transactions
+- Support headers in response
+- Include responses in Postman collection
+- Body parameters array

+ 5 - 2
resources/css/theme-default.style.css

@@ -22,11 +22,14 @@ hgroup,
 main,
 menu,
 nav,
-section,
-summary {
+section {
     display: block
 }
 
+summary {
+    cursor: pointer;
+}
+
 audio,
 canvas,
 progress,

+ 74 - 51
resources/views/components/body-parameters.blade.php

@@ -1,55 +1,78 @@
 
 @foreach($parameters as $name => $parameter)
-@if(!empty($parameter['__fields']))
-<p>
-<details>
-<summary>
-@component('scribe::components.field-details', [
-  'name' => $parameter['name'],
-  'type' => $parameter['type'] ?? 'string',
-  'required' => $parameter['required'] ?? false,
-  'description' => $parameter['description'] ?? '',
-  'endpointId' => $endpointId,
-  'hasChildren' => true,
-  'component' => 'body',
-])
-@endcomponent
-</summary>
-<br>
-@foreach($parameter['__fields'] as $subfieldName => $subfield)
-@if(!empty($subfield['__fields']))
-@component('scribe::components.body-parameters', ['parameters' => [$subfieldName => $subfield], 'endpointId' => $endpointId,])
-@endcomponent
-@else
-<p>
-@component('scribe::components.field-details', [
-  'name' => $subfield['name'],
-  'type' => $subfield['type'] ?? 'string',
-  'required' => $subfield['required'] ?? false,
-  'description' => $subfield['description'] ?? '',
-  'endpointId' => $endpointId,
-  'hasChildren' => false,
-  'component' => 'body',
-])
-@endcomponent
-</p>
-@endif
-@endforeach
-</details>
-</p>
-@else
-<p>
-@component('scribe::components.field-details', [
-  'name' => $parameter['name'],
-  'type' => $parameter['type'] ?? 'string',
-  'required' => $parameter['required'] ?? false,
-  'description' => $parameter['description'] ?? '',
-  'endpointId' => $endpointId,
-  'hasChildren' => false,
-  'component' => 'body',
-])
-@endcomponent
-</p>
-@endif
+    @if($name === '[]')
+        <p>
+            Body: <code>{{ $parameter['type'] }}</code> {!! Parsedown::instance()->text($parameter['description'] ?? '') !!}
+            @foreach($parameter['__fields'] as $subfieldName => $subfield)
+                @if(!empty($subfield['__fields']))
+                    @component('scribe::components.body-parameters', ['parameters' => [$subfieldName => $subfield], 'endpointId' => $endpointId,])
+                    @endcomponent
+                @else
+                    <p>
+                        @component('scribe::components.field-details', [
+                          'name' => $subfield['name'],
+                          'type' => $subfield['type'] ?? 'string',
+                          'required' => $subfield['required'] ?? false,
+                          'description' => $subfield['description'] ?? '',
+                          'endpointId' => $endpointId,
+                          'hasChildren' => false,
+                          'component' => 'body',
+                        ])
+                        @endcomponent
+                    </p>
+                @endif
+            @endforeach
+        </p>
+    @elseif(!empty($parameter['__fields']))
+        <p>
+        <details>
+            <summary>
+                @component('scribe::components.field-details', [
+                  'name' => $parameter['name'],
+                  'type' => $parameter['type'] ?? 'string',
+                  'required' => $parameter['required'] ?? false,
+                  'description' => $parameter['description'] ?? '',
+                  'endpointId' => $endpointId,
+                  'hasChildren' => true,
+                  'component' => 'body',
+                ])
+                @endcomponent
+            </summary>
+            <br>
+            @foreach($parameter['__fields'] as $subfieldName => $subfield)
+                @if(!empty($subfield['__fields']))
+                    @component('scribe::components.body-parameters', ['parameters' => [$subfieldName => $subfield], 'endpointId' => $endpointId,])
+                    @endcomponent
+                @else
+                    <p>
+                        @component('scribe::components.field-details', [
+                          'name' => $subfield['name'],
+                          'type' => $subfield['type'] ?? 'string',
+                          'required' => $subfield['required'] ?? false,
+                          'description' => $subfield['description'] ?? '',
+                          'endpointId' => $endpointId,
+                          'hasChildren' => false,
+                          'component' => 'body',
+                        ])
+                        @endcomponent
+                    </p>
+                @endif
+            @endforeach
+        </details>
+        </p>
+    @else
+        <p>
+            @component('scribe::components.field-details', [
+              'name' => $parameter['name'],
+              'type' => $parameter['type'] ?? 'string',
+              'required' => $parameter['required'] ?? false,
+              'description' => $parameter['description'] ?? '',
+              'endpointId' => $endpointId,
+              'hasChildren' => false,
+              'component' => 'body',
+            ])
+            @endcomponent
+        </p>
+    @endif
 @endforeach
 

+ 1 - 1
resources/views/partials/example-requests/bash.blade.php

@@ -22,6 +22,6 @@ curl --request {{$endpoint->methods[0]}} \
 @endforeach
 @endforeach
 @elseif(count($endpoint->cleanBodyParameters))
-    --data "{!! addslashes(json_encode($endpoint->cleanBodyParameters)) !!}"
+    --data "{!! addslashes(json_encode($endpoint->cleanBodyParameters, JSON_UNESCAPED_UNICODE)) !!}"
 @endif
 </code></pre>

+ 1 - 1
resources/views/partials/example-requests/javascript.blade.php

@@ -37,7 +37,7 @@ body.append('{!! $key !!}', document.querySelector('input[name="{!! $key !!}"]')
 @endforeach
 @endforeach
 @elseif(count($endpoint->cleanBodyParameters))
-let body = {!! json_encode($endpoint->cleanBodyParameters, JSON_PRETTY_PRINT) !!}
+let body = {!! json_encode($endpoint->cleanBodyParameters, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
 @endif
 
 fetch(url, {

+ 1 - 1
resources/views/partials/example-requests/python.blade.php

@@ -19,7 +19,7 @@ files = {
 }
 @endif
 @if(count($endpoint->cleanBodyParameters))
-payload = {!! json_encode($endpoint->cleanBodyParameters, JSON_PRETTY_PRINT) !!}
+payload = {!! json_encode($endpoint->cleanBodyParameters, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
 @endif
 @if(count($endpoint->cleanQueryParameters))
 params = {!! u::printQueryParamsAsKeyValue($endpoint->cleanQueryParameters, "'", ":", 2, "{}") !!}

+ 2 - 2
resources/views/themes/default/endpoint.blade.php

@@ -24,8 +24,8 @@
         </blockquote>
         @if(count($response->headers))
         <details class="annotation">
-            <summary onclick="textContent = parentElement.open ? 'Show headers ▼' : 'Hide headers ▲'">
-                Show headers ▼
+            <summary>
+                <small onclick="textContent = parentElement.parentElement.open ? 'Show headers' : 'Hide headers'">Show headers</small>
             </summary>
             <pre>
             <code class="language-http">@foreach($response->headers as $header => $values)

+ 65 - 15
src/Extracting/Extractor.php

@@ -226,11 +226,11 @@ class Extractor
     /**
      * This method prepares and simplifies request parameters for use in example requests and response calls.
      * It takes in an array with rich details about a parameter eg
-     *   ['age' => [
+     *   ['age' => new Parameter([
      *     'description' => 'The age',
-     *     'value' => 12,
+     *     'example' => 12,
      *     'required' => false,
-     *   ]]
+     *   ])]
      * And transforms them into key-example pairs : ['age' => 12]
      * It also filters out parameters which have null values and have 'required' as false.
      * It converts all file params that have string examples to actual files (instances of UploadedFile).
@@ -261,6 +261,19 @@ class Extractor
                 }
             }
 
+            if (Str::startsWith($paramName, '[].')) { // Entire body is an array
+                if (empty($parameters["[]"])) { // Make sure there's a parent
+                    $cleanParameters["[]"] = [[], []];
+                    $parameters["[]"] = new Parameter([
+                        "name" => "[]",
+                        "type" => "object[]",
+                        "description" => "",
+                        "required" => true,
+                        "example" => [$paramName => $details->example],
+                    ]);
+                }
+            }
+
             if (Str::contains($paramName, '.')) { // Object field (or array of objects)
                 self::setObject($cleanParameters, $paramName, $details->example, $parameters, $details->required);
             } else {
@@ -268,6 +281,11 @@ class Extractor
             }
         }
 
+        // Finally, if the body is an array, flatten it.
+        if (isset($cleanParameters['[]'])) {
+            $cleanParameters = $cleanParameters['[]'];
+        }
+
         return $cleanParameters;
     }
 
@@ -286,6 +304,10 @@ class Extractor
         while (Str::endsWith($baseNameInOriginalParams, '[]')) {
             $baseNameInOriginalParams = substr($baseNameInOriginalParams, 0, -2);
         }
+        // When the body is an array, param names will be  "[].paramname", so $baseNameInOriginalParams here will be empty
+        if (Str::startsWith($path, '[].')) {
+            $baseNameInOriginalParams = '[]';
+        }
 
         if (Arr::has($source, $baseNameInOriginalParams)) {
             /** @var Parameter $parentData */
@@ -297,14 +319,29 @@ class Extractor
                     Arr::set($results, $dotPath, $value);
                 }
             } else if ($parentData->type === 'object[]') {
-                if (!Arr::has($results, $dotPath)) {
-                    Arr::set($results, $dotPath, $value);
-                }
-                // If there's a second item in the array, set for that too.
-                if ($value !== null && Arr::has($results, Str::replaceLast('[]', '.1', $baseName))) {
-                    // If value is optional, flip a coin on whether to set or not
-                    if ($isRequired || array_rand([true, false], 1)) {
-                        Arr::set($results, Str::replaceLast('.0', '.1', $dotPath), $value);
+                // When the body is an array, param names will be  "[].paramname", so dot paths won't work correctly with "[]"
+                if (Str::startsWith($path, '[].')) {
+                    $valueDotPath = substr($dotPath, 3); // Remove initial '.0.'
+                    if (isset($results['[]'][0]) && !Arr::has($results['[]'][0], $valueDotPath)) {
+                        Arr::set($results['[]'][0], $valueDotPath, $value);
+                    }
+                    // If there's a second item in the array, set for that too.
+                    if ($value !== null && isset($results['[]'][1])) {
+                        // If value is optional, flip a coin on whether to set or not
+                        if ($isRequired || array_rand([true, false], 1)) {
+                            Arr::set($results['[]'][1], $valueDotPath, $value);
+                        }
+                    }
+                } else {
+                    if (!Arr::has($results, $dotPath)) {
+                        Arr::set($results, $dotPath, $value);
+                    }
+                    // If there's a second item in the array, set for that too.
+                    if ($value !== null && Arr::has($results, Str::replaceLast('[]', '.1', $baseName))) {
+                        // If value is optional, flip a coin on whether to set or not
+                        if ($isRequired || array_rand([true, false], 1)) {
+                            Arr::set($results, Str::replaceLast('.0', '.1', $dotPath), $value);
+                        }
                     }
                 }
             }
@@ -392,15 +429,19 @@ class Extractor
                 $parts = explode('.', $name);
                 $fieldName = array_pop($parts);
 
-                // If the user didn't add a parent field, we'll conveniently add it for them
+                // If the user didn't add a parent field, we'll helpfully add it for them
                 $parentName = rtrim(join('.', $parts), '[]');
+                // When the body is an array, param names will be  "[].paramname", so $parentName is empty
+                if (empty($parentName)) {
+                    $parentName = '[]';
+                }
                 if (empty($parameters[$parentName])) {
                     $normalisedParameters[$parentName] = new Parameter([
                         "name" => $parentName,
-                        "type" => "object",
+                        "type" => $parentName === '[]' ? "object[]" : "object",
                         "description" => "",
-                        "required" => false,
-                        "value" => [$fieldName => $parameter->value],
+                        "required" => $parentName === '[]' ? true : false,
+                        "example" => [$fieldName => $parameter->example],
                     ]);
                 }
             }
@@ -421,6 +462,10 @@ class Extractor
                 // The difference would be in the parent field's `type` property (object[] vs object)
                 // So we can get rid of all [] to get the parent name
                 $dotPathToParent = str_replace('[]', '', $baseName);
+                // When the body is an array, param names will be  "[].paramname", so $parts is ['[]']
+                if ($parts[0] == '[]') {
+                    $dotPathToParent = '[]'.$dotPathToParent;
+                }
 
                 $dotPath = $dotPathToParent . '.__fields.' . $fieldName;
                 Arr::set($finalParameters, $dotPath, $parameter);
@@ -434,6 +479,11 @@ class Extractor
 
         }
 
+        // Finally, if the body is an array, remove any other items.
+        if (isset($finalParameters['[]'])) {
+            $finalParameters = ["[]" => $finalParameters['[]']];
+        }
+
         return $finalParameters;
     }
 }

+ 7 - 1
src/Writing/OpenAPISpecWriter.php

@@ -208,10 +208,16 @@ class OpenAPISpecWriter
             $hasFileParameter = false;
 
             foreach ($endpoint->nestedBodyParameters as $name => $details) {
+                if ($name === "[]") { // Request body is an array
+                    $hasRequiredParameter = true;
+                    $schema = $this->generateFieldData($details);
+                    break;
+                }
+
                 if ($details['required']) {
                     $hasRequiredParameter = true;
                     // Don't declare this earlier.
-                    // Can't have an empty `required` array. Must have something there.
+                    // The spec doesn't allow for an empty `required` array. Must have something there.
                     $schema['required'][] = $name;
                 }
 

+ 1 - 1
src/Writing/PostmanCollectionWriter.php

@@ -163,7 +163,7 @@ class PostmanCollectionWriter
                 break;
             case 'raw':
             default:
-                $body[$inputMode] = json_encode($endpoint->cleanBodyParameters, JSON_PRETTY_PRINT);
+                $body[$inputMode] = json_encode($endpoint->cleanBodyParameters, JSON_UNESCAPED_UNICODE);
         }
         return $body;
     }

+ 7 - 7
tests/Fixtures/TestController.php

@@ -111,13 +111,13 @@ class TestController extends Controller
     /**
      * Endpoint with body parameters as array.
      *
-     * @bodyParam _ object[] Details.
-     * @bodyParam _[].first_name string The first name of the user. Example: John
-     * @bodyParam _[].last_name string The last name of the user. Example: Doe
-     * @bodyParam _[].contacts object[] Contact info
-     * @bodyParam _[].contacts[].first_name string The first name of the contact. Example: John
-     * @bodyParam _[].contacts[].last_name string The last name of the contact. Example: Doe
-     * @bodyParam _[].roles string[] The name of the role. Example: Admin
+     * @bodyParam [] object[] Details.
+     * @bodyParam [].first_name string required The first name of the user. Example: John
+     * @bodyParam [].last_name string required The last name of the user. Example: Doe
+     * @bodyParam [].contacts object[] required Contact info
+     * @bodyParam [].contacts[].first_name string required The first name of the contact. Example: Janelle
+     * @bodyParam [].contacts[].last_name string required The last name of the contact. Example: Monáe
+     * @bodyParam [].roles string[] required The name of the role. Example: ["Admin"]
      */
     public function withBodyParametersAsArray()
     {

+ 37 - 1
tests/Fixtures/collection.json

@@ -20,6 +20,42 @@
       "name": "Group A",
       "description": "",
       "item": [
+        {
+          "name": "Endpoint with body parameters as array.",
+          "request": {
+            "url": {
+              "protocol": "http",
+              "host": "{{baseUrl}}",
+              "path": "api/withBodyParametersAsArray",
+              "query": [],
+              "raw": "http://{{baseUrl}}/api/withBodyParametersAsArray"
+            },
+            "method": "POST",
+            "header": [
+              {
+                "key": "Custom-Header",
+                "value": "NotSoCustom"
+              },
+              {
+                "key": "Content-Type",
+                "value": "application/json"
+              },
+              {
+                "key": "Accept",
+                "value": "application/json"
+              }
+            ],
+            "body": {
+              "mode": "raw",
+              "raw": "[{\"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\"]}]"
+            },
+            "description": "",
+            "auth": {
+              "type": "noauth"
+            }
+          },
+          "response": []
+        },
         {
           "name": "Endpoint with body form data parameters.",
           "request": {
@@ -94,7 +130,7 @@
             ],
             "body": {
               "mode": "raw",
-              "raw": "{\n    \"user_id\": 9,\n    \"room_id\": \"consequatur\",\n    \"forever\": false,\n    \"another_one\": 11613.31890586,\n    \"yet_another_param\": {\n        \"name\": \"consequatur\"\n    },\n    \"even_more_param\": [\n        11613.31890586,\n        11613.31890586\n    ],\n    \"book\": {\n        \"name\": \"consequatur\",\n        \"author_id\": 17,\n        \"pages_count\": 17\n    },\n    \"ids\": [\n        17,\n        17\n    ],\n    \"users\": [\n        {\n            \"first_name\": \"John\",\n            \"last_name\": \"Doe\"\n        },\n        {\n            \"first_name\": \"John\",\n            \"last_name\": \"Doe\"\n        }\n    ]\n}"
+              "raw": "{\"user_id\":9,\"room_id\":\"consequatur\",\"forever\":false,\"another_one\":11613.31890586,\"yet_another_param\":{\"name\":\"consequatur\"},\"even_more_param\":[11613.31890586,11613.31890586],\"book\":{\"name\":\"consequatur\",\"author_id\":17,\"pages_count\":17},\"ids\":[17,17],\"users\":[{\"first_name\":\"John\",\"last_name\":\"Doe\"},{\"first_name\":\"John\",\"last_name\":\"Doe\"}]}"
             },
             "description": "",
             "auth": {

+ 44 - 0
tests/Fixtures/openapi.yaml

@@ -263,3 +263,47 @@ paths:
                     omitted:
                         summary: 'When the value is omitted'
                         value: ''
+    /api/withBodyParametersAsArray:
+        post:
+            summary: 'Endpoint with body parameters as array.'
+            description: ''
+            parameters:
+                - in: header
+                  name: Custom-Header
+                  description: ''
+                  example: NotSoCustom
+                  schema:
+                      type: string
+                - in: header
+                  name: Content-Type
+                  description: ''
+                  example: application/json
+                  schema:
+                      type: string
+            responses: {}
+            tags:
+                - Group A
+            requestBody:
+                required: true
+                content:
+                    application/json:
+                        schema:
+                            type: array
+                            description: Details.
+                            example:
+                                - [ ]
+                                - [ ]
+                            items:
+                                type: object
+                                properties:
+                                    first_name: { type: string, description: 'The first name of the user.', example: John }
+                                    last_name: { type: string, description: 'The last name of the user.', example: Doe }
+                                    contacts: { type: array, description: 'Contact info', example: [ [ ], [ ] ], items: { type: object, properties: { first_name: { type: string, description: 'The first name of the contact.', example: Janelle }, last_name: { type: string, description: 'The last name of the contact.', example: Monáe } }, 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: [ ]
+

+ 2 - 1
tests/GenerateDocumentationTest.php

@@ -226,7 +226,7 @@ class GenerateDocumentationTest extends BaseLaravelTest
     /** @test */
     public function generated_postman_collection_file_is_correct()
     {
-        // RouteFacade::get('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']);
+        RouteFacade::post('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']);
         RouteFacade::post('/api/withFormDataParams', [TestController::class, 'withFormDataParams']);
         RouteFacade::post('/api/withBodyParameters', [TestController::class, 'withBodyParameters']);
         RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']);
@@ -261,6 +261,7 @@ class GenerateDocumentationTest extends BaseLaravelTest
     /** @test */
     public function generated_openapi_spec_file_is_correct()
     {
+        RouteFacade::post('/api/withBodyParametersAsArray', [TestController::class, 'withBodyParametersAsArray']);
         RouteFacade::post('/api/withFormDataParams', [TestController::class, 'withFormDataParams']);
         RouteFacade::get('/api/withResponseTag', [TestController::class, 'withResponseTag']);
         RouteFacade::get('/api/withQueryParameters', [TestController::class, 'withQueryParameters']);