io.github.freya022.botcommands.api.parameters.Resolvers.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of BotCommands Show documentation
Show all versions of BotCommands Show documentation
A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.
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)
}
}