3
3
import io .getunleash .lang .Nullable ;
4
4
import io .getunleash .variant .Variant ;
5
5
import java .util .*;
6
+ import java .util .concurrent .ConcurrentHashMap ;
7
+ import java .util .concurrent .LinkedBlockingQueue ;
6
8
import java .util .function .BiPredicate ;
9
+ import java .util .function .Predicate ;
7
10
import java .util .stream .Collectors ;
11
+ import java .util .stream .Stream ;
8
12
9
13
public class FakeUnleash implements Unleash {
10
14
private boolean enableAll = false ;
11
15
private boolean disableAll = false ;
12
- private Map <String , Boolean > excludedFeatures = new HashMap <>();
13
- private Map <String , Boolean > features = new HashMap <>();
14
- private Map <String , Variant > variants = new HashMap <>();
16
+ /**
17
+ * @implNote This uses {@link Queue} instead of {@link List}, as there are concurrent queues,
18
+ * but no concurrent lists, in the jdk. This will never be drained. Only iterated over.
19
+ */
20
+ private final Map <String , Queue <Predicate <UnleashContext >>> conditionalFeatures =
21
+ new ConcurrentHashMap <>();
15
22
16
- @ Override
17
- public boolean isEnabled (String toggleName , boolean defaultSetting ) {
18
- if (enableAll ) {
19
- return excludedFeatures .getOrDefault (toggleName , true );
20
- } else if (disableAll ) {
21
- return excludedFeatures .getOrDefault (toggleName , false );
22
- } else {
23
- return features .containsKey (toggleName ) ? features .get (toggleName ) : defaultSetting ;
24
- }
25
- }
23
+ private final Map <String , Boolean > excludedFeatures = new ConcurrentHashMap <>();
24
+ private final Map <String , Boolean > features = new ConcurrentHashMap <>();
25
+ private final Map <String , Variant > variants = new ConcurrentHashMap <>();
26
26
27
27
@ Override
28
28
public boolean isEnabled (
29
29
String toggleName ,
30
30
UnleashContext context ,
31
31
BiPredicate <String , UnleashContext > fallbackAction ) {
32
- return isEnabled (toggleName , fallbackAction );
33
- }
34
-
35
- @ Override
36
- public boolean isEnabled (
37
- String toggleName , BiPredicate <String , UnleashContext > fallbackAction ) {
38
- if ((!enableAll && !disableAll || excludedFeatures .containsKey (toggleName ))
39
- && !features .containsKey (toggleName )) {
40
- return fallbackAction .test (toggleName , UnleashContext .builder ().build ());
32
+ if (enableAll ) {
33
+ return excludedFeatures .getOrDefault (toggleName , true );
34
+ } else if (disableAll ) {
35
+ return excludedFeatures .getOrDefault (toggleName , false );
36
+ } else {
37
+ Boolean unconditionallyEnabled = features .get (toggleName );
38
+ if (unconditionallyEnabled != null ) {
39
+ return unconditionallyEnabled ;
40
+ }
41
+ Queue <Predicate <UnleashContext >> conditionalFeaturePredicates =
42
+ conditionalFeatures .get (toggleName );
43
+ if (conditionalFeaturePredicates == null ) {
44
+ return fallbackAction .test (toggleName , context );
45
+ } else {
46
+ return conditionalFeaturePredicates .stream ()
47
+ .anyMatch (
48
+ conditionalFeaturePredicate ->
49
+ conditionalFeaturePredicate .test (context ));
50
+ }
41
51
}
42
- return isEnabled (toggleName );
43
52
}
44
53
45
54
@ Override
46
55
public Variant getVariant (String toggleName , UnleashContext context ) {
47
- return getVariant (toggleName , Variant .DISABLED_VARIANT );
56
+ return getVariant (toggleName , context , Variant .DISABLED_VARIANT );
48
57
}
49
58
50
59
@ Override
51
60
public Variant getVariant (String toggleName , UnleashContext context , Variant defaultValue ) {
52
- return getVariant (toggleName , defaultValue );
61
+ if (isEnabled (toggleName , context )) {
62
+ Variant variant = variants .get (toggleName );
63
+ if (variant != null ) {
64
+ return variant ;
65
+ }
66
+ }
67
+ return defaultValue ;
53
68
}
54
69
55
70
@ Override
@@ -59,11 +74,7 @@ public Variant getVariant(String toggleName) {
59
74
60
75
@ Override
61
76
public Variant getVariant (String toggleName , Variant defaultValue ) {
62
- if (isEnabled (toggleName ) && variants .containsKey (toggleName )) {
63
- return variants .get (toggleName );
64
- } else {
65
- return defaultValue ;
66
- }
77
+ return getVariant (toggleName , UnleashContext .builder ().build (), defaultValue );
67
78
}
68
79
69
80
@ Override
@@ -74,8 +85,9 @@ public MoreOperations more() {
74
85
public void enableAll () {
75
86
disableAll = false ;
76
87
enableAll = true ;
77
- excludedFeatures .clear ();
78
88
features .clear ();
89
+ excludedFeatures .clear ();
90
+ conditionalFeatures .clear ();
79
91
}
80
92
81
93
public void enableAllExcept (String ... excludedFeatures ) {
@@ -88,8 +100,9 @@ public void enableAllExcept(String... excludedFeatures) {
88
100
public void disableAll () {
89
101
disableAll = true ;
90
102
enableAll = false ;
91
- excludedFeatures .clear ();
92
103
features .clear ();
104
+ excludedFeatures .clear ();
105
+ conditionalFeatures .clear ();
93
106
}
94
107
95
108
public void disableAllExcept (String ... excludedFeatures ) {
@@ -104,48 +117,75 @@ public void resetAll() {
104
117
enableAll = false ;
105
118
excludedFeatures .clear ();
106
119
features .clear ();
120
+ conditionalFeatures .clear ();
107
121
variants .clear ();
108
122
}
109
123
110
124
public void enable (String ... features ) {
111
125
for (String name : features ) {
126
+ this .conditionalFeatures .remove (name );
112
127
this .features .put (name , true );
113
128
}
114
129
}
115
130
116
131
public void disable (String ... features ) {
117
132
for (String name : features ) {
133
+ this .conditionalFeatures .remove (name );
118
134
this .features .put (name , false );
119
135
}
120
136
}
121
137
122
138
public void reset (String ... features ) {
123
139
for (String name : features ) {
140
+ this .conditionalFeatures .remove (name );
124
141
this .features .remove (name );
125
142
}
126
143
}
127
144
128
- public void setVariant (String t1 , Variant a ) {
129
- variants .put (t1 , a );
145
+ /**
146
+ * Enables or disables feature toggles depending on the evaluation of the {@code
147
+ * contextMatcher}. This can be called multiple times. If <b>any</b> of the context matchers
148
+ * match, the feature is enabled. This lets you conditionally configure multiple different tests
149
+ * to do different things, while running concurrently.
150
+ *
151
+ * <p>This will be overwritten if {@link #enable(String...)} or {@link #disable(String...)} are
152
+ * called and vice versa.
153
+ *
154
+ * @param contextMatcher the context matcher to evaluate
155
+ * @param features the features for which the context matcher will be invoked
156
+ */
157
+ public void conditionallyEnable (Predicate <UnleashContext > contextMatcher , String ... features ) {
158
+ for (String name : features ) {
159
+ // calling conditionallyEnable() should override having called enable() or disable()
160
+ this .features .remove (name );
161
+ this .conditionalFeatures
162
+ .computeIfAbsent (name , ignored -> new LinkedBlockingQueue <>())
163
+ .add (contextMatcher );
164
+ }
165
+ }
166
+
167
+ public void setVariant (String toggleName , Variant variant ) {
168
+ variants .put (toggleName , variant );
130
169
}
131
170
132
171
public class FakeMore implements MoreOperations {
133
172
134
173
@ Override
135
174
public List <String > getFeatureToggleNames () {
136
- return new ArrayList <>(features .keySet ());
175
+ return Stream .concat (features .keySet ().stream (), conditionalFeatures .keySet ().stream ())
176
+ .distinct ()
177
+ .collect (Collectors .toList ());
137
178
}
138
179
139
180
@ Override
140
181
public Optional <FeatureDefinition > getFeatureToggleDefinition (String toggleName ) {
141
- return Optional .ofNullable (features .get (toggleName ))
142
- .map (
143
- value ->
144
- new FeatureDefinition (
145
- toggleName ,
146
- Optional .of ("experiment" ),
147
- "default" ,
148
- true ));
182
+ if (conditionalFeatures .containsKey (toggleName ) || features .containsKey (toggleName )) {
183
+ return Optional .of (
184
+ new FeatureDefinition (
185
+ toggleName , Optional .of ("experiment" ), "default" , true ));
186
+ } else {
187
+ return Optional .empty ();
188
+ }
149
189
}
150
190
151
191
@ Override
0 commit comments