Created
November 10, 2010 05:43
-
-
Save jpravetz/670414 to your computer and use it in GitHub Desktop.
Converts a tree of JSON objects into a tree of typed ActionScript objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/************************************************************************* | |
* Copyright 2010 Cayo Systems, Inc. | |
* Copying and distribution of this file, with or without modification, | |
* are permitted in any medium without royalty provided the copyright | |
* notice and this notice are preserved. | |
* Author: Jim Pravetz | |
* Date: 2010/11/11 | |
* Language: ActionScript 3.0 | |
**************************************************************************/ | |
package com.cayo.util | |
{ | |
import com.adobe.serialization.json.JSON; | |
import com.adobe.utils.DateUtil; | |
import flash.utils.describeType; | |
import flash.utils.getDefinitionByName; | |
/** | |
* Converts a JSON string to a hierarchical tree of typed objects. Uses com.adobe.serialization.json.JSON | |
* to convert the string to a hierarchical tree of generic objects, then executes the static method | |
* mapToTypedObjects() to convert the JSON Object tree to the tree of typed objects. | |
* | |
* <p>JSON objects may optionally each contain a <code>class_name</code> property, except for objects in | |
* array which must specify a <code>class_name</code> property. If <code>class_name</code> property is missing | |
* where it is required then it will be ignored. The roor JSON object must specify the <code>protocol</code> property | |
* unless this is specified by the caller. Root JSON objects lacking the <code>protocol</code> property | |
* will result in an exception unless this class' protocol property is set.</p> | |
* | |
* <p>JSON uses under_score naming, while the typed objects use CamelCase. | |
* The server-generated JSON must use under_score naming, as these names will be converted by the client. | |
* For example, JSON's <code>updated_at</code> property will be mapped to this object's | |
* <code>updatedAt</code> property. A full JSON example is included below.</p> | |
* | |
* <p>As an example, if <code>packageRoot</code> has the value <code>com.cayo.data</code>, | |
* <code>protocol</code> has the value <code>ab0</code>, <code>class_name</code> has the value | |
* <code>alerta_response</code>, then the actual class name will be <code>com.cayo.data.ab0.AlbertaResponseAB0</code>. | |
* Refer to the example JSON file for guidance on how to create your JSON objects.</p> | |
* | |
* <p>Note: The object(s) to which you are mapping your JSON object must be referenced somewhere in your project. | |
* If it is not then it will not be compiled into your project and you will get a runtime error.</p> | |
* | |
* <p>Note: You will break this mapper if you add the <code>[Bindable]</code> metadata tag to your | |
* object, because it will turn your properties into accessors rather then variables. Keep your objects simple | |
* with public properties. Stray from this advice at your own risk.</p> | |
* | |
* @example ActionScript typed object | |
* <listing version="3.0"> | |
* package com.cayo.data.ab0 | |
* { | |
* public class AlbertaResponseAB0 | |
* { | |
* public var generator:AuthorABO; | |
* public var myProperty:String; | |
* public var updatedAt:Date; | |
* } | |
* } | |
* * </listing> | |
* | |
* @example JSON response | |
* <listing version="3.0"> | |
* { | |
* "class_name": "alberta_response", | |
* "protocol": "ab0", | |
* "generator": { | |
* "class_name": "author", | |
* "name": "Calgary Development Server", | |
* "uri": "http://www.cayosys.com/calgary", | |
* "version": "Pre-Alpha" | |
* }, | |
* "my_property": "my_value" | |
* "updated_at": "2010-11-10T15:41:58-08:00" | |
* } | |
* </listing> | |
*/ | |
public class JsonMapper | |
{ | |
/** | |
* The name of the JSON object property from which the class name is determined. | |
*/ | |
protected static const CLASS_NAME:String = "class_name"; | |
/** | |
* The name of the JSON object property from which the package is extracted. | |
*/ | |
protected static const PROTOCOL:String = "protocol"; | |
/** | |
* A depth limiter for descending JSON object trees. | |
*/ | |
protected static const NESTING_LIMIT:int = 16; | |
/** | |
* (Required) The root of the package. Example <code>com.cayo.data</code>. | |
*/ | |
public var packageRoot:String; | |
/** | |
* (Optional) Specify the protocol name. If not specified then the root object in the JSON | |
* object tree must specify <code>protocol</code> as one of it's properties. | |
*/ | |
public var protocol:String; | |
/** | |
* Decodes the JSON string. | |
* | |
* @param jsonString A string containing JSON encoded objects. | |
* @param targetObject If specified and if CLASS_NAME is missing from the root JSON object, then will | |
* build an object of this type . | |
* @return A typed ActionScript object. | |
* @throws ReferenceError if the class indicated by <code>class_name</code> does not exist. | |
* @throws Error | |
*/ | |
public function decode(jsonString:String, targetClass:String=null ) : Object | |
{ | |
var jsonObj:Object = JSON.decode( jsonString ); | |
var result:Object = mapToTypedObjects( jsonObj, packageRoot, protocol, targetClass ); | |
return result; | |
} | |
public function encode(typedObject:Object) : String | |
{ | |
var genericObj:Object = mapFromTypedObjects( typedObject ); | |
var result:String = JSON.encode( genericObj ); | |
return result; | |
} | |
/** | |
* Map the generic Object with properties to typed objects. | |
* | |
* @param obj Object to be mapped | |
* @packageRoot The root of the package from which the response object will be created (example 'com.cayo.alberta.data') | |
* @level Levels of object nesting, limited to NESTING_LIMIT | |
* @protocol A name to include in the package string (as camelCase) and also to be appended to the classname (as upper case with no underscores). | |
* The top level object in the tree must specify a protocol, unless this value is set. | |
*/ | |
public static function mapToTypedObjects( obj:Object, packageRoot:String, protocol:String, targetClass:String=null, level:int=0 ) : Object | |
{ | |
if( obj == null || (!obj.hasOwnProperty(CLASS_NAME) && !targetClass) ) | |
return null; | |
if( packageRoot == null ) | |
throw new Error( 'Package root not specified for JSON object mapper' ); | |
if( ++level >= NESTING_LIMIT ) | |
throw new Error( 'Nesting limit exceeded for JSON object mapper' ); | |
if( !protocol && !obj.hasOwnProperty( PROTOCOL ) ) | |
throw new Error( 'No protocol specified for JSON object mapper' ); | |
var name:String = obj.hasOwnProperty(CLASS_NAME) ? convertUnderScoreToCamelCase( obj[CLASS_NAME] ) : targetClass; | |
if( !protocol ) | |
protocol = obj[PROTOCOL]; | |
var packageName:String = protocol.replace( /\_/, '.' ); | |
var classAppendName:String = convertUnderScoreToCamelCase(protocol,true); | |
// Compose the actual class name - This is a really good line to place a breakpoint on | |
var className:String = packageRoot + "." + packageName + "." + name + classAppendName; | |
var objClass:Class = getDefinitionByName(className) as Class; | |
if( objClass == null ) return null; | |
var returnObject:Object = new(objClass)(); | |
var propertyMap:XML = describeType(returnObject); | |
var propertyTypeClass:Class; | |
// Enumerate the properties of the JSON object | |
for each (var property:XML in propertyMap.variable) | |
{ | |
var propertyName:String = convertCamelCaseToUnderscore( property.@name ); | |
if ((obj as Object).hasOwnProperty(propertyName)) | |
{ | |
propertyTypeClass = getDefinitionByName(property.@type) as Class; | |
// var propertyType:String = property.@type; | |
if( property.@type == 'Date' ) | |
{ | |
returnObject[property.@name] = DateUtil.parseW3CDTF(obj[propertyName]); | |
} | |
else if( property.@type == 'String' || property.@type == 'Boolean' ) | |
{ | |
returnObject[property.@name] = obj[propertyName]; | |
} | |
else if( property.@type == 'Number' ) | |
{ | |
returnObject[property.@name] = Number(obj[propertyName]); | |
} | |
else if( property.@type == 'uint' ) | |
{ | |
returnObject[property.@name] = uint(obj[propertyName]); | |
} | |
else if( property.@type == 'int' ) | |
{ | |
returnObject[property.@name] = int(obj[propertyName]); | |
} | |
else if( property.@type == 'Array' && obj[propertyName] is (propertyTypeClass) ) | |
{ | |
returnObject[property.@name] = new Array(); | |
for each( var entry:Object in obj[propertyName] ) | |
{ | |
if( entry is String || entry is Number || entry is int || entry is Boolean || entry is uint ) | |
returnObject[property.@name].push( entry ); | |
else | |
returnObject[property.@name].push( mapToTypedObjects( entry, packageRoot, protocol, null, level ) ); | |
} | |
} | |
else | |
{ | |
var subclassName:String = null; | |
if( property.@type ) | |
{ | |
var m0:Array = [email protected]( /(\w+)$/ ); | |
subclassName = (m0 && m0.length > 1 ) ? (m0[1] as String).replace( classAppendName, "" ) : null; | |
} | |
returnObject[property.@name] = mapToTypedObjects( obj[propertyName], packageRoot, protocol, subclassName, level ); | |
} | |
} | |
} | |
return returnObject; | |
} | |
/** | |
* Map the typed object with properties to a generic Object. | |
* | |
* @param obj Object to be mapped | |
* @packageRoot The root of the package from which the object will be created (example 'com.cayo.alberta.data') | |
* @level Levels of object nesting, limited to NESTING_LIMIT | |
* @protocol A name to include in the package string (as camelCase) and also to be appended to the classname (as upper case with no underscores). | |
* The top level object in the tree must specify a protocol, unless this value is set. | |
*/ | |
public static function mapFromTypedObjects( obj:Object, level:int=0 ) : Object | |
{ | |
if( obj == null ) | |
return null; | |
// if( packageRoot == null ) | |
// throw new Error( 'Package root not specified for JSON object mapper' ); | |
if( ++level >= NESTING_LIMIT ) | |
throw new Error( 'Nesting limit exceeded for JSON object mapper' ); | |
// if( !protocol && !obj.hasOwnProperty( PROTOCOL ) ) | |
// throw new Error( 'No protocol specified for JSON object mapper' ); | |
var fullClassName:String = getQualifiedClassName(obj); | |
var parts:Array = fullClassName.match( /^(.*)\.([^\.]+)\.([^\.]+)::(.+)$/ ); | |
if( parts == null || parts.length < 5 ) | |
throw new Error( 'Internal JsonMapper error' ); | |
var packageRoot:String = parts[1]; // Don't need this | |
var protocol:String = parts[2] + "_" + parts[3]; | |
var classNameAS3:String = convertCamelCaseToUnderscore(parts[4] as String); | |
var classParts:Array = classNameAS3.match( /^(.*)\_[^\_]+\_[^\_]+$/ ); | |
if( parts == null || parts.length < 2 ) | |
throw new Error( 'Internal JsonMapper error' ); | |
var className:String = classParts[1]; | |
var propertyMap:XML = describeType(obj); | |
var propertyTypeClass:Class; | |
var returnObject:Object = new Object; | |
// Enumerate the properties of the JSON object | |
for each (var property:XML in propertyMap.variable) | |
{ | |
var propertyName:String = convertCamelCaseToUnderscore( property.@name ); | |
var x:Boolean = obj.hasOwnProperty(property.@name); | |
var y:Boolean = obj[property.@name]; | |
if( obj.hasOwnProperty(property.@name) && obj[property.@name] != null ) | |
{ | |
propertyTypeClass = getDefinitionByName(property.@type) as Class; | |
// var propertyType:String = property.@type; | |
if( property.@type == 'Date' ) | |
{ | |
returnObject[propertyName] = DateUtil2.toW3CDTF(obj[property.@name],false,true); | |
} | |
else if( property.@type == 'String' || property.@type == 'Boolean' ) | |
{ | |
returnObject[propertyName] = obj[property.@name]; | |
} | |
else if( property.@type == 'Number' || property.@type == 'uint' || property.@type == 'int') | |
{ | |
returnObject[propertyName] = obj[property.@name]; | |
} | |
else if( property.@type == 'Array' && obj[property.@name] is (propertyTypeClass) ) | |
{ | |
returnObject[propertyName] = new Array(); | |
for each( var entry:Object in obj[property.@name] ) | |
{ | |
if( entry is String || entry is Number || entry is int || entry is Boolean || entry is uint ) | |
returnObject[propertyName].push( entry ); | |
else | |
returnObject[propertyName].push( mapFromTypedObjects( entry, level ) ); | |
} | |
} | |
else | |
{ | |
returnObject[propertyName] = mapFromTypedObjects( obj[property.@name], level ); | |
} | |
} | |
} | |
returnObject[convertCamelCaseToUnderscore(CLASS_NAME)] = className; | |
if( level == 1 ) | |
returnObject[convertCamelCaseToUnderscore(PROTOCOL)] = protocol; | |
return returnObject; | |
} | |
/** | |
* A quick implementation to convert under_score to CamelCase. | |
* Probably there is a nice RegExp formula to do this in one pass. | |
* @param s String to convert | |
* @param bFirstLettercap If true then convert the first character of s to uppercase | |
* (eg. hello_world becomes HelloWorld, as opposed to helloWorld) | |
*/ | |
internal static function convertUnderScoreToCamelCase( s:String, bFirstLetterCap:Boolean=true ) : String | |
{ | |
return CamelCaseEncoder.underScoreToCamelCase( s, bFirstLetterCap ); | |
} | |
/** | |
* A quick implementation to convert CamelCase to under_score. | |
* Probably there is a nice RegExp formula to do this in one pass. | |
*/ | |
internal static function convertCamelCaseToUnderscore( s:String ) : String | |
{ | |
return CamelCaseEncoder.camelCaseToUnderscore( s ); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated so that class_name no longer needs to be specified within the JSON object tree, except where complex objects are within an array. In other cases the object type is inferred from the object to which the JSON object is being mapped.