Skip to content

Commit d0c0388

Browse files
Gerald W. LesterGerald W. Lester
Gerald W. Lester
authored and
Gerald W. Lester
committed
Changes to "contains" logic to clarify and expand.
1 parent 68e2c8e commit d0c0388

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def _match_by_action(action: str, condition_value: Any, context_value: Any) -> b
4848
schema.RuleAction.ENDSWITH.value: lambda a, b: a.endswith(b),
4949
schema.RuleAction.IN.value: lambda a, b: a in b,
5050
schema.RuleAction.NOT_IN.value: lambda a, b: a not in b,
51+
schema.RuleAction.KEY_IN_VALUE.value: lambda a, b: a in b,
52+
schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b,
53+
schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a,
54+
schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a,
5155
}
5256

5357
try:

aws_lambda_powertools/utilities/feature_flags/schema.py

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class RuleAction(str, Enum):
2222
ENDSWITH = "ENDSWITH"
2323
IN = "IN"
2424
NOT_IN = "NOT_IN"
25+
KEY_IN_VALUE = "KEY_IN_VALUE"
26+
KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE"
27+
VALUE_IN_KEY = "VALUE_IN_KEY"
28+
VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY"
2529

2630

2731
class SchemaValidator(BaseValidator):

docs/utilities/feature_flags.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and
450450
}
451451
```
452452

453-
The `action` configuration can have 5 different values: `EQUALS`, `STARTSWITH`, `ENDSWITH`, `IN`, `NOT_IN`.
453+
The `action` configuration can have 5 different values: `EQUALS`, `STARTSWITH`, `ENDSWITH`, `KEY_IN_VALUE`, `KEY_NOT_IN_VALUE`, `VALUE_IN_KEY`, and `VALUE_NOT_IN_KEY`. Note that `IN` and `NOT_IN` are also defined and are synonymous with `KEY_IN_VALUE` and `KEY_NOT_IN_VALUE` respectively.
454454

455455
The `key` and `value` will be compared to the input from the context parameter.
456456

tests/functional/feature_flags/test_feature_flags.py

+195
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ def test_flags_conditions_rule_match_multiple_actions_multiple_rules_multiple_co
301301

302302

303303
# check a case where the feature exists but the rule doesn't match so we revert to the default value of the feature
304+
305+
# Check IN/NOT_IN/KEY_IN_VALUE/KEY_NOT_IN_VALUE/VALUE_IN_KEY/VALUE_NOT_IN_KEY conditions
304306
def test_flags_match_rule_with_in_action(mocker, config):
305307
expected_value = True
306308
mocked_app_config_schema = {
@@ -395,8 +397,201 @@ def test_flags_no_match_rule_with_not_in_action(mocker, config):
395397
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
396398
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
397399
assert toggle == expected_value
400+
401+
def test_flags_match_rule_with_key_in_value_action(mocker, config):
402+
expected_value = True
403+
mocked_app_config_schema = {
404+
"my_feature": {
405+
"default": False,
406+
"rules": {
407+
"tenant id is contained in [6, 2]": {
408+
"when_match": expected_value,
409+
"conditions": [
410+
{
411+
"action": RuleAction.KEY_IN_VALUE.value,
412+
"key": "tenant_id",
413+
"value": ["6", "2"],
414+
}
415+
],
416+
}
417+
},
418+
}
419+
}
420+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
421+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
422+
assert toggle == expected_value
423+
424+
425+
def test_flags_no_match_rule_with_key_in_value_action(mocker, config):
426+
expected_value = False
427+
mocked_app_config_schema = {
428+
"my_feature": {
429+
"default": expected_value,
430+
"rules": {
431+
"tenant id is contained in [8, 2]": {
432+
"when_match": True,
433+
"conditions": [
434+
{
435+
"action": RuleAction.KEY_IN_VALUE.value,
436+
"key": "tenant_id",
437+
"value": ["8", "2"],
438+
}
439+
],
440+
}
441+
},
442+
}
443+
}
444+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
445+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
446+
assert toggle == expected_value
447+
448+
449+
def test_flags_match_rule_with_key_not_in_value_action(mocker, config):
450+
expected_value = True
451+
mocked_app_config_schema = {
452+
"my_feature": {
453+
"default": False,
454+
"rules": {
455+
"tenant id is contained in [8, 2]": {
456+
"when_match": expected_value,
457+
"conditions": [
458+
{
459+
"action": RuleAction.KEY_NOT_IN_VALUE.value,
460+
"key": "tenant_id",
461+
"value": ["10", "4"],
462+
}
463+
],
464+
}
465+
},
466+
}
467+
}
468+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
469+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
470+
assert toggle == expected_value
471+
472+
473+
def test_flags_no_match_rule_with_key_not_in_value_action(mocker, config):
474+
expected_value = False
475+
mocked_app_config_schema = {
476+
"my_feature": {
477+
"default": expected_value,
478+
"rules": {
479+
"tenant id is contained in [8, 2]": {
480+
"when_match": True,
481+
"conditions": [
482+
{
483+
"action": RuleAction.KEY_NOT_IN_VALUE.value,
484+
"key": "tenant_id",
485+
"value": ["6", "4"],
486+
}
487+
],
488+
}
489+
},
490+
}
491+
}
492+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
493+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a"}, default=False)
494+
assert toggle == expected_value
495+
496+
def test_flags_match_rule_with_value_in_key_action(mocker, config):
497+
expected_value = True
498+
mocked_app_config_schema = {
499+
"my_feature": {
500+
"default": False,
501+
"rules": {
502+
"tenant id is contained in [6, 2]": {
503+
"when_match": expected_value,
504+
"conditions": [
505+
{
506+
"action": RuleAction.VALUE_IN_KEY.value,
507+
"key": "groups",
508+
"value": "SYSADMIN",
509+
}
510+
],
511+
}
512+
},
513+
}
514+
}
515+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
516+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False)
517+
assert toggle == expected_value
518+
519+
520+
def test_flags_no_match_rule_with_value_in_key_action(mocker, config):
521+
expected_value = False
522+
mocked_app_config_schema = {
523+
"my_feature": {
524+
"default": expected_value,
525+
"rules": {
526+
"tenant id is contained in [8, 2]": {
527+
"when_match": True,
528+
"conditions": [
529+
{
530+
"action": RuleAction.VALUE_IN_KEY.value,
531+
"key": "groups",
532+
"value": "GUEST",
533+
}
534+
],
535+
}
536+
},
537+
}
538+
}
539+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
540+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False)
541+
assert toggle == expected_value
542+
543+
544+
def test_flags_match_rule_with_value_not_in_key_action(mocker, config):
545+
expected_value = True
546+
mocked_app_config_schema = {
547+
"my_feature": {
548+
"default": False,
549+
"rules": {
550+
"tenant id is contained in [8, 2]": {
551+
"when_match": expected_value,
552+
"conditions": [
553+
{
554+
"action": RuleAction.VALUE_NOT_IN_KEY.value,
555+
"key": "groups",
556+
"value": "GUEST",
557+
}
558+
],
559+
}
560+
},
561+
}
562+
}
563+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
564+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False)
565+
assert toggle == expected_value
566+
567+
568+
def test_flags_no_match_rule_with_value_not_in_key_action(mocker, config):
569+
expected_value = False
570+
mocked_app_config_schema = {
571+
"my_feature": {
572+
"default": expected_value,
573+
"rules": {
574+
"tenant id is contained in [8, 2]": {
575+
"when_match": True,
576+
"conditions": [
577+
{
578+
"action": RuleAction.VALUE_NOT_IN_KEY.value,
579+
"key": "groups",
580+
"value": "SYSADMIN",
581+
}
582+
],
583+
}
584+
},
585+
}
586+
}
587+
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)
588+
toggle = feature_flags.evaluate(name="my_feature", context={"tenant_id": "6", "username": "a", "groups": ["SYSADMIN", "IT"]}, default=False)
589+
assert toggle == expected_value
590+
591+
398592

399593

594+
# Check multiple features
400595
def test_multiple_features_enabled(mocker, config):
401596
expected_value = ["my_feature", "my_feature2"]
402597
mocked_app_config_schema = {

tests/functional/feature_flags/test_schema_validation.py

+20
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,26 @@ def test_valid_condition_all_actions():
220220
CONDITION_KEY: "username",
221221
CONDITION_VALUE: ["c"],
222222
},
223+
{
224+
CONDITION_ACTION: RuleAction.KEY_IN_VALUE.value,
225+
CONDITION_KEY: "username",
226+
CONDITION_VALUE: ["a", "b"],
227+
},
228+
{
229+
CONDITION_ACTION: RuleAction.KEY_NOT_IN_VALUE.value,
230+
CONDITION_KEY: "username",
231+
CONDITION_VALUE: ["c"],
232+
},
233+
{
234+
CONDITION_ACTION: RuleAction.VALUE_IN_KEY.value,
235+
CONDITION_KEY: "groups",
236+
CONDITION_VALUE: "SYSADMIN",
237+
},
238+
{
239+
CONDITION_ACTION: RuleAction.VALUE_NOT_IN_KEY.value,
240+
CONDITION_KEY: "groups",
241+
CONDITION_VALUE: "GUEST",
242+
},
223243
],
224244
}
225245
},

0 commit comments

Comments
 (0)