Commits

Andy Mikhailenko  committed 195fc1b

Fix issue #8: User can pass a list of custom validators to a Rule object

  • Participants
  • Parent commits d08dfeb
  • Tags 0.8.0

Comments (0)

Files changed (7)

File docs/similar.rst

      to subclass `Rule` in order to combine this datatype with an `inner_spec`
      in a meaningful manner.
 
-  .. warning:: Validators
-
-     At the moment Monk does not support `Rule(validators=...)`.
-     See issue8_
-
-  .. _issue8: https://bitbucket.org/neithere/monk/issue/8/user-can-pass-a-list-of-custom-validators
-
 Validation
 ----------
 

File monk/__init__.py

 .. _pymongo: http://api.mongodb.org/python/current/
 
 """
-__version__ = '0.7.0'
+__version__ = '0.8.0'
 # remember to also update:
 #
 # * PKGBUILD

File monk/schema.py

 Schema Definition
 ~~~~~~~~~~~~~~~~~
 """
-from . import compat, errors
+from . import compat, errors, validators
 
 
 __all__ = [
         ``bool``; if ``True``, :class:`~monk.validation.UnknownKey`
         is never raised.  Default is ``False``.
 
+    :param validators:
+        a list of callables.
+
     .. note:: Missing Value vs. Missing Key vs. Unknown Key
 
        :MissingValue:
             (of course on dictionary level).
 
     """
-    def __init__(self, datatype, inner_spec=None, optional=False, dict_allow_unknown_keys=False, default=None):
+    def __init__(self, datatype, inner_spec=None, optional=False,
+                 dict_allow_unknown_keys=False, default=None, validators=None):
         if isinstance(datatype, type(self)):
             raise ValueError('Cannot use a Rule instance as datatype')
         self.datatype = datatype
         self.optional = optional
         self.dict_allow_unknown_keys = dict_allow_unknown_keys
         self.default = default
+        self.validators = validators or []
 
         # sanity checks
 
 
 any_or_none = Rule(None, optional=True)
 "A shortcut for ``Rule(None, optional=True)``"
+
+
+def one_of(choices, first_is_default=False):
+    """
+    A shortcut::
+
+        choices = ['foo', 'bar']
+
+        # these expressions are equal:
+
+        one_of(choices)
+
+        Rule(str, validators=[monk.validators.validate_choice(choices)])
+
+        # default value can be taken from the first choice:
+
+        one_of(choices, first_is_default=True)
+
+        Rule(str, default=choices[0],
+             validators=[monk.validators.choice(choices)])
+
+    """
+    assert choices
+
+    if first_is_default:
+        default_choice = choices[0]
+    else:
+        default_choice = None
+
+    return Rule(datatype=type(default_choice),
+                default=default_choice,
+                validators=[validators.validate_choice(choices)])
+
+
+def in_range(start, stop, first_is_default=False):
+    """
+    A shortcut::
+
+        # these expressions are equal:
+
+        in_range(0, 200)
+
+        Rule(str, validators=[monk.validators.validate_range(0, 200)])
+
+        # default value can be taken from the first choice:
+
+        in_range(0, 200, first_is_default=True)
+
+        Rule(str, default=0,
+             validators=[monk.validators.validate_range(0, 200)])
+
+    """
+    if first_is_default:
+        default_value = start
+    else:
+        default_value = None
+
+    return Rule(datatype=int,
+                default=default_value,
+                validators=[validators.validate_range(start, stop)])

File monk/validation.py

         assert not rule.inner_spec
         if isinstance(rule.datatype, type):
             validate_type(rule, value)
+
+    for validator in rule.validators:
+        validator(value)

File monk/validators.py

+# -*- coding: utf-8 -*-
+#
+#    Monk is an unobtrusive data modeling, manipulation and validation library.
+#    Copyright © 2011—2013  Andrey Mikhaylenko
+#
+#    This file is part of Monk.
+#
+#    Monk is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Lesser General Public License as published
+#    by the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    Monk is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public License
+#    along with Monk.  If not, see <http://gnu.org/licenses/>.
+"""
+Validators
+~~~~~~~~~~
+"""
+from monk.errors import ValidationError
+
+
+def validate_choice(choices):
+    def _validate_choice(value):
+        if value not in choices:
+            raise ValidationError('expected one of {0}, got {1!r}'.format(choices, value))
+    return _validate_choice
+
+
+def validate_range(start, stop):
+    def _validate_range(value):
+        if not start <= value <= stop:
+            raise ValidationError('expected value in range {0}..{1}, got {2!r}'.format(start, stop, value))
+    return _validate_range
+
+
+def validate_length(expected):
+    def _validate_length(value):
+        if len(value) != expected:
+            raise ValidationError('expected value of length {0}, got {1!r}'.format(expected, value))
+    return _validate_length
+

File unittests/test_validation.py

 import pytest
 
 from monk.compat import text_type, safe_unicode
-from monk.errors import MissingKey, MissingValue, UnknownKey
+from monk.errors import MissingKey, MissingValue, UnknownKey, ValidationError
 from monk.schema import Rule, optional, any_value, any_or_none
 from monk.validation import validate
 
     def test_optional(self):
         assert optional(str) == Rule(str, optional=True)
         assert optional(Rule(str)) == Rule(str, optional=True)
+
+
+class TestCustomValidators:
+
+    def test_single(self):
+        def validate_foo(value):
+            if value != 'foo':
+                raise ValidationError('value must be "foo"')
+
+        spec = Rule(str, validators=[validate_foo])
+
+        validate(spec, 'foo')
+
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 'bar')
+        assert 'value must be "foo"' in excinfo.exconly()
+
+    def test_multiple(self):
+
+        def validate_gt_2(value):
+            if value <= 2:
+                raise ValidationError('value must be greater than 2')
+
+        def validate_lt_5(value):
+            if 5 <= value:
+                raise ValidationError('value must be lesser than 5')
+
+        spec = Rule(int, validators=[validate_gt_2, validate_lt_5])
+
+        # first validator fails
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 1)
+        assert 'value must be greater than 2' in excinfo.exconly()
+
+        # both validators ok
+        validate(spec, 3)
+
+        # second validator fails
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 6)
+        assert 'value must be lesser than 5' in excinfo.exconly()
+

File unittests/test_validators.py

+# -*- coding: utf-8 -*-
+#
+#    Monk is an unobtrusive data modeling, manipulation and validation library.
+#    Copyright © 2011—2013  Andrey Mikhaylenko
+#
+#    This file is part of Monk.
+#
+#    Monk is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Lesser General Public License as published
+#    by the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    Monk is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public License
+#    along with Monk.  If not, see <http://gnu.org/licenses/>.
+"""
+Validators tests
+================
+"""
+import pytest
+
+from monk.errors import ValidationError
+from monk.schema import Rule
+from monk.validation import validate
+from monk import validators
+
+
+class TestValidators:
+
+    def test_choice(self):
+
+        choices = 'a', 'b'
+        spec = Rule(str, validators=[validators.validate_choice(choices)])
+
+        validate(spec, 'a')
+        validate(spec, 'b')
+
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 'c')
+        assert "expected one of ('a', 'b'), got 'c'" in excinfo.exconly()
+
+    def test_range(self):
+
+        spec = Rule(int, validators=[validators.validate_range(2, 5)])
+
+        validate(spec, 2)
+        validate(spec, 4)
+
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 6)
+        assert 'expected value in range 2..5, got 6' in excinfo.exconly()
+
+    def test_length(self):
+
+        spec = Rule(str, validators=[validators.validate_length(3)])
+
+        validate(spec, 'foo')
+
+        with pytest.raises(ValidationError) as excinfo:
+            validate(spec, 'foobar')
+        assert "expected value of length 3, got 'foobar'" in excinfo.exconly()
+