All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.freya022.botcommands.api.parameters.Resolvers.kt Maven / Gradle / Ivy

Go to download

A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.

There is a newer version: 3.0.0-alpha.18
Show newest version
package io.github.freya022.botcommands.api.parameters

import io.github.freya022.botcommands.api.commands.application.slash.annotations.SlashOption
import io.github.freya022.botcommands.api.core.config.BApplicationConfigBuilder
import io.github.freya022.botcommands.api.core.service.annotations.Resolver
import io.github.freya022.botcommands.api.core.service.annotations.ResolverFactory
import io.github.freya022.botcommands.api.core.utils.enumSetOf
import io.github.freya022.botcommands.api.parameters.Resolvers.toHumanName
import io.github.freya022.botcommands.api.parameters.resolvers.ComponentParameterResolver
import io.github.freya022.botcommands.api.parameters.resolvers.SlashParameterResolver
import io.github.freya022.botcommands.api.parameters.resolvers.TimeoutParameterResolver
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction
import java.util.*
import javax.annotation.CheckReturnValue

/**
 * Utility factories to create commonly used parameter resolvers.
 */
object Resolvers {
    /**
     * Creates an enum resolver for [slash][SlashParameterResolver] commands,
     * as well as [component data][ComponentParameterResolver] and [timeout data][TimeoutParameterResolver].
     *
     * ### Text command support
     * To add support for text command options,
     * you have to use [EnumResolverBuilder.withTextSupport].
     *
     * ### Using choices
     *
     * You have to enable [SlashOption.usePredefinedChoices] for the choices to appear on your slash command option.
     *
     * ### Registration
     *
     * The created resolver needs to be registered as a service factory, with [@Resolver][Resolver], for example:
     *
     * ```java
     * @BConfiguration
     * public class EnumResolvers {
     *     // Resolver for DAYS/HOURS/MINUTES (and SECONDS in the test guild), where the displayed name is given by 'Resolvers#toHumanName'
     *     @Resolver
     *     public ParameterResolver timeUnitResolver() {
     *         return Resolvers.enumResolver(
     *             TimeUnit.class,
     *             guild -> Utils.isTestGuild(guild)
     *                 ? EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS)
     *                 : EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES)
     *         );
     *     }
     *
     *     ...other resolvers...
     * }
     * ```
     *
     * ### Localization
     *
     * The choices are localized automatically by using the bundles defined by [BApplicationConfigBuilder.addLocalizations],
     * using a path similar to **my.command.path**.options.**my_option**.choices.**choice_name**.name,
     * as required by [LocalizationFunction].
     *
     * The choice name is produced by [the name function][EnumResolverBuilder.nameFunction],
     * and is then lowercase with spaces modified to underscore by [LocalizationFunction].
     *
     * For example, using the [default name function][toHumanName]:
     *
     * 1. `MY_ENUM_VALUE` (Raw enum name)
     * 2. `My enum value` (Choice name displayed on Discord)
     * 3. `my_enum_value` (Choice name in your localization file)
     *
     * @param e                   The enum type
     * @param guildValuesSupplier Retrieves the values used for slash command choices, for each [Guild]
     *
     * @see toHumanName
     */
    @JvmStatic
    @CheckReturnValue
    fun > enumResolver(e: Class, guildValuesSupplier: EnumValuesSupplier): EnumResolverBuilder {
        return EnumResolverBuilder(e, guildValuesSupplier)
    }

    /**
     * Creates an enum resolver for [slash][SlashParameterResolver] commands,
     * as well as [component data][ComponentParameterResolver] and [timeout data][TimeoutParameterResolver].
     *
     * ### Text command support
     * To add support for text command options,
     * you have to use [EnumResolverBuilder.withTextSupport][EnumResolverBuilder.withTextSupport].
     *
     * ### Using choices
     *
     * You have to enable [SlashOption.usePredefinedChoices] for the choices to appear on your slash command option.
     *
     * ### Registration
     *
     * The created resolver needs to be registered as a service factory, with [@Resolver][Resolver], for example:
     *
     * ```java
     * @BConfiguration
     * public class EnumResolvers {
     *     // Resolver for DAYS/HOURS/MINUTES, where the displayed name is given by 'Resolvers#toHumanName'
     *     @Resolver
     *     public ParameterResolver timeUnitResolver() {
     *         return Resolvers.enumResolver(TimeUnit.class, EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES)).build();
     *     }
     *
     *     ...other resolvers...
     * }
     * ```
     *
     * ### Localization
     *
     * The choices are localized automatically by using the bundles defined by [BApplicationConfigBuilder.addLocalizations],
     * using a path similar to **my.command.path**.options.**my_option**.choices.**choice_name**.name,
     * as required by [LocalizationFunction].
     *
     * The choice name is produced by [the name function][EnumResolverBuilder.nameFunction],
     * and is then lowercase with spaces modified to underscore by [LocalizationFunction].
     *
     * For example, using the [default name function][toHumanName]:
     *
     * 1. `MY_ENUM_VALUE` (Raw enum name)
     * 2. `My enum value` (Choice name displayed on Discord)
     * 3. `my_enum_value` (Choice name in your localization file)
     *
     * @param e      The enum type
     * @param values The values used for slash command choices
     *
     * @see toHumanName
     */
    @JvmStatic
    @JvmOverloads
    @CheckReturnValue
    fun > enumResolver(e: Class, values: Collection = EnumSet.allOf(e)): EnumResolverBuilder {
        return EnumResolverBuilder(e, guildValuesSupplier = { values })
    }

    /**
     * Convert an enum to a more human-friendly name.
     *
     * This takes the enum value's name and capitalizes it, while replacing underscores with spaces, for example,
     * `MY_ENUM_VALUE` -> `Enum value name`.
     */
    @JvmStatic
    @JvmOverloads
    fun toHumanName(value: Enum<*>, locale: Locale = Locale.ROOT): String {
        return value.name.lowercase(locale)
            .replace('_', ' ')
            .replaceFirstChar { it.uppercaseChar() }
    }
}

/**
 * Creates an enum resolver for [slash][SlashParameterResolver] commands,
 * as well as [component data][ComponentParameterResolver] and [timeout data][TimeoutParameterResolver].
 *
 * ### Text command support
 * To add support for text command options,
 * you have to use [EnumResolverBuilder.withTextSupport] in the configuration [block].
 *
 * ### Using choices
 *
 * You have to enable [SlashOption.usePredefinedChoices] for the choices to appear on your slash command option.
 *
 * ### Registration
 *
 * The created resolver needs to be registered as a service factory, with [@Resolver][Resolver], for example:
 *
 * ```kt
 * @BConfiguration
 * object EnumResolvers {
 *     // Resolver for DAYS/HOURS/MINUTES, where the displayed name is given by 'Resolvers.Enum#toHumanName'
 *     @Resolver
 *     fun timeUnitResolver() = enumResolver(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES)
 *
 *     ...other resolvers...
 * }
 * ```
 *
 * ### Localization
 *
 * The choices are localized automatically by using the bundles defined by [BApplicationConfigBuilder.addLocalizations],
 * using a path similar to **my.command.path**.options.**my_option**.choices.**choice_name**.name,
 * as required by [LocalizationFunction].
 *
 * The choice name is produced by [the name function][nameFunction],
 * and is then lowercase with spaces modified to underscore by [LocalizationFunction].
 *
 * For example, using the [default name function][toHumanName]:
 *
 * 1. `MY_ENUM_VALUE` (Raw enum name)
 * 2. `My enum value` (Choice name displayed on Discord)
 * 3. `my_enum_value` (Choice name in your localization file)
 *
 * @param E            The enum type
 * @param nameFunction Retrieves a human friendly name for the enum value, defaults to [toHumanName]
 *
 * @see toHumanName
 */
inline fun > enumResolver(
    vararg values: E = enumValues(),
    noinline nameFunction: (e: E) -> String = { it.toHumanName() },
    block: EnumResolverBuilder.() -> Unit = {}
): ClassParameterResolver<*, E> = Resolvers.enumResolver(E::class.java, values.toCollection(enumSetOf()))
    .nameFunction(nameFunction)
    .apply(block)
    .build()

/**
 * Creates an enum resolver for [slash][SlashParameterResolver] commands,
 * as well as [component data][ComponentParameterResolver] and [timeout data][TimeoutParameterResolver].
 *
 * ### Text command support
 * To add support for text command options,
 * you have to use [EnumResolverBuilder.withTextSupport] in the configuration [block].
 *
 * ### Using choices
 *
 * You have to enable [SlashOption.usePredefinedChoices] for the choices to appear on your slash command option.
 *
 * ### Registration
 *
 * The created resolver needs to be registered as a service factory, with [@Resolver][Resolver], for example:
 *
 * ```kt
 * @BConfiguration
 * object EnumResolvers {
 *     // Resolver for DAYS/HOURS/MINUTES (and SECONDS in the test guild), where the displayed name is given by 'Resolvers.Enum#toHumanName'
 *     @Resolver
 *     fun timeUnitResolver() = enumResolver { guild ->
 *         if (guild.isTestGuild()) {
 *             enumSetOf(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS)
 *         } else {
 *             enumSetOf(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES)
 *         }
 *     }
 *
 *     ...other resolvers...
 * }
 * ```
 *
 * ### Localization
 *
 * The choices are localized automatically by using the bundles defined by [BApplicationConfigBuilder.addLocalizations],
 * using a path similar to **my.command.path**.options.**my_option**.choices.**choice_name**.name,
 * as required by [LocalizationFunction].
 *
 * The choice name is produced by [the name function][nameFunction],
 * and is then lowercase with spaces modified to underscore by [LocalizationFunction].
 *
 * For example, using the [default name function][toHumanName]:
 *
 * 1. `MY_ENUM_VALUE` (Raw enum name)
 * 2. `My enum value` (Choice name displayed on Discord)
 * 3. `my_enum_value` (Choice name in your localization file)
 *
 * @param E                   The enum type
 * @param guildValuesSupplier Retrieves the values used for slash command choices, for each [Guild]
 * @param nameFunction        Retrieves a human friendly name for the enum value, defaults to [toHumanName]
 *
 * @see toHumanName
 */
inline fun > enumResolver(
    guildValuesSupplier: EnumValuesSupplier,
    noinline nameFunction: (e: E) -> String = { it.toHumanName() },
    block: EnumResolverBuilder.() -> Unit = {}
): ClassParameterResolver<*, E> = Resolvers.enumResolver(E::class.java, guildValuesSupplier)
    .nameFunction(nameFunction)
    .apply(block)
    .build()

/**
 * Convert an enum to a more human-friendly name.
 *
 * This takes the enum value's name and capitalizes it, while replacing underscores with spaces, for example,
 * `MY_ENUM_VALUE` -> `Enum value name`.
 */
fun Enum<*>.toHumanName(locale: Locale = Locale.ROOT): String = toHumanName(this, locale)

/**
 * Creates a [parameter resolver factory][ParameterResolverFactory] from the provided resolver [producer].
 *
 * The [producer] is called for each function parameter with the exact [R] type.
 *
 * This should be returned in a service factory, using [@ResolverFactory][ResolverFactory].
 *
 * Example using a custom localization service:
 * ```kt
 * @BConfiguration
 * object MyCustomLocalizationResolverProvider {
 *     // The parameter resolver, which will be created once per parameter
 *     class MyCustomLocalizationResolver(
 *         private val localizationService: LocalizationService,
 *         private val guildSettingsService: GuildSettingsService,
 *         private val bundleName: String,
 *         private val prefix: String?
 *     ) : ClassParameterResolver(MyCustomLocalization::class),
 *         ICustomResolver {
 *
 *         // Called when a command is used
 *         override suspend fun resolveSuspend(executable: Executable, event: Event): MyCustomLocalization {
 *             return if (event is Interaction) {
 *                 val guild = event.guild
 *                     ?: throw IllegalStateException("Cannot get localization outside of guilds")
 *                 // The root localization file not existing isn't an issue on production
 *                 val localization = localizationService.getInstance(bundleName, guildSettingsService.getGuildLocale(guild.idLong))
 *                     ?: throw IllegalArgumentException("No root bundle exists for '$bundleName'")
 *
 *                 // Return resolved object
 *                 MyCustomLocalization(localization, prefix)
 *             } else {
 *                 throw UnsupportedOperationException("Unsupported event: ${event.javaClass.simpleNestedName}")
 *             }
 *         }
 *     }
 *
 *     // Service factory returning a resolver factory
 *     // The returned factory is used on each command/handler parameter of type "MyCustomLocalization",
 *     // which is the same type as what MyCustomLocalizationResolver returns
 *     @ResolverFactory
 *     fun myCustomLocalizationResolverProvider(localizationService: LocalizationService, guildSettingsService: GuildSettingsService) = resolverFactory { parameter ->
 *         // Find @LocalizationBundle on the parameter
 *         val bundle = parameter.parameter.findAnnotation()
 *             ?: throw IllegalArgumentException("Parameter ${parameter.parameter} must be annotated with LocalizationBundle")
 *
 *         // Return our resolver for that parameter
 *         MyCustomLocalizationResolver(localizationService, guildSettingsService, bundle.value, bundle.prefix.nullIfBlank())
 *     }
 * }
 * ```
 *
 * @param priority Priority of this resolver factory, see [ParameterResolverFactory.priority]
 * @param producer Function providing a [resolver][ParameterResolver] for the provided function parameter
 * @param T Type of the produced parameter resolver
 * @param R Type of the object returned by the resolver
 *
 * @see ParameterResolverFactory
 * @see ParameterResolver
 */
inline fun , reified R : Any> resolverFactory(priority: Int = 0, crossinline producer: (request: ResolverRequest) -> T): ParameterResolverFactory {
    return object : TypedParameterResolverFactory(T::class, R::class) {
        override val priority: Int get() = priority

        override fun get(request: ResolverRequest): T = producer(request)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy