From f191f32a722ef0c2eaad71dd33da4e7787ac2424 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi 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.