Last active
March 31, 2018 00:41
-
-
Save ryanguill/ac2201f9a13feb9ec07e4ed1aaade82e to your computer and use it in GitHub Desktop.
An implementation of Option and Result in cfml as a closure. Works on ACF 10, 11 and 2016, Lucee 4.5 and 5.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfscript> | |
function Option () { | |
var NIL = "__NIL__"; | |
var _isNil = function (input) { | |
return (isNull(input) || (isSimpleValue(input) && input == Nil)); | |
}; | |
var _isSome = false; | |
var _val = NIL; | |
var m = { | |
some: function (val) { | |
if (_isNil(val)) { | |
throw("value must be non-null"); | |
} | |
_val = arguments.val; | |
_isSome = true; | |
return m; | |
}, | |
none: function () { | |
_isSome = false; | |
_val = NIL; | |
return m; | |
}, | |
of: function (val) { | |
if (_isNil(val)) { | |
return m.none(); | |
} | |
return m.some(arguments.val); | |
}, | |
isSome: function () { | |
return _isSome; | |
}, | |
isNone: function () { | |
return !_isSome; | |
}, | |
toString: function () { | |
if (_isSome) { | |
return 'Some( ' & _val & ' )'; | |
} | |
return 'None()'; | |
}, | |
unwrap: function () { | |
if (_isSome) { | |
return _val; | |
} | |
throw("Cannot unwrap a none.") | |
}, | |
unwrapOr: function (required other) { | |
if (_isSome) { | |
return _val; | |
} | |
return other; | |
}, | |
unwrapOrElse: function (required fn) { | |
if(_isSome) { | |
return _val; | |
} | |
return fn(); | |
}, | |
map: function (required fn) hint="returns an option" { | |
if (_isSome) { | |
return Option().of(fn(_val)); | |
} | |
return Option().none(); | |
}, | |
filter: function (required conditionFn) hint="returns an option" { | |
if (_isSome) { | |
if (conditionFn(_val)) { | |
return m; | |
} | |
} | |
return Option().none(); | |
}, | |
forEach: function (required fn) hint="for side effects" { | |
if (_isSome) { | |
fn(_val); | |
} | |
}, | |
match: function (struct options = {}) hint="pass a struct with two keys with functions for values, `some` and `none`" { | |
if (_isSome) { | |
if (!structKeyExists(options, "some")) { | |
return m.toString(); | |
} | |
return options.some(_val); | |
} | |
if (!structKeyExists(options, "none")) { | |
return m.toString(); | |
} | |
return options.none(); | |
} | |
}; | |
m.get = m.unwrap; | |
m.getOr = m.unwrapOr; | |
m.getOrElse = m.unwrapOrElse; | |
return m; | |
} | |
function Result () { | |
var _isOk = false; | |
var _okVal = ""; | |
var _errVal = ""; | |
var r = { | |
ok: function (okVal) { | |
_isOk = true; | |
_okVal = okVal; | |
return r; | |
}, | |
err: function (errVal) { | |
_isOk = false; | |
_errVal = errVal; | |
return r; | |
}, | |
isOk: function () { | |
return _isOk; | |
}, | |
isErr: function () { | |
return !_isOk; | |
}, | |
toString: function () { | |
if (_isOk) { | |
return "Ok( " & _okVal & " )"; | |
} | |
return "Err( " & _errVal & " )"; | |
}, | |
getOk: function () { | |
if (_isOk) { | |
return Option().some(_okVal); | |
} | |
return Option().none(); | |
}, | |
getErr: function () { | |
if (!_isOk) { | |
return Option().some(_errVal); | |
} | |
return Option().none(); | |
}, | |
unwrap: function () { | |
if (_isOk) { | |
return _okVal; | |
} | |
throw("Called unwrap on a Result.Err"); | |
}, | |
unwrapErr: function () { | |
if (_isOk) { | |
throw("Called unwrapErr on a Result.Ok"); | |
} | |
return _errVal; | |
}, | |
unwrapOr: function (required other) { | |
if (_isOk) { | |
return _okVal; | |
} | |
return other; | |
}, | |
unwrapOrElse: function (required fn) { | |
if (_isOk) { | |
return _okVal; | |
} | |
return fn(_errVal); | |
}, | |
map: function (required fn) hint="returns a Result" { | |
/* | |
if isOk, transform the value, otherwise no-op | |
*/ | |
if (_isOk) { | |
return Result().ok(fn(_okVal)); | |
} | |
return r; | |
}, | |
mapErr: function (required fn) hint="returns a Result" { | |
/* | |
if isErr, transform the value, otherwise no-op | |
*/ | |
if (_isOk) { | |
return r; | |
} | |
return Result().err(fn(_errVal)); | |
}, | |
match: function (struct options = {}) hint="pass a struct with two keys with functions for values, `ok` and `err`" { | |
if (_isOk) { | |
if (!structKeyExists(options, "ok")) { | |
return r.toString(); | |
} | |
return options.ok(_okVal); | |
} | |
if (!structKeyExists(options, "err")) { | |
return r.toString(); | |
} | |
return options.err(_errVal); | |
} | |
}; | |
return r; | |
} | |
t = testSuite("Maybe()"); | |
expect = t.expect; | |
some1 = Option().some(1); | |
expectASome(some1, 1); | |
of1 = Option().of(1); | |
expectASome(of1, 1); | |
none1 = Option().none(); | |
expectANone(none1); | |
writeOutput(t.printResults()); | |
/* ========== */ | |
writeOutput("<hr />"); | |
t = testSuite("Result()"); | |
expect = t.expect; | |
ok1 = Result().ok(1); | |
expectAnOk(ok1, 1); | |
err2 = Result().err(2); | |
expectAnErr(err2, 2); | |
writeOutput(t.printResults()); | |
/* ========== */ | |
writeOutput("<hr />"); | |
/* function to test some and none */ | |
private function expectASome(option, expectedValue) { | |
expect(option.isSome(), true, "isSome() on a some is true"); | |
expect(option.isNone(), false, "isNone() on a none is false"); | |
expect(option.unwrap(), expectedValue, "unwrap() on a some gives the value"); | |
expect(option.unwrapOr(2), expectedValue, "unwrapOr() on a some gives the value"); | |
expect(option.unwrapOrElse(function() { | |
return 3; | |
}), expectedValue, "unwrapOrElse() on a some gives the value"); | |
expect(option.get(), expectedValue, "get() on a some gives the value"); | |
expect(option.getOr(2), expectedValue, "getOr() on a some gives the value"); | |
expect(option.getOrElse(function() { | |
return 3; | |
}), expectedValue, "getOrElse() on a some gives the value"); | |
expect(option.toString(), 'Some( ' & expectedValue & ' )', "toString on a some gives the value wrapped in a Some() label"); | |
expect(option.match({ | |
some: function (value) { | |
return 'was a some'; | |
}, none: function () { | |
return 'was a none'; | |
} | |
}), 'was a some', "match on a some returns the value of the some: function"); | |
expect(option.match(), option.toString(), "match on a some without a some function returns the same as toString()"); | |
if (isNull(arguments.secondPass)) { | |
var mapped = option.map(function (value) { | |
return 100; | |
}); | |
expectASome(option=mapped, expectedValue=100, secondPass=true); | |
var filtered = option.filter(function (value) { | |
return true; | |
}); | |
expectASome(option=filtered, expectedValue=expectedValue, secondPass=true); | |
var filtered = option.filter(function (value) { | |
return false; | |
}); | |
expectANone(option=filtered, secondPass=true); | |
var sideEffect = ""; | |
option.forEach(function (value) { | |
sideEffect = value; | |
}); | |
expect(sideEffect, expectedValue, "forEach on a some should execute the function."); | |
} | |
} | |
private function expectANone(option) { | |
expect(option.isSome(), false, "isSome() on a none is true"); | |
expect(option.isNone(), true, "isNone() on a none is false"); | |
var didThrow = false; | |
try { | |
option.unwrap(); | |
} catch (any e) { | |
didThrow = true; | |
} | |
expect(didThrow, true, "unwrap() on a none throws an error"); | |
expect(option.unwrapOr(2), 2, "unwrapOr() on a none gives the passed value"); | |
expect(option.unwrapOrElse(function() { | |
return 3; | |
}), 3, "unwrapOrElse() on a none gives the value of the passed function"); | |
var didThrow = false; | |
try { | |
option.get(); | |
} catch (any e) { | |
didThrow = true; | |
} | |
expect(didThrow, true, "get() on a none throws an error"); | |
expect(option.getOr(2), 2, "getOr() on a none gives the passed value"); | |
expect(option.getOrElse(function() { | |
return 3; | |
}), 3, "getOrElse() on a none gives the value of the passed function"); | |
expect(option.toString(), 'None()', "toString on a none returns `None()`"); | |
expect(option.match({ | |
some: function (value) { | |
return 'was a some'; | |
}, none: function () { | |
return 'was a none'; | |
} | |
}), 'was a none', "match on a none returns the value of the none: function"); | |
expect(option.match(), option.toString(), "match on a none without a none function returns the same as toString()"); | |
if (isNull(arguments.secondPass)) { | |
var mapped = option.map(function (value) { | |
return 100; | |
}); | |
expectaNone(option=mapped, secondPass=true); | |
var filtered = option.filter(function (value) { | |
return true; | |
}); | |
expectANone(option=filtered, secondPass=true); | |
var filtered = option.filter(function (value) { | |
return false; | |
}); | |
expectANone(option=filtered, secondPass=true); | |
var sideEffect = ""; | |
option.forEach(function (value) { | |
sideEffect = value; | |
}); | |
expect(sideEffect, "", "forEach on a none should _not_ execute the function."); | |
} | |
} | |
private function expectAnOk (result, expectedOkValue) { | |
expect(result.isOk(), true, "isOk on an ok should be true"); | |
expect(result.isErr(), false, "isErr on an ok should be false"); | |
expect(result.toString(), "Ok( " & expectedOkValue & " )", "toString() on an ok should return the value wrapped in an Ok() label"); | |
expect(result.getOk().isSome(), true, "getOk() on an ok should return a Maybe.some"); | |
expect(result.getOk().unwrap(), expectedOkValue, "getOk() should return a Maybe that when upwrapped returns the expected value"); | |
expect(result.getErr().isSome(), false, "getErr() on an ok should return a Maybe.none"); | |
expect(result.unwrap(), expectedOkValue, "unwrap on an Ok should return the wrapped value"); | |
var didThrow = false; | |
try { | |
result.unwrapErr(); | |
} catch (any e) { | |
didThrow = true; | |
} | |
expect(didThrow, true, "unwrapErr() on an Ok should throw an error"); | |
expect(result.unwrapOrElse(function (err) { | |
return err; | |
}), expectedOkValue, "unwrapOrElse() on an Ok should return the ok value"); | |
if (isNull(arguments.secondPass)) { | |
var mapped = result.map(function (x) { | |
return "mappedOkValue"; | |
}); | |
expect(mapped.unwrap(), "mappedOkValue", "mapping an Ok should apply the function"); | |
expectAnOk(result=mapped, expectedOkValue="mappedOkValue", secondPass=true); | |
var mappedErr = result.mapErr(function(x) { | |
return "mappedErrValue"; | |
}); | |
expect(mappedErr.unwrap(), expectedOkValue, "mapErr an Ok should not apply the function"); | |
expectAnOk(result=mappedErr, expectedOkValue=expectedOkValue, secondPass=true); | |
} | |
expect(result.match({ | |
ok: function (okVal) { | |
return 'was an ok'; | |
}, | |
err: function (errVal) { | |
return 'was an err'; | |
} | |
}), 'was an ok', "match on a ok returns the value of the ok: function"); | |
expect(result.match(), result.toString(), "match on a ok without a ok function returns the same as toString()"); | |
} | |
private function expectAnErr (result, expectedErrValue) { | |
expect(result.isOk(), false, "isOk on an err should be false"); | |
expect(result.isErr(), true, "isErr on an err should be true"); | |
expect(result.toString(), "Err( " & expectedErrValue & " )", "toString() on an err should return the value wrapped in an Err() label"); | |
expect(result.getOk().isSome(), false, "getOk() on an err should return a Maybe.none"); | |
expect(result.getErr().isSome(), true, "getErr() on an err should return a Maybe.some"); | |
expect(result.getErr().unwrap(), expectedErrValue, "getErr() should return a Maybe that when upwrapped returns the expected value"); | |
expect(result.unwrapErr(), expectedErrValue, "unwrap on an Err should return the wrapped value"); | |
var didThrow = false; | |
try { | |
result.unwrap(); | |
} catch (any e) { | |
didThrow = true; | |
} | |
expect(didThrow, true, "unwrap() on an Err should throw an error"); | |
expect(result.unwrapOrElse(function (err) { | |
return "foo"; | |
}), "foo", "unwrapOrElse() on an Err should return the mapped Err value"); | |
if (isNull(arguments.secondPass)) { | |
var mapped = result.map(function (x) { | |
return "mappedOkValue"; | |
}); | |
expect(mapped.unwrapErr(), expectedErrValue, "mapping an Err should not apply the function"); | |
expectAnErr(result=mapped, expectedErrValue=expectedErrValue, secondPass=true); | |
var mappedErr = result.mapErr(function(x) { | |
return "mappedErrValue"; | |
}); | |
expect(mappedErr.unwrapErr(), "mappedErrValue", "mapErr on an Err should apply the function"); | |
expectAnErr(result=mappedErr, expectedErrValue="mappedErrValue", secondPass=true); | |
} | |
expect(result.match({ | |
ok: function (okVal) { | |
return 'was an ok'; | |
}, | |
err: function (errVal) { | |
return 'was an err'; | |
} | |
}), 'was an err', "match on a err returns the value of the err: function"); | |
expect(result.match(), result.toString(), "match on a err without a err function returns the same as toString()"); | |
} | |
/* test suite */ | |
private function testSuite (label, supressPassMessages = true) { | |
var _results = { | |
pass: 0, | |
fail: 0, | |
failures: [] | |
}; | |
var t = { | |
expect: function (required any testValue, required any targetValue, string message = "", any dumpVar) { | |
if (arguments.testValue != arguments.targetValue) { | |
arguments.message &= "<br /> expected [" & encodeForHtml(toString(arguments.targetValue)) & "] <br /> but received [" & encodeForHtml(toString(testValue)) & "] <br />"; | |
var cs = callStackGet(); | |
var lineRef = ""; | |
var template = ""; | |
var lineNumber = ""; | |
for (var line in cs) { | |
if (structKeyExists(line, "Function") && line["Function"] == getFunctionCalledName()) { | |
continue; | |
} | |
if (structKeyExists(line, "Function") && findNoCase("closure", line["Function"])) { | |
continue; | |
} | |
if (findNoCase("trycf", cgi.SERVER_NAME)) { | |
lineRef = lineRef & "<br />" & "line"; | |
} else { | |
if (structKeyExists(line, "Template")) { | |
lineRef = lineRef & "<br />" & line["Template"]; | |
template = line["template"]; | |
} | |
} | |
if (structKeyExists(line, "LineNumber")) { | |
lineRef = lineRef & ":" & line["LineNumber"]; | |
lineNumber = line["lineNumber"]; | |
} | |
break; | |
} | |
_results.fail += 1; | |
arrayAppend(_results.failures, arguments.message & trim(lineRef)); | |
writeOutput('<pre style="font-weight:bold; color:red;">Fail! ' & arguments.message & trim(lineRef) & '</pre><br />'); | |
} else { | |
_results.pass += 1; | |
if (!supressPassMessages) { | |
writeoutput('<pre style="color:green;">Pass: #message#</pre>'); | |
} | |
} | |
}, | |
getResults: function () { | |
return _results; | |
}, | |
printResults: function () { | |
return label & ' results: <span style="color:green;">' & _results.pass & ' passed</span>, <span style="font-weight:bold; color:red;">' & _results.fail & ' failed.</span>'; | |
} | |
}; | |
return t; | |
} | |
</cfscript> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment