From 37c64657006e194248a1f70063a28e2c64888db8 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Tue, 30 Jun 2015 23:34:15 +0300 Subject: [PATCH 1/2] Add a parser for jsonSerializable classes --- Parser/JsonSerializableParser.php | 86 ++++++++++++++++ Resources/config/services.xml | 5 + ...sonSerializableOptionalConstructorTest.php | 22 +++++ ...sonSerializableRequiredConstructorTest.php | 22 +++++ Tests/Fixtures/Model/JsonSerializableTest.php | 24 +++++ Tests/Parser/JsonSerializableParserTest.php | 98 +++++++++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 Parser/JsonSerializableParser.php create mode 100644 Tests/Fixtures/Model/JsonSerializableOptionalConstructorTest.php create mode 100644 Tests/Fixtures/Model/JsonSerializableRequiredConstructorTest.php create mode 100644 Tests/Fixtures/Model/JsonSerializableTest.php create mode 100644 Tests/Parser/JsonSerializableParserTest.php diff --git a/Parser/JsonSerializableParser.php b/Parser/JsonSerializableParser.php new file mode 100644 index 0000000..e8835ac --- /dev/null +++ b/Parser/JsonSerializableParser.php @@ -0,0 +1,86 @@ +hasMethod('__construct')) { + foreach ($ref->getMethod('__construct')->getParameters() as $parameter) { + if (!$parameter->isOptional()) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function parse(array $item) + { + /** @var \JsonSerializable $obj */ + $obj = new $item['class'](); + + $encoded = $obj->jsonSerialize(); + $top = $this->getItemMetaData($encoded); + + return $top['children']; + } + + public function getItemMetaData($item) + { + $type = gettype($item); + + $meta = array( + 'dataType' => $type, + 'required' => true, + 'description' => '', + 'readonly' => false + ); + + if ($type == 'object' && $item instanceof \JsonSerializable) { + $meta = $this->getItemMetaData($item->jsonSerialize()); + $meta['class'] = get_class($item); + } elseif (($type == 'object' && $item instanceof \stdClass) || ($type == 'array' && !$this->isSequential($item))) { + $meta['dataType'] = 'object'; + $meta['children'] = array(); + foreach ($item as $key => $value) { + $meta['children'][$key] = $this->getItemMetaData($value); + } + } + + return $meta; + } + + /** + * Check for numeric sequential keys, just like the json encoder does + * Credit: http://stackoverflow.com/a/25206156/859027 + * + * @param array $arr + * @return bool + */ + private function isSequential(array $arr) + { + for ($i = count($arr) - 1; $i >= 0; $i--) { + if (!isset($arr[$i]) && !array_key_exists($i, $arr)) { + return false; + } + } + return true; + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index cc2fe13..12960ce 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -16,6 +16,7 @@ Nelmio\ApiDocBundle\Parser\CollectionParser Nelmio\ApiDocBundle\Parser\FormErrorsParser + Nelmio\ApiDocBundle\Parser\JsonSerializableParser @@ -68,6 +69,10 @@ + + + + diff --git a/Tests/Fixtures/Model/JsonSerializableOptionalConstructorTest.php b/Tests/Fixtures/Model/JsonSerializableOptionalConstructorTest.php new file mode 100644 index 0000000..ad631af --- /dev/null +++ b/Tests/Fixtures/Model/JsonSerializableOptionalConstructorTest.php @@ -0,0 +1,22 @@ + 123, + 'name' => 'My name', + 'child' => array( + 'value' => array(1, 2, 3) + ), + 'another' => new JsonSerializableOptionalConstructorTest() + ); + } +} diff --git a/Tests/Parser/JsonSerializableParserTest.php b/Tests/Parser/JsonSerializableParserTest.php new file mode 100644 index 0000000..0c5c1f4 --- /dev/null +++ b/Tests/Parser/JsonSerializableParserTest.php @@ -0,0 +1,98 @@ +parser = new JsonSerializableParser(); + } + + /** + * @dataProvider dataTestParser + */ + public function testParser($property, $expected) + { + $result = $this->parser->parse(array('class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableTest')); + foreach ($expected as $name => $value) { + $this->assertArrayHasKey($property, $result); + $this->assertArrayHasKey($name, $result[$property]); + $this->assertEquals($result[$property][$name], $expected[$name]); + } + } + + /** + * @dataProvider dataTestSupports + */ + public function testSupports($class, $expected) + { + $this->assertEquals($this->parser->supports(array('class' => $class)), $expected); + } + + public function dataTestParser() + { + return array( + array( + 'property' => 'id', + 'expected' => array( + 'dataType' => 'integer' + ) + ), + array( + 'property' => 'name', + 'expected' => array( + 'dataType' => 'string' + ) + ), + array( + 'property' => 'child', + 'expected' => array( + 'dataType' => 'object', + 'children' => array( + 'value' => array( + 'dataType' => 'array', + 'required' => true, + 'description' => '', + 'readonly' => false + ) + ) + ) + ) + ); + } + + public function dataTestSupports() + { + return array( + array( + 'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableTest', + 'expected' => true + ), + array( + 'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableRequiredConstructorTest', + 'expected' => false + ), + array( + 'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableOptionalConstructorTest', + 'expected' => true + ), + array( + 'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Popo', + 'expected' => false + ) + ); + } +} From 406a4e1b5b635d51b0dc6f7104fe765e8fbcd366 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Wed, 1 Jul 2015 14:01:44 +0300 Subject: [PATCH 2/2] Add support for using `name` in the input and output options for JsonSerializable and validation parsers --- Parser/JsonSerializableParser.php | 24 ++++++++++++++------- Parser/ValidationParser.php | 19 +++++++++++++++- Resources/config/services.xml | 3 ++- Resources/doc/index.md | 2 +- Tests/Parser/JsonSerializableParserTest.php | 8 ++++--- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/Parser/JsonSerializableParser.php b/Parser/JsonSerializableParser.php index e8835ac..2ff1b97 100644 --- a/Parser/JsonSerializableParser.php +++ b/Parser/JsonSerializableParser.php @@ -31,15 +31,21 @@ class JsonSerializableParser implements ParserInterface /** * {@inheritdoc} */ - public function parse(array $item) + public function parse(array $input) { /** @var \JsonSerializable $obj */ - $obj = new $item['class'](); + $obj = new $input['class'](); $encoded = $obj->jsonSerialize(); - $top = $this->getItemMetaData($encoded); + $parsed = $this->getItemMetaData($encoded); - return $top['children']; + if (isset($input['name']) && !empty($input['name'])) { + $output = array(); + $output[$input['name']] = $parsed; + return $output; + } + + return $parsed['children']; } public function getItemMetaData($item) @@ -47,10 +53,12 @@ class JsonSerializableParser implements ParserInterface $type = gettype($item); $meta = array( - 'dataType' => $type, - 'required' => true, - 'description' => '', - 'readonly' => false + 'dataType' => $type == 'NULL' ? null : $type, + 'actualType' => $type, + 'subType' => null, + 'required' => null, + 'description' => null, + 'readonly' => null ); if ($type == 'object' && $item instanceof \JsonSerializable) { diff --git a/Parser/ValidationParser.php b/Parser/ValidationParser.php index 27a671a..a0b6496 100644 --- a/Parser/ValidationParser.php +++ b/Parser/ValidationParser.php @@ -69,7 +69,24 @@ class ValidationParser implements ParserInterface, PostParserInterface { $className = $input['class']; - return $this->doParse($className, array()); + $parsed = $this->doParse($className, array()); + + if (isset($input['name']) && !empty($input['name'])) { + $output = array(); + $output[$input['name']] = array( + 'dataType' => 'object', + 'actualType' => 'object', + 'class' => $className, + 'subType' => null, + 'required' => null, + 'description' => null, + 'readonly' => null, + 'children' => $parsed + ); + return $output; + } + + return $parsed; } /** diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 12960ce..3b5bfca 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -70,8 +70,9 @@ + - + diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 6185cd3..bca5c56 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -164,7 +164,7 @@ class YourController * `parameters`: an array of parameters; * `input`: the input type associated to the method (currently this supports Form Types, classes with JMS Serializer - metadata, and classes with Validation component metadata) useful for POST|PUT methods, either as FQCN or as form type + metadata, classes with Validation component metadata and classes that implement JsonSerializable) useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container). * `output`: the output type associated with the response. Specified and parsed the same way as `input`. diff --git a/Tests/Parser/JsonSerializableParserTest.php b/Tests/Parser/JsonSerializableParserTest.php index 0c5c1f4..f4db0d3 100644 --- a/Tests/Parser/JsonSerializableParserTest.php +++ b/Tests/Parser/JsonSerializableParserTest.php @@ -64,9 +64,11 @@ class JsonSerializableParserTest extends \PHPUnit_Framework_TestCase 'children' => array( 'value' => array( 'dataType' => 'array', - 'required' => true, - 'description' => '', - 'readonly' => false + 'actualType' => 'array', + 'subType' => null, + 'required' => null, + 'description' => null, + 'readonly' => null ) ) )