]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add constraint checking to Property schema
authorZane Bitter <zbitter@redhat.com>
Tue, 13 Aug 2013 08:48:15 +0000 (10:48 +0200)
committerZane Bitter <zbitter@redhat.com>
Tue, 13 Aug 2013 08:48:23 +0000 (10:48 +0200)
Change-Id: Ibec4068a0497ca6ac1dd25decd27fa87585b18a3

heat/engine/properties.py
heat/tests/test_properties.py

index a1564132997c80d99b87b041c32fbb8c9147046b..33198e824ecbb5181732cc08fe61c9d0a87df89a 100644 (file)
@@ -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
index 542635a83652fc23be8018af45c390ce970359a1..944ef5ad7cf2dbe66537400f31b79230a5b3e493 100644 (file)
@@ -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',