@eluvio/elv-utils-js
Version:
Utilities for the Eluvio Content Fabric
346 lines (284 loc) • 13.3 kB
JavaScript
const R = require('@eluvio/ramda-fork')
const assign = require('crocks/helpers/assign')
const curry = require('crocks/helpers/curry')
const getProp = require('crocks/Maybe/getProp')
const getPropOr = require('crocks/helpers/getPropOr')
const liftA2 = require('crocks/helpers/liftA2')
const liftA3 = require('crocks/helpers/liftA3')
const map = require('crocks/pointfree/map')
const maybeToResult = require('crocks/Result/maybeToResult')
const Result = require('crocks/Result')
const {Ok} = Result
const setProp = require('crocks/helpers/setProp')
const unsetProp = require('crocks/helpers/unsetProp')
const kindOf = require('kind-of')
const objectPath = require('object-path')
const defObjectModel = require('@eluvio/elv-js-helpers/ModelFactory/defObjectModel')
const isModel = require('@eluvio/elv-js-helpers/Boolean/isModel')
const {CheckedNonBlankString, CheckedAbsentPropName, CheckedPresentPropName} = require('./models/Models')
const {CheckedWidgetData, EmptyWidgetData} = require('./models/WidgetData')
const {CheckedOptDef, CheckedOptDefMap, CheckedOptDefOverride, yargsOptFields} = require('./models/OptDef')
const {CheckedBlueprint} = require('./models/Blueprint')
const StandardOptions = require('./StandardOptions')
const {
camel2kebab, compare, join,
objUnwrapValues, subst,
valOrThrow
} = require('./helpers')
// =====================================
// utility functions
// =====================================
// Modified version of `assign()` to allow unsetting properties in base by passing in null as property value
// Used for ModOpt to allow e.g. removal of default value
const assignWithNull = curry(
(overrides, base) => {
const result = assign(overrides, base)
for (const [k, v] of Object.entries(overrides)) {
if (v === null) {
result[k] = undefined
}
}
return result
}
)
const fEnsureOptNameAlreadyExists = (rAccOptDefMap, rName) => join(liftA2(CheckedPresentPropName('not found'), rAccOptDefMap, rName))
const fEnsureOptNameNotAlreadyAdded = (rAccOptDefMap, rName) => join(liftA2(CheckedAbsentPropName('already added'), rAccOptDefMap, rName))
const fEnsureIsStdOptName = (rName) => join(map(CheckedPresentPropName('is not a standard option', StandardOptions), rName))
const fGetPropResult = curry((propName, object) => maybeToResult(Error(`property "${propName}" not found.`), getProp(propName, object)))
const fGetStdOption = (optName) => StandardOptions[optName]
const fLiftedAssign = liftA2(assign)
const fLiftedAssignWithNull = liftA2(assignWithNull)
const fLiftedSetProp = liftA3(setProp)
const fLiftedUnsetProp = liftA2(unsetProp)
const fLiftedCheckedOptDef = (rOptDef) => join(map(CheckedOptDef, rOptDef))
// =====================================
// Handlers for option add/modify/delete/standard
// =====================================
const _addStdOpt = curry((accOptDefMap, optName, overrides) => {
// console.log(`_addStdOpt(${JSON.stringify(accOptDefMap)}, ${JSON.stringify(optName)}, ${JSON.stringify(overrides)})`);
const rAccOptDefMap = CheckedOptDefMap(accOptDefMap)
const rSafeNonBlankName = CheckedNonBlankString(optName)
const rSafeNewName = fEnsureOptNameNotAlreadyAdded(rAccOptDefMap, rSafeNonBlankName)
const rSafeStdOptName = fEnsureIsStdOptName(rSafeNewName)
const rStdOption = map(fGetStdOption, rSafeStdOptName)
const rOverrides = CheckedOptDefOverride(overrides)
const rStdOptWithOverrides = fLiftedAssign(rOverrides, rStdOption)
const rValStdOptWithOverrides = fLiftedCheckedOptDef(rStdOptWithOverrides)
return fLiftedSetProp(rSafeStdOptName, rValStdOptWithOverrides, rAccOptDefMap)
})
const _delOpt = curry((accOptDefMap, optName) => {
// console.log(`_delOpt(${JSON.stringify(accOptDefMap)}, ${JSON.stringify(optName)})`);
const rAccOptDefMap = CheckedOptDefMap(accOptDefMap)
const rSafeNonBlankName = CheckedNonBlankString(optName)
const rSafeExistingName = fEnsureOptNameAlreadyExists(rAccOptDefMap, rSafeNonBlankName)
return fLiftedUnsetProp(rSafeExistingName, rAccOptDefMap)
})
const _modOpt = curry((accOptDefMap, optName, overrides) => {
// console.log(`_modOpt(${JSON.stringify(accOptDefMap)}, ${JSON.stringify(optName)}, ${JSON.stringify(overrides)})`);
const rAccOptDefMap = CheckedOptDefMap(accOptDefMap)
const rSafeNonBlankName = CheckedNonBlankString(optName)
const rSafeExistingName = fEnsureOptNameAlreadyExists(rAccOptDefMap, rSafeNonBlankName)
const rExistingOptDef = join(liftA2(fGetPropResult, rSafeExistingName, rAccOptDefMap))
const rOverrides = CheckedOptDefOverride(overrides)
const rExistingOptWithOverrides = fLiftedAssignWithNull(rOverrides, rExistingOptDef)
const rValOptWithOverrides = fLiftedCheckedOptDef(rExistingOptWithOverrides)
return fLiftedSetProp(rSafeExistingName, rValOptWithOverrides, rAccOptDefMap)
})
const _newOpt = (accOptDefMap, optName, newOptDef) => {
// console.log(`_newOpt(${JSON.stringify(accOptDefMap)}, ${JSON.stringify(optName)}, ${JSON.stringify(spec)})`);
const rAccOptDefMap = CheckedOptDefMap(accOptDefMap)
const rSafeNonBlankName = CheckedNonBlankString(optName)
const rSafeNewName = fEnsureOptNameNotAlreadyAdded(rAccOptDefMap, rSafeNonBlankName)
const rNewOptDef = CheckedOptDef(newOptDef)
return fLiftedSetProp(rSafeNewName, rNewOptDef, rAccOptDefMap)
}
// =====================================
// Conversion to yargs
// =====================================
// Used to sort [name, spec] pairs
// Forces "Options: (Main)" group to be last
const compareYargsOptKVPairs = (a, b) => {
const aName = a[0].toUpperCase()
const bName = b[0].toUpperCase()
const aGroup = a[1].group === 'Options: (Main)'
? 'Z'
: a[1].group.toUpperCase()
const bGroup = b[1].group === 'Options: (Main)'
? 'Z'
: b[1].group.toUpperCase()
return aGroup === bGroup
? compare(aName, bName) // both are in same group, sort by arg name
: compare(aGroup, bGroup) // they are in different groups, sort by group name
}
// return desc string created by doing var substitution on descTemplate
const renderDesc = optDef => {
const descTemplate = optDef.descTemplate || ''
const forX = optDef.forX ? ` for ${optDef.forX}` : null
const ofX = optDef.ofX ? ` of ${optDef.ofX}` : null
const X = optDef.X ? ` ${optDef.X}` : null
const finalX = forX || ofX || X || ''
return subst({X: finalX}, descTemplate)
}
// returns an array [...old alias(es), aliasToAdd]
const mergedAliases = curry((aliasToAdd, optDef) => {
const oldAliases = getPropOr([], 'alias', optDef)
return R.uniq([oldAliases, aliasToAdd].flat())
})
const wrapCoerceModel = (optName, optDef) => isModel(optDef.coerce)
? x => {
defObjectModel(optName, {[`--${optName}`]: optDef.coerce})({[`--${optName}`]: x})
return x
}
: optDef.coerce
const optDef2YargsOpt = (kvPair) => {
const [optName, optDef] = kvPair
// validate
const rOptDef = CheckedOptDef(optDef)
// create description from descTemplate, substituting {forX, ofX} if needed
const itemsToMerge = {
desc: join(rOptDef.map(renderDesc))
}
// add alias if option is camel-cased
const kebabCase = camel2kebab(optName)
if (kebabCase !== optName) {
itemsToMerge.alias = join(rOptDef.map(mergedAliases(kebabCase)))
}
// add string: true if type == "string"
if (join(rOptDef.map(getPropOr('', 'type'))) === 'string') {
itemsToMerge.string = true
}
const oldGroup = join(rOptDef.map(getPropOr(false, 'group')))
itemsToMerge.group = `Options: (${oldGroup ? oldGroup : 'Main'})`
// set requiresArg if not boolean
const type = join(rOptDef.map(getPropOr(false, 'type')))
if (type !== 'boolean') {
itemsToMerge.requiresArg = true
}
// convert coerce if it is a model
itemsToMerge.coerce = wrapCoerceModel(optName, optDef)
const rMergedOptDef = rOptDef.map(assign(itemsToMerge))
// remove props that don't belong in a YargsOpt
const rYargsOpt = rMergedOptDef.map(R.pick(R.keys(yargsOptFields)))
return [optName, rYargsOpt]
}
// create a new object with properties added in an order which alphabetizes by group
// then arg name, with "Options (Main)" last
const sortYargsOptMap = R.pipe(
R.toPairs,
R.sort(compareYargsOptKVPairs),
R.fromPairs
)
const OptDefMap2YargsOptMap = (optDefMap) => {
// validate
const rOptDefMap = CheckedOptDefMap(optDefMap)
return rOptDefMap.map((optDefMap) => {
const kvPairs = R.toPairs(optDefMap)
const yargsKVPairs = kvPairs.map(optDef2YargsOpt)
return R.fromPairs(yargsKVPairs)
})
}
// =====================================
// Compilation and concerns
// =====================================
const reduceOptions = curry((rAccumulator, optionsList) => optionsList.reduce(fOptDefMapReducer, rAccumulator))
const fOptDefMapReducer = (rAccOptDefMap, optFunc) => join(rAccOptDefMap.map(optFunc))
const fConcernsReducer = (rAccWidget, concern) => {
// console.log("fConcernsReducer: " + concern.blueprint.name);
const rConcernWidgetData = CheckedWidgetData(WidgetDataFromBlueprint(concern.blueprint))
const rConcernOptDefMap = rConcernWidgetData.map(getPropOr({}, 'optDefMap'))
const rAccOptDefMap = rAccWidget.map(getPropOr({}, 'optDefMap'))
const mergedOptDefMap = fLiftedAssign(rConcernOptDefMap, rAccOptDefMap)
const rConcernChecksMap = rConcernWidgetData.map(getPropOr({}, 'checksMap'))
const rAccChecksMap = rAccWidget.map(getPropOr({}, 'checksMap'))
const mergedChecksMap = fLiftedAssign(rConcernChecksMap, rAccChecksMap)
const rAccWidgetWithMergedOptSpecs = fLiftedSetProp(Ok('optDefMap'), mergedOptDefMap, rAccWidget)
return fLiftedSetProp(Ok('checksMap'), mergedChecksMap, rAccWidgetWithMergedOptSpecs)
}
const concernsToWidget = (concernArray) => concernArray.reduce(fConcernsReducer, Ok(EmptyWidgetData()))
const WidgetDataFromBlueprint = (blueprint) => {
// console.log("WidgetDataFromBlueprint called: " + blueprint.concerns.length
// + " concern(s), " + blueprint.options.length + " option(s)");
const rCheckedBlueprint = CheckedBlueprint(blueprint)
// assemble concerns
const rConcerns = rCheckedBlueprint.map(getPropOr([], 'concerns'))
const rConcernsWidget = join(rConcerns.map(concernsToWidget))
const rConcernsOptDefMap = rConcernsWidget.map(getPropOr({}, 'optDefMap'))
const rBlueprintOptions = rCheckedBlueprint.map(getPropOr([], 'options'))
const rBlueprintOptDefMap = join(rBlueprintOptions.map(reduceOptions(rConcernsOptDefMap)))
const rWidgetWithOptDefMap = fLiftedSetProp(Ok('optDefMap'), rBlueprintOptDefMap, Ok({}))
const rConcernsChecksMap = rConcernsWidget.map(getPropOr({}, 'checksMap'))
// merge self's checks (if any)
const rBlueprintChecksMap = rCheckedBlueprint.map(getPropOr({}, 'checksMap'))
const rMergedChecksMap = fLiftedAssign(rBlueprintChecksMap, rConcernsChecksMap)
const rWidgetWithChecksMap = fLiftedSetProp(Ok('checksMap'), rMergedChecksMap, rWidgetWithOptDefMap)
const rYargsOptMap = join(rBlueprintOptDefMap.map(OptDefMap2YargsOptMap))
const rUnwrappedValYargsOptMap = join(rYargsOptMap.map(objUnwrapValues))
const rSortedYargsOptMap = rUnwrappedValYargsOptMap.map(sortYargsOptMap)
const rWidgetWithYargsOpts = fLiftedSetProp(Ok('yargsOptMap'), rSortedYargsOptMap, rWidgetWithChecksMap)
// validate, then either throw error or return
const rCheckedWidgetData = join(rWidgetWithYargsOpts.map(CheckedWidgetData))
return valOrThrow(rCheckedWidgetData)
}
const BuildWidget = (blueprint) => {
const widgetData = WidgetDataFromBlueprint(blueprint)
const data = () => widgetData
return {data}
}
const objectPathList = (obj, parentKeys = []) => {
let ret = []
for (const [k, v] of R.toPairs(obj)) {
switch (kindOf(v)) {
case 'object':
ret.concat(objectPathList(v, [...parentKeys, k]))
break
case 'undefined':
case 'null':
break
default:
ret.concat([...parentKeys, k])
}
}
return ret
}
// convert an args object back into a command line arguments list
const argsMapToArgList = (argsMap) => {
let list = []
for (const [k, v] of R.toPairs(argsMap)) {
if (v !== undefined && v !== null) { // omit any options without values
if (v !== true) { // skip value for boolean flag values
switch (kindOf(v)) {
case 'array':
list.push(`--${k}`)
list = list.concat(v.map(x => x.toString()))
break
case 'object':
for (const onePathArray of objectPathList(v)) {
list.push(`--${k}.${onePathArray.join('.')}`)
list.push(objectPath.get(v, `${onePathArray}`))
}
break
default:
list.push(`--${k}`)
list.push(`${v}`)
}
}
}
}
return list
}
// The four functions available to use in Blueprint.options array
const DelOpt = (optName) => accOptDefMap => _delOpt(accOptDefMap, optName)
const ModOpt = (optName, overrides) => accOptDefMap => _modOpt(accOptDefMap, optName, overrides)
const NewOpt = (optName, newOptDef) => accOptDefMap => _newOpt(accOptDefMap, optName, newOptDef)
const StdOpt = (optName, overrides = {}) => accOptDefMap => _addStdOpt(accOptDefMap, optName, overrides)
module.exports = {
argsMapToArgList,
BuildWidget,
DelOpt,
ModOpt,
NewOpt,
StdOpt,
_addStdOpt,
fConcernsReducer,
optDef2YargsOpt
}