UNPKG

dazzling-fiction

Version:

Fictional facts generator based on a markdown fiction dialect

1,032 lines (887 loc) 30.3 kB
'use strict'; var Joi = require('joi'); var bluePromise = require("bluebird"); var fs = bluePromise.promisifyAll(require("fs-extra")); var httpRequest = bluePromise.promisifyAll(require('request')); var marked = require('marked'); var _ = require('lodash'); var S = require('string'); var dazzlingChance = require('dazzling-chance'); var FICTION_SUFFIX = '.fiction.md'; var YES_NO = 'yes/no'; var TYPE_REF = "REF"; var TYPE_MODEL = "MODEL"; var TYPE_WEIGHTING = "WEIGHTING"; //var TYPE_BOOLEAN = "BOOLEAN"; var TYPE_LIST = "LIST"; var TYPE_MAPPING = "MAPPING"; var TYPE_QUANTITY = "QTY"; var TYPE_UNKNOWN = "UNKNOWN"; var curieSchema = Joi.object().keys({ startsWith: Joi.string().min(1).required(), contentType: Joi.string().required().valid('application/json', 'text/plain'), prefix: Joi.string().required(), suffix: Joi.string().required() }); var fictionSchema = Joi.object().keys({ scriptFolder: Joi.string(), script: Joi.string().min(4).required(), curies: Joi.array().items(curieSchema) }); var scriptSchema = Joi.object().keys({ id: Joi.string().min(1).max(100).required(), query: Joi.string().min(5).max(100).required(), text: Joi.string().min(20).required() }); var extractKey = function(value) { return S(value).between('**', '**'); }; var extractValue = function(value) { return S(value).chompLeft('**').between('**'); }; var extractBeforeColon = function(value) { var svalue = S(value); if (!svalue.contains(':')) { return null; } return svalue.between('', ':').s; }; var extractAfterColon = function(value) { var svalue = S(value); if (!svalue.contains(':')) { return null; } return svalue.between(':').s; }; var asName = function(svalue) { if (svalue.contains(':')) { return svalue.trim().s; } else { return svalue.slugify().s; } }; var FREQUENCY = ['never', 'rarely', 'occasionally', 'sometimes', 'often', 'usually', 'always']; var isFrequencyWord = function(value) { return _.indexOf(FREQUENCY, value.trim().toLowerCase()) > -1; }; var containsNumber = function(value) { return value.search(/[0-9]/) > -1; }; var checkListType = function(list) { var first = _.first(list); var isNumber = /[0-9.]+/.test(first) && S(first).count('.') <= 1; return isNumber ? 'number' : 'string'; }; var checkMinMaxType = function(min) { var isInt = S(min).isNumeric(); return isInt ? 'int' : 'float'; }; var extractBetween = function(value, from, to) { var hasFrom = S(value).contains(from) || S(from).length === 0; var hasTo = S(value).contains(to) || S(to).length === 0; if (!(hasFrom && hasTo)) { return { found: false, remain: value, extracted: [] }; } var extracted = S(value).between(from, to); var remain = S(value).replaceAll(from + extracted.s + to, '').s; return { found: true, remain: remain, extracted: extracted }; }; var extractRefs = function(value) { var ref = extractBetween(value, '`', '`'); var r = ref; var extracted = []; while (ref.found) { extracted.push(asName(ref.extracted)); r = { found: true, remain: ref.remain, extracted: extracted }; ref = extractBetween(ref.remain, '`', '`'); } return r; }; var extractCommand = function(value) { var cmd = extractBetween(value, '`', '`'); return cmd; }; var extractMinMax = function(value) { var hasNumber = containsNumber(value); if (!hasNumber) { return []; } var hasMinMax = S(value).contains(' to '); if (hasMinMax) { return [S(value).between('', ' to ').toFloat(), S(value).between(' to ').toFloat()]; } else { return [S(value).toFloat(), S(value).toFloat()]; } }; var extractQuantity = function(value) { var hasQuantity = S(value).contains(' of '); if (!hasQuantity) { return {}; } var qtyPart = S(value).between('', ' of ').s; var refs = extractRefs(qtyPart); if (refs.found) { return { refs: refs.extracted }; } var hasMinMax = extractMinMax(qtyPart); if (!_.isEmpty(hasMinMax)) { return { type: 'int', min: hasMinMax[0], max: hasMinMax[1] }; } return {}; }; var extractBoolean = function(value) { return S(value).contains(YES_NO); }; var toNumber = function(value) { return S(value).toFloat(); }; var parseAttributeItem = function(value) { var remain = value; var r = { quantity: { type: 'int', min: 1, max: 1 }, value: {}, raw: value }; var list = extractBetween(remain, '*', '*'); if (list.found) { var csvList = list.extracted.parseCSV(); var listType = checkListType(csvList); if (listType === 'number') { csvList = _.map(csvList, toNumber); } r.value = { type: listType, list: csvList }; remain = list.remain; } var quantity = extractQuantity(remain); if (!_.isEmpty(quantity)) { r.quantity = quantity; remain = S(remain).between(' of ').s; } var yesNo = extractBoolean(remain); if (yesNo) { r.value = { type: 'bool' }; remain = S(remain).replaceAll(YES_NO, '').s; } var ref = extractBetween(remain, '`', '`'); if (ref.found) { r.value = { ref: asName(ref.extracted) }; remain = ref.remain; } var minMax = extractMinMax(remain); if (!_.isEmpty(minMax)) { r.value = { type: checkMinMaxType(list), min: minMax[0], max: minMax[1] }; } return r; }; var parseLists = function(value) { var command = extractCommand(value); if (command.found) { return { command: command.extracted.s }; } var list = extractBetween(value, '*', '*'); if (list.found) { var csvList = list.extracted.parseCSV(); var listType = checkListType(csvList); if (listType === 'number') { csvList = _.map(csvList, toNumber); } return { type: listType, list: csvList }; } else { return []; } }; var parseMappings = function(value) { var command = extractCommand(value); if (!command.found) { throw new Error("Mappings should be a reference to a file"); } return { command: command.extracted.s }; }; var extractModelAttribute = function(value) { var attrValue = extractValue(value).parseCSV(';'); Joi.assert(attrValue, Joi.array().min(1)); var r = {}; var hasFreqWord = isFrequencyWord(attrValue[0]); r.frequency = hasFreqWord ? attrValue[0].trim().toLowerCase() : 'always'; var values = hasFreqWord ? _.drop(attrValue) : attrValue; r.values = _.map(values, parseAttributeItem); return r; }; var md2obj = function(tokens) { var r = { title: "", imports: [], weighting: {}, frequency: {}, models: {}, lists: {}, mappings: {}, refs: {} }; var length = tokens.length; var section = ""; var modelName = ""; for (var i = 0; i < length; i++) { var token = tokens[i]; var isNotEmptyText = (token.type === 'text') && (!S(token.text).isEmpty()); if (token.depth === 1) { r.title = token.text; } else if (token.depth === 2) { section = token.text.toLowerCase(); } else if (token.depth === 3) { modelName = asName(S(token.text)); } else { var isImport = (section === 'import') && isNotEmptyText; if (isImport) { r.imports.push(token.text); r.imports = _.uniq(r.imports); } var isWeighting = (section === 'weighting') && isNotEmptyText; if (isWeighting) { r.weighting[asName(extractKey(token.text))] = extractValue(token.text).parseCSV(); } var isFrequency = (section === 'frequency') && isNotEmptyText; if (isFrequency) { var freqNames = extractKey(token.text).parseCSV(); var freqValues = extractValue(token.text).parseCSV(); var freqMax = 1; for (var j = freqNames.length - 1; j >= 0; j--) { var freqValue = S(freqValues[j]).toInt(); r.frequency[freqNames[j].toLowerCase()] = freqValue; freqMax = (freqValue > freqMax) ? freqValue : freqMax; } r.frequency.max = freqMax; } var isRefs = (section === 'references') && isNotEmptyText; if (isRefs) { r.refs[asName(extractKey(token.text))] = parseAttributeItem(extractValue(token.text).s); } var isLists = (section === 'lists') && isNotEmptyText; if (isLists) { r.lists[asName(extractKey(token.text))] = parseLists(extractValue(token.text).s); } var isMappings = (section === 'mappings') && isNotEmptyText; if (isMappings) { r.mappings[asName(extractKey(token.text))] = parseMappings(extractValue(token.text).s); } var isModelAttribute = (section === 'models') && isNotEmptyText; if (isModelAttribute) { var modelAttrName = asName(extractKey(token.text)); var previous = _.get(r.models, [modelName, modelAttrName]); var nextIdx = _.size(previous); _.set(r.models, [modelName, modelAttrName, nextIdx], extractModelAttribute(token.text)); } } } return r; }; var text2obj = function(text) { var lexer = new marked.Lexer({}); var m2Tokens = lexer.lex(text); return md2obj(m2Tokens); }; var cleanImport = function(value) { return S(value).trimLeft().chompLeft('*').trim().s; }; var isListItem = function(value) { return S(value).startsWith("*"); }; var text2Imports = function(text) { var hasImport = S(text).contains('## Import'); if (!hasImport) { return []; } else { var lines = S(text).between('## Import', '##').lines(); var onlyItems = _.filter(lines, isListItem); return _.map(onlyItems, cleanImport); } }; var cloneAndMergeArray = function(a, b) { var aa = _.cloneDeep(a); aa.push(b); return aa; }; var concatArray = function(a, b) { return _.uniq(_.flattenDeep(a.concat(b))); }; var ensureArray = function(value) { return _.isArray(value) ? value : [value]; }; var mergeObjs = function(total, n) { return _.merge(total, n); }; var mergeScripts = function(scripts) { if (!_.isArray(scripts)) { return scripts; } var _scripts = _.chain(scripts); var r = { title: scripts[0].title, imports: _scripts.map('imports').flatten().uniq().value(), weighting: _scripts.map('weighting').reduceRight(mergeObjs).value(), frequency: _scripts.map('frequency').reduceRight(mergeObjs).value(), models: _scripts.map('models').reduceRight(mergeObjs).value(), lists: _scripts.map('lists').reduceRight(mergeObjs).value(), mappings: _scripts.map('mappings').reduceRight(mergeObjs).value(), refs: _scripts.map('refs').reduceRight(mergeObjs).value() }; return r; }; var getValueType = function(value) { var hasRef = _.has(value, 'value.ref'); return hasRef ? TYPE_REF : _.get(value, 'value.type'); }; var getRefType = function(_script, ref) { var refPrefix = extractBeforeColon(ref); if (_script.has(['models', ref]).value()) { return TYPE_MODEL; } else if (_script.has(['refs', ref]).value()) { var vtype = getValueType(_script.get(['refs', ref]).value()); var hasQty = 'float' === vtype; return hasQty ? TYPE_QUANTITY : TYPE_REF; } else if (_script.has(['lists', ref]).value()) { return TYPE_LIST; } else if (_script.has(['weighting', ref]).value()) { return TYPE_WEIGHTING; } else if (_script.has(['mappings', refPrefix]).value()) { return TYPE_MAPPING; } else { return TYPE_UNKNOWN; } }; var getRefValue = function(_script, ref) { var refType = getRefType(_script, ref); switch (refType) { case TYPE_REF: return _script.get(['refs', ref]).value(); case TYPE_MODEL: return _script.get(['models', ref]).value(); case TYPE_LIST: return _script.get(['lists', ref]).value(); case TYPE_MAPPING: var map = extractBeforeColon(ref); var key = extractAfterColon(ref); return _script.get(['mappings', map, key]).value(); case TYPE_WEIGHTING: return _script.get(['.weighting', ref]).value(); default: throw new Error('Unkown ref type:' + refType); } }; var luckyQuantity = function(_script, chance, value) { var isDirectMinMax = _.has(value, 'quantity.max'); if (isDirectMinMax) { var qtyMin = _.get(value, 'quantity.min'); var qtyMax = _.get(value, 'quantity.max'); var isLuck = qtyMin < qtyMax; return isLuck ? chance.nextInt(qtyMin, qtyMax) : qtyMin; } var refs = _.get(value, 'quantity.refs', []); var qtyRefs = _script.get('refs').keys().intersection(refs).value(); var isQtyRef = (_.size(qtyRefs) > 0) && (getRefType(_script, qtyRefs[0]) === TYPE_QUANTITY); if (isQtyRef) { var qtyRefValue = _script.get(['refs', qtyRefs[0]]).value(); return chance.nextInt(_.get(qtyRefValue, 'value.min'), _.get(qtyRefValue, 'value.max')); } return 5; }; var luckyFrequency = function(_script, chance, value) { var freq = _script.get(['frequency', value]).value(); if (freq === 0) { return false; } var freqMax = _script.get('frequency.max').value(); var isMax = (freq === freqMax); if (isMax) { return true; } var luck = chance.nextInt(0, freqMax); return (luck <= freq); }; var luckyInList = function(chance, value, ownerId, predicate, cmd) { var hasSingleItem = _.size(value.list) === 1; if (hasSingleItem) { return _.chain(value).get('list').first().value(); } else { var isNotWordList = _.isUndefined(value.list); if (isNotWordList) { return chance.next(value); } var cloned = _.cloneDeep(value); var cacheId = [predicate, cmd].join('||'); var reducedList = _.get(chance, ['statefulLists', cacheId], cloned.list); cloned.list = reducedList; var item = chance.next(cloned); var listWithoutItem = _.without(reducedList, item); chance.statefulLists[cacheId] = _.size(listWithoutItem) < 2 ? _.cloneDeep(value.list) : listWithoutItem; return item; } }; var luckyDirectIndex = function(_script, chance, value, ownerId, predicate) { var hasRefValue = _.has(value, 'value.ref'); if (hasRefValue) { var ref = value.value.ref; return { v: ref, t: getRefType(_script, ref) }; } var refs = _.get(value, 'quantity.refs', []); var weightingRefs = _script.get('weighting').keys().intersection(refs).value(); var hasWeighting = (_.size(weightingRefs) > 0) && (_script.has(['weighting', weightingRefs[0]])); var luck = _.get(value, 'value'); if (hasWeighting) { luck.weight = weightingRefs[0]; } return luckyInList(chance, luck, ownerId, predicate); }; var incCounter = function(chance) { chance.counter++; return chance.counter; }; var inferChildToStack = function(_script, chance, stk, ref, idx) { var refType = getRefType(_script, ref); var refValue = getRefValue(_script, ref); var isReference = stk.parent && TYPE_REF === stk.parent.t; var name = isReference ? ['ref', stk.parent.n].join('-') : [stk.root.k, ref, incCounter(chance), idx + 1].join('-'); var cloned = { refs: stk.refs, root: stk.root, parent: stk.child, child: { k: ref, t: refType, v: refValue, n: name } }; return cloned; }; var stringToCSV = function(str) { return S(str).parseCSV("\n"); }; var normalizeList = function(list) { var listType = checkListType(list); var nList = listType === 'number' ? _.map(list, toNumber) : list; return { type: listType, list: nList }; }; var justBody = function(resp) { return _.chain(resp).first().get('body').value(); }; var UTF8_ENC = { 'encoding': 'utf8' }; var fileListLoaderAsync = function(v) { var isJson = v.contentType === 'application/json'; return isJson ? fs.readJsonAsync(v.path) : fs.readFileAsync(v.path, UTF8_ENC).then(stringToCSV); }; var webListLoaderAsync = function(v) { var isJson = v.contentType === 'application/json'; var reqConf = { uri: v.path, headers: { "accept": v.contentType }, json: isJson, encoding: "utf8", timeout: 5000 }; return isJson ? httpRequest.getAsync(reqConf).then(justBody) : httpRequest.getAsync(reqConf).then(justBody).then(stringToCSV); }; var extractList = function(v) { return _.isArray(v) ? v :_.flatMapDeep(_.pickBy(v, _.isArray)); }; var listLoaderAsync = function(v) { var hasHttp = S(v.path).startsWith('http://') || S(v.path).startsWith('https://'); var loader = hasHttp ? webListLoaderAsync(v) : fileListLoaderAsync(v); return loader.then(extractList).then(normalizeList); }; var mappingLoaderAsync = function(v) { var hasHttp = S(v.path).startsWith('http://') || S(v.path).startsWith('https://'); var loader = hasHttp ? webListLoaderAsync(v) : fileListLoaderAsync(v); return loader; }; var uncurify = function(cfg, uri) { var curies = cfg.curies; if (_.size(curies) < 1) { return { uri: uri, path: uri, contentType: 'text/plain' }; } var u = S(uri); for (var i = 0; i < curies.length; i++) { var curie = curies[i]; if (u.startsWith(curie.startsWith)) { var path = [curie.prefix, u.chompLeft(curie.startsWith).s, curie.suffix].join(''); return { uri: uri, path: path, contentType: curie.contentType //loaderAsync: listLoaderAsync(path) }; } } return { uri: uri, path: uri, contentType: 'text/plain', //loaderAsync: listLoaderAsync(uri) }; }; var resolveList = function(_script, chance, ref, ownerId, predicate) { var list = _script.get(['lists', ref]); var hasCommand = list.has('command').value(); if (hasCommand) { var cmd = list.get('command').value(); var cached = _script.get(['cachedList', cmd]).value(); if (!_.isObject(cached)) { throw new Error("List should have been cached"); } var lil= luckyInList(chance, cached, ownerId, predicate, cmd); return lil; } return luckyInList(chance, list.value(), ownerId, predicate); }; var resolveMapping = function(_script, chance, ref) { var map = extractBeforeColon(ref); var key = extractAfterColon(ref); var mapping = _script.get(['mappings', map]); var hasCommand = mapping.has('command').value(); if (!hasCommand) { throw new Error("Mapping should reference a file"); } var cmd = mapping.get('command').value(); var cached = _script.get(['cachedMapping', cmd]).value(); if (!_.isObject(cached)) { throw new Error("Mapping should have been cached"); } return cached[key]; }; var retrieveLists = function(script) { var cmdLists = _.map(_.filter(_.values(script.lists), 'command'), 'command'); if (_.isEmpty(cmdLists)) { return script; } var uncurifyCmd = function(cmd) { var ucmd = uncurify(script.cfg, cmd); return ucmd; }; var cmdObjs = _.map(cmdLists, uncurifyCmd); var loaders = _.map(cmdObjs, function(cmdObj) { return listLoaderAsync(cmdObj).then(function(rList) { script.cachedList[cmdObj.uri] = rList; }); }); var passScript = function() { return script; }; return bluePromise.all(loaders).then(passScript); }; var retrieveMappings = function(script) { var cmdMappings = _.map(_.filter(_.values(script.mappings), 'command'), 'command'); if (_.isEmpty(cmdMappings)) { return script; } var uncurifyCmd = function(cmd) { var ucmd = uncurify(script.cfg, cmd); return ucmd; }; var cmdObjs = _.map(cmdMappings, uncurifyCmd); var loaders = _.map(cmdObjs, function(cmdObj) { return mappingLoaderAsync(cmdObj).then(function(rMapping) { script.cachedMapping[cmdObj.uri] = rMapping; }); }); var passScript = function() { return script; }; return bluePromise.all(loaders).then(passScript); }; var resolveModelRight = function(_script, chance, facts, stack) { var resolveAttrValues = function(values, key) { var resolveAttr = function(value) { var notWished = !luckyFrequency(_script, chance, value.frequency); var noValue = _.isEmpty(value.values); var attrQuantity = (noValue || notWished) ? 1 : luckyQuantity(_script, chance, _.chain(value).get('values').first().value()); var resolveOneAttr = function(oneAttrIdx) { var resolveOneColumn = function(colValue) { var li = luckyDirectIndex(_script, chance, colValue, stack.child.n, key); var isRef = _.has(li, 'v'); if (isRef) { if (li.t === TYPE_MODEL) { var modelStack = inferChildToStack(_script, chance, stack, li.v, oneAttrIdx); var modelRight = _.flattenDeep(resolveModelRight(_script, chance, facts, modelStack)); facts.push(modelRight); return modelStack.child.n; } else if (li.t === TYPE_REF) { var relRefName = [li.t, stack.root.k, li.v].join('-').toLowerCase(); stack.refs[li.v] = relRefName; chance.refsToResolve[li.v] = 'Y'; return relRefName; } else if (li.t === TYPE_LIST) { return resolveList(_script, chance, li.v, stack.child.n, key); } else if (li.t === TYPE_MAPPING) { return resolveMapping(_script, chance, li.v); } else { throw new Error("Should not be here: " + JSON.stringify(stack)); } } else { return li; } }; var columns = _.map(value.values, resolveOneColumn); return { s: stack.child.n, p: key, o: columns }; }; return notWished ? [] : _.map(_.range(attrQuantity), resolveOneAttr); }; var tripleValues = _.map(values, resolveAttr); return tripleValues; }; return _.map(stack.child.v, resolveAttrValues); }; var resolveStack = function(_script, chance, facts, stack) { var triples = resolveModelRight(_script, chance, facts, stack); facts.push(triples); return facts; }; var produceQueryFacts = function(_script, chance, id, value) { if (TYPE_REF !== getValueType(value)) { throw new Error("The query should contain a reference !"); } var ref = _.get(value, 'value.ref'); var stack = { refs: {}, root: { k: id, v: value, n: id }, parent: { k: id, v: value, n: id }, }; var resolveQueryStack = function(n) { var newStack = inferChildToStack(_script, chance, stack, ref, n); return resolveStack(_script, chance, [], newStack); }; var qty = luckyQuantity(_script, chance, value); var r = _.map(_.range(qty), resolveQueryStack); return _.flattenDeep(r); }; var produceRefsFacts = function(_script, chance, runId) { var refs = _.keys(chance.refsToResolve); if (_.isEmpty(refs)) { return []; } var resolveRefStack = function(refId) { var value = _script.get(['refs', refId]).value(); if (TYPE_REF !== getValueType(value)) { throw new Error("The ref should contain a reference: " + refId); } var ref = _.get(value, 'value.ref'); var id = [runId, refId].join('-'); //TODO iterate on all var stack = { refs: {}, root: { k: id, v: value, t: TYPE_REF, n: id }, parent: { k: id, v: value, t: TYPE_REF, n: id }, }; var newStack = inferChildToStack(_script, chance, stack, ref); return resolveStack(_script, chance, [], newStack); }; var r = _.map(refs, resolveRefStack); return _.flattenDeep(r); }; var toChanceWeighting = function(keyValues) { return { name: keyValues[0], weights: _.map(keyValues[1], toNumber) }; }; var createChance = function(params, _script) { var chanceConf = { text: params.text, characters: [{ chars: "0-9a-z", integer: 0 }], weighting: _script.get('weighting').toPairs().map(toChanceWeighting).value() }; var chance = dazzlingChance(chanceConf); chance.counter = 0; chance.refsToResolve = {}; chance.statefulLists = {}; return chance; }; var produceScriptFacts = function(_script, chance, runId) { var modelFacts = function(modelAttributes, model) { var prefixWithModel = function(m) { var id = incCounter(chance); var r = { "i": runId + '-' + m + "-" + id, "s": m, "p": "child-of-fiction-model", "o": [ model ] }; return r; }; var modelAttrs = _.keys(modelAttributes); return _.map(modelAttrs, prefixWithModel); }; var scriptFacts = _script.get('models').map(modelFacts).value(); return _.flattenDeep(scriptFacts); }; var spiceScript = function(cfg, params, script) { script.state = { counter: 0 }; script.cachedList = {}; script.cachedMapping = {}; script.cfg = cfg; script.params = params; return script; }; var scriptToFacts = function(script) { var _script = _.chain(script); var params = script.params; var query = extractModelAttribute(" " + params.query); var chance = createChance(params, _script); var value = _.get(query, 'values[0]'); var queryFacts = produceQueryFacts(_script, chance, params.id, value); var scriptFacts = produceScriptFacts(_script, chance, params.id); var refFacts = produceRefsFacts(_script, chance, params.id); var facts = scriptFacts.concat(queryFacts, refFacts); return facts; }; var asCSVColumn = function(value) { return _.isObject(value) ? "@json@" + JSON.stringify(value) : value; }; var asTripleCSV = function(row) { var o = _.map(row.o, asCSVColumn); var mergedRow = [row.s, row.p].concat(o); var r = S(mergedRow).toCSV(); return r; }; var asCSVResults = function(results) { var rows = _.map(results, asTripleCSV); return rows.join("\n"); }; module.exports = function(cfg) { Joi.assert(cfg, fictionSchema); var fileScriptLoader = function(scriptName) { var scriptPath = cfg.scriptFolder + scriptName + FICTION_SUFFIX; return fs.readFileAsync(scriptPath, 'utf8'); }; var anyScriptLoader = fileScriptLoader; var loadMdScript = function(scriptName) { return anyScriptLoader(scriptName).then(text2obj); }; var readImports = function(info) { return anyScriptLoader(info.scriptName).then(text2Imports).then(function(importList) { var childParents = cloneAndMergeArray(info.parents, info.scriptName); var childInfos = _.map(importList, function(ii) { return { scriptName: ii, parents: childParents }; }); return _.isEmpty(childInfos) ? childParents : bluePromise.map(childInfos, readImports); }); }; var readScriptImports = function(scriptName) { return readImports({ scriptName: scriptName, parents: [] }).reduce(concatArray).then(ensureArray); }; var parseScript = function() { return readScriptImports(cfg.script).map(loadMdScript); }; var parseAndMergeScript = function() { return parseScript().then(mergeScripts); }; var runScript = function(params) { Joi.assert(params, scriptSchema); var localScript = function(v) { return spiceScript(cfg, params, v); }; return parseAndMergeScript() .then(localScript) .then(retrieveLists) .then(retrieveMappings) .then(scriptToFacts); }; var runScriptCsv = function(params) { return runScript(params).then(asCSVResults); }; var fiction = { _parseScript: parseScript, _parseAndMergeScript: parseAndMergeScript, readScriptImports: readScriptImports, runScript: runScript, runScriptCsv: runScriptCsv }; return fiction; };