aboutsummaryrefslogtreecommitdiff
path: root/src/server/repl.ts
diff options
context:
space:
mode:
authorvellichora <fangrui_tong@brown.edu>2020-01-07 10:49:04 -0500
committervellichora <fangrui_tong@brown.edu>2020-01-07 10:49:04 -0500
commit3633971bd9c1f739c0d6facd74754b99a7f26db6 (patch)
tree62aab716a9a027b3b63673e3134fa651135635e5 /src/server/repl.ts
parent9614ba541c30dc7d2b5183d5450864354d911643 (diff)
parentccd39c9a53ebf9aea84fcdcba6050145add4526f (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into textbox_fawn_fix
Diffstat (limited to 'src/server/repl.ts')
-rw-r--r--src/server/repl.ts115
1 files changed, 115 insertions, 0 deletions
diff --git a/src/server/repl.ts b/src/server/repl.ts
new file mode 100644
index 000000000..bd00e48cd
--- /dev/null
+++ b/src/server/repl.ts
@@ -0,0 +1,115 @@
+import { createInterface, Interface } from "readline";
+import { red, green, white } from "colors";
+
+export interface Configuration {
+ identifier: () => string | string;
+ onInvalid?: (culprit?: string) => string | string;
+ onValid?: (success?: string) => string | string;
+ isCaseSensitive?: boolean;
+}
+
+export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
+export interface Registration {
+ argPatterns: RegExp[];
+ action: ReplAction;
+}
+
+export default class Repl {
+ private identifier: () => string | string;
+ private onInvalid: (culprit?: string) => string | string;
+ private onValid: (success: string) => string | string;
+ private isCaseSensitive: boolean;
+ private commandMap = new Map<string, Registration[]>();
+ public interface: Interface;
+ private busy = false;
+ private keys: string | undefined;
+
+ constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
+ this.identifier = prompt;
+ this.onInvalid = onInvalid || this.usage;
+ this.onValid = onValid || this.success;
+ this.isCaseSensitive = isCaseSensitive ?? true;
+ this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
+ }
+
+ private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
+
+ private usage = () => {
+ const resolved = this.keys;
+ if (resolved) {
+ return resolved;
+ }
+ const members: string[] = [];
+ const keys = this.commandMap.keys();
+ let next: IteratorResult<string>;
+ while (!(next = keys.next()).done) {
+ members.push(next.value);
+ }
+ return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
+ }
+
+ private success = (command: string) => `${this.resolvedIdentifier()} completed execution of ${white(command)}`;
+
+ public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
+ const existing = this.commandMap.get(basename);
+ const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
+ const registration = { argPatterns: converted, action };
+ if (existing) {
+ existing.push(registration);
+ } else {
+ this.commandMap.set(basename, [registration]);
+ }
+ }
+
+ private invalid = (culprit?: string) => {
+ console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(culprit)));
+ this.busy = false;
+ }
+
+ private valid = (command: string) => {
+ console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
+ this.busy = false;
+ }
+
+ private considerInput = async (line: string) => {
+ if (this.busy) {
+ console.log(red("Busy"));
+ return;
+ }
+ this.busy = true;
+ line = line.trim();
+ if (this.isCaseSensitive) {
+ line = line.toLowerCase();
+ }
+ const [command, ...args] = line.split(/\s+/g);
+ if (!command) {
+ return this.invalid();
+ }
+ const registered = this.commandMap.get(command);
+ if (registered) {
+ const { length } = args;
+ const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
+ for (const { argPatterns, action } of candidates) {
+ const parsed: string[] = [];
+ let matched = false;
+ if (length) {
+ for (let i = 0; i < length; i++) {
+ let matches: RegExpExecArray | null;
+ if ((matches = argPatterns[i].exec(args[i])) === null) {
+ break;
+ }
+ parsed.push(matches[0]);
+ }
+ matched = true;
+ }
+ if (!length || matched) {
+ await action(parsed);
+ this.valid(`${command} ${parsed.join(" ")}`);
+ return;
+ }
+ }
+ }
+ this.invalid(command);
+ }
+
+} \ No newline at end of file