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

Initial groundwork for command builders (#19) #20

Open
wants to merge 97 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
a3bffa9
Initial changes
VelvetToroyashi Nov 25, 2022
1595549
Merge remote-tracking branch 'upstream/main' into feat/command-builders
VelvetToroyashi Nov 25, 2022
be84e1c
Add new constructors for parameter shapes
VelvetToroyashi Nov 26, 2022
b5329e2
Add `.FromBuilder` to `CommandShape`
VelvetToroyashi Nov 26, 2022
7d776ce
Fix shapes missing/not setting parameters and properties
VelvetToroyashi Nov 26, 2022
4c66ba4
Jerry-rig `CommandShape.FromMethod` to work with CommandParameterBuilder
VelvetToroyashi Nov 26, 2022
82997c3
Add new constructor for CommandNode, deprecate `CommandMethod`, and f…
VelvetToroyashi Nov 26, 2022
e52da64
Add `CommandBuilder` and necessary APIs; requires `GroupBuilder` to b…
VelvetToroyashi Nov 26, 2022
906facc
Implement GroupBuilder
VelvetToroyashi Nov 26, 2022
c87eceb
Fix various compiler errors
VelvetToroyashi Nov 26, 2022
7ebccb8
Temporary "fix" for registration. Rewrite pending.
VelvetToroyashi Nov 26, 2022
c2b5c3b
Create compiled expression and use new constructor for commands
VelvetToroyashi Nov 27, 2022
82ffb80
Fix typo in method name
VelvetToroyashi Nov 27, 2022
1ae9c23
Expose `Name` and `Children` in `GroupBuilder`
VelvetToroyashi Nov 27, 2022
42f6bca
Add `WithType` method to `CommandParameterBuilder`
VelvetToroyashi Nov 27, 2022
3977af2
Add `FromMethod` method to `CommnadBuilder`
VelvetToroyashi Nov 27, 2022
871efe7
Register commands using builders
VelvetToroyashi Nov 27, 2022
39c1f7d
Add invocation to command, oops
VelvetToroyashi Nov 27, 2022
a6dcdc7
Difference in nullability?
VelvetToroyashi Nov 27, 2022
9458e73
Add attributes and conditions to `IChildNode`
VelvetToroyashi Nov 28, 2022
b37a6a3
Rename properties on group node to fit `IChildNode`
VelvetToroyashi Nov 28, 2022
2292097
Pass cancellation token to invocation and make expression set token
VelvetToroyashi Nov 28, 2022
bdfb089
Simplify command execution by invoking delegate
VelvetToroyashi Nov 28, 2022
83f103f
Fix compiler errors before Jax kills me
VelvetToroyashi Nov 28, 2022
a7650f1
Add `GetAttributesAndConditions` method and use where appropriate
VelvetToroyashi Nov 28, 2022
697c61b
Add built command to registered list
VelvetToroyashi Nov 28, 2022
c465acb
Handle subcommands
VelvetToroyashi Nov 28, 2022
54e1294
Fix bug with pulling service from the container in the compiled expre…
VelvetToroyashi Nov 28, 2022
d371421
Add `GroupBuilder.FromType()`
VelvetToroyashi Nov 28, 2022
0026db3
🐛 Not all children are groups
VelvetToroyashi Nov 28, 2022
ecb5d3d
Remove code for builders
VelvetToroyashi Nov 28, 2022
4596c92
Add real indexes to parameter shapes and order by that
VelvetToroyashi Nov 28, 2022
9892b94
Fix null parameter names causing tests to fail
VelvetToroyashi Nov 28, 2022
ef0ee8d
Extract as much information about parameters as possible
VelvetToroyashi Nov 28, 2022
655486c
Restore prior collection shape default value behavior
VelvetToroyashi Nov 28, 2022
8873793
Fix deadlock when evaluating group conditions
VelvetToroyashi Nov 28, 2022
1ec2505
use block expression not call expression so that the CT is set
VelvetToroyashi Nov 28, 2022
5bdf616
Extract attributes from inhertance tree as needed
VelvetToroyashi Nov 29, 2022
fcb67d1
Attributes are now always attached to a node; fix tests
VelvetToroyashi Nov 29, 2022
b9241d1
Fix some compiler warnings and add cached version of method info
VelvetToroyashi Nov 29, 2022
d4697d1
Use reflection-free version of getting method infos
VelvetToroyashi Nov 29, 2022
d8e546f
Create API for registering builders
VelvetToroyashi Nov 29, 2022
96ae435
Nullable
VelvetToroyashi Nov 29, 2022
bb1ac29
Lets *not* get sued by Microsoft
VelvetToroyashi Nov 29, 2022
a01ee60
That method is internal, oops
VelvetToroyashi Nov 29, 2022
eeea846
Nonsensical null-forgiving operator usage
VelvetToroyashi Nov 29, 2022
554a2d1
Fix botched documentation in CoerceToValueTask
VelvetToroyashi Nov 29, 2022
fb9d087
Fix inconsistency in getting attributes in conditions in NamedGreedyP…
VelvetToroyashi Nov 29, 2022
bd99238
Create delegate type for command invocations
VelvetToroyashi Nov 29, 2022
4c0cae1
Improve(?) safety of builder for switches
VelvetToroyashi Nov 29, 2022
cc58f42
Inline variable
VelvetToroyashi Nov 29, 2022
b9cabcd
Various fixes suggested by Maxine
VelvetToroyashi Nov 29, 2022
81ef88f
Clean up code when creatin ga command shape
VelvetToroyashi Nov 29, 2022
4a07a68
Fix compiler errors
VelvetToroyashi Nov 29, 2022
ff66deb
RegisterBuilder ➜ RegisterNodeBuilder
VelvetToroyashi Nov 29, 2022
ff499d3
Indentation
VelvetToroyashi Dec 2, 2022
6b19431
Space (it builds this time I swear)
VelvetToroyashi Dec 2, 2022
be535a9
fix: Add paremeter builder to parent
VelvetToroyashi Jun 4, 2023
1690814
fix!: Allow `FromMethod` to create an invocation
VelvetToroyashi Jun 4, 2023
9ab6f10
test: Add tests for command builder and tree builder
VelvetToroyashi Jun 4, 2023
8b78a90
feat: Bind built commands to the tree
VelvetToroyashi Jun 4, 2023
3cdd5d9
test: Ensure that binding works
VelvetToroyashi Jun 4, 2023
1959c45
fix: Don't merge nonexistent groups
VelvetToroyashi Jun 4, 2023
2cc7f43
docs: Reword CommandBuilder.WithInvocation
VelvetToroyashi Jun 7, 2023
f2835c9
fix!: Use OneOf<char, string> in option methods
VelvetToroyashi Jun 7, 2023
ed4a4b9
fix: Conform to new API
VelvetToroyashi Jun 7, 2023
f7b0962
refactor: Formatting
VelvetToroyashi Jun 7, 2023
8649ed6
refactor!: Remove init from GroupBuilder.Children
VelvetToroyashi Jun 7, 2023
47e1816
refactor: Use `this.` where appropriate and remove unused usings.
VelvetToroyashi Jun 7, 2023
38eebcc
fix!: Use default value from base
VelvetToroyashi Jun 7, 2023
e3a60c1
fix: Use default value from base
VelvetToroyashi Jun 7, 2023
504b5c4
refactor: Place private readonly above
VelvetToroyashi Jun 7, 2023
dd439fb
refactor: Remove more erroneous `this.`
VelvetToroyashi Jun 7, 2023
aa7ac82
docs: Clarify "non-ephemeral"
VelvetToroyashi Jun 7, 2023
254a77b
Update Remora.Commands/Builders/CommandBuilder.cs
Nihlus Sep 17, 2023
f56179b
Update Remora.Commands/Builders/CommandBuilder.cs
Nihlus Sep 17, 2023
f9a205f
Update Remora.Commands/Builders/CommandBuilder.cs
Nihlus Sep 17, 2023
2aeb012
feat(builders)!: extract common features from builders into abstract …
VelvetToroyashi Mar 16, 2024
21a6352
fix(builders): Return TSelf and cast
VelvetToroyashi Mar 17, 2024
b8b6097
refactor(trees): Respect immutability (sorta) when merging commands
VelvetToroyashi Mar 17, 2024
60a89b1
fix(trees): Fix merging algorithm
VelvetToroyashi Mar 17, 2024
63f2207
refactor(backend)!: Remove parameter indexes
VelvetToroyashi Mar 17, 2024
30d2c4a
chore: fix merge conflict?
VelvetToroyashi Mar 17, 2024
6a4d11e
Merge branch 'main' into feat/command-builders
VelvetToroyashi Mar 17, 2024
91f9df0
refactor(builders): Make unecessarily static methods instanced
VelvetToroyashi Mar 17, 2024
6d4af34
chore: Address open issues/PR comments
VelvetToroyashi Mar 17, 2024
34a7854
fix(builders): Remove duplicate methods and pass parent down
VelvetToroyashi Mar 17, 2024
db7ea85
chore: fix compiler warnings
VelvetToroyashi Mar 17, 2024
5dac26a
Merge remote-tracking branch 'origin/feat/command-builders' into feat…
VelvetToroyashi Mar 17, 2024
0e5783d
chore: Fix build issue :v
VelvetToroyashi Mar 17, 2024
166667c
chore: :dumb: PackageVersion not PackageReference
VelvetToroyashi Mar 17, 2024
1cef637
fix(commands): Fix regression with commands that have named parameters
VelvetToroyashi Mar 17, 2024
8ec09ad
style: Fix `this.` qualifiers
VelvetToroyashi Mar 31, 2024
b2a760a
chore: Remove unused imports
VelvetToroyashi Mar 31, 2024
58a10bc
style: Reduce unecessary multi-line method sigs
VelvetToroyashi Mar 31, 2024
cd7ed3d
fix: Fix nullability in command parameters
VelvetToroyashi Mar 31, 2024
9a5530a
fix: Fix broken tests (and compiler warnings)
VelvetToroyashi Mar 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/.idea.Remora.Commands/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

342 changes: 342 additions & 0 deletions Remora.Commands/Builders/CommandBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
//
// CommandBuilder.cs
//
// Author:
// Jarl Gullberg <[email protected]>
//
// 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 <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using OneOf;
using Remora.Commands.Attributes;
using Remora.Commands.Conditions;
using Remora.Commands.DependencyInjection;
using Remora.Commands.Extensions;
using Remora.Commands.Signatures;
using Remora.Commands.Trees;
using Remora.Commands.Trees.Nodes;
using Remora.Results;

namespace Remora.Commands.Builders;

/// <summary>
/// A builder for commands, which exposes a fluent API.
/// </summary>
public class CommandBuilder
{
/// <summary>
/// Gets the parameters of the command.
/// </summary>
internal List<CommandParameterBuilder> Parameters { get; }

/// <summary>
/// Gets the description of the builder.
/// </summary>
internal string? Description { get; private set; }

private readonly List<string> _aliases;
private readonly List<Attribute> _attributes;

private readonly List<ConditionAttribute> _conditions;

private readonly GroupBuilder? _parent;
private readonly TreeRegistrationBuilder? _treeBuilder;

private string _name;
private CommandInvocation? _invocation;

/// <summary>
/// Initializes a new instance of the <see cref="CommandBuilder"/> class.
/// </summary>
/// <param name="parent">The parent of the command.</param>
public CommandBuilder(GroupBuilder? parent = null)
{
_name = string.Empty;
_aliases = new();
_attributes = new();
Parameters = new();
_conditions = new();

Nihlus marked this conversation as resolved.
Show resolved Hide resolved
_parent = parent;

_parent?.Children.Add(this);
}

/// <summary>
/// Initializes a new instance of the <see cref="CommandBuilder"/> class.
/// </summary>
/// <param name="treeBuilder">The registration builder.</param>
public CommandBuilder(TreeRegistrationBuilder treeBuilder)
: this()
{
_treeBuilder = treeBuilder;
}

/// <summary>
/// Sets the name of the command.
/// </summary>
/// <param name="name">The name of the command.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder WithName(string name)
{
_name = name;
return this;
}

/// <summary>
/// Sets the description of the command.
/// </summary>
/// <param name="description">The description of the command.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder WithDescription(string description)
{
Description = description;
return this;
}

/// <summary>
/// Adds an alias to the command.
/// </summary>
/// <param name="alias">The alias to add.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder AddAlias(string alias)
{
_aliases.Add(alias);
return this;
}

/// <summary>
/// Adds multiple aliases to the command.
/// </summary>
/// <param name="aliases">The aliases to add.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder AddAliases(IEnumerable<string> aliases)
{
_aliases.AddRange(aliases);
return this;
}

/// <summary>
/// Adds an attribute to the command. Conditions must be added via <see cref="AddCondition"/>.
/// </summary>
/// <param name="attribute">The attribute to add.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder AddAttribute(Attribute attribute)
{
if (attribute is ConditionAttribute)
{
throw new InvalidOperationException("Conditions must be added via AddCondition.");
}

_attributes.Add(attribute);
return this;
}

/// <summary>
/// Adds a condition to the command.
/// </summary>
/// <param name="condition">The condition to add.</param>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder AddCondition(ConditionAttribute condition)
{
_conditions.Add(condition);
return this;
}

/// <summary>
/// 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).
/// </summary>
/// <param name="invokeFunc">The function to invoke the command, or the command itself.</param>
/// <remarks>This method MUST be called before <see cref="Build"/>.</remarks>
/// <returns>The current builder to chain calls with.</returns>
public CommandBuilder WithInvocation(CommandInvocation invokeFunc)
{
_invocation = invokeFunc;
return this;
}

/// <summary>
/// Adds a new parameter to the command.
/// </summary>
/// <param name="type">The optional type of the parameter.</param>
/// <returns>The parameter builder to build the parameter with.</returns>
public CommandParameterBuilder AddParameter(Type? type = null)
{
var parameterBuilder = new CommandParameterBuilder(this, type);
Parameters.Add(parameterBuilder);
return parameterBuilder;
}

/// <summary>
/// Creates a <see cref="CommandBuilder"/> from a method.
/// </summary>
/// <param name="parent">The parent builder, if applicable.</param>
/// <param name="info">The method to extract from.</param>
/// <returns>The builder.</returns>
public static CommandBuilder FromMethod(GroupBuilder? parent, MethodInfo info)
{
var builder = new CommandBuilder(parent);

var commandAttribute = info.GetCustomAttribute<CommandAttribute>()!;

builder.WithName(commandAttribute.Name);
builder.AddAliases(commandAttribute.Aliases);

var descriptionAttribute = info.GetCustomAttribute<DescriptionAttribute>();

if (descriptionAttribute is not null)
{
builder.WithDescription(descriptionAttribute.Description);
}

var parameters = info.GetParameters();
var parameterTypes = parameters.Select(p => p.ParameterType).ToArray();

builder.WithInvocation(CommandTreeBuilder.CreateDelegate(info, parameterTypes));

foreach (var parameter in parameters)
{
var parameterBuilder = builder.AddParameter(parameter.ParameterType);
parameterBuilder.WithName(parameter.Name!);
VelvetToroyashi marked this conversation as resolved.
Show resolved Hide resolved

var description = parameter.GetCustomAttribute<DescriptionAttribute>();
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<OptionAttribute>();

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<GreedyAttribute>();

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);
VelvetToroyashi marked this conversation as resolved.
Show resolved Hide resolved

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<char, string, (char shortName, string longName)> GetAttributeValue(char? shortName, string? longName)

Check warning on line 294 in Remora.Commands/Builders/CommandBuilder.cs

View workflow job for this annotation

GitHub Actions / build

Check warning on line 294 in Remora.Commands/Builders/CommandBuilder.cs

View workflow job for this annotation

GitHub Actions / build

{
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),
};
}
}

/// <summary>
/// Builds the current <see cref="CommandBuilder"/> into a <see cref="CommandNode"/>.
/// </summary>
/// <param name="parent">The parrent of the constructed command.</param>
/// <returns>The built <see cref="CommandNode"/>.</returns>
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,
_name,
invoke,
shape,
_aliases,
_attributes,
_conditions
);
}

/// <summary>
/// Finishes building the command, and returns the group builder if applicable.
/// This method should only be called if the instance was generated from <see cref="GroupBuilder.AddCommand"/>.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the command builder was not associated with a group.</exception>
/// <returns>The parent builder.</returns>
public GroupBuilder Finish()
{
return _parent ?? throw new InvalidOperationException("The command builder was not attatched to a group.");
}
}
Loading
Loading