help for commands.

This commit is NOT contributed under the Apache-2.0 License.
Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
All rights reserved.
This commit is contained in:
gnuxie
2022-12-30 20:52:40 +00:00
parent 76c48cccfc
commit b9767532a8
4 changed files with 102 additions and 10 deletions
@@ -54,6 +54,14 @@ export class CommandTable<ExecutorType extends BaseFunction> {
}
/**
* Used to render the help command.
* @returns All of the commands in this table.
*/
public getCommands(): InterfaceCommand<BaseFunction>[] {
return [...this.flattenedCommands.values()];
}
// We use the argument stream so that they can use stream.rest() to get the unconsumed arguments.
public findAMatchingCommand(stream: ArgumentStream) {
const tableHelper = (table: CommandLookupEntry<ExecutorType>, argumentStream: ArgumentStream): undefined|InterfaceCommand<ExecutorType> => {
@@ -112,9 +120,13 @@ export function findCommandTable<ExecutorType extends BaseFunction>(name: string
export class InterfaceCommand<ExecutorType extends BaseFunction> {
constructor(
private readonly argumentListParser: IArgumentListParser,
public readonly argumentListParser: IArgumentListParser,
private readonly command: ExecutorType,
public readonly designator: string[],
/** A short one line summary of what the command does to display alongside it's help */
public readonly summary: string,
/** A longer description that goes into detail. */
public readonly description?: string,
) {
}
@@ -144,11 +156,15 @@ export function defineInterfaceCommand<ExecutorType extends BaseFunction>(descri
table: string,
command: ExecutorType,
designator: string[],
summary: string,
description?: string,
}) {
const command = new InterfaceCommand<ExecutorType>(
description.paramaters,
description.command,
description.designator,
description.summary,
description.description,
);
const table = findCommandTable(description.table);
table.internCommand(command);
@@ -0,0 +1,61 @@
/**
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
*/
import { MatrixSendClient } from "../../MatrixEmitter";
import { BaseFunction, InterfaceCommand } from "./InterfaceCommand";
import { KeywordParser } from "./ParamaterParsing";
import { CommandError, CommandResult } from "./Validation";
function requiredArgument(argumentName: string): string {
return `<${argumentName}>`;
}
function keywordArgument(keyword: string): string {
// ahh fuck what about defaults for keys?
return `[--${keyword}]`;
}
// they should be allowed to name the rest argument...
function restArgument(): string {
return `[...rest]`;
}
function renderCommandHelp(command: InterfaceCommand<BaseFunction>): string {
let text = '';
for (const designator of command.designator) {
text += `${designator} `
}
for (const description of command.argumentListParser.descriptions) {
text += `${requiredArgument(description.name)} `;
}
const restParser = command.argumentListParser.restParser;
if (restParser !== undefined) {
// not too happy with how keywords are represented here., like there's just the keys with no context smh.
if (restParser instanceof KeywordParser) {
for (const keyword of Object.keys(restParser.description)) {
if (keyword === "allowOtherKeys") {
continue;
}
// ahh fuck what about defaults for keys?
text += `${keywordArgument(keyword)} `;
}
if (restParser.description.allowOtherKeys) {
text += `${restArgument()} `;
}
} else {
text += `${restArgument()} `;
}
}
return text;
}
// What is really needed is a rendering protocol, that works with bullshit text+html that's really just string building like we're doing here or some other media format
export async function renderHelp(client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult<InterfaceCommand<BaseFunction>[], CommandError>): Promise<void> {
const commands = result.ok;
let text = ''
for (const command of commands) {
text += `${renderCommandHelp(command)}\n`;
}
await client.replyNotice(commandRoomId, event, text);
}
@@ -106,18 +106,15 @@ interface KeywordsDescription {
readonly allowOtherKeys: boolean
}
interface KeywordPropertyDescription {
interface KeywordPropertyDescription extends ParamaterDescription {
readonly isFlag: boolean;
readonly propertyPredicate?: PredicateIsParamater;
readonly name: string,
}
// Things that are also needed that are not done yet:
// 1) We need to figure out what happens to aliases for keywords..
// 2) We need to sort out the predicates thing.
export class KeywordParser extends RestParser {
constructor(private readonly description: KeywordsDescription) {
constructor(public readonly description: KeywordsDescription) {
super();
}
@@ -127,7 +124,6 @@ export class KeywordParser extends RestParser {
*/
public parseRest(itemStream: ArgumentStream): CommandResult<DestructableRest> {
const destructable: DestructableRest = { rest: [] };
// Wrong, we can't use position, we need a stream.
while (itemStream.peekItem() !== undefined) {
const item = itemStream.readItem();
if (item instanceof Keyword) {
@@ -141,7 +137,7 @@ export class KeywordParser extends RestParser {
return CommandResult.Ok(property);
} else {
if (!description.isFlag) {
return CommandError.Result(`An associated argument was not provided for the keyword ${description.name}.`)
return ArgumentParseError.Result(`An associated argument was not provided for the keyword ${description.name}.`, { paramater: description, stream: itemStream })
}
return CommandResult.Ok(true);
}
@@ -206,7 +202,7 @@ class ArgumentListParser implements IArgumentListParser {
for (const paramater of descriptions) {
if (itemStream.peekItem() === undefined) {
// FIXME asap: we need a proper paramater description?
return CommandError.Result(`An argument for the paramater ${paramater.name} was expected but was not provided.`);
return ArgumentParseError.Result(`An argument for the paramater ${paramater.name} was expected but was not provided.`, { paramater, stream: itemStream });
}
const item = itemStream.readItem()!;
const result = paramater.acceptor.validator(item);
@@ -228,6 +224,19 @@ class ArgumentListParser implements IArgumentListParser {
}
}
export class ArgumentParseError extends CommandError {
constructor(
public readonly paramater: ParamaterDescription,
public readonly stream: ArgumentStream,
message: string) {
super(message)
}
public static Result<Ok>(message: string, options: { paramater: ParamaterDescription, stream: ArgumentStream }): CommandResult<Ok, ArgumentParseError> {
return CommandResult.Err(new ArgumentParseError(options.paramater, options.stream, message));
}
}
/**
* I don't think we should use `union` and it should be replaced by a presentationTypeTranslator
* these are specific to applications e.g. imagine you want to resolve an alias or something.
+7 -1
View File
@@ -90,7 +90,13 @@ export class CommandError {
}
public static Result<Ok>(message: string): CommandResult<Ok> {
/**
* Utility to wrap the error into a Result.
* @param message The message for the CommandError.
* @param _options This exists so that the method is extensible by subclasses. Otherwise they wouldn't be able to pass other constructor arguments through this method.
* @returns
*/
public static Result<Ok>(message: string, _options = {}): CommandResult<Ok> {
return CommandResult.Err(new CommandError(message));
}
}