Skip to content

Commit

Permalink
Add authenticated annotation and badge support (closes #345)
Browse files Browse the repository at this point in the history
  • Loading branch information
shalvah committed Oct 13, 2018
1 parent 0d22ab1 commit b8ad92b
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 39 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,19 @@ class ExampleController extends Controller {

![Doc block result](http://headsquaredsoftware.co.uk/images/api_generator_docblock.png)

### Specifying request body parameters
### Specifying request parameters

To specify a list of valid parameters your API route accepts, use the `@bodyParam` annotation. It takes the name of the parameter, its type, an optional "required" label, and then its description
To specify a list of valid parameters your API route accepts, use the `@bodyParam` and `@queryParam` annotations.
- The `@bodyParam` annotation takes the name of the parameter, its type, an optional "required" label, and then its description.
- The `@queryParam` annotation (coming soon!) takes the name of the parameter, an optional "required" label, and then its description


```php
/**
* @bodyParam title string required The title of the post.
* @bodyParam body string required The title of the post.
* @bodyParam type The type of post to create. Defaults to 'textophonious'.
* @bodyParam type string The type of post to create. Defaults to 'textophonious'.
@bodyParam author_id int the ID of the author
* @bodyParam thumbnail image This is required if the post type is 'imagelicious'.
*/
public function createPost()
Expand All @@ -180,7 +183,11 @@ public function createPost()

They will be included in the generated documentation text and example requests.

**Result:** ![](body-params.png)
**Result:**
![](body_params.png)

### Indicating auth status
You can use the `@authenticated` annotation on a method to indicate if the endpoint is authenticated. A "Requires authentication" badge will be added to that route in the generated documentation.

### Providing an example response
You can provide an example response for a route. This will be disaplyed in the examples section. There are several ways of doing this.
Expand Down
Binary file removed body-params.png
Binary file not shown.
Binary file added body_params.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 33 additions & 24 deletions resources/views/partials/route.blade.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
<!-- START_{{$parsedRoute['id']}} -->
@if($parsedRoute['title'] != '')## {{ $parsedRoute['title']}}
@else## {{$parsedRoute['uri']}}
<!-- START_{{$route['id']}} -->
@if($route['title'] != '')## {{ $route['title']}}
@else## {{$route['uri']}}
@endif @if($route['authenticated'])<small style="
padding: 1px 9px 2px;
font-weight: bold;
white-space: nowrap;
color: #ffffff;
-webkit-border-radius: 9px;
-moz-border-radius: 9px;
border-radius: 9px;
background-color: #3a87ad;">Requires authentication</small>
@endif
@if($parsedRoute['description'])
@if($route['description'])

{!! $parsedRoute['description'] !!}
{!! $route['description'] !!}
@endif

> Example request:

```bash
curl -X {{$parsedRoute['methods'][0]}} {{$parsedRoute['methods'][0] == 'GET' ? '-G ' : ''}}"{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($parsedRoute['uri'], '/') }}" \
-H "Accept: application/json"@if(count($parsedRoute['headers'])) \
@foreach($parsedRoute['headers'] as $header => $value)
curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($route['uri'], '/') }}" \
-H "Accept: application/json"@if(count($route['headers'])) \
@foreach($route['headers'] as $header => $value)
-H "{{$header}}: {{$value}}" @if(! ($loop->last))\
@endif
@endforeach
@endif
@if(count($parsedRoute['parameters'])) \
@foreach($parsedRoute['parameters'] as $attribute => $parameter)
@if(count($route['parameters'])) \
@foreach($route['parameters'] as $attribute => $parameter)
-d "{{$attribute}}"={{$parameter['value']}} @if(! ($loop->last))\
@endif
@endforeach
Expand All @@ -30,14 +39,14 @@
var settings = {
"async": true,
"crossDomain": true,
"url": "{{ rtrim(config('app.docs_url') ?: config('app.url'), '/') }}/{{ ltrim($parsedRoute['uri'], '/') }}",
"method": "{{$parsedRoute['methods'][0]}}",
@if(count($parsedRoute['parameters']))
"data": {!! str_replace("\n}","\n }", str_replace(' ',' ',json_encode(array_combine(array_keys($parsedRoute['parameters']), array_map(function($param){ return $param['value']; },$parsedRoute['parameters'])), JSON_PRETTY_PRINT))) !!},
"url": "{{ rtrim(config('app.docs_url') ?: config('app.url'), '/') }}/{{ ltrim($route['uri'], '/') }}",
"method": "{{$route['methods'][0]}}",
@if(count($route['parameters']))
"data": {!! str_replace("\n}","\n }", str_replace(' ',' ',json_encode(array_combine(array_keys($route['parameters']), array_map(function($param){ return $param['value']; },$route['parameters'])), JSON_PRETTY_PRINT))) !!},
@endif
"headers": {
"accept": "application/json",
@foreach($parsedRoute['headers'] as $header => $value)
@foreach($route['headers'] as $header => $value)
"{{$header}}": "{{$value}}",
@endforeach
}
Expand All @@ -48,31 +57,31 @@
});
```

@if(in_array('GET',$parsedRoute['methods']) || (isset($parsedRoute['showresponse']) && $parsedRoute['showresponse']))
@if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse']))
> Example response:

```json
@if(is_object($parsedRoute['response']) || is_array($parsedRoute['response']))
{!! json_encode($parsedRoute['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
@if(is_object($route['response']) || is_array($route['response']))
{!! json_encode($route['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
@else
{!! json_encode(json_decode($parsedRoute['response']), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
{!! json_encode(json_decode($route['response']), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) !!}
@endif
```
@endif

### HTTP Request
@foreach($parsedRoute['methods'] as $method)
`{{$method}} {{$parsedRoute['uri']}}`
@foreach($route['methods'] as $method)
`{{$method}} {{$route['uri']}}`

@endforeach
@if(count($parsedRoute['parameters']))
@if(count($route['parameters']))
#### Parameters

Parameter | Type | Status | Description
--------- | ------- | ------- | ------- | -----------
@foreach($parsedRoute['parameters'] as $attribute => $parameter)
@foreach($route['parameters'] as $attribute => $parameter)
{{$attribute}} | {{$parameter['type']}} | @if($parameter['required']) required @else optional @endif | {!! $parameter['description'] !!}
@endforeach
@endif

<!-- END_{{$parsedRoute['id']}} -->
<!-- END_{{$route['id']}} -->
2 changes: 1 addition & 1 deletion src/Commands/GenerateDocumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private function writeMarkdown($parsedRoutes)

$parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
return $routeGroup->map(function ($route) {
$route['output'] = (string) view('apidoc::partials.route')->with('parsedRoute', $route)->render();
$route['output'] = (string) view('apidoc::partials.route')->with('route', $route)->render();

return $route;
});
Expand Down
20 changes: 18 additions & 2 deletions src/Generators/AbstractGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function processRoute($route)
'methods' => $this->getMethods($route),
'uri' => $this->getUri($route),
'parameters' => $this->getParametersFromDocBlock($docBlock['tags']),
'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
'response' => $content,
'showresponse' => ! empty($content),
];
Expand Down Expand Up @@ -104,7 +105,7 @@ protected function getDocblockResponse($tags)
*
* @return array
*/
protected function getParametersFromDocBlock($tags)
protected function getParametersFromDocBlock(array $tags)
{
$parameters = collect($tags)
->filter(function ($tag) {
Expand Down Expand Up @@ -136,6 +137,20 @@ protected function getParametersFromDocBlock($tags)
return $parameters;
}

/**
* @param array $tags
*
* @return bool
*/
protected function getAuthStatusFromDocBlock(array $tags)
{
$authTag = collect($tags)
->first(function ($tag) {
return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
});
return (bool) $authTag;
}

/**
* @param $route
* @param $bindings
Expand Down Expand Up @@ -430,6 +445,7 @@ private function generateDummyValue(string $type)
},
];

return $fakes[$type]() ?? $fakes['string']();
$fake = $fakes[$type] ?? $fakes['string'];
return $fake();
}
}
19 changes: 13 additions & 6 deletions tests/Fixtures/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,25 @@ public function withEndpointDescription()
}

/**
* @bodyParam user_id int required The id of the user.
* @bodyParam room_id string The id of the room.
* @bodyParam forever boolean Whether to ban the user forever.
* @bodyParam another_one number Just need something here.
* @bodyParam yet_another_param object required
* @bodyParam even_more_param array
* @bodyParam title string required The title of the post.
* @bodyParam body string required The title of the post.
* @bodyParam type string The type of post to create. Defaults to 'textophonious'.
@bodyParam author_id int the ID of the author
* @bodyParam thumbnail image This is required if the post type is 'imagelicious
*/
public function withBodyParameters()
{
return '';
}

/**
* @authenticated
*/
public function withAuthenticatedTag()
{
return '';
}

public function checkCustomHeaders(Request $request)
{
return $request->headers->all();
Expand Down
5 changes: 3 additions & 2 deletions tests/GenerateDocumentationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,10 @@ public function can_parse_partial_resource_routes()
/** @test */
public function generated_markdown_file_is_correct()
{
$this->markTestSkipped('Test is non-deterministic since example values for body parameters are random.');

RouteFacade::get('/api/withDescription', TestController::class.'@withEndpointDescription');
RouteFacade::get('/api/withResponseTag', TestController::class.'@withResponseTag');
RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters');
RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag');

config(['apidoc.routes.0.match.prefixes' => ['api/*']]);
config([
Expand All @@ -173,6 +172,8 @@ public function generated_markdown_file_is_correct()
$generatedMarkdown = __DIR__.'/../public/docs/source/index.md';
$compareMarkdown = __DIR__.'/../public/docs/source/.compare.md';
$fixtureMarkdown = __DIR__.'/Fixtures/index.md';

$this->markTestSkipped('Test is non-deterministic since example values for body parameters are random.');
$this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown);
$this->assertFilesHaveSameContent($fixtureMarkdown, $compareMarkdown);
}
Expand Down
12 changes: 12 additions & 0 deletions tests/Unit/GeneratorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ public function test_can_parse_body_parameters()
], $parameters);
}

/** @test */
public function test_can_parse_auth_tags()
{
$route = $this->createRoute('GET', '/api/test', 'withAuthenticatedTag');
$authenticated = $this->generator->processRoute($route)['authenticated'];
$this->assertTrue($authenticated);

$route = $this->createRoute('GET', '/api/test', 'dummy');
$authenticated = $this->generator->processRoute($route)['authenticated'];
$this->assertFalse($authenticated);
}

/** @test */
public function test_can_parse_route_methods()
{
Expand Down

0 comments on commit b8ad92b

Please sign in to comment.