From c71fa155d5d235d0390cd085b2249f58907cbf84 Mon Sep 17 00:00:00 2001 From: William Durand Date: Sat, 16 May 2015 12:17:59 +0200 Subject: [PATCH] Introduce the concept of 'views' Rewrite #619 --- Annotation/ApiDoc.php | 29 ++++---- Controller/ApiDocController.php | 5 +- Extractor/ApiDocExtractor.php | 14 ++-- Extractor/CachingApiDocExtractor.php | 5 +- Resources/config/routing.yml | 4 +- Resources/doc/index.md | 70 ++++++++++++------- Resources/views/layout.html.twig | 0 Tests/Annotation/ApiDocTest.php | 2 +- Tests/Extractor/ApiDocExtractorTest.php | 57 +++++++-------- .../Controller/ResourceController.php | 6 +- Tests/Fixtures/Controller/TestController.php | 8 +-- Tests/Formatter/SimpleFormatterTest.php | 50 ++++++++++--- 12 files changed, 149 insertions(+), 101 deletions(-) mode change 100755 => 100644 Resources/views/layout.html.twig diff --git a/Annotation/ApiDoc.php b/Annotation/ApiDoc.php index 315ffe2..a84cdd4 100644 --- a/Annotation/ApiDoc.php +++ b/Annotation/ApiDoc.php @@ -18,6 +18,8 @@ use Symfony\Component\Routing\Route; */ class ApiDoc { + const DEFAULT_VIEW = 'default'; + /** * Requirements are mandatory parameters in a route. * @@ -26,11 +28,11 @@ class ApiDoc private $requirements = array(); /** - * Which APIs is this route used. Defaults to "default" + * Which views is this route used. Defaults to "Default" * * @var array */ - private $apis = array(); + private $views = array(); /** * Filters are optional parameters in the query string. @@ -198,12 +200,13 @@ class ApiDoc } } - if (isset($data['api'])) { - if (! is_array($data['api'])) { - $data['api'] = array($data['api']); + if (isset($data['views'])) { + if (! is_array($data['views'])) { + $data['views'] = array($data['views']); } - foreach ($data['api'] as $api) { - $this->addApi($api); + + foreach ($data['views'] as $view) { + $this->addView($view); } } @@ -392,17 +395,17 @@ class ApiDoc /** * @return array */ - public function addApi($api) + public function addView($view) { - $this->apis[] = $api; + $this->views[] = $view; } /** * @return array */ - public function getApis() + public function getViews() { - return $this->apis; + return $this->views; } /** @@ -657,8 +660,8 @@ class ApiDoc $data['requirements'] = $requirements; } - if ($apis = $this->apis) { - $data['apis'] = $apis; + if ($views = $this->views) { + $data['views'] = $views; } if ($response = $this->response) { diff --git a/Controller/ApiDocController.php b/Controller/ApiDocController.php index 168a7a6..f10b28a 100644 --- a/Controller/ApiDocController.php +++ b/Controller/ApiDocController.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Controller; use Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter; +use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,9 +20,9 @@ use Symfony\Component\HttpFoundation\Response; class ApiDocController extends Controller { - public function indexAction($api = "default") + public function indexAction($view = ApiDoc::DEFAULT_VIEW) { - $extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all($api); + $extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all($view); $htmlContent = $this->get('nelmio_api_doc.formatter.html_formatter')->format($extractedDoc); return new Response($htmlContent, 200, array('Content-Type' => 'text/html')); diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index ec4a828..d6c75b4 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -96,9 +96,9 @@ class ApiDocExtractor * * @return array */ - public function all($api = "default") + public function all($view = ApiDoc::DEFAULT_VIEW) { - return $this->extractAnnotations($this->getRoutes(), $api); + return $this->extractAnnotations($this->getRoutes(), $view); } /** @@ -110,7 +110,7 @@ class ApiDocExtractor * * @return array */ - public function extractAnnotations(array $routes, $api = "default") + public function extractAnnotations(array $routes, $view = ApiDoc::DEFAULT_VIEW) { $array = array(); $resources = array(); @@ -123,10 +123,10 @@ class ApiDocExtractor if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) { $annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS); - if ($annotation && - ! in_array($annotation->getSection(), $excludeSections) && - ( in_array($api, $annotation->getApis()) || (count($annotation->getApis()) == 0 && $api == "default")) - ) { + if ( + $annotation && !in_array($annotation->getSection(), $excludeSections) && + (in_array($view, $annotation->getViews()) || (0 === count($annotation->getViews()) && $view === ApiDoc::DEFAULT_VIEW)) + ) { if ($annotation->isResource()) { if ($resource = $annotation->getResource()) { $resources[] = $resource; diff --git a/Extractor/CachingApiDocExtractor.php b/Extractor/CachingApiDocExtractor.php index 1c75d52..a87d38e 100644 --- a/Extractor/CachingApiDocExtractor.php +++ b/Extractor/CachingApiDocExtractor.php @@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Extractor; use Doctrine\Common\Annotations\Reader; use Nelmio\ApiDocBundle\Util\DocCommentExtractor; +use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Resource\FileResource; @@ -49,7 +50,7 @@ class CachingApiDocExtractor extends ApiDocExtractor $this->cache = new ConfigCache($this->cacheFile, $debug); } - public function all($api = "default") + public function all($view = ApiDoc::DEFAULT_VIEW) { if ($this->cache->isFresh() === false) { @@ -65,7 +66,7 @@ class CachingApiDocExtractor extends ApiDocExtractor $resources = array_merge($resources, $this->router->getRouteCollection()->getResources()); - $data = parent::all($api); + $data = parent::all($view); $this->cache->write(serialize($data), $resources); return $data; diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 48b8972..19f4cc3 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,5 +1,5 @@ nelmio_api_doc_index: - pattern: /{api} - defaults: { _controller: NelmioApiDocBundle:ApiDoc:index, api: "default" } + pattern: /{view} + defaults: { _controller: NelmioApiDocBundle:ApiDoc:index, view: 'Default' } requirements: _method: GET diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 8878be3..ba3032e 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -8,7 +8,8 @@ for your APIs. Installation ------------ -Require the `nelmio/api-doc-bundle` package in your composer.json and update your dependencies. +Require the `nelmio/api-doc-bundle` package in your composer.json and update +your dependencies. $ composer require nelmio/api-doc-bundle @@ -26,19 +27,24 @@ public function registerBundles() ``` Import the routing definition in `routing.yml`: + ```yaml # app/config/routing.yml NelmioApiDocBundle: resource: "@NelmioApiDocBundle/Resources/config/routing.yml" prefix: /api/doc ``` + Enable the bundle's configuration in `app/config/config.yml`: + ```yaml # app/config/config.yml nelmio_api_doc: ~ ``` -The **NelmioApiDocBundle** requires Twig as a template engine so do not forget to enable it: +The **NelmioApiDocBundle** requires Twig as a template engine so do not forget +to enable it: + ```yaml # app/config/config.yml framework: @@ -49,8 +55,9 @@ framework: Usage ----- -The main problem with documentation is to keep it up to date. That's why the **NelmioApiDocBundle** -uses introspection a lot. Thanks to an annotation, it's really easy to document an API method. +The main problem with documentation is to keep it up to date. That's why the +**NelmioApiDocBundle** uses introspection a lot. Thanks to an annotation, it's +really easy to document an API method. ### The ApiDoc() Annotation @@ -188,8 +195,8 @@ class YourController } ``` -* `api`: the api under which this resource will be shown. Leave empty to specify the default api. Either a single api, or - an array of apis. +* `views`: the view(s) under which this resource will be shown. Leave empty to + specify the default view. Either a single view, or an array of views. Each _filter_ has to define a `name` parameter, but other parameters are free. Filters are often optional parameters, and you can document them as you want, but keep in mind to be consistent for the whole documentation. @@ -220,13 +227,19 @@ class YourType extends AbstractType } ``` -The bundle will also get information from the routing definition (`requirements`, `pattern`, etc), so to get the -best out of it you should define strict _method requirements etc. +The bundle will also get information from the routing definition +(`requirements`, `pattern`, etc), so to get the best out of it you should +define strict _method requirements etc. -### Multiple API documentations ### -With the `api` tag in the `@apidoc` annotation, it's possible to create different sets of api documentations. Without -the tag, all methods are located in the `default` api and can be found under the normal api documentation url. With the -`api` tag you can specify one or more api names under which the method will be visible. +### Multiple API Documentation ("Views") + +With the `views` tag in the `@ApiDoc` annotation, it is possible to create +different views of your API documentation. Without the tag, all methods are +located in the `Default` view, and can be found under the normal API +documentation url. + +You can specify one or more _view_ names under which the method will be +visible. An example: ``` @@ -236,7 +249,7 @@ An example: * @ApiDoc( * resource=true, * description="This is a description of your API method", - * api = { "default", "premium" } + * views = { "Default", "premium" } * ) */ public function getAction() @@ -249,7 +262,7 @@ An example: * @ApiDoc( * resource=true, * description="This is a description of another API method", - * api = { "premium" } + * views = { "premium" } * ) */ public function getAnotherAction() @@ -257,22 +270,23 @@ An example: } ``` -In this case, only the first resource will be available under the default api documentation, while both methods will -be available under the `premium` api documentation. +In this case, only the first resource will be available under the default view, +while both methods will be available under the `premium` view. -#### Accessing API documentation #### -The normal `default` documentation can be found at the normal location. Other sets of documentation can be found at `documentationurl/`. +#### Accessing Specific API Views -For instance, if your documenation is located at +The `Default` view can be found at the normal location. Other views can be +found at `http://your.documentation/`. + +For instance, if your documentation is located at: http://example.org/doc/api/v1/ -then the `premium` api will be located at: +then the `premium` view will be located at: http://example.org/doc/api/v1/premium - ### Other Bundle Annotations Also bundle will get information from the other annotations: @@ -423,8 +437,8 @@ nelmio_api_doc: default_format: json # default is `json`, # default content format to request (see formats) - - entity_to_choice: false # default is `true`, if `false`, entity collection + + entity_to_choice: false # default is `true`, if `false`, entity collection # will not be mapped as choice ``` ### Command @@ -518,9 +532,11 @@ nelmio_api_doc: exclude_sections: ["privateapi", "testapi"] ``` -Note that `exclude_sections` will literally exclude a section from your api documentation. It's possible however to create -multiple apis by specifying the `api` within the `@apidoc` annotations. This allows you to move private or test methods to a -complete different set of api documentation instead. +Note that `exclude_sections` will literally exclude a section from your api +documentation. It's possible however to create multiple views by specifying the +`views` parameter within the `@ApiDoc` annotations. This allows you to move +private or test methods to a complete different view of your documentation +instead. The bundle provides a way to register multiple `input` parsers. The first parser that can handle the specified input is used, so you can configure their @@ -543,7 +559,7 @@ nelmio_api_doc: template: AcmeApiBundle::Components/motd.html.twig ``` You can define an alternate location where the ApiDoc configurations are to be cached: -```yaml +```yaml # app/config/config.yml nelmio_api_doc: cache: diff --git a/Resources/views/layout.html.twig b/Resources/views/layout.html.twig old mode 100755 new mode 100644 diff --git a/Tests/Annotation/ApiDocTest.php b/Tests/Annotation/ApiDocTest.php index db5918c..551faa5 100644 --- a/Tests/Annotation/ApiDocTest.php +++ b/Tests/Annotation/ApiDocTest.php @@ -26,7 +26,7 @@ class ApiDocTest extends TestCase $this->assertTrue(is_array($array)); $this->assertFalse(isset($array['filters'])); $this->assertFalse($annot->isResource()); - $this->assertEmpty($annot->getApis()); + $this->assertEmpty($annot->getViews()); $this->assertFalse($annot->getDeprecated()); $this->assertFalse(isset($array['description'])); $this->assertFalse(isset($array['requirements'])); diff --git a/Tests/Extractor/ApiDocExtractorTest.php b/Tests/Extractor/ApiDocExtractorTest.php index 1a91eaa..e6eaf2b 100644 --- a/Tests/Extractor/ApiDocExtractorTest.php +++ b/Tests/Extractor/ApiDocExtractorTest.php @@ -17,9 +17,9 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase; class ApiDocExtractorTest extends WebTestCase { - const ROUTES_QUANTITY_DEFAULT = 33; // Routes in the default api - const ROUTES_QUANTITY_PREMIUM = 6; // Routes tagged with premium api - const ROUTES_QUANTITY_TEST = 2; // Routes tagged with test api + private static $ROUTES_QUANTITY_DEFAULT = 38; // Routes in the default view + private static $ROUTES_QUANTITY_PREMIUM = 11; // Routes in the premium view + private static $ROUTES_QUANTITY_TEST = 7; // Routes in the test view public function testAll() { @@ -29,13 +29,8 @@ class ApiDocExtractorTest extends WebTestCase $data = $extractor->all(); restore_error_handler(); - if (class_exists('Dunglas\ApiBundle\DunglasApiBundle')) { - $routesQuantity = 38; - $httpsKey = 25; - } else { - $routesQuantity = self::ROUTES_QUANTITY_DEFAULT; - $httpsKey = 20; - } + $routesQuantity = self::$ROUTES_QUANTITY_DEFAULT; + $httpsKey = 25; $this->assertTrue(is_array($data)); $this->assertCount($routesQuantity, $data); @@ -292,19 +287,20 @@ class ApiDocExtractorTest extends WebTestCase $this->assertFalse($parameters['required_field']['required']); } - public function multiDocProvider() + public static function viewsProvider() { return array( - array('default', self::ROUTES_QUANTITY_DEFAULT), - array('premium', self::ROUTES_QUANTITY_PREMIUM), - array('test', self::ROUTES_QUANTITY_TEST), - array('foobar', 0), - array("", 0), - array(null, 0), + array('default', self::$ROUTES_QUANTITY_DEFAULT), + array('premium', self::$ROUTES_QUANTITY_PREMIUM), + array('test', self::$ROUTES_QUANTITY_TEST), + // 5 = DunglasApiBundle + array('foobar', 5), + array("", 5), + array(null, 5), ); } - public function testAllMultiDocsForTest() + public function testViewNamedTest() { $container = $this->getContainer(); $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); @@ -313,18 +309,18 @@ class ApiDocExtractorTest extends WebTestCase restore_error_handler(); $this->assertTrue(is_array($data)); - $this->assertCount(self::ROUTES_QUANTITY_TEST, $data); + $this->assertCount(self::$ROUTES_QUANTITY_TEST, $data); $a1 = $data[0]['annotation']; - $this->assertCount(3, $a1->getApis()); + $this->assertCount(3, $a1->getViews()); $this->assertEquals('List resources.', $a1->getDescription()); $a2 = $data[1]['annotation']; - $this->assertCount(2, $a2->getApis()); + $this->assertCount(2, $a2->getViews()); $this->assertEquals('create another test', $a2->getDescription()); } - public function testAllMultiDocsForPremium() + public function testViewNamedPremium() { $container = $this->getContainer(); $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); @@ -333,34 +329,33 @@ class ApiDocExtractorTest extends WebTestCase restore_error_handler(); $this->assertTrue(is_array($data)); - $this->assertCount(self::ROUTES_QUANTITY_PREMIUM, $data); + $this->assertCount(self::$ROUTES_QUANTITY_PREMIUM, $data); $a1 = $data[0]['annotation']; - $this->assertCount(2, $a1->getApis()); + $this->assertCount(2, $a1->getViews()); $this->assertEquals('List another resource.', $a1->getDescription()); $a2 = $data[1]['annotation']; - $this->assertCount(3, $a2->getApis()); + $this->assertCount(3, $a2->getViews()); $this->assertEquals('List resources.', $a2->getDescription()); - $a3 = $data[4]['annotation']; - $this->assertCount(2, $a3->getApis()); + $a3 = $data[9]['annotation']; + $this->assertCount(2, $a3->getViews()); $this->assertEquals('create test', $a3->getDescription()); } /** - * @dataProvider multiDocProvider + * @dataProvider viewsProvider */ - public function testAllMultiDocs($api, $count) + public function testForViews($view, $count) { $container = $this->getContainer(); $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); set_error_handler(array($this, 'handleDeprecation')); - $data = $extractor->all($api); + $data = $extractor->all($view); restore_error_handler(); $this->assertTrue(is_array($data)); $this->assertCount($count, $data); } - } diff --git a/Tests/Fixtures/Controller/ResourceController.php b/Tests/Fixtures/Controller/ResourceController.php index 1f653aa..9a25a00 100644 --- a/Tests/Fixtures/Controller/ResourceController.php +++ b/Tests/Fixtures/Controller/ResourceController.php @@ -17,7 +17,7 @@ class ResourceController /** * @ApiDoc( * resource=true, - * api={ "test", "premium", "default" }, + * views={ "test", "premium", "default" }, * resourceDescription="Operations on resource.", * description="List resources.", * output="array as tests", @@ -48,7 +48,7 @@ class ResourceController /** * @ApiDoc( * description="Create a new resource.", - * api={ "default", "premium" }, + * views={ "default", "premium" }, * input={"class" = "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", "name" = ""}, * output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested", * responseMap={ @@ -64,7 +64,7 @@ class ResourceController /** * @ApiDoc( * resource=true, - * api={ "default", "premium" }, + * views={ "default", "premium" }, * description="List another resource.", * resourceDescription="Operations on another resource.", * output="array" diff --git a/Tests/Fixtures/Controller/TestController.php b/Tests/Fixtures/Controller/TestController.php index 92b41d9..516d204 100644 --- a/Tests/Fixtures/Controller/TestController.php +++ b/Tests/Fixtures/Controller/TestController.php @@ -24,7 +24,7 @@ class TestController /** * @ApiDoc( * resource="TestResource", - * api="default" + * views="default" * ) */ public function namedResourceAction() @@ -49,7 +49,7 @@ class TestController /** * @ApiDoc( * description="create test", - * api={ "default", "premium" }, + * views={ "default", "premium" }, * input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType" * ) */ @@ -60,7 +60,7 @@ class TestController /** * @ApiDoc( * description="post test 2", - * api={ "default", "premium" }, + * views={ "default", "premium" }, * resource=true * ) */ @@ -112,7 +112,7 @@ class TestController /** * @ApiDoc( - * api= { "default", "test" }, + * views= { "default", "test" }, * description="create another test", * input="dependency_type" * ) diff --git a/Tests/Formatter/SimpleFormatterTest.php b/Tests/Formatter/SimpleFormatterTest.php index a8e2496..9803bee 100644 --- a/Tests/Formatter/SimpleFormatterTest.php +++ b/Tests/Formatter/SimpleFormatterTest.php @@ -315,6 +315,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'premium', + ), ), 1 => array ( @@ -413,6 +417,11 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'test', + 'premium', + 'default', + ), ), 1 => array ( @@ -850,6 +859,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'premium', + ), ), 2 => array ( @@ -1046,6 +1059,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'premium', + ), ), 3 => array ( @@ -1111,6 +1128,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'premium', + ), ), ), '/tests2' => @@ -1135,6 +1156,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'premium', + ), ), ), 'TestResource' => @@ -1149,6 +1174,9 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + ), ), ), 'others' => @@ -1190,6 +1218,10 @@ With multiple lines.', array ( ), 'deprecated' => false, + 'views' => array( + 'default', + 'test', + ), ), 1 => array ( @@ -2557,7 +2589,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', 'premium', @@ -2621,7 +2653,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', 'premium', @@ -2662,7 +2694,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', 'test', @@ -3691,7 +3723,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', 'premium', @@ -3717,7 +3749,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', 'premium', @@ -3734,7 +3766,7 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => + 'views' => array( 'default', ), @@ -3761,7 +3793,7 @@ With multiple lines.', 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => array( + 'views' => array( 'default', 'premium', ), @@ -4087,7 +4119,7 @@ With multiple lines.', 'authenticationRoles' => array(), 'deprecated' => false, - 'apis' => array( + 'views' => array( 'test', 'default', 'premium', @@ -4205,7 +4237,7 @@ With multiple lines.', 'description' => '', ), ), - 'apis' => + 'views' => array( 'default', 'premium',