chimera-framework
Version:
Language agnostic framework for stand-alone and distributed computing
656 lines (620 loc) • 21.6 kB
JavaScript
module.exports = {
executeChain
}
// imports
const requireOnce = require('./require-once.js')
let preprocessor, chimlParser, safeEval, stringify, nsync, fs, util, path, $
// constants
const SILENT = 0
const VERBOSE = 1
const VERY_VERBOSE = 2
const ULTRA_VERBOSE = 3
const COLOR_FG_YELLOW = '\x1b[33m'
const COLOR_FG_RED = '\x1b[31m'
const COLOR_RESET = '\x1b[0m'
const MERE_UNQUOTED_STRING_PATTERN = /^[\w\s_:]+$/g
const ARRAY_PATTERN = /^\[([\w\s.,'"_]+)\]$/i
const DOTTED_VARIABLE_PATTERN = /^\w+(\.\w+)+$/g
const VALID_URL = /^([a-z]+):\/\/[^\s/$.?#].[^\s]*$/g
let OBJ_CACHE = {}
let JS_CACHE = {}
let ENV_CACHE = {}
function executeChain (chain, ins, vars, callback) {
path = requireOnce('path')
util = requireOnce('./util.js')
$ = requireOnce('./core-dollar.js')
let VARIABLES_PROXY
let VARIABLES = {}
let defaultIns = []
let defaultVars = {}
if (util.isFunction(ins)) {
callback = ins
ins = defaultIns
vars = defaultVars
} else if (util.isFunction(vars)) {
callback = vars
vars = defaultVars
} else {
ins = util.isNullOrUndefined(ins) ? defaultIns : ins
vars = util.isNullOrUndefined(vars) ? defaultVars : vars
}
callback = defineDefaultFinalCallback(callback)
vars._init_cwd = process.cwd() + '/'
// chain is an object, run it
if (util.isRealObject(chain)) {
return runRootChain(chain, ins, vars, callback)
}
// chain is in OBJ_CACHE
if (chain in OBJ_CACHE) {
vars = util.getPatchedObject(vars, ENV_CACHE[chain], false)
const chainObj = OBJ_CACHE[chain]
return runRootChain(chainObj, ins, vars, callback)
}
// chain is in JS_CACHE
if (chain in JS_CACHE) {
vars = util.getPatchedObject(vars, ENV_CACHE[chain], false)
const jsChain = JS_CACHE[chain]
return executeJsChain(jsChain, ins, vars, callback)
}
// chain is not an object, it might be a script or a file
const chainPath = path.resolve(chain)
chimlParser = requireOnce('./core-chiml-parser.js')
return chimlParser.parseChiml(chain, (error, chainObj) => {
if (!error && chain !== chainObj) {
// no doubt, it must be CHIML script
ENV_CACHE[chain] = {}
OBJ_CACHE[chain] = chainObj
return runRootChain(chainObj, ins, vars, callback)
}
// is it a file?
fs = requireOnce('fs')
return fs.readFile(chainPath, (error, chainScript) => {
if (error) {
// no, it is not a file. it probably a CHIML script
chainScript = chain
vars._description = 'script: ' + util.getSlicedString(chainScript, 50)
return executeChainScript(chainScript, ins, vars, callback)
}
// it must be a file
const dirname = path.dirname(chainPath)
const basename = path.basename(chainPath)
vars._description = 'file: ' + basename
vars._chain_cwd = dirname + '/'
// is it a javascript module?
if (chainPath.substr(chainPath.length - 3).toLowerCase() === '.js') {
try {
const jsChain = require(chainPath)
JS_CACHE[chain] = jsChain
ENV_CACHE[chain] = {_description: vars._description, _chain_cwd: vars._chain_cwd}
return executeJsChain(jsChain, ins, vars, callback)
} catch (error) {
return setImmediate(() => { callback(error, null) })
}
}
// chain is CHIML file
return executeChainScript(chainScript, ins, vars, callback)
})
})
function executeJsChain (jsChain, ins, vars, callback) {
const newVars = util.getPatchedObject({$}, vars)
injectSpecialProperties(newVars)
return jsChain(util.getDeepCopiedObject(ins), newVars, callback)
}
function getPublishedState () {
const filteredState = util.getFilteredObject(VARIABLES, ['$'])
return util.getInspectedObject(filteredState)
}
function getInsNameAsArray (ins) {
if (util.isString(ins)) {
return util.getSmartSplitted(ins, ',')
} else if (util.isArray(ins)) {
return ins
}
return []
}
function defineDefaultFinalCallback (finalCallback) {
return (error, value) => {
if (VARIABLES._error) {
if ('_error_object' in VARIABLES && util.isRealObject(VARIABLES._error_object)) {
return finalCallback(VARIABLES._error_object, value)
}
return finalCallback(VARIABLES._error_message, value)
}
return finalCallback(error, value)
}
}
function wrapFinalCallback (chain, finalCallback) {
return (error) => {
const result = evaluateStatement(chain.out)
finalCallback(error, result)
}
}
function evaluateObjectStatement (statement) {
let result = {}
const keys = Object.keys(statement)
for (let i = 0; i < keys.length; i++) {
if (keys[i] === '$' || keys[i].indexOf('_') === 0) {
result[keys[i]] = statement[keys[i]]
} else {
result[keys[i]] = evaluateStatement(statement[keys[i]])
}
}
return result
}
function evaluateArrayStatement (statement) {
return statement.map((element) => {
return evaluateStatement(element)
})
}
function evaluateDottedVariable (statement) {
let value = VARIABLES
let isValid = false
let isMatch = false
if (statement.match(DOTTED_VARIABLE_PATTERN)) {
isMatch = true
const variableSegments = statement.split('.')
for (let i = 0; i < variableSegments.length; i++) {
const segment = variableSegments[i]
if (segment in value) {
isValid = true
value = value[segment]
} else {
isValid = false
break
}
}
}
return {isValid, isMatch, value}
}
function evaluateStatement (statement, log = false) {
if (util.isRealObject(statement)) {
return evaluateObjectStatement(statement)
}
if (util.isArray(statement)) {
return evaluateArrayStatement(statement)
}
if (util.isString(statement)) {
if (statement === 'true') {
// boolean true
return true
}
if (statement === 'false') {
// boolean false
return false
}
if (statement === 'null') {
// null
return null
}
if (statement in VARIABLES) {
// variable
return VARIABLES[statement]
}
if (statement === '' || statement[0] === '/' ||
statement.indexOf('file: ') === 0 || statement.indexOf('script: ') === 0 ||
statement.indexOf('C:\\\\') === 0 || statement.indexOf('D:\\\\') === 0 ||
statement.indexOf('E:\\\\') === 0 || statement.indexOf('F:\\\\') === 0 ||
statement.indexOf('H:\\\\') === 0 || statement.indexOf('I:\\\\') === 0 ||
statement.match(VALID_URL) || statement.match(MERE_UNQUOTED_STRING_PATTERN)) {
return statement
}
const normalizedValue = getNormalizedValue(statement)
if (normalizedValue !== statement) {
return normalizedValue
}
const arrayPatternMatch = statement.match(ARRAY_PATTERN)
if (arrayPatternMatch) {
// string representation of array
const elementList = util.getSmartSplitted(arrayPatternMatch[1], ',')
return evaluateArrayStatement(elementList)
}
const dottedVariableEvaluation = evaluateDottedVariable(statement)
if (dottedVariableEvaluation.isValid) {
// dotted variable name
return dottedVariableEvaluation.value
} else if (dottedVariableEvaluation.isMatch) {
// match dottedVariable but not a variable
return statement
}
// script
safeEval = requireOnce('./safe-eval.js')
return safeEval(statement, VARIABLES)
}
return statement
}
function createNestedChainActions (chain, finalCallback) {
const actions = chain.chains.map((subChain) => {
return (next) => {
const chainRunner = createChainRunner(subChain, finalCallback)
chainRunner(next)
}
})
return actions
}
function createObjectProxy (object) {
const handler = {
get: function (target, prop, receiver) {
if (util.isNullOrUndefined(target[prop])) {
target[prop] = {}
}
if (util.isRealObject(target[prop])) {
const rv = new Proxy(target[prop], handler)
return rv
}
return target[prop]
},
set: function (obj, prop, value) {
if (util.isString(value)) {
value = getNormalizedValue(value)
}
obj[prop] = value
return Reflect.set(...arguments)
}
}
return new Proxy(object, handler)
}
function getNormalizedValue (val) {
if (!util.isString(val)) {
return val
}
return util.getParsedJson(val)
}
function setVariable (name, val) {
const normalizedVal = getNormalizedValue(val)
if (!util.isString(name)) {
name = '_ans'
}
if (name in VARIABLES || (name.indexOf('.') === -1 && name.indexOf('[') === -1)) {
VARIABLES[name] = normalizedVal
return true
} else if (name.indexOf('[') === -1) {
const nameParts = name.split('.')
let variable = VARIABLES
for (let i = 0; i < nameParts.length; i++) {
let namePart = nameParts[i]
if (i === nameParts.length - 1) {
variable[namePart] = val
return true
}
if (util.isNullOrUndefined(variable[namePart])) {
variable[namePart] = {}
}
variable = variable[namePart]
}
}
stringify = requireOnce('json-stringify-safe')
const scriptVal = stringify(normalizedVal)
const script = '(() => {_vars.' + name + ' = ' + scriptVal + '; return _vars})()'
safeEval = requireOnce('./safe-eval.js')
safeEval(script, VARIABLES)
return true
}
function assignError (error, finalCallback) {
VARIABLES._error = true
VARIABLES._error_message = error.message
VARIABLES._error_object = error
return finalCallback(error, null)
}
function createSingleChainCallback (chain, next, finalCallback) {
return (error, result) => {
if (error) {
return assignError(error, finalCallback)
}
try {
setVariable(chain.out, result)
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') Set ' + chain.out + ' into: ' + util.getInspectedObject(evaluateStatement(chain.out)))
}
if (ULTRA_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') state after execution:\n' + getPublishedState())
}
return next()
} catch (setVariableError) {
return assignError(setVariableError, finalCallback)
}
}
}
function createSingleChainAction (chain, finalCallback) {
return (next) => {
let inputStatement, inputs
try {
// assemble inputStatement
if (util.isString(chain.ins)) {
inputStatement = '[' + chain.ins + ']'
} else {
inputStatement = chain.ins
}
if ((chain.command === '[_map]' || chain.command === '[_filter]') && chain.ins.length === 2) {
inputs = [evaluateStatement(chain.ins[0]), chain.ins[1]]
} else if (chain.command === '[_chain]' && chain.ins.length > 0) {
let newInputs = [inputs[0]]
for (let i = 0; i < inputs.length; i++) {
newInputs.push(evaluateStatement(chain.ins[i]))
}
inputs = newInputs
} else {
inputs = evaluateStatement(inputStatement)
}
// if inputs is undefined, turn it into empty array
if (inputs === undefined) {
inputs = []
}
// inputs should be array
if (!util.isArray(inputs)) {
inputs = [inputs]
}
if (VARIABLES._error) {
return finalCallback(VARIABLES._error_object, null)
}
if (VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') input values: ' + util.getInspectedObject(inputs))
}
// add VARIABLES as the first input (context)
inputs.unshift(VARIABLES)
// prepare command callback
const commandCallback = createSingleChainCallback(chain, next, finalCallback)
// add command callback as the last input
inputs.push(commandCallback)
// run the command, and continue to the next chain
return chain.compiledCommand(...inputs)
} catch (error) {
if (SILENT < VARIABLES._verbose) {
const inputValues = util.isArray(inputs) ? inputs.slice(1) : []
let errorLog = 'ERROR ON CHAIN #' + chain.id + '\n'
errorLog += 'INPUT: ' + util.getInspectedObject(chain.ins) + '\n'
errorLog += 'INPUT VALUES: ' + util.getInspectedObject(inputValues) + '\n'
errorLog += 'COMMAND: ' + util.getInspectedObject(chain.command) + '\n'
logMessage(errorLog)
}
return assignError(error, finalCallback)
}
}
}
function createChainRunner (chain, finalCallback, firstTime = true) {
nsync = requireOnce('neo-async')
const isHavingChildren = 'chains' in chain && util.isArray(chain.chains)
const asyncWorker = isHavingChildren && chain.mode === 'parallel' ? nsync.parallel : nsync.series
return (callback) => {
let actions = []
if (VARIABLES._error) {
return asyncWorker(actions, callback)
}
if (VERBOSE < VARIABLES._verbose) {
logMessage('Run chain #' + chain.id)
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') Checking `if` condition: ' + chain.branch)
}
}
if (firstTime) {
if (!evaluateStatement(chain.branch)) {
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') `if` condition rejected: ' + chain.branch)
}
return asyncWorker(actions, callback)
}
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') `if` condition resolved: ' + chain.branch)
}
} else if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') Performing operation again due to `while` condition')
}
if ('chains' in chain && util.isArray(chain.chains)) {
if (VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') is nested')
}
const nestedChainActions = createNestedChainActions(chain, finalCallback)
actions = actions.concat(nestedChainActions)
} else if ('command' in chain) {
if (VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') is single')
}
const singleCommandAction = createSingleChainAction(chain, finalCallback)
actions.push(singleCommandAction)
}
actions.push((next) => {
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') Checking `while` condition: ' + chain.loop)
}
if (!evaluateStatement(chain.loop)) {
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') `while` condition rejected: ' + chain.loop)
}
next()
} else {
if (VERY_VERBOSE < VARIABLES._verbose) {
logMessage('(chain #' + chain.id + ') `while` condition resolved: ' + chain.loop)
}
const loopChainRunner = createChainRunner(chain, finalCallback, false)
loopChainRunner(next)
}
})
return asyncWorker(actions, callback)
}
}
function assignVariableByInsValue (chain, insValue) {
// get ins and embed it into variables
const ins = getInsNameAsArray(chain.ins)
for (let i = 0; i < ins.length; i++) {
const key = ins[i]
if (i < insValue.length) {
try {
VARIABLES[key] = evaluateStatement(insValue[i])
} catch (error) {
VARIABLES[key] = insValue[i]
}
} else if (!(key in VARIABLES)) {
VARIABLES[key] = null
}
}
}
function runRootChain (chain, insValue, vars, finalCallback) {
nsync = requireOnce('neo-async')
preprocessor = require('./core-preprocessor.js')
// get the root chain
chain = preprocessor.getTrueRootChain(chain)
const processCwd = process.cwd() + '/'
// prepare variables
VARIABLES = Object.assign(chain.vars, {$}, {_ans: null,
_error: false,
_error_message: '',
_error_object: null,
_init_cwd: processCwd,
_chain_cwd: processCwd,
_description: ''
})
// patch variables
VARIABLES = util.getPatchedObject(VARIABLES, vars, false)
injectSpecialProperties(VARIABLES)
// add ins to variables
assignVariableByInsValue(chain, insValue)
if (VERBOSE < VARIABLES._verbose) {
logMessage('CHAIN SEMANTIC:\n' + util.getInspectedObject(chain))
logMessage('INITIAL STATE:\n' + getPublishedState())
}
// get `out` and embed it into variables
if (!(chain.out in VARIABLES)) {
VARIABLES[chain.out] = null
}
// prepare final callback
finalCallback = wrapFinalCallback(chain, finalCallback)
// create chain runner and run it
const chainRunner = createChainRunner(chain, finalCallback, true)
chainRunner(finalCallback)
}
function injectSpecialProperties (variables) {
if (!variables._vars) {
Object.defineProperty(variables, '_vars', {
get: () => {
if (VARIABLES_PROXY) {
return VARIABLES_PROXY
}
VARIABLES_PROXY = createObjectProxy(variables)
return VARIABLES_PROXY
}
})
}
if (!variables._map) {
Object.defineProperty(variables, '_map', {
get: () => {
return wrappedMap
}
})
}
if (!variables._filter) {
Object.defineProperty(variables, '_filter', {
get: () => {
return wrappedFilter
}
})
}
if (!variables._runChain) {
Object.defineProperty(variables, '_runChain', {
get: () => {
// use closure to create runChain function
return wrapRunChain(variables)
}
})
}
}
function wrapRunChain (variables) {
return (chain, ...args) => {
const callback = args.pop()
// only dollar should be passed to the sub-chain
let dollar = {}
if (util.isRealObject(variables.$)) {
dollar = Object.assign(dollar, variables.$)
}
executeChain(chain, args, {$: dollar}, callback)
}
}
function wrappedMap (list, chain, callback) {
map(list, chain, VARIABLES, callback)
}
function wrappedFilter (list, chain, callback) {
filter(list, chain, VARIABLES, callback)
}
function map (list, chain, variables, callback) {
if (!util.isArray(list) || list.length === 0) {
return setImmediate(() => { callback(null, []) })
}
nsync = requireOnce('neo-async')
// just for testing and trigger cache mechanism
executeChain(chain, [list[0]], variables, (error, firstElement) => {
if (error) {
return setImmediate(() => { callback(error, null) })
}
const action = (element, next) => {
executeChain(chain, [element], variables, (error, result) => {
next(error, result)
})
}
return nsync.map(list.slice(1), action, (error, elements) => {
if (error) {
return callback(error, null)
}
elements.unshift(firstElement)
return callback(null, elements)
})
})
}
function filter (list, chain, variables, callback) {
if (!util.isArray(list) || list.length === 0) {
return setImmediate(() => { callback(null, []) })
}
nsync = requireOnce('neo-async')
// just for testing and trigger cache mechanism
executeChain(chain, [list[0]], variables, (error, firstFilter) => {
if (error) {
return setImmediate(() => { callback(error, null) })
}
const action = (element, next) => {
executeChain(chain, [element], variables, (error, result) => {
next(error, result)
})
}
return nsync.filter(list.slice(1), action, (error, elements) => {
if (error) {
return callback(error, null)
}
if (firstFilter) {
elements.unshift(list[0])
}
return callback(null, elements)
})
})
}
function parseAndCacheChain (chainScript, vars, callback) {
chimlParser = requireOnce('./core-chiml-parser.js')
return chimlParser.parseChiml(chainScript, (error, chainObj) => {
if (error) {
return callback(error, null, null)
}
ENV_CACHE[chain] = {}
if (vars._description) {
ENV_CACHE[chain]._description = vars._description
}
if (vars._chain_cwd) {
ENV_CACHE[chain]._chain_cwd = vars._chain_cwd
}
OBJ_CACHE[chain] = chainObj
return callback(null, chainObj, vars)
})
}
function executeChainScript (chainScript, ins, vars, callback) {
parseAndCacheChain(String(chainScript), vars, (error, chainObj, newVars) => {
if (error) {
console.error(COLOR_FG_YELLOW + 'CHIML Script: \n' + chainScript + COLOR_FG_RED)
console.error(error)
console.error(COLOR_RESET)
return setImmediate(() => { callback(error, null) })
}
return runRootChain(chainObj, ins, newVars, callback)
})
}
function logMessage (message) {
const description = VARIABLES._description
const isoDate = (new Date()).toISOString()
return console.error(COLOR_FG_YELLOW + '[' + description + ' ' + isoDate + '] ' + message + COLOR_RESET)
}
}