Merge pull request #53 from evillemez/parser_interface

Implemented `ParserInterface`
This commit is contained in:
William Durand 2012-08-06 03:12:17 -07:00
commit 4b69e1149a
14 changed files with 181 additions and 51 deletions

View File

@ -26,7 +26,7 @@ class ApiDoc
/** /**
* @var string * @var string
*/ */
private $formType = null; private $input = null;
/** /**
* @var string * @var string
@ -70,8 +70,8 @@ class ApiDoc
public function __construct(array $data) public function __construct(array $data)
{ {
if (isset($data['formType'])) { if (isset($data['input'])) {
$this->formType = $data['formType']; $this->input = $data['input'];
} elseif (isset($data['filters'])) { } elseif (isset($data['filters'])) {
foreach ($data['filters'] as $filter) { foreach ($data['filters'] as $filter) {
if (!isset($filter['name'])) { if (!isset($filter['name'])) {
@ -121,9 +121,9 @@ class ApiDoc
/** /**
* @return string|null * @return string|null
*/ */
public function getFormType() public function getInput()
{ {
return $this->formType; return $this->input;
} }
/** /**

View File

@ -0,0 +1,39 @@
<?php
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class RegisterExtractorParsersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition('nelmio_api_doc.extractor.api_doc_extractor')) {
return;
}
$definition = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor');
//find registered parsers and sort by priority
$sortedParsers = array();
foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.parser') as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$sortedParsers[$priority][] = $id;
}
}
//add parsers if any
if (!empty($sortedParsers)) {
krsort($sortedParsers);
$sortedParsers = call_user_func_array('array_merge', $sortedParsers);
//add method call for each registered parsers
foreach ($sortedParsers as $id) {
$definition->addMethodCall('addParser', array(new Reference($id)));
}
}
}
}

View File

@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Extractor;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Parser\FormTypeParser; use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
@ -41,16 +41,15 @@ class ApiDocExtractor
private $reader; private $reader;
/** /**
* @var \Nelmio\ApiDocBundle\Parser\FormTypeParser * @var array \Nelmio\ApiDocBundle\Parser\ParserInterface
*/ */
private $parser; private $parsers = array();
public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader, FormTypeParser $parser) public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader)
{ {
$this->container = $container; $this->container = $container;
$this->router = $router; $this->router = $router;
$this->reader = $reader; $this->reader = $reader;
$this->parser = $parser;
} }
/** /**
@ -122,8 +121,6 @@ class ApiDocExtractor
return strcmp($a['resource'], $b['resource']); return strcmp($a['resource'], $b['resource']);
}); });
return $array; return $array;
} }
@ -179,6 +176,16 @@ class ApiDocExtractor
return null; return null;
} }
/**
* Registers a class parser to use for parsing input class metadata
*
* @param ParserInterface $parser
*/
public function addParser(ParserInterface $parser)
{
$this->parsers[] = $parser;
}
/** /**
* Returns a new ApiDoc instance with more data. * Returns a new ApiDoc instance with more data.
* *
@ -215,9 +222,15 @@ class ApiDocExtractor
// doc // doc
$annotation->setDocumentation($this->getDocCommentText($method)); $annotation->setDocumentation($this->getDocCommentText($method));
// formType // input
if (null !== $formType = $annotation->getFormType()) { if (null !== $input = $annotation->getInput()) {
$parameters = $this->parser->parse($formType); $parameters = array();
foreach ($this->parsers as $parser) {
if ($parser->supports($input)) {
$parameters = $parser->parse($input);
}
}
if ('PUT' === $method) { if ('PUT' === $method) {
// All parameters are optional with PUT (update) // All parameters are optional with PUT (update)
@ -280,7 +293,7 @@ class ApiDocExtractor
} }
/** /**
* @param Reflector $reflected * @param Reflector $reflected
* @return string * @return string
*/ */
protected function getDocComment(\Reflector $reflected) protected function getDocComment(\Reflector $reflected)
@ -300,7 +313,7 @@ class ApiDocExtractor
} }
/** /**
* @param Reflector $reflected * @param Reflector $reflected
* @return string * @return string
*/ */
protected function getDocCommentText(\Reflector $reflected) protected function getDocCommentText(\Reflector $reflected)

View File

@ -26,7 +26,7 @@ interface FormatterInterface
/** /**
* Format documentation data for one route. * Format documentation data for one route.
* *
* @param ApiDoc $annotation * @param ApiDoc $annotation
* return string|array * return string|array
*/ */
public function formatOne(ApiDoc $annotation); public function formatOne(ApiDoc $annotation);

View File

@ -3,7 +3,15 @@
namespace Nelmio\ApiDocBundle; namespace Nelmio\ApiDocBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Nelmio\ApiDocBundle\DependencyInjection\RegisterExtractorParsersPass;
class NelmioApiDocBundle extends Bundle class NelmioApiDocBundle extends Bundle
{ {
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new RegisterExtractorParsersPass());
}
} }

View File

@ -11,10 +11,10 @@
namespace Nelmio\ApiDocBundle\Parser; namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\Exception\FormException;
class FormTypeParser class FormTypeParser implements ParserInterface
{ {
/** /**
* @var \Symfony\Component\Form\FormFactoryInterface * @var \Symfony\Component\Form\FormFactoryInterface
@ -42,13 +42,25 @@ class FormTypeParser
} }
/** /**
* Returns an array of data where each data is an array with the following keys: * {@inheritdoc}
* - dataType */
* - required public function supports($item)
* - description {
* try {
* @param string|\Symfony\Component\Form\FormTypeInterface $type if (is_string($item) && class_exists($item)) {
* @return array $item = new $item();
}
$form = $this->formFactory->create($item);
} catch (FormException $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/ */
public function parse($type) public function parse($type)
{ {
@ -93,6 +105,7 @@ class FormTypeParser
'dataType' => $bestType, 'dataType' => $bestType,
'required' => $config->getRequired(), 'required' => $config->getRequired(),
'description' => $config->getAttribute('description'), 'description' => $config->getAttribute('description'),
'readonly' => $config->getDisabled(),
); );
} }

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the NelmioApiDocBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Parser;
/**
* This is the interface parsers must implement in order to be registered in the ApiDocExtractor.
*/
interface ParserInterface
{
/**
* Return true/false whether this class supports parsing the given class.
*
* @param string $item The string type of input to parse.
* @return boolean
*/
public function supports($item);
/**
* Returns an array of class property metadata where each item is a key (the property name) and
* an array of data with the following keys:
* - dataType string
* - required boolean
* - description string
* - readonly boolean
*
* @param string $item The string type of input to parse.
* @return array
*/
public function parse($item);
}

View File

@ -82,7 +82,7 @@ class YourController extends Controller
/** /**
* @ApiDoc( * @ApiDoc(
* description="Create a new Object", * description="Create a new Object",
* formType="Your\Namespace\Form\Type\YourType" * input="Your\Namespace\Form\Type\YourType"
* ) * )
*/ */
public function postAction() public function postAction()
@ -99,16 +99,16 @@ The following properties are available:
* `filters`: an array of filters; * `filters`: an array of filters;
* `formType`: the Form Type associated to the method, useful for POST|PUT methods, either as FQCN or * `input`: the input type associated to the method, currently this only supports Form Types, useful for POST|PUT methods, either as FQCN or
as form type (if it is registered in the form factory in the container) as form type (if it is registered in the form factory in the container)
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.
If you set a `formType`, then the bundle automatically extracts parameters based on the given type, If you set `input`, then the bundle automatically extracts parameters based on the given type,
and determines for each parameter its data type, and if it's required or not. and determines for each parameter its data type, and if it's required or not.
You can add an extra option named `description` on each field: For Form Types, you can add an extra option named `description` on each field:
``` php ``` php
<?php <?php
@ -181,6 +181,16 @@ configure this sandbox using the following parameters:
endpoint: http://sandbox.example.com/ # default: /app_dev.php, use this parameter to define which URL to call through the sandbox endpoint: http://sandbox.example.com/ # default: /app_dev.php, use this parameter to define which URL to call through the sandbox
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:
#app/config/config.yml
services:
mybundle.api_doc.extractor.custom_parser:
class: MyBundle\Parser\CustomDocParser;
tags:
- {name: nelmio_api_doc.extractor.parser, priority: 2}
## Credits ## ## Credits ##
The design is heavily inspired by the [swagger-ui](https://github.com/wordnik/swagger-ui) project. The design is heavily inspired by the [swagger-ui](https://github.com/wordnik/swagger-ui) project.

View File

@ -14,6 +14,7 @@
<services> <services>
<service id="nelmio_api_doc.parser.form_type_parser" class="%nelmio_api_doc.parser.form_type_parser.class%"> <service id="nelmio_api_doc.parser.form_type_parser" class="%nelmio_api_doc.parser.form_type_parser.class%">
<argument type="service" id="form.factory" /> <argument type="service" id="form.factory" />
<tag name="nelmio_api_doc.extractor.parser" />
</service> </service>
<service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" /> <service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" />
<service id="nelmio_api_doc.formatter.markdown_formatter" class="%nelmio_api_doc.formatter.markdown_formatter.class%" <service id="nelmio_api_doc.formatter.markdown_formatter" class="%nelmio_api_doc.formatter.markdown_formatter.class%"

View File

@ -14,7 +14,6 @@
<argument type="service" id="service_container"/> <argument type="service" id="service_container"/>
<argument type="service" id="router" /> <argument type="service" id="router" />
<argument type="service" id="annotation_reader" /> <argument type="service" id="annotation_reader" />
<argument type="service" id="nelmio_api_doc.parser.form_type_parser" />
</service> </service>
<service id="nelmio_api_doc.form.extension.description_form_type_extension" class="%nelmio_api_doc.form.extension.description_form_type_extension.class%"> <service id="nelmio_api_doc.form.extension.description_form_type_extension" class="%nelmio_api_doc.form.extension.description_form_type_extension.class%">

View File

@ -27,7 +27,7 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertFalse(isset($array['description'])); $this->assertFalse(isset($array['description']));
$this->assertNull($annot->getFormType()); $this->assertNull($annot->getInput());
} }
public function testConstructWithInvalidData() public function testConstructWithInvalidData()
@ -44,7 +44,7 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertFalse(isset($array['description'])); $this->assertFalse(isset($array['description']));
$this->assertNull($annot->getFormType()); $this->assertNull($annot->getInput());
} }
public function testConstruct() public function testConstruct()
@ -60,14 +60,14 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertNull($annot->getFormType()); $this->assertNull($annot->getInput());
} }
public function testConstructDefinesAFormType() public function testConstructDefinesAFormType()
{ {
$data = array( $data = array(
'description' => 'Heya', 'description' => 'Heya',
'formType' => 'My\Form\Type', 'input' => 'My\Form\Type',
); );
$annot = new ApiDoc($data); $annot = new ApiDoc($data);
@ -77,7 +77,7 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['formType'], $annot->getFormType()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodIsResource() public function testConstructMethodIsResource()
@ -85,7 +85,7 @@ class ApiDocTest extends TestCase
$data = array( $data = array(
'resource' => true, 'resource' => true,
'description' => 'Heya', 'description' => 'Heya',
'formType' => 'My\Form\Type', 'input' => 'My\Form\Type',
); );
$annot = new ApiDoc($data); $annot = new ApiDoc($data);
@ -95,7 +95,7 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertTrue($annot->isResource()); $this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['formType'], $annot->getFormType()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodResourceIsFalse() public function testConstructMethodResourceIsFalse()
@ -103,7 +103,7 @@ class ApiDocTest extends TestCase
$data = array( $data = array(
'resource' => false, 'resource' => false,
'description' => 'Heya', 'description' => 'Heya',
'formType' => 'My\Form\Type', 'input' => 'My\Form\Type',
); );
$annot = new ApiDoc($data); $annot = new ApiDoc($data);
@ -113,7 +113,7 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['formType'], $annot->getFormType()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodHasFilters() public function testConstructMethodHasFilters()
@ -135,7 +135,7 @@ class ApiDocTest extends TestCase
$this->assertEquals(array('a-filter' => array()), $array['filters']); $this->assertEquals(array('a-filter' => array()), $array['filters']);
$this->assertTrue($annot->isResource()); $this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertNull($annot->getFormType()); $this->assertNull($annot->getInput());
} }
/** /**
@ -158,7 +158,7 @@ class ApiDocTest extends TestCase
$data = array( $data = array(
'resource' => true, 'resource' => true,
'description' => 'Heya', 'description' => 'Heya',
'formType' => 'My\Form\Type', 'input' => 'My\Form\Type',
'filters' => array( 'filters' => array(
array('name' => 'a-filter'), array('name' => 'a-filter'),
), ),
@ -171,6 +171,6 @@ class ApiDocTest extends TestCase
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertTrue($annot->isResource()); $this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['formType'], $annot->getFormType()); $this->assertEquals($data['input'], $annot->getInput());
} }
} }

View File

@ -39,28 +39,28 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertTrue($a1->isResource()); $this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription()); $this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters'])); $this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getFormType()); $this->assertNull($a1->getInput());
$a1 = $data[1]['annotation']; $a1 = $data[1]['annotation'];
$array1 = $a1->toArray(); $array1 = $a1->toArray();
$this->assertTrue($a1->isResource()); $this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription()); $this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters'])); $this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getFormType()); $this->assertNull($a1->getInput());
$a2 = $data[2]['annotation']; $a2 = $data[2]['annotation'];
$array2 = $a2->toArray(); $array2 = $a2->toArray();
$this->assertFalse($a2->isResource()); $this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription()); $this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters'])); $this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getFormType()); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a2 = $data[3]['annotation']; $a2 = $data[3]['annotation'];
$array2 = $a2->toArray(); $array2 = $a2->toArray();
$this->assertFalse($a2->isResource()); $this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription()); $this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters'])); $this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getFormType()); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
} }
public function testGet() public function testGet()
@ -76,7 +76,7 @@ class ApiDocExtractorTest extends WebTestCase
$array = $annotation->toArray(); $array = $annotation->toArray();
$this->assertTrue(is_array($array['filters'])); $this->assertTrue(is_array($array['filters']));
$this->assertNull($annotation->getFormType()); $this->assertNull($annotation->getInput());
$annotation2 = $extractor->get('nemlio.test.controller:indexAction', 'test_service_route_1'); $annotation2 = $extractor->get('nemlio.test.controller:indexAction', 'test_service_route_1');
$annotation2->getRoute() $annotation2->getRoute()

View File

@ -35,7 +35,7 @@ class TestController
/** /**
* @ApiDoc( * @ApiDoc(
* description="create test", * description="create test",
* formType="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType" * input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType"
* ) * )
*/ */
public function postTestAction() public function postTestAction()
@ -76,7 +76,7 @@ class TestController
/** /**
* @ApiDoc( * @ApiDoc(
* description="create another test", * description="create another test",
* formType="dependency_type" * input="dependency_type"
* ) * )
*/ */
public function anotherPostAction() public function anotherPostAction()

View File

@ -81,18 +81,21 @@ class SimpleFormatterTest extends WebTestCase
'dataType' => 'string', 'dataType' => 'string',
'required' => true, 'required' => true,
'description' => 'A nice description', 'description' => 'A nice description',
'readonly' => false
), ),
'b' => 'b' =>
array( array(
'dataType' => 'string', 'dataType' => 'string',
'required' => false, 'required' => false,
'description' => '', 'description' => '',
'readonly' => false
), ),
'c' => 'c' =>
array( array(
'dataType' => 'boolean', 'dataType' => 'boolean',
'required' => true, 'required' => true,
'description' => '', 'description' => '',
'readonly' => false
), ),
), ),
'description' => 'create test', 'description' => 'create test',
@ -108,18 +111,21 @@ class SimpleFormatterTest extends WebTestCase
'dataType' => 'string', 'dataType' => 'string',
'required' => true, 'required' => true,
'description' => 'A nice description', 'description' => 'A nice description',
'readonly' => false
), ),
'b' => 'b' =>
array( array(
'dataType' => 'string', 'dataType' => 'string',
'required' => false, 'required' => false,
'description' => '', 'description' => '',
'readonly' => false
), ),
'c' => 'c' =>
array( array(
'dataType' => 'boolean', 'dataType' => 'boolean',
'required' => true, 'required' => true,
'description' => '', 'description' => '',
'readonly' => false
), ),
), ),
'description' => 'create test', 'description' => 'create test',
@ -138,6 +144,7 @@ class SimpleFormatterTest extends WebTestCase
'dataType' => 'string', 'dataType' => 'string',
'required' => true, 'required' => true,
'description' => 'A nice description', 'description' => 'A nice description',
'readonly' => false
), ),
), ),
'description' => 'create another test', 'description' => 'create another test',