Repository: rossmacarthur/serde

Issue:
Add length validate methods
Example

```python
from serde import Model, fields, validate

class Example(Model):
    a = fields.Field(validators=[validate.length(10)])
    b = fields.Field(validators=[validate.length_between(10, 20, inclusive=False)])
    c = fields.Field(validators=[validate.length_min(10, inclusive=True)])
    d = fields.Field(validators=[validate.length_max(10)])
```

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=h1) Report
> Merging [#69](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=desc) into [master](https://codecov.io/gh/rossmacarthur/serde/commit/ef2580e4983e4e326080c59606527a2038c4c6c4?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/rossmacarthur/serde/pull/69/graphs/tree.svg?width=650&token=kZWHiIewTt&height=150&src=pr)](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #69   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           6      6           
  Lines         537    559   +22     
=====================================
+ Hits          537    559   +22
```


| [Impacted Files](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/serde/validate.py](https://codecov.io/gh/rossmacarthur/serde/pull/69/diff?src=pr&el=tree#diff-c3JjL3NlcmRlL3ZhbGlkYXRlLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=footer). Last update [ef2580e...5427f89](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/RELEASES.rst b/RELEASES.rst
index 53a17a1..dfb0492 100644
--- a/RELEASES.rst
+++ b/RELEASES.rst
@@ -6,10 +6,14 @@ Releases
 
 *Unreleased*
 
-- Add basestring and unicode to built-in Field map (`#68`_)
+- Add equal and length validators. (`#67`_, `#69`_)
+- Add basestring and unicode to built-in Field map. (`#68`_)
 
+.. _#69: https://github.com/rossmacarthur/serde/pull/69
 .. _#68: https://github.com/rossmacarthur/serde/pull/68
 
+.. _#67: https://github.com/rossmacarthur/serde/issues/67
+
 0.4.1
 -----
 
diff --git a/src/serde/validate.py b/src/serde/validate.py
index 0a07c29..111f1e7 100644
--- a/src/serde/validate.py
+++ b/src/serde/validate.py
@@ -54,6 +54,36 @@ def instance(type):
     return instance_
 
 
+def equal(to):
+    """
+    Validate that a value is equal to something.
+
+    Args:
+        to: the value to check against.
+
+    Returns:
+        function: the validator function.
+    """
+    def equal_(value):
+        """
+        Validate that the given value is equal to the specified value.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value is not equal to
+                the expected value.
+        """
+        if value != to:
+            raise ValidationError(
+                'expected {!r} but got {!r}'
+                .format(to, value)
+            )
+
+    return equal_
+
+
 def min(endpoint, inclusive=True):
     """
     Validate that a value is greater than and/or equal to the given endpoint.
@@ -159,6 +189,115 @@ def between(min_endpoint, max_endpoint, inclusive=True):
     return between_
 
 
+def length(length):
+    """
+    Validate that the given value has a particular length.
+
+    Args:
+        length (int): the length allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_(value):
+        """
+        Validate that the given value has the expected length.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value does not have
+                the expected length.
+        """
+        equal(length)(len(value))
+
+    return length_
+
+
+def length_min(endpoint, inclusive=True):
+    """
+    Validate that a value's length is greater than/or equal to a minimum.
+
+    Args:
+        endpoint: the minimum length allowed.
+        inclusive (bool): whether the minimum length value itself allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_min_(value):
+        """
+        Validate that a value's length is greater than/or equal to a minimum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is
+                greater than and/or equal to the maximum.
+        """
+        min(endpoint, inclusive=inclusive)(len(value))
+
+    return length_min_
+
+
+def length_max(endpoint, inclusive=True):
+    """
+    Validate that a value's length is less than/or equal to a maximum.
+
+    Args:
+        endpoint: the maximum length allowed.
+        inclusive (bool): whether the maximum length value itself allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_max_(value):
+        """
+        Validate that a value's length is less than/or equal to a maximum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is
+                less than and/or equal to the maximum.
+        """
+        max(endpoint, inclusive=inclusive)(len(value))
+
+    return length_max_
+
+
+def length_between(min_endpoint, max_endpoint, inclusive=True):
+    """
+    Validate that the given value's length is between a minimum and maximum.
+
+    Args:
+        min_endpoint: the minimum length allowed.
+        max_endpoint: the maximum length allowed.
+        inclusive (bool): whether the endpoint length values are allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_between_(value):
+        """
+        Validate that the given value's length is between a minimum and maximum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is not
+                between the minimum and maximum.
+        """
+        min(min_endpoint, inclusive=inclusive)(len(value))
+        max(max_endpoint, inclusive=inclusive)(len(value))
+
+    return length_between_
+
+
 def contains(allowed):
     """
     Validate that the given list/range/tuple contains the given value.


Test patch:
diff --git a/tests/test_validate.py b/tests/test_validate.py
index b7d7040..ab9e308 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -20,6 +20,14 @@ def test_instance():
         validate.instance(float)(1)
 
 
+def test_equal():
+    value = object()
+    validate.equal(value)(value)
+
+    with raises(ValidationError):
+        validate.equal(object())(20)
+
+
 def test_min():
     validate.min(100)(100)
     validate.min(100)(1000)
@@ -53,6 +61,45 @@ def test_between():
         validate.between(-100, 100, inclusive=False)(100)
 
 
+def test_length():
+    validate.length(10)(range(10))
+    validate.length(3)([1, 2, 3])
+
+    with raises(ValidationError):
+        validate.length(10)((1, 2))
+
+
+def test_length_min():
+    validate.length_min(5)(range(10))
+    validate.length_min(5, inclusive=False)(range(6))
+
+    with raises(ValidationError):
+        validate.length_min(5)(range(2))
+
+    with raises(ValidationError):
+        validate.length_min(5, inclusive=False)(range(5))
+
+
+def test_length_max():
+    validate.length_max(5)(range(2))
+
+    with raises(ValidationError):
+        validate.length_max(5)(range(10))
+
+    with raises(ValidationError):
+        validate.length_max(5, inclusive=False)(range(5))
+
+
+def test_length_between():
+    validate.length_between(0, 10)(range(10))
+
+    with raises(ValidationError):
+        validate.length_between(10, 100)(range(5))
+
+    with raises(ValidationError):
+        validate.length_between(0, 10, inclusive=False)([])
+
+
 def test_contains():
     validate.contains(range(5))(3)
     validate.contains((0, 1, 2, 3, 4))(3)