Skip to content

Commit d53dc31

Browse files
committed
feat: Add dedicated annotations for boolean, String, int and double flags in Junit5 extension
1 parent 0030528 commit d53dc31

File tree

15 files changed

+1136
-60
lines changed

15 files changed

+1136
-60
lines changed

tools/junit-openfeature/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<artifactId>junit-jupiter-api</artifactId>
3838
</dependency>
3939

40+
<dependency>
41+
<groupId>org.junit.platform</groupId>
42+
<artifactId>junit-platform-testkit</artifactId>
43+
</dependency>
44+
4045
<dependency>
4146
<groupId>org.junit-pioneer</groupId>
4247
<artifactId>junit-pioneer</artifactId>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Repeatable;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
/**
11+
* Repeatable annotation that allows you to define boolean feature flags for the default domain.
12+
* Can be used as a standalone flag configuration but also within {@link OpenFeature}.
13+
*/
14+
@Target({ElementType.METHOD, ElementType.TYPE})
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Repeatable(BooleanFlags.class)
17+
@ExtendWith(OpenFeatureExtension.class)
18+
public @interface BooleanFlag {
19+
/**
20+
* The key of the FeatureFlag.
21+
*/
22+
String name();
23+
24+
/**
25+
* The value of the FeatureFlag.
26+
*/
27+
boolean value();
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
9+
/**
10+
* Collection of {@link BooleanFlag} configurations.
11+
*/
12+
@Target({ElementType.METHOD, ElementType.TYPE})
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@ExtendWith(OpenFeatureExtension.class)
15+
public @interface BooleanFlags {
16+
/**
17+
* Collection of {@link BooleanFlag} configurations.
18+
*/
19+
BooleanFlag[] value() default {};
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Repeatable;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
/**
11+
* Repeatable annotation that allows you to define double feature flags for the default domain.
12+
* Can be used as a standalone flag configuration but also within {@link OpenFeature}.
13+
*/
14+
@Target({ElementType.METHOD, ElementType.TYPE})
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Repeatable(DoubleFlags.class)
17+
@ExtendWith(OpenFeatureExtension.class)
18+
public @interface DoubleFlag {
19+
/**
20+
* The key of the FeatureFlag.
21+
*/
22+
String name();
23+
/**
24+
* The value of the FeatureFlag.
25+
*/
26+
double value();
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
9+
/**
10+
* Collection of {@link DoubleFlag} configurations.
11+
*/
12+
@Target({ElementType.METHOD, ElementType.TYPE})
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@ExtendWith(OpenFeatureExtension.class)
15+
public @interface DoubleFlags {
16+
/**
17+
* Collection of {@link DoubleFlag} configurations.
18+
*/
19+
DoubleFlag[] value() default {};
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Repeatable;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
/**
11+
* Repeatable annotation that allows you to define integer feature flags for the default domain.
12+
* Can be used as a standalone flag configuration but also within {@link OpenFeature}.
13+
*/
14+
@Target({ElementType.METHOD, ElementType.TYPE})
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Repeatable(IntegerFlags.class)
17+
@ExtendWith(OpenFeatureExtension.class)
18+
public @interface IntegerFlag {
19+
/**
20+
* The key of the FeatureFlag.
21+
*/
22+
String name();
23+
/**
24+
* The value of the FeatureFlag.
25+
*/
26+
int value();
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
9+
/**
10+
* Collection of {@link IntegerFlag} configurations.
11+
*/
12+
@Target({ElementType.METHOD, ElementType.TYPE})
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@ExtendWith(OpenFeatureExtension.class)
15+
public @interface IntegerFlags {
16+
/**
17+
* Collection of {@link IntegerFlag} configurations.
18+
*/
19+
IntegerFlag[] value() default {};
20+
}

tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeature.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
/**
1111
* Annotation for generating an extended configuration for OpenFeature.
1212
* This annotation allows you to specify a list of flags for a specific domain.
13+
* <p>
14+
* Flags with duplicate names across different flag arrays
15+
* (e.g., in {@link OpenFeature#value()} and {@link OpenFeature#booleanFlags()}
16+
* or {@link OpenFeature#booleanFlags()} and {@link OpenFeature#stringFlags()})
17+
* are not permitted and will result in an {@link IllegalArgumentException}.
1318
*/
1419
@Target({ElementType.METHOD, ElementType.TYPE})
1520
@Retention(RetentionPolicy.RUNTIME)
@@ -23,5 +28,21 @@
2328
/**
2429
* Collection of {@link Flag} configurations for this domain.
2530
*/
26-
Flag[] value();
31+
Flag[] value() default {};
32+
/**
33+
* Collection of {@link BooleanFlag} configurations for this domain.
34+
*/
35+
BooleanFlag[] booleanFlags() default {};
36+
/**
37+
* Collection of {@link StringFlag} configurations for this domain.
38+
*/
39+
StringFlag[] stringFlags() default {};
40+
/**
41+
* Collection of {@link IntegerFlag} configurations for this domain.
42+
*/
43+
IntegerFlag[] integerFlags() default {};
44+
/**
45+
* Collection of {@link DoubleFlag} configurations for this domain.
46+
*/
47+
DoubleFlag[] doubleFlags() default {};
2748
}

tools/junit-openfeature/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java

Lines changed: 118 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
import dev.openfeature.sdk.OpenFeatureAPI;
44
import dev.openfeature.sdk.providers.memory.Flag;
55
import java.lang.reflect.Method;
6+
import java.util.AbstractMap;
67
import java.util.Arrays;
78
import java.util.HashMap;
9+
import java.util.HashSet;
10+
import java.util.List;
811
import java.util.Map;
12+
import java.util.Set;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
915
import org.apache.commons.lang3.BooleanUtils;
1016
import org.junit.jupiter.api.extension.AfterEachCallback;
1117
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -23,37 +29,127 @@ public class OpenFeatureExtension implements BeforeEachCallback, AfterEachCallba
2329

2430
private static Map<String, Map<String, Flag<?>>> handleExtendedConfiguration(
2531
ExtensionContext extensionContext, Map<String, Map<String, Flag<?>>> configuration) {
26-
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, OpenFeature.class)
27-
.forEachOrdered(annotation -> {
28-
Map<String, Flag<?>> domainFlags = configuration.getOrDefault(annotation.domain(), new HashMap<>());
29-
30-
Arrays.stream(annotation.value())
31-
.filter(flag -> !domainFlags.containsKey(flag.name()))
32-
.forEach(flag -> {
33-
Flag.FlagBuilder<?> builder = generateFlagBuilder(flag);
34-
domainFlags.put(flag.name(), builder.build());
35-
});
36-
configuration.put(annotation.domain(), domainFlags);
37-
});
32+
List<OpenFeature> openFeatureAnnotationList = PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(
33+
extensionContext, OpenFeature.class)
34+
.collect(Collectors.toList());
35+
Map<String, Set<String>> nonTypedFlagNamesByDomain = getFlagNamesByDomain(openFeatureAnnotationList);
36+
openFeatureAnnotationList.forEach(annotation -> {
37+
Map<String, Flag<?>> domainFlags = configuration.getOrDefault(annotation.domain(), new HashMap<>());
38+
39+
Arrays.stream(annotation.value())
40+
.filter(flag -> !domainFlags.containsKey(flag.name()))
41+
.forEach(flag -> {
42+
Flag.FlagBuilder<?> builder = generateFlagBuilder(flag);
43+
domainFlags.put(flag.name(), builder.build());
44+
});
45+
addTypedFlags(
46+
annotation,
47+
domainFlags,
48+
nonTypedFlagNamesByDomain.getOrDefault(annotation.domain(), new HashSet<>()));
49+
configuration.put(annotation.domain(), domainFlags);
50+
});
3851
return configuration;
3952
}
4053

54+
private static Map<String, Set<String>> getFlagNamesByDomain(List<OpenFeature> openFeatureList) {
55+
return openFeatureList.stream()
56+
.map(o -> {
57+
Set<String> flagNames = Arrays.stream(o.value())
58+
.map(dev.openfeature.contrib.tools.junitopenfeature.Flag::name)
59+
.collect(Collectors.toSet());
60+
return new AbstractMap.SimpleEntry<>(o.domain(), flagNames);
61+
})
62+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (t1, t2) -> {
63+
t1.addAll(t2);
64+
return t1;
65+
}));
66+
}
67+
68+
private static void addTypedFlags(OpenFeature annotation, Map<String, Flag<?>> domainFlags, Set<String> flagNames) {
69+
addBooleanFlags(Arrays.stream(annotation.booleanFlags()), domainFlags, flagNames);
70+
addStringFlags(Arrays.stream(annotation.stringFlags()), domainFlags, flagNames);
71+
addIntegerFlags(Arrays.stream(annotation.integerFlags()), domainFlags, flagNames);
72+
addDoubleFlags(Arrays.stream(annotation.doubleFlags()), domainFlags, flagNames);
73+
}
74+
75+
private static void addBooleanFlags(
76+
Stream<BooleanFlag> booleanFlags, Map<String, Flag<?>> domainFlags, Set<String> flagNames) {
77+
78+
booleanFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
79+
}
80+
81+
private static void addStringFlags(
82+
Stream<StringFlag> stringFlags, Map<String, Flag<?>> domainFlags, Set<String> flagNames) {
83+
stringFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
84+
}
85+
86+
private static void addIntegerFlags(
87+
Stream<IntegerFlag> integerFlags, Map<String, Flag<?>> domainFlags, Set<String> flagNames) {
88+
integerFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
89+
}
90+
91+
private static void addDoubleFlags(
92+
Stream<DoubleFlag> doubleFlags, Map<String, Flag<?>> domainFlags, Set<String> flagNames) {
93+
doubleFlags.forEach(flag -> addFlag(domainFlags, flagNames, flag.name(), flag.value()));
94+
}
95+
96+
private static <T> void addFlag(
97+
Map<String, Flag<?>> domainFlags, Set<String> domainFlagNames, String flagName, T value) {
98+
if (domainFlagNames.contains(flagName)) {
99+
throw new IllegalArgumentException("Flag with name " + flagName + " already exists. "
100+
+ "There shouldn't be @Flag and @" + value.getClass().getSimpleName() + "Flag with the same name!");
101+
}
102+
103+
if (domainFlags.containsKey(flagName)) {
104+
return;
105+
}
106+
Flag.FlagBuilder<Object> builder =
107+
Flag.builder().variant(String.valueOf(value), value).defaultVariant(String.valueOf(value));
108+
domainFlags.put(flagName, builder.build());
109+
}
110+
41111
private static Map<String, Map<String, Flag<?>>> handleSimpleConfiguration(ExtensionContext extensionContext) {
42112
Map<String, Map<String, Flag<?>>> configuration = new HashMap<>();
43113
String defaultDomain = PioneerAnnotationUtils.findClosestEnclosingAnnotation(
44114
extensionContext, OpenFeatureDefaultDomain.class)
45115
.map(OpenFeatureDefaultDomain::value)
46116
.orElse("");
47-
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(
48-
extensionContext, dev.openfeature.contrib.tools.junitopenfeature.Flag.class)
49-
.forEachOrdered(flag -> {
50-
Map<String, Flag<?>> domainFlags = configuration.getOrDefault(defaultDomain, new HashMap<>());
51-
if (!domainFlags.containsKey(flag.name())) {
52-
Flag.FlagBuilder<?> builder = generateFlagBuilder(flag);
53-
domainFlags.put(flag.name(), builder.build());
54-
configuration.put(defaultDomain, domainFlags);
55-
}
56-
});
117+
Map<String, Flag<?>> domainFlags = configuration.getOrDefault(defaultDomain, new HashMap<>());
118+
List<dev.openfeature.contrib.tools.junitopenfeature.Flag> flagList =
119+
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(
120+
extensionContext, dev.openfeature.contrib.tools.junitopenfeature.Flag.class)
121+
.collect(Collectors.toList());
122+
Set<String> flagNames = flagList.stream()
123+
.map(dev.openfeature.contrib.tools.junitopenfeature.Flag::name)
124+
.collect(Collectors.toSet());
125+
126+
flagList.forEach(flag -> {
127+
if (!domainFlags.containsKey(flag.name())) {
128+
Flag.FlagBuilder<?> builder = generateFlagBuilder(flag);
129+
domainFlags.put(flag.name(), builder.build());
130+
configuration.put(defaultDomain, domainFlags);
131+
}
132+
});
133+
134+
Stream<BooleanFlag> booleanFlags =
135+
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, BooleanFlag.class);
136+
addBooleanFlags(booleanFlags, domainFlags, flagNames);
137+
138+
Stream<StringFlag> stringFlags =
139+
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, StringFlag.class);
140+
addStringFlags(stringFlags, domainFlags, flagNames);
141+
142+
Stream<IntegerFlag> integerFlags =
143+
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, IntegerFlag.class);
144+
addIntegerFlags(integerFlags, domainFlags, flagNames);
145+
146+
Stream<DoubleFlag> doubleFlags =
147+
PioneerAnnotationUtils.findAllEnclosingRepeatableAnnotations(extensionContext, DoubleFlag.class);
148+
addDoubleFlags(doubleFlags, domainFlags, flagNames);
149+
150+
if (!domainFlags.isEmpty()) {
151+
configuration.put(defaultDomain, domainFlags);
152+
}
57153

58154
return configuration;
59155
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.openfeature.contrib.tools.junitopenfeature;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Repeatable;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
/**
11+
* Repeatable annotation that allows you to define String feature flags for the default domain.
12+
* Can be used as a standalone flag configuration but also within {@link OpenFeature}.
13+
*/
14+
@Target({ElementType.METHOD, ElementType.TYPE})
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Repeatable(StringFlags.class)
17+
@ExtendWith(OpenFeatureExtension.class)
18+
public @interface StringFlag {
19+
/**
20+
* The key of the FeatureFlag.
21+
*/
22+
String name();
23+
24+
/**
25+
* The value of the FeatureFlag.
26+
*/
27+
String value();
28+
}

0 commit comments

Comments
 (0)