This is an odd one. I was looking at some Ruby code the other day... well it was CoffeeScript but one of the bits influenced by Ruby, and I was reminded that languages like Ruby and various SQL flavours have - in addition to switch/case constructs - have a case/when construct too. And in Ruby's case it's in the form of an expression. This is pretty cool as one can do this:
myVar = case
when colour == "blue" then
"it's blue"
when number == 1 then
"it's one"
else
"shrug"
end
And depending on the values of
colour
or number
, myVar
will be assigned accordingly. I like this. And think it would be good for CFML. So was gonna raise an E/R for it.But then I wondered... "Cameron, you could probably implement this using 'clever' (for me) use of function expressions, and somehow recursive calls to themselves to... um... well I dunno, but there's a challenge. Do it".
So I set out to write a
case
/when
/then
/else
/end
implementation... as a single UDF. The syntax would be thus:// example.cfm
param name="URL.number" default="";
param name="URL.colour" default="";
include "case.cfm"
result =
case()
.when(URL.number=="tahi")
.then(function(){return "one"})
.when(function(){return URL.colour=="whero"})
.then(function(){return "red"})
.else(function(){return "I dunno what to say"})
.end()
echo(result)
This is obviously not as elegant as the Ruby code, but I can only play the hand I am dealt, so it needs to be in familiar CFML syntax.
Basically the construct is this:
case().when(cond).then(value)
.when(cond).then(value).else(value).end()
Where the condition can be either a boolean value or a function which returns one, and the value is represented as a function (so it's only actually called if it needs to be). And then
when()
/then()
calls can be chained as much as one likes, with only the then()
value for the first preceding when()
condition that is true being processed. Clear? You probably already understood how the construct worked before I tried to explain it. Sorry.Anyway, doing the design for this was greatly helped by using the BDD-flavoured unit tests that TestBox provides. I could just write out my rules, then then infill them with tests after that.
So I started with this lot (below). Just a note: this code is specifically aimed at Railo, because a few things I needed to do simply weren't possible with ColdFusion.
// TestCase.cfc
component extends="testbox.system.BaseSpec" {
function run(){
describe("Tests for case()", function(){
describe("Tests for case() function", function(){
it("compiles when called", function(){})
it("returns when() function", function(){})
});
describe("Tests for when() function", function(){
it("is a function", function(){})
it("requires a condition argument", function(){})
it("accepts a condition argument which is a function", function(){})
it("accepts a condition argument which is a boolean", function(){})
it("rejects a condition argument is neither a function nor a boolean", function(){})
it("returns a struct containing a then() function", function(){})
it("can be chained", function(){
})
it("correctly handles a function returning true as a condition", function(){})
it("correctly handles a function returning false as a condition", function(){})
it("correctly handles a boolean true as a condition", function(){})
it("correctly handles a boolean false as a condition", function(){})
})
describe("Tests for then() function", function(){
it("is a function", function(){})
it("requires a value argument", function(){})
it("requires a value argument which is a function", function(){})
it("returns a struct containing when(), else() and end() functions", function(){})
it("can be chained", function(){})
it("executes the value", function(){})
it("doesn't execute a subsequent value when the condition is already true", function(){})
it("doesn't execute a false condition", function(){})
})
describe("Tests for else() function", function(){
it("is a function", function(){})
it("requires a value argument", function(){})
it("requires a value argument which is a function", function(){})
it("returns a struct containing an end() function", function(){})
it("cannot be chained", function(){})
it("executes when the condition is not already true", function(){})
it("doesn't execute when the condition is already true", function(){})
})
describe("Tests for end() function", function(){
it("is a function", function(){})
it("returns the result", function(){})
it("returns the result of an earlier true condition followed by false conditions", function(){})
it("returns the result of the first true condition", function(){})
})
})
}
}
TestBox is cool in that I can group the sets of tests with nested
describe()
calls. This doesn't impact how the tests are run - well as far as it impacts my intent, anyhow - it just makes for clearer visual output, and also helps me scan down to make sure I've covered all the necessary bases for the intended functionality.I then chipped away at the functionality of each individual sub function, making sure they all worked as I went. I ended up with this test code: