From dca141dc291a8da9171588d7584b1e6ffd7cfb78 Mon Sep 17 00:00:00 2001 From: Gwenneg Lepage Date: Fri, 1 Mar 2024 15:02:13 +0100 Subject: [PATCH] Forbid @FeatureToggle on boolean fields at build time --- .../BooleanFeatureToggleException.java | 23 ++++++++ .../quarkiverse/unleash/UnleashProcessor.java | 33 +++++++++++ .../BooleanFeatureToggleExceptionTest.java | 56 +++++++++++++++++++ docs/modules/ROOT/pages/index.adoc | 6 ++ 4 files changed, 118 insertions(+) create mode 100644 deployment/src/main/java/io/quarkiverse/unleash/BooleanFeatureToggleException.java create mode 100644 deployment/src/test/java/io/quarkiverse/unleash/BooleanFeatureToggleExceptionTest.java diff --git a/deployment/src/main/java/io/quarkiverse/unleash/BooleanFeatureToggleException.java b/deployment/src/main/java/io/quarkiverse/unleash/BooleanFeatureToggleException.java new file mode 100644 index 0000000..946d13f --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/unleash/BooleanFeatureToggleException.java @@ -0,0 +1,23 @@ +package io.quarkiverse.unleash; + +import jakarta.enterprise.inject.Instance; + +import org.jboss.jandex.ClassInfo; + +public class BooleanFeatureToggleException extends RuntimeException { + + private static final String MESSAGE = "@" + FeatureToggle.class.getName() + + " is not allowed on a boolean field or method argument, please use the " + Instance.class.getName() + + " type instead [class=%s]"; + + private final ClassInfo classInfo; + + public BooleanFeatureToggleException(ClassInfo classInfo) { + super(String.format(MESSAGE, classInfo.name())); + this.classInfo = classInfo; + } + + public ClassInfo getClassInfo() { + return classInfo; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java b/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java index 1b63c59..73dd2c0 100644 --- a/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java @@ -1,10 +1,13 @@ package io.quarkiverse.unleash; +import static io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import jakarta.enterprise.inject.Produces; @@ -47,6 +50,9 @@ public class UnleashProcessor { public static final Set IGNORE = Set.of(DN_VARIANT, DN_STRING); public static final DotName DN_FEATURE_VARIANT = DotName.createSimple(FeatureVariant.class); + private static final DotName DN_FEATURE_TOGGLE = DotName.createSimple(FeatureToggle.class); + private static final DotName DN_PRIMITIVE_BOOLEAN = DotName.createSimple(boolean.class); + @BuildStep @Record(RUNTIME_INIT) void configureRuntimeProperties(UnleashRecorder recorder, UnleashRuntimeTimeConfig runtimeConfig, @@ -102,6 +108,33 @@ AdditionalBeanBuildItem additionalBeans() { .build(); } + @BuildStep + void validateFeatureToggleAnnotations(CombinedIndexBuildItem combinedIndex, + BuildProducer validationErrors) { + List throwables = new ArrayList<>(); + for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DN_FEATURE_TOGGLE)) { + AnnotationTarget target = annotation.target(); + switch (target.kind()) { + case FIELD -> { + if (DN_PRIMITIVE_BOOLEAN.equals(target.asField().type().name())) { + ClassInfo declaringClass = target.asField().declaringClass(); + throwables.add(new BooleanFeatureToggleException(declaringClass)); + } + } + case METHOD_PARAMETER -> { + if (DN_PRIMITIVE_BOOLEAN.equals(target.asMethodParameter().type().name())) { + ClassInfo declaringClass = target.asMethodParameter().method().declaringClass(); + throwables.add(new BooleanFeatureToggleException(declaringClass)); + } + } + default -> { + // No validation required for all other target kinds. + } + } + } + validationErrors.produce(new ValidationErrorBuildItem(throwables.toArray(new Throwable[0]))); + } + @BuildStep void generateProducer(CombinedIndexBuildItem combinedIndex, BuildProducer generatedBeans) { diff --git a/deployment/src/test/java/io/quarkiverse/unleash/BooleanFeatureToggleExceptionTest.java b/deployment/src/test/java/io/quarkiverse/unleash/BooleanFeatureToggleExceptionTest.java new file mode 100644 index 0000000..50ce68b --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/unleash/BooleanFeatureToggleExceptionTest.java @@ -0,0 +1,56 @@ +package io.quarkiverse.unleash; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Arrays; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class BooleanFeatureToggleExceptionTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(BooleanField.class, BooleanMethodArg.class)) + .assertException(t -> { + assertEquals(DeploymentException.class, t.getClass()); + assertEquals(2, t.getSuppressed().length); + assertBooleanFeatureToggleException(t, BooleanField.class); + assertBooleanFeatureToggleException(t, BooleanMethodArg.class); + }); + + private static void assertBooleanFeatureToggleException(Throwable t, Class expectedClassName) { + assertEquals(1, Arrays.stream(t.getSuppressed()).filter(throwable -> { + if (throwable instanceof BooleanFeatureToggleException e) { + return expectedClassName.getName().equals(e.getClassInfo().name().toString()); + } + return false; + }).count()); + } + + @Test + void shouldNotBeInvoked() { + fail("A deployment exception should be thrown before this method is ever invoked"); + } + + @ApplicationScoped + static class BooleanField { + + @FeatureToggle(name = "toggle") + boolean toggle; + } + + @Singleton + static class BooleanMethodArg { + + void doSomething(@FeatureToggle(name = "toggle") boolean toggle) { + } + } +} diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index b546045..4f45175 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -65,6 +65,12 @@ public class TestService { } ---- +[NOTE] +==== +If the `@FeatureToggle` annotation is placed on a `boolean` field or method argument, an exception will be thrown at build time. +`@FeatureToggle` should only be used with the `Instance` type. +==== + === @FeatureVariant By using the `@FeatureVariant` annotation there is a shortcut to inject feature toggle