From 3a618c96741b595225d2ee366e4d8773782effd6 Mon Sep 17 00:00:00 2001 From: "coder.cso" Date: Tue, 24 Mar 2020 16:31:24 +0700 Subject: [PATCH] Add support for @responseProp annotation --- composer.json | 8 +- config/apidoc.php | 67 ++++++++------- docs/plugins.md | 1 + src/Extracting/Generator.php | 12 +++ .../GetFromResponseParamTag.php | 84 +++++++++++++++++++ 5 files changed, 139 insertions(+), 33 deletions(-) create mode 100644 src/Extracting/Strategies/ResponseParameters/GetFromResponseParamTag.php diff --git a/composer.json b/composer.json index b32e816d..eb6b1ed0 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,12 @@ "email": "m.pociot@gmail.com" } ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/coder-cso/documentarian" + } + ], "require": { "php": ">=7.2.0", "ext-json": "*", @@ -22,7 +28,7 @@ "illuminate/routing": "^5.7|^6.0|^7.0", "illuminate/support": "^5.7|^6.0|^7.0", "league/flysystem": "^1.0", - "mpociot/documentarian": "^0.4.0", + "mpociot/documentarian": "dev-master", "mpociot/reflection-docblock": "^1.0.1", "nunomaduro/collision": "^3.0|^4.0", "ramsey/uuid": "^3.8", diff --git a/config/apidoc.php b/config/apidoc.php index 80c5e73d..984dc574 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -7,24 +7,24 @@ * - "laravel" will generate the documentation as a Blade view, * so you can add routing and authentication. */ - 'type' => 'static', + 'type' => 'static', /* * Settings for `laravel` type output. */ - 'laravel' => [ + 'laravel' => [ /* * Whether to automatically create a docs endpoint for you to view your generated docs. * If this is false, you can still set up routing manually. */ - 'autoload' => false, + 'autoload' => false, /* * URL path to use for the docs endpoint (if `autoload` is true). * * By default, `/doc` opens the HTML page, and `/doc.json` downloads the Postman collection. */ - 'docs_url' => '/doc', + 'docs_url' => '/doc', /* * Middleware to attach to the docs endpoint (if `autoload` is true). @@ -35,7 +35,7 @@ /* * The router to be used (Laravel or Dingo). */ - 'router' => 'laravel', + 'router' => 'laravel', /* * The base URL to be used in examples and the Postman collection. @@ -49,16 +49,16 @@ * For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json. * The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection. */ - 'postman' => [ + 'postman' => [ /* * Specify whether the Postman collection should be generated. */ - 'enabled' => true, + 'enabled' => true, /* * The name for the exported Postman collection. Default: config('app.name')." API" */ - 'name' => null, + 'name' => null, /* * The description for the exported Postman collection. @@ -69,7 +69,7 @@ * The "Auth" section that should appear in the postman collection. See the schema docs for more information: * https://schema.getpostman.com/json/collection/v2.0.0/docs/index.html */ - 'auth' => null, + 'auth' => null, ], /* @@ -77,18 +77,18 @@ * Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections) * and rules which should be applied to them ('apply' section). */ - 'routes' => [ + 'routes' => [ [ /* * Specify conditions to determine what routes will be parsed in this group. * A route must fulfill ALL conditions to pass. */ - 'match' => [ + 'match' => [ /* * Match only routes whose domains match this pattern (use * as a wildcard to match any characters). */ - 'domains' => [ + 'domains' => [ '*', // 'domain1.*', ], @@ -131,13 +131,13 @@ /* * Specify rules to be applied to all the routes in this group when generating documentation */ - 'apply' => [ + 'apply' => [ /* * Specify headers to be added to the example requests */ - 'headers' => [ + 'headers' => [ 'Content-Type' => 'application/json', - 'Accept' => 'application/json', + 'Accept' => 'application/json', // 'Authorization' => 'Bearer {token}', // 'Api-Version' => 'v2', ], @@ -152,7 +152,7 @@ * API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc). * List the methods here or use '*' to mean all methods. Leave empty to disable API calls. */ - 'methods' => ['GET'], + 'methods' => ['GET'], /* * Laravel config variables which should be set for the API call. @@ -160,8 +160,8 @@ * and other external services are not triggered * during the documentation API calls */ - 'config' => [ - 'app.env' => 'documentation', + 'config' => [ + 'app.env' => 'documentation', 'app.debug' => false, // 'service.key' => 'value', ], @@ -169,7 +169,7 @@ /* * Cookies which should be sent with the API call. */ - 'cookies' => [ + 'cookies' => [ // 'name' => 'value' ], @@ -183,7 +183,7 @@ /* * Body parameters which should be sent with the API call. */ - 'bodyParams' => [ + 'bodyParams' => [ // 'key' => 'value', ], ], @@ -191,23 +191,26 @@ ], ], - 'strategies' => [ - 'metadata' => [ + 'strategies' => [ + 'metadata' => [ \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, ], - 'urlParameters' => [ + 'urlParameters' => [ \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, ], - 'queryParameters' => [ + 'queryParameters' => [ \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, ], - 'headers' => [ + 'headers' => [ \Mpociot\ApiDoc\Extracting\Strategies\RequestHeaders\GetFromRouteRules::class, ], - 'bodyParameters' => [ + 'bodyParameters' => [ \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], - 'responses' => [ + 'responseParameters' => [ + \Mpociot\ApiDoc\Extracting\Strategies\ResponseParameters\GetFromResponseParamTag::class, + ], + 'responses' => [ \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, @@ -226,12 +229,12 @@ * If you want to use this, please be aware of the following rules: * - the image size must be 230 x 52 */ - 'logo' => false, + 'logo' => false, /* * Name for the group of routes which do not have a @group set. */ - 'default_group' => 'general', + 'default_group' => 'general', /* * Example requests for each endpoint will be shown in each of these languages. @@ -251,7 +254,7 @@ * Requires league/fractal package: composer require league/fractal * */ - 'fractal' => [ + 'fractal' => [ /* If you are using a custom serializer with league/fractal, * you can specify it here. * @@ -270,12 +273,12 @@ * set this to any number (eg. 1234) * */ - 'faker_seed' => null, + 'faker_seed' => null, /* * If you would like to customize how routes are matched beyond the route configuration you may * declare your own implementation of RouteMatcherInterface * */ - 'routeMatcher' => \Mpociot\ApiDoc\Matching\RouteMatcher::class, + 'routeMatcher' => \Mpociot\ApiDoc\Matching\RouteMatcher::class, ]; diff --git a/docs/plugins.md b/docs/plugins.md index 66d44671..12766e47 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -8,6 +8,7 @@ Route processing is performed in six stages: - queryParameters - headers (headers to be added to example request and response calls) - bodyParameters +- responseParameters - responses For each stage, the Generator attempts the specified strategies to fetch data. The Generator will call of the strategies configured, progressively combining their results together before to produce the final output of that stage. diff --git a/src/Extracting/Generator.php b/src/Extracting/Generator.php index 0a5f1df5..7471c124 100644 --- a/src/Extracting/Generator.php +++ b/src/Extracting/Generator.php @@ -81,6 +81,10 @@ public function processRoute(Route $route, array $routeRules = []) $parsedRoute['bodyParameters'] = $bodyParameters; $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters); + $responseParameters = $this->fetchResponseParameters($controller, $method, $route, $routeRules, $parsedRoute); + $parsedRoute['responseParameters'] = $responseParameters; + $parsedRoute['cleanResponseParameters'] = $this->cleanParams($responseParameters); + $responses = $this->fetchResponses($controller, $method, $route, $routeRules, $parsedRoute); $parsedRoute['responses'] = $responses; $parsedRoute['showresponse'] = ! empty($responses); @@ -116,6 +120,11 @@ protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMe return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]); } + protected function fetchResponseParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) + { + return $this->iterateThroughStrategies('responseParameters', $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]); @@ -153,6 +162,9 @@ protected function iterateThroughStrategies(string $stage, array $context, array 'bodyParameters' => [ \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], + 'responseParameters' => [ + \Mpociot\ApiDoc\Extracting\Strategies\ResponseParameters\GetFromResponseParamTag::class, + ], 'responses' => [ \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, diff --git a/src/Extracting/Strategies/ResponseParameters/GetFromResponseParamTag.php b/src/Extracting/Strategies/ResponseParameters/GetFromResponseParamTag.php new file mode 100644 index 00000000..c8a6f85a --- /dev/null +++ b/src/Extracting/Strategies/ResponseParameters/GetFromResponseParamTag.php @@ -0,0 +1,84 @@ +getParameters() as $param) { + $paramType = $param->getType(); + if ($paramType === null) { + continue; + } + + $parameterClassName = version_compare(phpversion(), '7.1.0', '<') + ? $paramType->__toString() + : $paramType->getName(); + + try { + $parameterClass = new ReflectionClass($parameterClassName); + } catch (\ReflectionException $e) { + continue; + } + + if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { + $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); + $bodyParametersFromDocBlock = $this->getResponseParametersFromDocBlock($formRequestDocBlock->getTags()); + + if (count($bodyParametersFromDocBlock)) { + return $bodyParametersFromDocBlock; + } + } + } + + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; + return $this->getResponseParametersFromDocBlock($methodDocBlock->getTags()); + } + + /** + * @param array $tags + * + * @return array + */ + protected function getResponseParametersFromDocBlock(array $tags) + { + $parameters = collect($tags) + ->filter(function ($tag) { + return $tag instanceof Tag && $tag->getName() === 'responseProp'; + }) + ->mapWithKeys(function ($tag) { + preg_match('/(.+?)\s+(.+?)\s+(.*)/', $tag->getContent(), $content); + if (empty($content)) { + // this means only name and type were supplied + list($name, $type) = preg_split('/\s+/', $tag->getContent()); + $description = ''; + } else { + list($_, $name, $type, $description) = $content; + $description = trim($description); + } + + $type = $this->normalizeParameterType($type); + list($description, $example) = $this->parseParamDescription($description, $type); + $value = is_null($example) ? $this->generateDummyValue($type) : $example; + + return [$name => compact('type', 'description', 'value')]; + })->toArray(); + + return $parameters; + } +}