diff --git a/lib/Weasel/JsonMarshaller/Config/ClassAnnotationDriver.php b/lib/Weasel/JsonMarshaller/Config/ClassAnnotationDriver.php index ddc5b3f..89eaf3d 100644 --- a/lib/Weasel/JsonMarshaller/Config/ClassAnnotationDriver.php +++ b/lib/Weasel/JsonMarshaller/Config/ClassAnnotationDriver.php @@ -16,6 +16,7 @@ use Weasel\JsonMarshaller\Config\IAnnotations\IJsonAnyGetter; use Weasel\JsonMarshaller\Config\IAnnotations\IJsonAnySetter; use Weasel\JsonMarshaller\Config\IAnnotations\IJsonTypeInfo; +use Weasel\JsonMarshaller\Utils\TypeParser; /** * Load the configuration for a given class from annotations @@ -137,7 +138,7 @@ protected function _configureGetter(\ReflectionMethod $method, IJsonProperty $pr throw new \Exception("Serialization for property of name $property has already been configured."); } $getterConfig->method = $name; - $getterConfig->type = $propertyConfig->getType(); + $getterConfig->type = TypeParser::parseType($propertyConfig->getType(), true); $this->config->serialization->properties[$property] = $getterConfig; } @@ -169,7 +170,7 @@ protected function _configureSetter(\ReflectionMethod $method, IJsonProperty $pr } $setterConfig = new Deserialization\SetterDeserialization(); $setterConfig->method = $name; - $setterConfig->type = $propertyConfig->getType(); + $setterConfig->type = TypeParser::parseType($propertyConfig->getType(), true); $setterConfig->typeInfo = $this->_getDeserializationTypeInfo($typeInfo, $subTypes); $setterConfig->strict = $propertyConfig->getStrict(); @@ -213,7 +214,7 @@ protected function _configureCreator(\ReflectionMethod $method, IJsonCreator $cr foreach ($creatorConfig->getParams() as $paramConfig) { $param = new Deserialization\Param(); $param->name = $paramConfig->getName(); - $param->type = $paramConfig->getType(); + $param->type = TypeParser::parseType($paramConfig->getType(), true); $param->strict = $paramConfig->getStrict(); if (!isset($param->name)) { if (!isset($paramNames[$i])) { @@ -284,7 +285,7 @@ protected function _configureProperty(\ReflectionProperty $property) if (!isset($this->config->deserialization->properties[$propertyName])) { $setterConfig = new Deserialization\DirectDeserialization(); $setterConfig->property = $name; - $setterConfig->type = $propertyConfig->getType(); + $setterConfig->type = TypeParser::parseType($propertyConfig->getType(), true); $setterConfig->typeInfo = $this->_getDeserializationTypeInfo($typeInfo, $subTypes); $this->config->deserialization->properties[$propertyName] = $setterConfig; @@ -294,7 +295,7 @@ protected function _configureProperty(\ReflectionProperty $property) if (!isset($this->config->serialization->properties[$propertyName])) { $getterConfig = new Serialization\DirectSerialization(); $getterConfig->property = $name; - $getterConfig->type = $propertyConfig->getType(); + $getterConfig->type = TypeParser::parseType($propertyConfig->getType(), true); /** * @var Annotations\JsonInclude $includer diff --git a/lib/Weasel/JsonMarshaller/Config/Type/ListType.php b/lib/Weasel/JsonMarshaller/Config/Type/ListType.php new file mode 100644 index 0000000..d0b4441 --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Config/Type/ListType.php @@ -0,0 +1,19 @@ +valueType = $valueType; + } + +} \ No newline at end of file diff --git a/lib/Weasel/JsonMarshaller/Config/Type/MapType.php b/lib/Weasel/JsonMarshaller/Config/Type/MapType.php new file mode 100644 index 0000000..1c84b81 --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Config/Type/MapType.php @@ -0,0 +1,20 @@ +keyType = $keyType; + parent::__construct($valueType); + } + +} \ No newline at end of file diff --git a/lib/Weasel/JsonMarshaller/Config/Type/ObjectType.php b/lib/Weasel/JsonMarshaller/Config/Type/ObjectType.php new file mode 100644 index 0000000..867b76e --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Config/Type/ObjectType.php @@ -0,0 +1,19 @@ +class = $class; + } + +} \ No newline at end of file diff --git a/lib/Weasel/JsonMarshaller/Config/Type/ScalarType.php b/lib/Weasel/JsonMarshaller/Config/Type/ScalarType.php new file mode 100644 index 0000000..81cb8eb --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Config/Type/ScalarType.php @@ -0,0 +1,25 @@ +typeName = $typeName; + } + + /** + * @var string + * @JsonProperty(type="string") + */ + public $typeName; + + /** + * This is a bit of a dirty hack... + * @var JsonType + */ + public $jsonType; + +} \ No newline at end of file diff --git a/lib/Weasel/JsonMarshaller/Config/Type/Type.php b/lib/Weasel/JsonMarshaller/Config/Type/Type.php new file mode 100644 index 0000000..fdfbce9 --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Config/Type/Type.php @@ -0,0 +1,20 @@ +strict; } - return $this->_decodeValue($decoded, $type, $strict); + return $this->_decodeValue($decoded, TypeParser::parseType($type, true), $strict); } /** @@ -128,7 +134,7 @@ public function writeString($data, $type = null) if (!isset($type)) { $type = $this->_guessType($data); } - return $this->_encodeValue($data, $type); + return $this->_encodeValue($data, TypeParser::parseType($type, true)); } /** @@ -276,7 +282,7 @@ protected function _encodeObject($object, $typeInfo = null, $type = null) break; } $property = $typeInfo->typeInfoProperty; - $properties[$property] = $this->_encodeValue($classId, "string"); + $properties[$property] = $this->_encodeValue($classId, new ScalarType("string")); break; case Config\Serialization\TypeInfo::TI_AS_WRAPPER_ARRAY: // We're actually going to encase this encoded object in an array containing the classId. @@ -284,7 +290,7 @@ protected function _encodeObject($object, $typeInfo = null, $type = null) break; } return '[' . $this->_encodeValue($classId, - 'string') . ', ' . $this->_objectToJson($properties) . ']'; + new ScalarType('string')) . ', ' . $this->_objectToJson($properties) . ']'; break; case Config\Serialization\TypeInfo::TI_AS_WRAPPER_OBJECT: // Very similar yo the wrapper array case, but this time it's a map from the classId to the object. @@ -292,7 +298,7 @@ protected function _encodeObject($object, $typeInfo = null, $type = null) break; } return '{' . $this->_encodeValue($classId, - 'string') . ': ' . $this->_objectToJson($properties) . '}'; + new ScalarType('string')) . ': ' . $this->_objectToJson($properties) . '}'; break; default: throw new \Exception("Unsupported type info storage at class level"); @@ -307,7 +313,7 @@ protected function _objectToJson($properties) { $elements = array(); foreach ($properties as $key => $property) { - $elements[] = $this->_encodeValue($key, 'string') . ': ' . $property; + $elements[] = $this->_encodeValue($key, new ScalarType('string')) . ': ' . $property; } return '{' . implode(', ', $elements) . '}'; } @@ -472,73 +478,82 @@ protected function _decodeClass($array, $class, $strict) } - protected function _parseType($type) - { - if (isset($this->typeHandlers[$type])) { - // Assumption: if there's a type handler for this type string, then it's the right thing to use. - return array($type, $this->typeHandlers[$type]); - } - - // Assume type strings are well formed: look for the last [ to see if it's an array or map. - // Note that this might be an array of arrays, and we're after the outermost type, so we're after the last [! - $pos = strrpos($type, '['); - if ($pos === false) { - // If there wasn't a [ then this must be an object. - return array("complex"); + /** + * @param Type $type + * @return Type + */ + protected function _parseType($type) { + if (!$type instanceof Type) { + if (defined('E_USER_DEPRECATED')) { + // TODO: need to handle serialized configs before we can properly deprecate this. +// trigger_error("Use of unexpanded types is deprecated", E_USER_DEPRECATED); + } + // This is the really slow path. + $type = TypeParser::parseType($type, false); } - - // Extract the base type, and whatever's between the [...] as the index type. - // Potentially the type string is actually badly formed: - // e.g. this code will accept string[int! as being an array of string with index int. - // Bah. I'll ignore that case for now. This bit of code gets called a lot, I'd rather not add another substr. - $elementType = substr($type, 0, $pos); - $indexType = substr($type, $pos + 1, -1); - - if ($indexType === "") { - // The [...] were empty. It's an array. - return array("array", $elementType); + if ($type instanceof ScalarType) { + if (isset($this->typeHandlers[$type->typeName])) { + // Assumption: if there's a type handler for this type string, then it's the right thing to use. + $type->jsonType = $this->typeHandlers[$type->typeName]; + } else { + $type = new ObjectType($type->typeName); + } } - // Must be a map then. - return array("map", - $indexType, - $elementType - ); + return $type; } + /** + * @param $value + * @param $type + * @throws Exception\InvalidTypeException + * @throws Exception\JsonMarshallerException + * @throws \Exception + * @return mixed + */ protected function _decodeKey($value, $type) { if (!isset($value)) { throw new JsonMarshallerException("Key values cannot be null"); } $typeData = $this->_parseType($type); - switch (array_shift($typeData)) { - case "complex": - case "array": - throw new JsonMarshallerException("Keys must be of type int or string, not " . $type); - default: - // Keys are always strings, however we will allow other types, and disable strict type checking. - return $this->_decodeValue($value, $type, false); + if ($typeData instanceof ScalarType) { + // Keys are always strings, however we will allow other types, and disable strict type checking. + return $this->_decodeValue($value, $typeData, false); + } else { + throw new JsonMarshallerException("Keys must be of type int or string, not " . $type); } } + /** + * @param $value + * @param $type + * @param $strict + * @return mixed + * @throws Exception\InvalidTypeException + * @throws Exception\JsonMarshallerException + * @throws \Exception + */ protected function _decodeValue($value, $type, $strict) { if (!isset($value)) { return null; } $typeData = $this->_parseType($type); - switch (array_shift($typeData)) { - case "complex": + switch (true) { + case ($typeData instanceof ObjectType): if (!is_array($value)) { - throw new InvalidTypeException($type, $value); + throw new InvalidTypeException($typeData->class, $value); } - return $this->_decodeClass($value, $type, $strict); + return $this->_decodeClass($value, $typeData->class, $strict); break; /** @noinspection PhpMissingBreakStatementInspection */ - case "array": - array_unshift($typeData, 'int'); - case "map": - list ($indexType, $elementType) = $typeData; + case ($typeData instanceof MapType): + $indexType = $typeData->keyType; + case ($typeData instanceof ListType): + if (!isset($indexType)) { + $indexType = new ScalarType("int"); + } + $elementType = $typeData->valueType; $result = array(); if (!is_array($value)) { $value = array($value); @@ -547,13 +562,10 @@ protected function _decodeValue($value, $type, $strict) $result[$this->_decodeKey($key, $indexType)] = $this->_decodeValue($element, $elementType, $strict); } return $result; - break; + case ($typeData instanceof ScalarType): + return $typeData->jsonType->decodeValue($value, $this, $strict); default: - /** - * @var $typeHandler Types\JsonType - */ - list ($typeHandler) = $typeData; - return $typeHandler->decodeValue($value, $this, $strict); + throw new \Exception("Invalid config for type"); } } @@ -564,13 +576,10 @@ protected function _encodeKey($value, $type) throw new JsonMarshallerException("Key values cannot be null"); } $typeData = $this->_parseType($type); - switch (array_shift($typeData)) { - /** @noinspection PhpMissingBreakStatementInspection */ - case "complex": - case "array": - throw new JsonMarshallerException("Keys must be of type int or string, not " . $type); - default: - return $this->_encodeValue($value, "string"); + if ($typeData instanceof ScalarType) { + return $this->_encodeValue($value, new ScalarType("string")); + } else { + throw new JsonMarshallerException("Keys must be of type int or string, not " . $type); } } @@ -587,43 +596,39 @@ protected function _encodeValue($value, $type, $typeInfo = null) return json_encode(null); } $typeData = $this->_parseType($type); - switch (array_shift($typeData)) { - case "complex": + switch (true) { + case ($typeData instanceof ObjectType): if (!is_object($value)) { - throw new InvalidTypeException($type, $value); + throw new InvalidTypeException($typeData->class, $value); } - return $this->_encodeObject($value, $typeInfo, $type); - break; - case "array": - list ($elementType) = $typeData; + return $this->_encodeObject($value, $typeInfo, $typeData->class); + case ($typeData instanceof MapType): + $indexType = $typeData->keyType; + $elementType = $typeData->valueType; if (!is_array($value)) { $value = array($value); } $elements = array(); - foreach ($value as $element) { - $elements[] = $this->_encodeValue($element, $elementType); + foreach ($value as $key => $element) { + $elements[] = $this->_encodeKey($key, $indexType) . ': ' . $this->_encodeValue($element, + $elementType); } - return '[' . implode(', ', $elements) . ']'; - case "map": - list ($indexType, $elementType) = $typeData; + return '{' . implode(', ', $elements) . '}'; + case ($typeData instanceof ListType): + $elementType = $typeData->valueType; if (!is_array($value)) { $value = array($value); } $elements = array(); - foreach ($value as $key => $element) { - $elements[] = $this->_encodeKey($key, $indexType) . ': ' . $this->_encodeValue($element, - $elementType); + foreach ($value as $element) { + $elements[] = $this->_encodeValue($element, $elementType); } - return '{' . implode(', ', $elements) . '}'; + return '[' . implode(', ', $elements) . ']'; + case ($typeData instanceof ScalarType): + return $typeData->jsonType->encodeValue($value, $this); default: - /** - * @var $typeHandler Types\JsonType - */ - list ($typeHandler) = $typeData; - return $typeHandler->encodeValue($value, $this); - + throw new \Exception("Unable to work out what to do with type"); } - } diff --git a/lib/Weasel/JsonMarshaller/Utils/TypeParser.php b/lib/Weasel/JsonMarshaller/Utils/TypeParser.php new file mode 100644 index 0000000..fa41055 --- /dev/null +++ b/lib/Weasel/JsonMarshaller/Utils/TypeParser.php @@ -0,0 +1,38 @@ +getConfig(); $eProperties = array( - "stuff" => new GetterSerialization("getStuff", "string", 1), - "good" => new GetterSerialization("isGood", "bool", 1), + "stuff" => new GetterSerialization("getStuff", new ScalarType("string"), 1), + "good" => new GetterSerialization("isGood", new ScalarType("bool"), 1), ); $this->assertEquals($eProperties, $config->serialization->properties); @@ -65,7 +64,7 @@ public function testGetterNameIsNoBool() ); $mockReader->shouldReceive('getSingleMethodAnnotation')->with('isGood', '\Weasel\JsonMarshaller\Config\Annotations\JsonProperty')->andReturn( - new JsonProperty(null, "string", null) + new JsonProperty(null, "string[]", null) ); $mockReader->shouldReceive('getSingleMethodAnnotation')->withAnyArgs()->andReturnNull(); @@ -80,8 +79,8 @@ public function testGetterNameIsNoBool() $config = $driver->getConfig(); $eProperties = array( - "stuff" => new GetterSerialization("getStuff", "string", 1), - "isGood" => new GetterSerialization("isGood", "string", 1), + "stuff" => new GetterSerialization("getStuff", new ScalarType("string"), 1), + "isGood" => new GetterSerialization("isGood", new ListType(new ScalarType("string")), 1), ); $this->assertEquals($eProperties, $config->serialization->properties); diff --git a/tests/Weasel/JsonMarshaller/JsonMapperTest.php b/tests/Weasel/JsonMarshaller/JsonMapperTest.php index 8e0c752..a2604b3 100644 --- a/tests/Weasel/JsonMarshaller/JsonMapperTest.php +++ b/tests/Weasel/JsonMarshaller/JsonMapperTest.php @@ -7,6 +7,7 @@ namespace Weasel\JsonMarshaller; use Weasel\JsonMarshaller\Config\JsonConfigProvider; +use Weasel\JsonMarshaller\Utils\TypeParser; class JsonMapperTest extends \PHPUnit_Framework_TestCase { @@ -440,6 +441,8 @@ public function testAnyGetter() protected function addPropConfig(Config\ClassMarshaller $config, $name, $type) { + $type = TypeParser::parseType($type, true); + $prop = new Config\Serialization\DirectSerialization(); $prop->type = $type; $prop->property = $name; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 92213d3..8f05f89 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,6 +1,8 @@