Skip to content

Commit 4aba507

Browse files
committed
refactor(schema): use new rules dict over list
1 parent 4bfe2a8 commit 4aba507

File tree

4 files changed

+98
-95
lines changed

4 files changed

+98
-95
lines changed

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any
3737
self._logger.debug(f"caught exception while matching action: action={action}, exception={str(exc)}")
3838
return False
3939

40-
def _is_rule_matched(self, feature_name: str, rule: Dict[str, Any], context: Dict[str, Any]) -> bool:
41-
rule_name = rule.get(schema.RULE_NAME_KEY, "")
40+
def _is_rule_matched(
41+
self, rule_name: str, feature_name: str, rule: Dict[str, Any], context: Dict[str, Any]
42+
) -> bool:
4243
rule_default_value = rule.get(schema.RULE_DEFAULT_VALUE)
4344
conditions = cast(List[Dict], rule.get(schema.CONDITIONS_KEY))
4445

@@ -68,11 +69,11 @@ def _handle_rules(
6869
feature_name: str,
6970
context: Dict[str, Any],
7071
feature_default_value: bool,
71-
rules: List[Dict[str, Any]],
72+
rules: Dict[str, Any],
7273
) -> bool:
73-
for rule in rules:
74+
for rule_name, rule in rules.items():
7475
rule_default_value = rule.get(schema.RULE_DEFAULT_VALUE)
75-
if self._is_rule_matched(feature_name, rule, context):
76+
if self._is_rule_matched(rule_name=rule_name, feature_name=feature_name, rule=rule, context=context):
7677
return bool(rule_default_value)
7778
# no rule matched, return default value of feature
7879
logger.debug(
@@ -211,16 +212,16 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
211212
return features_enabled
212213

213214
for feature_name, feature_dict_def in features.items():
214-
rules_list = feature_dict_def.get(schema.RULES_KEY, [])
215+
rules = feature_dict_def.get(schema.RULES_KEY, {})
215216
feature_default_value = feature_dict_def.get(schema.FEATURE_DEFAULT_VAL_KEY)
216-
if feature_default_value and not rules_list:
217+
if feature_default_value and not rules:
217218
self._logger.debug(f"feature is enabled by default and has no defined rules, name={feature_name}")
218219
features_enabled.append(feature_name)
219220
elif self._handle_rules(
220221
feature_name=feature_name,
221222
context=context,
222223
feature_default_value=feature_default_value,
223-
rules=rules_list,
224+
rules=rules,
224225
):
225226
self._logger.debug(f"feature's calculated value is True, name={feature_name}")
226227
features_enabled.append(feature_name)

aws_lambda_powertools/utilities/feature_flags/schema.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def validate(self):
4848

4949
for name, feature in self.features.items():
5050
self.validate_feature(name, feature)
51-
rules = RulesValidator(feature=feature, feature_name=name)
51+
rules = RulesValidator(feature=feature)
5252
rules.validate()
5353

5454
@staticmethod
@@ -62,50 +62,48 @@ def validate_feature(name, feature):
6262

6363

6464
class RulesValidator(BaseValidator):
65-
def __init__(self, feature: Dict[str, Any], feature_name: str):
65+
def __init__(self, feature: Dict[str, Any]):
6666
self.feature = feature
67-
self.feature_name = feature_name
68-
self.rules: Optional[List[Dict]] = self.feature.get(RULES_KEY)
67+
self.feature_name = next(iter(self.feature))
68+
self.rules: Optional[Dict] = self.feature.get(RULES_KEY)
6969

7070
def validate(self):
7171
if not self.rules:
7272
logger.debug("Rules are empty, ignoring validation")
7373
return
7474

75-
if not isinstance(self.rules, list):
76-
raise ConfigurationError(f"Feature rules is not a list, feature_name={self.feature_name}")
75+
if not isinstance(self.rules, dict):
76+
raise ConfigurationError(f"Feature rules must be a dictionary, feature_name={self.feature_name}")
7777

78-
for rule in self.rules:
79-
self.validate_rule(rule, self.feature)
80-
conditions = ConditionsValidator(rule=rule, rule_name=rule.get(RULE_NAME_KEY))
78+
for rule_name, rule in self.rules.items():
79+
self.validate_rule(rule=rule, rule_name=rule_name, feature_name=self.feature_name)
80+
conditions = ConditionsValidator(rule=rule, rule_name=rule_name)
8181
conditions.validate()
8282

83-
def validate_rule(self, rule, feature_name):
83+
def validate_rule(self, rule, rule_name, feature_name):
8484
if not rule or not isinstance(rule, dict):
8585
raise ConfigurationError(f"Feature rule must be a dictionary, feature_name={feature_name}")
8686

87-
self.validate_rule_name(rule, feature_name)
88-
self.validate_rule_default_value(rule)
87+
self.validate_rule_name(rule_name=rule_name, feature_name=feature_name)
88+
self.validate_rule_default_value(rule=rule, rule_name=rule_name)
8989

9090
@staticmethod
91-
def validate_rule_name(rule, feature_name):
92-
rule_name = rule.get(RULE_NAME_KEY)
93-
if not rule_name or rule_name is None or not isinstance(rule_name, str):
91+
def validate_rule_name(rule_name: str, feature_name: str):
92+
if not rule_name or not isinstance(rule_name, str):
9493
raise ConfigurationError(
9594
f"'rule_name' key must be present and have a non-empty string, feature_name={feature_name}"
9695
)
9796

9897
@staticmethod
99-
def validate_rule_default_value(rule):
100-
rule_name = rule.get(RULE_NAME_KEY)
98+
def validate_rule_default_value(rule: Dict, rule_name: str):
10199
rule_default_value = rule.get(RULE_DEFAULT_VALUE)
102-
if rule_default_value is None or not isinstance(rule_default_value, bool):
100+
if not isinstance(rule_default_value, bool):
103101
raise ConfigurationError(f"'rule_default_value' key must have be bool, rule_name={rule_name}")
104102

105103

106104
class ConditionsValidator(BaseValidator):
107105
def __init__(self, rule: Dict[str, Any], rule_name: str):
108-
self.conditions = rule.get(CONDITIONS_KEY, {})
106+
self.conditions: List[Dict[str, Any]] = rule.get(CONDITIONS_KEY, {})
109107
self.rule_name = rule_name
110108

111109
def validate(self):

tests/functional/feature_toggles/test_feature_toggles.py

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ def test_toggles_rule_does_not_match(mocker, config):
5050
"features": {
5151
"my_feature": {
5252
"default": expected_value,
53-
"rules": [
54-
{
55-
"rule_name": "tenant id equals 345345435",
53+
"rules": {
54+
"tenant id equals 345345435": {
5655
"when_match": False,
5756
"conditions": [
5857
{
@@ -61,10 +60,10 @@ def test_toggles_rule_does_not_match(mocker, config):
6160
"value": "345345435",
6261
}
6362
],
64-
},
65-
],
63+
}
64+
},
6665
}
67-
},
66+
}
6867
}
6968

7069
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
@@ -100,9 +99,8 @@ def test_toggles_conditions_no_match(mocker, config):
10099
"features": {
101100
"my_feature": {
102101
"default": expected_value,
103-
"rules": [
104-
{
105-
"rule_name": "tenant id equals 345345435",
102+
"rules": {
103+
"tenant id equals 345345435": {
106104
"when_match": False,
107105
"conditions": [
108106
{
@@ -111,10 +109,10 @@ def test_toggles_conditions_no_match(mocker, config):
111109
"value": "345345435",
112110
}
113111
],
114-
},
115-
],
112+
}
113+
},
116114
}
117-
},
115+
}
118116
}
119117
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
120118
toggle = conf_store.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
@@ -130,9 +128,8 @@ def test_toggles_conditions_rule_match_equal_multiple_conditions(mocker, config)
130128
"features": {
131129
"my_feature": {
132130
"default": True,
133-
"rules": [
134-
{
135-
"rule_name": "tenant id equals 6 and username is a",
131+
"rules": {
132+
"tenant id equals 6 and username is a": {
136133
"when_match": expected_value,
137134
"conditions": [
138135
{
@@ -146,8 +143,8 @@ def test_toggles_conditions_rule_match_equal_multiple_conditions(mocker, config)
146143
"value": username_val,
147144
},
148145
],
149-
},
150-
],
146+
}
147+
},
151148
}
152149
},
153150
}
@@ -172,9 +169,9 @@ def test_toggles_conditions_no_rule_match_equal_multiple_conditions(mocker, conf
172169
"features": {
173170
"my_feature": {
174171
"default": expected_val,
175-
"rules": [
176-
{
177-
"rule_name": "tenant id equals 645654 and username is a", # rule will not match
172+
"rules": {
173+
# rule will not match
174+
"tenant id equals 645654 and username is a": {
178175
"when_match": False,
179176
"conditions": [
180177
{
@@ -188,10 +185,10 @@ def test_toggles_conditions_no_rule_match_equal_multiple_conditions(mocker, conf
188185
"value": "a",
189186
},
190187
],
191-
},
192-
],
188+
}
189+
},
193190
}
194-
},
191+
}
195192
}
196193
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
197194
toggle = conf_store.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
@@ -208,9 +205,8 @@ def test_toggles_conditions_rule_match_multiple_actions_multiple_rules_multiple_
208205
"features": {
209206
"my_feature": {
210207
"default": expected_value_third_check,
211-
"rules": [
212-
{
213-
"rule_name": "tenant id equals 6 and username startswith a",
208+
"rules": {
209+
"tenant id equals 6 and username startswith a": {
214210
"when_match": expected_value_first_check,
215211
"conditions": [
216212
{
@@ -225,8 +221,7 @@ def test_toggles_conditions_rule_match_multiple_actions_multiple_rules_multiple_
225221
},
226222
],
227223
},
228-
{
229-
"rule_name": "tenant id equals 4446 and username startswith a and endswith z",
224+
"tenant id equals 4446 and username startswith a and endswith z": {
230225
"when_match": expected_value_second_check,
231226
"conditions": [
232227
{
@@ -246,9 +241,9 @@ def test_toggles_conditions_rule_match_multiple_actions_multiple_rules_multiple_
246241
},
247242
],
248243
},
249-
],
244+
},
250245
}
251-
},
246+
}
252247
}
253248

254249
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
@@ -277,9 +272,8 @@ def test_toggles_match_rule_with_contains_action(mocker, config):
277272
"features": {
278273
"my_feature": {
279274
"default": False,
280-
"rules": [
281-
{
282-
"rule_name": "tenant id is contained in [6, 2]",
275+
"rules": {
276+
"tenant id is contained in [6, 2]": {
283277
"when_match": expected_value,
284278
"conditions": [
285279
{
@@ -288,10 +282,10 @@ def test_toggles_match_rule_with_contains_action(mocker, config):
288282
"value": ["6", "2"],
289283
}
290284
],
291-
},
292-
],
285+
}
286+
},
293287
}
294-
},
288+
}
295289
}
296290
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
297291
toggle = conf_store.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
@@ -331,9 +325,8 @@ def test_multiple_features_enabled(mocker, config):
331325
"features": {
332326
"my_feature": {
333327
"default": False,
334-
"rules": [
335-
{
336-
"rule_name": "tenant id is contained in [6, 2]",
328+
"rules": {
329+
"tenant id is contained in [6, 2]": {
337330
"when_match": True,
338331
"conditions": [
339332
{
@@ -342,16 +335,16 @@ def test_multiple_features_enabled(mocker, config):
342335
"value": ["6", "2"],
343336
}
344337
],
345-
},
346-
],
338+
}
339+
},
347340
},
348341
"my_feature2": {
349342
"default": True,
350343
},
351344
"my_feature3": {
352345
"default": False,
353346
},
354-
},
347+
}
355348
}
356349
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
357350
enabled_list: List[str] = conf_store.get_enabled_features(context={"tenant_id": "6", "username": "a"})
@@ -364,9 +357,8 @@ def test_multiple_features_only_some_enabled(mocker, config):
364357
"features": {
365358
"my_feature": { # rule will match here, feature is enabled due to rule match
366359
"default": False,
367-
"rules": [
368-
{
369-
"rule_name": "tenant id is contained in [6, 2]",
360+
"rules": {
361+
"tenant id is contained in [6, 2]": {
370362
"when_match": True,
371363
"conditions": [
372364
{
@@ -375,20 +367,20 @@ def test_multiple_features_only_some_enabled(mocker, config):
375367
"value": ["6", "2"],
376368
}
377369
],
378-
},
379-
],
370+
}
371+
},
380372
},
381373
"my_feature2": {
382374
"default": True,
383375
},
384376
"my_feature3": {
385377
"default": False,
386378
},
387-
"my_feature4": { # rule will not match here, feature is enabled by default
379+
# rule will not match here, feature is enabled by default
380+
"my_feature4": {
388381
"default": True,
389-
"rules": [
390-
{
391-
"rule_name": "tenant id equals 7",
382+
"rules": {
383+
"tenant id equals 7": {
392384
"when_match": False,
393385
"conditions": [
394386
{
@@ -397,10 +389,10 @@ def test_multiple_features_only_some_enabled(mocker, config):
397389
"value": "7",
398390
}
399391
],
400-
},
401-
],
392+
}
393+
},
402394
},
403-
},
395+
}
404396
}
405397
conf_store = init_configuration_store(mocker, mocked_app_config_schema, config)
406398
enabled_list: List[str] = conf_store.get_enabled_features(context={"tenant_id": "6", "username": "a"})
@@ -468,7 +460,7 @@ def test_is_rule_matched_no_matches(mocker, config):
468460
conf_store = init_configuration_store(mocker, {}, config)
469461

470462
# WHEN calling _is_rule_matched
471-
result = conf_store._is_rule_matched("name", rule, rules_context)
463+
result = conf_store._is_rule_matched(rule_name="dummy", feature_name="dummy", rule=rule, context=rules_context)
472464

473465
# THEN return False
474466
assert result is False

0 commit comments

Comments
 (0)