[Feature] RespondWithModal() which accepts an IModal instance as template (#2564)

* introduce overload for responding to an interaction with an instatiated IModal obj

* add inline docs to ModalInfo.PropertyInfo

* Apply suggestions from code review

Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>

---------

Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>
This commit is contained in:
Cenk Ergen
2023-02-06 13:52:16 +03:00
committed by GitHub
parent 91e208474d
commit e7bda0f8a5
7 changed files with 79 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Discord.Interactions.Builders
{
@@ -38,6 +39,11 @@ namespace Discord.Interactions.Builders
/// </summary>
Type Type { get; }
/// <summary>
/// Get the <see cref="PropertyInfo"/> of this component's property.
/// </summary>
PropertyInfo PropertyInfo { get; }
/// <summary>
/// Get the <see cref="ComponentTypeConverter"/> assigned to this input.
/// </summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Discord.Interactions.Builders
{
@@ -33,6 +34,9 @@ namespace Discord.Interactions.Builders
/// <inheritdoc/>
public Type Type { get; private set; }
/// <inheritdoc/>
public PropertyInfo PropertyInfo { get; internal set; }
/// <inheritdoc/>
public ComponentTypeConverter TypeConverter { get; private set; }

View File

@@ -37,6 +37,8 @@ namespace Discord.Interactions.Builders
if (!typeof(IModal).IsAssignableFrom(type))
throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type));
Type = type;
_interactionService = interactionService;
_components = new();
}

View File

@@ -596,6 +596,7 @@ namespace Discord.Interactions.Builders
builder.Label = propertyInfo.Name;
builder.DefaultValue = defaultValue;
builder.WithType(propertyInfo.PropertyType);
builder.PropertyInfo = propertyInfo;
foreach(var attribute in attributes)
{

View File

@@ -44,6 +44,44 @@ namespace Discord.Interactions
await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
}
/// <summary>
/// Respond to an interaction with an <see cref="IModal"/> and fills the value fields of the modal using the property values of the provided
/// instance.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="modal">The <see cref="IModal"/> instance to get field values from.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <returns></returns>
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null,
Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
var builder = new ModalBuilder(modal.Title, customId);
foreach (var input in modalInfo.Components)
switch (input)
{
case TextInputComponentInfo textComponent:
{
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
textComponent.MaxLength, textComponent.IsRequired, textComponent.Getter(modal) as string);
}
break;
default:
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}
if (modifyModal is not null)
modifyModal(builder);
await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false);
}
private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
{
var builder = new ModalBuilder(modalInfo.Title, customId);

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
namespace Discord.Interactions
{
@@ -9,6 +10,10 @@ namespace Discord.Interactions
/// </summary>
public abstract class InputComponentInfo
{
private Lazy<Func<object, object>> _getter;
internal Func<object, object> Getter => _getter.Value;
/// <summary>
/// Gets the parent modal of this component.
/// </summary>
@@ -39,6 +44,11 @@ namespace Discord.Interactions
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets the property linked to this component.
/// </summary>
public PropertyInfo PropertyInfo { get; }
/// <summary>
/// Gets the <see cref="ComponentTypeConverter"/> assigned to this component.
/// </summary>
@@ -62,9 +72,12 @@ namespace Discord.Interactions
IsRequired = builder.IsRequired;
ComponentType = builder.ComponentType;
Type = builder.Type;
PropertyInfo = builder.PropertyInfo;
TypeConverter = builder.TypeConverter;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();
_getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo));
}
}
}

View File

@@ -10,7 +10,6 @@ namespace Discord.Interactions
internal static class ReflectionUtils<T>
{
private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo();
internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) =>
CreateBuilder(typeInfo, commandService)(services);
@@ -166,6 +165,21 @@ namespace Discord.Interactions
return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile();
}
internal static Func<T, object> CreateLambdaPropertyGetter(PropertyInfo propertyInfo)
{
var instanceParam = Expression.Parameter(typeof(T), "instance");
var prop = Expression.Property(instanceParam, propertyInfo);
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile();
}
internal static Func<T, object> CreateLambdaPropertyGetter(Type type, PropertyInfo propertyInfo)
{
var instanceParam = Expression.Parameter(typeof(T), "instance");
var instanceAccess = Expression.Convert(instanceParam, type);
var prop = Expression.Property(instanceAccess, propertyInfo);
return Expression.Lambda<Func<T, object>>(prop, instanceParam).Compile();
}
internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null)
{
propertySelect ??= x => true;