Skip to content

Commit

Permalink
Feature: Component TypeConverters and CustomID TypeReaders (#2169)
Browse files Browse the repository at this point in the history
* fix sharded client current user

* add custom setter to group property of module builder

* rename serilazation method

* init

* create typemap and default typereaders

* add default readers

* create typereader targetting flags

* seperate custom id readers with component typeconverters

* add typereaders

* add customid readers

* clean up component info argument parsing

* remove obsolete method

* add component typeconverters to modals

* fix build errors

* add inline docs

* bug fixes

* code cleanup and refactorings

* fix build errors

* add GenerateCustomIdString method to interaction service

* add GenerateCustomIdString method to interaction service

* add inline docs to componentparameterbuilder

* add inline docs to GenerateCustomIdStringAsync method
  • Loading branch information
Cenngo committed Mar 9, 2022
1 parent cc6918d commit fb4250b
Show file tree
Hide file tree
Showing 34 changed files with 812 additions and 238 deletions.
Expand Up @@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>.
/// </summary>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, ComponentCommandParameterBuilder>
{
protected override ComponentCommandBuilder Instance => this;

Expand All @@ -26,9 +26,9 @@ public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInf
/// <returns>
/// The builder instance.
/// </returns>
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure)
public override ComponentCommandBuilder AddParameter (Action<ComponentCommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
var parameter = new ComponentCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
Expand Down
Expand Up @@ -38,6 +38,11 @@ public interface IInputComponentBuilder
/// </summary>
Type Type { get; }

/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
/// </summary>
ComponentTypeConverter TypeConverter { get; }

/// <summary>
/// Gets the default value of this input component.
/// </summary>
Expand Down
Expand Up @@ -33,6 +33,9 @@ public abstract class InputComponentBuilder<TInfo, TBuilder> : IInputComponentBu
/// <inheritdoc/>
public Type Type { get; private set; }

/// <inheritdoc/>
public ComponentTypeConverter TypeConverter { get; private set; }

/// <inheritdoc/>
public object DefaultValue { get; set; }

Expand Down Expand Up @@ -111,6 +114,7 @@ public TBuilder WithComponentType(ComponentType componentType)
public TBuilder WithType(Type type)
{
Type = type;
TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
return Instance;
}

Expand Down
6 changes: 4 additions & 2 deletions src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
Expand Up @@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders
/// </summary>
public class ModalBuilder
{
internal readonly InteractionService _interactionService;
internal readonly List<IInputComponentBuilder> _components;

/// <summary>
Expand All @@ -31,19 +32,20 @@ public class ModalBuilder
/// </summary>
public IReadOnlyCollection<IInputComponentBuilder> Components => _components;

internal ModalBuilder(Type type)
internal ModalBuilder(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));

_interactionService = interactionService;
_components = new();
}

/// <summary>
/// Initializes a new <see cref="ModalBuilder"/>
/// </summary>
/// <param name="modalInitializer">The initialization delegate for this modal.</param>
public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type)
public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService)
{
ModalInitializer = modalInitializer;
}
Expand Down
21 changes: 13 additions & 8 deletions src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
Expand Up @@ -231,9 +231,6 @@ static bool IsLoadableModule (TypeInfo info)
private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[])))
throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}");

var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;
Expand All @@ -260,8 +257,10 @@ static bool IsLoadableModule (TypeInfo info)

var parameters = methodInfo.GetParameters();

var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;

foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}
Expand Down Expand Up @@ -310,8 +309,8 @@ static bool IsLoadableModule (TypeInfo info)
if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1)
throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter.");

if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType)))
throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}");
if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType))
throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}");

var attributes = methodInfo.GetCustomAttributes();

Expand Down Expand Up @@ -464,6 +463,12 @@ private static void BuildSlashParameter (SlashCommandParameterBuilder builder, P
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}

private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam)
{
builder.SetIsRouteSegment(!isComponentParam);
BuildParameter(builder, paramInfo);
}

private static void BuildParameter<TInfo, TBuilder> (ParameterBuilder<TInfo, TBuilder> builder, ParameterInfo paramInfo)
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder<TInfo, TBuilder>
Expand Down Expand Up @@ -495,7 +500,7 @@ private static void BuildSlashParameter (SlashCommandParameterBuilder builder, P
#endregion

#region Modals
public static ModalInfo BuildModalInfo(Type modalType)
public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(modalType))
throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}");
Expand All @@ -504,7 +509,7 @@ public static ModalInfo BuildModalInfo(Type modalType)

try
{
var builder = new ModalBuilder(modalType)
var builder = new ModalBuilder(modalType, interactionService)
{
Title = instance.Title
};
Expand Down
@@ -0,0 +1,77 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandParameterInfo"/>.
/// </summary>
public class ComponentCommandParameterBuilder : ParameterBuilder<ComponentCommandParameterInfo, ComponentCommandParameterBuilder>
{
/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="false"/>.
/// </summary>
public ComponentTypeConverter TypeConverter { get; private set; }

/// <summary>
/// Get the <see cref="Discord.Interactions.TypeReader"/> assigned to this parameter, if <see cref="IsRouteSegmentParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; private set; }

/// <summary>
/// Gets whether this parameter is a CustomId segment or a Component value parameter.
/// </summary>
public bool IsRouteSegmentParameter { get; private set; }

/// <inheritdoc/>
protected override ComponentCommandParameterBuilder Instance => this;

internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { }

/// <summary>
/// Initializes a new <see cref="ComponentCommandParameterBuilder"/>.
/// </summary>
/// <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 ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }

/// <inheritdoc/>
public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null);

/// <summary>
/// Sets <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.
/// </summary>
/// <param name="type">New value of the <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.</param>
/// <param name="services">Service container to be used to resolve the dependencies of this parameters <see cref="Interactions.TypeConverter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services)
{
base.SetParameterType(type);

if (IsRouteSegmentParameter)
TypeReader = Command.Module.InteractionService.GetTypeReader(type);
else
TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services);

return this;
}

/// <summary>
/// Sets <see cref="IsRouteSegmentParameter"/>.
/// </summary>
/// <param name="isRouteSegment">New value of the <see cref="IsRouteSegmentParameter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment)
{
IsRouteSegmentParameter = isRouteSegment;
return this;
}

internal override ComponentCommandParameterInfo Build(ICommandInfo command)
=> new(this, command);
}
}
Expand Up @@ -20,6 +20,11 @@ public class ModalCommandParameterBuilder : ParameterBuilder<ModalCommandParamet
/// </summary>
public bool IsModalParameter => Modal is not null;

/// <summary>
/// Gets the <see cref="TypeReader"/> assigned to this parameter, if <see cref="IsModalParameter"/> is <see langword="true"/>.
/// </summary>
public TypeReader TypeReader { get; private set; }

internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { }

/// <summary>
Expand All @@ -34,7 +39,9 @@ public class ModalCommandParameterBuilder : ParameterBuilder<ModalCommandParamet
public override ModalCommandParameterBuilder SetParameterType(Type type)
{
if (typeof(IModal).IsAssignableFrom(type))
Modal = ModalUtils.GetOrAdd(type);
Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService);
else
TypeReader = Command.Module.InteractionService.GetTypeReader(type);

return base.SetParameterType(type);
}
Expand Down
12 changes: 12 additions & 0 deletions src/Discord.Net.Interactions/Entities/ITypeConverter.cs
@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
internal interface ITypeConverter<T>
{
public bool CanConvertTo(Type type);

public Task<TypeConverterResult> ReadAsync(IInteractionContext context, T option, IServiceProvider services);
}
}
Expand Up @@ -41,14 +41,7 @@ public override async Task<IResult> ExecuteAsync(IInteractionContext context, IS
if (context.Interaction is not IAutocompleteInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction");

try
{
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
}

/// <inheritdoc/>
Expand Down
35 changes: 14 additions & 21 deletions src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
Expand Up @@ -123,10 +123,7 @@ async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute
return moduleResult;

var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
if (!commandResult.IsSuccess)
return commandResult;

return PreconditionResult.FromSuccess();
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
}

protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services)
Expand All @@ -140,8 +137,8 @@ protected async Task<IResult> RunAsync(IInteractionContext context, object[] arg
using var scope = services?.CreateScope();
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
}
else
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);

return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
}
case RunMode.Async:
_ = Task.Run(async () =>
Expand Down Expand Up @@ -170,20 +167,14 @@ private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, ob
{
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false);
return preconditionResult;
}
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false);

var index = 0;
foreach (var parameter in Parameters)
{
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false);
if (!result.IsSuccess)
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}
return await InvokeEventAndReturn(context, result).ConfigureAwait(false);
}

var task = _action(context, args, services, this);
Expand All @@ -192,20 +183,16 @@ private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, ob
{
var result = await resultTask.ConfigureAwait(false);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
if (result is RuntimeResult || result is ExecuteResult)
if (result is RuntimeResult or ExecuteResult)
return result;
}
else
{
await task.ConfigureAwait(false);
var result = ExecuteResult.FromSuccess();
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false);
}

var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason");
await InvokeModuleEvent(context, failResult).ConfigureAwait(false);
return failResult;
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -234,6 +221,12 @@ private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, ob
}
}

protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result)
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}

private static bool CheckTopLevel(ModuleInfo parent)
{
var currentParent = parent;
Expand Down

0 comments on commit fb4250b

Please sign in to comment.