33import io .getunleash .lang .Nullable ;
44import io .getunleash .variant .Variant ;
55import java .util .*;
6+ import java .util .concurrent .ConcurrentHashMap ;
7+ import java .util .concurrent .LinkedBlockingQueue ;
68import java .util .function .BiPredicate ;
9+ import java .util .function .Predicate ;
710import java .util .stream .Collectors ;
11+ import java .util .stream .Stream ;
812
913public class FakeUnleash implements Unleash {
1014 private boolean enableAll = false ;
1115 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 <>();
1522
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 <>();
2626
2727 @ Override
2828 public boolean isEnabled (
2929 String toggleName ,
3030 UnleashContext context ,
3131 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+ }
4151 }
42- return isEnabled (toggleName );
4352 }
4453
4554 @ Override
4655 public Variant getVariant (String toggleName , UnleashContext context ) {
47- return getVariant (toggleName , Variant .DISABLED_VARIANT );
56+ return getVariant (toggleName , context , Variant .DISABLED_VARIANT );
4857 }
4958
5059 @ Override
5160 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 ;
5368 }
5469
5570 @ Override
@@ -59,11 +74,7 @@ public Variant getVariant(String toggleName) {
5974
6075 @ Override
6176 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 );
6778 }
6879
6980 @ Override
@@ -74,8 +85,9 @@ public MoreOperations more() {
7485 public void enableAll () {
7586 disableAll = false ;
7687 enableAll = true ;
77- excludedFeatures .clear ();
7888 features .clear ();
89+ excludedFeatures .clear ();
90+ conditionalFeatures .clear ();
7991 }
8092
8193 public void enableAllExcept (String ... excludedFeatures ) {
@@ -88,8 +100,9 @@ public void enableAllExcept(String... excludedFeatures) {
88100 public void disableAll () {
89101 disableAll = true ;
90102 enableAll = false ;
91- excludedFeatures .clear ();
92103 features .clear ();
104+ excludedFeatures .clear ();
105+ conditionalFeatures .clear ();
93106 }
94107
95108 public void disableAllExcept (String ... excludedFeatures ) {
@@ -104,48 +117,75 @@ public void resetAll() {
104117 enableAll = false ;
105118 excludedFeatures .clear ();
106119 features .clear ();
120+ conditionalFeatures .clear ();
107121 variants .clear ();
108122 }
109123
110124 public void enable (String ... features ) {
111125 for (String name : features ) {
126+ this .conditionalFeatures .remove (name );
112127 this .features .put (name , true );
113128 }
114129 }
115130
116131 public void disable (String ... features ) {
117132 for (String name : features ) {
133+ this .conditionalFeatures .remove (name );
118134 this .features .put (name , false );
119135 }
120136 }
121137
122138 public void reset (String ... features ) {
123139 for (String name : features ) {
140+ this .conditionalFeatures .remove (name );
124141 this .features .remove (name );
125142 }
126143 }
127144
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 );
130169 }
131170
132171 public class FakeMore implements MoreOperations {
133172
134173 @ Override
135174 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 ());
137178 }
138179
139180 @ Override
140181 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+ }
149189 }
150190
151191 @ Override
0 commit comments