Skip to content
This repository has been archived by the owner on Apr 20, 2022. It is now read-only.

Interaction Service Complex Parameters #371

Merged
merged 17 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Registers a parameter as a complex parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class ComplexParameterAttribute : Attribute
Cenngo marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Gets the parameter array of the constructor method that should be prioritized.
/// </summary>
public Type[] PrioritizedCtorSignature { get; }

/// <summary>
/// Registers a slash command parameter as a complex parameter.
/// </summary>
public ComplexParameterAttribute() { }

/// <summary>
/// Registers a slash command parameter as a complex parameter with a specified constructor signature.
/// </summary>
/// <param name="types">Type array of the preferred constructor parameters.</param>
public ComplexParameterAttribute(Type[] types)
{
PrioritizedCtorSignature = types;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Tag a type constructor as the preferred Complex command constructor.
/// </summary>
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)]
public class ComplexParameterCtorAttribute : Attribute { }
}
57 changes: 56 additions & 1 deletion src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ private static void BuildSlashParameter (SlashCommandParameterBuilder builder, P
builder.Description = paramInfo.Name;
builder.IsRequired = !paramInfo.IsOptional;
builder.DefaultValue = paramInfo.DefaultValue;
builder.SetParameterType(paramType, services);

foreach (var attribute in attributes)
{
Expand Down Expand Up @@ -388,12 +387,32 @@ private static void BuildSlashParameter (SlashCommandParameterBuilder builder, P
case MinValueAttribute minValue:
builder.MinValue = minValue.Value;
break;
case ComplexParameterAttribute complexParameter:
{
builder.IsComplexParameter = true;
ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter);

foreach (var parameter in ctor.GetParameters())
{
if (parameter.IsDefined(typeof(ComplexParameterAttribute)))
throw new InvalidOperationException("You cannot create nested complex parameters.");

builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services));
}

var initializer = builder.Command.Module.InteractionService._useCompiledLambda ?
ReflectionUtils<object>.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke;
builder.ComplexParameterInitializer = args => initializer(args);
}
break;
default:
builder.AddAttributes(attribute);
break;
}
}

builder.SetParameterType(paramType, services);

// Replace pascal casings with '-'
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}
Expand Down Expand Up @@ -465,5 +484,41 @@ private static bool IsValidAutocompleteCommandDefinition (MethodInfo methodInfo)
!methodInfo.IsGenericMethod &&
methodInfo.GetParameters().Length == 0;
}

private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
{
var ctors = typeInfo.GetConstructors();

if (ctors.Length == 0)
throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\".");

if (complexParameter.PrioritizedCtorSignature is not null)
{
var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature);

if (ctor is null)
throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}");

return ctor;
}

var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true));

switch (prioritizedCtors.Count())
{
case > 1:
throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type.");
case 1:
return prioritizedCtors.First();
}

switch (ctors.Length)
{
case > 1:
throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\".");
default:
return ctors.First();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Interactions.Builders
{
Expand All @@ -10,6 +11,7 @@ public sealed class SlashCommandParameterBuilder : ParameterBuilder<SlashCommand
{
private readonly List<ParameterChoice> _choices = new();
private readonly List<ChannelType> _channelTypes = new();
private readonly List<SlashCommandParameterBuilder> _complexParameterFields = new();

/// <summary>
/// Gets or sets the description of this parameter.
Expand All @@ -36,6 +38,11 @@ public sealed class SlashCommandParameterBuilder : ParameterBuilder<SlashCommand
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes;

/// <summary>
/// Gets the constructor parameters of this parameter, if <see cref="IsComplexParameter"/> is <see langword="true"/>.
/// </summary>
public IReadOnlyCollection<SlashCommandParameterBuilder> ComplexParameterFields => _complexParameterFields;

/// <summary>
/// Gets or sets whether this parameter should be configured for Autocomplete Interactions.
/// </summary>
Expand All @@ -46,6 +53,16 @@ public sealed class SlashCommandParameterBuilder : ParameterBuilder<SlashCommand
/// </summary>
public TypeConverter TypeConverter { get; private set; }

/// <summary>
/// Gets whether this type should be treated as a complex parameter.
/// </summary>
public bool IsComplexParameter { get; internal set; }

/// <summary>
/// Gets the initializer delegate for this parameter, if <see cref="IsComplexParameter"/> is <see langword="true"/>.
/// </summary>
public ComplexParameterInitializer ComplexParameterInitializer { get; internal set; }

/// <summary>
/// Gets or sets the <see cref="IAutocompleteHandler"/> of this parameter.
/// </summary>
Expand All @@ -60,7 +77,14 @@ internal SlashCommandParameterBuilder(ICommandBuilder command) : base(command) {
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }
public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type, ComplexParameterInitializer complexParameterInitializer = null)
: base(command, name, type)
{
ComplexParameterInitializer = complexParameterInitializer;

if (complexParameterInitializer is not null)
IsComplexParameter = true;
}

/// <summary>
/// Sets <see cref="Description"/>.
Expand Down Expand Up @@ -168,7 +192,47 @@ public SlashCommandParameterBuilder WithAutocompleteHandler(Type autocompleteHan
public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null)
{
base.SetParameterType(type);
TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);

if(!IsComplexParameter)
TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);

return this;
}

/// <summary>
/// Adds a parameter builders to <see cref="ComplexParameterFields"/>.
/// </summary>
/// <param name="configure"><see cref="SlashCommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the added field has a <see cref="ComplexParameterAttribute"/>.</exception>
public SlashCommandParameterBuilder AddComplexParameterField(Action<SlashCommandParameterBuilder> configure)
{
SlashCommandParameterBuilder builder = new(Command);
configure(builder);

if(builder.IsComplexParameter)
throw new InvalidOperationException("You cannot create nested complex parameters.");

_complexParameterFields.Add(builder);
return this;
}

/// <summary>
/// Adds parameter builders to <see cref="ComplexParameterFields"/>.
/// </summary>
/// <param name="fields">New parameter builders to be added to <see cref="ComplexParameterFields"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the added field has a <see cref="ComplexParameterAttribute"/>.</exception>
public SlashCommandParameterBuilder AddComplexParameterFields(params SlashCommandParameterBuilder[] fields)
{
if(fields.Any(x => x.IsComplexParameter))
throw new InvalidOperationException("You cannot create nested complex parameters.");

_complexParameterFields.AddRange(fields);
return this;
}

Expand Down
3 changes: 3 additions & 0 deletions src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public abstract class CommandInfo<TParameter> : ICommandInfo where TParameter :
private readonly ExecuteCallback _action;
private readonly ILookup<string, PreconditionAttribute> _groupedPreconditions;

internal IReadOnlyDictionary<string, TParameter> _parameterDictionary { get; }

/// <inheritdoc/>
public ModuleInfo Module { get; }

Expand Down Expand Up @@ -78,6 +80,7 @@ internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, Intera

_action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
_parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary();
}

/// <inheritdoc/>
Expand Down
Loading