Skip to content

Commit

Permalink
Forbid @FeatureToggle on boolean fields at build time
Browse files Browse the repository at this point in the history
  • Loading branch information
gwenneg committed Mar 1, 2024
1 parent 8a36b48 commit dca141d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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()
+ "<Boolean> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -47,6 +50,9 @@ public class UnleashProcessor {
public static final Set<DotName> 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,
Expand Down Expand Up @@ -102,6 +108,33 @@ AdditionalBeanBuildItem additionalBeans() {
.build();
}

@BuildStep
void validateFeatureToggleAnnotations(CombinedIndexBuildItem combinedIndex,
BuildProducer<ValidationErrorBuildItem> validationErrors) {
List<Throwable> 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<GeneratedBeanBuildItem> generatedBeans) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
}
}
6 changes: 6 additions & 0 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>` type.
====

=== @FeatureVariant

By using the `@FeatureVariant` annotation there is a shortcut to inject feature toggle
Expand Down

0 comments on commit dca141d

Please sign in to comment.