* 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>pull/2585/head
| @@ -1,5 +1,6 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Reflection; | |||||
| namespace Discord.Interactions.Builders | namespace Discord.Interactions.Builders | ||||
| { | { | ||||
| @@ -38,6 +39,11 @@ namespace Discord.Interactions.Builders | |||||
| /// </summary> | /// </summary> | ||||
| Type Type { get; } | Type Type { get; } | ||||
| /// <summary> | |||||
| /// Get the <see cref="PropertyInfo"/> of this component's property. | |||||
| /// </summary> | |||||
| PropertyInfo PropertyInfo { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | /// Get the <see cref="ComponentTypeConverter"/> assigned to this input. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1,5 +1,6 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Reflection; | |||||
| namespace Discord.Interactions.Builders | namespace Discord.Interactions.Builders | ||||
| { | { | ||||
| @@ -33,6 +34,9 @@ namespace Discord.Interactions.Builders | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Type Type { get; private set; } | public Type Type { get; private set; } | ||||
| /// <inheritdoc/> | |||||
| public PropertyInfo PropertyInfo { get; internal set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ComponentTypeConverter TypeConverter { get; private set; } | public ComponentTypeConverter TypeConverter { get; private set; } | ||||
| @@ -37,6 +37,8 @@ namespace Discord.Interactions.Builders | |||||
| if (!typeof(IModal).IsAssignableFrom(type)) | if (!typeof(IModal).IsAssignableFrom(type)) | ||||
| throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); | ||||
| Type = type; | |||||
| _interactionService = interactionService; | _interactionService = interactionService; | ||||
| _components = new(); | _components = new(); | ||||
| } | } | ||||
| @@ -596,6 +596,7 @@ namespace Discord.Interactions.Builders | |||||
| builder.Label = propertyInfo.Name; | builder.Label = propertyInfo.Name; | ||||
| builder.DefaultValue = defaultValue; | builder.DefaultValue = defaultValue; | ||||
| builder.WithType(propertyInfo.PropertyType); | builder.WithType(propertyInfo.PropertyType); | ||||
| builder.PropertyInfo = propertyInfo; | |||||
| foreach(var attribute in attributes) | foreach(var attribute in attributes) | ||||
| { | { | ||||
| @@ -44,6 +44,44 @@ namespace Discord.Interactions | |||||
| await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); | 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) | 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); | var builder = new ModalBuilder(modalInfo.Title, customId); | ||||
| @@ -1,6 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Reflection; | |||||
| namespace Discord.Interactions | namespace Discord.Interactions | ||||
| { | { | ||||
| @@ -9,6 +10,10 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public abstract class InputComponentInfo | public abstract class InputComponentInfo | ||||
| { | { | ||||
| private Lazy<Func<object, object>> _getter; | |||||
| internal Func<object, object> Getter => _getter.Value; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the parent modal of this component. | /// Gets the parent modal of this component. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -39,6 +44,11 @@ namespace Discord.Interactions | |||||
| /// </summary> | /// </summary> | ||||
| public Type Type { get; } | public Type Type { get; } | ||||
| /// <summary> | |||||
| /// Gets the property linked to this component. | |||||
| /// </summary> | |||||
| public PropertyInfo PropertyInfo { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | /// Gets the <see cref="ComponentTypeConverter"/> assigned to this component. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -62,9 +72,12 @@ namespace Discord.Interactions | |||||
| IsRequired = builder.IsRequired; | IsRequired = builder.IsRequired; | ||||
| ComponentType = builder.ComponentType; | ComponentType = builder.ComponentType; | ||||
| Type = builder.Type; | Type = builder.Type; | ||||
| PropertyInfo = builder.PropertyInfo; | |||||
| TypeConverter = builder.TypeConverter; | TypeConverter = builder.TypeConverter; | ||||
| DefaultValue = builder.DefaultValue; | DefaultValue = builder.DefaultValue; | ||||
| Attributes = builder.Attributes.ToImmutableArray(); | Attributes = builder.Attributes.ToImmutableArray(); | ||||
| _getter = new(() => ReflectionUtils<object>.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -10,7 +10,6 @@ namespace Discord.Interactions | |||||
| internal static class ReflectionUtils<T> | internal static class ReflectionUtils<T> | ||||
| { | { | ||||
| private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); | private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); | ||||
| internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) => | internal static T CreateObject (TypeInfo typeInfo, InteractionService commandService, IServiceProvider services = null) => | ||||
| CreateBuilder(typeInfo, commandService)(services); | CreateBuilder(typeInfo, commandService)(services); | ||||
| @@ -166,6 +165,21 @@ namespace Discord.Interactions | |||||
| return Expression.Lambda<Action<T, object>>(assign, instanceParam, valueParam).Compile(); | 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) | internal static Func<object[], object[], T> CreateLambdaMemberInit(TypeInfo typeInfo, ConstructorInfo constructor, Predicate<PropertyInfo> propertySelect = null) | ||||
| { | { | ||||
| propertySelect ??= x => true; | propertySelect ??= x => true; | ||||