Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CommandInfoService #13

Open
Foxtrek64 opened this issue Dec 17, 2021 · 3 comments · May be fixed by #14
Open

Add CommandInfoService #13

Foxtrek64 opened this issue Dec 17, 2021 · 3 comments · May be fixed by #14

Comments

@Foxtrek64
Copy link

Foxtrek64 commented Dec 17, 2021

Getting command information to build a help command is a common activity in any environment where a dedicated command parser is required. This command service should be able to return information on all commands, a branch of the command tree, or a single command node depending on user selection. Additionally, the command service should not make any assumptions about how the consumer of the CommandHelpService will render their command information, as each application would be different. For instance, a consumer in a console command line would likely want to display results differently than a consumer in a Discord bot.

The description of the group, command, and argument would be pulled from System.ComponentModel.DescriptionAttribute. If no attribute is found, Description will be null.

Proposed API

// Contains info on a specific command.
public interface ICommandInfo
{
    // The name of the command
    string Name { get; }

    // The description of the command.
    string? Description { get; }

    // A collection of command aliases
    IReadOnlyList<string> Aliases { get; }

    // Whether the command was adorned with a hidden attribute.
    bool Hidden { get; }

    // A collection of conditions which must be satisfied for this
    // command to run.
    IReadOnlyList<IConditionInfo> Conditions { get; }

    // A collection of info about the command's arguments, if any.
    IReadOnlyList<ICommandArgumentInfo> Arguments { get; }
}
// Contains info about a specific command argument.
public interface ICommandArgumentInfo
{
    // The name of the argument.
    string Name { get; }

    // The description of the argument.
    string? Description { get; }

    // The position of the argument
    int Position { get; }

    // The type of the argument (of TValue)
    Type ArgumentType { get; }

    // Returns true if the argument is not required.
    // To satisfy this condition, an argument must have a default value.
    // Depending on performance, this may be used to merge command
    // overloads which only add an additional argument or arguments.
    bool IsOptional { get; }

    // Whether the argument has a default value
    bool HasDefaultValue { get; }

    // The provided default value, if present.
    // Throws an InvalidOperationException if HasDefaultValue is false.
    object? DefaultValue { get; }
}
// Contains info about a particular command group.
public interface IGroupInfo
{
    // The name of the group.
    string Name { get; }

    // The group's description.
    string? Description { get; }

    // Whether the group was adorned with a hidden attribute.
    bool Hidden { get; }

    // A list of commands contained in this group.
    IReadOnlyList<ICommandInfo> { get; }

    // A list of child groups contained in this group.
    IReadOnlyList<IGroupInfo> { get; }
}
Contains information about a condition.
public interface IConditionInfo
{
    // The name of the condition. Usually just `nameof(MyCondition)`
    string Name { get; }

    // The description of the condition.
    string? Description { get; }
}
/// <summary>
/// A service which handles retrieving informational classes for building command help.
/// </summary>
public interface ICommandHelpService
{
    /// <summary>
    /// Attempts to perform a search against the command tree using the provided command string.
    /// </summary>
    /// <param name="commandString">A search string, either terminating with a command group or the command node itself, but without any parameters.</param>
    /// <param name="tokenizerOptions">Tokenization options to determine how to parse the <paramref name="commandString"/>.</param>
    /// <param name="treeSearchOptions">Tree search options to determine how to perform the search.</param>
    /// <returns>One of <see cref="IGroupInfo"/>, <see cref="ICommandInfo"/>, or an immutable collection of <see cref="ICommandInfo"/>s where multiple overloads are present.</returns>
    /// <example>
    /// User executes: -help "options reload"
    /// Where "options reload" would normally be the command string passed in place of help.
    /// This would locate a "reload" command located in the "commands" module.
    /// </example>
    Result<OneOf<IGroupInfo, ICommandInfo, ICommandInfo[]>> FindInfo(string commandString, Tokenization.TokenizerOptions? tokenizerOptions = null, Trees.TreeSearchOptions? treeSearchOptions = null);

    /// <summary>
    /// Gets information about the specified group.
    /// </summary>
    /// <param name="commandGroupType">The type of the command group to gather information on.</param>
    /// <param name="buildChildGroups">If true, child nodes will be created and populated. If false, the <see cref="IGroupInfo.ChildGroups"/> collection will be empty.</param>
    /// <returns>A <see cref="GroupInfo"/> representing the retrieved group.</returns>
    Result<IGroupInfo> GetGroupInfo(Type commandGroupType, bool buildChildGroups = false);

    /// <inheritdoc cref="GetGroupInfo(string, bool)"/>
    /// <typeparam name="TCommandGroup">The type of the command group to gather information on.</typeparam>
    Result<IGroupInfo> GetGroupInfo<TCommandGroup>(bool buildChildGroups = false)
        where TCommandGroup : CommandGroup;

    /// <summary>
    /// Gets information about all registered command groups and their commands.
    /// </summary>
    /// <param name="commandService">A <see cref="CommandService"/>containing all currently registered commands.</param>
    /// <returns>A read-only list of <see cref="GroupInfo"/>s representing the retrieved groups.</returns>
    Result<IRootInfo> GetAllCommands();
}
// Marks a group or command as being hidden.
// Attribute usage: class, method
public sealed class HiddenFromHelpAttribute : Attribute
{
    public string? Comment { get; init; }

    public HiddenFromHelpAttribute(string? comment = null)
    {
        Comment = comment;
    }
}
@Foxtrek64 Foxtrek64 changed the title Add CommandHelpService Add CommandInfoService Dec 17, 2021
@Nihlus
Copy link
Member

Nihlus commented Dec 21, 2021

This looks good! I have some feedback on the proposed API, though.

  1. Having a generic parameter on ICommandArgumentInfo doesn't seem neccesary. Simply storing the Type would be fine.
  2. Is Position all that useful? The order of parameters could be inferred from their order in the corresponding list, though I could see the argument for keeping it.
  3. IConditionInfo; same thing with generic parameters. Also, storing an actual instance of the condition won't be realistic - it's instantiated via DI, and can take a dependency on various services (scoped or otherwise). This API should be DI-unaware, and preferably be statically inspectable (i.e, it shouldn't require a built service provider to function).
  4. ICommandInfoService; the string parameters should probably be treated as search patterns instead of full names - oftentimes, multiple commands with the same name might exist under different groups.
  5. Following on that, the GetAllCommands method can't just return IGroupInfo, since you can have top-level commands without an associated group.

@Foxtrek64
Copy link
Author

1. Having a generic parameter on ICommandArgumentInfo doesn't seem neccesary. Simply storing the `Type` would be fine.

I made this determination too once I got into laying down the actual interfaces. The service has to return the non-generic anyways, since List<Foo<>> isn't a thing yet, so it would go unused.

2. Is `Position` all that useful? The order of parameters could be inferred from their order in the corresponding list, though I could see the argument for keeping it.

No clue, but I put it in anyways until I find out otherwise.

3. `IConditionInfo`; same thing with generic parameters. Also, storing an actual instance of the condition won't be realistic - it's instantiated via DI, and can take a dependency on various services (scoped or otherwise). This API should be DI-unaware, and preferably be statically inspectable (i.e, it shouldn't require a built service provider to function).

Good point. Removed.

4. `ICommandInfoService`; the `string` parameters should probably be treated as search patterns instead of full names - oftentimes, multiple commands with the same name might exist under different groups.

I wasn't quite sure how to handle this to be honest. I sort of figured I'd just figure this out when I got to it. I'm open to ideas though.

5. Following on that, the `GetAllCommands` method can't just return `IGroupInfo`, since you can have top-level commands without an associated group.

I was thinking of cheating here and pretending the root node was a group in its own right, just with a static name and description, since the root node (like a group), holds a list of groups and commands. Let me know if you don't feel this is realistic.

@Foxtrek64 Foxtrek64 linked a pull request Dec 21, 2021 that will close this issue
17 tasks
@Nihlus
Copy link
Member

Nihlus commented Dec 21, 2021

I think the data model should be as close to the real thing as it can get, and the root node of the command tree is not a group node. What you could do is use OneOf for the return type in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants