showandtell
Version:
A Javascript library providing debugger-like command-line interactivity for program state inspection and modification
165 lines (153 loc) • 4.9 kB
JavaScript
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
}