Commits

Andy Mikhailenko  committed 93cf4ae

Validation: enable optional fields

  • Participants
  • Parent commits 5e8bbd2

Comments (0)

Files changed (2)

File monk/validation.py

 from manipulation import merged
 
 
+__all__ = [
+    # errors
+    'ValidationError', 'StructureSpecificationError', 'MissingKey',
+    'UnknownKey',
+    # validators
+    'ValueValidator', 'DictValidator', 'ListValidator', 'TypeValidator',
+    'InstanceValidator', 'FuncValidator',
+    # functions
+    'validate_structure_spec', 'validate_structure', 'validate_value',
+    # helpers
+    'Rule', 'optional'
+]
+
+
 class ValidationError(Exception):
     "Raised when a document or its part cannot pass validation."
 
         # any value is acceptable
         return
 
+    if isinstance(spec, Rule):
+        rule = spec
+    else:
+        rule = Rule(spec)
+
     for validator_class in validators:
-        validator = validator_class(spec, value, skip_missing, skip_unknown,
+        validator = validator_class(rule.spec, value, skip_missing, skip_unknown,
                    value_preprocessor=value_preprocessor)
         if validator.check():
             return validator.validate()
         pass  # for test coverage
 
 
+#def canonize(spec):
+#    canonic = {}
+#    for key, value in spec.iteritems():
+#        canonic[key] = value if isinstance(value, Rule) else Rule(value)
+#    return canonic
+
+
 def validate_structure(spec, data, skip_missing=False, skip_unknown=False,
                        validators=VALUE_VALIDATORS, value_preprocessor=None):
+                       #this_level_skip_missing=None,
+                       #this_level_skip_unknown=None):
     """ Validates given document against given structure specification.
     Always returns ``None``.
 
     missing = spec_keys - data_keys
     unknown = data_keys - spec_keys
 
-    if missing and not skip_missing:
-        raise MissingKey('Missing keys: {0}'.format(', '.join(missing)))
+#    if missing and not skip_missing:
+#        raise MissingKey('Missing keys: {0}'.format(', '.join(missing)))
 
     if unknown and not skip_unknown:
         raise UnknownKey('Unknown keys: {0}'.format(', '.join(unknown)))
     # check types and deal with nested lists
     for key in spec_keys | data_keys:
         typespec = spec.get(key)
-        value = data.get(key)
-        if value_preprocessor:
-            value = value_preprocessor(typespec, value)
-        try:
-            validate_value(typespec, value, validators,
-                           skip_missing, skip_unknown,
-                           value_preprocessor=value_preprocessor)
-        except (MissingKey, UnknownKey, TypeError) as e:
-            raise type(e)('{k}: {e}'.format(k=key, e=e))
+        rule = typespec if isinstance(typespec, Rule) else Rule(typespec)
+        if key in data_keys:
+            value = data.get(key)
+            if value_preprocessor:
+                value = value_preprocessor(typespec, value)
+            try:
+                validate_value(typespec, value, validators,
+                               skip_missing, skip_unknown,
+                               value_preprocessor=value_preprocessor)
+            except (MissingKey, UnknownKey, TypeError) as e:
+                raise type(e)('{k}: {e}'.format(k=key, e=e))
+        else:
+            if skip_missing or rule.skip_missing:
+                continue
+            raise MissingKey('{0}'.format(key))
 
 
 def validate_structure_spec(spec, validators=VALUE_VALIDATORS):
         return value
     validate_structure(spec, merged(spec, {}), skip_missing=True, skip_unknown=True,
                        validators=validators, value_preprocessor=dictmerger)
+
+
+class Rule:
+    "Extended specification of a field.  Allows marking it as optional."
+    def __init__(self, spec, skip_missing=False):
+        self.spec = spec
+        self.skip_missing = skip_missing
+
+    def __repr__(self):
+        return '<Rule {spec} missing:{missing}>'.format(
+            spec=str(self.spec).replace('<','[').replace('>',']'),
+            missing=self.skip_missing)
+
+
+optional = lambda x: Rule(x, skip_missing=True)

File unittests/test_validation.py

 
 from monk.validation import (
     validate_structure_spec, validate_structure, StructureSpecificationError,
-    MissingKey, UnknownKey
+    MissingKey, UnknownKey, Rule, optional
 )
 
 
 
         with pytest.raises(StructureSpecificationError) as excinfo:
             validate_structure_spec({'foo': [unicode, unicode]})
-        assert single_elem_err_msg in str(excinfo)
+        assert single_elem_err_msg in excinfo.exconly()
 
         with pytest.raises(StructureSpecificationError) as excinfo:
             validate_structure_spec({'foo': {'bar': [unicode, unicode]}})
-        assert single_elem_err_msg in str(excinfo)
+        assert single_elem_err_msg in excinfo.exconly()
 
         with pytest.raises(StructureSpecificationError) as excinfo:
             validate_structure_spec({'foo': {'bar': [{'baz': [unicode, unicode]}]}})
-        assert single_elem_err_msg in str(excinfo)
+        assert single_elem_err_msg in excinfo.exconly()
 
 
 class TestDocumentStructureValidation:
     def test_bad_types(self):
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': int}, {'a': 'bad'})
-        assert "a: expected int, got str 'bad'" in str(excinfo)
+        assert "a: expected int, got str 'bad'" in excinfo.exconly()
 
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': [int]}, {'a': 'bad'})
-        assert "a: expected list, got str 'bad'" in str(excinfo)
+        assert "a: expected list, got str 'bad'" in excinfo.exconly()
 
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': [int]}, {'a': ['bad']})
-        assert "a: expected int, got str 'bad'" in str(excinfo)
+        assert "a: expected int, got str 'bad'" in excinfo.exconly()
 
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': {'b': int}}, {'a': 'bad'})
-        assert "a: expected dict, got str 'bad'" in str(excinfo)
+        assert "a: expected dict, got str 'bad'" in excinfo.exconly()
 
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': {'b': int}}, {'a': {'b': 'bad'}})
-        assert "a: b: expected int, got str 'bad'" in str(excinfo)
+        assert "a: b: expected int, got str 'bad'" in excinfo.exconly()
 
         with pytest.raises(TypeError) as excinfo:
             validate_structure({'a': [{'b': [int]}]}, {'a': [{'b': ['bad']}]})
-        assert "a: b: expected int, got str 'bad'" in str(excinfo)
+        assert "a: b: expected int, got str 'bad'" in excinfo.exconly()
 
     def test_empty(self):
         validate_structure({'a': unicode}, {'a': None})
             ],
         }
         validate_structure(spec, data)
+
+
+class TestValidationRules:
+    def test_simple(self):
+        # simple rule behaves as the spec within it
+        spec = {
+            'a': Rule(int),
+        }
+        validate_structure(spec, {'a': 1})
+        with pytest.raises(MissingKey):
+            validate_structure(spec, {})
+        with pytest.raises(TypeError):
+            validate_structure(spec, {'a': 'bogus'})
+
+    def test_skip_missing(self):
+        # the rule modifies behaviour of nested validator
+        spec = {
+            'a': optional(int),
+        }
+        validate_structure(spec, {})
+
+    def test_skip_missing_nested(self):
+        spec = {
+            'a': {'b': optional(int)},
+        }
+
+        validate_structure(spec, {'a': None})
+
+        with pytest.raises(MissingKey) as excinfo:
+            validate_structure(spec, {})
+        assert excinfo.exconly() == 'MissingKey: a'
+
+        validate_structure(spec, {'a': {}})
+
+    def test_skip_missing_nested_required(self):
+        "optional dict contains a dict with required values"
+        spec = {
+            'a': optional({'b': int}),
+        }
+
+        # None is OK (optional)
+        validate_structure(spec, {'a': None})
+
+        # empty dict is OK (optional)
+        validate_structure(spec, {})
+
+        # empty subdict fails because only its parent is optional
+        with pytest.raises(MissingKey) as excinfo:
+            validate_structure(spec, {'a': {}})
+        assert excinfo.exconly() == 'MissingKey: a: b'