Selaa lähdekoodia

Merge pull request #599 from mpociot/feature/urlparams

Switch from `bindings` to `@urlParam` annotation
Shalvah 5 vuotta sitten
vanhempi
commit
ddefe36429

+ 1 - 2
TODO.md

@@ -1,4 +1,3 @@
 Major
-- Bring `bindings` outside of `response_calls`
-- Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`?
+- Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? yes
 - Should we move HTML generation from Blade to fully PHP? (L)

+ 5 - 16
config/apidoc.php

@@ -119,20 +119,6 @@ return [
                      */
                     'methods' => ['GET'],
 
-                    /*
-                     * For URLs which have parameters (/users/{user}, /orders/{id?}),
-                     * specify what values the parameters should be replaced with.
-                     * Note that you must specify the full parameter,
-                     * including curly brackets and question marks if any.
-                     *
-                     * You may also specify the preceding path, to allow for variations,
-                     * for instance 'users/{id}' => 1 and 'apps/{id}' => 'htTviP'.
-                     * However, there must only be one parameter per path.
-                     */
-                    'bindings' => [
-                        // '{user}' => 1,
-                    ],
-
                     /*
                      * Laravel config variables which should be set for the API call.
                      * This is a good place to ensure that notifications, emails
@@ -183,12 +169,15 @@ return [
         'metadata' => [
             \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
         ],
-        'bodyParameters' => [
-            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        'urlParameters' => [
+            \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
         ],
         'queryParameters' => [
             \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
         ],
+        'bodyParameters' => [
+            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        ],
         'responses' => [
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,

+ 3 - 2
docs/documenting.md

@@ -48,9 +48,10 @@ class UserController extends Controller
 ![Doc block result](http://headsquaredsoftware.co.uk/images/api_generator_docblock.png)
 
 ## Specifying request parameters
-To specify a list of valid parameters your API route accepts, use the `@bodyParam` and `@queryParam` annotations.
+To specify a list of valid parameters your API route accepts, use the `@urlParam`, `@bodyParam` and `@queryParam` annotations.
+- The `@urlParam` annotation is used for describing parameters in your URl. For instance, in a Laravel Route like this: "/users/{id}/{lang?}", you would use this annotation to describe the `id` and `lang` parameters. It takes the name of the parameter, an optional "required" label, and then its description.
+- The `@queryParam` annotation takes the name of the parameter, an optional "required" label, and then its description.
 - The `@bodyParam` annotation takes the name of the parameter, its type, an optional "required" label, and then its description. 
-- The `@queryParam` annotation takes the name of the parameter, an optional "required" label, and then its description,
 
 Examples:
 

+ 8 - 3
docs/plugins.md

@@ -56,12 +56,15 @@ The last thing to do is to register the strategy. Strategies are registered in a
         'metadata' => [
             \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
         ],
-        'bodyParameters' => [
-            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        'urlParameters' => [
+            \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
         ],
         'queryParameters' => [
             \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
         ],
+        'bodyParameters' => [
+            \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+        ],
         'responses' => [
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
             \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,
@@ -109,9 +112,11 @@ public function __invoke(Route $route, \ReflectionClass $controller, \Reflection
 }
 ```
 
+> Note: If you would like a parameter (body or query) to be included in the documentation but excluded from examples, set its `value` property to `null`.
+
 The strategy class also has access to the current apidoc configuration via its `config` property. For instance, you can retrieve the default group with `$this->config->get('default_group')`.
 
-Yopu are also provided with the instance pproperty `stage`, which is set to the name of the currently executing stage.
+You are also provided with the instance pproperty `stage`, which is set to the name of the currently executing stage.
 
 
 ## Utilities

+ 14 - 6
resources/views/partials/route.blade.php

@@ -47,13 +47,13 @@
 `{{$method}} {{$route['uri']}}`
 
 @endforeach
-@if(count($route['bodyParameters']))
-#### Body Parameters
+@if(count($route['urlParameters']))
+#### URL Parameters
 
-Parameter | Type | Status | Description
---------- | ------- | ------- | ------- | -----------
-@foreach($route['bodyParameters'] as $attribute => $parameter)
-    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
+Parameter | Status | Description
+--------- | ------- | ------- | -------
+@foreach($route['urlParameters'] as $attribute => $parameter)
+    {{$attribute}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
 @endforeach
 @endif
 @if(count($route['queryParameters']))
@@ -65,5 +65,13 @@ Parameter | Status | Description
     {{$attribute}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
 @endforeach
 @endif
+@if(count($route['bodyParameters']))
+#### Body Parameters
+Parameter | Type | Status | Description
+--------- | ------- | ------- | ------- | -----------
+@foreach($route['bodyParameters'] as $attribute => $parameter)
+    {{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
+    @endforeach
+@endif
 
 <!-- END_{{$route['id']}} -->

+ 6 - 3
src/Strategies/BodyParameters/GetFromBodyParamTag.php

@@ -25,9 +25,7 @@ class GetFromBodyParamTag extends Strategy
                 continue;
             }
 
-            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
-                ? $paramType->__toString()
-                : $paramType->getName();
+            $parameterClassName = $paramType->getName();
 
             try {
                 $parameterClass = new ReflectionClass($parameterClassName);
@@ -59,6 +57,11 @@ class GetFromBodyParamTag extends Strategy
                 return $tag instanceof Tag && $tag->getName() === 'bodyParam';
             })
             ->mapWithKeys(function ($tag) {
+                // Format:
+                // @bodyParam <name> <type> <"required" (optional)> <description>
+                // Examples:
+                // @bodyParam text string required The text.
+                // @bodyParam user_id integer The ID of the user.
                 preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
                 $content = preg_replace('/\s?No-example.?/', '', $content);
                 if (empty($content)) {

+ 8 - 5
src/Strategies/QueryParameters/GetFromQueryParamTag.php

@@ -26,9 +26,7 @@ class GetFromQueryParamTag extends Strategy
                 continue;
             }
 
-            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
-                ? $paramType->__toString()
-                : $paramType->getName();
+            $parameterClassName = $paramType->getName();
 
             try {
                 $parameterClass = new ReflectionClass($parameterClassName);
@@ -40,7 +38,7 @@ class GetFromQueryParamTag extends Strategy
             if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class)
                 || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) {
                 $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
-                $queryParametersFromDocBlock = $this->getqueryParametersFromDocBlock($formRequestDocBlock->getTags());
+                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
 
                 if (count($queryParametersFromDocBlock)) {
                     return $queryParametersFromDocBlock;
@@ -50,7 +48,7 @@ class GetFromQueryParamTag extends Strategy
 
         $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method'];
 
-        return $this->getqueryParametersFromDocBlock($methodDocBlock->getTags());
+        return $this->getQueryParametersFromDocBlock($methodDocBlock->getTags());
     }
 
     private function getQueryParametersFromDocBlock($tags)
@@ -60,6 +58,11 @@ class GetFromQueryParamTag extends Strategy
                 return $tag instanceof Tag && $tag->getName() === 'queryParam';
             })
             ->mapWithKeys(function ($tag) {
+                // Format:
+                // @queryParam <name> <"required" (optional)> <description>
+                // Examples:
+                // @queryParam text string required The text.
+                // @queryParam user_id The ID of the user.
                 preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
                 $content = preg_replace('/\s?No-example.?/', '', $content);
                 if (empty($content)) {

+ 4 - 3
src/Strategies/Responses/ResponseCalls.php

@@ -40,7 +40,8 @@ class ResponseCalls extends Strategy
         // Mix in parsed parameters with manually specified parameters.
         $bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['body'] ?? []);
         $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['query'] ?? []);
-        $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters);
+        $urlParameters = $context['cleanUrlParameters'];
+        $request = $this->prepareRequest($route, $rulesToApply, $urlParameters, $bodyParameters, $queryParameters);
 
         try {
             $response = $this->makeApiCall($request);
@@ -80,9 +81,9 @@ class ResponseCalls extends Strategy
      *
      * @return Request
      */
-    protected function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
+    protected function prepareRequest(Route $route, array $rulesToApply, array $urlParams, array $bodyParams, array $queryParams)
     {
-        $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []);
+        $uri = Utils::getFullUrl($route, $urlParams);
         $routeMethods = $this->getMethods($route);
         $method = array_shift($routeMethods);
         $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];

+ 95 - 0
src/Strategies/UrlParameters/GetFromUrlParamTag.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Mpociot\ApiDoc\Strategies\UrlParameters;
+
+use ReflectionClass;
+use ReflectionMethod;
+use Illuminate\Support\Str;
+use Illuminate\Routing\Route;
+use Mpociot\Reflection\DocBlock;
+use Mpociot\Reflection\DocBlock\Tag;
+use Mpociot\ApiDoc\Strategies\Strategy;
+use Mpociot\ApiDoc\Tools\RouteDocBlocker;
+use Dingo\Api\Http\FormRequest as DingoFormRequest;
+use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers;
+use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest;
+
+class GetFromUrlParamTag extends Strategy
+{
+    use DocBlockParamHelpers;
+
+    public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = [])
+    {
+        foreach ($method->getParameters() as $param) {
+            $paramType = $param->getType();
+            if ($paramType === null) {
+                continue;
+            }
+
+            $parameterClassName = $paramType->getName();
+
+            try {
+                $parameterClass = new ReflectionClass($parameterClassName);
+            } catch (\ReflectionException $e) {
+                continue;
+            }
+
+            // If there's a FormRequest, we check there for @urlParam tags.
+            if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class)
+                || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) {
+                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
+                $queryParametersFromDocBlock = $this->getUrlParametersFromDocBlock($formRequestDocBlock->getTags());
+
+                if (count($queryParametersFromDocBlock)) {
+                    return $queryParametersFromDocBlock;
+                }
+            }
+        }
+
+        $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method'];
+
+        return $this->getUrlParametersFromDocBlock($methodDocBlock->getTags());
+    }
+
+    private function getUrlParametersFromDocBlock($tags)
+    {
+        $parameters = collect($tags)
+            ->filter(function ($tag) {
+                return $tag instanceof Tag && $tag->getName() === 'urlParam';
+            })
+            ->mapWithKeys(function ($tag) {
+                // Format:
+                // @urlParam <name> <"required" (optional)> <description>
+                // Examples:
+                // @urlParam id string required The id of the post.
+                // @urlParam user_id The ID of the user.
+                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
+                $content = preg_replace('/\s?No-example.?/', '', $content);
+                if (empty($content)) {
+                    // this means only name was supplied
+                    list($name) = preg_split('/\s+/', $tag->getContent());
+                    $required = false;
+                    $description = '';
+                } else {
+                    list($_, $name, $required, $description) = $content;
+                    $description = trim($description);
+                    if ($description == 'required' && empty(trim($required))) {
+                        $required = $description;
+                        $description = '';
+                    }
+                    $required = trim($required) == 'required' ? true : false;
+                }
+
+                list($description, $value) = $this->parseParamDescription($description, 'string');
+                if (is_null($value) && ! $this->shouldExcludeExample($tag)) {
+                    $value = Str::contains($description, ['number', 'count', 'page'])
+                        ? $this->generateDummyValue('integer')
+                        : $this->generateDummyValue('string');
+                }
+
+                return [$name => compact('description', 'required', 'value')];
+            })->toArray();
+
+        return $parameters;
+    }
+}

+ 21 - 8
src/Tools/Generator.php

@@ -57,18 +57,23 @@ class Generator
             'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
             'methods' => $this->getMethods($route),
             'uri' => $this->getUri($route),
-            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
         ];
         $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['metadata'] = $metadata;
-        $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
-        $parsedRoute['bodyParameters'] = $bodyParameters;
-        $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters);
+
+        $urlParameters = $this->fetchUrlParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
+        $parsedRoute['urlParameters'] = $urlParameters;
+        $parsedRoute['cleanUrlParameters'] = $this->cleanParams($urlParameters);
+        $parsedRoute['boundUri'] = Utils::getFullUrl($route, $parsedRoute['cleanUrlParameters']);
 
         $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['queryParameters'] = $queryParameters;
         $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters);
 
+        $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute);
+        $parsedRoute['bodyParameters'] = $bodyParameters;
+        $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters);
+
         $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute);
         $parsedRoute['response'] = $responses;
         $parsedRoute['showresponse'] = ! empty($responses);
@@ -93,9 +98,9 @@ class Generator
         return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]);
     }
 
-    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
+    protected function fetchUrlParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
     {
-        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
+        return $this->iterateThroughStrategies('urlParameters', $context, [$route, $controller, $method, $rulesToApply]);
     }
 
     protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
@@ -103,6 +108,11 @@ class Generator
         return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]);
     }
 
+    protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
+    {
+        return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
+    }
+
     protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
     {
         $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]);
@@ -124,12 +134,15 @@ class Generator
             'metadata' => [
                 \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
             ],
-            'bodyParameters' => [
-                \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+            'urlParameters' => [
+                \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
             ],
             'queryParameters' => [
                 \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
             ],
+            'bodyParameters' => [
+                \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
+            ],
             'responses' => [
                 \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
                 \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,

+ 2 - 2
src/Tools/Traits/DocBlockParamHelpers.php

@@ -33,8 +33,8 @@ trait DocBlockParamHelpers
     protected function parseParamDescription(string $description, string $type)
     {
         $example = null;
-        if (preg_match('/(.*)\s+Example:\s*(.+)\s*/', $description, $content)) {
-            $description = $content[1];
+        if (preg_match('/(.*)\bExample:\s*(.+)\s*/', $description, $content)) {
+            $description = trim($content[1]);
 
             // examples are parsed as strings by default, we need to cast them properly
             $example = $this->castToType($content[2], $type);

+ 0 - 1
tests/Fixtures/TestController.php

@@ -188,7 +188,6 @@ class TestController extends Controller
     {
         return [
             '{id}' => $id,
-            'APP_ENV' => getenv('APP_ENV'),
             'header' => request()->header('header'),
             'queryParam' => request()->query('queryParam'),
             'bodyParam' => request()->get('bodyParam'),

+ 12 - 0
tests/Fixtures/collection.json

@@ -117,12 +117,24 @@
                                     "type": "text",
                                     "enabled": true
                                 },
+                                {
+                                    "key": "yet_another_param.name",
+                                    "value": "consequatur",
+                                    "type": "text",
+                                    "enabled": true
+                                },
                                 {
                                     "key": "even_more_param",
                                     "value": [],
                                     "type": "text",
                                     "enabled": true
                                 },
+                                {
+                                    "key": "even_more_param.*",
+                                    "value": 11613.31890586,
+                                    "type": "text",
+                                    "enabled": true
+                                },
                                 {
                                     "key": "book.name",
                                     "value": "consequatur",

+ 14 - 2
tests/Fixtures/collection_with_body_parameters.json

@@ -19,7 +19,7 @@
                         "header": [
                             {
                                 "key": "Accept",
-                                "value": "application/json"
+                                "value": "application\/json"
                             }
                         ],
                         "body": {
@@ -51,7 +51,13 @@
                                 },
                                 {
                                     "key": "yet_another_param",
-                                    "value": [],
+                                    "value": {},
+                                    "type": "text",
+                                    "enabled": true
+                                },
+                                {
+                                    "key": "yet_another_param.name",
+                                    "value": "consequatur",
                                     "type": "text",
                                     "enabled": true
                                 },
@@ -61,6 +67,12 @@
                                     "type": "text",
                                     "enabled": true
                                 },
+                                {
+                                    "key": "even_more_param.*",
+                                    "value": 11613.31890586,
+                                    "type": "text",
+                                    "enabled": true
+                                },
                                 {
                                     "key": "book.name",
                                     "value": "consequatur",

+ 137 - 43
tests/Fixtures/index.md

@@ -32,20 +32,23 @@ It can also be multiple lines long.
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/withDescription" \
+curl -X GET \
+    -G "http://localhost/api/withDescription" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withDescription");
+const url = new URL(
+    "http://localhost/api/withDescription"
+);
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -73,20 +76,23 @@ null
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/withResponseTag" \
+curl -X GET \
+    -G "http://localhost/api/withResponseTag" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withResponseTag");
+const url = new URL(
+    "http://localhost/api/withResponseTag"
+);
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -122,31 +128,38 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/withBodyParameters" \
+curl -X GET \
+    -G "http://localhost/api/withBodyParameters" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom" \
     -H "Content-Type: application/json" \
-    -d '{"user_id":9,"room_id":"consequatur","forever":false,"another_one":11613.31890586,"yet_another_param":{},"even_more_param":[],"book":{"name":"consequatur","author_id":17,"pages_count":17},"ids":[17],"users":[{"first_name":"John","last_name":"Doe"}]}'
+    -d '{"user_id":9,"room_id":"consequatur","forever":false,"another_one":11613.31890586,"yet_another_param":{"name":"consequatur"},"even_more_param":[11613.31890586],"book":{"name":"consequatur","author_id":17,"pages_count":17},"ids":[17],"users":[{"first_name":"John","last_name":"Doe"}]}'
 
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withBodyParameters");
+const url = new URL(
+    "http://localhost/api/withBodyParameters"
+);
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Content-Type": "application/json",
     "Accept": "application/json",
-}
+};
 
 let body = {
     "user_id": 9,
     "room_id": "consequatur",
     "forever": false,
     "another_one": 11613.31890586,
-    "yet_another_param": {},
-    "even_more_param": [],
+    "yet_another_param": {
+        "name": "consequatur"
+    },
+    "even_more_param": [
+        11613.31890586
+    ],
     "book": {
         "name": "consequatur",
         "author_id": 17,
@@ -183,22 +196,23 @@ null
 `GET api/withBodyParameters`
 
 #### Body Parameters
-
 Parameter | Type | Status | Description
 --------- | ------- | ------- | ------- | -----------
     user_id | integer |  required  | The id of the user.
-    room_id | string |  optional  | The id of the room.
-    forever | boolean |  optional  | Whether to ban the user forever.
-    another_one | number |  optional  | Just need something here.
-    yet_another_param | object |  required  | 
-    even_more_param | array |  optional  | 
-    book.name | string |  optional  | 
-    book.author_id | integer |  optional  | 
-    book[pages_count] | integer |  optional  | 
-    ids.* | integer |  optional  | 
-    users.*.first_name | string |  optional  | The first name of the user.
-    users.*.last_name | string |  optional  | The last name of the user.
-
+        room_id | string |  optional  | The id of the room.
+        forever | boolean |  optional  | Whether to ban the user forever.
+        another_one | number |  optional  | Just need something here.
+        yet_another_param | object |  required  | Some object params.
+        yet_another_param.name | string |  required  | Subkey in the object param.
+        even_more_param | array |  optional  | Some array params.
+        even_more_param.* | float |  optional  | Subkey in the array param.
+        book.name | string |  optional  | 
+        book.author_id | integer |  optional  | 
+        book[pages_count] | integer |  optional  | 
+        ids.* | integer |  optional  | 
+        users.*.first_name | string |  optional  | The first name of the user.
+        users.*.last_name | string |  optional  | The last name of the user.
+    
 <!-- END_a25cb3b490fa579d7d77b386bbb7ec03 -->
 
 <!-- START_5c545aa7f913d84b23ac4cfefc1de659 -->
@@ -206,29 +220,33 @@ Parameter | Type | Status | Description
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D" \
+curl -X GET \
+    -G "http://localhost/api/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withQueryParameters");
-
-    let params = {
-            "location_id": "consequatur",
-            "user_id": "me",
-            "page": "4",
-            "filters": "consequatur",
-            "url_encoded": "+ []&amp;=",
-        };
-    Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
+const url = new URL(
+    "http://localhost/api/withQueryParameters"
+);
+
+let params = {
+    "location_id": "consequatur",
+    "user_id": "me",
+    "page": "4",
+    "filters": "consequatur",
+    "url_encoded": "+ []&amp;=",
+};
+Object.keys(params)
+    .forEach(key => url.searchParams.append(key, params[key]));
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -266,20 +284,23 @@ Parameter | Status | Description
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/withAuthTag" \
+curl -X GET \
+    -G "http://localhost/api/withAuthTag" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withAuthTag");
+const url = new URL(
+    "http://localhost/api/withAuthTag"
+);
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -307,20 +328,23 @@ null
 > Example request:
 
 ```bash
-curl -X POST "http://localhost/api/withMultipleResponseTagsAndStatusCode" \
+curl -X POST \
+    "http://localhost/api/withMultipleResponseTagsAndStatusCode" \
     -H "Authorization: customAuthToken" \
     -H "Custom-Header: NotSoCustom"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/withMultipleResponseTagsAndStatusCode");
+const url = new URL(
+    "http://localhost/api/withMultipleResponseTagsAndStatusCode"
+);
 
 let headers = {
     "Authorization": "customAuthToken",
     "Custom-Header": "NotSoCustom",
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "POST",
@@ -357,4 +381,74 @@ fetch(url, {
 
 <!-- END_55f321056bfc0de7269ac70e24eb84be -->
 
+#Other😎
+
+
+<!-- START_33e62c07bc6d7286628b18c0e046ebea -->
+## api/echoesUrlParameters/{param}-{param2}/{param3?}
+> Example request:
+
+```bash
+curl -X GET \
+    -G "http://localhost/api/echoesUrlParameters/4-consequatur/?something=consequatur" \
+    -H "Authorization: customAuthToken" \
+    -H "Custom-Header: NotSoCustom"
+```
+
+```javascript
+const url = new URL(
+    "http://localhost/api/echoesUrlParameters/4-consequatur/"
+);
+
+let params = {
+    "something": "consequatur",
+};
+Object.keys(params)
+    .forEach(key => url.searchParams.append(key, params[key]));
+
+let headers = {
+    "Authorization": "customAuthToken",
+    "Custom-Header": "NotSoCustom",
+    "Accept": "application/json",
+    "Content-Type": "application/json",
+};
+
+fetch(url, {
+    method: "GET",
+    headers: headers,
+})
+    .then(response => response.json())
+    .then(json => console.log(json));
+```
+
+
+> Example response (200):
+
+```json
+{
+    "param": "4",
+    "param2": "consequatur",
+    "param3": null,
+    "param4": null
+}
+```
+
+### HTTP Request
+`GET api/echoesUrlParameters/{param}-{param2}/{param3?}`
+
+#### URL Parameters
+
+Parameter | Status | Description
+--------- | ------- | ------- | -------
+    param |  required  | 
+    param2 |  optional  | 
+    param4 |  optional  | 
+#### Query Parameters
+
+Parameter | Status | Description
+--------- | ------- | ------- | -----------
+    something |  optional  | 
+
+<!-- END_33e62c07bc6d7286628b18c0e046ebea -->
+
 

+ 12 - 6
tests/Fixtures/partial_resource_index.md

@@ -29,17 +29,20 @@ Welcome to the generated API reference.
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users" \
+curl -X GET \
+    -G "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users");
+const url = new URL(
+    "http://localhost/api/users"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -70,17 +73,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users/create" \
+curl -X GET \
+    -G "http://localhost/api/users/create" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/create");
+const url = new URL(
+    "http://localhost/api/users/create"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",

+ 42 - 21
tests/Fixtures/resource_index.md

@@ -29,17 +29,20 @@ Welcome to the generated API reference.
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users" \
+curl -X GET \
+    -G "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users");
+const url = new URL(
+    "http://localhost/api/users"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -70,17 +73,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users/create" \
+curl -X GET \
+    -G "http://localhost/api/users/create" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/create");
+const url = new URL(
+    "http://localhost/api/users/create"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -111,17 +117,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X POST "http://localhost/api/users" \
+curl -X POST \
+    "http://localhost/api/users" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users");
+const url = new URL(
+    "http://localhost/api/users"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "POST",
@@ -145,17 +154,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users/1" \
+curl -X GET \
+    -G "http://localhost/api/users/1" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/1");
+const url = new URL(
+    "http://localhost/api/users/1"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -186,17 +198,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X GET -G "http://localhost/api/users/1/edit" \
+curl -X GET \
+    -G "http://localhost/api/users/1/edit" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/1/edit");
+const url = new URL(
+    "http://localhost/api/users/1/edit"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "GET",
@@ -227,17 +242,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X PUT "http://localhost/api/users/1" \
+curl -X PUT \
+    "http://localhost/api/users/1" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/1");
+const url = new URL(
+    "http://localhost/api/users/1"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "PUT",
@@ -263,17 +281,20 @@ fetch(url, {
 > Example request:
 
 ```bash
-curl -X DELETE "http://localhost/api/users/1" \
+curl -X DELETE \
+    "http://localhost/api/users/1" \
     -H "Accept: application/json"
 ```
 
 ```javascript
-const url = new URL("http://localhost/api/users/1");
+const url = new URL(
+    "http://localhost/api/users/1"
+);
 
 let headers = {
     "Accept": "application/json",
     "Content-Type": "application/json",
-}
+};
 
 fetch(url, {
     method: "DELETE",

+ 2 - 1
tests/GenerateDocumentationTest.php

@@ -163,12 +163,13 @@ class GenerateDocumentationTest extends TestCase
     /** @test */
     public function generated_markdown_file_is_correct()
     {
-        RouteFacade::get('/api/withDescription', TestController::class.'@withEndpointDescription');
+        RouteFacade::get('/api/withDescription', [TestController::class, 'withEndpointDescription']);
         RouteFacade::get('/api/withResponseTag', TestController::class.'@withResponseTag');
         RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters');
         RouteFacade::get('/api/withQueryParameters', TestController::class.'@withQueryParameters');
         RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag');
         RouteFacade::post('/api/withMultipleResponseTagsAndStatusCode', [TestController::class, 'withMultipleResponseTagsAndStatusCode']);
+        RouteFacade::get('/api/echoesUrlParameters/{param}-{param2}/{param3?}', [TestController::class, 'echoesUrlParameters']);
 
         // We want to have the same values for params each time
         config(['apidoc.faker_seed' => 1234]);

+ 54 - 50
tests/Unit/GeneratorTestCase.php

@@ -1,5 +1,7 @@
 <?php
 
+/** @noinspection ALL */
+
 namespace Mpociot\ApiDoc\Tests\Unit;
 
 use Illuminate\Support\Arr;
@@ -97,12 +99,54 @@ abstract class GeneratorTestCase extends TestCase
             'yet_another_param' => [
                 'type' => 'object',
                 'required' => true,
-                'description' => '',
+                'description' => 'Some object params.',
+            ],
+            'yet_another_param.name' => [
+                'type' => 'string',
+                'description' => 'Subkey in the object param.',
+                'required' => true,
             ],
             'even_more_param' => [
                 'type' => 'array',
                 'required' => false,
+                'description' => 'Some array params.',
+            ],
+            'even_more_param.*' => [
+                'type' => 'float',
+                'description' => 'Subkey in the array param.',
+                'required' => false,
+            ],
+            'book.name' => [
+                'type' => 'string',
                 'description' => '',
+                'required' => false,
+            ],
+            'book.author_id' => [
+                'type' => 'integer',
+                'description' => '',
+                'required' => false,
+            ],
+            'book[pages_count]' => [
+                'type' => 'integer',
+                'description' => '',
+                'required' => false,
+            ],
+            'ids.*' => [
+                'type' => 'integer',
+                'description' => '',
+                'required' => false,
+            ],
+            'users.*.first_name' => [
+                'type' => 'string',
+                'description' => 'The first name of the user.',
+                'required' => false,
+                'value' => 'John',
+            ],
+            'users.*.last_name' => [
+                'type' => 'string',
+                'description' => 'The last name of the user.',
+                'required' => false,
+                'value' => 'Doe',
             ],
         ], $bodyParameters);
     }
@@ -558,71 +602,35 @@ abstract class GeneratorTestCase extends TestCase
     }
 
     /** @test */
-    public function can_override_url_path_parameters_with_bindings()
+    public function can_override_url_path_parameters_with_urlparam_annotation()
     {
-        $route = $this->createRoute('POST', '/echoesUrlPathParameters/{param}', 'echoesUrlPathParameters', true);
-
-        $rand = rand();
+        $route = $this->createRoute('POST', '/echoesUrlParameters/{param}', 'echoesUrlParameters', true);
         $rules = [
             'response_calls' => [
                 'methods' => ['*'],
-                'bindings' => [
-                    '{param}' => $rand,
-                ],
             ],
         ];
         $parsed = $this->generator->processRoute($route, $rules);
         $response = json_decode(Arr::first($parsed['response'])['content'], true);
-        $param = $response['param'];
-        $this->assertEquals($rand, $param);
+        $this->assertEquals(4, $response['param']);
     }
 
     /** @test */
-    public function replaces_optional_url_path_parameters_with_bindings()
+    public function ignores_or_inserts_optional_url_path_parameters_according_to_annotations()
     {
-        $route = $this->createRoute('POST', '/echoesUrlPathParameters/{param?}', 'echoesUrlPathParameters', true);
+        $route = $this->createRoute('POST', '/echoesUrlParameters/{param}/{param2?}/{param3}/{param4?}', 'echoesUrlParameters', true);
 
-        $rand = rand();
         $rules = [
             'response_calls' => [
                 'methods' => ['*'],
-                'bindings' => [
-                    '{param?}' => $rand,
-                ],
             ],
         ];
         $parsed = $this->generator->processRoute($route, $rules);
         $response = json_decode(Arr::first($parsed['response'])['content'], true);
-        $param = $response['param'];
-        $this->assertEquals($rand, $param);
-    }
-
-    /** @test */
-    public function uses_correct_bindings_by_prefix()
-    {
-        $route1 = $this->createRoute('POST', '/echoesUrlPathParameters/first/{param}', 'echoesUrlPathParameters', true);
-        $route2 = $this->createRoute('POST', '/echoesUrlPathParameters/second/{param}', 'echoesUrlPathParameters', true);
-
-        $rand1 = rand();
-        $rand2 = rand();
-        $rules = [
-            'response_calls' => [
-                'methods' => ['*'],
-                'bindings' => [
-                    'first/{param}' => $rand1,
-                    'second/{param}' => $rand2,
-                ],
-            ],
-        ];
-        $parsed = $this->generator->processRoute($route1, $rules);
-        $response = json_decode(Arr::first($parsed['response'])['content'], true);
-        $param = $response['param'];
-        $this->assertEquals($rand1, $param);
-
-        $parsed = $this->generator->processRoute($route2, $rules);
-        $response = json_decode(Arr::first($parsed['response'])['content'], true);
-        $param = $response['param'];
-        $this->assertEquals($rand2, $param);
+        $this->assertEquals(4, $response['param']);
+        $this->assertNotNull($response['param2']);
+        $this->assertEquals(1, $response['param3']);
+        $this->assertNull($response['param4']);
     }
 
     /** @test */
@@ -747,9 +755,6 @@ abstract class GeneratorTestCase extends TestCase
                     'Accept' => 'application/json',
                     'header' => 'value',
                 ],
-                'bindings' => [
-                    '{id}' => 3,
-                ],
                 'query' => [
                     'queryParam' => 'queryValue',
                 ],
@@ -767,7 +772,6 @@ abstract class GeneratorTestCase extends TestCase
         $this->assertTrue(is_array($response));
         $this->assertEquals(200, $response['status']);
         $responseContent = json_decode($response['content'], true);
-        $this->assertEquals(3, $responseContent['{id}']);
         $this->assertEquals('queryValue', $responseContent['queryParam']);
         $this->assertEquals('bodyValue', $responseContent['bodyParam']);
         $this->assertEquals('value', $responseContent['header']);