Sometimes it's useful to store function names and parameters in a JSON-like object structure, to allow inclusion of values in that vary, or are otherwise not known until run-time. For example, AWS Cloudformation allows functions prefixed with Fn::
to be included, such as Fn::Select, an example of which might be:
const sourceObject = {
"fruit": { "Fn::Select" : [ "1", [ "apples", "grapes", "oranges", "mangoes" ] ] },
"somethingElse": [
{"Fn::Join" : [ " ", [ "a", "b", "c" ] ]},
"some value"
]
}
const substituted = substituteFunctionsInObject(sourceObject)
// {"fruit": "grapes", "somethingElse":["a b c", "some value"]}
This form of substitution gives some balance between expressive freedom, allowing data to be stored and passed around in pure JSON, and preventing arbitrary code injection. (See for example EVO's answer to How to store a javascript function in JSON for another example, which uses "$" as a function tag instead of "Fn::")
Can anybody suggest a good version of the function that does such a substitution (substituteFunctionsInObject
in the example above)? Perhaps one in an existing library, or one more elegant or less problematic that what I might have thought of.
Such a function should allow specification of arbitrary functions to substitute, and should work recursively (deeply), perhaps allowing function arguments to be generated by additional function calls.
I've written my own implementation based on lodash's transform function, and allowing additional data
to be supplied via curried arguments. This works, but I can't help thinking this problem must have been solved already in a more rigorous way (since AWS, for example, uses it), perhaps using a more elegant functional programming pattern.
const {transform} = require('lodash') // fp.transform is broken
const fp = require('lodash/fp')
const substituteFunctionsInObject = ({
validFunctions = {
"date": x=>new Date(x),
"get": fp.get
},
getFunctionTag = fnName => "$" + fnName
}={}) => function applyFunctionsInObjectInner (obj){
return (...data) => transform(
obj,
(acc, item, key) => {
const firstKey = Object.keys(item)?.[0];
const functionIndex = Object.keys(validFunctions).map(getFunctionTag).indexOf(firstKey);
const newItem = (fp.isPlainObject(item) || fp.isArray(item) )
// only allow where one key, which is valid function name
? (firstKey && Object.keys(item).length===1 && functionIndex >-1 )
// call the function
? Object.values(validFunctions)[functionIndex](
// allow substitution on arguments
...applyFunctionsInObjectInner(fp.castArray(item[firstKey]))(...data),
// optional data
...fp.castArray(data)
)
// recurse objects (deepness)
: applyFunctionsInObjectInner(item)(...data)
: item ; // pass non-object/array values directly
acc[key]=newItem;
}
)
}
const source = {
dateExample: {"$date":["01-01-2020"]},
getExample: {"$get":"test"},
getWithRecursedArg: {"$get":{"$get":"test2"}},
};
const data = {"test":99,"test2":"test"};
const result = substituteFunctionsInObject({
get: fp.get,
date: x=>new Date(x)
})(source)(data);
console.log(JSON.stringify(result))
// {"dateExample":"2020-01-01T00:00:00.000Z","getExample":99,"getWithRecursedArg":99}