Skip to content

Commit c837e0a

Browse files
gwlesterGerald W. Lesterheitorlessa
authored
feat(feature-flags): improve "IN/NOT_IN"; new rule actions (#710)
Co-authored-by: Gerald W. Lester <[email protected]> Co-authored-by: heitorlessa <[email protected]>
1 parent 19b1526 commit c837e0a

File tree

5 files changed

+256
-4
lines changed

5 files changed

+256
-4
lines changed

Diff for: 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:

Diff for: aws_lambda_powertools/utilities/feature_flags/schema.py

+7-1
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):
@@ -80,7 +84,9 @@ class SchemaValidator(BaseValidator):
8084
The value MUST contain the following members:
8185
8286
* **action**: `str`. Operation to perform to match a key and value.
83-
The value MUST be either EQUALS, STARTSWITH, ENDSWITH, IN, NOT_IN
87+
The value MUST be either EQUALS, STARTSWITH, ENDSWITH,
88+
KEY_IN_VALUE KEY_NOT_IN_VALUE VALUE_IN_KEY VALUE_NOT_IN_KEY
89+
8490
* **key**: `str`. Key in given context to perform operation
8591
* **value**: `Any`. Value in given context that should match action operation.
8692

Diff for: docs/utilities/feature_flags.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ You can use `get_enabled_features` method for scenarios where you need a list of
366366
"when_match": true,
367367
"conditions": [
368368
{
369-
"action": "IN",
369+
"action": "KEY_IN_VALUE",
370370
"key": "CloudFront-Viewer-Country",
371371
"value": ["NL", "IE", "UK", "PL", "PT"]
372372
}
@@ -450,9 +450,20 @@ 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 the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above:
454454

455-
The `key` and `value` will be compared to the input from the context parameter.
455+
Action | Equivalent expression
456+
------------------------------------------------- | ---------------------------------------------------------------------------------
457+
**EQUALS** | `lambda a, b: a == b`
458+
**STARTSWITH** | `lambda a, b: a.startswith(b)`
459+
**ENDSWITH** | `lambda a, b: a.endswith(b)`
460+
**KEY_IN_VALUE** | `lambda a, b: a in b`
461+
**KEY_NOT_IN_VALUE** | `lambda a, b: a not in b`
462+
**VALUE_IN_KEY** | `lambda a, b: b in a`
463+
**VALUE_NOT_IN_KEY** | `lambda a, b: b not in a`
464+
465+
466+
!!! info "The `**key**` and `**value**` will be compared to the input from the `**context**` parameter."
456467

457468
**For multiple conditions**, we will evaluate the list of conditions as a logical `AND`, so all conditions needs to match to return `when_match` value.
458469

@@ -671,3 +682,11 @@ Method | When to use | Requires new deployment on changes | Supported services
671682
**[Environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html){target="_blank"}** | Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment. | Yes | Lambda
672683
**[Parameters utility](parameters.md)** | Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB. | No | Parameter Store, DynamoDB, Secrets Manager, AppConfig
673684
**Feature flags utility** | Rule engine to define when one or multiple features should be enabled depending on the input. | No | AppConfig
685+
686+
687+
## Deprecation list when GA
688+
689+
Breaking change | Recommendation
690+
------------------------------------------------- | ---------------------------------------------------------------------------------
691+
`IN` RuleAction | Use `KEY_IN_VALUE` instead
692+
`NOT_IN` RuleAction | Use `KEY_NOT_IN_VALUE` instead

Diff for: tests/functional/feature_flags/test_feature_flags.py

+203
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 = {
@@ -397,6 +399,207 @@ def test_flags_no_match_rule_with_not_in_action(mocker, config):
397399
assert toggle == expected_value
398400

399401

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

Diff for: 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)