From c3dc6b2eb912d62a9750c7294daf60146630a9e9 Mon Sep 17 00:00:00 2001 From: Ben Nadel Date: Mon, 12 Aug 2013 09:53:17 -0400 Subject: [PATCH] Allow closest key to be passed-down through serialization process such that arrays of simple-values can be type-cast. --- README.md | 7 ++ example/index.cfm | 2 + lib/JsonSerializer.cfc | 179 ++++++++++++++++------------- tests/specs/JsonSerializerTest.cfc | 15 +++ 4 files changed, 126 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 2cc8bd3..6577e51 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,12 @@ given the explicitly defined casing. So, if you want to use "id" in one place and "ID" in another place within the same data-structure, you're out of luck. Both keys will match "id" and will be given the same case. +## API Philosophy + +This is primarily intended to be used to return data from a server-side API. As +part of that use-case, some of my philosophy is baked into it. Namely, an API +usually returns a top-level struct / hash-map that defines the API result. This +is why the serialization process is driven by the name of keys. + [1]: http://www.bennadel.com \ No newline at end of file diff --git a/example/index.cfm b/example/index.cfm index 737c9be..afa4d32 100644 --- a/example/index.cfm +++ b/example/index.cfm @@ -8,6 +8,7 @@ .asAny( "createdAt" ) .asDate( "dateOfBirth" ) .asString( "favoriteColor" ) + .asInteger( "favoriteNumbers" ) .asString( "firstName" ) .asString( "lastName" ) .asString( "nickName" ) @@ -23,6 +24,7 @@ DATEOFBIRTH = dateConvert( "local2utc", "1975/01/01" ), NICKNAME = "Trish", FAVORITECOLOR = "333333", + FAVORITENUMBERS = [ true, 4.0, 137, false ], AGE = 38, CREATEDAT = now(), PASSWORD = "I<3ColdFusion&Cookies" diff --git a/lib/JsonSerializer.cfc b/lib/JsonSerializer.cfc index df4179f..be01522 100644 --- a/lib/JsonSerializer.cfc +++ b/lib/JsonSerializer.cfc @@ -174,7 +174,10 @@ component // I prepare the given array for serialization. Since the array doesn't have keys, this // function will simply walk the array and prepare each value contained within the array. - private array function prepareArrayForSerialization( required array input ) { + private array function prepareArrayForSerialization( + required array input, + required string closestKey + ) { var preparedInput = []; @@ -182,7 +185,7 @@ component arrayAppend( preparedInput, - prepareInputForSerialization( value ) + prepareInputForSerialization( value, closestKey ) ); } @@ -193,27 +196,38 @@ component // I prepare the input for case/value-sensitive serialization. - private any function prepareInputForSerialization( required any input ) { + private any function prepareInputForSerialization( + required any input, + string closestKey = "" + ) { // Convert the response based on its type. if ( isArray( input ) ) { return( - prepareArrayForSerialization( input ) + prepareArrayForSerialization( input, closestKey ) ); } else if ( isStruct( input ) ) { + // NOTE: No need to pass-in the closestKey since struct will provide its own keys. return( prepareStructForSerialization( input ) ); } else if ( isQuery( input ) ) { + // NOTE: No need to pass-in the closestKey since query will provide its own keys. return( prepareQueryForSerialization( input ) ); + } else if ( isSimpleValue( input ) ) { + + return( + prepareSimpleValueForSerialization( input, closestKey ) + ); + } // If the input is not a complex type, then we can gain no insight on how it's supposed @@ -224,76 +238,19 @@ component } - // I prepare the key-value pair for use in the prepared input. I will attempt to convert the - // value into the serialization-specific data type before adding it to the input. - private struct function prepareKeyValuePairForSerialization( - required struct preparedInput, - required string key, - required any value - ) { - - // If this key has been blocked, just return the unaltered input. - if ( structKeyExists( blockedKeyList, key ) ) { - - return( preparedInput ); - - } - - // Now that we know this key isn't blocked, get the case-sensitive version of it as - // defined in our key list. If the key has not been defined, we'll use lowercase as - // the default formatting. - var preparedKey = ( - structKeyExists( fullKeyList, key ) - ? fullKeyList[ key ] - : lcase( key ) - ); - - // Check to see if the key was defined in a data-type-specific list. If so, we'll try to - // convert the value as we copy it over into the prepared input. - if ( - structKeyExists( integerKeyList, key ) && - isNumeric( value ) - ) { - - var preparedValue = javaCast( "long", value ); - - } else if ( - structKeyExists( floatKeyList, key ) && - isValid( "float", value ) - ) { + // Return the key with the appropriate casing (or all lowercase if no case has been provided). + private string function prepareKeyForSerialization( required string key ) { - var preparedValue = javaCast( "float", value ); - - } else if ( - structKeyExists( booleanKeyList, key ) && - isBoolean( value ) - ) { - - var preparedValue = javaCast( "boolean", value ); - - } else if ( - structKeyExists( dateKeyList, key ) && - isNumericDate( value ) - ) { - - var preparedValue = getIsoTimeString( value ); - - } else if ( isSimpleValue( value ) ) { + if ( structKeyExists( fullKeyList, key ) ) { - // Prepend the string-value with the start-of-string marker so that ColdFusion won't - // be tempted to serialize the string value as a number. - var preparedValue = ( START_OF_STRING & value ); + return( fullKeyList[ key ] ); } else { - var preparedValue = prepareInputForSerialization( value ); - - } + return( lcase( key ) ); - // Add the prepared key/value pair into the prepared input. - preparedInput[ preparedKey ] = preparedValue; - return( preparedInput ); + } } @@ -328,11 +285,17 @@ component for ( var key in listToArray( input.columnList ) ) { - prepareKeyValuePairForSerialization( - preparedInput, - key, - input[ key ][ rowIndex ] - ); + // If this key is black-listed, skip it. + if ( structKeyExists( blockedKeyList, key ) ) { + + continue; + + } + + // Get the appropriate casing for the key. + var preparedKey = prepareKeyForSerialization( key ); + + preparedInput[ preparedKey ] = prepareInputForSerialization( input[ key ][ rowIndex ], key ); } @@ -348,11 +311,17 @@ component for ( var key in input ) { - prepareKeyValuePairForSerialization( - preparedInput, - key, - input[ key ] - ); + // If this key is black-listed, skip it. + if ( structKeyExists( blockedKeyList, key ) ) { + + continue; + + } + + // Get the appropriate casing for the key. + var preparedKey = prepareKeyForSerialization( key ); + + preparedInput[ preparedKey ] = prepareInputForSerialization( input[ key ], key ); } @@ -361,6 +330,62 @@ component } + // I prepare the given simple value for serialization by converting (or attempting to convert + // it) into the data type defined by the closest key in the contextual data structure. + private any function prepareSimpleValueForSerialization( + required any value, + required string closestKey + ) { + + // If we don't have any known container key, then we have no extra insight into how to + // serialize this value. As such, force it to be a string. + if ( closestKey == "" ) { + + return( START_OF_STRING & value ); + + } + + // Check to see if the key was defined in a data-type-specific list. If so, we'll try to + // convert the value as we copy it over into the prepared input. + if ( + structKeyExists( integerKeyList, closestKey ) && + ( isNumeric( value ) || isBoolean( value ) ) + ) { + + return( javaCast( "long", value ) ); + + } else if ( + structKeyExists( floatKeyList, closestKey ) && + ( isNumeric( value ) || isBoolean( value ) ) + ) { + + return( javaCast( "float", value ) ); + + } else if ( + structKeyExists( booleanKeyList, closestKey ) && + isBoolean( value ) + ) { + + return( javaCast( "boolean", value ) ); + + } else if ( + structKeyExists( dateKeyList, closestKey ) && + isNumericDate( value ) + ) { + + return( getIsoTimeString( value ) ); + + } else { + + // Prepend the string-value with the start-of-string marker so that ColdFusion won't + // be tempted to serialize the string value as a number. + return( START_OF_STRING & value ); + + } + + } + + // I strip out the start-of-string markers that were used to force ColdFusion to serialize // the given value as a string (ie, blocks accidental numeric conversions). private string function removeStartOfStringMarkers( required string response ) { diff --git a/tests/specs/JsonSerializerTest.cfc b/tests/specs/JsonSerializerTest.cfc index 480a884..95e4d9f 100644 --- a/tests/specs/JsonSerializerTest.cfc +++ b/tests/specs/JsonSerializerTest.cfc @@ -19,6 +19,7 @@ component .asInteger( "id" ) .asString( "lastName" ) .exclude( "password" ) + .asBoolean( "quizAnswers" ) .asFloat( "rating" ) ; @@ -69,6 +70,10 @@ component user.favoriteColors = colors; + quizAnswers = [ 1, 1, 0, 1 ]; + + user.quizAnswers = quizAnswers; + } @@ -152,6 +157,7 @@ component assertUserValues( serializedInput ); assertMovieValues( serializedInput ); assertColorValues( serializedInput ); + assertQuizAnswerValues( serializedInput ); } @@ -165,6 +171,7 @@ component assertUserValues( serializedInput ); assertMovieValues( serializedInput ); assertColorValues( serializedInput ); + assertQuizAnswerValues( serializedInput ); } @@ -236,6 +243,14 @@ component } + public void function assertQuizAnswerValues( required string serializedInput ) { + + // Test the values. + assert( find( "true,true,false,true", serializedInput ) ); + + } + + public void function assertUserValues( required string serializedInput ) { // Test the keys.