I wasn't gonna write anything today, but then Jessica on Slack (to use her full name) posted a code puzzle on the CFML Slack channel, which I caught whilst I was sardined in a train to Ilford en route home. I nutted out a coupla solutions in my head instead of reading my book, and I saw this as a good opportunity (read: "excuse") to pop down to the local once I got home and squared away a coupla things, and key the code in and see if it worked. I was moderately pleased with the results, and I think I've solved it in an interesting way, so am gonna reproduce here.
Jessica's problem was thus:
so, i am attempting to write a function that lets you set variables specifically in a complex structure [...]
cached:{ foo: { bar: "hi" } } setProperty("foo.bar", "chicken"); writeDump(cached); // should == cached.foo.bar = chicken
On the Slack channel there was talk of loops and recursion and that sort of thing, which all sounded fine (other people came up with answers, but I purposely did not look at them lest they influenced my own efforts). The more I work with CFML and its iteration methods (
map()
, reduce()
, etc), the more I think actually having to loop over something seems a bit primitive, and non-descriptive. I looked at this for a few minutes... [furrowed my brow]... and thought "you could reduce that dotted path to a reference to the substruct I reckon". There were a few challenges there - if CFML had proper references it'd be easier - but I got an idea of the code in my head, and it seemed nice and easy.Whilst waiting to Skype with my boy I wrote my tests:
// TestCache.cfc
component extends=testbox.system.BaseSpec {
function run(){
describe("setProperty() tests", function(){
it("fulfils the specified requirement", function(){
var cache = new Cache({foo={bar="hi"}});
cache.setProperty("foo.bar", "chicken");
var result = cache.cached.foo.bar;
expect(result).toBe("chicken");
});
it("works with a deeper struct", function(){
var original = {
first = {
second = {
third = ""
}
}
};
var value = "fourth";
var expected = duplicate(original);
expected.first.second.third = value;
var cache = new Cache(original);
cache.setProperty("first.second.third", value);
var result = cache.cached;
expect(result).toBe(expected);
});
it("doesn't interfere with adjacent data", function(){
var original = {
first = {
second = {
third = "",
thirdBis = "thirdBis"
},
secondBis = "secondBis"
},
firstBis = "firstBis"
};
var value = "fourth";
var expected = duplicate(original);
expected.first.second.third = value;
var cache = new Cache(original);
cache.setProperty("first.second.third", value);
var result = cache.cached;
expect(result).toBe(expected);
});
});
}
}
This is pretty superficial, but it's a good starting point. First up: it needs to actually do exactly what Jessica needs. There's no point doing anything else if it doesn't do that. Next I checked its scalability (logic-wise), and also that it doesn't mess with anything it's not supposed to. That's a start.
Obviously when I run these they just faceplant, as I don't even have the Cache class yet.
Next, I knock together the rest of the supporting code in the Cache class. The stuff the the method I'm testing needs to even be able to run:
// Cache.cfc
component {
this.cached = {};
function init(base){
this.cached = base;
}
function setProperty(path, value){
}
}
Nor running the tests doesn't faceplant entirely, they just fail. Excellent. We're ready to code.
This is the function I ended up with. This requires ColdFusion 11 or Lucee 4.5, btw:
function setProperty(path, value){
var d = ".";
var key = path.listLast(d);
var keyIndex = path.listLen(d);
var obj = path.listDeleteAt(keyIndex, d).listReduce(function(obj, key){
return obj[key];
}, this.cached, d);
obj[key] = value;
}
My idea is that I can use
reduce()
to sequentially reference deeper and deeper into the struct, one key element at a time. Because CFML doesn't do true references (possibly not the correct term: I mean two variables that point to exactly the same memory), I can't use this technique to drill right down to the value I need to change, I need to offset by one: I need a struct and a key to reference the value by. This means I need to grab the last level of the path to be the key, then drill down through the rest of the path to drill through the struct. Each iteration of the reduce()
receives a struct, and a key, and return thatStruct[thatKey]
... and the next iteration receives that, as well as the next key in the path. Cool.This kind of implements recursion even without a loop (obviously
reduce()
itself is iterating under the hood, but there's no loop or recursion in my own code here).Once I've got the bottom level object, all I need to do is to assign the key of that object the passed-in value.
Pleasingly, the first iteration of this function passed all the tests! All I did from there is to factor out the list delimiter into a variable, to save some clutter.
So that's the "cool" answer, and I flicked that to Jessica.
Then I did the cheeky answer.
All one really needs to do is this:
function setProperty(path, value){
evaluate("this.cached.#path# = value");
}
Don't write off
evaluate()
. It has its place.Update:
Ha! One can even just do this:function setProperty(path, value){
"this.cached.#path# = value;
}
I was using Lucee for a lot of this, and mis-remembered something about Lucee having a CFML incompat with strings holding dotted paths, but it's not the case here. So the above works fine in both ColdFusion and Lucee.
Right so Jessica had another requirement:
so, if i set up a contract of... say:OK, fair enough. BTW Jessica... I had Ben Nadel up for objectifying females in his blog years ago, so I'll now have you up for objectifying males in... um... my blog. Oops.
cute_boys: { eyes: "", hair: "", skillz: { hasNunchuck: 0, hasCheezBurger: 0 } }
I want to make sure that the `skillz` don't get overrun with lots of extra skills i didn't say i wanted in that structure
This was an interesting one, and I was able to leverage another collection iteration method to help me with this. Note: this code only works on Lucee. ColdFusion doesn't support two of the things I'm doing here.
First the tests:
describe("protected elements tests", function(){
it("protects the bottom element", function(){
expect(function(){
var cache = new Cache({foo={bar="hi"}});
cache.protected = ["bar"];
cache.setProperty("foo.bar", "chicken");
}).toThrow("ProtectionException");
});
it("protects the top element", function(){
expect(function(){
var cache = new Cache({foo={bar="hi"}});
cache.protected = ["foo"];
cache.setProperty("foo.bar", "chicken");
}).toThrow("ProtectionException");
});
it("protects multiple elements", function(){
var original = {
top = {
block1 = {
key = "value"
},
middle = {
block2 = {
lower = {
key = "value"
}
}
}
}
};
var value = "updated";
var cache = new Cache(original);
cache.protected = ["block1", "block2"]
expect(function(){
cache.setProperty("top.block1.key", "updated");
}).toThrow("ProtectionException");
expect(function(){
cache.setProperty("top.middle.block2.lower.key", "updated");
}).toThrow("ProtectionException");
});
it("doesn't interfere with non-protected elememts", function(){
expect(function(){
var cache = new Cache({foo={bar="hi"}});
cache.protected = ["baz"];
cache.setProperty("foo.bar", "chicken");
}).notToThrow("ProtectionException");
});
});
One cool thing (well: cool to me) in this lot is that I can check for two different exception cases in one test. I love the way TestBox hierarchicalises (?) its tests.
So here we're testing one protected path element, and two different protected path elements. And that the new protection features don't interfere with existing operations.
And the revised implementation:
// Cache.cfc
component {
d = ".";
this.cached = {};
this.protected = [];
function init(base){
this.cached = base;
}
function setProperty(path, value){
protect(path);
var key = path.listLast(d);
var keyIndex = path.listLen(d);
var obj = path.listDeleteAt(keyIndex, d).listReduce(function(obj, key){
return obj[key];
}, this.cached, d);
obj[key] = value;
}
function protect(path){
return this.protected.some(function(element){
return path.listFindNoCase(element, d);
}, d) ? throw(type="ProtectionException") : null;
}
}
- I allow the class to set an array of path elements to protect.
- I check them before I do anything else when calling
setProperty()
. - I use
some()
to check if any of the protected elements match the path... - ... and throw an exception if so;
- otherwise I just return (I only return null because the ?: operator requires a third operand)
I like this code too.
The bits that Lucee does that ColdFusion does not are:
some()
. ColdFusion doesn't have that (4032723);- ColdFusion has ballsed-up their implementation of
throw()
. It looks like a function, but it's actually a statement so it does not return an expression, so cannot be used as an operand (3788414). - Obviously ColdFusion doesn't know what
null
is. How embarrassing.
I commented as much on Twitter just now:
And I'm not tooting my own horn when I say this, but I think this code shows that CFML really has moved on from the "tags for everything" philosophy that has dominated the language (to its detriment) for so many years. I think with ColdFusion 11 and Lucee, CFML has got its act together.One can actually write pretty good-looking, modern code with #CFML. I forget this sometimes.— Adam Cameron (@DAC_dev) August 6, 2015
I quite enjoyed my messing around with code down the pub this evening. Cheers Jessica.
Oh and go join the CFML Slack channel if you have not already. Just click and sign-up:
--
Adam