Skip to content

Commit

Permalink
Move to extension system and add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Goldmensch committed Jan 16, 2025
1 parent 7744998 commit 7cb9fd5
Show file tree
Hide file tree
Showing 31 changed files with 295 additions and 173 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.github.kaktushose.jda.commands.guice;

import com.github.kaktushose.jda.commands.JDACommandsBuilder;
import com.github.kaktushose.jda.commands.dispatching.instance.InstanceProvider;
import com.github.kaktushose.jda.commands.extension.Extension;
import com.github.kaktushose.jda.commands.guice.internal.GuiceInstanceProvider;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.jetbrains.annotations.NotNull;

/// The implementation of [Extension] for using Google's [Guice] as an [InstanceProvider].
///
/// @see GuiceExtensionData
public class GuiceExtension implements Extension<GuiceExtensionData> {

@Override
public void configure(@NotNull JDACommandsBuilder builder, GuiceExtensionData data) {
Injector injector = data != null
? data.providedInjector()
: Guice.createInjector();

builder.instanceProvider(new GuiceInstanceProvider(injector, false));
}

@Override
public @NotNull Class<GuiceExtensionData> dataType() {
return GuiceExtensionData.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.kaktushose.jda.commands.guice;

import com.github.kaktushose.jda.commands.extension.Extension;
import com.google.inject.Injector;
import org.jetbrains.annotations.NotNull;

/// Custom [Extension.Data] to be used to configure this extension.
/// @param providedInjector The [Injector] to be used instead of creating one
public record GuiceExtensionData(
@NotNull
Injector providedInjector
) implements Extension.Data {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.kaktushose.jda.commands.guice.internal;

import com.github.kaktushose.jda.commands.annotations.interactions.Interaction;
import com.github.kaktushose.jda.commands.dispatching.instance.InstanceProvider;
import com.google.inject.Injector;

public class GuiceInstanceProvider implements InstanceProvider {

private final Injector injector;
private final boolean runtimeBound;

public GuiceInstanceProvider(Injector injector, boolean runtimeBound) {
this.injector = injector;
this.runtimeBound = runtimeBound;
}

@Override
public <T> T instance(Class<T> clazz, Context context) {
if (!runtimeBound) {
throw new UnsupportedOperationException("GuiceInstanceProvider must be used runtime bound!");
}

return injector.getInstance(clazz);
}

/// Creates a new child injector with its own [JDACommandsModule] for each runtime.
/// This has the effect, that each class annotated with [Interaction] will be treated as a runtime scoped singleton.
@Override
public InstanceProvider forRuntime(String id) {
return new GuiceInstanceProvider(injector.createChildInjector(new JDACommandsModule()), true);
}
}

This file was deleted.

This file was deleted.

4 changes: 1 addition & 3 deletions guice/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import com.github.kaktushose.jda.commands.guice.internal.GuiceInstantiatorProvider;

module jda.commands.guice {
requires transitive jda.commands;
//noinspection requires-transitive-automatic -- must be
Expand All @@ -8,5 +6,5 @@

exports com.github.kaktushose.jda.commands.guice;

provides com.github.kaktushose.jda.commands.dispatching.instantiation.spi.InstantiatorProvider with GuiceInstantiatorProvider;
provides com.github.kaktushose.jda.commands.extension.Extension with com.github.kaktushose.jda.commands.guice.GuiceExtension;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.github.kaktushose.jda.commands.guice.GuiceExtension
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
import com.github.kaktushose.jda.commands.definitions.interactions.InteractionRegistry;
import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition;
import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.SelectMenuDefinition;
import com.github.kaktushose.jda.commands.dispatching.instantiation.Instantiator;
import com.github.kaktushose.jda.commands.dispatching.DispatchingContext;
import com.github.kaktushose.jda.commands.dispatching.JDAEventListener;
import com.github.kaktushose.jda.commands.dispatching.adapter.internal.TypeAdapters;
import com.github.kaktushose.jda.commands.dispatching.expiration.ExpirationStrategy;
import com.github.kaktushose.jda.commands.dispatching.handling.DispatchingContext;
import com.github.kaktushose.jda.commands.dispatching.instance.InstanceProvider;
import com.github.kaktushose.jda.commands.dispatching.middleware.internal.Middlewares;
import com.github.kaktushose.jda.commands.embeds.error.ErrorMessageFactory;
import com.github.kaktushose.jda.commands.internal.JDAContext;
Expand Down Expand Up @@ -48,12 +48,12 @@ public final class JDACommands {
ErrorMessageFactory errorMessageFactory,
GuildScopeProvider guildScopeProvider,
InteractionRegistry interactionRegistry,
Instantiator instantiator,
InstanceProvider instanceProvider,
InteractionDefinition.ReplyConfig globalReplyConfig) {
this.jdaContext = jdaContext;
this.interactionRegistry = interactionRegistry;
this.updater = new SlashCommandUpdater(jdaContext, guildScopeProvider, interactionRegistry);
this.jdaEventListener = new JDAEventListener(new DispatchingContext(middlewares, errorMessageFactory, interactionRegistry, typeAdapters, expirationStrategy, instantiator, globalReplyConfig));
this.jdaEventListener = new JDAEventListener(new DispatchingContext(middlewares, errorMessageFactory, interactionRegistry, typeAdapters, expirationStrategy, instanceProvider, globalReplyConfig));
}

JDACommands start(Collection<ClassFinder> classFinders, Class<?> clazz, String[] packages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapter;
import com.github.kaktushose.jda.commands.dispatching.adapter.internal.TypeAdapters;
import com.github.kaktushose.jda.commands.dispatching.expiration.ExpirationStrategy;
import com.github.kaktushose.jda.commands.dispatching.instantiation.Instantiator;
import com.github.kaktushose.jda.commands.dispatching.instantiation.spi.InstantiatorProvider;
import com.github.kaktushose.jda.commands.dispatching.instance.InstanceProvider;
import com.github.kaktushose.jda.commands.dispatching.middleware.Middleware;
import com.github.kaktushose.jda.commands.dispatching.middleware.Priority;
import com.github.kaktushose.jda.commands.dispatching.middleware.internal.Middlewares;
import com.github.kaktushose.jda.commands.dispatching.validation.Validator;
import com.github.kaktushose.jda.commands.dispatching.validation.internal.Validators;
import com.github.kaktushose.jda.commands.embeds.error.DefaultErrorMessageFactory;
import com.github.kaktushose.jda.commands.embeds.error.ErrorMessageFactory;
import com.github.kaktushose.jda.commands.extension.Extension;
import com.github.kaktushose.jda.commands.internal.JDAContext;
import com.github.kaktushose.jda.commands.permissions.DefaultPermissionsProvider;
import com.github.kaktushose.jda.commands.permissions.PermissionsProvider;
Expand All @@ -25,16 +25,29 @@
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction;
import net.dv8tion.jda.api.interactions.commands.localization.ResourceBundleLocalizationFunction;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;

/// This builder is used to build instances of [JDACommands].
///
/// Please note that values that can be set have a default implementation;
/// These default implementations are sometimes bases on reflections. If you want to avoid reflections, you have to provide your own implementations for:
/// These default implementations are sometimes based on reflections. If you want to avoid reflections, you have to provide your own implementations for:
/// - [#descriptor(com.github.kaktushose.jda.commands.definitions.description.Descriptor)]
/// - [#classFinders(ClassFinder...)]
/// - [#instanceProvider(InstanceProvider)]
///
///
/// In addition to manually configuring this builder, you can also provide implementations of [Extension] trough java's [`service
/// provider interface`][ServiceLoader], which are then applied by [#start()] or [#applyExtensions(FilterStrategy, String...)].
///
/// These implementations of [Extension] can be additionally configured by adding the according implementations of [Extension.Data]
/// by calling [#extensionData(Extension.Data...)]. (if supported by the extension)
///
///
/// ## Example
/// ```java
Expand All @@ -44,15 +57,15 @@
/// .classFinders(ClassFinder.reflective(Main.class), ClassFinders.explicit(ButtonInteraction.class))
/// .start();
/// ```
///
/// @see Extension
public class JDACommandsBuilder {
private static final Logger log = LoggerFactory.getLogger(JDACommandsBuilder.class);
private final Class<?> baseClass;
private final String[] packages;
private final JDAContext context;

private LocalizationFunction localizationFunction = ResourceBundleLocalizationFunction.empty().build();
private Instantiator instantiator = null;
private InstantiatorProvider.Data instatiatorProviderData = null;
private InstanceProvider instanceProvider = null;

private ExpirationStrategy expirationStrategy = ExpirationStrategy.AFTER_15_MINUTES;

Expand All @@ -65,6 +78,9 @@ public class JDACommandsBuilder {

private ReplyConfig globalReplyConfig = new ReplyConfig();

private final Map<Class<? extends Extension.Data>, Extension.Data> extensionData = new HashMap<>();
private boolean extensionsAlreadyApplied = false;

// registries
private final Map<Class<?>, TypeAdapter<?>> typeAdapters = new HashMap<>();
private final Set<Map.Entry<Priority, Middleware>> middlewares = new HashSet<>();
Expand Down Expand Up @@ -105,14 +121,9 @@ public JDACommandsBuilder localizationFunction(@NotNull LocalizationFunction loc
return this;
}

/// @param instantiator The [Instantiator] to be used to instantiate interaction classes
public JDACommandsBuilder instantiator(Instantiator instantiator) {
this.instantiator = instantiator;
return this;
}

public JDACommandsBuilder instantiatorProviderData(InstantiatorProvider.Data instatiatorProviderData) {
this.instatiatorProviderData = instatiatorProviderData;
/// @param instanceProvider the implementation of [InstanceProvider] to be used.
public JDACommandsBuilder instanceProvider(InstanceProvider instanceProvider) {
this.instanceProvider = instanceProvider;
return this;
}

Expand Down Expand Up @@ -184,12 +195,68 @@ public JDACommandsBuilder globalReplyConfig(@NotNull ReplyConfig globalReplyConf
return this;
}

/// This method instantiates an instance of [JDACommands] and starts the framework.
/// Registers instances of implementations of [Extension.Data] to be used by the according implementation
/// of [Extension] to configure it properly.
///
/// @param data the instances of [Extension.Data] to be used
public JDACommandsBuilder extensionData(Extension.Data... data) {
for (Extension.Data entity : data) {
extensionData.put(entity.getClass(), entity);
}
return this;
}

/// After filtering the by SPI registered implementations of [Extension] according to the chosen [FilterStrategy],
/// this method loads all implementations of [Extension] by executing [Extension#configure(JDACommandsBuilder, Extension.Data)].
///
/// If this method is not called explicitly all [Extension]s will be loaded by [#start()].
///
/// This methods should only be used if you want to filter some implementations of [Extension] or if you want to override
/// some values set by any [Extension#configure(JDACommandsBuilder, Extension.Data)].
///
/// @apiNote This method compares the [`fully classified class name`][Class#getName()] of all [Extension] implementations by using [String#startsWith(String)],
/// so it's possible to include/exclude a bunch of classes in the same package by just providing the package name.
///
/// @param strategy the filtering strategy to be used either [FilterStrategy#INCLUDE] or [FilterStrategy#EXCLUDE]
/// @param classes the classes to be filtered
@SuppressWarnings("unchecked")
public JDACommandsBuilder applyExtensions(FilterStrategy strategy, String... classes) {
ServiceLoader.load(Extension.class)
.stream()
.peek(provider -> log.debug("Found extension: {}", provider.type()))
.filter(provider -> {
Stream<String> filterStream = Arrays.stream(classes);
Predicate<String> startsWith = s -> provider.type().getName().startsWith(s);

return switch (strategy) {
case INCLUDE -> filterStream.anyMatch(startsWith);
case EXCLUDE -> filterStream.noneMatch(startsWith);
};
})
.peek(provider -> log.debug("Using extension {}", provider.type()))
.map(ServiceLoader.Provider::get)
.forEach(extension -> extension.configure(this, extensionData.get(extension.dataType())));

extensionsAlreadyApplied = true;
return this;
}

/// The two available filter strategies
public enum FilterStrategy {
/// includes the defined classes
INCLUDE,
/// excludes the defined classes
EXCLUDE
}

/// This method applies all found implementations of [Extension] (if [#applyExtensions(FilterStrategy, String...)] isn't called explicitly),
/// instantiates an instance of [JDACommands] and starts the framework.
@NotNull
public JDACommands start() {
if (instantiator == null) {
instantiator = findDefaultInstantiator();
}
// exclude no packages -> apply all
if (!extensionsAlreadyApplied) applyExtensions(FilterStrategy.EXCLUDE);

validateConfiguration();

JDACommands jdaCommands = new JDACommands(
context,
Expand All @@ -199,21 +266,21 @@ public JDACommands start() {
errorMessageFactory,
guildScopeProvider,
new InteractionRegistry(new Validators(this.validators), localizationFunction, descriptor),
instantiator,
instanceProvider,
globalReplyConfig
);

return jdaCommands.start(classFinders, baseClass, packages);
}

private Instantiator findDefaultInstantiator() {
return ServiceLoader.load(InstantiatorProvider.class)
.stream()
.map(ServiceLoader.Provider::get)
.max(Comparator.comparingInt(InstantiatorProvider::priority))
.orElseThrow(() -> new IllegalStateException(
"No InstantiatorProvider was found! Please use a default integration provided by jda-commands like the guice integration or write your own.")
)
.create(instatiatorProviderData);
private void validateConfiguration() {
if (instanceProvider == null) throw new ConfigurationException("An implementation of com.github.kaktushose.jda.commands.dispatching.instantiation.InstanceProvider must be set!");
}

/// Will be thrown if anything goes wrong while configuring jda-commands.
public static class ConfigurationException extends RuntimeException {
public ConfigurationException(String message) {
super("Error while trying to configure jda-commands: " + message);
}
}
}
Loading

0 comments on commit 7cb9fd5

Please sign in to comment.