diff --git a/.idea/.idea.Remora.Commands/.idea/vcs.xml b/.idea/.idea.Remora.Commands/.idea/vcs.xml
index 94a25f7..4c6280e 100644
--- a/.idea/.idea.Remora.Commands/.idea/vcs.xml
+++ b/.idea/.idea.Remora.Commands/.idea/vcs.xml
@@ -1,5 +1,11 @@
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 660c8fb..185226b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,5 +5,6 @@
+
\ No newline at end of file
diff --git a/Remora.Commands/Builders/AbstractCommandBuilder.cs b/Remora.Commands/Builders/AbstractCommandBuilder.cs
new file mode 100644
index 0000000..3357804
--- /dev/null
+++ b/Remora.Commands/Builders/AbstractCommandBuilder.cs
@@ -0,0 +1,175 @@
+//
+// AbstractCommandBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using Remora.Commands.Conditions;
+using Remora.Commands.DependencyInjection;
+
+namespace Remora.Commands.Builders;
+
+///
+/// An abstract builder class with shared functionality/data between commands and groups.
+///
+/// The type of the builder.
+public abstract class AbstractCommandBuilder where TSelf : AbstractCommandBuilder
+{
+ ///
+ /// Gets or sets the description of the group or command.
+ ///
+ public string? Description { get; protected set; }
+
+ ///
+ /// Gets the associated list of aliases.
+ ///
+ protected List Aliases { get; }
+
+ ///
+ /// Gets the associated list of attributes.
+ ///
+ protected List Attributes { get; }
+
+ ///
+ /// Gets the associated list of conditions.
+ ///
+ protected List Conditions { get; }
+
+ ///
+ /// Gets the associated parent node if any.
+ ///
+ protected GroupBuilder? Parent { get; }
+
+ ///
+ /// Gets the associated for the command or group.
+ ///
+ protected TreeRegistrationBuilder? TreeBuilder { get; }
+
+ ///
+ /// Gets or sets the name of the command or group.
+ ///
+ protected string Name { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The registration builder.
+ protected AbstractCommandBuilder(TreeRegistrationBuilder treeBuilder)
+ : this()
+ {
+ this.TreeBuilder = treeBuilder;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent of the builder.
+ protected AbstractCommandBuilder(GroupBuilder? parent = null)
+ {
+ this.Name = string.Empty;
+ this.Aliases = new();
+ this.Attributes = new();
+ this.Conditions = new();
+ this.Parent = parent;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private AbstractCommandBuilder()
+ {
+ this.Aliases = new();
+ this.Attributes = new();
+ this.Conditions = new();
+ this.Name = string.Empty;
+ }
+
+ ///
+ /// Sets the name of the builder.
+ ///
+ /// The name of the builder.
+ /// The current builder to chain calls with.
+ public TSelf WithName(string name)
+ {
+ this.Name = name;
+ return (TSelf)this;
+ }
+
+ ///
+ /// Sets the description of the builder.
+ ///
+ /// The description of the builder.
+ /// The current builder to chain calls with.
+ public TSelf WithDescription(string description)
+ {
+ this.Description = description;
+ return (TSelf)this;
+ }
+
+ ///
+ /// Adds an alias to the builder.
+ ///
+ /// The alias to add.
+ /// The current builder to chain calls with.
+ public TSelf AddAlias(string alias)
+ {
+ this.Aliases.Add(alias);
+ return (TSelf)this;
+ }
+
+ ///
+ /// Adds multiple aliases to the builder.
+ ///
+ /// The aliases to add.
+ /// The current builder to chain calls with.
+ public TSelf AddAliases(IEnumerable aliases)
+ {
+ this.Aliases.AddRange(aliases);
+ return (TSelf)this;
+ }
+
+ ///
+ /// Adds an attribute to the builder. Conditions must be added via .
+ ///
+ /// The attribute to add.
+ /// The current builder to chain calls with.
+ public TSelf AddAttribute(Attribute attribute)
+ {
+ if (attribute is ConditionAttribute)
+ {
+ throw new InvalidOperationException("Conditions must be added via AddCondition.");
+ }
+
+ this.Attributes.Add(attribute);
+ return (TSelf)this;
+ }
+
+ ///
+ /// Adds a condition to the builder.
+ ///
+ /// The condition to add.
+ /// The current builder to chain calls with.
+ public TSelf AddCondition(ConditionAttribute condition)
+ {
+ this.Conditions.Add(condition);
+ return (TSelf)this;
+ }
+}
diff --git a/Remora.Commands/Builders/CommandBuilder.cs b/Remora.Commands/Builders/CommandBuilder.cs
new file mode 100644
index 0000000..54c70a1
--- /dev/null
+++ b/Remora.Commands/Builders/CommandBuilder.cs
@@ -0,0 +1,243 @@
+//
+// CommandBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using OneOf;
+using Remora.Commands.Attributes;
+using Remora.Commands.DependencyInjection;
+using Remora.Commands.Extensions;
+using Remora.Commands.Signatures;
+using Remora.Commands.Trees;
+using Remora.Commands.Trees.Nodes;
+
+namespace Remora.Commands.Builders;
+
+///
+/// A builder for commands, which exposes a fluent API.
+///
+public class CommandBuilder : AbstractCommandBuilder
+{
+ private CommandInvocation? _invocation;
+
+ ///
+ /// Gets the parameters of the command.
+ ///
+ internal List Parameters { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent of the command.
+ public CommandBuilder(GroupBuilder? parent = null)
+ : base(parent)
+ {
+ this.Parameters = new List();
+ this.Parent?.Children.Add(this);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The registration builder.
+ public CommandBuilder(TreeRegistrationBuilder treeBuilder)
+ : base(treeBuilder)
+ {
+ this.Parameters = new List();
+ }
+
+ ///
+ /// Sets the delegate that represents the command.
+ /// This delegate may do additional work prior to the actual invocation of the command (such as resolving dependencies).
+ ///
+ /// The function to invoke the command, or the command itself.
+ /// This method MUST be called before .
+ /// The current builder to chain calls with.
+ public CommandBuilder WithInvocation(CommandInvocation invokeFunc)
+ {
+ _invocation = invokeFunc;
+ return this;
+ }
+
+ ///
+ /// Adds a new parameter to the command.
+ ///
+ /// The optional type of the parameter.
+ /// The parameter builder to build the parameter with.
+ public CommandParameterBuilder AddParameter(Type type)
+ {
+ var parameterBuilder = new CommandParameterBuilder(this, type);
+ this.Parameters.Add(parameterBuilder);
+ return parameterBuilder;
+ }
+
+ ///
+ /// Creates a from a method.
+ ///
+ /// The parent builder, if applicable.
+ /// The method to extract from.
+ /// The builder.
+ public static CommandBuilder FromMethod(GroupBuilder? parent, MethodInfo info)
+ {
+ var builder = new CommandBuilder(parent);
+
+ var commandAttribute = info.GetCustomAttribute()!;
+
+ builder.WithName(commandAttribute.Name);
+ builder.AddAliases(commandAttribute.Aliases);
+
+ var descriptionAttribute = info.GetCustomAttribute();
+
+ if (descriptionAttribute is not null)
+ {
+ builder.WithDescription(descriptionAttribute.Description);
+ }
+
+ var parameters = info.GetParameters();
+ builder.WithInvocation(CommandTreeBuilder.CreateDelegate(info));
+
+ foreach (var parameter in parameters)
+ {
+ var parameterBuilder = builder.AddParameter(parameter.ParameterType);
+ parameterBuilder.WithName(parameter.Name!);
+
+ var description = parameter.GetCustomAttribute();
+ if (description is not null)
+ {
+ parameterBuilder.WithDescription(description.Description);
+ }
+
+ if (parameter.HasDefaultValue)
+ {
+ parameterBuilder.WithDefaultValue(parameter.DefaultValue);
+ }
+
+ parameter.GetAttributesAndConditions(out var attributes, out var conditions);
+
+ foreach (var attribute in attributes)
+ {
+ parameterBuilder.AddAttribute(attribute);
+ }
+
+ foreach (var condition in conditions)
+ {
+ parameterBuilder.AddCondition(condition);
+ }
+
+ var switchOrOptionAttribute = parameter.GetCustomAttribute();
+
+ if (switchOrOptionAttribute is SwitchAttribute sa)
+ {
+ if (parameter.ParameterType != typeof(bool))
+ {
+ throw new InvalidOperationException("Switches must be of type bool.");
+ }
+
+ if (!parameter.HasDefaultValue)
+ {
+ throw new InvalidOperationException("Switches must have a default value.");
+ }
+
+ parameterBuilder.IsSwitch((bool)parameter.DefaultValue!, GetAttributeValue(sa.ShortName, sa.LongName));
+ }
+ else if (switchOrOptionAttribute is OptionAttribute oa)
+ {
+ parameterBuilder.IsOption(GetAttributeValue(oa.ShortName, oa.LongName));
+ }
+
+ var greedyAttribute = parameter.GetCustomAttribute();
+
+ if (greedyAttribute is not null)
+ {
+ parameterBuilder.IsGreedy();
+ }
+ }
+
+ // Alternatively check if the builder is null? Expected case is from Remora.Commands invoking this,
+ // in which case it's expected that a group builder is ALWAYS passed if the command is within a group.
+ if (!info.DeclaringType!.TryGetGroupName(out _))
+ {
+ info.DeclaringType!.GetAttributesAndConditions(out var attributes, out var conditions);
+
+ builder.Attributes.AddRange(attributes);
+ builder.Conditions.AddRange(conditions);
+ }
+
+ info.GetAttributesAndConditions(out var methodAttributes, out var methodConditions);
+
+ builder.Attributes.AddRange(methodAttributes);
+ builder.Conditions.AddRange(methodConditions);
+
+ return builder;
+
+ OneOf GetAttributeValue(char? shortName, string? longName)
+ {
+ return (shortName, longName) switch
+ {
+ (null, null) => throw new InvalidOperationException("Switches and options must have a name."),
+ (null, string ln) => ln,
+ (char sn, null) => sn,
+ (char sn, string ln) => (sn, ln),
+ };
+ }
+ }
+
+ ///
+ /// Builds the current into a .
+ ///
+ /// The parrent of the constructed command.
+ /// The built .
+ public CommandNode Build(IParentNode parent)
+ {
+ if (_invocation is not { } invoke)
+ {
+ throw new InvalidOperationException("Cannot create a command without an entrypoint.");
+ }
+
+ var shape = CommandShape.FromBuilder(this);
+
+ return new CommandNode
+ (
+ parent,
+ this.Name,
+ invoke,
+ shape,
+ this.Aliases,
+ this.Attributes,
+ this.Conditions
+ );
+ }
+
+ ///
+ /// Finishes building the command, and returns the group builder if applicable.
+ /// This method should only be called if the instance was generated from .
+ ///
+ /// Thrown if the command builder was not associated with a group.
+ /// The parent builder.
+ public GroupBuilder Finish()
+ {
+ return this.Parent ?? throw new InvalidOperationException("The command builder was not attatched to a group.");
+ }
+}
diff --git a/Remora.Commands/Builders/CommandParameterBuilder.cs b/Remora.Commands/Builders/CommandParameterBuilder.cs
new file mode 100644
index 0000000..7e1b8cf
--- /dev/null
+++ b/Remora.Commands/Builders/CommandParameterBuilder.cs
@@ -0,0 +1,446 @@
+//
+// CommandParameterBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using OneOf;
+using Remora.Commands.Attributes;
+using Remora.Commands.Conditions;
+using Remora.Commands.Extensions;
+using Remora.Commands.Signatures;
+
+namespace Remora.Commands.Builders;
+
+///
+/// A builder class for command parameters.
+///
+public class CommandParameterBuilder
+{
+ private readonly CommandBuilder _builder;
+ private readonly List _attributes;
+ private readonly List _conditions;
+
+ private string _name;
+ private bool _isGreedy;
+ private bool _isOptional;
+ private string? _description;
+ private object? _defaultValue;
+ private Type _parameterType;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The command this parameter belongs to.
+ /// The optional type of the parameter.
+ /// If is null, MUST be called before .
+ public CommandParameterBuilder(CommandBuilder builder, Type type)
+ {
+ _name = string.Empty;
+ _builder = builder;
+ _attributes = new();
+ _conditions = new();
+ builder.Parameters.Add(this);
+
+ _parameterType = type;
+ }
+
+ ///
+ /// Sets the name of the parameter.
+ ///
+ /// The name of the parameter.
+ /// The builder to chain calls with.
+ public CommandParameterBuilder WithName(string name)
+ {
+ _name = name;
+ return this;
+ }
+
+ ///
+ /// Sets the description of the parameter.
+ ///
+ /// The description of the parameter.
+ /// The builder to chain calls with.
+ public CommandParameterBuilder WithDescription(string description)
+ {
+ _description = description;
+ return this;
+ }
+
+ ///
+ /// Sets the default value of the parameter. This must match the parameter's type.
+ ///
+ /// The default value.
+ /// The parameter type.
+ ///
+ /// Invoking this method will mark the parameter as optional regardless of the value
+ /// passed.
+ ///
+ /// The builder to chain calls with.
+ public CommandParameterBuilder WithDefaultValue([MaybeNull] T value)
+ {
+ if (typeof(T).IsValueType && !typeof(T).IsNullableStruct() && value is null)
+ {
+ throw new InvalidOperationException("Non-nullable value types must have a non-null default value.");
+ }
+
+ if (value is not null && _parameterType is { } parameterType && !parameterType.IsInstanceOfType(value))
+ {
+ throw new ArgumentException
+ (
+ "The default value must match the parameter's type.",
+ nameof(value)
+ );
+ }
+
+ _defaultValue = value;
+ _isOptional = true;
+
+ return this;
+ }
+
+ ///
+ /// Sets the type of the parameter.
+ ///
+ /// The type to set the parameter to.
+ /// The builder to chain calls with.
+ /// Thrown if the type set on the builder does not match the default value (if set).
+ public CommandParameterBuilder WithType(Type type)
+ {
+ if (_defaultValue is not null && _defaultValue.GetType() != type)
+ {
+ throw new InvalidOperationException("The type of the parameter must match the default value's type.");
+ }
+
+ _parameterType = type;
+ return this;
+ }
+
+ ///
+ /// Sets the parameter to be greedy.
+ ///
+ /// The builder to chain calls with.
+ public CommandParameterBuilder IsGreedy()
+ {
+ _isGreedy = true;
+
+ return this;
+ }
+
+ ///
+ /// Sets the parameter to be a named switch.
+ ///
+ /// The default value of the switch.
+ /// Either the short name (e.g. '-o') or the long name (e.g. '--switch') of the switch.
+ /// The builder to chain calls with.
+ ///
+ /// This method will set a default value for the parameter, which will
+ /// implicitly make it optional.
+ ///
+ /// Thrown if both tuple values in are null.
+ public CommandParameterBuilder IsSwitch(bool defaultValue, OneOf shortOrLongName = default)
+ {
+ _defaultValue = defaultValue;
+ _parameterType = typeof(bool);
+
+ var canBeSwitch = !_attributes.Any(r => r is OptionAttribute);
+
+ if (!canBeSwitch)
+ {
+ throw new InvalidOperationException("A parameter marked as an option cannot be a switch.");
+ }
+
+ var attribute = shortOrLongName.Match
+ (
+ shortName => new SwitchAttribute(shortName),
+ longName => new SwitchAttribute(longName),
+ shortAndLongName => new SwitchAttribute(shortAndLongName.Short, shortAndLongName.Long)
+ );
+
+ _attributes.Add(attribute);
+
+ return this;
+ }
+
+ ///
+ /// Marks the parameter as being a named option.
+ ///
+ /// Either the short name (e.g. '-o') or the long name (e.g. '--option') of the option.
+ /// The builder to chain calls with.
+ /// Thrown if both values for are null..
+ public CommandParameterBuilder IsOption(OneOf shortOrLongName = default)
+ {
+ var canBeOption = !_attributes.Any(r => r is SwitchAttribute);
+
+ if (!canBeOption)
+ {
+ throw new InvalidOperationException("A parameter marked as an option cannot be a switch.");
+ }
+
+ var attribute = shortOrLongName.Match
+ (
+ shortName => new OptionAttribute(shortName),
+ longName => new OptionAttribute(longName),
+ shortAndLongName => new OptionAttribute(shortAndLongName.Short, shortAndLongName.Long)
+ );
+
+ _attributes.Add(attribute);
+
+ return this;
+ }
+
+ ///
+ /// Adds an attribute to the parameter. Conditions must be added via .
+ ///
+ /// The attriubte to add.
+ /// The builder to chain calls with.
+ public CommandParameterBuilder AddAttribute(Attribute attribute)
+ {
+ if (attribute is ConditionAttribute)
+ {
+ throw new InvalidOperationException("Conditions must be added via AddCondition.");
+ }
+
+ _attributes.Add(attribute);
+ return this;
+ }
+
+ ///
+ /// Adds a condition to the parameter.
+ ///
+ /// The condition to add.
+ /// The builder to chain with.
+ public CommandParameterBuilder AddCondition(ConditionAttribute condition)
+ {
+ _conditions.Add(condition);
+ return this;
+ }
+
+ ///
+ /// Finishes creating the parameter, and returns the original builder.
+ ///
+ /// The builder this parameter belongs to.
+ public CommandBuilder Finish()
+ {
+ return _builder;
+ }
+
+ ///
+ /// Builds the current builder into a .
+ ///
+ /// The constructed .
+ public IParameterShape Build()
+ {
+ var rangeAttribute = _attributes.OfType().SingleOrDefault();
+ var optionAttribute = _attributes.OfType().SingleOrDefault();
+
+ if (optionAttribute is SwitchAttribute)
+ {
+ if (_defaultValue is not bool || _parameterType != typeof(bool))
+ {
+ throw new InvalidOperationException
+ (
+ $"A switch parameter must be a {typeof(bool)}" +
+ $" (The parameter \"{_name}\" was marked as ({_parameterType})."
+ );
+ }
+ }
+
+ return optionAttribute is null
+ ? CreatePositionalParameterShape(rangeAttribute)
+ : CreateNamedParameterShape(optionAttribute, rangeAttribute);
+ }
+
+ private IParameterShape CreateNamedParameterShape(OptionAttribute optionAttribute, RangeAttribute? rangeAttribute)
+ {
+ var isCollection = _parameterType.IsSupportedCollection();
+
+ IParameterShape newNamedParameter;
+ if (optionAttribute is SwitchAttribute)
+ {
+ newNamedParameter = CreateNamedSwitchParameterShape(optionAttribute);
+ }
+ else if (!isCollection)
+ {
+ newNamedParameter = _isGreedy
+ ? CreateNamedSingleValueParameterShape(optionAttribute)
+ : CreateGreedyNamedSingleValueParameterShape(optionAttribute);
+ }
+ else
+ {
+ newNamedParameter = CreateNamedCollectionParameterShape(optionAttribute, rangeAttribute);
+ }
+
+ return newNamedParameter;
+ }
+
+ private IParameterShape CreateNamedCollectionParameterShape
+ (
+ OptionAttribute optionAttribute,
+ RangeAttribute? rangeAttribute
+ )
+ {
+ var description = _description ?? Constants.DefaultDescription;
+
+ IParameterShape newNamedParameter = new NamedCollectionParameterShape
+ (
+ optionAttribute.ShortName,
+ optionAttribute.LongName,
+ rangeAttribute?.GetMin(),
+ rangeAttribute?.GetMax(),
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+
+ return newNamedParameter;
+ }
+
+ private IParameterShape CreateNamedSwitchParameterShape(OptionAttribute optionAttribute)
+ {
+ if (!_isOptional)
+ {
+ throw new InvalidOperationException("Switches must have a default value.");
+ }
+
+ if (_parameterType != typeof(bool))
+ {
+ throw new InvalidOperationException("Switches must be booleans.");
+ }
+
+ var description = _description ?? Constants.DefaultDescription;
+
+ IParameterShape newNamedParameter = new SwitchParameterShape
+ (
+ optionAttribute.ShortName,
+ optionAttribute.LongName,
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+
+ return newNamedParameter;
+ }
+
+ private IParameterShape CreateNamedSingleValueParameterShape
+ (
+ OptionAttribute optionAttribute
+ )
+ {
+ var description = _description ?? Constants.DefaultDescription;
+
+ IParameterShape newNamedParameter = new NamedParameterShape
+ (
+ optionAttribute.ShortName,
+ optionAttribute.LongName,
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+
+ return newNamedParameter;
+ }
+
+ private IParameterShape CreateGreedyNamedSingleValueParameterShape(OptionAttribute optionAttribute)
+ {
+ var description = _description ?? Constants.DefaultDescription;
+
+ IParameterShape newNamedParameter = new NamedGreedyParameterShape
+ (
+ optionAttribute.ShortName,
+ optionAttribute.LongName,
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+
+ return newNamedParameter;
+ }
+
+ private IParameterShape CreatePositionalParameterShape(RangeAttribute? rangeAttribute)
+ {
+ var description = _description ?? Constants.DefaultDescription;
+ var isCollection = _parameterType.IsSupportedCollection();
+
+ IParameterShape newPositionalParameter;
+ if (!isCollection)
+ {
+ var greedyAttribute = _attributes.OfType().SingleOrDefault();
+
+ newPositionalParameter = greedyAttribute is null
+ ? new PositionalParameterShape
+ (
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ )
+ : new PositionalGreedyParameterShape
+ (
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+ }
+ else
+ {
+ newPositionalParameter = new PositionalCollectionParameterShape
+ (
+ rangeAttribute?.GetMin(),
+ rangeAttribute?.GetMax(),
+ _name,
+ _parameterType,
+ _isOptional,
+ _defaultValue,
+ _attributes,
+ _conditions,
+ description
+ );
+ }
+
+ return newPositionalParameter;
+ }
+}
diff --git a/Remora.Commands/Builders/GroupBuilder.cs b/Remora.Commands/Builders/GroupBuilder.cs
new file mode 100644
index 0000000..8945a7a
--- /dev/null
+++ b/Remora.Commands/Builders/GroupBuilder.cs
@@ -0,0 +1,226 @@
+//
+// GroupBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using OneOf;
+using Remora.Commands.Attributes;
+using Remora.Commands.DependencyInjection;
+using Remora.Commands.Extensions;
+using Remora.Commands.Groups;
+using Remora.Commands.Trees.Nodes;
+using Remora.Results;
+
+namespace Remora.Commands.Builders;
+
+///
+/// A builder class for creating s.
+///
+public class GroupBuilder : AbstractCommandBuilder
+{
+ private List _groupTypes;
+
+ ///
+ /// Gets the children of the group.
+ ///
+ internal List> Children { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent this group belongs to, if any.
+ public GroupBuilder(GroupBuilder? parent = null)
+ : base(parent)
+ {
+ this.Name = string.Empty;
+ this.Children = new List>();
+ _groupTypes = new List();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The registration builder.
+ public GroupBuilder(TreeRegistrationBuilder treeBuilder)
+ : base(treeBuilder)
+ {
+ this.Name = string.Empty;
+ this.Children = new List>();
+ _groupTypes = new List();
+ }
+
+ ///
+ /// Adds a command to the group.
+ ///
+ /// A to build the command with.
+ public CommandBuilder AddCommand()
+ {
+ return new CommandBuilder(this);
+ }
+
+ ///
+ /// Adds another group to the group builder.
+ ///
+ /// The current builder to chain calls with.
+ public GroupBuilder AddGroup()
+ {
+ var builder = new GroupBuilder(this);
+ this.Children.Add(builder);
+
+ return builder;
+ }
+
+ ///
+ /// Returns the parent builder of the current group.
+ ///
+ /// The parent builder of the group, if applicable.
+ /// Thrown if the group does not belong to a parent.
+ /// This method should only be called if the builder was instantiated from a call to .
+ public GroupBuilder Complete()
+ {
+ if (this.Parent is null)
+ {
+ throw new InvalidOperationException("Cannot complete a group that has no parent.");
+ }
+
+ return this.Parent;
+ }
+
+ ///
+ /// Returns the registration builder of the current group.
+ ///
+ /// The that created this builder, if applicable.
+ /// Thrown if the group was not created via a registration builder.
+ /// This method should only be called if the builder was instnatiated from a call to .
+ public TreeRegistrationBuilder Finish()
+ {
+ if (this.TreeBuilder is null)
+ {
+ throw new InvalidOperationException("Cannot complete a group that has no parent.");
+ }
+
+ return this.TreeBuilder;
+ }
+
+ ///
+ /// Constructs a from a given module type.
+ ///
+ /// The type of the module to construct from.
+ /// The parent of the builder, if applicable.
+ /// The constructed builder.
+ /// Thrown when a command method is marked with
+ /// but the return type of the command is not supported.
+ public static GroupBuilder FromType(Type moduleType, GroupBuilder? parent = null)
+ {
+ var builder = new GroupBuilder(parent);
+
+ builder._groupTypes = new List { moduleType };
+
+ var groupAttribute = moduleType.GetCustomAttribute()!;
+
+ builder.WithName(groupAttribute.Name);
+
+ builder.AddAliases(groupAttribute.Aliases);
+
+ var description = moduleType.GetCustomAttribute();
+
+ if (description is not null)
+ {
+ builder.WithDescription(description.Description);
+ }
+
+ moduleType.GetAttributesAndConditions(out var attributes, out var conditions);
+
+ builder.Attributes.AddRange(attributes);
+ builder.Conditions.AddRange(conditions);
+
+ foreach (var childMethod in moduleType.GetMethods())
+ {
+ var commandAttribute = childMethod.GetCustomAttribute();
+
+ if (commandAttribute is null)
+ {
+ continue;
+ }
+
+ if (!childMethod.ReturnType.IsSupportedCommandReturnType())
+ {
+ throw new InvalidOperationException
+ (
+ $"Methods marked as commands must return a {typeof(Task<>)} or {typeof(ValueTask<>)}, " +
+ $"containing a type that implements {typeof(IResult)}."
+ );
+ }
+
+ var commandBuilder = CommandBuilder.FromMethod(builder, childMethod);
+ builder.Children.Add(commandBuilder);
+ }
+
+ foreach (var childType in moduleType.GetNestedTypes())
+ {
+ if (!childType.TryGetGroupName(out _) || !childType.IsSubclassOf(typeof(CommandGroup)))
+ {
+ continue;
+ }
+
+ var childGroup = FromType(childType, builder);
+ builder.Children.Add(childGroup);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Recursively builds the current into a .
+ ///
+ /// The parent of the group.
+ /// The built group node.
+ public GroupNode Build(IParentNode parent)
+ {
+ var children = new List();
+
+ var node = new GroupNode
+ (
+ Type.EmptyTypes,
+ children,
+ parent,
+ this.Name,
+ this.Aliases,
+ this.Attributes,
+ this.Conditions,
+ this.Description
+ );
+
+ foreach (var child in this.Children)
+ {
+ children.Add(child.Match
+ (
+ commandBuilder => commandBuilder.Build(node),
+ groupBuilder => groupBuilder.Build(node)
+ ));
+ }
+
+ return node;
+ }
+}
diff --git a/Remora.Commands/DependencyInjection/TreeRegistrationBuilder.cs b/Remora.Commands/DependencyInjection/TreeRegistrationBuilder.cs
index 622db42..ae00179 100644
--- a/Remora.Commands/DependencyInjection/TreeRegistrationBuilder.cs
+++ b/Remora.Commands/DependencyInjection/TreeRegistrationBuilder.cs
@@ -24,6 +24,7 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Remora.Commands.Builders;
using Remora.Commands.Groups;
using Remora.Commands.Trees;
@@ -107,6 +108,40 @@ void AddGroupsScoped(Type groupType)
return this;
}
+ ///
+ /// Creates a new to add to the command tree.
+ ///
+ /// The created builder.
+ public GroupBuilder CreateCommandGroup()
+ {
+ var builder = new GroupBuilder(this);
+
+ _serviceCollection.Configure
+ (
+ _treeName,
+ b => b.RegisterNodeBuilder(builder)
+ );
+
+ return builder;
+ }
+
+ ///
+ /// Creates a new to add to the command tree.
+ ///
+ /// The created builder.
+ public CommandBuilder CreateCommand()
+ {
+ var builder = new CommandBuilder(this);
+
+ _serviceCollection.Configure
+ (
+ _treeName,
+ b => b.RegisterNodeBuilder(builder)
+ );
+
+ return builder;
+ }
+
///
/// Finishes configuring the tree, returning the service collection.
///
diff --git a/Remora.Commands/Extensions/CustomAttributeProviderExtensions.cs b/Remora.Commands/Extensions/CustomAttributeProviderExtensions.cs
index 835f28c..69274cc 100644
--- a/Remora.Commands/Extensions/CustomAttributeProviderExtensions.cs
+++ b/Remora.Commands/Extensions/CustomAttributeProviderExtensions.cs
@@ -25,6 +25,7 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
+using Remora.Commands.Conditions;
namespace Remora.Commands.Extensions;
@@ -55,6 +56,18 @@ public static TAttribute? GetCustomAttribute
return attributeProvider.GetCustomAttributes(typeof(TAttribute), inherit).FirstOrDefault() as TAttribute;
}
+ ///
+ /// Gets the conditions and attributes of the given attribute provider.
+ ///
+ /// The attribute provider.
+ /// The attributes extracted from the provider, excluding conditions.
+ /// The conditions extracted from the provider.
+ public static void GetAttributesAndConditions(this ICustomAttributeProvider attributeProvider, out Attribute[] attributes, out ConditionAttribute[] conditions)
+ {
+ attributes = attributeProvider.GetCustomAttributes(true).Where(att => att is not ConditionAttribute).Cast().ToArray();
+ conditions = attributeProvider.GetCustomAttributes(typeof(ConditionAttribute), true).Cast().ToArray();
+ }
+
///
/// Gets a user-configured description of the given attribute provider. The description is taken from an
/// instance of the .
diff --git a/Remora.Commands/Remora.Commands.csproj b/Remora.Commands/Remora.Commands.csproj
index 464273d..90b040b 100644
--- a/Remora.Commands/Remora.Commands.csproj
+++ b/Remora.Commands/Remora.Commands.csproj
@@ -19,6 +19,8 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Remora.Commands/Services/CommandService.cs b/Remora.Commands/Services/CommandService.cs
index e0a9615..7b98d87 100644
--- a/Remora.Commands/Services/CommandService.cs
+++ b/Remora.Commands/Services/CommandService.cs
@@ -23,14 +23,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Remora.Commands.Conditions;
using Remora.Commands.Extensions;
-using Remora.Commands.Groups;
using Remora.Commands.Results;
using Remora.Commands.Signatures;
using Remora.Commands.Tokenization;
@@ -335,43 +333,9 @@ public async Task> TryExecuteAsync
{
var (boundCommandNode, parameters) = preparedCommand;
- var groupType = boundCommandNode.Node.GroupType;
- var groupInstance = (CommandGroup)services.GetRequiredService(groupType);
-
- groupInstance.SetCancellationToken(ct);
-
- var method = boundCommandNode.Node.CommandMethod;
-
try
{
- IResult result;
- if (method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
- {
- var genericUnwrapMethod = GetType()
- .GetMethod(nameof(UnwrapCommandValueTask), BindingFlags.NonPublic | BindingFlags.Static)
- ?? throw new InvalidOperationException();
-
- var unwrapMethod = genericUnwrapMethod
- .MakeGenericMethod(method.ReturnType.GetGenericArguments().Single());
-
- var invocationResult = method.Invoke(groupInstance, parameters);
- var unwrapTask = (Task)(unwrapMethod.Invoke
- (
- null, new[] { invocationResult }
- ) ?? throw new InvalidOperationException());
-
- result = await unwrapTask;
- }
- else
- {
- var invocationResult = (Task)(method.Invoke(groupInstance, parameters)
- ?? throw new InvalidOperationException());
- await invocationResult;
-
- result = (IResult)(invocationResult.GetType().GetProperty(nameof(Task
/// The available services.
/// The node whose conditions are being checked.
- /// The group.
/// The cancellation token for this operation.
/// A condition result which may or may not have succeeded.
private async Task CheckConditionsAsync
(
IServiceProvider services,
IChildNode? node,
- ICustomAttributeProvider attributeProvider,
CancellationToken ct
)
{
- var conditionAttributes = attributeProvider.GetCustomAttributes(typeof(ConditionAttribute), false);
+ var conditionAttributes = node?.Conditions ?? Array.Empty();
if (!conditionAttributes.Any())
{
return Result.FromSuccess();
@@ -575,7 +532,7 @@ CancellationToken ct
var invocationResult = conditionMethod.Invoke
(
condition,
- new[] { conditionAttribute, ct }
+ new object[] { conditionAttribute, ct }
)
?? throw new InvalidOperationException();
@@ -614,12 +571,12 @@ private async Task CheckConditionsAsync
(
IServiceProvider services,
IChildNode node,
- ParameterInfo parameter,
+ IParameterShape parameter,
object? value,
CancellationToken ct
)
{
- var conditionAttributes = parameter.GetCustomAttributes(typeof(ConditionAttribute), false);
+ var conditionAttributes = parameter.Conditions;
if (!conditionAttributes.Any())
{
return Result.FromSuccess();
@@ -700,25 +657,26 @@ CancellationToken ct
{
var materializedParameters = new List