Skip to content

Instantly share code, notes, and snippets.

@jpravetz
Created November 10, 2010 05:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpravetz/670414 to your computer and use it in GitHub Desktop.
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
/*************************************************************************
* 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 = property.@type.match( /(\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 );
}
}
}
package com.cayo.data.ab0
{
/**
* Sample typed ActionScript object
*/
public class MyAuthorAB0
{
public var name:String;
public var physicalAddress:PhysicalAddressAB0;
}
}
@jpravetz
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment