Initial setup on a multi-api documentation

This commit is contained in:
Joshua Thijssen 2014-11-22 22:27:13 +01:00 committed by William Durand
parent f7611613af
commit 6052643b9f
No known key found for this signature in database
GPG Key ID: A509BCF1C1274F3B
11 changed files with 239 additions and 12 deletions

View File

@ -25,6 +25,13 @@ class ApiDoc
*/ */
private $requirements = array(); 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. * 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'])) { if (isset($data['parameters'])) {
foreach ($data['parameters'] as $parameter) { foreach ($data['parameters'] as $parameter) {
if (!isset($parameter['name'])) { if (!isset($parameter['name'])) {
@ -373,6 +389,23 @@ class ApiDoc
return $this->section; return $this->section;
} }
/**
* @return array
*/
public function addApi($api)
{
$this->apis[] = $api;
}
/**
* @return array
*/
public function getApis()
{
return $this->apis;
}
/** /**
* @param string $documentation * @param string $documentation
*/ */
@ -625,6 +658,11 @@ class ApiDoc
$data['requirements'] = $requirements; $data['requirements'] = $requirements;
} }
if ($apis = $this->apis) {
$data['apis'] = $apis;
}
if ($response = $this->response) { if ($response = $this->response) {
$data['response'] = $response; $data['response'] = $response;
} }

View File

@ -19,9 +19,9 @@ use Symfony\Component\HttpFoundation\Response;
class ApiDocController extends Controller 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); $htmlContent = $this->get('nelmio_api_doc.formatter.html_formatter')->format($extractedDoc);
return new Response($htmlContent, 200, array('Content-Type' => 'text/html')); return new Response($htmlContent, 200, array('Content-Type' => 'text/html'));

View File

@ -96,9 +96,9 @@ class ApiDocExtractor
* *
* @return array * @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 * @return array
*/ */
public function extractAnnotations(array $routes) public function extractAnnotations(array $routes, $api = "default")
{ {
$array = array(); $array = array();
$resources = array(); $resources = array();
@ -123,7 +123,10 @@ class ApiDocExtractor
if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) { if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) {
$annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS); $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 ($annotation->isResource()) {
if ($resource = $annotation->getResource()) { if ($resource = $annotation->getResource()) {
$resources[] = $resource; $resources[] = $resource;

View File

@ -49,7 +49,7 @@ class CachingApiDocExtractor extends ApiDocExtractor
$this->cache = new ConfigCache($this->cacheFile, $debug); $this->cache = new ConfigCache($this->cacheFile, $debug);
} }
public function all() public function all($api = "default")
{ {
if ($this->cache->isFresh() === false) { if ($this->cache->isFresh() === false) {
@ -65,7 +65,7 @@ class CachingApiDocExtractor extends ApiDocExtractor
$resources = array_merge($resources, $this->router->getRouteCollection()->getResources()); $resources = array_merge($resources, $this->router->getRouteCollection()->getResources());
$data = parent::all(); $data = parent::all($api);
$this->cache->write(serialize($data), $resources); $this->cache->write(serialize($data), $resources);
return $data; return $data;

View File

@ -1,5 +1,5 @@
nelmio_api_doc_index: nelmio_api_doc_index:
pattern: / pattern: /{api}
defaults: { _controller: NelmioApiDocBundle:ApiDoc:index } defaults: { _controller: NelmioApiDocBundle:ApiDoc:index, api: "default" }
requirements: requirements:
_method: GET _method: GET

View File

@ -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 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. 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 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. 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/<tagname>`.
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 ### Other Bundle Annotations
Also bundle will get information from the other 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: nelmio_api_doc:
exclude_sections: ["privateapi", "testapi"] 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 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 that can handle the specified input is used, so you can configure their
priorities via container tags. Here's an example parser service registration: priorities via container tags. Here's an example parser service registration:
@ -481,6 +539,7 @@ nelmio_api_doc:
enabled: true enabled: true
file: "/tmp/symfony-app/%kernel.environment%/api-doc.cache" file: "/tmp/symfony-app/%kernel.environment%/api-doc.cache"
``` ```
### Using Your Own Annotations ### Using Your Own Annotations
If you have developed your own project-related annotations, and you want to parse them to populate If you have developed your own project-related annotations, and you want to parse them to populate

View File

@ -26,6 +26,7 @@ class ApiDocTest extends TestCase
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertEmpty($annot->getApis());
$this->assertFalse($annot->getDeprecated()); $this->assertFalse($annot->getDeprecated());
$this->assertFalse(isset($array['description'])); $this->assertFalse(isset($array['description']));
$this->assertFalse(isset($array['requirements'])); $this->assertFalse(isset($array['requirements']));

View File

@ -17,6 +17,10 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase;
class ApiDocExtractorTest extends 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() public function testAll()
{ {
$container = $this->getContainer(); $container = $this->getContainer();
@ -29,7 +33,7 @@ class ApiDocExtractorTest extends WebTestCase
$routesQuantity = 38; $routesQuantity = 38;
$httpsKey = 25; $httpsKey = 25;
} else { } else {
$routesQuantity = 33; $routesQuantity = self::ROUTES_QUANTITY_DEFAULT;
$httpsKey = 20; $httpsKey = 20;
} }
@ -287,4 +291,76 @@ class ApiDocExtractorTest extends WebTestCase
$parameters = $annotation->getParameters(); $parameters = $annotation->getParameters();
$this->assertFalse($parameters['required_field']['required']); $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);
}
} }

View File

@ -17,6 +17,7 @@ class ResourceController
/** /**
* @ApiDoc( * @ApiDoc(
* resource=true, * resource=true,
* api={ "test", "premium", "default" },
* resourceDescription="Operations on resource.", * resourceDescription="Operations on resource.",
* description="List resources.", * description="List resources.",
* output="array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test> as tests", * output="array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test> as tests",
@ -47,6 +48,7 @@ class ResourceController
/** /**
* @ApiDoc( * @ApiDoc(
* description="Create a new resource.", * description="Create a new resource.",
* api={ "default", "premium" },
* input={"class" = "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", "name" = ""}, * input={"class" = "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", "name" = ""},
* output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested", * output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested",
* responseMap={ * responseMap={
@ -62,6 +64,7 @@ class ResourceController
/** /**
* @ApiDoc( * @ApiDoc(
* resource=true, * resource=true,
* api={ "default", "premium" },
* description="List another resource.", * description="List another resource.",
* resourceDescription="Operations on another resource.", * resourceDescription="Operations on another resource.",
* output="array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest>" * output="array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest>"

View File

@ -23,7 +23,8 @@ class TestController
{ {
/** /**
* @ApiDoc( * @ApiDoc(
* resource="TestResource" * resource="TestResource",
* api="default"
* ) * )
*/ */
public function namedResourceAction() public function namedResourceAction()
@ -48,6 +49,7 @@ class TestController
/** /**
* @ApiDoc( * @ApiDoc(
* description="create test", * description="create test",
* api={ "default", "premium" },
* input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType" * input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType"
* ) * )
*/ */
@ -58,6 +60,7 @@ class TestController
/** /**
* @ApiDoc( * @ApiDoc(
* description="post test 2", * description="post test 2",
* api={ "default", "premium" },
* resource=true * resource=true
* ) * )
*/ */
@ -109,6 +112,7 @@ class TestController
/** /**
* @ApiDoc( * @ApiDoc(
* api= { "default", "test" },
* description="create another test", * description="create another test",
* input="dependency_type" * input="dependency_type"
* ) * )

View File

@ -2552,6 +2552,11 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
'premium',
),
), ),
3 => 3 =>
array( array(
@ -2611,6 +2616,11 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
'premium',
),
), ),
), ),
'others' => 'others' =>
@ -2647,6 +2657,11 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
'test',
),
), ),
1 => 1 =>
array( array(
@ -3671,6 +3686,11 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
'premium',
),
), ),
), ),
'/tests2' => '/tests2' =>
@ -3692,6 +3712,11 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
'premium',
),
), ),
), ),
'TestResource' => 'TestResource' =>
@ -3704,6 +3729,10 @@ With multiple lines.',
'authentication' => false, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
'apis' =>
array(
'default',
),
), ),
), ),
'/api/other-resources' => '/api/other-resources' =>
@ -3727,6 +3756,10 @@ With multiple lines.',
'authenticationRoles' => 'authenticationRoles' =>
array(), array(),
'deprecated' => false, 'deprecated' => false,
'apis' => array(
'default',
'premium',
),
'response' => array( 'response' => array(
'' => '' =>
array( array(
@ -4049,6 +4082,11 @@ With multiple lines.',
'authenticationRoles' => 'authenticationRoles' =>
array(), array(),
'deprecated' => false, 'deprecated' => false,
'apis' => array(
'test',
'default',
'premium',
),
'response' => 'response' =>
array( array(
'tests' => 'tests' =>
@ -4162,6 +4200,11 @@ With multiple lines.',
'description' => '', 'description' => '',
), ),
), ),
'apis' =>
array(
'default',
'premium',
),
'response' => 'response' =>
array( array(
'foo' => 'foo' =>