From 5722c050918df85d208e7bfa26fa4cf104e405eb Mon Sep 17 00:00:00 2001 From: JCThePants Date: Wed, 29 Apr 2020 17:48:45 -0700 Subject: [PATCH] Initial commit --- .github/workflows/build.yaml | 57 ++++++ .github/workflows/publish-release.yaml | 16 ++ .gitignore | 5 + LICENSE | 21 ++ README.md | 31 +++ index.js | 12 ++ libs/class.Category.js | 50 +++++ libs/class.CliServer.js | 243 +++++++++++++++++++++++ libs/class.Command.js | 238 ++++++++++++++++++++++ libs/class.CommandArg.js | 75 +++++++ libs/class.CommandArgs.js | 99 ++++++++++ libs/class.CommandDispatcher.js | 192 ++++++++++++++++++ libs/class.CommandError.js | 28 +++ libs/class.CommandParameter.js | 114 +++++++++++ libs/class.Commands.js | 151 ++++++++++++++ libs/service.argParser.js | 261 +++++++++++++++++++++++++ libs/service.cli.js | 74 +++++++ libs/service.cmdParser.js | 66 +++++++ package-lock.json | 245 +++++++++++++++++++++++ package.json | 32 +++ scripts/cli.js | 88 +++++++++ tests/mocha.Command.js | 120 ++++++++++++ tests/mocha.CommandArg.js | 147 ++++++++++++++ tests/mocha.CommandArgs.js | 91 +++++++++ tests/mocha.CommandDispatcher.js | 206 +++++++++++++++++++ tests/mocha.CommandParameter.js | 137 +++++++++++++ tests/mocha.Commands.js | 178 +++++++++++++++++ tests/mocha.argParser.js | 178 +++++++++++++++++ tests/mocha.cmdParser.js | 179 +++++++++++++++++ 29 files changed, 3334 insertions(+) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/publish-release.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.js create mode 100644 libs/class.Category.js create mode 100644 libs/class.CliServer.js create mode 100644 libs/class.Command.js create mode 100644 libs/class.CommandArg.js create mode 100644 libs/class.CommandArgs.js create mode 100644 libs/class.CommandDispatcher.js create mode 100644 libs/class.CommandError.js create mode 100644 libs/class.CommandParameter.js create mode 100644 libs/class.Commands.js create mode 100644 libs/service.argParser.js create mode 100644 libs/service.cli.js create mode 100644 libs/service.cmdParser.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/cli.js create mode 100644 tests/mocha.Command.js create mode 100644 tests/mocha.CommandArg.js create mode 100644 tests/mocha.CommandArgs.js create mode 100644 tests/mocha.CommandDispatcher.js create mode 100644 tests/mocha.CommandParameter.js create mode 100644 tests/mocha.Commands.js create mode 100644 tests/mocha.argParser.js create mode 100644 tests/mocha.cmdParser.js diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..c0c5616 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,57 @@ +name: Build & Test Package +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + linux-node10: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '10.x' + - run: npm config set "@mintpond:registry" https://npm.pkg.github.com/mintpond + - run: npm config set //npm.pkg.github.com/:_authToken ${{secrets.NPM_INSTALL_TOKEN}} + - run: npm install + - run: npm test + + linux-node12: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '12.x' + - run: npm config set "@mintpond:registry" https://npm.pkg.github.com + - run: npm config set //npm.pkg.github.com/:_authToken ${{secrets.NPM_INSTALL_TOKEN}} + - run: npm install + - run: npm test + + windows-node10: + runs-on: windows-2016 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '10.x' + - run: npm config set "@mintpond:registry" https://npm.pkg.github.com + - run: npm config set //npm.pkg.github.com/:_authToken ${{secrets.NPM_INSTALL_TOKEN}} + - run: npm install + - run: npm test + + windows-node12: + runs-on: windows-2016 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '12.x' + - run: npm config set "@mintpond:registry" https://npm.pkg.github.com + - run: npm config set //npm.pkg.github.com/:_authToken ${{secrets.NPM_INSTALL_TOKEN}} + - run: npm install + - run: npm test \ No newline at end of file diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml new file mode 100644 index 0000000..4154a3b --- /dev/null +++ b/.github/workflows/publish-release.yaml @@ -0,0 +1,16 @@ +name: Publish Package +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '10.x' + - run: npm config set "@mintpond:registry" https://npm.pkg.github.com/mintpond + - run: npm config set //npm.pkg.github.com/:_authToken ${{secrets.GITHUB_TOKEN}} + - run: npm install + - run: npm publish \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d274723 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.idea/ +.forever/ +.vscode/ +*.iml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..787f0c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 JCThePants, contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..df463d1 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +mint-commands +============= + +This module contains CLI command line interface building utilities for NodeJS used by [MintPond Mining Pool](https://mintpond.com). + +## Install ## +__Install as Dependency in NodeJS Project__ +```bash +# Install from Github NPM repository + +npm config set @mintpond:registry https://npm.pkg.github.com/mintpond +npm config set //npm.pkg.github.com/:_authToken + +npm install @mintpond/mint-commands@1.0.0 --save +``` +[Creating a personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) + +__Install & Test__ +```bash +# Install nodejs v10 +curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +sudo apt-get install nodejs -y + +# Download mint-commands +git clone https://github.com/MintPond/mint-commands + +# build & test +cd mint-commands +npm install +npm test +``` \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..1d0fcb8 --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + CliServer: require('./libs/class.CliServer'), + Command: require('./libs/class.Command'), + CommandDispatcher: require('./libs/class.CommandDispatcher'), + CommandError: require('./libs/class.CommandError'), + Commands: require('./libs/class.Commands'), + argParser: require('./libs/service.argParser'), + cli: require('./libs/service.cli'), + cmdParser: require('./libs/service.cmdParser') +}; \ No newline at end of file diff --git a/libs/class.Category.js b/libs/class.Category.js new file mode 100644 index 0000000..afaf650 --- /dev/null +++ b/libs/class.Category.js @@ -0,0 +1,50 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + pu = require('@mintpond/mint-utils').prototypes, + Command = require('./class.Command'); + + +/** + * A command definition to use where an actual command is not defined (i.e. categories). + */ +class Category extends Command { + + /** + * Constructor. + * + * @param args + * @param args.path {string} + * @param [args.description] {string} + */ + constructor(args) { + precon.string(args.path, 'path'); + precon.opt_string(args.description, 'description'); + + super(args); + } + + + /* Override */ + execute(argMap, callback) { + throw new Error('Cannot execute a category.'); + } + + /* Override */ + toJSON() { + const _ = this; + return { + path: _.path, + description: _.description + }; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'Category') && + obj instanceof Command; + } +} + +module.exports = Category; \ No newline at end of file diff --git a/libs/class.CliServer.js b/libs/class.CliServer.js new file mode 100644 index 0000000..4e8420a --- /dev/null +++ b/libs/class.CliServer.js @@ -0,0 +1,243 @@ +'use strict'; + +const + EventEmitter = require('events'), + net = require('net'), + precon = require('@mintpond/mint-precon'), + mu = require('@mintpond/mint-utils'), + pu = require('@mintpond/mint-utils').prototypes, + CommandDispatcher = require('./class.CommandDispatcher'); + +/** + * A server to receive and execute commands. + */ +class CliServer extends EventEmitter { + + /** + * Constructor. + * + * @param args + * @param args.cmdDispatcher {CommandDispatcher} + */ + constructor(args) { + precon.instanceOf(args.cmdDispatcher, CommandDispatcher, 'cmdDispatcher'); + + super(); + + const _ = this; + _._cmdDispatcher = args.cmdDispatcher; + + _._clientId = 0; + _._server = null; + _._clientsMap = new Map(); + } + + + /** + * Name of the event emitted when a command is received. + * @returns {string} + */ + static get EVENT_CMD_RECEIVED() { return 'cmdReceived'; } + + /** + * Name of the event emitted when an unparsable command is received. + * @returns {string} + */ + static get EVENT_INVALID_CMD_RECEIVED() { return 'invalidCmdReceived'; } + + /** + * Name of the event emitted when a socket error occurs. + * @returns {string} + */ + static get EVENT_SOCKET_ERROR() { return 'socketError'; } + + + + /** + * Start server. + * + * @param host {string} + * @param port {number} + * @param [callback] {function()} + */ + start(host, port, callback) { + precon.string(host, 'host'); + precon.minMaxInteger(port, 1, 65535, 'port'); + precon.opt_funct(callback, 'callback'); + + const _ = this; + + if (_._server) + throw new Error('CLI server is already started.'); + + _._server = net.createServer(_._onClientConnect.bind(_)); + _._server.listen(port, host, () => { + callback && callback(); + }); + } + + + /** + * Stop server. + * + * @param [callback] {function()} + */ + stop(callback) { + precon.opt_funct(callback, 'callback'); + + const _ = this; + if (_._server) { + _._server.close(() => { + callback && setImmediate(callback); + callback = null; + }); + + for (const client of _._clientsMap.values()) { + client.destroy(); + } + _._server = null; + } + else { + callback && setImmediate(callback); + } + } + + + _onClientConnect(socket) { + + const _ = this; + + _._clientId++; + socket.clientId = _._clientId; + _._clientsMap.set(socket.clientId, socket); + + let stringBuffer = ''; + + // DATA + socket.on('data', data => { + + if (!data) + return; + + stringBuffer += data.toString(); + if (stringBuffer.endsWith('\n')) { + + const parts = stringBuffer.split('\n'); + stringBuffer = ''; + + parts.forEach(json => { + + if (!json) + return; + + const message = _parseJSON(json); + if (message) { + + _.emit(CliServer.EVENT_CMD_RECEIVED, { message: message, ip: socket.remoteAddress, json: json }); + + _._executeCommand(message, (err, repliesArr) => { + if (err) { + socket.write(`${message.id || 0}:\n${JSON.stringify(err)}\n\n`); + } + else { + socket.write(`${message.id || 0}:\n${JSON.stringify(repliesArr)}\n\n`); + } + }); + + } else { + _.emit(CliServer.EVENT_INVALID_CMD_RECEIVED, { ip: socket.remoteAddress, json: json }); + } + }); + } + + }); + + // ERROR + socket.on('error', err => { + const _ = this; + if (err.code !== 'ECONNRESET') { + _.emit(CliServer.EVENT_SOCKET_ERROR, { error: err, ip: socket.remoteAddress }); + } + }); + + socket.on('close', () => { + const _ = this; + _._clientsMap.delete(socket.clientId); + }); + } + + + _executeCommand(message, callback) { + const _ = this; + if (!Array.isArray(message.query) && !mu.isString(message.query)) { + callback('Invalid message format.'); + return; + } + + let parsed; + + try { + parsed = _._cmdDispatcher.parseQuery(message.query); + } + catch (err) { + if (err.isCommandError) { + callback(err.message); + return; + } + throw err; + } + + if (parsed === false) { + callback('Command not found'); + } + else if (parsed.isHelp) { + try { + _._cmdDispatcher.help(parsed.path, (err, helpStr) => { + callback(err, [helpStr]); + }); + } + catch (err) { + callback({ + msg: 'Exception while executing CLI command help', + path: parsed.path, + error: err.toString(), + stack: err.stack + }); + } + } + else { + try { + _._cmdDispatcher.execute(parsed.path, parsed.argsOMap, callback); + } + catch (err) { + callback({ + msg: 'Exception while executing CLI command', + path: parsed.path, + argsOMap: parsed.argsOMap, + error: err.toString(), + stack: err.stack + }); + } + } + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CliServer') && + pu.isFunction(obj.start) && + pu.isFunction(obj.stop); + } +} + + +function _parseJSON(json) { + try { + return JSON.parse(json); + } + catch (err) { + return false; + } +} + + +module.exports = CliServer; \ No newline at end of file diff --git a/libs/class.Command.js b/libs/class.Command.js new file mode 100644 index 0000000..6b5dcbe --- /dev/null +++ b/libs/class.Command.js @@ -0,0 +1,238 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + pu = require('@mintpond/mint-utils').prototypes, + CommandParameter = require('./class.CommandParameter'); + + +/** + * A command definition + */ +class Command { + + /** + * Constructor. + * + * @param args + * @param args.path {string} + * @param [args.params] {string[]} + * @param [args.options] {string[]} + * @param [args.flags] {string[]} + * @param [args.extra] {{}} + * @param [args.description] {string} + * @param [args.execute] {function(args:{},finish:function(err:object,result:object))} + */ + constructor(args) { + precon.string(args.path, 'path'); + precon.opt_array(args.params, 'params'); + precon.opt_array(args.options, 'options'); + precon.opt_array(args.flags, 'flags'); + precon.opt_obj(args.extra, 'extra'); + precon.opt_string(args.description, 'description'); + precon.opt_funct(args.execute, 'execute'); + + const _ = this; + _._path = args.path; + _._paramsOMap = {}; + _._paramsArr = (args.params || []).map(param => { + param = param instanceof CommandParameter ? param : new CommandParameter(param); + _._paramsOMap[param.name] = param; + return param; + }); + _._optionsOMap = {}; + _._optionsArr = (args.options || []).map(param => { + param = param instanceof CommandParameter ? param : new CommandParameter(param); + _._optionsOMap[param.name] = param; + return param; + }); + _._flagsOMap = {}; + _._flagsArr = (args.flags || []).map(param => { + param = param instanceof CommandParameter ? param : new CommandParameter(param, true/*isFlag*/); + _._paramsOMap[param.name] = param; + return param; + }); + _._extra = args.extra || {}; + _._description = args.description || ''; + _._executeCallback = args.execute || ((args, finish) => { + finish(); + }); + } + + + /** + * The period delimited command path. The path is used to categorize commands and break them up into smaller easier + * to display help lists. (i.e. 'bigsys.subsystem.commandName'). Only lowercase letters should be used. + * @returns {string} + */ + get path() { return this._path; } + + /** + * An array of parameters whose arguments are expected as a series of arguments after the command whose order will + * map the the order of parameters in the array. + * @returns {CommandParameter[]} + */ + get paramsArr() { return this._paramsArr; } + + /** + * An array of parameters options whose arguments are expected as a dash and name of the parameter followed by the + * argument. (i.e. 'command -param1 arg1') + * @returns {CommandParameter[]} + */ + get optionsArr() { return this._optionsArr; } + + /** + * An array of parameter flags whose arguments are expected as a double dash and name of the parameter to indicate + * true. The absence of the parameter flag indicates false. + * argument. (i.e. 'command --flag1') + * @returns {CommandParameter[]} + */ + get flagsArr() { return this._flagsArr; } + + /** + * A description of the parameter for use in the auto generated usage. + * @returns {string} + */ + get description() { return this._description; } + + /** + * An auto generated usage description. + * @returns {string} + */ + get usage() { return this.$generateUsage(); } + + /** + * An auto generated usage description with more verbose information. + * @returns {string} + */ + get verboseUsage() { return this.$generateVerboseUsage(); } + + /** + * An object containing extra data associated with the command. + * @returns {{}} + */ + get extra() { return this._extra; } + + + /** + * Execute the command using an object map of arguments. + * + * @param argMap {{}} An object map containing arguments to pass into the command. + * @param callback {function(error:*,result:*)} A function to be called when the + * command execution is completed and to pass a result back. + */ + execute(argMap, callback) { + precon.obj(argMap, 'argMap'); + precon.funct(callback, 'callback'); + + const _ = this; + try { + _._executeCallback(argMap, callback); + } + catch(err) { + callback(err.stack, null); + } + } + + + toJSON() { + const _ = this; + return { + path: _.path, + params: _.params, + options: _.options, + flags: _.flags, + extra: _.extra, + usage: _.usage, + description: _.description + }; + } + + + $generateUsage() { + + const _ = this; + const pathArray = _._path.split('.'); + const name = pathArray[pathArray.length - 1]; + const rootPath = pathArray.splice(0); + rootPath.pop(); + + const buffer = []; + + buffer.push(rootPath.join(' ')); + buffer.push(name); + + // params + _._paramsArr.forEach(param => { + const isRequired = !param.hasDefaultValue; + buffer.push(`${isRequired ? '<' : '['}${param.name}${isRequired ? '>' : ']'}`); + }); + + // options + _._optionsArr.forEach(option => { + buffer.push(`[-${option.name}]`); + }); + + // flags + _._flagsArr.forEach(option => { + buffer.push(`[--${option.name}]`); + }); + + return buffer.join(' '); + } + + + $generateVerboseUsage() { + + const _ = this; + + const buffer = [_.$generateUsage(), '\n']; + buffer.push(`${_._description}`); + + const paramsArr = _._paramsArr.filter(param => !!param.description); + if (paramsArr.length) { + buffer.push('\n'); + paramsArr.forEach(param => { + if (param.hasDefaultValue) { + buffer.push(` ${param.name} - ${param.description}\n`); + } + else { + buffer.push(` ${param.name}=${param.defaultValue} - ${param.description}\n`); + } + }); + } + + const optionsArr = _._optionsArr.filter(option => !!option.description); + if (optionsArr.length) { + buffer.push('\n'); + optionsArr.forEach(param => { + if (param.hasDefaultValue) { + buffer.push(` -${param.name} - ${param.description}\n`) + } + else { + buffer.push(` -${param.name} - ${param.description}\n`) + } + }); + } + + const flagsArr = _._flagsArr.filter(flag => !!flag.description); + if (flagsArr.length) { + buffer.push('\n'); + flagsArr.forEach(param => { + buffer.push(` --${param.name} - ${param.description}\n`) + }); + } + + return buffer.join(''); + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'Command') && + pu.isFunction(obj.execute) && + pu.hasGetters(obj, 'path', 'paramsArr', 'optionsArr', 'flagsArr', 'description', + 'usage', 'verboseUsage', 'extra'); + } +} + +module.exports = Command; \ No newline at end of file diff --git a/libs/class.CommandArg.js b/libs/class.CommandArg.js new file mode 100644 index 0000000..a5f60d9 --- /dev/null +++ b/libs/class.CommandArg.js @@ -0,0 +1,75 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + mu = require('@mintpond/mint-utils'), + pu = require('@mintpond/mint-utils').prototypes; + + +/** + * A parsed argument for a CommandParameter. + */ +class CommandArg { + + /** + * Constructor. + * + * @param parameter {CommandParameter} The parameter the argument is for. + * @param value {string|boolean} The argument value. + */ + constructor(parameter, value) { + precon.notNull(parameter, 'parameter'); + + const _ = this; + const isDefaultValue = (!mu.isString(value) && !mu.isBoolean(value)) || value === '' || value === false; + _._parameter = parameter; + _._value = isDefaultValue ? parameter.defaultValue : value; + _._isDefaultValue = isDefaultValue; + } + + + /** + * The name of the parameter the argument is for. + * @returns {string} + */ + get name() { return this._parameter.name; } + + /** + * The parameter the argument is for. + * @returns {CommandParameter} + */ + get parameter() { return this._parameter; } + + /** + * The parsed argument value. + * @returns {string} + */ + get value() { return this._value; } + + /** + * Determine if the argument value is the default value. + * + * This will return true only if no value was provided. If the value provided is the same as the default value then + * this will still return false. + * @returns {boolean} + */ + get isDefaultValue() { return this._isDefaultValue; } + + + toJSON() { + const _ = this; + return { + name: _.name, + value: _.value, + isDefaultValue: _.isDefaultValue + }; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CommandArg') && + pu.hasGetters(obj, 'name', 'parameter', 'value', 'isDefaultValue'); + } +} + +module.exports = CommandArg; \ No newline at end of file diff --git a/libs/class.CommandArgs.js b/libs/class.CommandArgs.js new file mode 100644 index 0000000..a67f13b --- /dev/null +++ b/libs/class.CommandArgs.js @@ -0,0 +1,99 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + pu = require('@mintpond/mint-utils').prototypes, + CommandArg = require('./class.CommandArg'); + + +/** + * Parsed command arguments. + */ +class CommandArgs { + + /** + * Constructor. + * + * @param command {Command} The command the arguments are for. + * @param [args] {{params:CommandArg[],options:{},flags:{}}} + */ + constructor(command, args) { + precon.notNull(command, 'command'); + precon.opt_obj(args, 'args'); + + args = args || {}; + precon.opt_array(args.params, 'args.params'); + precon.opt_obj(args.options, 'args.options'); + precon.opt_obj(args.flags, 'args.flags'); + + const _ = this; + _._paramsArr = args.params || []; + _._optionsOMap = args.options || {}; + _._flagsOMap = args.flags || {}; + _._argsOMap = {}; + + _._paramsArr.forEach((arg, i) => { + const param = command.paramsArr[i]; + if (param) { + _._argsOMap[param.name] = arg ? arg.value : param.defaultValue; + } + }); + + Object.keys(_._optionsOMap).forEach(name => { + _._argsOMap[name] = _._optionsOMap[name].value; + }); + + command.flagsArr.forEach(flag => { + let arg = _._flagsOMap[flag.name]; + if (!arg) + arg = new CommandArg(flag, false); + + _._flagsOMap[flag.name] = arg; + _._argsOMap[flag.name] = arg.value; + }); + } + + + /** + * Get parameter arguments array. + * @returns {CommandArg[]} + */ + get paramsArr() { return this._paramsArr; } + + /** + * Get options arguments in an object map of CommandArg by parameter name. + * @returns {{}} + */ + get optionsOMap() { return this._optionsOMap; } + + /** + * Get flags in an object map of booleans by parameter name. + * @returns {{}} + */ + get flagsOMap() { return this._flagsOMap; } + + /** + * Get all argument values in an object map by parameter name. + * @returns {{}} + */ + get argsOMap() { return this._argsOMap; } + + + toJSON() { + const _ = this; + return { + params: _.paramsArr, + options: _.optionsOMap, + flags: _.flagsOMap, + args: _.argsOMap + }; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CommandArgs') && + pu.hasGetters(obj, 'paramsArr', 'optionsOMap', 'flagsOMap', 'argsOMap'); + } +} + +module.exports = CommandArgs; \ No newline at end of file diff --git a/libs/class.CommandDispatcher.js b/libs/class.CommandDispatcher.js new file mode 100644 index 0000000..6be0960 --- /dev/null +++ b/libs/class.CommandDispatcher.js @@ -0,0 +1,192 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + pu = require('@mintpond/mint-utils').prototypes, + Category = require('./class.Category'), + argParser = require('./service.argParser'), + cmdParser = require('./service.cmdParser'), + Commands = require('./class.Commands.js'); + + +class CommandDispatcher { + + /** + * Constructor. + */ + constructor(commands) { + precon.opt_instanceOf(commands, Commands, 'commands'); + + const _ = this; + _._commands = commands || new Commands(); + } + + + /** + * Add a command to the dispatcher. + * + * @param definition {object} The command definition. + * @param definition.path {string} + * @param [definition.params] {string[]} + * @param [definition.options] {string[]} + * @param [definition.flags] {string[]} + * @param [definition.description] {string} + * @param definition.execute {function(args:object, done:function())} + * @returns {Command} + */ + addCommand(definition) { + precon.notNull(definition, 'definition'); + + const _ = this; + return _._commands.define(definition); + } + + + /** + * Parse a command query. + * + * @param query {string[]|string} + * @returns {boolean|{path: string, argOMap: {}, isHelp: boolean}} + */ + parseQuery(query) { + + const _ = this; + const inputArr = Array.isArray(query) ? query : query.split(' '); + const parsed = cmdParser.parse(_._commands, inputArr); + + if (!parsed.path && parsed.isHelp) { + return { + path: '', + argsOMap: {}, + isHelp: true + }; + } + + const cmd = _._commands.get(parsed.path); + if (!cmd) + return false; + + if (parsed.isHelp || cmd instanceof Category) { + return { + path: cmd.path || '', + argsOMap: {}, + isHelp: true + }; + } + + const parsedArgs = argParser.parse(cmd, parsed.args); + + return { + path: parsed.path, + argsOMap: parsedArgs.argsOMap, + isHelp: false + }; + } + + + /** + * Execute a registered command. + * + * @param path {string} The command path. + * @param argsOMap {object} An object containing arguments mapped to parameter name. + * @param callback {function(err:*,result:*[])} + * @returns {boolean} + */ + execute(path, argsOMap, callback) { + precon.string(path, 'path'); + precon.obj(argsOMap, 'argsOMap'); + precon.opt_funct(callback, 'callback'); + + const _ = this; + const command = _._commands.get(path); + if (!command) { + callback && callback('Command not found', null); + return false; + } + + if (command instanceof Category) { + callback && callback('Cannot execute category', null); + return false; + } + + _.$onExecuteCommand(command, argsOMap, callback); + + return true; + } + + + /** + * Generate a help string for the specified command path. + * + * @param path {string} The command path. + * @param callback {function(err:*,help:string)} + */ + help(path, callback) { + precon.string(path, 'path'); + precon.opt_funct(callback, 'callback'); + + const _ = this; + const allCommandsArr = _._commands.getAll(path, 1); + const commandsArr = []; + const categoryArr = []; + + // find categories + allCommandsArr.forEach(command => { + if (command instanceof Category) { + if (command.path !== path) + categoryArr.push(command); + } + else { + commandsArr.push(command); + } + }); + + const bufferArr = []; + + if (commandsArr.length === 1 && categoryArr.length === 0) { + bufferArr.push(_.$getCommandVerboseUsageHelpStr(commandsArr[0])); + } + else { + commandsArr.forEach(command => { + bufferArr.push(_.$getCommandUsageHelpStr(command)); + }); + + categoryArr.forEach(category => { + bufferArr.push(_.$getCommandCategoryHelpStr(category.path)); + }); + } + + callback && callback(null, bufferArr.join('\n')); + } + + + $onExecuteCommand(command, argOMap, done) { + command.execute(argOMap, done); + } + + + $getCommandUsageHelpStr(command) { + return ` > ${command.path.split('.').pop()}\n USAGE: ${command.usage}\n DESCR: ${command.description}\n`; + } + + + $getCommandVerboseUsageHelpStr(command) { + return command.verboseUsage; + } + + + $getCommandCategoryHelpStr(categoryPath) { + return `>> ${categoryPath.split('.').join(' ')} [?]\n`; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CommandDispatcher') && + pu.isFunction(obj.addCommand) && + pu.isFunction(obj.parseQuery) && + pu.isFunction(obj.execute) && + pu.isFunction(obj.help); + } +} + +module.exports = CommandDispatcher; \ No newline at end of file diff --git a/libs/class.CommandError.js b/libs/class.CommandError.js new file mode 100644 index 0000000..a9d838b --- /dev/null +++ b/libs/class.CommandError.js @@ -0,0 +1,28 @@ +'use strict'; + +const pu = require('@mintpond/mint-utils').prototypes; + + +class CommandError extends Error{ + + /** + * Constructor. + * + * @param description {string} A description of the error. + * @param [data] {*} Optional data to attach to the error. + */ + constructor(description, data) { + super(description); + + this.isCommandError = true; + this.data = data || {}; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CommandError') && + obj instanceof Error; + } +} + +module.exports = CommandError; \ No newline at end of file diff --git a/libs/class.CommandParameter.js b/libs/class.CommandParameter.js new file mode 100644 index 0000000..f42042a --- /dev/null +++ b/libs/class.CommandParameter.js @@ -0,0 +1,114 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + mu = require('@mintpond/mint-utils'), + pu = require('@mintpond/mint-utils').prototypes; + + +class CommandParameter { + + /** + * Constructor + * + * @param param {string|{}} + * @param [isFlag] {boolean} + */ + constructor(param, isFlag) { + precon.notNull(param, 'param'); + precon.opt_boolean(isFlag, 'isFlag'); + + const _ = this; + + if (mu.isString(param)) { + + const parts = param.split('='); + _._name = parts[0]; + _._hasDefaultValue = parts.length > 1; + parts.shift(); + _._defaultValue = parts.length ? parts.join('=') : isFlag ? false : ''; + _._description = ''; + } + else if (mu.isObject(param)) { + + precon.string(param.name, 'name'); + if (isFlag) { + precon.undef(param.defaultValue, 'defaultValue'); + } + else { + precon.opt_string(param.defaultValue, 'defaultValue'); + } + precon.opt_string(param.description, 'description'); + + _._name = param.name; + _._hasDefaultValue = isFlag ? false : mu.isString(param.defaultValue); + _._defaultValue = isFlag ? false : param.defaultValue || ''; + _._description = param.description || ''; + Object.keys(param).forEach(propName => { + + if (propName[0] === '_') + return; // continue + + switch (propName) { + case 'name': + case 'defaultValue': + case 'description': + break; + default: + _[propName] = param[propName]; + break; + } + }); + } + else { + throw new Error('Invalid parameter'); + } + } + + + /** + * The name of the parameter. + * @returns {string} + */ + get name() { return this._name; } + + /** + * Determine if the parameter has a default value defined. + * @returns {boolean} + */ + get hasDefaultValue() { return this._hasDefaultValue; } + + /** + * Get the default argument value. + * @returns {string} + */ + get defaultValue() { return this._defaultValue; } + + /** + * Get the description of the parameter to use in help definitions. + * @returns {string} + */ + get description() { return this._description; } + set description(descr) { + precon.string(descr, 'description'); + this._description = descr; + } + + + toJSON() { + const _ = this; + return { + name: _.name, + defaultValue: _.defaultValue, + description: _.description + }; + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'CommandParameter') && + pu.hasGetters(obj, 'name', 'hasDefaultValue', 'defaultValue', 'description'); + } +} + +module.exports = CommandParameter; \ No newline at end of file diff --git a/libs/class.Commands.js b/libs/class.Commands.js new file mode 100644 index 0000000..ef5fdfb --- /dev/null +++ b/libs/class.Commands.js @@ -0,0 +1,151 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + pu = require('@mintpond/mint-utils').prototypes, + Category = require('./class.Category'), + Command = require('./class.Command'); + + +/** + * A collection of Commands. + */ +class Commands { + + /** + * Constructor. + */ + constructor() { + const _ = this; + _._pathSet = new Set(); + _._commandMap = new Map(); + } + + + /** + * Array of command paths. + * @returns {string[]} + */ + get pathsArr() { return Array.from(this._commandMap.keys()); } + + + /** + * Define a command. + * + * @param definition + * @param definition.path {string} + * @param definition.params {string[]} + * @param definition.flags {string[]} + * @param definition.description {string} + * @param definition.execute {function(args:{}, done:function(err:*, result:*))} + * @returns {Command} + */ + define(definition) { + precon.notNull(definition, 'definition'); + + const _ = this; + _._addPathsAndCategories(definition.path); + const cmd = _.$createCommand(definition); + _._commandMap.set(definition.path, cmd); + return cmd; + } + + + /** + * Get a command by path. + * + * @param path {string} + * @returns {undefined|Command} + */ + get(path) { + precon.string(path, 'path'); + + const _ = this; + return _._commandMap.get(path); + } + + + /** + * Get all commands that match or are children of the specified command path. + * + * @param path {string} + * @param [maxSubDepth] {number} The maximum depth of commands in addition to the path commands. + * @returns {Command[]} + */ + getAll(path, maxSubDepth) { + precon.string(path, 'path'); + precon.opt_positiveInteger(maxSubDepth, 'maxSubDepth'); + + const _ = this; + const resultArr = []; + for (const [p, command] of _._commandMap.entries()) { + if (!p.startsWith(path)) + continue; + + if (maxSubDepth) { + const subPath = p.substr(path.length + 1/*period*/) || ''; + if (subPath) { + const subParts = subPath.split('.'); + if (subParts.length > maxSubDepth) { + continue; + } + } + } + + resultArr.push(command); + } + return resultArr; + } + + + /** + * Determine if a path is a valid category or command. + * + * @param path {string} + * @returns {boolean} + */ + isPath(path) { + precon.string(path, 'path'); + + const _ = this; + return _._pathSet.has(path); + } + + + $createCommand(definition) { + return new Command(definition); + } + + + _addPathsAndCategories(path) { + const _ = this; + const pathPartsArr = path.split('.'); + if (pathPartsArr.length > 1) { + const subPathArr = []; + pathPartsArr.forEach(part => { + subPathArr.push(part); + const subPath = subPathArr.join('.'); + + _._pathSet.add(subPath); + + if (!_._commandMap.has(subPath)) + _._commandMap.set(subPath, new Category({ path: subPath })); + }); + } + else { + _._pathSet.add(path); + } + } + + + static [Symbol.hasInstance](obj) { + return pu.isInstanceOfByName(obj, 'Commands') && + pu.isFunction(obj.define) && + pu.isFunction(obj.get) && + pu.isFunction(obj.getAll) && + pu.isFunction(obj.isPath) && + pu.hasGetters(obj, 'pathsArr'); + } +} + +module.exports = Commands; \ No newline at end of file diff --git a/libs/service.argParser.js b/libs/service.argParser.js new file mode 100644 index 0000000..97f3b5d --- /dev/null +++ b/libs/service.argParser.js @@ -0,0 +1,261 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + CommandArg = require('./class.CommandArg'), + CommandArgs = require('./class.CommandArgs'), + CommandError = require('./class.CommandError'); + +const COMMON_PREFIX = '-'; +const FLAG_PREFIX = COMMON_PREFIX + COMMON_PREFIX; + + +module.exports = { + + /** + * Parse array of arguments.. + * + * @param command {Command} The command the arguments are for. + * @param args {string[]} Array of argument values (split by space character) + * @returns {CommandArgs} + */ + parse: parse +}; + + +function parse(command, args) { + precon.notNull(command, 'command'); + precon.arrayOf(args, 'string', 'args'); + + const argResults = new CommandArgs(command); + + const cmdArgsArr = args.slice(0); + const parametersArr = command.paramsArr.slice(0); + + // parse arguments for parameters + _parseParameters(command, argResults, parametersArr, cmdArgsArr); + + // check if there are options for the command + if (!command.optionsArr.length && !command.flagsArr.length) { + + // if there are no more parameters but there are still arguments left, + // then there are too many arguments. + if (cmdArgsArr.length) + throw new CommandError('Too many arguments'); + + // nothing left to do + return argResults; + } + + // get ready to parse options and flag parameters + const optionsOMap = _createOMap(command.optionsArr); + const flagsOMap = _createOMap(command.flagsArr); + + // parse arguments for non-static parameters. + _parseOptionsAndFlags(command, argResults, optionsOMap, flagsOMap, cmdArgsArr); + + return argResults; +} + + +function _parseParameters(command, argResults, parametersArr, cmdArgsArr) { + + while (parametersArr.length) { + + const parameter = parametersArr.shift(); + + const paramName = parameter.name; + let value = null; + + if (cmdArgsArr.length) { + + const arg = cmdArgsArr.shift() || ''; + + // Should not be any options or flag parameters before required arguments + if (arg.startsWith(COMMON_PREFIX)) { + + // the last parameter might be optional, in which + // case it should have a default value. + + // If there are still more static parameters, it means this + // is not the last parameter but the expected argument was not provided. + if (parametersArr.length) { + throw new CommandError(`Missing required argument for parameter: ${parameter.name}`, { + missingParamName: parameter.name, + missingParam: parameter + }); + } + + // No default value defined means a discreet value is expected. + if (!parameter.hasDefaultValue) { + throw new CommandError(`Missing required argument for parameter: ${parameter.name}`, { + missingParamName: parameter.name, + missingParam: parameter + }); + } + + // re-insert option argument so the other parsers + // will see it. Since this is the last parameter, + // this is the end of the loop. + cmdArgsArr.unshift(arg); + } + else { + value = _parseArgValue(arg, cmdArgsArr); + } + } + + // add argument + if (value !== null || parameter.hasDefaultValue) { + + const commandArg = new CommandArg(parameter, value); + + if (paramName in argResults.argsOMap) { + throw new CommandError(`Duplicate argument for parameter: ${parameter.name}`, { + missingParamName: parameter.name, + missingParam: parameter + }); + } + + argResults.paramsArr.push(commandArg); + argResults.argsOMap[paramName] = commandArg.value; + } + else { + throw new CommandError(`Missing required argument for parameter: ${parameter.name}`, { + missingParamName: parameter.name, + missingParam: parameter + }); + } + } +} + + +function _parseOptionsAndFlags(command, argResults, optionsOMap, flagsOMap, cmdArgs) { + + while (cmdArgs.length) { + + let paramName = cmdArgs.shift(); + + if (!paramName.startsWith(COMMON_PREFIX)) + throw new CommandError(`Too many arguments ${JSON.stringify(cmdArgs)}`); + + // check for flag + if (paramName.startsWith(FLAG_PREFIX)) { + + paramName = paramName.substr(FLAG_PREFIX.length); + + if (!(paramName in flagsOMap)) { + + if (paramName in argResults.flags) { + // flag was already added + throw new CommandError(`Duplicate argument for parameter: ${paramName}`); + } + + // unknown flag + throw new CommandError(`Invalid flag: ${paramName}`); + } + + const commandArg = new CommandArg(flagsOMap[paramName], true); + delete flagsOMap[paramName]; + + argResults.flagsOMap[paramName] = commandArg; + argResults.argsOMap[paramName] = commandArg.value; + } + // check for option parameter + else if (paramName.startsWith(COMMON_PREFIX)) { + + paramName = paramName.substring(COMMON_PREFIX.length); + + if (!(paramName in optionsOMap)) + throw new CommandError(`Unrecognized option: ${paramName}`); + + if (!cmdArgs.length) + throw new CommandError(`Duplicate argument for option: ${paramName}`); + + const arg = _parseArgValue(cmdArgs.shift(), cmdArgs); + const commandArg = new CommandArg(optionsOMap[paramName], arg); + + delete optionsOMap[paramName]; + + argResults.optionsOMap[paramName] = commandArg; + argResults.argsOMap[paramName] = commandArg.value; + } + } + + // add missing arguments if default or error on missing required arg + + Object.keys(optionsOMap).forEach(optionName => { + const opt = optionsOMap[optionName]; + const commandArg = new CommandArg(opt, ''); + argResults.optionsOMap[opt.name] = commandArg; + argResults.argsOMap[opt.name] = commandArg.value; + }); +} + + +function _parseArgValue(arg, cmdArgs) { + + // check to see if parsing a literal + let quote = null; + + // double quote + if (arg[0] === '"') { + quote = '"'; + } + // single quote + else if (arg[0] === '\'') { + quote = '\''; + } + // tilde quote + else if (arg[0] === '`') { + quote = '`'; + } + + if (quote === null) + return arg; // value is unquoted argument + + // value is quoted literal + + const firstWord = arg.substr(1); // remove starting quotation + + // make sure the literal isn't closed on the same word + if (firstWord.endsWith(quote)) { + // remove end quote. + return firstWord.substr(0, firstWord.length - 1); + } + + // otherwise parse ahead until end of literal + + const literal = []; + literal.push(firstWord); + + while (cmdArgs.length) { + let nextArg = cmdArgs.shift(); + + // check if this is the final word in the literal + if (nextArg.endsWith(quote)) { + + // remove end quote + nextArg = nextArg.substr(0, nextArg.length - 1); + + literal.push(nextArg); + break; + } + + literal.push(nextArg); + } + + return literal.join(' '); +} + + +function _createOMap(collection) { + + const result = {}; + + collection.forEach(item => { + result[item.name] = item; + }); + + return result; +} + diff --git a/libs/service.cli.js b/libs/service.cli.js new file mode 100644 index 0000000..7858ec1 --- /dev/null +++ b/libs/service.cli.js @@ -0,0 +1,74 @@ +'use strict'; + +const + net = require('net'), + precon = require('@mintpond/mint-precon'); + + +/** + * Send a command to a listening CliServer. + * + * @param args + * @param args.query {string} A command query + * @param args.host {string} The host to connect to. + * @param args.port {number} The port to connect to. + * @param [args.callback] {function(err:*,result:*)} + */ +module.exports = function(args) { + precon.string(args.query, 'query'); + precon.string(args.host, 'host'); + precon.minMaxInteger(args.port, 1, 65535, 'port'); + precon.opt_funct(args.callback, 'callback'); + + const host = args.host; + const port = args.port; + const query = args.query; + let callback = args.callback; + + const client = net.connect(port, host, () => { + + client.on('error', err => { + callback && callback(err); + callback = null; + }); + + const bufferArr = []; + + client.on('data', data => { + + data = data.toString(); + bufferArr.push(data); + if (data.endsWith('\n\n')) { + + data = bufferArr.join(''); + + const parts = data.split('\n'); + var response = parts[1]; + + try { + response = JSON.parse(response); + } + catch(err) {} + + bufferArr.length = 0; + + callback && callback(null, response); + callback = null; + + client.end(); + } + }); + + client.write(JSON.stringify({ + id: 0, + query: query + }) + '\n'); + }); + + client.on('close', () => { + callback && callback(); + callback = null; + }); +} + + diff --git a/libs/service.cmdParser.js b/libs/service.cmdParser.js new file mode 100644 index 0000000..2fbda65 --- /dev/null +++ b/libs/service.cmdParser.js @@ -0,0 +1,66 @@ +'use strict'; + +const + precon = require('@mintpond/mint-precon'), + Commands = require('./class.Commands'); + +module.exports = { + + /** + * Parse a command query that has been split into a string array of commands and arguments. + * + * @param commands {Commands} + * @param argsArr {string[]} + * @returns {{args: [], path: string, isHelp: boolean}} + */ + parse: parse +} + + +function parse(commands, argsArr) { + precon.instanceOf(commands, Commands, 'commands'); + precon.arrayOf(argsArr, 'string', 'args'); + + return _parse(commands, [], argsArr); +} + + +function _parse(commands, commandPathArr, argsArr) { + + if (argsArr.length === 0 || !argsArr[0]) { + return { + path: commandPathArr.join('.').toLowerCase(), + args: [], + isHelp: false + }; + } + + const subCmd = argsArr[0]; + const subCmdPath = commandPathArr.slice(0); + + subCmdPath.push(subCmd); + const subCmdPathStr = subCmdPath.join('.').toLowerCase(); + + // command not found + if (!commands.isPath(subCmdPathStr)) { + + if (subCmd === '?' || subCmd === '--help') { + return { + path: commandPathArr.join('.').toLowerCase(), + args: [], + isHelp: true + }; + } + + return { + path: commandPathArr.join('.').toLowerCase(), + args: argsArr, + isHelp: false + }; + } + + // trim first arg (its a command now) + argsArr = argsArr.slice(1); + + return _parse(commands, subCmdPath, argsArr); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1f9a07b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,245 @@ +{ + "name": "@mintpond/mint-commands", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@mintpond/mint-precon": { + "version": "1.0.0", + "resolved": "https://npm.pkg.github.com/download/@mintpond/mint-precon/1.0.0/36b8d1195b8fe25ac8843653d7f0ff5d543dc35a81d772108ee741197e4ae5ab", + "integrity": "sha512-Ao5iO3L418nSmwxKxP+tkUlqg0zDlyrb44nEjF1A0vGugByhCn/b0QuVklFwbjExMVtcSQ/2tK6OOQc9Y5vMIQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "@mintpond/mint-utils": { + "version": "1.2.1", + "resolved": "https://npm.pkg.github.com/download/@mintpond/mint-utils/1.2.1/581a34788e46d2f0128fe0faf6af1f99b7d93f6508e21eb4bf9c55f211db9d6e", + "integrity": "sha512-6DNs0Cak9+6lQ5JrHDFWwnxeEXHFKwUUjUb9e8/BnItoNRAZgZk17bzkGsp+1z1apTz895TUkO/DX36noecSBw==", + "requires": { + "@mintpond/mint-precon": "^1.0.0", + "async": "1.5.2", + "cron": "~1.3.0" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cron": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-1.3.1.tgz", + "integrity": "sha512-iZlSOfm2IQUUMMko4Nj0+B8Sk5S/wWwJF++X+L4TfAfd3Y0i8s5beqm4nhwFyorkOOnmy8czymLyh+MeYKLxzg==", + "requires": { + "moment-timezone": "^0.5.x" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz", + "integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d681616 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "@mintpond/mint-commands", + "version": "1.0.0", + "description": "CLI Command line interface for NodeJS", + "author": "JCThePants", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@mintpond/mint-precon": "^1.0.0", + "@mintpond/mint-utils": "^1.2.1" + }, + "devDependencies": { + "mocha": "^5.2.0" + }, + "scripts": { + "test": "mocha tests/mocha.*" + }, + "homepage": "https://github.com/MintPond/mint-commands", + "bugs": { + "url": "https://github.com/MintPond/mint-commands/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MintPond/mint-commands.git" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/" + }, + "engines": { + "node": ">=10.17.0" + } +} diff --git a/scripts/cli.js b/scripts/cli.js new file mode 100644 index 0000000..1726668 --- /dev/null +++ b/scripts/cli.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * This is a script used to send commands to a CLI server. + * + * eg: ./cli.js category1 command1 arg1 arg2 --flag + */ +'use strict'; + +const host = '127.0.0.1'; +const port = 2020; +const queryArr = process.argv.slice(2); + +const net = require('net'); + +const client = net.connect(port, host, () => { + + client.on('error', err => { + + if (err.code === 'ECONNREFUSED') { + console.log(`Could not connect to MintPond instance at ${host}:${port}`); + } + else { + console.log(`Socket error ${JSON.stringify(err)}`); + } + }); + + const bufferArr = []; + + client.on('data', data => { + + data = data.toString(); + bufferArr.push(data); + if (data.endsWith('\n\n')) { + + data = bufferArr.join(''); + + const parts = data.split('\n'); + var response = parts[1]; + + try { + response = JSON.parse(response); + } + catch(err) {} + + bufferArr.length = 0; + console.log(''); + + if (Array.isArray(response)) { + + response.forEach(item => { + if (typeof item === 'string') { + console.log(_indentLines(item) + '\n'); + } + else { + console.log(_indentLines(JSON.stringify(item, null, 4))); + } + }); + } + else { + if (typeof response === 'string') { + console.log(_indentLines(response) + '\n'); + } + else { + console.log(_indentLines(JSON.stringify(response, null, 4))); + } + } + client.end(); + } + }); + + client.write(JSON.stringify({ + id: 0, + query: queryArr + }) + '\n'); +}); + +client.on('close', () => { + setTimeout(() => { + process.exit(0); + }, 1); +}); + + +function _indentLines(str) { + return str.split('\n').map(line => { + return ' ' + line; + }).join('\n'); +} \ No newline at end of file diff --git a/tests/mocha.Command.js b/tests/mocha.Command.js new file mode 100644 index 0000000..59dac67 --- /dev/null +++ b/tests/mocha.Command.js @@ -0,0 +1,120 @@ +'use strict'; + +const + assert = require('assert'), + MCommand = require('./../libs/class.Command'), + CommandParameter = require('./../libs/class.CommandParameter'); + +let cmd; + +function globalBe() { + cmd = new MCommand({ + path: 'category.command', + params: [ + {name: 'param1', description: '1st parameter'}, + 'param2=' + ], + options: ['option1=defVal', 'option2'], + flags: ['flag1', {name: 'flag2'}], + description: 'command description' + }); +} + + +describe('Command', () => { + + beforeEach(globalBe); + + it('should have correct path', () => { + assert.strictEqual(cmd.path, 'category.command'); + }); + + it('should return CommandParameter instances from paramsArr property', () => { + cmd.paramsArr.forEach(param => { + assert.strictEqual(param instanceof CommandParameter, true); + }) + }); + + it('should return CommandParameter instances from optionsArr property', () => { + cmd.optionsArr.forEach(param => { + assert.strictEqual(param instanceof CommandParameter, true); + }) + }); + + it('should return CommandParameter instances from flagsArr property', () => { + cmd.flagsArr.forEach(param => { + assert.strictEqual(param instanceof CommandParameter, true); + }) + }); + + it('should have param1 parameter', () => { + assert.strictEqual(cmd.paramsArr[0].name, 'param1'); + }); + + it('should have param2 parameter', () => { + assert.strictEqual(cmd.paramsArr[1].name, 'param2'); + }); + + it('should have option1 option', () => { + assert.strictEqual(cmd.optionsArr[0].name, 'option1'); + }); + + it('should have option2 option', () => { + assert.strictEqual(cmd.optionsArr[1].name, 'option2'); + }); + + it('should have flag1 flag', () => { + assert.strictEqual(cmd.flagsArr[0].name, 'flag1'); + }); + + it('should have flag2 flag', () => { + assert.strictEqual(cmd.flagsArr[1].name, 'flag2'); + }); + + it('should have correct description', () => { + assert.strictEqual(cmd.description, 'command description'); + }); + + describe('instanceof handling', () => { + beforeEach(globalBe); + + it('should return true when the instance is exact', () => { + assert.strictEqual(cmd instanceof MCommand, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotAdaptiveConfig {} + const not = new NotAdaptiveConfig(); + + assert.strictEqual(not instanceof MCommand, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommand extends MCommand {} + const extended = new ExtendedCommand({ path: 'command1' }); + + assert.strictEqual(extended instanceof MCommand, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class Command { + execute() {} + get path() {} + get paramsArr() {} + get optionsArr() {} + get flagsArr() {} + get description() {} + get usage() {} + get verboseUsage() {} + get extra() {} + } + + const substitute = new Command(); + + assert.strictEqual(substitute instanceof MCommand, true); + }); + }); +}); \ No newline at end of file diff --git a/tests/mocha.CommandArg.js b/tests/mocha.CommandArg.js new file mode 100644 index 0000000..b8f92a9 --- /dev/null +++ b/tests/mocha.CommandArg.js @@ -0,0 +1,147 @@ +'use strict'; + +const + assert = require('assert'), + MCommandArg = require('./../libs/class.CommandArg'), + CommandParameter = require('./../libs/class.CommandParameter'); + +let cmdParam; +let cmdArg; + + +describe('CommandArg', () => { + + context('with value, no default specified', () => { + beforeEach(() => { + cmdParam = new CommandParameter('param1'); + cmdArg = new MCommandArg(cmdParam, 'value1'); + }); + + it('should have correct parameter name', () => { + assert.strictEqual(cmdArg.name, 'param1'); + }); + + it('should have correct parameter', () => { + assert.strictEqual(cmdArg.parameter, cmdParam); + }); + + it('should have correct value', () => { + assert.strictEqual(cmdArg.value, 'value1'); + }); + + it('should return correct value from isDefaultValue', () => { + assert.strictEqual(cmdArg.isDefaultValue, false); + }); + }); + + context('no value, no default specified', () => { + beforeEach(() => { + cmdParam = new CommandParameter('param1'); + cmdArg = new MCommandArg(cmdParam, ''); + }); + + it('should have correct parameter name', () => { + assert.strictEqual(cmdArg.name, 'param1'); + }); + + it('should have correct parameter', () => { + assert.strictEqual(cmdArg.parameter, cmdParam); + }); + + it('should have correct value', () => { + assert.strictEqual(cmdArg.value, ''); + }); + + it('should return correct value from isDefaultValue', () => { + assert.strictEqual(cmdArg.isDefaultValue, true); + }); + }); + + context('with value, with default specified', () => { + beforeEach(() => { + cmdParam = new CommandParameter('param1=abc'); + cmdArg = new MCommandArg(cmdParam, 'value1'); + }); + + it('should have correct parameter name', () => { + assert.strictEqual(cmdArg.name, 'param1'); + }); + + it('should have correct parameter', () => { + assert.strictEqual(cmdArg.parameter, cmdParam); + }); + + it('should have correct value', () => { + assert.strictEqual(cmdArg.value, 'value1'); + }); + + it('should return correct value from isDefaultValue', () => { + assert.strictEqual(cmdArg.isDefaultValue, false); + }); + }); + + context('no value, with default specified', () => { + beforeEach(() => { + cmdParam = new CommandParameter('param1=abc'); + cmdArg = new MCommandArg(cmdParam, ''); + }); + + it('should have correct parameter name', () => { + assert.strictEqual(cmdArg.name, 'param1'); + }); + + it('should have correct parameter', () => { + assert.strictEqual(cmdArg.parameter, cmdParam); + }); + + it('should have correct value', () => { + assert.strictEqual(cmdArg.value, 'abc'); + }); + + it('should return correct value from isDefaultValue', () => { + assert.strictEqual(cmdArg.isDefaultValue, true); + }); + }); + + describe('instanceof handling', () => { + beforeEach(() => { + cmdParam = new CommandParameter('param1=abc'); + cmdArg = new MCommandArg(cmdParam, ''); + }); + + it('should return true when the instance is exact', () => { + assert.strictEqual(cmdArg instanceof MCommandArg, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotCommandArg {} + const not = new NotCommandArg(); + + assert.strictEqual(not instanceof MCommandArg, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommandArg extends MCommandArg {} + const extended = new ExtendedCommandArg(cmdParam, ''); + + assert.strictEqual(extended instanceof MCommandArg, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class CommandArg { + get name() {} + get parameter() {} + get value() {} + get isDefaultValue() {} + } + + const substitute = new CommandArg(); + + assert.strictEqual(substitute instanceof MCommandArg, true); + }); + }); +}); + diff --git a/tests/mocha.CommandArgs.js b/tests/mocha.CommandArgs.js new file mode 100644 index 0000000..a20d45e --- /dev/null +++ b/tests/mocha.CommandArgs.js @@ -0,0 +1,91 @@ +'use strict'; + +const + assert = require('assert'), + Command = require('./../libs/class.Command'), + CommandArg = require('./../libs/class.CommandArg'), + MCommandArgs = require('./../libs/class.CommandArgs'), + argParser = require('./../libs/service.argParser'); + +let cmd; +let paramArg; +let optionArg; +let flagArg; +let args; + +function globalBe() { + + cmd = new Command({ + path: 'command1', + params: ['param1'], + options: ['option1'], + flags: ['flag1'] + }); + + paramArg = new CommandArg(cmd.paramsArr[0], 'value1'); + optionArg = new CommandArg(cmd.optionsArr[0], 'value2'); + flagArg = new CommandArg(cmd.flagsArr[0], true); + + args = argParser.parse(cmd, ['value1', '-option1', 'value2', '--flag1']); +} + +describe('CommandArgs', () => { + + beforeEach(globalBe); + + it('should have correct params parameters', () => { + assert.strictEqual(args.paramsArr[0].name, 'param1'); + }); + + it('should have correct options parameters', () => { + assert.strictEqual(args.optionsOMap['option1'].name, 'option1'); + }); + + it('should have correct flag parameters', () => { + assert.strictEqual(args.flagsOMap['flag1'].name, 'flag1'); + }); + + it('should have correct values in values object map', () => { + assert.strictEqual(args.argsOMap['param1'], 'value1'); + assert.strictEqual(args.argsOMap['option1'], 'value2'); + assert.strictEqual(args.argsOMap['flag1'], true); + }); + + describe('instanceof handling', () => { + beforeEach(globalBe); + + it('should return true when the instance is exact', () => { + assert.strictEqual(args instanceof MCommandArgs, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotCommandArgs {} + const not = new NotCommandArgs(); + + assert.strictEqual(not instanceof MCommandArgs, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommandArgs extends MCommandArgs {} + const extended = new ExtendedCommandArgs(cmd); + + assert.strictEqual(extended instanceof MCommandArgs, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class CommandArgs { + get paramsArr() {} + get optionsOMap() {} + get flagsOMap() {} + get argsOMap() {} + } + + const substitute = new CommandArgs(); + + assert.strictEqual(substitute instanceof MCommandArgs, true); + }); + }); +}); \ No newline at end of file diff --git a/tests/mocha.CommandDispatcher.js b/tests/mocha.CommandDispatcher.js new file mode 100644 index 0000000..c185a6b --- /dev/null +++ b/tests/mocha.CommandDispatcher.js @@ -0,0 +1,206 @@ +'use strict'; + +const + assert = require('assert'), + CommandError = require('./../libs/class.CommandError'), + MCommandDispatcher = require('./../libs/class.CommandDispatcher'); + +let dispatcher; + + +describe('CommandDispatcher', () => { + + describe('parseQuery function', () => { + beforeEach(() => { + dispatcher = new MCommandDispatcher(); + dispatcher.addCommand({ + path: 'category1.command1', + params: ['param1'], + options: ['option1'], + flags: ['flag1'], + }); + }); + + it('should correctly parse query (1)', () => { + const result = dispatcher.parseQuery('category1 command1 value1'); + assert.strictEqual(result.path, 'category1.command1'); + assert.strictEqual(result.argsOMap['param1'], 'value1'); + assert.strictEqual(result.argsOMap['option1'], ''); + assert.strictEqual(result.argsOMap['flag1'], false); + assert.strictEqual(result.isHelp, false); + }); + + it('should correctly parse query (2)', () => { + const result = dispatcher.parseQuery('category1 command1 value1 -option1 value2'); + assert.strictEqual(result.path, 'category1.command1'); + assert.strictEqual(result.argsOMap['param1'], 'value1'); + assert.strictEqual(result.argsOMap['option1'], 'value2'); + assert.strictEqual(result.argsOMap['flag1'], false); + assert.strictEqual(result.isHelp, false); + }); + + it('should correctly parse query (3)', () => { + const result = dispatcher.parseQuery('category1 command1 value1 -option1 value2 --flag1'); + assert.strictEqual(result.path, 'category1.command1'); + assert.strictEqual(result.argsOMap['param1'], 'value1'); + assert.strictEqual(result.argsOMap['option1'], 'value2'); + assert.strictEqual(result.argsOMap['flag1'], true); + assert.strictEqual(result.isHelp, false); + }); + + it('should correctly parse query (4)', () => { + const result = dispatcher.parseQuery('category1 command1 value1 --flag1'); + assert.strictEqual(result.path, 'category1.command1'); + assert.strictEqual(result.argsOMap['param1'], 'value1'); + assert.strictEqual(result.argsOMap['option1'], ''); + assert.strictEqual(result.argsOMap['flag1'], true); + assert.strictEqual(result.isHelp, false); + }); + + it('should return false if command not found', () => { + const result = dispatcher.parseQuery('fakecommand value1 --flag1'); + assert.strictEqual(result, false); + }); + + it('should throw if command is a category', () => { + const result = dispatcher.parseQuery('category1'); + assert.strictEqual(result.path, 'category1'); + assert.deepEqual(result.argsOMap, {}); + assert.strictEqual(result.isHelp, true); + }); + + it('should return help flag if command help arg is included (?)', () => { + const result = dispatcher.parseQuery('category1 command1 ?'); + assert.strictEqual(result.path, 'category1.command1'); + assert.deepEqual(result.argsOMap, {}); + assert.strictEqual(result.isHelp, true); + }); + + it('should return help flag if command help arg is included (--help)', () => { + const result = dispatcher.parseQuery('category1 command1 --help'); + assert.strictEqual(result.path, 'category1.command1'); + assert.deepEqual(result.argsOMap, {}); + assert.strictEqual(result.isHelp, true); + }); + + it('should throw CommandError if required argument is missing', () => { + try { + dispatcher.parseQuery('category1 command1'); + } + catch (err) { + assert.strictEqual(err instanceof CommandError, true); + return; + } + throw new Error('Exception expected'); + }); + }); + + describe('execute function', () => { + beforeEach(() => { + dispatcher = new MCommandDispatcher(); + }); + + it('should execute a valid command', itDone => { + dispatcher.addCommand({ + path: 'category1.command1', + params: ['param1'], + options: ['option1'], + flags: ['flag1'], + execute: (args, done) => { + assert.strictEqual(args.param1, 'value1'); + assert.strictEqual(args.option1, 'value2'); + assert.strictEqual(args.flag1, true); + done('this is error', 'this is result'); + } + }); + dispatcher.execute('category1.command1', { + param1: 'value1', + option1: 'value2', + flag1: true + }, (err, result) => { + assert.strictEqual(err, 'this is error'); + assert.strictEqual(result, 'this is result'); + itDone(); + }); + }); + + it('should return error if command not found', itDone => { + dispatcher.addCommand({ + path: 'category1.command1', + params: ['param1'], + options: ['option1'], + flags: ['flag1'], + execute: () => { + throw new Error('should not execute'); + } + }); + dispatcher.execute('category1.command2', { + param1: 'value1' + }, (err, result) => { + assert.strictEqual(!!err, true); + assert.strictEqual(!!result, false); + itDone(); + }); + }); + + it('should return error if command is a category', itDone => { + dispatcher.addCommand({ + path: 'category1.command1', + params: ['param1'], + options: ['option1'], + flags: ['flag1'], + execute: () => { + throw new Error('should not execute'); + } + }); + dispatcher.execute('category1', { + param1: 'value1' + }, (err, result) => { + assert.strictEqual(!!err, true); + assert.strictEqual(!!result, false); + itDone(); + }); + }) + }); + + + describe('instanceof handling', () => { + beforeEach(() => { + dispatcher = new MCommandDispatcher(); + }); + + it('should return true when the instance is exact', () => { + assert.strictEqual(dispatcher instanceof MCommandDispatcher, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotCommandDispatcher {} + const not = new NotCommandDispatcher(); + + assert.strictEqual(not instanceof MCommandDispatcher, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommandDispatcher extends MCommandDispatcher {} + const extended = new ExtendedCommandDispatcher(); + + assert.strictEqual(extended instanceof MCommandDispatcher, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class CommandDispatcher { + addCommand() {} + parseQuery() {} + execute() {} + help() {} + } + + const substitute = new CommandDispatcher(); + + assert.strictEqual(substitute instanceof MCommandDispatcher, true); + }); + }); +}); \ No newline at end of file diff --git a/tests/mocha.CommandParameter.js b/tests/mocha.CommandParameter.js new file mode 100644 index 0000000..83b8272 --- /dev/null +++ b/tests/mocha.CommandParameter.js @@ -0,0 +1,137 @@ +'use strict'; + +const + assert = require('assert'), + MCommandParameter = require('./../libs/class.CommandParameter'); + +let parameter; + +describe('CommandParameter', () => { + + context('string parameter, no default', () => { + + beforeEach(() => { parameter = new MCommandParameter('param1'); }) + + it('should be named "param1"', () => { + assert.strictEqual(parameter.name, 'param1'); + }); + + it('should not have a default value', () => { + assert.strictEqual(parameter.hasDefaultValue, false); + assert.strictEqual(parameter.defaultValue, ''); + }); + + it('should not have a description', () => { + assert.strictEqual(parameter.description, '') + }); + }); + + context('string parameter, with default', () => { + + beforeEach(() => { parameter = new MCommandParameter('param1=dv'); }) + + it('should be named "param1"', () => { + assert.strictEqual(parameter.name, 'param1'); + }); + + it('should have a default value', () => { + assert.strictEqual(parameter.hasDefaultValue, true); + assert.strictEqual(parameter.defaultValue, 'dv'); + }); + + it('should not have a description', () => { + assert.strictEqual(parameter.description, '') + }); + }); + + context('object parameter', () => { + + beforeEach(() => { + parameter = new MCommandParameter({ + name: 'param1', + defaultValue: 'dv', + description: 'this is a description' + }); + }) + + it('should be named "param1"', () => { + assert.strictEqual(parameter.name, 'param1'); + }); + + it('should have a default value', () => { + assert.strictEqual(parameter.hasDefaultValue, true); + assert.strictEqual(parameter.defaultValue, 'dv'); + }); + + it('should have a description', () => { + assert.strictEqual(parameter.description, 'this is a description') + }); + }); + + context('object parameter w/ extra properties', () => { + + beforeEach(() => { + parameter = new MCommandParameter({ + name: 'param1', + defaultValue: 'dv', + description: 'this is a description', + extra1: 'extra value' + }); + }) + + it('should be named "param1"', () => { + assert.strictEqual(parameter.name, 'param1'); + }); + + it('should have a default value', () => { + assert.strictEqual(parameter.hasDefaultValue, true); + assert.strictEqual(parameter.defaultValue, 'dv'); + }); + + it('should have a description', () => { + assert.strictEqual(parameter.description, 'this is a description') + }); + + it('should have extra property', () => { + assert.strictEqual(parameter.extra1, 'extra value'); + }) + }); + + describe('instanceof handling', () => { + beforeEach(() => { parameter = new MCommandParameter('param1=dv'); }) + + it('should return true when the instance is exact', () => { + assert.strictEqual(parameter instanceof MCommandParameter, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotCommandParameter {} + const not = new NotCommandParameter(); + + assert.strictEqual(not instanceof MCommandParameter, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommandParameter extends MCommandParameter {} + const extended = new ExtendedCommandParameter('param1'); + + assert.strictEqual(extended instanceof MCommandParameter, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class CommandParameter { + get name() {} + get hasDefaultValue() {} + get defaultValue() {} + get description() {} + } + + const substitute = new CommandParameter(); + + assert.strictEqual(substitute instanceof MCommandParameter, true); + }); + }); +}); diff --git a/tests/mocha.Commands.js b/tests/mocha.Commands.js new file mode 100644 index 0000000..b602baf --- /dev/null +++ b/tests/mocha.Commands.js @@ -0,0 +1,178 @@ +'use strict'; + +const + assert = require('assert'), + Category = require('./../libs/class.Category'), + Command = require('./../libs/class.Command'), + MCommands = require('./../libs/class.Commands'); + +let commands; + +describe('Commands', () => { + + function globalBe() { + commands = new MCommands(); + } + + describe('define function', () => { + beforeEach(globalBe); + + it('should add command path to pathsArr', () => { + commands.define({ path: 'test' }); + assert.strictEqual(commands.pathsArr[0], 'test'); + }); + + it('should add command sub-paths to pathsArr', () => { + commands.define({ path: 'base.sub.path.test' }); + assert.strictEqual(commands.pathsArr.indexOf('base') !== -1, true); + assert.strictEqual(commands.pathsArr.indexOf('base.sub') !== -1, true); + assert.strictEqual(commands.pathsArr.indexOf('base.sub.path') !== -1, true); + assert.strictEqual(commands.pathsArr.indexOf('base.sub.path.test') !== -1, true); + }); + + it('should return a Command', () => { + const cmd = commands.define({ path: 'test' }); + assert.strictEqual(cmd instanceof Command, true); + assert.strictEqual(cmd.path, 'test'); + }); + + it('should return a Command with params', () => { + const cmd = commands.define({ path: 'test', params: ['param1', 'param2='] }); + assert.strictEqual(cmd.paramsArr.length, 2); + assert.strictEqual(cmd.paramsArr[0].name, 'param1'); + assert.strictEqual(cmd.paramsArr[1].name, 'param2'); + }); + + it('should return a Command with options', () => { + const cmd = commands.define({ path: 'test', options: ['option1', 'option2=a'] }); + assert.strictEqual(cmd.optionsArr.length, 2); + assert.strictEqual(cmd.optionsArr[0].name, 'option1'); + assert.strictEqual(cmd.optionsArr[1].name, 'option2'); + }); + + it('should return a Command with flags', () => { + const cmd = commands.define({ path: 'test', flags: ['flag1', 'flag2'] }); + assert.strictEqual(cmd.flagsArr.length, 2); + assert.strictEqual(cmd.flagsArr[0].name, 'flag1'); + assert.strictEqual(cmd.flagsArr[1].name, 'flag2'); + }); + + it('should return a Command with description', () => { + const cmd = commands.define({ path: 'test', description: 'A test command.' }); + assert.strictEqual(cmd.description, 'A test command.'); + }); + }); + + describe('get function', () => { + + let cmd; + beforeEach(globalBe); + beforeEach(() => { + cmd = commands.define({ + path: 'this.is.a.test', + params: ['param1', 'param2='], + options: ['option1', 'option2'], + flags: ['flag1', 'flag2'] + }); + }); + + it('should get a command by path', () => { + const cmd2 = commands.get('this.is.a.test'); + assert.strictEqual(cmd2, cmd); + }); + + it('should get a category by path', () => { + const cmd2 = commands.get('this.is'); + assert.strictEqual(cmd2 instanceof Category, true); + }) + }); + + describe('getAll function', () => { + + let cmd; + beforeEach(globalBe); + beforeEach(() => { + cmd = commands.define({ + path: 'this.is.a.test', + params: ['param1', 'param2='], + options: ['option1', 'option2'], + flags: ['flag1', 'flag2'] + }); + }); + + it('should get all commands and categories that match path or descend from path', () => { + + const cmdArr = commands.getAll('this.is'); + + assert.strictEqual(cmdArr.length, 3); + + const pathsArr = cmdArr.map(cmd => cmd.path); + assert.strictEqual(pathsArr.indexOf('this.is') !== -1, true); + assert.strictEqual(pathsArr.indexOf('this.is.a') !== -1, true); + assert.strictEqual(pathsArr.indexOf('this.is.a.test') !== -1, true); + }); + }); + + describe('isPath function', () => { + + let cmd; + beforeEach(globalBe); + beforeEach(() => { + cmd = commands.define({ + path: 'this.is.a.test', + params: ['param1', 'param2='], + options: ['option1', 'option2'], + flags: ['flag1', 'flag2'] + }); + }); + + it('should return true for valid paths', () => { + assert.strictEqual(commands.isPath('this.is'), true); + assert.strictEqual(commands.isPath('this.is.a.test'), true); + }); + + it('should return true for invalid paths', () => { + assert.strictEqual(commands.isPath('this.isnt'), false); + assert.strictEqual(commands.isPath('this.issnt.a.test'), false); + }); + }); + + describe('instanceof handling', () => { + beforeEach(globalBe); + + it('should return true when the instance is exact', () => { + assert.strictEqual(commands instanceof MCommands, true); + }); + + it('should return false when the instance is NOT exact', () => { + + class NotCommands {} + const not = new NotCommands(); + + assert.strictEqual(not instanceof MCommands, false); + }); + + it('should return true when the instance extends the valid class', () => { + + class ExtendedCommands extends MCommands {} + const extended = new ExtendedCommands(); + + assert.strictEqual(extended instanceof MCommands, true); + }); + + it('should return true if the instance meets all of the API criteria', () => { + + class Commands { + define() {} + get() {} + getAll() {} + isPath() {} + get pathsArr() {} + } + + const substitute = new Commands(); + + assert.strictEqual(substitute instanceof MCommands, true); + }); + }); +}); \ No newline at end of file diff --git a/tests/mocha.argParser.js b/tests/mocha.argParser.js new file mode 100644 index 0000000..5e7eaa3 --- /dev/null +++ b/tests/mocha.argParser.js @@ -0,0 +1,178 @@ +'use strict'; + +const + assert = require('assert'), + Command = require('./../libs/class.Category'), + argParser = require('./../libs/service.argParser'); + +let cmd1; + +function globalBe() { + cmd1 = new Command({ + path: 'cat1.cat2.command', + params: ['param1', 'param2=7'], + options: ['option1', 'option2', 'option3=hello'], + flags: ['flag1', 'flag2'], + description: 'This is a command', + execute: (args, done) => { + } + }); +} + +describe('argParser', () => { + + context('parameters', () => { + beforeEach(globalBe); + + it ('should correctly parse parameters (1)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '-option1', 'value1', '--flag2']); + assert.strictEqual(cmdArgs.paramsArr[0].name, 'param1'); + assert.strictEqual(cmdArgs.paramsArr[0].isDefaultValue, false); + assert.strictEqual(cmdArgs.paramsArr[0].value, 'arg1'); + }); + + it ('should correctly parse parameters (2)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '--flag1', '-option1', 'value1']); + assert.strictEqual(cmdArgs.paramsArr[1].name, 'param2'); + assert.strictEqual(cmdArgs.paramsArr[1].isDefaultValue, false); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'arg2'); + }); + + it ('should fill in default values', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1']); + assert.strictEqual(cmdArgs.paramsArr[1].name, 'param2'); + assert.strictEqual(cmdArgs.paramsArr[1].isDefaultValue, true); + assert.strictEqual(cmdArgs.paramsArr[1].value, '7'); + }); + }); + + context('options', () => { + beforeEach(globalBe); + + it('should correctly parse options (1)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '-option1', 'value1', '--flag2']); + assert.strictEqual(cmdArgs.optionsOMap['option1'].value, 'value1'); + assert.strictEqual(cmdArgs.optionsOMap['option2'].value, ''); + }); + + it('should correctly parse options (2)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '--flag1', '-option2', 'value2']); + assert.strictEqual(cmdArgs.optionsOMap['option2'].value, 'value2'); + assert.strictEqual(cmdArgs.optionsOMap['option1'].value, ''); + }); + + it('should correctly parse options (3)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '--flag1', '-option2', 'value2', '-option1', 'value1']); + assert.strictEqual(cmdArgs.optionsOMap['option2'].value, 'value2'); + assert.strictEqual(cmdArgs.optionsOMap['option1'].value, 'value1'); + }); + + it('should correctly add default value when option is not specified', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1']); + assert.strictEqual(cmdArgs.optionsOMap['option3'].value, 'hello'); + }); + + it('should correctly override default value when options is specified', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '-option3', 'goodbye']); + assert.strictEqual(cmdArgs.optionsOMap['option3'].value, 'goodbye'); + }); + }); + + context('flags', () => { + beforeEach(globalBe); + + it('should correctly parse flags (1)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '-option1', 'value1', '--flag2']); + assert.strictEqual(cmdArgs.flagsOMap['flag1'].value, false); + assert.strictEqual(cmdArgs.flagsOMap['flag2'].value, true); + }); + + it('should correctly parse flags (2)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '--flag1', '-option1', 'value1']); + assert.strictEqual(cmdArgs.flagsOMap['flag1'].value, true); + assert.strictEqual(cmdArgs.flagsOMap['flag2'].value, false); + }); + + it('should correctly parse flags (3)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '-option1', 'value1']); + assert.strictEqual(cmdArgs.flagsOMap['flag1'].value, false); + assert.strictEqual(cmdArgs.flagsOMap['flag2'].value, false); + }); + + it('should correctly parse flags (4)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '--flag2']); + assert.strictEqual(cmdArgs.flagsOMap['flag1'].value, false); + assert.strictEqual(cmdArgs.flagsOMap['flag2'].value, true); + }); + + it('should correctly parse flags (5)', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'arg2', '--flag2', '--flag1']); + assert.strictEqual(cmdArgs.flagsOMap['flag1'].value, true); + assert.strictEqual(cmdArgs.flagsOMap['flag2'].value, true); + }); + }); + + context('quoted values', ()=> { + beforeEach(globalBe); + + it('should keep double quoted text in the same value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '"this', 'is', 'quoted"']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this is quoted'); + }); + + it('should keep single quoted text in the same value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '\'this', 'is', 'quoted\'']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this is quoted'); + }); + + it('should keep tilde quoted text in the same value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '`this', 'is', 'quoted`']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this is quoted'); + }); + + it('should ignore single quotes while in a double quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '"this', '\'is\'', 'quoted"']); + assert.strictEqual(cmdArgs.paramsArr[1].value, `this 'is' quoted`); + }); + + it('should ignore tilde quotes while in a double quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '"this', '`is`', 'quoted"']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this `is` quoted'); + }); + + it('should ignore double quotes while in a single quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '\'this', '"is"', 'quoted\'']); + assert.strictEqual(cmdArgs.paramsArr[1].value, `this "is" quoted`); + }); + + it('should ignore tilde quotes while in a single quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '\'this', '`is`', 'quoted\'']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this `is` quoted'); + }); + + it('should ignore double quotes while in a tilde quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '`this', '"is"', 'quoted`']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'this "is" quoted'); + }); + + it('should ignore single quotes while in a tilde quote', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '`this', '\'is\'', 'quoted`']); + assert.strictEqual(cmdArgs.paramsArr[1].value, `this 'is' quoted`); + }); + + it('should ignore single quotes that do not start the value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', `john's`]); + assert.strictEqual(cmdArgs.paramsArr[1].value, `john's`); + }); + + it('should ignore tilde quotes that do not start the value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', 'john`s']); + assert.strictEqual(cmdArgs.paramsArr[1].value, 'john`s'); + }); + + it('should ignore double quotes that do not start the value', () => { + const cmdArgs = argParser.parse(cmd1, ['arg1', '`"john"`']); + assert.strictEqual(cmdArgs.paramsArr[1].value, '"john"'); + }); + }); +}); \ No newline at end of file diff --git a/tests/mocha.cmdParser.js b/tests/mocha.cmdParser.js new file mode 100644 index 0000000..e0badfd --- /dev/null +++ b/tests/mocha.cmdParser.js @@ -0,0 +1,179 @@ +'use strict'; + +const + assert = require('assert'), + Commands = require('./../libs/class.Commands'), + cmdParser = require('./../libs/service.cmdParser'); + +let commands; + +function globalBe() { + commands = new Commands(); + commands.define({ + path: 'command1', + params: ['param1', 'param2='], + options: ['option1', 'option2'], + flags: ['flag1', 'flag2'] + }); + commands.define({ + path: 'category1.command2', + params: ['param1', 'param2='], + options: ['option1', 'option2'], + flags: ['flag1', 'flag2'] + }); +} + +describe('cmdParser', () => { + + beforeEach(globalBe); + + it('should parse correct command path (command1)', () => { + const result = cmdParser.parse(commands, ['command1', 'arg1', '-option2', 'value2', '--flag1']); + assert.strictEqual(result.path, 'command1'); + }); + + it('should parse correct args (command1)', () => { + const result = cmdParser.parse(commands, ['command1', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.args, ['arg1', '-option2', 'value2', '--flag1']); + }); + + it('should return correct isHelp result (command1)', () => { + const result = cmdParser.parse(commands, ['command1', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.isHelp, false); + }); + + it('should parse correct command path (category1.command2)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', 'arg1', '-option2', 'value2', '--flag1']); + assert.strictEqual(result.path, 'category1.command2'); + }); + + it('should parse correct args (category1.command2)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.args, ['arg1', '-option2', 'value2', '--flag1']); + }); + + it('should return correct isHelp result (category1.command2)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.isHelp, false); + }); + + it('should return known categories and commands and exclude unregistered command (fakecommand)', () => { + const result = cmdParser.parse(commands, ['fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.strictEqual(result.path, ''); + }); + + it('should return unregistered command as argument (fakecommand)', () => { + const result = cmdParser.parse(commands, ['fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.args, ['fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + }); + + it('should return correct isHelp result (fakecommand)', () => { + const result = cmdParser.parse(commands, ['fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.isHelp, false); + }); + + it('should return known categories and commands and exclude unregistered command (category1.fakecommand)', () => { + const result = cmdParser.parse(commands, ['category1', 'fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.strictEqual(result.path, 'category1'); + }); + + it('should return unregistered command as argument (category1.fakecommand)', () => { + const result = cmdParser.parse(commands, ['category1', 'fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.args, ['fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + }); + + it('should return correct isHelp result (category1.fakecommand)', () => { + const result = cmdParser.parse(commands, ['category1', 'fakecommand', 'arg1', '-option2', 'value2', '--flag1']); + assert.deepEqual(result.isHelp, false); + }); + + it('should parse correct command path (?)', () => { + const result = cmdParser.parse(commands, ['?']); + assert.strictEqual(result.path, ''); + }); + + it('should parse correct args (?)', () => { + const result = cmdParser.parse(commands, ['?']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (?)', () => { + const result = cmdParser.parse(commands, ['?']); + assert.deepEqual(result.isHelp, true); + }); + + it('should parse correct command path (command1 ?)', () => { + const result = cmdParser.parse(commands, ['command1', '?']); + assert.strictEqual(result.path, 'command1'); + }); + + it('should parse correct args (command1 ?)', () => { + const result = cmdParser.parse(commands, ['command1', '?']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (command1 ?)', () => { + const result = cmdParser.parse(commands, ['command1', '?']); + assert.deepEqual(result.isHelp, true); + }); + + it('should parse correct command path (category1.command2 ?)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '?']); + assert.strictEqual(result.path, 'category1.command2'); + }); + + it('should parse correct args (category1.command2 ?)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '?']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (category1.command2 ?)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '?']); + assert.deepEqual(result.isHelp, true); + }); + + it('should parse correct command path (--help)', () => { + const result = cmdParser.parse(commands, ['--help']); + assert.strictEqual(result.path, ''); + }); + + it('should parse correct args (--help)', () => { + const result = cmdParser.parse(commands, ['--help']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (--help)', () => { + const result = cmdParser.parse(commands, ['--help']); + assert.deepEqual(result.isHelp, true); + }); + + it('should parse correct command path (command1 --help)', () => { + const result = cmdParser.parse(commands, ['command1', '--help']); + assert.strictEqual(result.path, 'command1'); + }); + + it('should parse correct args (command1 --help)', () => { + const result = cmdParser.parse(commands, ['command1', '--help']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (command1 --help)', () => { + const result = cmdParser.parse(commands, ['command1', '--help']); + assert.deepEqual(result.isHelp, true); + }); + + it('should parse correct command path (category1.command2 --help)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '--help']); + assert.strictEqual(result.path, 'category1.command2'); + }); + + it('should parse correct args (category1.command2 --help)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '--help']); + assert.deepEqual(result.args, []); + }); + + it('should return correct isHelp result (category1.command2 --help)', () => { + const result = cmdParser.parse(commands, ['category1', 'command2', '--help']); + assert.deepEqual(result.isHelp, true); + }); +}); \ No newline at end of file