From f191f32a722ef0c2eaad71dd33da4e7787ac2424 Mon Sep 17 00:00:00 2001
From: Ken'ichi Ohmichi <oomichi@mxs.nes.nec.co.jp>
Date: Mon, 28 Oct 2013 21:45:27 +0900
Subject: [PATCH] Add IntegerType and some classes for validation

This patch adds the following classes for API parameter validation:

IntegerType
  * Value range validation (minimum, maximum)

StringType
  * String length validation (min_length, max_length)
  * Allowed string (pattern): e.g. should contain [a-zA-Z0-9_.- ] only.

IPv4AddressType
  * String format validation for IPv4

IPv6AddressType
  * String format validation for IPv6

UuidType
  * String format validation for UUID

Partially implements blueprint nova-api-validation-fw

Closes-Bug: 1245795
Change-Id: I5aead6c51b74464681e4ac41fa2a9c66c09adab2
---
 setup.py                 |   3 +
 wsme/tests/test_types.py |  59 +++++++++++++++++
 wsme/types.py            | 140 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 202 insertions(+)

diff --git a/setup.py b/setup.py
index 2553ca9..1fbb3bf 100644
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,9 @@ install_requires = [
 if sys.version_info[:2] <= (2, 6):
     install_requires += ('ordereddict',)
 
+if sys.version_info[:2] < (3, 3):
+    install_requires += ('ipaddr',)
+
 setup(
     setup_requires=['pbr>=0.5.21'],
     install_requires=install_requires,
diff --git a/wsme/tests/test_types.py b/wsme/tests/test_types.py
index e40bd7d..1c28495 100644
--- a/wsme/tests/test_types.py
+++ b/wsme/tests/test_types.py
@@ -1,3 +1,4 @@
+import re
 try:
     import unittest2 as unittest
 except ImportError:
@@ -293,6 +294,64 @@ Value: 'v3'. Value should be one of: v., v.",
         self.assertEqual(types.validate_value(int, six.u('1')), 1)
         self.assertRaises(ValueError, types.validate_value, int, 1.1)
 
+    def test_validate_integer_type(self):
+        v = types.IntegerType(minimum=1, maximum=10)
+        v.validate(1)
+        v.validate(5)
+        v.validate(10)
+        self.assertRaises(ValueError, v.validate, 0)
+        self.assertRaises(ValueError, v.validate, 11)
+
+    def test_validate_string_type(self):
+        v = types.StringType(min_length=1, max_length=10,
+                             pattern='^[a-zA-Z0-9]*$')
+        v.validate('1')
+        v.validate('12345')
+        v.validate('1234567890')
+        self.assertRaises(ValueError, v.validate, '')
+        self.assertRaises(ValueError, v.validate, '12345678901')
+
+        # Test a pattern validation
+        v.validate('a')
+        v.validate('A')
+        self.assertRaises(ValueError, v.validate, '_')
+
+    def test_validate_string_type_precompile(self):
+        precompile = re.compile('^[a-zA-Z0-9]*$')
+        v = types.StringType(min_length=1, max_length=10,
+                             pattern=precompile)
+
+        # Test a pattern validation
+        v.validate('a')
+        v.validate('A')
+        self.assertRaises(ValueError, v.validate, '_')
+
+    def test_validate_ipv4_address_type(self):
+        v = types.IPv4AddressType()
+        v.validate('127.0.0.1')
+        v.validate('192.168.0.1')
+        self.assertRaises(ValueError, v.validate, '')
+        self.assertRaises(ValueError, v.validate, 'foo')
+        self.assertRaises(ValueError, v.validate,
+                          '2001:0db8:bd05:01d2:288a:1fc0:0001:10ee')
+
+    def test_validate_ipv6_address_type(self):
+        v = types.IPv6AddressType()
+        v.validate('0:0:0:0:0:0:0:1')
+        v.validate('2001:0db8:bd05:01d2:288a:1fc0:0001:10ee')
+        self.assertRaises(ValueError, v.validate, '')
+        self.assertRaises(ValueError, v.validate, 'foo')
+        self.assertRaises(ValueError, v.validate, '192.168.0.1')
+
+    def test_validate_uuid_type(self):
+        v = types.UuidType()
+        v.validate('6a0a707c-45ef-4758-b533-e55adddba8ce')
+        v.validate('6a0a707c45ef4758b533e55adddba8ce')
+        self.assertRaises(ValueError, v.validate, '')
+        self.assertRaises(ValueError, v.validate, 'foo')
+        self.assertRaises(ValueError, v.validate,
+                          '6a0a707c-45ef-4758-b533-e55adddba8ce-a')
+
     def test_register_invalid_array(self):
         self.assertRaises(ValueError, types.register_type, [])
         self.assertRaises(ValueError, types.register_type, [int, str])
diff --git a/wsme/types.py b/wsme/types.py
index 991450b..0902ce3 100644
--- a/wsme/types.py
+++ b/wsme/types.py
@@ -3,10 +3,17 @@ import datetime
 import decimal
 import inspect
 import logging
+import re
 import six
 import sys
+import uuid
 import weakref
 
+try:
+    import ipaddress
+except ImportError:
+    import ipaddr as ipaddress
+
 from wsme import exc
 
 log = logging.getLogger(__name__)
@@ -136,6 +143,139 @@ class BinaryType(UserType):
 binary = BinaryType()
 
 
+class IntegerType(UserType):
+    """
+    A simple integer type. Can validate a value range.
+
+    :param minimum: Possible minimum value
+    :param maximum: Possible maximum value
+
+    Example::
+
+        Price = IntegerType(minimum=1)
+
+    """
+    basetype = int
+    name = "integer"
+
+    def __init__(self, minimum=None, maximum=None):
+        self.minimum = minimum
+        self.maximum = maximum
+
+    @staticmethod
+    def frombasetype(value):
+        return int(value) if value is not None else None
+
+    def validate(self, value):
+        if self.minimum is not None and value < self.minimum:
+            error = 'Value should be greater or equal to %s' % self.minimum
+            raise ValueError(error)
+
+        if self.maximum is not None and value > self.maximum:
+            error = 'Value should be lower or equal to %s' % self.maximum
+            raise ValueError(error)
+
+        return value
+
+
+class StringType(UserType):
+    """
+    A simple string type. Can validate a length and a pattern.
+
+    :param min_length: Possible minimum length
+    :param max_length: Possible maximum length
+    :param pattern: Possible string pattern
+
+    Example::
+
+        Name = StringType(min_length=1, pattern='^[a-zA-Z ]*$')
+
+    """
+    basetype = six.string_types
+    name = "string"
+
+    def __init__(self, min_length=None, max_length=None, pattern=None):
+        self.min_length = min_length
+        self.max_length = max_length
+        if isinstance(pattern, six.string_types):
+            self.pattern = re.compile(pattern)
+        else:
+            self.pattern = pattern
+
+    def validate(self, value):
+        if not isinstance(value, self.basetype):
+            error = 'Value should be string'
+            raise ValueError(error)
+
+        if self.min_length is not None and len(value) < self.min_length:
+            error = 'Value should have a minimum character requirement of %s' \
+                    % self.min_length
+            raise ValueError(error)
+
+        if self.max_length is not None and len(value) > self.max_length:
+            error = 'Value should have a maximum character requirement of %s' \
+                    % self.max_length
+            raise ValueError(error)
+
+        if self.pattern is not None and not self.pattern.search(value):
+            error = 'Value should match the pattern %s' % self.pattern
+            raise ValueError(error)
+
+        return value
+
+
+class IPv4AddressType(UserType):
+    """
+    A simple IPv4 type.
+    """
+    basetype = six.string_types
+    name = "ipv4address"
+
+    @staticmethod
+    def validate(value):
+        try:
+            ipaddress.IPv4Address(value)
+        except ipaddress.AddressValueError:
+            error = 'Value should be IPv4 format'
+            raise ValueError(error)
+
+
+class IPv6AddressType(UserType):
+    """
+    A simple IPv6 type.
+    """
+    basetype = six.string_types
+    name = "ipv6address"
+
+    @staticmethod
+    def validate(value):
+        try:
+            ipaddress.IPv6Address(value)
+        except ipaddress.AddressValueError:
+            error = 'Value should be IPv6 format'
+            raise ValueError(error)
+
+
+class UuidType(UserType):
+    """
+    A simple UUID type.
+
+    This type allows not only UUID having dashes but also UUID not
+    having dashes. For example, '6a0a707c-45ef-4758-b533-e55adddba8ce'
+    and '6a0a707c45ef4758b533e55adddba8ce' are distinguished as valid.
+    """
+    basetype = six.string_types
+    name = "uuid"
+
+    @staticmethod
+    def validate(value):
+        try:
+            uuid.UUID(value)
+        except (TypeError, ValueError, AttributeError):
+            error = 'Value should be UUID format'
+            raise ValueError(error)
+
+
 class Enum(UserType):
     """
     A simple enumeration type. Can be based on any non-complex type.