diff --git a/Parser/JsonSerializableParser.php b/Parser/JsonSerializableParser.php new file mode 100644 index 0000000..2ff1b97 --- /dev/null +++ b/Parser/JsonSerializableParser.php @@ -0,0 +1,94 @@ +hasMethod('__construct')) { + foreach ($ref->getMethod('__construct')->getParameters() as $parameter) { + if (!$parameter->isOptional()) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function parse(array $input) + { + /** @var \JsonSerializable $obj */ + $obj = new $input['class'](); + + $encoded = $obj->jsonSerialize(); + $parsed = $this->getItemMetaData($encoded); + + if (isset($input['name']) && !empty($input['name'])) { + $output = array(); + $output[$input['name']] = $parsed; + return $output; + } + + return $parsed['children']; + } + + public function getItemMetaData($item) + { + $type = gettype($item); + + $meta = array( + 'dataType' => $type == 'NULL' ? null : $type, + 'actualType' => $type, + 'subType' => null, + 'required' => null, + 'description' => null, + 'readonly' => null + ); + + 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/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 cc2fe13..3b5bfca 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,11 @@ + + + + + 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/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..f4db0d3 --- /dev/null +++ b/Tests/Parser/JsonSerializableParserTest.php @@ -0,0 +1,100 @@ +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', + 'actualType' => 'array', + 'subType' => null, + 'required' => null, + 'description' => null, + 'readonly' => null + ) + ) + ) + ) + ); + } + + 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 + ) + ); + } +}