[TASK] Backport Flow JsonView 42/27642/4
authorJan Kiesewetter <jan@t3easy.de>
Sun, 16 Feb 2014 14:38:12 +0000 (15:38 +0100)
committerStefan Neufeind <typo3.neufeind@speedpartner.de>
Fri, 21 Mar 2014 22:26:54 +0000 (23:26 +0100)
Change-Id: Ia750e9997bb69b00652a6cc30dd3442574c0b97b
Resolves: #56007
Releases: 6.2
Reviewed-on: https://review.typo3.org/27642
Reviewed-by: Stefan Neufeind
Tested-by: Stefan Neufeind
typo3/sysext/extbase/Classes/Mvc/View/JsonView.php [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php [new file with mode: 0644]

diff --git a/typo3/sysext/extbase/Classes/Mvc/View/JsonView.php b/typo3/sysext/extbase/Classes/Mvc/View/JsonView.php
new file mode 100644 (file)
index 0000000..afc580e
--- /dev/null
@@ -0,0 +1,313 @@
+<?php
+namespace TYPO3\CMS\Extbase\Mvc\View;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2010-2014 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
+ *  Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *  A copy is found in the text file GPL.txt and important notices to the license
+ *  from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+/**
+ * A JSON view
+ *
+ * @api
+ */
+class JsonView extends \TYPO3\CMS\Extbase\Mvc\View\AbstractView {
+
+       /**
+        * Definition for the class name exposure configuration,
+        * that is, if the class name of an object should also be
+        * part of the output JSON, if configured.
+        *
+        * Setting this value, the object's class name is fully
+        * put out, including the namespace.
+        */
+       const EXPOSE_CLASSNAME_FULLY_QUALIFIED = 1;
+
+       /**
+        * Puts out only the actual class name without namespace.
+        * See EXPOSE_CLASSNAME_FULL for the meaning of the constant at all.
+        */
+       const EXPOSE_CLASSNAME_UNQUALIFIED = 2;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
+        * @inject
+        */
+       protected $reflectionService;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
+        */
+       protected $controllerContext;
+
+       /**
+        * Only variables whose name is contained in this array will be rendered
+        *
+        * @var array
+        */
+       protected $variablesToRender = array('value');
+
+       /**
+        * The rendering configuration for this JSON view which
+        * determines which properties of each variable to render.
+        *
+        * The configuration array must have the following structure:
+        *
+        * Example 1:
+        *
+        * array(
+        *              'variable1' => array(
+        *                      '_only' => array('property1', 'property2', ...)
+        *              ),
+        *              'variable2' => array(
+        *                      '_exclude' => array('property3', 'property4, ...)
+        *              ),
+        *              'variable3' => array(
+        *                      '_exclude' => array('secretTitle'),
+        *                      '_descend' => array(
+        *                              'customer' => array(
+        *                                      '_only' => array('firstName', 'lastName')
+        *                              )
+        *                      )
+        *              ),
+        *              'somearrayvalue' => array(
+        *                      '_descendAll' => array(
+        *                              '_only' => array('property1')
+        *                      )
+        *              )
+        * )
+        *
+        * Of variable1 only property1 and property2 will be included.
+        * Of variable2 all properties except property3 and property4
+        * are used.
+        * Of variable3 all properties except secretTitle are included.
+        *
+        * If a property value is an array or object, it is not included
+        * by default. If, however, such a property is listed in a "_descend"
+        * section, the renderer will descend into this sub structure and
+        * include all its properties (of the next level).
+        *
+        * The configuration of each property in "_descend" has the same syntax
+        * like at the top level. Therefore - theoretically - infinitely nested
+        * structures can be configured.
+        *
+        * To export indexed arrays the "_descendAll" section can be used to
+        * include all array keys for the output. The configuration inside a
+        * "_descendAll" will be applied to each array element.
+        *
+        *
+        * Example 2: exposing object identifier
+        *
+        * array(
+        *              'variableFoo' => array(
+        *                      '_exclude' => array('secretTitle'),
+        *                      '_descend' => array(
+        *                              'customer' => array(    // consider 'customer' being a persisted entity
+        *                                      '_only' => array('firstName'),
+        *                                      '_exposeObjectIdentifier' => TRUE,
+        *                                      '_exposedObjectIdentifierKey' => 'guid'
+        *                              )
+        *                      )
+        *              )
+        * )
+        *
+        * Note for entity objects you are able to expose the object's identifier
+        * also, just add an "_exposeObjectIdentifier" directive set to TRUE and
+        * an additional property '__identity' will appear keeping the persistence
+        * identifier. Renaming that property name instead of '__identity' is also
+        * possible with the directive "_exposedObjectIdentifierKey".
+        * Example 2 above would output (summarized):
+        * {"customer":{"firstName":"John","guid":"892693e4-b570-46fe-af71-1ad32918fb64"}}
+        *
+        *
+        * Example 3: exposing object's class name
+        *
+        * array(
+        *              'variableFoo' => array(
+        *                      '_exclude' => array('secretTitle'),
+        *                      '_descend' => array(
+        *                              'customer' => array(    // consider 'customer' being an object
+        *                                      '_only' => array('firstName'),
+        *                                      '_exposeClassName' => TYPO3\Flow\Mvc\View\JsonView::EXPOSE_CLASSNAME_FULLY_QUALIFIED
+        *                              )
+        *                      )
+        *              )
+        * )
+        *
+        * The ``_exposeClassName`` is similar to the objectIdentifier one, but the class name is added to the
+        * JSON object output, for example (summarized):
+        * {"customer":{"firstName":"John","__class":"Acme\Foo\Domain\Model\Customer"}}
+        *
+        * The other option is EXPOSE_CLASSNAME_UNQUALIFIED which only will give the last part of the class
+        * without the namespace, for example (summarized):
+        * {"customer":{"firstName":"John","__class":"Customer"}}
+        * This might be of interest to not provide information about the package or domain structure behind.
+        *
+        * @var array
+        */
+       protected $configuration = array();
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
+        * @inject
+        */
+       protected $persistenceManager;
+
+       /**
+        * Specifies which variables this JsonView should render
+        * By default only the variable 'value' will be rendered
+        *
+        * @param array $variablesToRender
+        * @return void
+        * @api
+        */
+       public function setVariablesToRender(array $variablesToRender) {
+               $this->variablesToRender = $variablesToRender;
+       }
+
+       /**
+        * @param array $configuration The rendering configuration for this JSON view
+        * @return void
+        */
+       public function setConfiguration(array $configuration) {
+               $this->configuration = $configuration;
+       }
+
+       /**
+        * Transforms the value view variable to a serializable
+        * array represantion using a YAML view configuration and JSON encodes
+        * the result.
+        *
+        * @return string The JSON encoded variables
+        * @api
+        */
+       public function render() {
+               $this->controllerContext->getResponse()->setHeader('Content-Type', 'application/json');
+               $propertiesToRender = $this->renderArray();
+               return json_encode($propertiesToRender);
+       }
+
+       /**
+        * Loads the configuration and transforms the value to a serializable
+        * array.
+        *
+        * @return array An array containing the values, ready to be JSON encoded
+        * @api
+        */
+       protected function renderArray() {
+               if (count($this->variablesToRender) === 1) {
+                       $variableName = current($this->variablesToRender);
+                       $valueToRender = isset($this->variables[$variableName]) ? $this->variables[$variableName] : NULL;
+                       $configuration = isset($this->configuration[$variableName]) ? $this->configuration[$variableName] : array();
+               } else {
+                       $valueToRender = array();
+                       foreach ($this->variablesToRender as $variableName) {
+                               $valueToRender[$variableName] = isset($this->variables[$variableName]) ? $this->variables[$variableName] : NULL;
+                       }
+                       $configuration = $this->configuration;
+               }
+               return $this->transformValue($valueToRender, $configuration);
+       }
+
+       /**
+        * Transforms a value depending on type recursively using the
+        * supplied configuration.
+        *
+        * @param mixed $value The value to transform
+        * @param array $configuration Configuration for transforming the value
+        * @return array The transformed value
+        */
+       protected function transformValue($value, array $configuration) {
+               if (is_array($value) || $value instanceof \ArrayAccess) {
+                       $array = array();
+                       foreach ($value as $key => $element) {
+                               if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) {
+                                       $array[$key] = $this->transformValue($element, $configuration['_descendAll']);
+                               } else {
+                                       if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($key, $configuration['_only'])) {
+                                               continue;
+                                       }
+                                       if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($key, $configuration['_exclude'])) {
+                                               continue;
+                                       }
+                                       $array[$key] = $this->transformValue($element, isset($configuration[$key]) ? $configuration[$key] : array());
+                               }
+                       }
+                       return $array;
+               } elseif (is_object($value)) {
+                       return $this->transformObject($value, $configuration);
+               } else {
+                       return $value;
+               }
+       }
+
+       /**
+        * Traverses the given object structure in order to transform it into an
+        * array structure.
+        *
+        * @param object $object Object to traverse
+        * @param array $configuration Configuration for transforming the given object or NULL
+        * @return array Object structure as an array
+        */
+       protected function transformObject($object, array $configuration) {
+               if ($object instanceof \DateTime) {
+                       return $object->format(\DateTime::ISO8601);
+               } else {
+                       $propertyNames = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getGettablePropertyNames($object);
+
+                       $propertiesToRender = array();
+                       foreach ($propertyNames as $propertyName) {
+                               if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($propertyName, $configuration['_only'])) {
+                                       continue;
+                               }
+                               if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($propertyName, $configuration['_exclude'])) {
+                                       continue;
+                               }
+
+                               $propertyValue = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getProperty($object, $propertyName);
+
+                               if (!is_array($propertyValue) && !is_object($propertyValue)) {
+                                       $propertiesToRender[$propertyName] = $propertyValue;
+                               } elseif (isset($configuration['_descend']) && array_key_exists($propertyName, $configuration['_descend'])) {
+                                       $propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $configuration['_descend'][$propertyName]);
+                               }
+                       }
+                       if (isset($configuration['_exposeObjectIdentifier']) && $configuration['_exposeObjectIdentifier'] === TRUE) {
+                               if (isset($configuration['_exposedObjectIdentifierKey']) && strlen($configuration['_exposedObjectIdentifierKey']) > 0) {
+                                       $identityKey = $configuration['_exposedObjectIdentifierKey'];
+                               } else {
+                                       $identityKey = '__identity';
+                               }
+                               $propertiesToRender[$identityKey] = $this->persistenceManager->getIdentifierByObject($object);
+                       }
+                       if (isset($configuration['_exposeClassName']) && ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED || $configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_UNQUALIFIED)) {
+                               $className = get_class($object);
+                               $classNameParts = explode('\\', $className);
+                               $propertiesToRender['__class'] = ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED ? $className : array_pop($classNameParts));
+                       }
+
+                       return $propertiesToRender;
+               }
+       }
+}
diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php
new file mode 100644 (file)
index 0000000..5e49006
--- /dev/null
@@ -0,0 +1,434 @@
+<?php
+namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\View;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2010-2014 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
+ *  Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *  A copy is found in the text file GPL.txt and important notices to the license
+ *  from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use TYPO3\CMS\Extbase\Mvc\View\JsonView;
+
+/**
+ * Testcase for the JSON view
+ *
+ */
+class JsonViewTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Mvc\View\JsonView
+        */
+       protected $view;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
+        */
+       protected $controllerContext;
+
+       /**
+        * @var \TYPO3\CMS\Extbase\Mvc\Web\Response
+        */
+       protected $response;
+
+       /**
+        * Sets up this test case
+        * @return void
+        */
+       public function setUp() {
+               $this->view = $this->getMock('TYPO3\CMS\Extbase\Mvc\View\JsonView', array('loadConfigurationFromYamlFile'));
+               $this->controllerContext = $this->getMock('TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext', array(), array(), '', FALSE);
+               $this->response = $this->getMock('TYPO3\CMS\Extbase\Mvc\Web\Response', array());
+               $this->controllerContext->expects($this->any())->method('getResponse')->will($this->returnValue($this->response));
+               $this->view->setControllerContext($this->controllerContext);
+       }
+
+       /**
+        * data provider for testTransformValue()
+        * @return array
+        */
+       public function jsonViewTestData() {
+               $output = array();
+
+               $object = new \stdClass();
+               $object->value1 = 'foo';
+               $object->value2 = 1;
+               $configuration = array();
+               $expected = array('value1' => 'foo', 'value2' => 1);
+               $output[] = array($object, $configuration, $expected, 'all direct child properties should be serialized');
+
+               $configuration = array('_only' => array('value1'));
+               $expected = array('value1' => 'foo');
+               $output[] = array($object, $configuration, $expected, 'if "only" properties are specified, only these should be serialized');
+
+               $configuration = array('_exclude' => array('value1'));
+               $expected = array('value2' => 1);
+               $output[] = array($object, $configuration, $expected, 'if "exclude" properties are specified, they should not be serialized');
+
+               $object = new \stdClass();
+               $object->value1 = new \stdClass();
+               $object->value1->subvalue1 = 'Foo';
+               $object->value2 = 1;
+               $configuration = array();
+               $expected = array('value2' => 1);
+               $output[] = array($object, $configuration, $expected, 'by default, sub objects of objects should not be serialized.');
+
+               $object = new \stdClass();
+               $object->value1 = array('subarray' => 'value');
+               $object->value2 = 1;
+               $configuration = array();
+               $expected = array('value2' => 1);
+               $output[] = array($object, $configuration, $expected, 'by default, sub arrays of objects should not be serialized.');
+
+               $object = array('foo' => 'bar', 1 => 'baz', 'deep' => array('test' => 'value'));
+               $configuration = array();
+               $expected = array('foo' => 'bar', 1 => 'baz', 'deep' => array('test' => 'value'));
+               $output[] = array($object, $configuration, $expected, 'associative arrays should be serialized deeply');
+
+               $object = array('foo', 'bar');
+               $configuration = array();
+               $expected = array('foo', 'bar');
+               $output[] = array($object, $configuration, $expected, 'numeric arrays should be serialized');
+
+               $nestedObject = new \stdClass();
+               $nestedObject->value1 = 'foo';
+               $object = array($nestedObject);
+               $configuration = array();
+               $expected = array(array('value1' => 'foo'));
+               $output[] = array($object, $configuration, $expected, 'array of objects should be serialized');
+
+               $properties = array('foo' => 'bar', 'prohibited' => 'xxx');
+               $nestedObject = $this->getMock('Test' . md5(uniqid(mt_rand(), TRUE)), array('getName', 'getPath', 'getProperties', 'getOther'));
+               $nestedObject->expects($this->any())->method('getName')->will($this->returnValue('name'));
+               $nestedObject->expects($this->any())->method('getPath')->will($this->returnValue('path'));
+               $nestedObject->expects($this->any())->method('getProperties')->will($this->returnValue($properties));
+               $nestedObject->expects($this->never())->method('getOther');
+               $object = $nestedObject;
+               $configuration = array(
+                       '_only' => array('name', 'path', 'properties'),
+                       '_descend' => array(
+                                'properties' => array(
+                                         '_exclude' => array('prohibited')
+                                )
+                       )
+               );
+               $expected = array(
+                       'name' => 'name',
+                       'path' => 'path',
+                       'properties' => array('foo' => 'bar')
+               );
+               $output[] = array($object, $configuration, $expected, 'descending into arrays should be possible');
+
+               $nestedObject = new \stdClass();
+               $nestedObject->value1 = 'foo';
+               $value = new \SplObjectStorage();
+               $value->attach($nestedObject);
+               $configuration = array();
+               $expected = array(array('value1' => 'foo'));
+               $output[] = array($value, $configuration, $expected, 'SplObjectStorage with objects should be serialized');
+
+               $dateTimeObject = new \DateTime('2011-02-03T03:15:23', new \DateTimeZone('UTC'));
+               $configuration = array();
+               $expected = '2011-02-03T03:15:23+0000';
+               $output[] = array($dateTimeObject, $configuration, $expected, 'DateTime object in UTC time zone could not be serialized.');
+
+               $dateTimeObject = new \DateTime('2013-08-15T15:25:30', new \DateTimeZone('America/Los_Angeles'));
+               $configuration = array();
+               $expected = '2013-08-15T15:25:30-0700';
+               $output[] = array($dateTimeObject, $configuration, $expected, 'DateTime object in America/Los_Angeles time zone could not be serialized.');
+               return $output;
+       }
+
+       /**
+        * @test
+        * @dataProvider jsonViewTestData
+        */
+       public function testTransformValue($object, $configuration, $expected, $description) {
+               $jsonView = $this->getAccessibleMock('TYPO3\CMS\Extbase\Mvc\View\JsonView', array('dummy'), array(), '', FALSE);
+
+               $actual = $jsonView->_call('transformValue', $object, $configuration);
+
+               $this->assertEquals($expected, $actual, $description);
+       }
+
+       /**
+        * data provider for testTransformValueWithObjectIdentifierExposure()
+        * @return array
+        */
+       public function objectIdentifierExposureTestData() {
+               $output = array();
+
+               $dummyIdentifier = 'e4f40dfc-8c6e-4414-a5b1-6fd3c5cf7a53';
+
+               $object = new \stdClass();
+               $object->value1 = new \stdClass();
+               $configuration = array(
+                       '_descend' => array(
+                                'value1' => array(
+                                         '_exposeObjectIdentifier' => TRUE
+                                )
+                       )
+               );
+
+               $expected = array('value1' => array('__identity' => $dummyIdentifier));
+               $output[] = array($object, $configuration, $expected, $dummyIdentifier, 'boolean TRUE should result in __identity key');
+
+               $configuration['_descend']['value1']['_exposedObjectIdentifierKey'] = 'guid';
+               $expected = array('value1' => array('guid' => $dummyIdentifier));
+               $output[] = array($object, $configuration, $expected, $dummyIdentifier, 'string value should result in string-equal key');
+
+               return $output;
+       }
+
+       /**
+        * @test
+        * @dataProvider objectIdentifierExposureTestData
+        */
+       public function testTransformValueWithObjectIdentifierExposure($object, $configuration, $expected, $dummyIdentifier, $description) {
+               $persistenceManagerMock = $this->getMock('TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager', array('getIdentifierByObject'));
+               $jsonView = $this->getAccessibleMock('TYPO3\CMS\Extbase\Mvc\View\JsonView', array('dummy'), array(), '', FALSE);
+               $jsonView->_set('persistenceManager', $persistenceManagerMock);
+
+               $persistenceManagerMock->expects($this->once())->method('getIdentifierByObject')->with($object->value1)->will($this->returnValue($dummyIdentifier));
+
+               $actual = $jsonView->_call('transformValue', $object, $configuration);
+
+               $this->assertEquals($expected, $actual, $description);
+       }
+
+       /**
+        * A data provider
+        */
+       public function exposeClassNameSettingsAndResults() {
+               $className = 'DummyClass' . md5(uniqid(mt_rand(), TRUE));
+               $namespace = 'TYPO3\CMS\Extbase\Tests\Unit\Mvc\View\\' . $className;
+               return array(
+                       array(
+                               JsonView::EXPOSE_CLASSNAME_FULLY_QUALIFIED,
+                               $className,
+                               $namespace,
+                               array('value1' => array('__class' => $namespace . '\\' . $className))
+                       ),
+                       array(
+                               JsonView::EXPOSE_CLASSNAME_UNQUALIFIED,
+                               $className,
+                               $namespace,
+                               array('value1' => array('__class' => $className))
+                       ),
+                       array(
+                               NULL,
+                               $className,
+                               $namespace,
+                               array('value1' => array())
+                       )
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider exposeClassNameSettingsAndResults
+        */
+       public function viewExposesClassNameFullyIfConfiguredSo($exposeClassNameSetting, $className, $namespace, $expected) {
+               $fullyQualifiedClassName = $namespace . '\\' . $className;
+               if (class_exists($fullyQualifiedClassName) === FALSE) {
+                       eval('namespace ' . $namespace . '; class ' . $className . ' {}');
+               }
+
+               $object = new \stdClass();
+               $object->value1 = new $fullyQualifiedClassName();
+               $configuration = array(
+                       '_descend' => array(
+                               'value1' => array(
+                                       '_exposeClassName' => $exposeClassNameSetting
+                               )
+                       )
+               );
+               $reflectionService = $this->getMock('TYPO3\CMS\Extbase\Reflection\ReflectionService');
+               $reflectionService->expects($this->any())->method('getClassNameByObject')->will($this->returnCallback(function($object) {
+                       return get_class($object);
+               }));
+
+               $jsonView = $this->getAccessibleMock('TYPO3\CMS\Extbase\Mvc\View\JsonView', array('dummy'), array(), '', FALSE);
+               $this->inject($jsonView, 'reflectionService', $reflectionService);
+               $actual = $jsonView->_call('transformValue', $object, $configuration);
+               $this->assertEquals($expected, $actual);
+       }
+
+       /**
+        * @test
+        */
+       public function renderSetsContentTypeHeader() {
+               $this->response->expects($this->once())->method('setHeader')->with('Content-Type', 'application/json');
+
+               $this->view->render();
+       }
+
+       /**
+        * @test
+        */
+       public function renderReturnsJsonRepresentationOfAssignedObject() {
+               $object = new \stdClass();
+               $object->foo = 'Foo';
+               $this->view->assign('value', $object);
+
+               $expectedResult = '{"foo":"Foo"}';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderReturnsJsonRepresentationOfAssignedArray() {
+               $array = array('foo' => 'Foo', 'bar' => 'Bar');
+               $this->view->assign('value', $array);
+
+               $expectedResult = '{"foo":"Foo","bar":"Bar"}';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderReturnsJsonRepresentationOfAssignedSimpleValue() {
+               $value = 'Foo';
+               $this->view->assign('value', $value);
+
+               $expectedResult = '"Foo"';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderReturnsNullIfNameOfAssignedVariableIsNotEqualToValue() {
+               $value = 'Foo';
+               $this->view->assign('foo', $value);
+
+               $expectedResult = 'null';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderOnlyRendersVariableWithTheNameValue() {
+               $this->view
+                       ->assign('value', 'Value')
+                       ->assign('someOtherVariable', 'Foo');
+
+               $expectedResult = '"Value"';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function setVariablesToRenderOverridesValueToRender() {
+               $value = 'Foo';
+               $this->view->assign('foo', $value);
+               $this->view->setVariablesToRender(array('foo'));
+
+               $expectedResult = '"Foo"';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderRendersMultipleValuesIfTheyAreSpecifiedAsVariablesToRender() {
+               $this->view
+                       ->assign('value', 'Value1')
+                       ->assign('secondValue', 'Value2')
+                       ->assign('someOtherVariable', 'Value3');
+               $this->view->setVariablesToRender(array('value', 'secondValue'));
+
+               $expectedResult = '{"value":"Value1","secondValue":"Value2"}';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderCanRenderMultipleComplexObjects() {
+               $array = array('foo' => array('bar' => 'Baz'));
+               $object = new \stdClass();
+               $object->foo = 'Foo';
+
+               $this->view
+                       ->assign('array', $array)
+                       ->assign('object', $object)
+                       ->assign('someOtherVariable', 'Value3');
+               $this->view->setVariablesToRender(array('array', 'object'));
+
+               $expectedResult = '{"array":{"foo":{"bar":"Baz"}},"object":{"foo":"Foo"}}';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function renderCanRenderPlainArray() {
+               $array = array(array('name' => 'Foo', 'secret' => TRUE), array('name' => 'Bar', 'secret' => TRUE));
+
+               $this->view->assign('value', $array);
+               $this->view->setConfiguration(array(
+                       'value' => array(
+                               '_descendAll' => array(
+                                       '_only' => array('name')
+                               )
+                       )
+               ));
+
+               $expectedResult = '[{"name":"Foo"},{"name":"Bar"}]';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
+        * @test
+        */
+       public function descendAllKeepsArrayIndexes() {
+               $array = array(array('name' => 'Foo', 'secret' => TRUE), array('name' => 'Bar', 'secret' => TRUE));
+
+               $this->view->assign('value', $array);
+               $this->view->setConfiguration(array(
+                       'value' => array(
+                               '_descendAll' => array(
+                                       '_descendAll' => array()
+                               )
+                       )
+               ));
+
+               $expectedResult = '[{"name":"Foo","secret":true},{"name":"Bar","secret":true}]';
+               $actualResult = $this->view->render();
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+}