From 68841467ee5e113e79541ed1b2b2be9b41627c5c Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Tue, 13 Aug 2013 10:48:15 +0200 Subject: [PATCH] Add constraint checking to Property schema Change-Id: Ibec4068a0497ca6ac1dd25decd27fa87585b18a3 --- heat/engine/properties.py | 59 +++++++++++++++++++++++++++++++++++ heat/tests/test_properties.py | 24 ++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/heat/engine/properties.py b/heat/engine/properties.py index a1564132..33198e82 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -111,6 +111,12 @@ class Schema(collections.Mapping): raise InvalidPropertySchemaError(err_msg) self.default = default + if self.default is not None: + try: + self.validate_constraints(self.default) + except (ValueError, TypeError) as exc: + raise InvalidPropertySchemaError('Invalid default %s (%s)' % + (self.default, exc)) @classmethod def from_legacy(cls, schema_dict): @@ -168,6 +174,10 @@ class Schema(collections.Mapping): constraints=list(constraints()), implemented=schema_dict.get(IMPLEMENTED, True)) + def validate_constraints(self, value): + for constraint in self.constraints: + constraint.validate(value) + def __getitem__(self, key): if key == self.TYPE: return self.type.lower() @@ -242,6 +252,14 @@ class Constraint(collections.Mapping): def __init__(self, description=None): self.description = description + def validate(self, value): + if not self._is_valid(value): + if self.description: + err_msg = self.description + else: + err_msg = self._err_msg(value) + raise ValueError(err_msg) + @classmethod def _name(cls): return '_'.join(w.lower() for w in re.findall('[A-Z]?[a-z]+', @@ -293,6 +311,24 @@ class Range(Constraint): if not isinstance(param, (float, int, long, type(None))): raise InvalidPropertySchemaError('min/max must be numeric') + def _err_msg(self, value): + return '%s is out of range (min: %s, max: %s)' % (value, + self.min, + self.max) + + def _is_valid(self, value): + value = Property.str_to_num(value) + + if self.min is not None: + if value < self.min: + return False + + if self.max is not None: + if value > self.max: + return False + + return True + def _constraint(self): def constraints(): if self.min is not None: @@ -325,6 +361,14 @@ class Length(Range): msg = 'min/max length must be integral' raise InvalidPropertySchemaError(msg) + def _err_msg(self, value): + return 'length (%d) is out of range (min: %s, max: %s)' % (len(value), + self.min, + self.max) + + def _is_valid(self, value): + return super(Length, self)._is_valid(len(value)) + class AllowedValues(Constraint): """ @@ -347,6 +391,13 @@ class AllowedValues(Constraint): raise InvalidPropertySchemaError('AllowedValues must be a list') self.allowed = tuple(allowed) + def _err_msg(self, value): + allowed = '[%s]' % ', '.join(str(a) for a in self.allowed) + return '"%s" is not an allowed value %s' % (value, allowed) + + def _is_valid(self, value): + return value in self.allowed + def _constraint(self): return list(self.allowed) @@ -368,6 +419,14 @@ class AllowedPattern(Constraint): def __init__(self, pattern, description=None): super(AllowedPattern, self).__init__(description) self.pattern = pattern + self.match = re.compile(pattern).match + + def _err_msg(self, value): + return '"%s" does not match pattern "%s"' % (value, self.pattern) + + def _is_valid(self, value): + match = self.match(value) + return match is not None and match.end() == len(value) def _constraint(self): return self.pattern diff --git a/heat/tests/test_properties.py b/heat/tests/test_properties.py index 542635a8..944ef5ad 100644 --- a/heat/tests/test_properties.py +++ b/heat/tests/test_properties.py @@ -63,6 +63,30 @@ class SchemaTest(testtools.TestCase): description='alphanumeric') self.assertEqual(d, dict(r)) + def test_range_validate(self): + r = properties.Range(min=5, max=5, description='a range') + r.validate(5) + + def test_range_min_fail(self): + r = properties.Range(min=5, description='a range') + self.assertRaises(ValueError, r.validate, 4) + + def test_range_max_fail(self): + r = properties.Range(max=5, description='a range') + self.assertRaises(ValueError, r.validate, 6) + + def test_length_validate(self): + l = properties.Length(min=5, max=5, description='a range') + l.validate('abcde') + + def test_length_min_fail(self): + l = properties.Length(min=5, description='a range') + self.assertRaises(ValueError, l.validate, 'abcd') + + def test_length_max_fail(self): + l = properties.Length(max=5, description='a range') + self.assertRaises(ValueError, l.validate, 'abcdef') + def test_schema_all(self): d = { 'type': 'string', -- 2.45.2