Skip to content

Commit

Permalink
Merge pull request #416 from HydrefLab/feature/support-multiple-respo…
Browse files Browse the repository at this point in the history
…nse-tags

Support for multiple response tags
  • Loading branch information
shalvah authored Dec 3, 2018
2 parents b5ddfb0 + fb4ba04 commit 0e1418b
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 41 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,24 @@ public function show($id)
}
```

Moreover, you can define multiple `@response` tags as well as the HTTP status code related to a particular response (if no status code set, `200` will be returned):
```php
/**
* @response {
* "id": 4,
* "name": "Jessica Jones",
* "roles": ["admin"]
* }
* @response 404 {
* "message": "No query results for model [\App\User]"
* }
*/
public function show($id)
{
return User::findOrFail($id);
}
```

#### @transformer, @transformerCollection, and @transformerModel
You can define the transformer that is used for the result of the route using the `@transformer` tag (or `@transformerCollection` if the route returns a list). The package will attempt to generate an instance of the model to be transformed using the following steps, stopping at the first successful one:

Expand Down Expand Up @@ -308,7 +326,7 @@ composer require league/fractal

#### @responseFile

For large reponse bodies, you may want to use a dump of an actual response. You can put this response in a file (as a JSON string) within your Laravel storage directory and link to it. For instance, we can put this response in a file named `users.get.json` in `storage/responses`:
For large response bodies, you may want to use a dump of an actual response. You can put this response in a file (as a JSON string) within your Laravel storage directory and link to it. For instance, we can put this response in a file named `users.get.json` in `storage/responses`:

```
{"id":5,"name":"Jessica Jones","gender":"female"}
Expand All @@ -327,6 +345,18 @@ public function getUser(int $id)
```
The package will parse this response and display in the examples for this route.

Similarly to `@response` tag, you can provide multiple `@responseFile` tags along with the HTTP status code of the response:
```php
/**
* @responseFile responses/users.get.json
* @responseFile 404 responses/model.not.found.json
*/
public function getUser(int $id)
{
// ...
}
```

#### Generating responses automatically
If you don't specify an example response using any of the above means, this package will attempt to get a sample response by making a request to the route (a "response call"). A few things to note about response calls:
- They are done within a database transaction and changes are rolled back afterwards.
Expand Down
14 changes: 14 additions & 0 deletions resources/views/partials/route.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@
```

@if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse']))
@if(is_array($route['response']))
@foreach($route['response'] as $response)
> Example response ({{$response['status']}}):

```json
@if(is_object($response['content']) || is_array($response['content']))
{!! json_encode($response['content'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
@else
{!! json_encode(json_decode($response['content']), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
@endif
```
@endforeach
@else
> Example response:

```json
Expand All @@ -79,6 +92,7 @@
@endif
```
@endif
@endif

### HTTP Request
@foreach($route['methods'] as $method)
Expand Down
32 changes: 28 additions & 4 deletions src/Tools/ResponseResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
namespace Mpociot\ApiDoc\Tools;

use Illuminate\Routing\Route;
use Symfony\Component\HttpFoundation\Response;
use Mpociot\ApiDoc\Tools\ResponseStrategies\ResponseTagStrategy;
use Mpociot\ApiDoc\Tools\ResponseStrategies\ResponseCallStrategy;
use Mpociot\ApiDoc\Tools\ResponseStrategies\ResponseFileStrategy;
use Mpociot\ApiDoc\Tools\ResponseStrategies\TransformerTagsStrategy;

class ResponseResolver
{
/**
* @var array
*/
public static $strategies = [
ResponseTagStrategy::class,
TransformerTagsStrategy::class,
Expand All @@ -22,23 +26,43 @@ class ResponseResolver
*/
private $route;

/**
* @param Route $route
*/
public function __construct(Route $route)
{
$this->route = $route;
}

/**
* @param array $tags
* @param array $routeProps
*
* @return array|null
*/
private function resolve(array $tags, array $routeProps)
{
$response = null;
foreach (static::$strategies as $strategy) {
$strategy = new $strategy();
$response = $strategy($this->route, $tags, $routeProps);
if (! is_null($response)) {
return $this->getResponseContent($response);

/** @var Response[]|null $response */
$responses = $strategy($this->route, $tags, $routeProps);

if (! is_null($responses)) {
return array_map(function (Response $response) {
return ['status' => $response->getStatusCode(), 'content' => $this->getResponseContent($response)];
}, $responses);
}
}
}

/**
* @param $route
* @param $tags
* @param $routeProps
*
* @return array
*/
public static function getResponse($route, $tags, $routeProps)
{
return (new static($route))->resolve($tags, $routeProps);
Expand Down
83 changes: 81 additions & 2 deletions src/Tools/ResponseStrategies/ResponseCallStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
*/
class ResponseCallStrategy
{
/**
* @param Route $route
* @param array $tags
* @param array $routeProps
*
* @return array|null
*/
public function __invoke(Route $route, array $tags, array $routeProps)
{
$rulesToApply = $routeProps['rules']['response_calls'] ?? [];
Expand All @@ -21,8 +28,9 @@ public function __invoke(Route $route, array $tags, array $routeProps)

$this->configureEnvironment($rulesToApply);
$request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']);

try {
$response = $this->makeApiCall($request);
$response = [$this->makeApiCall($request)];
} catch (\Exception $e) {
$response = null;
} finally {
Expand All @@ -32,12 +40,25 @@ public function __invoke(Route $route, array $tags, array $routeProps)
return $response;
}

/**
* @param array $rulesToApply
*
* @return void
*/
private function configureEnvironment(array $rulesToApply)
{
$this->startDbTransaction();
$this->setEnvironmentVariables($rulesToApply['env'] ?? []);
}

/**
* @param Route $route
* @param array $rulesToApply
* @param array $bodyParams
* @param array $queryParams
*
* @return Request
*/
private function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
{
$uri = $this->replaceUrlParameterBindings($route, $rulesToApply['bindings'] ?? []);
Expand Down Expand Up @@ -77,6 +98,11 @@ protected function replaceUrlParameterBindings(Route $route, $bindings)
return $uri;
}

/**
* @param array $env
*
* @return void
*/
private function setEnvironmentVariables(array $env)
{
foreach ($env as $name => $value) {
Expand All @@ -87,6 +113,9 @@ private function setEnvironmentVariables(array $env)
}
}

/**
* @return void
*/
private function startDbTransaction()
{
try {
Expand All @@ -95,6 +124,9 @@ private function startDbTransaction()
}
}

/**
* @return void
*/
private function endDbTransaction()
{
try {
Expand All @@ -103,11 +135,19 @@ private function endDbTransaction()
}
}

/**
* @return void
*/
private function finish()
{
$this->endDbTransaction();
}

/**
* @param Request $request
*
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function callDingoRoute(Request $request)
{
/** @var Dispatcher $dispatcher */
Expand Down Expand Up @@ -138,11 +178,23 @@ public function callDingoRoute(Request $request)
return $response;
}

/**
* @param Route $route
*
* @return array
*/
public function getMethods(Route $route)
{
return array_diff($route->methods(), ['HEAD']);
}

/**
* @param Request $request
* @param Route $route
* @param array|null $headers
*
* @return Request
*/
private function addHeaders(Request $request, Route $route, $headers)
{
// set the proper domain
Expand All @@ -162,6 +214,12 @@ private function addHeaders(Request $request, Route $route, $headers)
return $request;
}

/**
* @param Request $request
* @param array $query
*
* @return Request
*/
private function addQueryParameters(Request $request, array $query)
{
$request->query->add($query);
Expand All @@ -170,13 +228,26 @@ private function addQueryParameters(Request $request, array $query)
return $request;
}

/**
* @param Request $request
* @param array $body
*
* @return Request
*/
private function addBodyParameters(Request $request, array $body)
{
$request->request->add($body);

return $request;
}

/**
* @param Request $request
*
* @throws \Exception
*
* @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response
*/
private function makeApiCall(Request $request)
{
if (config('apidoc.router') == 'dingo') {
Expand All @@ -189,7 +260,9 @@ private function makeApiCall(Request $request)
}

/**
* @param $request
* @param Request $request
*
* @throws \Exception
*
* @return \Symfony\Component\HttpFoundation\Response
*/
Expand All @@ -202,6 +275,12 @@ private function callLaravelRoute(Request $request): \Symfony\Component\HttpFoun
return $response;
}

/**
* @param Route $route
* @param array $rulesToApply
*
* @return bool
*/
private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
{
$allowedMethods = $rulesToApply['methods'] ?? [];
Expand Down
27 changes: 20 additions & 7 deletions src/Tools/ResponseStrategies/ResponseFileStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,50 @@
namespace Mpociot\ApiDoc\Tools\ResponseStrategies;

use Illuminate\Routing\Route;
use Illuminate\Http\JsonResponse;
use Mpociot\Reflection\DocBlock\Tag;

/**
* Get a response from from a file in the docblock ( @responseFile ).
*/
class ResponseFileStrategy
{
/**
* @param Route $route
* @param array $tags
* @param array $routeProps
*
* @return array|null
*/
public function __invoke(Route $route, array $tags, array $routeProps)
{
return $this->getFileResponse($tags);
return $this->getFileResponses($tags);
}

/**
* Get the response from the file if available.
*
* @param array $tags
*
* @return mixed
* @return array|null
*/
protected function getFileResponse(array $tags)
protected function getFileResponses(array $tags)
{
$responseFileTags = array_filter($tags, function ($tag) {
return $tag instanceof Tag && strtolower($tag->getName()) == 'responsefile';
return $tag instanceof Tag && strtolower($tag->getName()) === 'responsefile';
});

if (empty($responseFileTags)) {
return;
}
$responseFileTag = array_first($responseFileTags);

$json = json_decode(file_get_contents(storage_path($responseFileTag->getContent()), true), true);
return array_map(function (Tag $responseFileTag) {
preg_match('/^(\d{3})?\s?([\s\S]*)$/', $responseFileTag->getContent(), $result);

$status = $result[1] ?: 200;
$content = $result[2] ? file_get_contents(storage_path($result[2]), true) : '{}';

return response()->json($json);
return new JsonResponse(json_decode($content, true), (int) $status);
}, $responseFileTags);
}
}
Loading

0 comments on commit 0e1418b

Please sign in to comment.