org.junit-pioneer
junit-pioneer
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlag.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlag.java
new file mode 100755
index 000000000..ea0b00dd3
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlag.java
@@ -0,0 +1,28 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Repeatable annotation that allows you to define boolean feature flags for the default domain.
+ * Can be used as a standalone flag configuration but also within {@link OpenFeature}.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(BooleanFlags.class)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface BooleanFlag {
+ /**
+ * The key of the FeatureFlag.
+ */
+ String name();
+
+ /**
+ * The value of the FeatureFlag.
+ */
+ boolean value();
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlags.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlags.java
new file mode 100755
index 000000000..42a3c9f80
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/BooleanFlags.java
@@ -0,0 +1,20 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Collection of {@link BooleanFlag} configurations.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface BooleanFlags {
+ /**
+ * Collection of {@link BooleanFlag} configurations.
+ */
+ BooleanFlag[] value() default {};
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlag.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlag.java
new file mode 100755
index 000000000..2adfa139c
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlag.java
@@ -0,0 +1,27 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Repeatable annotation that allows you to define double feature flags for the default domain.
+ * Can be used as a standalone flag configuration but also within {@link OpenFeature}.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(DoubleFlags.class)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface DoubleFlag {
+ /**
+ * The key of the FeatureFlag.
+ */
+ String name();
+ /**
+ * The value of the FeatureFlag.
+ */
+ double value();
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlags.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlags.java
new file mode 100755
index 000000000..7eeb3def6
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/DoubleFlags.java
@@ -0,0 +1,20 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Collection of {@link DoubleFlag} configurations.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface DoubleFlags {
+ /**
+ * Collection of {@link DoubleFlag} configurations.
+ */
+ DoubleFlag[] value() default {};
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlag.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlag.java
new file mode 100755
index 000000000..10cc182c8
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlag.java
@@ -0,0 +1,27 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Repeatable annotation that allows you to define integer feature flags for the default domain.
+ * Can be used as a standalone flag configuration but also within {@link OpenFeature}.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(IntegerFlags.class)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface IntegerFlag {
+ /**
+ * The key of the FeatureFlag.
+ */
+ String name();
+ /**
+ * The value of the FeatureFlag.
+ */
+ int value();
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlags.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlags.java
new file mode 100755
index 000000000..71e73ec6e
--- /dev/null
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/IntegerFlags.java
@@ -0,0 +1,20 @@
+package dev.openfeature.contrib.tools.junitopenfeature;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Collection of {@link IntegerFlag} configurations.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(OpenFeatureExtension.class)
+public @interface IntegerFlags {
+ /**
+ * Collection of {@link IntegerFlag} configurations.
+ */
+ IntegerFlag[] value() default {};
+}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeature.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeature.java
index 2e7ab0586..fe5dcfd43 100644
--- a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeature.java
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeature.java
@@ -10,6 +10,11 @@
/**
* Annotation for generating an extended configuration for OpenFeature.
* This annotation allows you to specify a list of flags for a specific domain.
+ *
+ * Flags with duplicate names across different flag arrays
+ * (e.g., in {@link OpenFeature#value()} and {@link OpenFeature#booleanFlags()}
+ * or {@link OpenFeature#booleanFlags()} and {@link OpenFeature#stringFlags()})
+ * are not permitted and will result in an {@link IllegalArgumentException}.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@@ -23,5 +28,21 @@
/**
* Collection of {@link Flag} configurations for this domain.
*/
- Flag[] value();
+ Flag[] value() default {};
+ /**
+ * Collection of {@link BooleanFlag} configurations for this domain.
+ */
+ BooleanFlag[] booleanFlags() default {};
+ /**
+ * Collection of {@link StringFlag} configurations for this domain.
+ */
+ StringFlag[] stringFlags() default {};
+ /**
+ * Collection of {@link IntegerFlag} configurations for this domain.
+ */
+ IntegerFlag[] integerFlags() default {};
+ /**
+ * Collection of {@link DoubleFlag} configurations for this domain.
+ */
+ DoubleFlag[] doubleFlags() default {};
}
diff --git a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java
index cebf5e6b0..01429580a 100644
--- a/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java
+++ b/tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java
@@ -3,9 +3,15 @@
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.providers.memory.Flag;
import java.lang.reflect.Method;
+import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.commons.lang3.BooleanUtils;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -23,37 +29,127 @@ public class OpenFeatureExtension implements BeforeEachCallback, AfterEachCallba
private static Map>> handleExtendedConfiguration(
ExtensionContext extensionContext, Map>> configuration) {
- PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, OpenFeature.class)
- .forEachOrdered(annotation -> {
- Map> domainFlags = configuration.getOrDefault(annotation.domain(), new HashMap<>());
-
- Arrays.stream(annotation.value())
- .filter(flag -> !domainFlags.containsKey(flag.name()))
- .forEach(flag -> {
- Flag.FlagBuilder> builder = generateFlagBuilder(flag);
- domainFlags.put(flag.name(), builder.build());
- });
- configuration.put(annotation.domain(), domainFlags);
- });
+ List openFeatureAnnotationList = PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(
+ extensionContext, OpenFeature.class)
+ .collect(Collectors.toList());
+ Map> nonTypedFlagNamesByDomain = getFlagNamesByDomain(openFeatureAnnotationList);
+ openFeatureAnnotationList.forEach(annotation -> {
+ Map> domainFlags = configuration.getOrDefault(annotation.domain(), new HashMap<>());
+
+ Arrays.stream(annotation.value())
+ .filter(flag -> !domainFlags.containsKey(flag.name()))
+ .forEach(flag -> {
+ Flag.FlagBuilder> builder = generateFlagBuilder(flag);
+ domainFlags.put(flag.name(), builder.build());
+ });
+ addTypedFlags(
+ annotation,
+ domainFlags,
+ nonTypedFlagNamesByDomain.getOrDefault(annotation.domain(), new HashSet<>()));
+ configuration.put(annotation.domain(), domainFlags);
+ });
return configuration;
}
+ private static Map> getFlagNamesByDomain(List openFeatureList) {
+ return openFeatureList.stream()
+ .map(o -> {
+ Set flagNames = Arrays.stream(o.value())
+ .map(dev.openfeature.contrib.tools.junitopenfeature.Flag::name)
+ .collect(Collectors.toSet());
+ return new AbstractMap.SimpleEntry<>(o.domain(), flagNames);
+ })
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (t1, t2) -> {
+ t1.addAll(t2);
+ return t1;
+ }));
+ }
+
+ private static void addTypedFlags(OpenFeature annotation, Map> domainFlags, Set flagNames) {
+ addBooleanFlags(Arrays.stream(annotation.booleanFlags()), domainFlags, flagNames);
+ addStringFlags(Arrays.stream(annotation.stringFlags()), domainFlags, flagNames);
+ addIntegerFlags(Arrays.stream(annotation.integerFlags()), domainFlags, flagNames);
+ addDoubleFlags(Arrays.stream(annotation.doubleFlags()), domainFlags, flagNames);
+ }
+
+ private static void addBooleanFlags(
+ Stream booleanFlags, Map> domainFlags, Set flagNames) {
+
+ booleanFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
+ }
+
+ private static void addStringFlags(
+ Stream stringFlags, Map> domainFlags, Set flagNames) {
+ stringFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
+ }
+
+ private static void addIntegerFlags(
+ Stream integerFlags, Map> domainFlags, Set flagNames) {
+ integerFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
+ }
+
+ private static void addDoubleFlags(
+ Stream doubleFlags, Map> domainFlags, Set flagNames) {
+ doubleFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
+ }
+
+ private static void addFlag(
+ Map> domainFlags, Set domainFlagNames, String flagName, T value) {
+ if (domainFlagNames.contains(flagName)) {
+ throw new IllegalArgumentException("Flag with name " + flagName + " already exists. "
+ + "There shouldn't be @Flag and @" + value.getClass().getSimpleName() + "Flag with the same name!");
+ }
+
+ if (domainFlags.containsKey(flagName)) {
+ return;
+ }
+ Flag.FlagBuilder