Exploring Linked / Ordered Structs In Lucee 5.3.2.77
The Struct data-type in ColdFusion is an "unordered" collection of key-value pairs. Anecdotally, if you iterate over the keys in a Struct, they are returned in alphabetical order. However, this ordering is not guaranteed; and, you should not depend upon this ordering in your logic. That said, if the ordering of your Struct keys is important, you can use an "ordered" or "linked" Struct in Lucee 5.3.2.77.
There are two ways to create an Ordered, or Linked, Struct in Lucee CFML. The first way is to pass the type of struct into the structNew()
function:
structNew( "linked" )
And, the second way is to use brackets - instead of braces - to delimit your Struct literal notation:
[ key: value, key: value ]
When creating a Struct in one of these two ways, the order of the keys in the Struct will be iterated in the same order in which they were inserted. To see this in action, we can create different kinds of structs and insert their keys in reverse alphabetical order:
<cfscript>
// As a control, let's see what happens when we insert keys in a descending
// alphabetical order using a struct literal. When output, they _should_ be listed in
// ascending order.
data = {
zomething: "different",
hello: "world",
foo: "bar",
anna: "banana"
};
dump( label = "Struct Literal - {}", var = data );
echo( "<br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Lucee allows us to define an ORDERED struct literal by mixing Array and Struct
// notation. Notice that the enclosing syntax uses brackets; but, that the internal
// key-value pairs are using the normal struct syntax.
orderedData = [
zomething: "different",
hello: "world",
foo: "bar",
anna: "banana"
];
dump( label = "Ordered Struct Literal - []", var = orderedData );
echo( "<br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Lucee also offers the option to define a Type of struct when using structNew().
// In this case we are creating a "linked" (ordered) struct that will record the
// order in which the keys are subsequently inserted.
orderedData2 = structNew( "linked" );
orderedData2.zomething = "different";
orderedData2.hello = "world";
orderedData2.foo = "bar";
orderedData2.anna = "banana";
dump( label = "StructNew( 'linked' )", var = orderedData2 );
</cfscript>
As you can see, we're creating one "normal" struct as our control case; and then, we're creating two ordered, or linked, structs using the alternate options. And, when we run this Lucee CFML code, we get the following output:
As you can see, the control struct (using the "normal" notation) outputs its keys in a non-guaranteed order (though, anecdotally, its alphabetical). And, the linked, or ordered, structs output their keys in the same order in which they were inserted.
In the vast majority of cases, I don't need a Struct to be ordered. Usually, a Struct is just a bag of properties that I can reference by name instead of by index. As such, I don't have a great instinct for where this feature would be most helpful. Perhaps one use-case for linked Structs is in the generation of secure signatures.
When making an HTTP request to a secure end-point, the caller needs to pass along a signature that can be used, by the consumer, to verify that the request was not tampered with. In such cases, the order of the signature inputs is critical. Misalignment, between the producer and the consumer, on what is being signed could lead to inconsistent signatures and rejected requests.
In such cases, we could use an ordered Struct to ensure that the request properties are provided to the signing function in a predictable order. Let's look at an example:
<cfscript>
signature = generateSignature(
"PUT",
"https://some-end-point",
// NOTE: When passing the end-point parameters into the signer, I am using an
// ORDERED STRUCT literal. This way, the order of the parameters, as applied to
// the underlying message, will be predictable.
[
d: "dee",
c: "see",
z: "zeta",
a: "anna"
]
);
echo( "Message Signature: #signature.lcase()# <br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I generate a signature for the request to the secure end-point.
*
* @method I am the HTTP method being used.
* @scriptName I am the end-point being accessed.
* @parameters I am the collection of ORDERED key-value pairs being passed through.
*/
public string function generateSignature(
required string method,
required string scriptName,
required struct parameters
) {
// CAUTION: Since the parameters are assumed to be an ORDERED / LINKED struct,
// the keys can be iterated-over in a predictable, repeatable manner.
var pairs = parameters
.keyArray()
.map(
( key ) => {
return( "#key.lcase()#=#parameters[ key ]#" );
}
)
.toList( "&" )
;
var message = "#method.ucase()#:#scriptName#?#pairs#";
var secretKey = "tot3sMaGoate$";
// For debugging the demo, log pre-signature message.
systemOutput( "Signing: #message#", true );
return( hmac( message, secretKey, "hmacSha256" ) );
}
</cfscript>
As you can see, one of the arguments that the generateSignature()
function accepts is a collection of query-string parameters. And, in this case, we're going to use an ordered struct to ensure that the underlying message is created using the exact order of the provided key-value pairs.
Now, when we run the above Lucee ColdFusion code, we get the following system-output of the pre-signature message:
Signing: PUT:https://some-end-point?d=dee&c=see&z=zeta&a=anna
As you can see, the set of parameters is added to the secure message in the same, predictable order in which it was passed to the generateSignature()
function.
Of course, you could argue that this now becomes a very platform-specific implementation. But, that's an entirely different type of conversation.
A Struct, by any other name, is just a bag of key-value pairs. And, in the vast majority of cases, this is completely sufficient. However, there are uses-cases in which it is helpful to be able to iterate over a struct in a predictable manner. In such cases, it is nice to know that Lucee CFML 5.3.2.77 provides an ordered, or linked, struct implementation.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Once place where the ordered struct will really shine is in the communication with external systems like MongoDB:
www.bennadel.com/blog/3670-ordered-structs-are-perfect-for-creating-mongodb-bson-documents-in-lucee-5-3-2-77.htm
Since MongoDB is highly dependent on the predictable iteration of a BJSON document, the ordered, or linked, struct in Lucee makes it perfect for communication with MongoDB.
This is very exciting! Historically, communicating with MongoDB has been a janky nightmare of hand-crafted JSON strings that have to be parsed by a MongoDB utility class. So janky! This is going to be so much nicer!
Excellent.
Can you tell me whether this is ACF11+ compatible?
I have a feeling the answer is no? In which case, we can fall back on:
However, there is one small caveat. I have found, in CF11, that dumping a linked Hash map does not preserve the key order. This can be the source of some confusion. My advice when debugging with a linked Hash map is to just loop the struct out using something like:
I like to use ordered structs when returning API data. Sure a programmatic consumer won't care about the order, but for developers familiarizing themselves with the returned data by looking at the json I think it helps to present it in a logical way, e.g. having the
id
value first.@Charles,
I believe ACF has a different word. I think it might be
"ordered"
instead of"linked"
. Not sure. And, I don't believe that they have any struct-literal notation for the Lucee feature.@Julian,
Ah, that's an interesting idea. And, it also jives with what I would consider talking to an "external system", which is where ordering may become more relevant. I am OK with this :D