Bläddra i källkod

Add authenticated annotation and badge support (closes #345)

shalvah 6 år sedan
förälder
incheckning
b8ad92b9e5

+ 11 - 4
README.md

@@ -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()
@@ -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.

BIN
body-params.png


BIN
body_params.png


+ 33 - 24
resources/views/partials/route.blade.php

@@ -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
@@ -30,14 +39,14 @@ curl -X {{$parsedRoute['methods'][0]}} {{$parsedRoute['methods'][0] == 'GET' ? '
 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
     }
@@ -48,31 +57,31 @@ $.ajax(settings).done(function (response) {
 });
 ```
 
-@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']}} -->

+ 1 - 1
src/Commands/GenerateDocumentation.php

@@ -84,7 +84,7 @@ class GenerateDocumentation extends Command
 
         $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;
             });

+ 18 - 2
src/Generators/AbstractGenerator.php

@@ -65,6 +65,7 @@ abstract class AbstractGenerator
             'methods' => $this->getMethods($route),
             'uri' => $this->getUri($route),
             'parameters' => $this->getParametersFromDocBlock($docBlock['tags']),
+            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
             'response' => $content,
             'showresponse' => ! empty($content),
         ];
@@ -104,7 +105,7 @@ abstract class AbstractGenerator
      *
      * @return array
      */
-    protected function getParametersFromDocBlock($tags)
+    protected function getParametersFromDocBlock(array $tags)
     {
         $parameters = collect($tags)
             ->filter(function ($tag) {
@@ -136,6 +137,20 @@ abstract class AbstractGenerator
         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
@@ -430,6 +445,7 @@ abstract class AbstractGenerator
             },
         ];
 
-        return $fakes[$type]() ?? $fakes['string']();
+        $fake = $fakes[$type] ?? $fakes['string'];
+        return $fake();
     }
 }

+ 13 - 6
tests/Fixtures/TestController.php

@@ -23,18 +23,25 @@ class TestController extends Controller
     }
 
     /**
-     * @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();

+ 3 - 2
tests/GenerateDocumentationTest.php

@@ -155,11 +155,10 @@ class GenerateDocumentationTest extends TestCase
     /** @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([
@@ -173,6 +172,8 @@ class GenerateDocumentationTest extends TestCase
         $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);
     }

+ 12 - 0
tests/Unit/GeneratorTestCase.php

@@ -80,6 +80,18 @@ abstract class GeneratorTestCase extends TestCase
         ], $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()
     {