Skip to content

Commit

Permalink
✨ Add injection services
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Söderberg committed Jan 13, 2021
1 parent 27ab229 commit 2bb7ced
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 25 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Expose the Command which led to `InvalidCommandSenderException`s
- Expose the CommandContext which led to `CommandExecutionException`s
- Added helper methods for command flags to MutableCommandBuilder
- Added injection services
- Added a Guice injection service

### Deprecated
- Deprecated ParameterInjectorRegistry#injectors

## [1.3.0] - 2020-12-18

Expand All @@ -27,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Deprecated
- String keyed command meta
- Deprecated ParameterInjectorRegistry#injectors

### Fixed
- Fixed issue with task synchronization
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ object Versions {
const val errorprone = "2.4.0"
const val errorprone_javac = "9+181-r4173-1"

// INTEGRATION DEPENDENCIES
const val guice = "4.2.3"

// DISCORD DEPENDENCIES
const val javacord = "3.1.1"
const val jda = "4.2.0_209"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
//
package cloud.commandframework.annotations;

import cloud.commandframework.annotations.injection.ParameterInjector;
import cloud.commandframework.annotations.injection.ParameterInjectorRegistry;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.FlagContext;
Expand All @@ -37,9 +36,9 @@
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;

class MethodCommandExecutionHandler<C> implements CommandExecutionHandler<C> {

Expand Down Expand Up @@ -90,19 +89,13 @@ public void execute(final @NonNull CommandContext<C> commandContext) {
if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) {
arguments.add(commandContext.getSender());
} else {
final Collection<ParameterInjector<C, ?>> injectors = this.injectorRegistry.injectors(parameter.getType());
Object value = null;
for (final ParameterInjector<C, ?> injector : injectors) {
value = injector.create(
commandContext,
this.annotationAccessor
);
if (value != null) {
break;
}
}
if (value != null) {
arguments.add(value);
final Optional<?> value = this.injectorRegistry.getInjectable(
parameter.getType(),
commandContext,
this.annotationAccessor
);
if (value.isPresent()) {
arguments.add(value.get());
} else {
throw new IllegalArgumentException(String.format(
"Unknown command parameter '%s' in method '%s'",
Expand Down
1 change: 1 addition & 0 deletions cloud-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dependencies {
api(project(":cloud-services"))
implementation("com.google.inject", "guice", Versions.guice)
testImplementation("org.openjdk.jmh", "jmh-core", Versions.jmh)
testImplementation("org.openjdk.jmh", "jmh-generator-annprocess", Versions.jmh)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package cloud.commandframework.annotations.injection;

import cloud.commandframework.annotations.AnnotationAccessor;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.types.tuples.Triplet;
import com.google.inject.ConfigurationException;
import com.google.inject.Injector;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* {@link InjectionService Injection service} that injects using a Guice {@link Injector}
*
* @param <C> Command sender type
* @since 1.4.0
*/
public final class GuiceInjectionService<C> implements InjectionService<C> {

private final Injector injector;

private GuiceInjectionService(final @NonNull Injector injector) {
this.injector = injector;
}

/**
* Create a new Guice injection service that wraps the given injector
*
* @param injector Injector to wrap
* @param <C> Command sender type
* @return the created injection service
*/
public static <C> GuiceInjectionService<C> create(final @NonNull Injector injector) {
return new GuiceInjectionService<>(injector);
}

@Override
@SuppressWarnings("EmptyCatch")
public @Nullable Object handle(final @NonNull Triplet<CommandContext<C>, Class<?>, AnnotationAccessor> triplet) {
try {
return this.injector.getInstance(triplet.getSecond());
} catch (final ConfigurationException ignored) {
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package cloud.commandframework.annotations.injection;

import cloud.commandframework.annotations.AnnotationAccessor;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.services.types.Service;
import cloud.commandframework.types.tuples.Triplet;

/**
* Service that can be registered to the {@link ParameterInjectorRegistry} in order to provide
* custom injection support. This can be used to integrate the Cloud with existing dependency
* injection frameworks.
*
* @param <C> Command sender type
* @since 1.4.0
*/
@FunctionalInterface
public interface InjectionService<C> extends
Service<Triplet<CommandContext<C>, Class<?>, AnnotationAccessor>, Object> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
//
package cloud.commandframework.annotations.injection;

import cloud.commandframework.annotations.AnnotationAccessor;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.services.ServicePipeline;
import cloud.commandframework.types.tuples.Triplet;
import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -32,17 +38,28 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* Registry containing mappings between {@link Class classes} and {@link ParameterInjector injectors}
*
* @param <C> Command sender type
* @since 1.2.0
*/
public final class ParameterInjectorRegistry<C> {
@SuppressWarnings("ALL")
public final class ParameterInjectorRegistry<C> implements InjectionService<C> {

private volatile int injectorCount = 0;
private final Map<Class<?>, List<ParameterInjector<C, ?>>> injectors = new HashMap<>();
private final ServicePipeline servicePipeline = ServicePipeline.builder().build();

/**
* Create a new parameter injector registry
*/
public ParameterInjectorRegistry() {
servicePipeline.registerServiceType(new TypeToken<InjectionService<C>>() {
}, this);
}

/**
* Register an injector for a particular type
Expand All @@ -61,12 +78,16 @@ public synchronized <T> void registerInjector(
}

/**
* Get a collection of all injectors that could potentially inject a value of the given type
* Get a collection of all injectors that could potentially inject a value of the given type. This
* does not include injectors from external injector services, instead it only uses injectors
* registered using {@link #registerInjector(Class, ParameterInjector)}.
*
* @param clazz Type to query for
* @param <T> Generic type
* @return Immutable collection containing all injectors that could potentially inject a value of the given type
* @deprecated Inject directly instead of relying on this list
*/
@Deprecated
public synchronized <T> @NonNull Collection<@NonNull ParameterInjector<C, ?>> injectors(
final @NonNull Class<T> clazz
) {
Expand All @@ -79,4 +100,55 @@ public synchronized <T> void registerInjector(
return Collections.unmodifiableCollection(injectors);
}

@Override
public @Nullable Object handle(final @NonNull Triplet<CommandContext<C>, Class<?>, AnnotationAccessor> triplet) {
for (final ParameterInjector<C, ?> injector : this.injectors(triplet.getSecond())) {
final Object value = injector.create(triplet.getFirst(), triplet.getThird());
if (value != null) {
return value;
}
}
return null;
}

/**
* Attempt to get an injectable value for the given context. This will consider all registered
* {@link InjectionService injection services}, and not just the {@link ParameterInjector injectors}
* registered using {@link #registerInjector(Class, ParameterInjector)}.
*
* @param clazz Class of the to inject
* @param context The command context that requests the injection
* @param annotationAccessor Annotation accessor for the injection. If the object is requested without access to annotations,
* use {@link AnnotationAccessor#empty()}
* @param <T> Type to inject
* @return The injected value, if an injector was able to provide a value
* @since 1.4.0
*/
@SuppressWarnings("EmptyCatch")
public <@NonNull T> @NonNull Optional<T> getInjectable(
final @NonNull Class<T> clazz,
final @NonNull CommandContext<C> context,
final @NonNull AnnotationAccessor annotationAccessor
) {
final Triplet<CommandContext<C>, Class<?>, AnnotationAccessor> triplet = Triplet.of(context, clazz, annotationAccessor);
try {
return Optional.of(clazz.cast(this.servicePipeline.pump(triplet).through(new TypeToken<InjectionService<C>>() {
}).getResult()));
} catch (final IllegalStateException ignored) {
}
return Optional.empty();
}

/**
* Register an injection service that will be able to provide injections using
* {@link #getInjectable(Class, CommandContext, AnnotationAccessor)}.
*
* @param service Service implementation
* @since 1.4.0
*/
public void registerInjectionService(final InjectionService<C> service) {
this.servicePipeline.registerServiceImplementation(new TypeToken<InjectionService<C>>() {
}, service, Collections.emptyList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import cloud.commandframework.CommandManager;
import cloud.commandframework.annotations.AnnotationAccessor;
import cloud.commandframework.annotations.injection.ParameterInjector;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.flags.FlagContext;
import cloud.commandframework.captions.Caption;
Expand Down Expand Up @@ -603,13 +602,7 @@ public void setCurrentArgument(final @Nullable CommandArgument<C, ?> argument) {
"Cannot retrieve injectable values from a command context that is not associated with a command manager"
);
}
for (final ParameterInjector<C, ?> injector : this.commandManager.parameterInjectorRegistry().injectors(clazz)) {
final Object value = injector.create(this, AnnotationAccessor.empty());
if (value != null) {
return Optional.of((T) value);
}
}
return Optional.empty();
return this.commandManager.parameterInjectorRegistry().getInjectable(clazz, this, AnnotationAccessor.empty());
}


Expand Down
Loading

0 comments on commit 2bb7ced

Please sign in to comment.