UNPKG

showandtell

Version:

A Javascript library providing debugger-like command-line interactivity for program state inspection and modification

165 lines (153 loc) 4.9 kB
'use strict' const argParser = require('./argparser') /** * Create an executable command that will be provided with program state * as well as arguments parsed from a description of the command in order * to produce a new state. * * @param {Object} config is an object containing: * @param {String} name is the name of the command which will be matched to invoke it * @param {String} help is a string explaining the argument format for the command * @param {Object} args is a parser & description for the command, see github.com/tj/commander * @param {Function} func is the state transition function to invoke * @public */ function Command (config) { this.commandName = config.name this.commandArgs = config.args this.stateUpdate = config.func this.usageString = config.help } /** * Execute a command in the context of a given state and with a line of input from the * user, not including the command name. * * @param {Object} state is the state that the command is allowed to inspect/manipulate * @param {String} input is a line of input from the user, with the command name removed * @param {Function} next is a function that must be invoked to continue handling commands * @returns the state produced by the command's state transition function * @public */ Command.prototype.execute = function (state, input, next) { const args = argParser.parse(input) let parsedArgs = {} for (let i = this.commandArgs.length - 1; i >= 0; i--) { const arg = this.commandArgs[i] if (i >= args.length) { parsedArgs[arg.name] = arg.default } else { const parser = arg.parser || ((x) => x) parsedArgs[arg.name] = parser(args[i]) } } return this.stateUpdate(state, parsedArgs, next) } /** * Produces a help string describing the command by accumulating information from * the command's name, help (usage string), and information about its arguments. * * @returns a help string to show the user * @public */ Command.prototype.help = function () { let helpString = this.usageString || this.commandName || '' for (const arg of this.commandArgs) { helpString += `\n${arg.name}` if (typeof arg.default !== 'undefined') { helpString += ` - \tdefault: ${arg.default}` } if (typeof arg.help !== 'undefined') { helpString += ` - \t${arg.help}` } } return helpString } /** * Get the name of a command. * * @public */ Command.prototype.name = function () { return this.commandName } /** * Get the list of arguments the command accepts. * * @public */ Command.prototype.args = function () { return this.commandArgs } /** * A command that can be used to set a value in the state provided. * Its argument is a string describing the path to the variable. For example, * to set the value of `number` in the state * { values: { numeric: { number: 1 } } } * one would provide the variable name "values.numeric.number" * * @public */ const set = new Command({ name: 'SET', args: [ {name: 'variable', help: 'A path to the variable to set, e.g. state.hello.world'}, {name: 'value', help: 'The value to assign'} ], func: function (state, args, next) { const path = args.variable.split('.') function assignToState (obj, attrPath, newValue) { if (attrPath.length === 0) { return obj } else if (attrPath.length === 1) { obj[attrPath[0]] = newValue } else { let nextObj = obj.hasOwnProperty(attrPath[0]) ? obj[attrPath[0]] : {} obj[attrPath[0]] = assignToState(nextObj, attrPath.slice(1), newValue) } return obj } state = assignToState(state, path, args.value) next(null, state) } }) /** * A command that allows for the inspection of a value in the state provided. * Its argument is a string describing the path to the variable. For example, * to inspect the value of `number` in the state * { values: { numeric: { number: 1 } } } * one would provide the variable name "values.numeric.number" * * @public */ const show = new Command({ name: 'SHOW', args: [ {name: 'variable', help: 'A path to the variable to show, e.g. state.hello.world'} ], func: function (state, args, next) { if (typeof args.variable === 'undefined') { console.log(JSON.stringify(state, null, 4)) next(null, state) } else { const path = args.variable.split('.') const value = path.reduce(function (currentState, nextKey) { if (typeof currentState === 'undefined') { return undefined } else { return currentState[nextKey] } }, state) if (typeof value !== 'undefined') { console.log(value) next(null, state) } else { next(new Error(`Could not access variable ${args.variable} in ${state}`), state) } } } }) module.exports = { Command: Command, set: set, show: show }