initial
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
component
|
||||
output = false
|
||||
hint = "I provide a way to serialize complex ColdFusion data values as case-sensitive JavaScript Object Notation (JSON) strings."
|
||||
{
|
||||
//2024-06-27 modified by msyu: added server timezone
|
||||
|
||||
// I return the initialized component.
|
||||
public any function init() {
|
||||
|
||||
// Every key is added to the full key list - used for one-time key serialization.
|
||||
fullKeyList = {};
|
||||
|
||||
// Every key is added to the hint list so that we don't have to switch on the lists.
|
||||
fullHintList = {};
|
||||
|
||||
// These key lists determine special data serialization.
|
||||
stringKeyList = {};
|
||||
booleanKeyList = {};
|
||||
integerKeyList = {};
|
||||
floatKeyList = {};
|
||||
dateKeyList = {};
|
||||
|
||||
// These keys will NOT be used in serialization (ie. the key/value pairs will not be
|
||||
// added to the serialized output).
|
||||
blockedKeyList = {};
|
||||
|
||||
// Return the initialized component.
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ---
|
||||
// PUBLIC METHODS.
|
||||
// ---
|
||||
|
||||
|
||||
// I define the given key without a type. This is here to provide key-casing without caring
|
||||
// about why type of data conversion takes place. Returns serializer.
|
||||
public any function asAny( required string key ) {
|
||||
|
||||
return( defineKey( fullKeyList, key, "any" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a boolean. Returns serializer.
|
||||
public any function asBoolean( required string key ) {
|
||||
|
||||
return( defineKey( booleanKeyList, key, "boolean" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a date. Returns serializer.
|
||||
public any function asDate( required string key ) {
|
||||
|
||||
return( defineKey( dateKeyList, key, "date" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a float / decimal. Returns serializer.
|
||||
public any function asFloat( required string key ) {
|
||||
|
||||
return( defineKey( floatKeyList, key, "float" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as an integer. Returns serializer.
|
||||
public any function asInteger( required string key ) {
|
||||
|
||||
return( defineKey( integerKeyList, key, "integer" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a string. Returns serializer.
|
||||
public any function asString( required string key ) {
|
||||
|
||||
return( defineKey( stringKeyList, key, "string" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the key as one that should not be included in the serialized response.
|
||||
public any function exclude( required string key ) {
|
||||
|
||||
blockedKeyList[ key ] = true;
|
||||
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize the given input as JavaScript Object Notation (JSON) using the case-sensitive
|
||||
* values defined in the key-list.
|
||||
*
|
||||
* @output false
|
||||
*/
|
||||
public string function serialize( required any input ) {
|
||||
|
||||
// Write the serialized value to the output buffer.
|
||||
savecontent variable = "local.serializedInput" {
|
||||
|
||||
serializeInput( input, "any" );
|
||||
|
||||
}
|
||||
|
||||
return( serializedInput );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ---
|
||||
// PRIVATE METHODS.
|
||||
// ---
|
||||
|
||||
|
||||
// I define the given key within the given key list.
|
||||
private any function defineKey(
|
||||
required struct keyList,
|
||||
required string key,
|
||||
required string hint
|
||||
) {
|
||||
|
||||
if ( structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
throw(
|
||||
type = "DuplicateKey",
|
||||
message = "The key [#key#] has already been defined within the serializer.",
|
||||
detail = "The current key list is: #structKeyList( fullKeyList, ', ' )#"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Add to the appropriate data-type lists. This one is used for existence checking.
|
||||
keyList[ key ] = key;
|
||||
|
||||
// Add all keys to the full key list as well. This one is used to store the serialization
|
||||
// of the key so that it doesn't have to be recalculated each time the object is serialized.
|
||||
fullKeyList[ key ] = serializeString( key );
|
||||
|
||||
// If we have a specific type, then add the hint to the full hint list as well. This will
|
||||
// allow us to quickly look up the pass-through data type hint during serialization.
|
||||
// --
|
||||
// NOTE: The reason we don't want to pass through "any" is that we want parent types to be
|
||||
// able to "fall through" during the object traversal. If we added "any" to the type list,
|
||||
// then it would always overwrite the parent data type.
|
||||
if ( hint != "any" ) {
|
||||
|
||||
fullHintList[ key ] = hint;
|
||||
|
||||
}
|
||||
|
||||
// Return this reference for method chaining.
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I walk the given object, writing the serialized value to the output (which is expected to
|
||||
// be a content buffer).
|
||||
// ---
|
||||
// NOTE: THIS METHOD IS HUGE - this is on purpose. Since serialization is a rather intense
|
||||
// process, I am trying to cut out as much overhead as possible. In this case, we're cutting
|
||||
// out extra stack space by inlining and duplicating a lot of functionality. This is being done
|
||||
// at the COST of clarity and non-repetitive code.
|
||||
private void function serializeInput(
|
||||
required any input,
|
||||
required string hint
|
||||
) {
|
||||
|
||||
// Serialize the data base on the type of input. We are organizing this in terms of the
|
||||
// most commonly-used values first. The anticipation is that the vast majority of data
|
||||
// types will be simple values.
|
||||
if ( isSimpleValue( input ) ) {
|
||||
|
||||
if ( ( hint == "string" ) || ( hint == "any" ) ) {
|
||||
|
||||
// If the string appears to be numeric, then we have to prefix it to make sure
|
||||
// ColdFusion doesn't accidentally convert it to a number.
|
||||
if ( isNumeric( input ) ) {
|
||||
|
||||
writeOutput( """" & input & """" );
|
||||
|
||||
} else {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
} else if ( ( hint == "boolean" ) && isBoolean( input ) ) {
|
||||
|
||||
writeOutput( input ? "true" : "false" );
|
||||
|
||||
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isNumeric( input ) ) {
|
||||
|
||||
writeOutput( input );
|
||||
|
||||
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isBoolean( input ) ) {
|
||||
|
||||
writeOutput( input ? "1" : "0" );
|
||||
|
||||
} else if ( ( hint == "date" ) && ( isDate( input ) || isNumericDate( input ) ) ) {
|
||||
|
||||
// Write the date in ISO 8601 time string format. We're going to assume that the
|
||||
// date is already in the desired timezone.
|
||||
///writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.l" ) & "Z""" );
|
||||
//2024-06-27 modified by msyu: added server timezone
|
||||
writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.lXX" ) & """" );
|
||||
|
||||
} else {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} // END: isSimpleValue().
|
||||
|
||||
|
||||
// I'm expecting the struct to be the next most common data type since it will likely be
|
||||
// the container for the majority of data values.
|
||||
if ( isStruct( input ) ) {
|
||||
|
||||
writeOutput( "{" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
for ( var key in input ) {
|
||||
|
||||
// Skip any black-listed keys.
|
||||
if ( structKeyExists( blockedKeyList, key ) ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
// Handle the item delimiter.
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
// Ensure that the given key can be referenced on the full-key list. This way,
|
||||
// the subsequent logic will be easier.
|
||||
if ( ! structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
asAny( lcase( key ) );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( fullKeyList[ key ] & ":" );
|
||||
|
||||
// Pass in the most appropriate data-type hint based on the parent key.
|
||||
if ( structKeyExists( fullHintList, key ) ) {
|
||||
|
||||
serializeInput( input[ key ], fullHintList[ key ] );
|
||||
|
||||
// If the given key is unknown, just pass through the most recent hint as
|
||||
// it may be defining the type for an entire structure.
|
||||
} else {
|
||||
|
||||
serializeInput( input[ key ], hint );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "}" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isStruct().
|
||||
|
||||
|
||||
if ( isArray( input ) ) {
|
||||
|
||||
writeOutput( "[" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
// Handle the item delimiter.
|
||||
for ( var value in input ) {
|
||||
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
// Since we don't have a key to go off of, pass-through the most recent hint.
|
||||
serializeInput( value, hint );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "]" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isArray().
|
||||
|
||||
|
||||
// When we serialize a query, we're going to treat it like an array of structs.
|
||||
if ( isQuery( input ) ) {
|
||||
|
||||
var keys = listToArray( input.columnList );
|
||||
|
||||
// Make sure each column is defined as a known key - makes the subsequent logic easier.
|
||||
for ( var key in keys ) {
|
||||
|
||||
if ( ! structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
asAny( lcase( key ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "[" );
|
||||
|
||||
// Serialize each row of the query as a struct.
|
||||
for ( var i = 1 ; i <= input.recordCount ; i++ ) {
|
||||
|
||||
// Handle the row delimiter.
|
||||
if ( i > 1 ) {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "{" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
for ( var key in keys ) {
|
||||
|
||||
// Skip any black-listed keys.
|
||||
if ( structKeyExists( blockedKeyList, key ) ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
// Handle the item delimiter (in the current row).
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( fullKeyList[ key ] & ":" );
|
||||
|
||||
// Pass in the most appropriate data-type hint based on the parent key.
|
||||
if ( structKeyExists( fullHintList, key ) ) {
|
||||
|
||||
serializeInput( input[ key ][ i ], fullHintList[ key ] );
|
||||
|
||||
// If the given key is unknown, just pass through the most recent hint as
|
||||
// it may be defining the type for an entire structure.
|
||||
} else {
|
||||
|
||||
serializeInput( input[ key ][ i ], hint );
|
||||
|
||||
}
|
||||
|
||||
} // END: Key list.
|
||||
|
||||
writeOutput( "}" );
|
||||
|
||||
} // END: Row list.
|
||||
|
||||
writeOutput( "]" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isQuery().
|
||||
|
||||
|
||||
// If we made it this far, we were given a data type that we're not actively supporting.
|
||||
// As such, we just have to hand this off to the native serializer.
|
||||
writeOutput( serializeJson( input ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize and write the given string to the current output context, escaping all appropriate
|
||||
* characters for the JSON specification.
|
||||
*
|
||||
* NOTE: We are using this manual-encoding process rather than the built-in serializeJson() function
|
||||
* as there is a rather nasty bug that corrupts certain patterns in the output. Read more:
|
||||
*
|
||||
* http://www.bennadel.com/blog/2842-serializejson-and-the-input-and-output-encodings-are-not-same-errors-in-coldfusion.htm
|
||||
*
|
||||
* @input I am the string being serialized.
|
||||
*/
|
||||
private void function serializeInputString( required string input ) {
|
||||
|
||||
// While this may not be technically needed, this will ensure that we are not using any
|
||||
// "undocumented features" of the language. If we explicitly cast to a Java string, and
|
||||
// something goes wrong due to odd type-casting, it's a ColdFusion bug, at that point, not
|
||||
// a logic error ;)
|
||||
input = javaCast( "string", input );
|
||||
|
||||
var length = input.length();
|
||||
|
||||
writeOutput( """" );
|
||||
|
||||
for ( var i = 1 ; i <= length ; i++ ) {
|
||||
|
||||
var charCode = input.codePointAt( javaCast( "int", i - 1 ) );
|
||||
|
||||
// Check for the most common case first (normal characters).
|
||||
if (
|
||||
( charCode >= 32 ) &&
|
||||
( charCode != 34 ) &&
|
||||
( charCode != 47 ) &&
|
||||
( charCode != 92 ) &&
|
||||
( charCode != 8232 ) &&
|
||||
( charCode != 8233 )
|
||||
) {
|
||||
|
||||
writeOutput( chr( charCode ) );
|
||||
|
||||
// Check for the special cases next (control characters, characters that
|
||||
// need to be escaped, and characters that need to be encoded nicely).
|
||||
} else if ( charCode == 8 ) {
|
||||
|
||||
writeOutput( "\b" );
|
||||
|
||||
} else if ( charCode == 9 ) {
|
||||
|
||||
writeOutput( "\t" );
|
||||
|
||||
} else if ( charCode == 10 ) {
|
||||
|
||||
writeOutput( "\n" );
|
||||
|
||||
} else if ( charCode == 12 ) {
|
||||
|
||||
writeOutput( "\f" );
|
||||
|
||||
} else if ( charCode == 13 ) {
|
||||
|
||||
writeOutput( "\r" );
|
||||
|
||||
} else if (
|
||||
( charCode < 32 ) ||
|
||||
( charCode == 8232 ) ||
|
||||
( charCode == 8233 )
|
||||
) {
|
||||
|
||||
// For Unicode hex values, we need to enforce a 4-digit code.
|
||||
writeOutput( "\u" & right( ( "000" & formatBaseN( charCode, 16 ) ), 4 ) );
|
||||
|
||||
} else if ( charCode == 34 ) {
|
||||
|
||||
writeOutput( "\""" );
|
||||
|
||||
} else if ( charCode == 47 ) {
|
||||
|
||||
writeOutput( "\/" );
|
||||
|
||||
} else if ( charCode == 92 ) {
|
||||
|
||||
writeOutput( "\\" );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( """" );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize and return the given string, escaping all appropriate characters for the
|
||||
* JSON specification.
|
||||
*
|
||||
* @input I am the string being serialized.
|
||||
* @output false
|
||||
*/
|
||||
private string function serializeString( required string input ) {
|
||||
|
||||
savecontent variable = "local.json" {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
return( json );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user