From 6052643b9fa0cf9f39199fe0054cc0953fbffca6 Mon Sep 17 00:00:00 2001 From: Joshua Thijssen Date: Sat, 22 Nov 2014 22:27:13 +0100 Subject: [PATCH] Initial setup on a multi-api documentation --- Annotation/ApiDoc.php | 38 +++++++++ Controller/ApiDocController.php | 4 +- Extractor/ApiDocExtractor.php | 11 ++- Extractor/CachingApiDocExtractor.php | 4 +- Resources/config/routing.yml | 4 +- Resources/doc/index.md | 59 ++++++++++++++ Tests/Annotation/ApiDocTest.php | 1 + Tests/Extractor/ApiDocExtractorTest.php | 78 ++++++++++++++++++- .../Controller/ResourceController.php | 3 + Tests/Fixtures/Controller/TestController.php | 6 +- Tests/Formatter/SimpleFormatterTest.php | 43 ++++++++++ 11 files changed, 239 insertions(+), 12 deletions(-) diff --git a/Annotation/ApiDoc.php b/Annotation/ApiDoc.php index a132a1b..6699f1d 100644 --- a/Annotation/ApiDoc.php +++ b/Annotation/ApiDoc.php @@ -25,6 +25,13 @@ class ApiDoc */ private $requirements = array(); + /** + * Which APIs is this route used. Defaults to "default" + * + * @var array + */ + private $apis = array(); + /** * Filters are optional parameters in the query string. * @@ -191,6 +198,15 @@ class ApiDoc } } + if (isset($data['api'])) { + if (! is_array($data['api'])) { + $data['api'] = array($data['api']); + } + foreach ($data['api'] as $api) { + $this->addApi($api); + } + } + if (isset($data['parameters'])) { foreach ($data['parameters'] as $parameter) { if (!isset($parameter['name'])) { @@ -373,6 +389,23 @@ class ApiDoc return $this->section; } + /** + * @return array + */ + public function addApi($api) + { + $this->apis[] = $api; + } + + /** + * @return array + */ + public function getApis() + { + return $this->apis; + } + + /** * @param string $documentation */ @@ -625,6 +658,11 @@ class ApiDoc $data['requirements'] = $requirements; } + if ($apis = $this->apis) { + $data['apis'] = $apis; + } + + if ($response = $this->response) { $data['response'] = $response; } diff --git a/Controller/ApiDocController.php b/Controller/ApiDocController.php index deebcf2..168a7a6 100644 --- a/Controller/ApiDocController.php +++ b/Controller/ApiDocController.php @@ -19,9 +19,9 @@ use Symfony\Component\HttpFoundation\Response; class ApiDocController extends Controller { - public function indexAction() + public function indexAction($api = "default") { - $extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all(); + $extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all($api); $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 f37e617..ec4a828 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -96,9 +96,9 @@ class ApiDocExtractor * * @return array */ - public function all() + public function all($api = "default") { - return $this->extractAnnotations($this->getRoutes()); + return $this->extractAnnotations($this->getRoutes(), $api); } /** @@ -110,7 +110,7 @@ class ApiDocExtractor * * @return array */ - public function extractAnnotations(array $routes) + public function extractAnnotations(array $routes, $api = "default") { $array = array(); $resources = array(); @@ -123,7 +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)) { + if ($annotation && + ! in_array($annotation->getSection(), $excludeSections) && + ( in_array($api, $annotation->getApis()) || (count($annotation->getApis()) == 0 && $api == "default")) + ) { if ($annotation->isResource()) { if ($resource = $annotation->getResource()) { $resources[] = $resource; diff --git a/Extractor/CachingApiDocExtractor.php b/Extractor/CachingApiDocExtractor.php index 817e684..1c75d52 100644 --- a/Extractor/CachingApiDocExtractor.php +++ b/Extractor/CachingApiDocExtractor.php @@ -49,7 +49,7 @@ class CachingApiDocExtractor extends ApiDocExtractor $this->cache = new ConfigCache($this->cacheFile, $debug); } - public function all() + public function all($api = "default") { if ($this->cache->isFresh() === false) { @@ -65,7 +65,7 @@ class CachingApiDocExtractor extends ApiDocExtractor $resources = array_merge($resources, $this->router->getRouteCollection()->getResources()); - $data = parent::all(); + $data = parent::all($api); $this->cache->write(serialize($data), $resources); return $data; diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 4591631..48b8972 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,5 +1,5 @@ nelmio_api_doc_index: - pattern: / - defaults: { _controller: NelmioApiDocBundle:ApiDoc:index } + pattern: /{api} + defaults: { _controller: NelmioApiDocBundle:ApiDoc:index, api: "default" } requirements: _method: GET diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 9458235..b8d8967 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -180,6 +180,9 @@ 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. + 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. @@ -212,6 +215,56 @@ 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. +### 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. + +An example: +``` + /** + * A resource + * + * @ApiDoc( + * resource=true, + * description="This is a description of your API method", + * api = { "default", "premium" } + * ) + */ + public function getAction() + { + } + + /** + * Another resource + * + * @ApiDoc( + * resource=true, + * description="This is a description of another API method", + * api = { "premium" } + * ) + */ + public function getAnotherAction() + { + } +``` + +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. + +#### Accessing API documentation #### +The normal `default` documentation can be found at the normal location. Other sets of documentation can be found at `documentationurl/`. + +For instance, if your documenation is located at + + http://example.org/doc/api/v1/ + +then the `premium` api will be located at: + + http://example.org/doc/api/v1/premium + + + ### Other Bundle Annotations Also bundle will get information from the other annotations: @@ -453,6 +506,11 @@ You can specify which sections to exclude from the documentation generation: 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. + 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 priorities via container tags. Here's an example parser service registration: @@ -481,6 +539,7 @@ nelmio_api_doc: enabled: true file: "/tmp/symfony-app/%kernel.environment%/api-doc.cache" ``` + ### Using Your Own Annotations If you have developed your own project-related annotations, and you want to parse them to populate diff --git a/Tests/Annotation/ApiDocTest.php b/Tests/Annotation/ApiDocTest.php index 916015b..db5918c 100644 --- a/Tests/Annotation/ApiDocTest.php +++ b/Tests/Annotation/ApiDocTest.php @@ -26,6 +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->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 b12a0d2..d64627f 100644 --- a/Tests/Extractor/ApiDocExtractorTest.php +++ b/Tests/Extractor/ApiDocExtractorTest.php @@ -17,6 +17,10 @@ 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 + public function testAll() { $container = $this->getContainer(); @@ -29,7 +33,7 @@ class ApiDocExtractorTest extends WebTestCase $routesQuantity = 38; $httpsKey = 25; } else { - $routesQuantity = 33; + $routesQuantity = self::ROUTES_QUANTITY_DEFAULT; $httpsKey = 20; } @@ -287,4 +291,76 @@ class ApiDocExtractorTest extends WebTestCase $parameters = $annotation->getParameters(); $this->assertFalse($parameters['required_field']['required']); } + + public function multiDocProvider() + { + 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), + ); + } + + public function testAllMultiDocsForTest() + { + $container = $this->getContainer(); + $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); + set_error_handler(array($this, 'handleDeprecation')); + $data = $extractor->all('test'); + restore_error_handler(); + + $this->assertTrue(is_array($data)); + $this->assertCount(self::ROUTES_QUANTITY_TEST, $data); + + $a1 = $data[0]['annotation']; + $this->assertCount(3, $a1->getApis()); + $this->assertEquals('List resources.', $a1->getDescription()); + + $a2 = $data[1]['annotation']; + $this->assertCount(2, $a2->getApis()); + $this->assertEquals('create another test', $a2->getDescription()); + } + + public function testAllMultiDocsForPremium() + { + $container = $this->getContainer(); + $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); + set_error_handler(array($this, 'handleDeprecation')); + $data = $extractor->all('premium'); + restore_error_handler(); + + $this->assertTrue(is_array($data)); + $this->assertCount(self::ROUTES_QUANTITY_PREMIUM, $data); + + $a1 = $data[0]['annotation']; + $this->assertCount(2, $a1->getApis()); + $this->assertEquals('List another resource.', $a1->getDescription()); + + $a2 = $data[1]['annotation']; + $this->assertCount(3, $a2->getApis()); + $this->assertEquals('List resources.', $a2->getDescription()); + + $a3 = $data[4]['annotation']; + $this->assertCount(2, $a3->getApis()); + $this->assertEquals('create test', $a3->getDescription()); + } + + /** + * @dataProvider multiDocProvider + */ + public function testAllMultiDocs($api, $count) + { + $container = $this->getContainer(); + $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); + set_error_handler(array($this, 'handleDeprecation')); + $data = $extractor->all($api); + 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 6e5bfc4..1f653aa 100644 --- a/Tests/Fixtures/Controller/ResourceController.php +++ b/Tests/Fixtures/Controller/ResourceController.php @@ -17,6 +17,7 @@ class ResourceController /** * @ApiDoc( * resource=true, + * api={ "test", "premium", "default" }, * resourceDescription="Operations on resource.", * description="List resources.", * output="array as tests", @@ -47,6 +48,7 @@ class ResourceController /** * @ApiDoc( * description="Create a new resource.", + * api={ "default", "premium" }, * input={"class" = "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", "name" = ""}, * output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested", * responseMap={ @@ -62,6 +64,7 @@ class ResourceController /** * @ApiDoc( * resource=true, + * api={ "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 5fa7deb..92b41d9 100644 --- a/Tests/Fixtures/Controller/TestController.php +++ b/Tests/Fixtures/Controller/TestController.php @@ -23,7 +23,8 @@ class TestController { /** * @ApiDoc( - * resource="TestResource" + * resource="TestResource", + * api="default" * ) */ public function namedResourceAction() @@ -48,6 +49,7 @@ class TestController /** * @ApiDoc( * description="create test", + * api={ "default", "premium" }, * input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType" * ) */ @@ -58,6 +60,7 @@ class TestController /** * @ApiDoc( * description="post test 2", + * api={ "default", "premium" }, * resource=true * ) */ @@ -109,6 +112,7 @@ class TestController /** * @ApiDoc( + * api= { "default", "test" }, * description="create another test", * input="dependency_type" * ) diff --git a/Tests/Formatter/SimpleFormatterTest.php b/Tests/Formatter/SimpleFormatterTest.php index c5ac64b..f131a0c 100644 --- a/Tests/Formatter/SimpleFormatterTest.php +++ b/Tests/Formatter/SimpleFormatterTest.php @@ -2552,6 +2552,11 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + 'premium', + ), ), 3 => array( @@ -2611,6 +2616,11 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + 'premium', + ), ), ), 'others' => @@ -2647,6 +2657,11 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + 'test', + ), ), 1 => array( @@ -3671,6 +3686,11 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + 'premium', + ), ), ), '/tests2' => @@ -3692,6 +3712,11 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + 'premium', + ), ), ), 'TestResource' => @@ -3704,6 +3729,10 @@ With multiple lines.', 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => + array( + 'default', + ), ), ), '/api/other-resources' => @@ -3727,6 +3756,10 @@ With multiple lines.', 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => array( + 'default', + 'premium', + ), 'response' => array( '' => array( @@ -4049,6 +4082,11 @@ With multiple lines.', 'authenticationRoles' => array(), 'deprecated' => false, + 'apis' => array( + 'test', + 'default', + 'premium', + ), 'response' => array( 'tests' => @@ -4162,6 +4200,11 @@ With multiple lines.', 'description' => '', ), ), + 'apis' => + array( + 'default', + 'premium', + ), 'response' => array( 'foo' =>