Compare commits

...

2 Commits

Author SHA1 Message Date
c669d60428
default help option to commands and subcommands
All checks were successful
continuous-integration/drone/push Build is passing
bugfix for boolean type parse
2023-05-05 17:32:29 +03:00
eb4faa489b
v1.4.4 2023-05-05 17:31:22 +03:00
11 changed files with 121 additions and 25 deletions

12
babel.config.cjs Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
]
]
};

View File

@ -1,3 +0,0 @@
module.exports = {
presets: [[ '@babel/preset-env', { targets: { node: 'current' } }]]
};

View File

@ -1,6 +1,6 @@
{ {
"name": "@navy.gif/commandparser", "name": "@navy.gif/commandparser",
"version": "1.4.3", "version": "1.4.4",
"description": "Parser meant to parse commands and their options for discord bots", "description": "Parser meant to parse commands and their options for discord bots",
"author": "Navy.gif", "author": "Navy.gif",
"license": "MIT", "license": "MIT",
@ -34,8 +34,8 @@
}, },
"scripts": { "scripts": {
"build": "tsc && tsc -p tsconfig.cjs.json && node ./scripts/declareTypes.js", "build": "tsc && tsc -p tsconfig.cjs.json && node ./scripts/declareTypes.js",
"test": "tsc && jest", "test": "yarn build && jest",
"release": "tsc && yarn publish", "release": "yarn build && yarn publish",
"lint": "eslint --fix" "lint": "eslint --fix"
} }
} }

View File

@ -63,7 +63,6 @@ class Parser extends EventEmitter {
return command || null; return command || null;
} }
/** /**
* *
* *
@ -161,9 +160,12 @@ class Parser extends EventEmitter {
continue; continue;
} }
const short = match.groups.flag.startsWith('-') && !match.groups.flag.startsWith('--');
const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase(); const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase();
let aliased = false; let aliased = false;
const flag = flags.find((f) => { const flag = flags.find((f) => {
if (short)
return f.name[0] === _flag;
aliased = f.valueAsAlias && f.choices.some((c) => c === _flag); aliased = f.valueAsAlias && f.choices.some((c) => c === _flag);
return f.name === _flag || aliased; return f.name === _flag || aliased;
}); });
@ -192,7 +194,7 @@ class Parser extends EventEmitter {
if (flag.choices.length) { if (flag.choices.length) {
throw new ParseError(`Invalid choice for ${flag.name}, Valid choices are ${flag.choices.join(', ')}.`); throw new ParseError(`Invalid choice for ${flag.name}, Valid choices are ${flag.choices.join(', ')}.`);
} }
throw new ParseError(`Failed to parse value for ${flag.name}, expected value type: ${flag.type}`); // return { option: flag, ...result.removed }; throw new ParseError(`Failed to parse value for ${flag.name}, expected value type: ${OptionType[flag.type]}`); // return { option: flag, ...result.removed };
} }
this.debug(`Cleaning up params after ${flag.name}`); this.debug(`Cleaning up params after ${flag.name}`);
for (const r of result.removed) for (const r of result.removed)

View File

@ -22,15 +22,26 @@ abstract class Command implements ICommand {
this.aliases = def.aliases || []; this.aliases = def.aliases || [];
this.options = []; // Add a built-in help flag to any non-subcommand option
this.options = [
new CommandOption({
name: 'help',
type: OptionType.BOOLEAN,
flag: true,
valueOptional: true,
defaultValue: true
})
];
for (const opt of def.options || []) { for (const opt of def.options || []) {
if (opt instanceof CommandOption) if (opt instanceof CommandOption)
this.options.push(opt); this.options.push(opt);
else else
this.options.push(new CommandOption(opt)); this.options.push(new CommandOption(opt));
} }
if (this.options.some(opt => [ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type)) if (this.options.some(opt => [ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type))
&& this.options.some(opt => ![ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type))) && this.options.some(opt => ![ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type) && opt.name !== 'help'))
throw new Error('Cannot have subcommand(group)s on the same level as an option'); throw new Error('Cannot have subcommand(group)s on the same level as an option');
} }

View File

@ -2,6 +2,7 @@
import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption.js"; import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption.js";
import IResolver from "../interfaces/Resolver.js"; import IResolver from "../interfaces/Resolver.js";
const SUB = [ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ];
class CommandOption implements ICommandOption { class CommandOption implements ICommandOption {
[key: string]: unknown; [key: string]: unknown;
@ -36,7 +37,18 @@ class CommandOption implements ICommandOption {
this.flag = def.flag || false; this.flag = def.flag || false;
this.options = []; this.options = [];
if (SUB.includes(this.type))
this.options.push(new CommandOption({
name: 'help',
type: OptionType.BOOLEAN,
flag: true,
valueOptional: true,
defaultValue: true
}));
for (const opt of def.options || []) { for (const opt of def.options || []) {
if (this.options.some(o => o.name === opt.name))
throw new Error(`An option by the name ${opt.name} already exists`);
if (opt instanceof CommandOption) if (opt instanceof CommandOption)
this.options.push(opt); this.options.push(opt);
else else
@ -49,12 +61,17 @@ class CommandOption implements ICommandOption {
throw new Error('Cannot have subcommand groups under a subcommand'); throw new Error('Cannot have subcommand groups under a subcommand');
if (this.type === OptionType.SUB_COMMAND && this.options.some(opt => opt.type === OptionType.SUB_COMMAND)) if (this.type === OptionType.SUB_COMMAND && this.options.some(opt => opt.type === OptionType.SUB_COMMAND))
throw new Error('Cannot nest subcommands'); throw new Error('Cannot nest subcommands');
if (![ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(this.type) if (!SUB.includes(this.type)
&& this.options.some(opt => [ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type))) && this.options.some(opt => SUB.includes(opt.type))
&& this.name !== 'help') // Allow the help flag at any level
throw new Error('Cannot have subcommand(group)s under an option'); throw new Error('Cannot have subcommand(group)s under an option');
if (this.options.some(opt => [ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type)) // if sub commands and normal options exists together, throw an error
&& this.options.some(opt => ![ OptionType.SUB_COMMAND, OptionType.SUB_COMMAND_GROUP ].includes(opt.type))) // Checks for subcommands: if opt.type is a subcommand type
// console.log(this.options, this.options.some(opt => SUB.includes(opt.type)), this.options.some(opt => !SUB.includes(opt.type)), this.options.some(opt => !SUB.includes(opt.type) && opt.name !== 'help'));
if (this.options.some(opt => SUB.includes(opt.type))
// Check for non-subcommands: if opt.type is not a sub type
&& this.options.some(opt => !SUB.includes(opt.type) && opt.name !== 'help'))
throw new Error('Cannot have subcommand(group)s on the same level as an option'); throw new Error('Cannot have subcommand(group)s on the same level as an option');
this.choices = def.choices || []; this.choices = def.choices || [];
@ -81,13 +98,12 @@ class CommandOption implements ICommandOption {
if (!this[OptionType[this.type]]) if (!this[OptionType[this.type]])
throw new Error(`Missing parsing function for ${this.type}`); throw new Error(`Missing parsing function for ${this.type}`);
if (typeof this[OptionType[this.type]] !== 'function') if (typeof this[OptionType[this.type]] !== 'function')
throw new Error(`Expected function type for memberr ${OptionType[this.type]}`); throw new Error(`Expected function type for member ${OptionType[this.type]}`);
this.guild = guild; this.guild = guild;
// eslint-disable-next-line @typescript-eslint/ban-types const func = this[OptionType[this.type]] as () => Promise<ParseResult>;
const func = this[OptionType[this.type]] as Function;
const result = await func.bind(this)(); const result = await func.bind(this)();
this.value = result.value; this.value = result.value;
return result as ParseResult; return result;
} }
get plural (): boolean { get plural (): boolean {
@ -149,14 +165,20 @@ class CommandOption implements ICommandOption {
} }
protected BOOLEAN () { protected BOOLEAN () {
if (!this.rawValue) if (!this.rawValue && !this.valueOptional)
return { error: true }; return { error: true };
const boolean = this.resolver?.resolveBoolean(this.rawValue[0]) || null;
const boolean = this.rawValue ? this.resolver?.resolveBoolean(this.rawValue[0]) : this.defaultValue;
if (boolean === null && this.valueOptional) if (boolean === null && this.valueOptional)
return { value: this.defaultValue, removed: [] }; return { value: this.defaultValue, removed: [] };
else if (boolean === null) else if (boolean === null)
return { error: true }; return { error: true };
return { value: boolean, removed: [ this.rawValue[0] ] };
const removed = [];
if (this.rawValue)
removed.push(this.rawValue[0]);
return { value: boolean, removed };
} }
} }

View File

@ -65,7 +65,8 @@ type DependsOnMode = 'AND' | 'OR';
type ParseResult = { type ParseResult = {
error: boolean, error: boolean,
removed: string[] removed: string[],
value: never
} }
type CommandOptionDefinition = { type CommandOptionDefinition = {

View File

@ -1,4 +1,5 @@
import { Parser, Command, CommandOption, OptionType } from '../build/esm' // import { Parser, Command, CommandOption, OptionType } from '../build/esm'
const {Parser, Command, CommandOption, OptionType} = require('../build/cjs')
const opt = new CommandOption({ const opt = new CommandOption({
name: 'text', name: 'text',

View File

@ -1,4 +1,5 @@
import { Parser, Command, OptionType } from '../build/esm/index'; // import { Parser, Command, OptionType } from '../build/esm/index';
const { Parser, Command, OptionType } = require('../build/cjs');
test('Command definition', () => { test('Command definition', () => {
expect(() => new Command()).toThrow(); expect(() => new Command()).toThrow();
@ -42,6 +43,34 @@ test('Command definition', () => {
}] })).not.toThrow(); }] })).not.toThrow();
}) })
test('Command Parser', async () => {
const command = new Command({
name: 'create',
options: [{
name: 'registration-code',
aliases: ['code'],
type: OptionType.SUB_COMMAND,
options: [{
name: 'amount',
flag: true,
type: OptionType.INTEGER,
defaultValue: 1,
valueOptional: true
}]
}]
});
const parser = new Parser({ commands: [command], prefix: '', debug: true });
await expect(parser.parseMessage('create')).rejects.toThrow();
// Cannot have an option at the sub-command level
await expect(parser.parseMessage('create test')).rejects.toThrow();
await expect(parser.parseMessage('create code')).resolves.toHaveProperty('command.name', 'create');
await expect(parser.parseMessage('create code -a 1')).resolves.toHaveProperty('args.amount.value', 1)
});
test('Command Parser', async () => { test('Command Parser', async () => {
const command = new Command({ const command = new Command({
name: 'create', name: 'create',

21
tests/playground.js Normal file
View File

@ -0,0 +1,21 @@
import { Parser, Command, OptionType } from '../build/esm/index.js';
const command = new Command({
name: 'create',
options: [{
name: 'registration-code',
aliases: ['code'],
type: OptionType.SUB_COMMAND,
options: [{
name: 'amount',
flag: true,
type: OptionType.INTEGER,
defaultValue: 1,
valueOptional: true
}]
}]
});
const parser = new Parser({ commands: [command], prefix: '', debug: true });
parser.on('debug', console.log)
console.log(await parser.parseMessage('create code -a 1'));
console.log(await parser.parseMessage('create code --help'))