├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── src └── utils.js └── test ├── index.test.js ├── resources ├── .invalidrc ├── .jarvisrc ├── executor.jarvis ├── executors.jarvis ├── import-env.invalid ├── import-env.jarvis ├── import-script-1.jarvis ├── import-script-2.jarvis ├── import-script-3.jarvis ├── import-with-arguments.jarvis ├── invalid-json-import.jarvis ├── invalid-json-path-import.jarvis ├── json-import.jarvis ├── json │ ├── sample-invalid.json │ └── sample.json ├── source-script.jarvis └── test.jarvis └── utils.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .coveralls.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hasitha Liyanage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # J.A.R.V.I.S - Just Another Rudimentary Verbal Instruction Shell (BETA) 2 | 3 | ![build](https://travis-ci.org/hliyan/jarvis.svg?branch=master) 4 | [![Coverage Status](https://coveralls.io/repos/github/hliyan/jarvis/badge.svg?branch=master)](https://coveralls.io/github/hliyan/jarvis?branch=master) 5 | 6 | ## Table of Contents 7 | 8 | - [Introduction](#introduction) 9 | - [Installation](#installation) 10 | - [Basic example: wrapping an existing library](#basic-example-wrapping-an-existing-library) 11 | - [Command line integration](#command-line-integration) 12 | - [Interactive CLI](#interactive-cli) 13 | - [Script mode](#script-mode) 14 | - [Constants](#constants) 15 | - [Macros and variables](#macros-and-variables 16 | 17 | ## Introduction 18 | 19 | JARVIS helps you write rudimentary English wrappers around libraries or APIs, like this: 20 | 21 | ```javascript 22 | // wrap your JavaScript function with an English API: 23 | jarvis.addCommand({ 24 | command: '$number to the power of $power', 25 | handler: ({args: {number, power}}) => { 26 | const result = Math.pow(parseInt(number), parseInt(power)); 27 | return `${number} to the power of ${power} is ${result}!`; 28 | } 29 | }); 30 | ``` 31 | 32 | Use it from an interactive command line prompt 33 | 34 | ```shell 35 | > 2 to the power of 3 36 | 2 to the power of 3 is 8! 37 | ``` 38 | 39 | ## Installation 40 | 41 | ``` 42 | npm install --save hliyan/jarvis 43 | ``` 44 | 45 | ## Basic example: wrapping an existing library 46 | 47 | Invoke an API using natural language. 48 | 49 | ```javascript 50 | 51 | const Jarvis = require('jarvis'); // use jarvis to 52 | const IssueClient = require('issue-client'); // wrap this with a basic english API 53 | 54 | const app = new Jarvis(); 55 | const client = new IssueClient(); 56 | 57 | // register command 58 | app.addCommand({ 59 | command: 'connectToRepository $repoName', 60 | aliases: [ 61 | 'connect to $repoName', 62 | 'connect repo $repoName', 63 | 'connect to $repoName repo' 64 | ], 65 | handler: async ({args: {repoName}}) => { 66 | const res = await client.connect(repoName); 67 | return res.success ? `Connected to ${repoName}.` : `Could not connect to ${repoName}. Here's the error: ${res.error}`; 68 | } 69 | }); 70 | 71 | // exercise the command 72 | const res = await app.send('connect to hliyan/jarvis'); 73 | console.log(res); // "Connected to hliyan/jarvis." 74 | ``` 75 | 76 | ## Command line integration 77 | 78 | Invoke an API using natural language, as a shell command. 79 | 80 | ```javascript 81 | const FAQClient = require('./faq'); // business logic from here 82 | const Jarvis = require('jarvis'); // wrapped by jarvis 83 | const readline = require('readline'); // and connected to a command line 84 | 85 | const app = new Jarvis(); 86 | const client = new FAQClient(); 87 | 88 | // register the command 89 | app.addCommand({ 90 | command: 'getCountryPresident $country', 91 | aliases: [ 92 | 'who is the president of $country', 93 | '$country president' 94 | ], 95 | handler: async ({args: {country}}) => { 96 | const president = await client.getPresident(country); 97 | return president ? `the president of ${country} is ${president}` 98 | : `i don't know ${country}`; 99 | } 100 | }); 101 | 102 | // start the CLI 103 | const rl = readline.createInterface({ 104 | input: process.stdin, 105 | output: process.stdout, 106 | prompt: 'jarvis> ' 107 | }); 108 | 109 | rl.prompt(); 110 | 111 | // feed CLI input to the app, and app output back to CLI 112 | rl.on('line', async (line) => { 113 | const res = await app.send(line.trim()); 114 | console.log(res ? ` ${res}` : ' I don\'t understand'); 115 | rl.prompt(); 116 | }); 117 | 118 | // TODO: error handling and other best practices 119 | ``` 120 | 121 | Running: 122 | ```shell 123 | $ node index.js 124 | jarvis> who is the president of russia 125 | the president of russia is Vladamir Putin 126 | jarvis> usa president 127 | the president of usa is Barack Obama 128 | jarvis> us president 129 | i don't know us 130 | jarvis> foo 131 | I don't understand 132 | jarvis> 133 | ``` 134 | 135 | * Full source: [hliyan/jarvis-sample-app](https://github.com/hliyan/jarvis-sample-app) 136 | 137 | ## Interactive CLI 138 | 139 | Use this when the workflow you're trying to wrap is too complicated to execute as a single line command. 140 | 141 | You can enter an interactive command session using `jarvis.startCommand($name)` and exit that particular session using `jarvis.endCommand()`. State that needs to be maintained for the duration of the interactive session can be set using `jarvis.setCommandState($object)`. 142 | 143 | ```javascript 144 | const jarvis = new Jarvis(); 145 | jarvis.addCommand({ 146 | command: 'repl', 147 | handler: ({context, line}) => { 148 | if (!context.activeCommand) { 149 | context.startCommand('repl'); 150 | context.setCommandState({status: 'awaitInput'}); 151 | return 'Enter input: '; 152 | } 153 | 154 | if (context.state.status === 'awaitInput') { 155 | const out = 'Handled: ' + line; 156 | return out; 157 | } 158 | } 159 | }); 160 | ``` 161 | 162 | Expected output 163 | ``` 164 | $ repl 165 | $ Enter input: 166 | $ bar 167 | $ Handled: bar 168 | $ .. # built in exit 169 | $ Done with repl. 170 | ``` 171 | 172 | ## Script mode 173 | 174 | You can use this to run your natural language commands as a script. 175 | 176 | Create a script file, e.g. 177 | 178 | ``` 179 | start 180 | connect to repo 'hliyan/jarvis' 181 | get open issues 182 | write issues to 'home/john/issues.json' 183 | end 184 | ``` 185 | 186 | Create a script runner with the correct bindings 187 | 188 | ``` 189 | const Jarvis = require('jarvis'); 190 | const app = new Jarvis(); 191 | 192 | // bind commands as described earlier 193 | 194 | // run script 195 | app.run('test.jarvis', function(input, output) { 196 | console.log(input); 197 | console.log(output); 198 | }); 199 | 200 | ``` 201 | 202 | ## Constants 203 | 204 | ``` 205 | in this context 206 | HOME is 'https://foo.bar.com' 207 | USER is 'john' 208 | end 209 | ``` 210 | 211 | ## Macros and variables 212 | 213 | You can use this to re-use blocks of commands within a script. 214 | 215 | ``` 216 | in this context 217 | PI is 3.14 218 | end 219 | 220 | how to get area of circle with radius $radius 221 | # more statements here 222 | end 223 | ``` 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const URL = require('url'); 2 | const events = require('events'); 3 | const { 4 | parseCommand, 5 | tokenize, 6 | parseInputTokens, 7 | parseMacroInputTokens, 8 | parseMacroSubCommand, 9 | parseScript, 10 | validateScript, 11 | importJson, 12 | validateEnvFileName 13 | } = require("./src/utils"); 14 | 15 | class Jarvis { 16 | constructor() { 17 | this.commands = []; // list of registered commands 18 | this.macros = []; // list of registered macros 19 | this.activeCommand = null; // currently active command 20 | this.activeMacro = null; // currently active macro 21 | this.activeContext = null; // temporally holding details of currently active constants and imports in command `in this context` 22 | this.state = {}; // state variables for currently active command 23 | this.constants = {}; // registered constants 24 | this.environmentVariables = {};// store extracted environment variables 25 | this.isInStartBlock = false; // state variable to check whether inside start block 26 | this.importStack = [__filename]; // use at the time of interpretation to keep track of the import hierarchy in files, default value as current file which used in CLI mode 27 | this.baseScriptPath = __filename; // the path of the file with which jarvis was invoked. used for resolving import paths, default value as current file which used in CLI mode 28 | this.importScriptDetails = {}; // contains the imported constants and macros based on the imported script path, USAGE: {'./test.jarvis': ['BASE_URL']} 29 | this.eventEmitter = new events.EventEmitter(); // use at the time of script interpretation to emit the responses in run time 30 | } 31 | 32 | /** 33 | * Checks for available scripts to switch mode to script mode if a 34 | * script and env file(optional parameter) with specified extension is provided 35 | * re-initialize the base script path according to given script 36 | * USAGE: jarvis.addScriptMode('jarvis', 'script.jarvis', '.jarvisrc'); 37 | */ 38 | async addScriptMode(extension, script, envFile) { 39 | if (!(script && validateScript(extension, script))) { 40 | return null; 41 | } 42 | if (envFile && validateEnvFileName(`${extension}rc`, envFile)) { 43 | const res = await this._loadEnvFile(envFile); 44 | if (res.error) { 45 | return res.error; 46 | } 47 | } 48 | this.baseScriptPath = script; 49 | this.importStack = [script]; 50 | return await this._runScript(script); 51 | } 52 | 53 | /** 54 | * Registers a new command with Jarvis 55 | * USAGE: jarvis.addCommand({ command: 'test', handler: () => {}}); 56 | */ 57 | addCommand({ command, handler, aliases, help, description }) { 58 | const patterns = []; 59 | patterns.push({ tokens: parseCommand(command) }); 60 | if (aliases) { 61 | aliases.forEach((alias) => { 62 | patterns.push({ tokens: parseCommand(alias) }) 63 | }); 64 | } 65 | 66 | this.commands.push({ 67 | command: command, 68 | handler: handler, 69 | help: help, 70 | tokens: parseCommand(command), 71 | patterns, 72 | description, 73 | aliases 74 | }); 75 | } 76 | 77 | /** 78 | * To be called only by command handlers, to indicate the start of 79 | * an interactive shell session 80 | */ 81 | startCommand(commandName) { 82 | this.activeCommand = this._findCommand(commandName); 83 | } 84 | 85 | /** 86 | * To be called only by command handlers, to indicate the end of 87 | * an interactive shell session 88 | */ 89 | endCommand() { 90 | this.activeCommand = null; 91 | this.state = {}; 92 | } 93 | 94 | /** 95 | * To be called only by command handlers for an interactive shell command 96 | * Can be used to set variables for the duration of that shell command 97 | */ 98 | setCommandState(data) { 99 | Object.assign(this.state, data); 100 | } 101 | 102 | 103 | _findCommand(line) { 104 | const inputTokens = tokenize(line); 105 | for (let i = 0; i < this.commands.length; i++) { 106 | const command = this.commands[i]; 107 | if (parseInputTokens(command, inputTokens)) { 108 | return command; 109 | } 110 | } 111 | return null; 112 | } 113 | 114 | /** 115 | * checks whether the constant format is found in the tokens 116 | * if found replace the constant with the corresponding value 117 | * else leaves the token as it is 118 | * returns the parsed token array at the end 119 | */ 120 | _parseConstants(tokens) { 121 | return tokens.map((token) => { 122 | const innerConstants = token.match(/\$[A-Z_][0-9A-Z_]*/g); 123 | if (innerConstants) { 124 | /** 125 | * if a token is an exact constant match 126 | * returns the corresponding value of the constant 127 | * return value is either a string or an object 128 | * ex: token = "$HOST" returns "google.lk" 129 | * ex: token = "$APP_OBJECT" returns {name: "JARVIS"} 130 | */ 131 | if (innerConstants.length === 1 && token === innerConstants[0]) { 132 | const key = token.replace('$', ''); 133 | const value = this.constants[key]; 134 | return value ? value : token; 135 | } 136 | 137 | /** 138 | * if a token string contains constants within the string 139 | * returns the constant replaced string 140 | * return value is a string (objects are stringified) 141 | * ex: token = "$HOST/$API_VERSION/index.html" returns "google.lk/v1/index.html" 142 | * ex: token = "Object: $JSON_OBJECT" returns "Object: {"name": "JARVIS"}" 143 | */ 144 | innerConstants.forEach((innerConstant) => { 145 | const key = innerConstant.replace('$', ''); 146 | const value = this.constants[key]; 147 | if (value) { 148 | token = (typeof value === 'object') ? token.replace(innerConstant, JSON.stringify(value)) : token.replace(innerConstant, value); 149 | } 150 | }) 151 | } 152 | return token; 153 | }) 154 | } 155 | 156 | /** 157 | * parses the constant to corresponding values 158 | * if command is null, then consider as accepting prompted input 159 | * for the active command 160 | * then run the command handler with the arguments 161 | */ 162 | async _runCommand(command, line) { 163 | const inputTokens = tokenize(line); 164 | const constantParsedTokens = this._parseConstants(inputTokens); 165 | const handler = command ? command.handler : this.activeCommand.handler; 166 | 167 | return await handler({ 168 | context: this, 169 | line, 170 | tokens: inputTokens, 171 | args: command ? parseInputTokens(command, constantParsedTokens).args : {} 172 | }); 173 | } 174 | 175 | /** 176 | * validate the constant format, 177 | * if valid, add the constant to active context 178 | * else return an error message 179 | */ 180 | _setConstantInActiveContext(key, value) { 181 | if (!/[A-Z_][0-9A-Z_]/.test(key)) { 182 | return 'A constant name should be in block letters.'; 183 | } 184 | if (this.constants[key]) { 185 | return `'${key}' constant already exists!`; 186 | } 187 | // get the top most value of the stack which is the currently active script 188 | const currentScriptPath = this.importStack[this.importStack.length - 1]; 189 | let constant = { key, value }; 190 | this.activeContext[currentScriptPath].push(constant); 191 | } 192 | 193 | /** 194 | * Sends a command to the shell 195 | */ 196 | async send(line) { 197 | if (!line) { 198 | return null; 199 | } 200 | line = line.trim(); 201 | 202 | if (this.activeCommand) { 203 | if (line === '..') { 204 | const out = 'Done with ' + this.activeCommand.command + '.'; 205 | this.endCommand(); 206 | return out; 207 | } 208 | return this._runCommand(null, line); 209 | } 210 | 211 | if (line.startsWith("how to ")) { 212 | let macroCommand = line.replace('how to', '').trim(); 213 | if (this._findMacro(macroCommand)) { 214 | return `Macro name already exists!` 215 | } 216 | 217 | this.activeMacro = { 218 | command: macroCommand, 219 | subCommands: [] 220 | } 221 | 222 | const out = 'You are now entering a macro. Type the statements, one line at a time. When done, type \'end\'.' 223 | return out; 224 | } 225 | 226 | if (this.activeMacro) { 227 | if (line === 'end') { 228 | this._addMacro(this.activeMacro); 229 | const out = `Macro "${this.activeMacro.command}" has been added.`; 230 | this.activeMacro = null; 231 | return out; 232 | } 233 | 234 | if (this._findCommand(line) || this._findMacro(line)) { 235 | this.activeMacro.subCommands.push(line); 236 | return; 237 | } else { 238 | return `Not a valid Command/Macro.` 239 | } 240 | } 241 | 242 | // get the top most value of the stack which is currently active script 243 | const currentScriptPath = this.importStack[this.importStack.length - 1]; 244 | 245 | /** 246 | * active context stores currently active context details 247 | * temporally keeps the details of constant definitions in import hierarchy based on the script path 248 | * USAGE: {'./script.jarvis': [{key: 'BASE_URL', value: 'www.google.com'}]} 249 | */ 250 | if (line.startsWith("in this context")) { 251 | this.activeContext = { 252 | ...this.activeContext, 253 | [currentScriptPath]: [] 254 | } 255 | const out = 'You are now entering constants. Type the constants, one line at a time. When done, type \'end\'.' 256 | return out; 257 | } 258 | 259 | if (this.activeContext && this.activeContext[currentScriptPath]) { 260 | /** 261 | * adds temporally stored constants to global constants 262 | * clear the active context when traverse back to the base script 263 | */ 264 | if (line === 'end') { 265 | let keyList = []; 266 | this.activeContext[currentScriptPath].forEach(constant => { 267 | this.constants[constant.key] = constant.value; 268 | keyList.push(constant.key); 269 | }) 270 | 271 | if (currentScriptPath === this.baseScriptPath && this.activeContext != null) { 272 | this.activeContext = null; 273 | } 274 | const out = `Constants "${keyList}" have been added.`; 275 | return out; 276 | } 277 | 278 | /** 279 | * handle constant imports from env file 280 | * check whether the constant is available within loaded constants from env file 281 | */ 282 | const envParams = line.match(/(.+) is from env/); 283 | if (envParams) { 284 | const [, envKey] = envParams; 285 | if (!this.environmentVariables[envKey]) { 286 | return `${envKey} is not defined in env file!` 287 | } 288 | this._setConstantInActiveContext(envKey, this.environmentVariables[envKey]); 289 | return; 290 | } 291 | 292 | /** 293 | * checks whether the `line` is in the format of import script or JSON import 294 | * if so it extracts the importing resource (`constant`, `macro` or `JSON`) and the `path` 295 | * then import the file if it is not already imported or set the constant if a JSON import 296 | */ 297 | const importParams = line.match(/(.+) is from ['"](.+)['"]/i); 298 | if (importParams) { 299 | const [, resource, relativeScriptPath] = importParams; 300 | const scriptPath = URL.resolve(this.baseScriptPath, relativeScriptPath); 301 | 302 | /** 303 | * checks whether the importing file is a JSON 304 | * if so parse the JSON file to a JSON object 305 | * then save the JSON object as a constant 306 | * if an invalid JSON import, returns the error message 307 | */ 308 | if (scriptPath && /(.)+.json$/gi.test(scriptPath)) { 309 | const jsonObject = importJson(scriptPath); 310 | if (jsonObject.error) { 311 | return jsonObject.error; 312 | } 313 | return this._setConstantInActiveContext(resource, jsonObject); 314 | } 315 | 316 | /** 317 | * checks whether the importing file is already imported 318 | * if not add the script path to stack and run the importing script 319 | */ 320 | if (!this.importScriptDetails[scriptPath]) { 321 | this.importScriptDetails[scriptPath] = []; 322 | this.importStack.push(scriptPath); 323 | await this._runScript(scriptPath); 324 | this.importStack.pop(); 325 | } 326 | 327 | /** 328 | * TODO: 329 | * whitelisting only the imported constants and macros 330 | */ 331 | this.importScriptDetails[scriptPath].push(resource); 332 | return `Script: ${scriptPath} imported`; 333 | } 334 | 335 | /** 336 | * checks whether the `line` is in the format of constant definition 337 | * if so it extracts the `key` and `value` and set it in the active context 338 | */ 339 | const constantParams = line.match(/(.+) is ['"](.+)['"]/i); 340 | if (constantParams) { 341 | const [, key, value] = constantParams; 342 | return this._setConstantInActiveContext(key, value); 343 | } 344 | } 345 | 346 | return await this._execute(line); 347 | } 348 | 349 | /** 350 | * Wrapper for the event emitter 351 | */ 352 | on(event, callback) { 353 | this.eventEmitter.on(event, callback); 354 | } 355 | 356 | /** 357 | * if the 'line' is not found in commands 358 | * it will search in macros 359 | */ 360 | async _execute(line) { 361 | const command = this._findCommand(line); 362 | if (command) { 363 | return this._runCommand(command, line); 364 | } else { 365 | const macro = this._findMacro(line); 366 | return macro ? await this._runMacro(macro) : null; 367 | } 368 | } 369 | 370 | /** 371 | * Register a new macro with JARVIS 372 | * USAGE: jarvis._addMacro(macro); 373 | */ 374 | _addMacro({ command, subCommands }) { 375 | this.macros.push({ 376 | command: command, 377 | tokens: parseCommand(command), 378 | subCommands: subCommands, 379 | }) 380 | } 381 | 382 | /** 383 | * Execute sub commands of the macro 384 | */ 385 | async _runMacro(macro) { 386 | let subCommandsStatus = []; 387 | for (let line of macro.subCommands) { 388 | line = parseMacroSubCommand(line, macro.args); 389 | subCommandsStatus.push(await this._execute(line)); 390 | } 391 | return subCommandsStatus; 392 | } 393 | 394 | /** 395 | * Find the macro by sending macro name 396 | */ 397 | _findMacro(line) { 398 | const inputTokens = tokenize(line); 399 | for (let i = 0; i < this.macros.length; i++) { 400 | const macro = this.macros[i]; 401 | const args = parseMacroInputTokens(macro, inputTokens); 402 | if (args) 403 | return Object.assign({}, macro, args) 404 | } 405 | return null; 406 | } 407 | 408 | /** 409 | * Execute a provided script 410 | */ 411 | async _runScript(script) { 412 | let res = []; 413 | const commands = parseScript(script); 414 | for (const command of commands) { 415 | if (this._isInGlobalScope()) { 416 | if (command.startsWith("start")) { 417 | this.isInStartBlock = true; 418 | continue; 419 | } 420 | else if (command === 'end') { 421 | this.isInStartBlock = false; 422 | continue; 423 | } 424 | } 425 | if (this._isInExecutableContext(command)) { 426 | const response = await this.send(command); 427 | /** 428 | * Emits the response along with the corresponding command 429 | * so the listener can get the response via `command` event in run time 430 | */ 431 | if (this.isInStartBlock) { 432 | this.eventEmitter.emit('command', { command, response }); 433 | } 434 | res.push(response); 435 | } 436 | } 437 | return res; 438 | } 439 | 440 | /** 441 | * Check whether a specific line is in executable context 442 | */ 443 | _isInExecutableContext(line) { 444 | // line is sent to shell if inside a start block or if an active macro/a context exist or if special keyword 445 | if (this.isInStartBlock || !this._isInGlobalScope() || line.startsWith('in this context') 446 | || line.startsWith('how to')) { 447 | return true; 448 | } 449 | } 450 | 451 | /** 452 | * Check for global scope 453 | */ 454 | _isInGlobalScope() { 455 | if (!this.activeContext && !this.activeMacro) { 456 | return true; 457 | } 458 | } 459 | 460 | /** 461 | * Load a provided env file 462 | * Save env variables as constants 463 | * returns an error for invalid env files 464 | */ 465 | async _loadEnvFile(envFile) { 466 | let fileContent; 467 | try { 468 | fileContent = parseScript(envFile); 469 | } catch (error) { 470 | return { error: 'Could not read env file from specified location!' }; 471 | } 472 | //check whether the first and last lines of the file matches the required syntax 473 | if (!(fileContent[0] === "in this context" && fileContent[fileContent.length - 1] === "end")) { 474 | return { error: 'Invalid syntax in env file!' }; 475 | } 476 | for (let line of fileContent) { 477 | line = line.trim(); 478 | const matches = this._matchConstantFormat(line); 479 | if (matches) { 480 | const [, key, value] = matches; 481 | this.environmentVariables[key] = value; 482 | } 483 | } 484 | return { success: 'Successfully loaded environment file!' } 485 | } 486 | 487 | /** 488 | * checks whether a given `line` is in the format of a constant definition 489 | * if so returns the matches 490 | */ 491 | _matchConstantFormat(line) { 492 | return line.match(/(.+) is ['"](.+)['"]/i); 493 | } 494 | } 495 | 496 | module.exports = Jarvis; 497 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jarvis", 3 | "version": "1.0.0", 4 | "description": "J.A.R.V.I.S - Just Another Reusable Verbal Interpreter Shell", 5 | "main": "./index.js", 6 | "directories": { 7 | "lib": "./" 8 | }, 9 | "scripts": { 10 | "test": "jest --maxWorkers=4 --coverage --coverageReporters=text-lcov | coveralls" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/hliyan/jarvis.git" 15 | }, 16 | "author": "Hasitha N. Liyanage", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/hliyan/jarvis/issues" 20 | }, 21 | "homepage": "https://github.com/hliyan/jarvis#readme", 22 | "devDependencies": { 23 | "coveralls": "^3.0.2", 24 | "jest": "^24.8.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // jarvis, just another rudimentary verbal interface shell 2 | // converts 'hello "John Doe"' to ['hello', 'John, Doe'] 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const tokenize = line => { 6 | const tokens = line.match(/"([^"]+)"|\S+/g); 7 | for (let i = 0; i < tokens.length; i++) { 8 | tokens[i] = tokens[i].replace(/"/g, ''); 9 | } 10 | return tokens; 11 | }; 12 | exports.tokenize = tokenize; 13 | 14 | // converts 'hello $name' to 15 | // [{value: 'hello', isArg: false}, {value: name, isArg: true}] 16 | const parseCommand = (commandStr) => { 17 | const tokens = []; 18 | commandStr.split(" ").forEach(token => { 19 | tokens.push({ 20 | value: token.replace(/\$/g, ""), 21 | isArg: token.includes("$") 22 | }); 23 | }); 24 | return tokens; 25 | }; 26 | exports.parseCommand = parseCommand; 27 | 28 | // checks tokens against all the patterns in the command 29 | // returns args if match, else null 30 | const parseInputTokens = (command, inputTokens) => { 31 | for (let i = 0; i < command.patterns.length; i++) { // for each pattern 32 | const patternTokens = command.patterns[i].tokens; 33 | if (patternTokens.length === inputTokens.length) { 34 | const args = {}; 35 | let match = true; 36 | for (let j = 0; j < patternTokens.length; j++) { // for each token in pattern 37 | const patternToken = patternTokens[j]; 38 | if (patternToken.isArg) { 39 | args[patternToken.value] = inputTokens[j]; 40 | } else { 41 | if (inputTokens[j] !== patternToken.value) { 42 | match = false; 43 | break; 44 | } 45 | } 46 | } 47 | if (match) 48 | return { args }; 49 | } 50 | } 51 | return null; 52 | }; 53 | 54 | exports.parseInputTokens = parseInputTokens; 55 | 56 | // checks tokens against the macro command 57 | // returns args if match, else null 58 | const parseMacroInputTokens = (macro, inputTokens) => { 59 | const patternTokens = macro.tokens; 60 | if (patternTokens.length === inputTokens.length) { 61 | const args = {}; 62 | let match = true; 63 | for (let j = 0; j < patternTokens.length; j++) { // for each token in pattern 64 | const patternToken = patternTokens[j]; 65 | if (patternToken.isArg) { 66 | args[patternToken.value] = inputTokens[j]; 67 | } else { 68 | if (inputTokens[j] !== patternToken.value) { 69 | match = false; 70 | break; 71 | } 72 | } 73 | } 74 | if (match) 75 | return { args }; 76 | } 77 | return null; 78 | }; 79 | exports.parseMacroInputTokens = parseMacroInputTokens; 80 | 81 | // change variable tokens to values of args 82 | // returns same string if no variables found 83 | const parseMacroSubCommand = (line, args) => { 84 | let tokens = parseCommand(line); 85 | let parsedLine = line; 86 | tokens.forEach((token) => { 87 | if (token.isArg) { 88 | if (args[token.value]) { 89 | parsedLine = parsedLine.replace(`$${token.value}`, `"${args[token.value]}"`); 90 | } 91 | } 92 | }) 93 | 94 | return parsedLine; 95 | }; 96 | exports.parseMacroSubCommand = parseMacroSubCommand; 97 | 98 | // returns string content by reading a script 99 | const parseScript = filename => { 100 | let content; 101 | try { 102 | content = fs.readFileSync(filename, "utf8"); 103 | } catch (error) { 104 | throw new Error('Could not read file from the specified location!'); 105 | } 106 | const lines = content.split("\n"); 107 | const filteredCommands = lines.filter(line => { 108 | return line !== "" && line.trim() !== "" && !line.trim().startsWith("#"); 109 | }); 110 | return filteredCommands; 111 | }; 112 | exports.parseScript = parseScript; 113 | 114 | // checks the validity of a provided script 115 | const validateScript = (extension, file) => { 116 | if (path.extname(file) === `.${extension}`) { 117 | return true; 118 | } 119 | }; 120 | exports.validateScript = validateScript; 121 | 122 | //checks whether a given env file/file path matches to a given file name 123 | const validateEnvFileName = (fileName, envFile) => { 124 | return envFile.split('.').pop() === fileName; 125 | }; 126 | exports.validateEnvFileName = validateEnvFileName; 127 | 128 | // read and parse the JSON file 129 | // if a valid JSON, returns the parsed JSON object 130 | // else returns an error object 131 | const importJson = (filename) => { 132 | try { 133 | const content = fs.readFileSync(filename, "utf8"); 134 | return JSON.parse(content); 135 | } catch (error) { 136 | if (error.code === 'ENOENT') { 137 | return { error: 'Could not read the JSON file from the specified location!' }; 138 | } else { 139 | return { error: 'Invalid JSON import!' }; 140 | } 141 | } 142 | } 143 | exports.importJson = importJson; 144 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const Jarvis = require("../index"); 2 | 3 | describe("basic command", () => { 4 | const jarvis = new Jarvis(); 5 | jarvis.addCommand({ 6 | command: "simple", 7 | handler: ({ context, line }) => { 8 | return "tested: " + line; 9 | } 10 | }); 11 | 12 | test("should return the expected output", async () => { 13 | expect(await jarvis.send("simple")).toEqual("tested: simple"); 14 | }); 15 | 16 | test("should return null for unknown command", async () => { 17 | expect(await jarvis.send("foo")).toBe(null); 18 | }); 19 | 20 | test("should return null for undefined command", async () => { 21 | expect(await jarvis.send()).toBe(null); 22 | }); 23 | 24 | test("should return null for empty string", async () => { 25 | expect(await jarvis.send("")).toBe(null); 26 | }); 27 | }); 28 | 29 | describe("interactive command", () => { 30 | const jarvis = new Jarvis(); 31 | jarvis.addCommand({ 32 | command: "repl", 33 | handler: ({ context, line }) => { 34 | if (!context.activeCommand) { 35 | context.startCommand("repl"); 36 | context.setCommandState({ status: "awaitInput" }); 37 | return "Enter input: "; 38 | } 39 | 40 | if (context.state.status === "awaitInput") { 41 | const out = "Handled: " + line; 42 | return out; 43 | } 44 | } 45 | }); 46 | 47 | test("should enter the mode with the keyword", async () => { 48 | expect(await jarvis.send("repl")).toEqual("Enter input: "); 49 | }); 50 | 51 | test("should prompt for input in sequence", async () => { 52 | expect(await jarvis.send("bar")).toEqual("Handled: bar"); 53 | }); 54 | 55 | test("should exit the mode with ..", async () => { 56 | expect(await jarvis.send("..")).toEqual("Done with repl."); 57 | }); 58 | }); 59 | 60 | describe("static phrase command", () => { 61 | const jarvis = new Jarvis(); 62 | jarvis.addCommand({ 63 | command: "how are you", 64 | handler: ({ line, args }) => { 65 | return "I'm fine"; 66 | } 67 | }); 68 | 69 | jarvis.addCommand({ 70 | command: "how are you doing", 71 | handler: ({ line, args }) => { 72 | return "I'm doing well"; 73 | } 74 | }); 75 | 76 | test("should match the phrase exactly", async () => { 77 | expect(await jarvis.send("how are you")).toEqual("I'm fine"); 78 | expect(await jarvis.send("how are")).toEqual(null); 79 | expect(await jarvis.send("how are you doing")).toEqual("I'm doing well"); 80 | }); 81 | }); 82 | 83 | describe("command handler", () => { 84 | const jarvis = new Jarvis(); 85 | 86 | test("should receive argument array", async () => { 87 | jarvis.addCommand({ 88 | command: "how are you", 89 | handler: ({ line, tokens }) => { 90 | expect(tokens).toEqual(["how", "are", "you", "doing", "John Doe"]); 91 | return "I'm fine"; 92 | } 93 | }); 94 | await jarvis.send('how are you doing "John Doe"'); 95 | }); 96 | }); 97 | 98 | describe("command with args", () => { 99 | const jarvis = new Jarvis(); 100 | 101 | test("should match with variables", async () => { 102 | jarvis.addCommand({ 103 | command: "say hello to $name now", 104 | handler: ({ tokens, args }) => { 105 | expect(tokens).toEqual(["say", "hello", "to", "John Doe", "now"]); 106 | expect(args).toEqual({ name: "John Doe" }); 107 | return `Hello ${args.name}`; 108 | } 109 | }); 110 | 111 | expect(await jarvis.send('say hello to "John Doe" now')).toEqual( 112 | "Hello John Doe" 113 | ); 114 | }); 115 | }); 116 | 117 | describe("aliases", () => { 118 | const jarvis = new Jarvis(); 119 | jarvis.addCommand({ 120 | command: "greet $name", 121 | aliases: ["hello $name how are you"], 122 | handler: ({ args }) => { 123 | return `Hello ${args.name}`; 124 | } 125 | }); 126 | 127 | test("should match main command", async () => { 128 | expect(await jarvis.send('greet "John Doe"')).toEqual("Hello John Doe"); 129 | }); 130 | 131 | test("should match alias", async () => { 132 | expect(await jarvis.send('hello "John Doe" how are you')).toEqual( 133 | "Hello John Doe" 134 | ); 135 | }); 136 | }); 137 | 138 | describe("command help", () => { 139 | const jarvis = new Jarvis(); 140 | jarvis.addCommand({ 141 | command: "greet $name", 142 | help: "greet 'John' - Greets a specified person", 143 | handler: ({ args }) => { 144 | return `Hello ${args.name}`; 145 | } 146 | }); 147 | 148 | test("should ", async () => { 149 | expect(jarvis.commands[0].help).toEqual("greet 'John' - Greets a specified person"); 150 | }); 151 | }); 152 | 153 | describe('macros', () => { 154 | const jarvis = new Jarvis(); 155 | 156 | jarvis.addCommand({ 157 | command: 'run hello', 158 | handler: ({ args }) => { 159 | return `Hello`; 160 | } 161 | }); 162 | 163 | jarvis.addCommand({ 164 | command: 'run world', 165 | handler: ({ args }) => { 166 | return `world`; 167 | } 168 | }); 169 | 170 | jarvis.addCommand({ 171 | command: 'load $language', 172 | handler: ({ args }) => { 173 | return `Running, ${args.language}`; 174 | } 175 | }); 176 | 177 | jarvis.addCommand({ 178 | command: 'say $string', 179 | handler: ({ args }) => { 180 | return `${args.string}`; 181 | } 182 | }); 183 | 184 | test('initialize a macro', async () => { 185 | expect(await jarvis.send('how to programme')) 186 | .toEqual('You are now entering a macro. Type the statements, one line at a time. When done, type \'end\'.'); 187 | }); 188 | 189 | test('add macro with no variables', async () => { 190 | await jarvis.send('how to write'); 191 | await jarvis.send('run hello'); 192 | await jarvis.send('run world'); 193 | 194 | expect(await jarvis.send('end')) 195 | .toEqual('Macro "write" has been added.'); 196 | }); 197 | 198 | test('run a macro', async () => { 199 | expect(await jarvis.send('write')) 200 | .toEqual(['Hello', 'world']); 201 | }); 202 | 203 | test('macro with multiple variables', async () => { 204 | await jarvis.send('how to code $language $message'); 205 | await jarvis.send('load $language'); 206 | await jarvis.send('say $message') 207 | await jarvis.send('end'); 208 | 209 | expect(await jarvis.send('code JavaScript "Hello World"')) 210 | .toEqual(['Running, JavaScript', 'Hello World']); 211 | }); 212 | 213 | test('not a valid command or macro', async () => { 214 | await jarvis.send('how to existing macro'); 215 | 216 | expect(await jarvis.send('invalid command')) 217 | .toEqual('Not a valid Command/Macro.'); 218 | }); 219 | 220 | test('providing a duplicate name', async () => { 221 | await jarvis.send('how to test $language'); 222 | await jarvis.send('run hello'); 223 | await jarvis.send('end'); 224 | 225 | expect(await jarvis.send('how to test $language')) 226 | .toEqual(`Macro name already exists!`); 227 | }); 228 | 229 | test('macro with inner macro', async () => { 230 | await jarvis.send('how to inner_macro $str1 $str2'); 231 | await jarvis.send('say $str1'); 232 | await jarvis.send('say $str2'); 233 | await jarvis.send('end'); 234 | 235 | await jarvis.send('how to outer_macro $string1 $string2 $string3'); 236 | await jarvis.send('say $string1'); 237 | await jarvis.send('inner_macro $string2 $string3'); 238 | await jarvis.send('end'); 239 | 240 | expect(await jarvis.send('outer_macro "Normal Command" "Inner Command 1" "Inner Command 2"')) 241 | .toEqual(['Normal Command', ['Inner Command 1', 'Inner Command 2']]); 242 | }); 243 | }); 244 | 245 | describe("constants", () => { 246 | const jarvis = new Jarvis(); 247 | 248 | jarvis.addCommand({ 249 | command: 'say $string', 250 | handler: ({ args }) => { 251 | return `${args.string}`; 252 | } 253 | }); 254 | 255 | test("define constant", async () => { 256 | expect(await jarvis.send('in this context')).toEqual('You are now entering constants. Type the constants, one line at a time. When done, type \'end\'.'); 257 | await jarvis.send('NAME is "JARVIS"'); 258 | await jarvis.send('VERSION is "1"'); 259 | await jarvis.send('JOB_ID is "255"'); 260 | await jarvis.send('_IS_LOADED is "TRUE"') 261 | expect(await jarvis.send('Author is "John"')).toEqual('A constant name should be in block letters.'); 262 | expect(await jarvis.send('end')).toEqual('Constants "NAME,VERSION,JOB_ID,_IS_LOADED" have been added.'); 263 | }); 264 | 265 | test("constant usage in command", async () => { 266 | expect(await jarvis.send('say $NAME')).toEqual('JARVIS'); 267 | }); 268 | 269 | test("more than one constant in one argument", async () => { 270 | expect(await jarvis.send('say "$NAME is an interpreter"')).toEqual('JARVIS is an interpreter'); 271 | expect(await jarvis.send('say "Name: $NAME, Version: $VERSION"')).toEqual('Name: JARVIS, Version: 1'); 272 | }); 273 | 274 | test("more than one constant in one argument: some constants are not defined", async () => { 275 | expect(await jarvis.send('say "Name: $NAME, Mode: $MODE"')).toEqual('Name: JARVIS, Mode: $MODE'); 276 | }); 277 | 278 | test("required constant is not defined", async () => { 279 | expect(await jarvis.send('say $TYPE')).toEqual('$TYPE') 280 | }); 281 | 282 | test("redefine constant", async () => { 283 | await jarvis.send('in this context'); 284 | expect(await jarvis.send('NAME is "JARVIS"')).toEqual(`'NAME' constant already exists!`); 285 | await jarvis.send('end'); 286 | }); 287 | 288 | test("invalid constant", async () => { 289 | await jarvis.send('in this context'); 290 | expect(await jarvis.send('NAME s JARVIS')).toEqual(null); 291 | await jarvis.send('end'); 292 | }); 293 | 294 | test("constant usage in macro", async () => { 295 | await jarvis.send('how to describe $string'); 296 | await jarvis.send('say $string'); 297 | await jarvis.send('say $VERSION'); 298 | await jarvis.send('say $JOB_ID'); 299 | await jarvis.send('say $_IS_LOADED'); 300 | await jarvis.send('end'); 301 | 302 | expect(await jarvis.send('describe $NAME')).toEqual(['JARVIS', '1', '255', 'TRUE']); 303 | }); 304 | }); 305 | 306 | describe("scripts", () => { 307 | const jarvis = new Jarvis(); 308 | 309 | jarvis.addCommand({ 310 | command: "start jarvis", 311 | handler: ({ args }) => { 312 | return `Started jarvis`; 313 | } 314 | }); 315 | 316 | jarvis.addCommand({ 317 | command: "run hello", 318 | handler: ({ args }) => { 319 | return `Hello`; 320 | } 321 | }); 322 | 323 | jarvis.addCommand({ 324 | command: "run world", 325 | handler: ({ args }) => { 326 | return `world`; 327 | } 328 | }); 329 | 330 | jarvis.addCommand({ 331 | command: "load $language", 332 | handler: ({ args }) => { 333 | return `Running, ${args.language}`; 334 | } 335 | }); 336 | 337 | jarvis.addCommand({ 338 | command: "say $string", 339 | handler: ({ args }) => { 340 | return `${args.string}`; 341 | } 342 | }); 343 | 344 | test("Run in script mode", async () => { 345 | expect(await jarvis.addScriptMode("jarvis", `${__dirname}/resources/test.jarvis`)).toEqual(["Hello", "world", "Running, JavaScript", "Bye"]); 346 | }); 347 | 348 | test("Run script with a single executor", async () => { 349 | const scriptResponse = await jarvis.addScriptMode("jarvis", `${__dirname}/resources/executor.jarvis`); 350 | expect(scriptResponse[scriptResponse.length - 3]).toEqual(["Hello", "world"]); 351 | expect(scriptResponse[scriptResponse.length - 2]).toEqual(["Started jarvis"]); 352 | expect(scriptResponse[scriptResponse.length - 1]).toEqual(["Running, JavaScript", "Bye"]); 353 | }); 354 | 355 | test("Run script with multiple executors", async () => { 356 | const scriptResponse = await jarvis.addScriptMode("jarvis", `${__dirname}/resources/executors.jarvis`); 357 | expect(scriptResponse[scriptResponse.length - 2]).toEqual(['Hello', 'world']); 358 | expect(scriptResponse[scriptResponse.length - 1]).toEqual(['Running, JavaScript', 'Bye']); 359 | }); 360 | 361 | test("Script file not specified", async () => { 362 | expect(await jarvis.addScriptMode("jarvis", null)).toEqual(null); 363 | }); 364 | 365 | test("Invalid script extension", async () => { 366 | expect(await jarvis.addScriptMode("jarvis", `${__dirname}/resources/test.invalid`)).toEqual(null); 367 | }); 368 | 369 | test("Invalid script path", async () => { 370 | try { 371 | await jarvis.addScriptMode("jarvis", `${__dirname}/invalidPath/test.jarvis`) 372 | } catch (error) { 373 | expect(error.message).toEqual('Could not read file from the specified location!'); 374 | } 375 | }); 376 | }); 377 | 378 | describe("import in script mode", () => { 379 | const jarvis = new Jarvis(); 380 | 381 | jarvis.addCommand({ 382 | command: "run hello", 383 | handler: ({ args }) => { 384 | return `Hello`; 385 | } 386 | }); 387 | 388 | jarvis.addCommand({ 389 | command: "end $bot", 390 | handler: ({ args }) => { 391 | return `Ending, ${args.bot}`; 392 | } 393 | }); 394 | 395 | jarvis.addCommand({ 396 | command: "load $language", 397 | handler: ({ args }) => { 398 | return `Running, ${args.language}`; 399 | } 400 | }); 401 | 402 | jarvis.addCommand({ 403 | command: "say $string", 404 | handler: ({ args }) => { 405 | return `${args.string}`; 406 | } 407 | }); 408 | 409 | test("Import in script mode", async () => { 410 | const scriptResponse = await jarvis.addScriptMode("jarvis", `./test/resources/source-script.jarvis`) 411 | expect(scriptResponse[scriptResponse.length - 1]).toEqual(['Good Morning', ['Running, JARVIS'], ['Hello', 'Running, BOT', ['Running, layer 3']], 'Ending, BOT']); 412 | }); 413 | 414 | test("Import macros with arguments in script mode", async () => { 415 | const scriptResponse = await jarvis.addScriptMode("jarvis", `./test/resources/import-with-arguments.jarvis`) 416 | expect(scriptResponse[scriptResponse.length - 1]).toEqual(['Running, JARVIS']); 417 | }); 418 | }); 419 | 420 | describe("Import from environment file", () => { 421 | const jarvis = new Jarvis(); 422 | 423 | jarvis.addCommand({ 424 | command: "run hello", 425 | handler: () => { 426 | return `Hello`; 427 | } 428 | }); 429 | 430 | jarvis.addCommand({ 431 | command: "load $language", 432 | handler: ({ args }) => { 433 | return `Running, ${args.language}`; 434 | } 435 | }); 436 | 437 | jarvis.addCommand({ 438 | command: "say $string", 439 | handler: ({ args }) => { 440 | return `${args.string}`; 441 | } 442 | }); 443 | 444 | test("Import constants from env", async () => { 445 | const scriptResponse = await jarvis.addScriptMode("jarvis", `./test/resources/import-env.jarvis`, `./test/resources/.jarvisrc`); 446 | expect(scriptResponse[scriptResponse.length - 1]).toEqual(['Hello', 'Running, JARVIS']); 447 | }); 448 | 449 | test("Import env with invalid syntax", async () => { 450 | const scriptResponse = await jarvis.addScriptMode("invalid", `./test/resources/import-env.invalid`, `./test/resources/.invalidrc`); 451 | expect(scriptResponse).toEqual('Invalid syntax in env file!'); 452 | }); 453 | 454 | test("Import env with invalid file path", async () => { 455 | const scriptResponse = await jarvis.addScriptMode("jarvis", `./test/resources/import-env.jarvis`, `./test/invalidDir/.jarvisrc`) 456 | expect(scriptResponse).toEqual('Could not read env file from specified location!'); 457 | }); 458 | }); 459 | 460 | describe("Event emitter", () => { 461 | const jarvis = new Jarvis(); 462 | 463 | jarvis.addCommand({ 464 | command: "run hello", 465 | handler: ({ args }) => { 466 | return `Hello`; 467 | } 468 | }); 469 | 470 | jarvis.addCommand({ 471 | command: "run world", 472 | handler: ({ args }) => { 473 | return `world`; 474 | } 475 | }); 476 | 477 | jarvis.addCommand({ 478 | command: "load $language", 479 | handler: ({ args }) => { 480 | return `Running, ${args.language}`; 481 | } 482 | }); 483 | 484 | jarvis.addCommand({ 485 | command: "say $string", 486 | handler: ({ args }) => { 487 | return `${args.string}`; 488 | } 489 | }); 490 | 491 | test("Command events", async () => { 492 | const emitObjectArray = []; 493 | jarvis.on('command', (res) => { 494 | emitObjectArray.push(res); 495 | }); 496 | 497 | await jarvis.addScriptMode("jarvis", `./test/resources/test.jarvis`); 498 | expect(emitObjectArray).toEqual([ 499 | { "command": " run hello", "response": "Hello" }, 500 | { "command": " run world", "response": "world" }, 501 | { "command": " load JavaScript", "response": "Running, JavaScript" }, 502 | { "command": " say Bye", "response": "Bye" } 503 | ]) 504 | }); 505 | }); 506 | 507 | describe("JSON imports", () => { 508 | const jarvis = new Jarvis(); 509 | 510 | jarvis.addCommand({ 511 | command: "run $argument", 512 | handler: ({ args: { argument } }) => { 513 | return argument; 514 | } 515 | }); 516 | 517 | test("script with a JSON import", async () => { 518 | const scriptResponse = await jarvis.addScriptMode("jarvis", `${__dirname}/resources/json-import.jarvis`); 519 | expect(scriptResponse[scriptResponse.length - 1]) 520 | .toEqual(['Hello', { name: 'JARVIS Interpreter', version: 'version 1.0.0' }, 'JSON Object: {"name":"JARVIS Interpreter","version":"version 1.0.0"}']); 521 | }); 522 | 523 | test("script with a invalid JSON import", async () => { 524 | const scriptResponse = await jarvis.addScriptMode("jarvis", `${__dirname}/resources/invalid-json-import.jarvis`); 525 | expect(scriptResponse[2]).toEqual('Invalid JSON import!'); 526 | }); 527 | 528 | test("script with a invalid JSON file path ", async () => { 529 | const scriptResponse = await jarvis.addScriptMode("jarvis", `${__dirname}/resources/invalid-json-path-import.jarvis`); 530 | expect(scriptResponse[2]).toEqual('Could not read the JSON file from the specified location!'); 531 | }); 532 | }); 533 | -------------------------------------------------------------------------------- /test/resources/.invalidrc: -------------------------------------------------------------------------------- 1 | INVALID is "NOT_JARVIS" 2 | -------------------------------------------------------------------------------- /test/resources/.jarvisrc: -------------------------------------------------------------------------------- 1 | in this context 2 | PROGRAM is "JARVIS" 3 | end 4 | -------------------------------------------------------------------------------- /test/resources/executor.jarvis: -------------------------------------------------------------------------------- 1 | # define constant 2 | in this context 3 | GREETING is "Bye" 4 | end 5 | 6 | # define macro 7 | how to greet 8 | run hello 9 | run world 10 | end 11 | 12 | how to work 13 | start jarvis 14 | end 15 | 16 | how to depart 17 | load JavaScript 18 | say $GREETING 19 | end 20 | 21 | # execute macros 22 | start farewell 23 | greet 24 | work 25 | depart 26 | end 27 | -------------------------------------------------------------------------------- /test/resources/executors.jarvis: -------------------------------------------------------------------------------- 1 | # sample constant 2 | in this context 3 | GREETING is "Bye" 4 | end 5 | 6 | # sample macro 7 | how to greet 8 | run hello 9 | run world 10 | end 11 | 12 | how to depart 13 | load JavaScript 14 | say $GREETING 15 | end 16 | 17 | # first execution block 18 | start welcome 19 | greet 20 | end 21 | 22 | # second execution block 23 | start farewell 24 | depart 25 | end -------------------------------------------------------------------------------- /test/resources/import-env.invalid: -------------------------------------------------------------------------------- 1 | in this context 2 | INVALID is from env 3 | end 4 | 5 | how to play 6 | run hello 7 | load $INVALID 8 | end 9 | 10 | start test 11 | play 12 | end 13 | 14 | -------------------------------------------------------------------------------- /test/resources/import-env.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | PROGRAM is from env 3 | NOT_DEFINED is from env 4 | end 5 | 6 | how to work 7 | run hello 8 | load $PROGRAM 9 | end 10 | 11 | start test 12 | work 13 | end 14 | 15 | -------------------------------------------------------------------------------- /test/resources/import-script-1.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | PROGRAM is "JARVIS" 3 | end 4 | 5 | how to start jarvis 6 | load $PROGRAM 7 | end -------------------------------------------------------------------------------- /test/resources/import-script-2.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | USER is "BOT" 3 | try nested import is from "./import-script-3.jarvis" 4 | end 5 | 6 | how to start bot 7 | run hello 8 | load $USER 9 | try nested import 10 | end -------------------------------------------------------------------------------- /test/resources/import-script-3.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | LAYER is "layer 3" 3 | end 4 | 5 | how to try nested import 6 | load $LAYER 7 | end 8 | 9 | how to load user $name 10 | load $name 11 | end -------------------------------------------------------------------------------- /test/resources/import-with-arguments.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | load user $name is from "./import-script-3.jarvis" 3 | end 4 | 5 | start JARVIS 6 | load user JARVIS 7 | end -------------------------------------------------------------------------------- /test/resources/invalid-json-import.jarvis: -------------------------------------------------------------------------------- 1 | # define constant 2 | in this context 3 | GREETING_STRING is "Hello" 4 | JARVIS_JSON is from "./json/sample-invalid.json" 5 | end -------------------------------------------------------------------------------- /test/resources/invalid-json-path-import.jarvis: -------------------------------------------------------------------------------- 1 | # define constant 2 | in this context 3 | GREETING_STRING is "Hello" 4 | JARVIS_JSON is from "./json/invalid-path/sample-invalid.json" 5 | end -------------------------------------------------------------------------------- /test/resources/json-import.jarvis: -------------------------------------------------------------------------------- 1 | # define constant 2 | in this context 3 | GREETING_STRING is "Hello" 4 | JARVIS_JSON is from "./json/sample.json" 5 | end 6 | 7 | how to log $greeting 8 | run $GREETING_STRING 9 | run $greeting 10 | run "JSON Object: $JARVIS_JSON" 11 | end 12 | 13 | start json test 14 | log $JARVIS_JSON 15 | end -------------------------------------------------------------------------------- /test/resources/json/sample-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": JARVIS Interpreter, 3 | "version": "version 1.0.0" 4 | } -------------------------------------------------------------------------------- /test/resources/json/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JARVIS Interpreter", 3 | "version": "version 1.0.0" 4 | } -------------------------------------------------------------------------------- /test/resources/source-script.jarvis: -------------------------------------------------------------------------------- 1 | in this context 2 | start jarvis is from "./import-script-1.jarvis" 3 | start bot is from "./import-script-2.jarvis" 4 | USER is from "./import-script-2.jarvis" 5 | GREETING is "Good Morning" 6 | end 7 | 8 | how to describe bot 9 | say $GREETING 10 | start jarvis 11 | start bot 12 | end $USER 13 | end 14 | 15 | start the bot 16 | describe bot 17 | end -------------------------------------------------------------------------------- /test/resources/test.jarvis: -------------------------------------------------------------------------------- 1 | start 2 | run hello 3 | run world 4 | # single line comment 5 | load JavaScript 6 | say Bye 7 | end -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | parseCommand, 3 | tokenize, 4 | parseInputTokens, 5 | parseMacroInputTokens, 6 | parseMacroSubCommand, 7 | parseScript 8 | } = require("../src/utils"); 9 | 10 | describe('tokenize', () => { 11 | 12 | test('double quoted strings', () => { 13 | expect(tokenize('hello world "Hello World"')) 14 | .toEqual(['hello', 'world', 'Hello World']); 15 | }); 16 | 17 | test('double quoted strings with punctuation', () => { 18 | expect(tokenize('hello world "Hello, World"')) 19 | .toEqual(['hello', 'world', 'Hello, World']); 20 | }); 21 | 22 | // TODO 23 | test('double quoted strings with escaped double quotes', () => { 24 | //expect(tokenize('hello world "Hello \"World\""')) 25 | // .toEqual(['hello', 'world', 'Hello \"World\"']); 26 | }); 27 | 28 | }); 29 | 30 | describe('parseCommand', () => { 31 | 32 | test('basic command', () => { 33 | expect(parseCommand('hello $name')) 34 | .toEqual([ 35 | { value: 'hello', isArg: false }, { value: 'name', isArg: true } 36 | ]); 37 | }); 38 | 39 | test('basic command with infix', () => { 40 | expect(parseCommand('hello $name how are you')) 41 | .toEqual([ 42 | { value: 'hello', isArg: false }, 43 | { value: 'name', isArg: true }, 44 | { value: 'how', isArg: false }, 45 | { value: 'are', isArg: false }, 46 | { value: 'you', isArg: false } 47 | ]); 48 | }); 49 | }); 50 | 51 | describe('macros', () => { 52 | test('macro input tokens validation with no variables', () => { 53 | expect(parseMacroInputTokens( 54 | { 55 | tokens: [ 56 | { value: 'how', isArg: false }, 57 | { value: 'to', isArg: false }, 58 | { value: 'programme', isArg: false } 59 | ] 60 | }, 61 | ['how', 'to', 'programme'] 62 | )) 63 | .toEqual({ args: {} }); 64 | }); 65 | 66 | test('macro input tokens mismatch', () => { 67 | expect(parseMacroInputTokens( 68 | { 69 | tokens: [ 70 | { value: 'how', isArg: false }, 71 | { value: 'to', isArg: false }, 72 | { value: 'programme', isArg: false } 73 | ] 74 | }, 75 | ['how', 'to', 'login'] 76 | )) 77 | .toEqual(null); 78 | }); 79 | 80 | test('macro input tokens validation with variables', () => { 81 | expect(parseMacroInputTokens( 82 | { 83 | tokens: [ 84 | { value: 'how', isArg: false }, 85 | { value: 'to', isArg: false }, 86 | { value: 'programme', isArg: false }, 87 | { value: 'language', isArg: true } 88 | ] 89 | }, 90 | ['how', 'to', 'programme', 'JavaScript'] 91 | )) 92 | .toEqual({ args: { language: 'JavaScript' } }); 93 | }); 94 | 95 | test('macro sub command with no variables', () => { 96 | expect(parseMacroSubCommand('run hello', {})) 97 | .toEqual('run hello'); 98 | }); 99 | 100 | test('macro sub command with variables', () => { 101 | expect(parseMacroSubCommand('run $code', { code: "Hello World" })) 102 | .toEqual("run \"Hello World\""); 103 | }); 104 | 105 | test('macro sub command with missing variable in args', () => { 106 | expect(parseMacroSubCommand('run $code', {})) 107 | .toEqual("run $code"); 108 | }); 109 | }); 110 | 111 | describe("scripts", () => { 112 | test("single line comments", () => { 113 | expect(parseScript(`${__dirname}/resources/test.jarvis`)) 114 | .toEqual(["start", " run hello", " run world", " load JavaScript", " say Bye", "end"]); 115 | }); 116 | }); --------------------------------------------------------------------------------