Skip to content

Commit a55131f

Browse files
committed
refactor: add SchemaValidationError
1 parent e9bb190 commit a55131f

File tree

5 files changed

+61
-41
lines changed

5 files changed

+61
-41
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
class ConfigurationError(Exception):
2-
"""When a a configuration store raises an exception on config retrieval or parsing"""
2+
"""When a configuration store raises an exception on config retrieval or parsing"""
3+
4+
5+
class SchemaValidationError(Exception):
6+
"""When feature flag schema fails validation"""

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def get_configuration(self) -> Union[Dict[str, Dict], Dict]:
105105
------
106106
ConfigurationError
107107
Any validation error
108+
SchemaValidationError
109+
When schema doesn't conform with feature flag schema
108110
109111
Returns
110112
------
@@ -170,6 +172,11 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
170172
------
171173
bool
172174
whether feature should be enabled or not
175+
176+
Raises
177+
------
178+
SchemaValidationError
179+
When schema doesn't conform with feature flag schema
173180
"""
174181
if context is None:
175182
context = {}
@@ -214,6 +221,11 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
214221
```python
215222
["premium_features", "my_feature_two", "always_true_feature"]
216223
```
224+
225+
Raises
226+
------
227+
SchemaValidationError
228+
When schema doesn't conform with feature flag schema
217229
"""
218230
if context is None:
219231
context = {}

aws_lambda_powertools/utilities/feature_flags/schema.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Any, Dict, List, Optional
44

55
from .base import BaseValidator
6-
from .exceptions import ConfigurationError
6+
from .exceptions import SchemaValidationError
77

88
logger = logging.getLogger(__name__)
99

@@ -26,6 +26,11 @@ class RuleAction(str, Enum):
2626
class SchemaValidator(BaseValidator):
2727
"""Validates feature flag schema configuration
2828
29+
Raises
30+
------
31+
SchemaValidationError
32+
When schema doesn't conform with feature flag schema
33+
2934
Schema
3035
------
3136
@@ -105,7 +110,7 @@ def __init__(self, schema: Dict[str, Any]):
105110
def validate(self) -> None:
106111
logger.debug("Validating schema")
107112
if not isinstance(self.schema, dict):
108-
raise ConfigurationError(f"Features must be a dictionary, schema={str(self.schema)}")
113+
raise SchemaValidationError(f"Features must be a dictionary, schema={str(self.schema)}")
109114

110115
features = FeaturesValidator(schema=self.schema)
111116
features.validate()
@@ -127,11 +132,11 @@ def validate(self):
127132
@staticmethod
128133
def validate_feature(name, feature):
129134
if not feature or not isinstance(feature, dict):
130-
raise ConfigurationError(f"Feature must be a non-empty dictionary, feature={name}")
135+
raise SchemaValidationError(f"Feature must be a non-empty dictionary, feature={name}")
131136

132137
default_value = feature.get(FEATURE_DEFAULT_VAL_KEY)
133138
if default_value is None or not isinstance(default_value, bool):
134-
raise ConfigurationError(f"feature 'default' boolean key must be present, feature={name}")
139+
raise SchemaValidationError(f"feature 'default' boolean key must be present, feature={name}")
135140

136141

137142
class RulesValidator(BaseValidator):
@@ -148,7 +153,7 @@ def validate(self):
148153
return
149154

150155
if not isinstance(self.rules, dict):
151-
raise ConfigurationError(f"Feature rules must be a dictionary, feature={self.feature_name}")
156+
raise SchemaValidationError(f"Feature rules must be a dictionary, feature={self.feature_name}")
152157

153158
for rule_name, rule in self.rules.items():
154159
logger.debug(f"Attempting to validate rule '{rule_name}'")
@@ -159,21 +164,21 @@ def validate(self):
159164
@staticmethod
160165
def validate_rule(rule, rule_name, feature_name):
161166
if not rule or not isinstance(rule, dict):
162-
raise ConfigurationError(f"Feature rule must be a dictionary, feature={feature_name}")
167+
raise SchemaValidationError(f"Feature rule must be a dictionary, feature={feature_name}")
163168

164169
RulesValidator.validate_rule_name(rule_name=rule_name, feature_name=feature_name)
165170
RulesValidator.validate_rule_default_value(rule=rule, rule_name=rule_name)
166171

167172
@staticmethod
168173
def validate_rule_name(rule_name: str, feature_name: str):
169174
if not rule_name or not isinstance(rule_name, str):
170-
raise ConfigurationError(f"Rule name key must have a non-empty string, feature={feature_name}")
175+
raise SchemaValidationError(f"Rule name key must have a non-empty string, feature={feature_name}")
171176

172177
@staticmethod
173178
def validate_rule_default_value(rule: Dict, rule_name: str):
174179
rule_default_value = rule.get(RULE_MATCH_VALUE)
175180
if not isinstance(rule_default_value, bool):
176-
raise ConfigurationError(f"'rule_default_value' key must have be bool, rule={rule_name}")
181+
raise SchemaValidationError(f"'rule_default_value' key must have be bool, rule={rule_name}")
177182

178183

179184
class ConditionsValidator(BaseValidator):
@@ -183,15 +188,15 @@ def __init__(self, rule: Dict[str, Any], rule_name: str):
183188

184189
def validate(self):
185190
if not self.conditions or not isinstance(self.conditions, list):
186-
raise ConfigurationError(f"Invalid condition, rule={self.rule_name}")
191+
raise SchemaValidationError(f"Invalid condition, rule={self.rule_name}")
187192

188193
for condition in self.conditions:
189194
self.validate_condition(rule_name=self.rule_name, condition=condition)
190195

191196
@staticmethod
192197
def validate_condition(rule_name: str, condition: Dict[str, str]) -> None:
193198
if not condition or not isinstance(condition, dict):
194-
raise ConfigurationError(f"Feature rule condition must be a dictionary, rule={rule_name}")
199+
raise SchemaValidationError(f"Feature rule condition must be a dictionary, rule={rule_name}")
195200

196201
# Condition can contain PII data; do not log condition value
197202
logger.debug(f"Attempting to validate condition for '{rule_name}'")
@@ -204,18 +209,18 @@ def validate_condition_action(condition: Dict[str, Any], rule_name: str):
204209
action = condition.get(CONDITION_ACTION, "")
205210
if action not in RuleAction.__members__:
206211
allowed_values = [_action.value for _action in RuleAction]
207-
raise ConfigurationError(
212+
raise SchemaValidationError(
208213
f"'action' value must be either {allowed_values}, rule_name={rule_name}, action={action}"
209214
)
210215

211216
@staticmethod
212217
def validate_condition_key(condition: Dict[str, Any], rule_name: str):
213218
key = condition.get(CONDITION_KEY, "")
214219
if not key or not isinstance(key, str):
215-
raise ConfigurationError(f"'key' value must be a non empty string, rule={rule_name}")
220+
raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}")
216221

217222
@staticmethod
218223
def validate_condition_value(condition: Dict[str, Any], rule_name: str):
219224
value = condition.get(CONDITION_VALUE, "")
220225
if not value:
221-
raise ConfigurationError(f"'value' key must not be empty, rule={rule_name}")
226+
raise SchemaValidationError(f"'value' key must not be empty, rule={rule_name}")

tests/functional/feature_toggles/test_feature_toggles.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,8 @@ def test_toggles_no_match_rule_with_contains_action(mocker, config):
291291
mocked_app_config_schema = {
292292
"my_feature": {
293293
"default": expected_value,
294-
"rules": [
295-
{
296-
"rule_name": "tenant id is contained in [8, 2]",
294+
"rules": {
295+
"tenant id is contained in [8, 2]": {
297296
"when_match": True,
298297
"conditions": [
299298
{
@@ -302,8 +301,8 @@ def test_toggles_no_match_rule_with_contains_action(mocker, config):
302301
"value": ["8", "2"],
303302
}
304303
],
305-
},
306-
],
304+
}
305+
},
307306
}
308307
}
309308
feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config)

tests/functional/feature_toggles/test_schema_validation.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest # noqa: F401
44

5-
from aws_lambda_powertools.utilities.feature_flags.exceptions import ConfigurationError
5+
from aws_lambda_powertools.utilities.feature_flags.exceptions import SchemaValidationError
66
from aws_lambda_powertools.utilities.feature_flags.schema import (
77
CONDITION_ACTION,
88
CONDITION_KEY,
@@ -24,7 +24,7 @@
2424

2525
def test_invalid_features_dict():
2626
validator = SchemaValidator(schema=[])
27-
with pytest.raises(ConfigurationError):
27+
with pytest.raises(SchemaValidationError):
2828
validator.validate()
2929

3030

@@ -45,7 +45,7 @@ def test_empty_features_not_fail():
4545
)
4646
def test_invalid_feature(schema):
4747
validator = SchemaValidator(schema)
48-
with pytest.raises(ConfigurationError):
48+
with pytest.raises(SchemaValidationError):
4949
validator.validate()
5050

5151

@@ -73,7 +73,7 @@ def test_invalid_rule():
7373
}
7474
}
7575
validator = SchemaValidator(schema)
76-
with pytest.raises(ConfigurationError):
76+
with pytest.raises(SchemaValidationError):
7777
validator.validate()
7878

7979
# rules RULE_MATCH_VALUE is not bool
@@ -88,7 +88,7 @@ def test_invalid_rule():
8888
}
8989
}
9090
validator = SchemaValidator(schema)
91-
with pytest.raises(ConfigurationError):
91+
with pytest.raises(SchemaValidationError):
9292
validator.validate()
9393

9494
# missing conditions list
@@ -103,7 +103,7 @@ def test_invalid_rule():
103103
}
104104
}
105105
validator = SchemaValidator(schema)
106-
with pytest.raises(ConfigurationError):
106+
with pytest.raises(SchemaValidationError):
107107
validator.validate()
108108

109109
# condition list is empty
@@ -116,7 +116,7 @@ def test_invalid_rule():
116116
}
117117
}
118118
validator = SchemaValidator(schema)
119-
with pytest.raises(ConfigurationError):
119+
with pytest.raises(SchemaValidationError):
120120
validator.validate()
121121

122122
# condition is invalid type, not list
@@ -129,7 +129,7 @@ def test_invalid_rule():
129129
}
130130
}
131131
validator = SchemaValidator(schema)
132-
with pytest.raises(ConfigurationError):
132+
with pytest.raises(SchemaValidationError):
133133
validator.validate()
134134

135135

@@ -147,7 +147,7 @@ def test_invalid_condition():
147147
}
148148
}
149149
validator = SchemaValidator(schema)
150-
with pytest.raises(ConfigurationError):
150+
with pytest.raises(SchemaValidationError):
151151
validator.validate()
152152

153153
# missing condition key and value
@@ -163,7 +163,7 @@ def test_invalid_condition():
163163
}
164164
}
165165
validator = SchemaValidator(schema)
166-
with pytest.raises(ConfigurationError):
166+
with pytest.raises(SchemaValidationError):
167167
validator.validate()
168168

169169
# invalid condition key type, not string
@@ -183,7 +183,7 @@ def test_invalid_condition():
183183
}
184184
}
185185
validator = SchemaValidator(schema)
186-
with pytest.raises(ConfigurationError):
186+
with pytest.raises(SchemaValidationError):
187187
validator.validate()
188188

189189

@@ -229,8 +229,8 @@ def test_validate_condition_invalid_condition_type():
229229
condition = {}
230230

231231
# WHEN calling validate_condition
232-
# THEN raise ConfigurationError
233-
with pytest.raises(ConfigurationError, match="Feature rule condition must be a dictionary"):
232+
# THEN raise SchemaValidationError
233+
with pytest.raises(SchemaValidationError, match="Feature rule condition must be a dictionary"):
234234
ConditionsValidator.validate_condition(condition=condition, rule_name="dummy")
235235

236236

@@ -239,8 +239,8 @@ def test_validate_condition_invalid_condition_action():
239239
condition = {"action": "INVALID", "key": "tenant_id", "value": "12345"}
240240

241241
# WHEN calling validate_condition
242-
# THEN raise ConfigurationError
243-
with pytest.raises(ConfigurationError, match="'action' value must be either"):
242+
# THEN raise SchemaValidationError
243+
with pytest.raises(SchemaValidationError, match="'action' value must be either"):
244244
ConditionsValidator.validate_condition_action(condition=condition, rule_name="dummy")
245245

246246

@@ -249,8 +249,8 @@ def test_validate_condition_invalid_condition_key():
249249
condition = {"action": RuleAction.EQUALS.value, "value": "12345"}
250250

251251
# WHEN calling validate_condition
252-
# THEN raise ConfigurationError
253-
with pytest.raises(ConfigurationError, match="'key' value must be a non empty string"):
252+
# THEN raise SchemaValidationError
253+
with pytest.raises(SchemaValidationError, match="'key' value must be a non empty string"):
254254
ConditionsValidator.validate_condition_key(condition=condition, rule_name="dummy")
255255

256256

@@ -262,21 +262,21 @@ def test_validate_condition_missing_condition_value():
262262
}
263263

264264
# WHEN calling validate_condition
265-
with pytest.raises(ConfigurationError, match="'value' key must not be empty"):
265+
with pytest.raises(SchemaValidationError, match="'value' key must not be empty"):
266266
ConditionsValidator.validate_condition_value(condition=condition, rule_name="dummy")
267267

268268

269269
def test_validate_rule_invalid_rule_type():
270270
# GIVEN an invalid rule type of empty list
271271
# WHEN calling validate_rule
272-
# THEN raise ConfigurationError
273-
with pytest.raises(ConfigurationError, match="Feature rule must be a dictionary"):
272+
# THEN raise SchemaValidationError
273+
with pytest.raises(SchemaValidationError, match="Feature rule must be a dictionary"):
274274
RulesValidator.validate_rule(rule=[], rule_name="dummy", feature_name="dummy")
275275

276276

277277
def test_validate_rule_invalid_rule_name():
278278
# GIVEN a rule name is empty
279279
# WHEN calling validate_rule_name
280-
# THEN raise ConfigurationError
281-
with pytest.raises(ConfigurationError, match="Rule name key must have a non-empty string"):
280+
# THEN raise SchemaValidationError
281+
with pytest.raises(SchemaValidationError, match="Rule name key must have a non-empty string"):
282282
RulesValidator.validate_rule_name(rule_name="", feature_name="dummy")

0 commit comments

Comments
 (0)