From 0624ab852cfeae17c4254c8fb2ab4997d306ff84 Mon Sep 17 00:00:00 2001
From: Jan Kiesewetter <jan@t3easy.de>
Date: Sun, 16 Feb 2014 15:38:12 +0100
Subject: [PATCH] [TASK] Backport Flow JsonView

Change-Id: Ia750e9997bb69b00652a6cc30dd3442574c0b97b
Resolves: #56007
Releases: 6.2
Reviewed-on: https://review.typo3.org/27642
Reviewed-by: Stefan Neufeind
Tested-by: Stefan Neufeind
---
 .../extbase/Classes/Mvc/View/JsonView.php     | 313 +++++++++++++
 .../Tests/Unit/Mvc/View/JsonViewTest.php      | 434 ++++++++++++++++++
 2 files changed, 747 insertions(+)
 create mode 100644 typo3/sysext/extbase/Classes/Mvc/View/JsonView.php
 create mode 100644 typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php

diff --git a/typo3/sysext/extbase/Classes/Mvc/View/JsonView.php b/typo3/sysext/extbase/Classes/Mvc/View/JsonView.php
new file mode 100644
index 000000000000..afc580e66b60
--- /dev/null
+++ b/typo3/sysext/extbase/Classes/Mvc/View/JsonView.php
@@ -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
index 000000000000..5e490066f2ce
--- /dev/null
+++ b/typo3/sysext/extbase/Tests/Unit/Mvc/View/JsonViewTest.php
@@ -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);
+	}
+}
-- 
GitLab