Created
May 28, 2011 11:09
-
-
Save andreypopp/996795 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" Validating Python data structures.""" | |
__all__ = ( | |
"ValidationError", | |
"Type", "Or", "And", "Optional", "NullType", "Nullable", "Null", | |
"IntType", "StringType", "BoolType", | |
"Int", "String", "Bool", | |
"Sequence", "Mapping") | |
class ValidationError(TypeError): | |
pass | |
class Type(object): | |
""" Validates any value. | |
>>> Any.validate(1) | |
>>> Any.validate("1") | |
>>> Any.validate(True) | |
""" | |
def validate(self, value): | |
pass | |
def __or__(self, type): | |
return Or(self, type) | |
def __and__(self, type): | |
return And(self, type) | |
Any = Type() | |
class Or(Type): | |
""" Represent sum type. | |
>>> schema = Or(Int, String) | |
>>> schema.validate(1) | |
>>> schema.validate("1") | |
>>> schema.validate([]) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
There's also different syntax for constructing sum types:: | |
>>> schema = Int | String | |
>>> schema.validate(1) | |
>>> schema.validate("1") | |
>>> schema.validate([]) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def __init__(self, left, right): | |
if isinstance(left, type) and issubclass(left, Type): | |
left = left() | |
if isinstance(right, type) and issubclass(right, Type): | |
right = right() | |
self.left = left | |
self.right = right | |
def validate(self, value): | |
try: | |
self.left.validate(value) | |
except ValidationError: | |
self.right.validate(value) | |
class And(Type): | |
""" Represent logical AND of types. | |
>>> schema = And(Int, Bool) | |
>>> schema.validate(True) | |
>>> schema.validate(1) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
There's also different syntax for constructing sum types:: | |
>>> schema = Int & Bool | |
>>> schema.validate(True) | |
>>> schema.validate(1) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def __init__(self, left, right): | |
if isinstance(left, type) and issubclass(left, Type): | |
left = left() | |
if isinstance(right, type) and issubclass(right, Type): | |
right = right() | |
self.left = left | |
self.right = right | |
def validate(self, value): | |
self.left.validate(value) | |
self.right.validate(value) | |
class Optional(Type): | |
""" Type, which is optional. | |
This kind of value behaves exactly like ``Type``, but treated differently | |
by containers:: | |
>>> schema = Optional(Int) | |
>>> schema.validate(1) | |
>>> schema.validate("1") | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def __init__(self, node): | |
if isinstance(node, type) and issubclass(node, Type): | |
node = node() | |
self.node = node | |
def validate(self, value): | |
self.node.validate(value) | |
class NullType(Type): | |
""" Validates only ``None``:: | |
>>> Null.validate(None) | |
>>> Null.validate(1) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def validate(self, value): | |
if not value is None: | |
raise ValidationError() | |
Null = NullType() | |
Nullable = lambda type: type | Null | |
class IntType(Type): | |
""" Integer type. | |
>>> Int.validate(1) | |
>>> Int.validate("1") | |
Traceback (most recent call last): | |
... | |
ValidationError | |
>>> (Int > 5).validate(6) | |
>>> (Int > 5).validate(3) | |
Traceback (most recent call last): | |
... | |
ValidationError: 3 < 5 | |
""" | |
def __init__(self, min=None, max=None): | |
self.min = min | |
self.max = max | |
def validate(self, value): | |
if not isinstance(value, (int, long)): | |
raise ValidationError() | |
if not self.max is None and value > self.max: | |
raise ValidationError("%s > %s" % (value, self.max)) | |
if not self.min is None and value < self.min: | |
raise ValidationError("%s < %s" % (value, self.min)) | |
def __gt__(self, value): | |
return Int(min=value, max=self.max) | |
def __lt__(self, value): | |
return Int(min=self.min, max=value) | |
def __call__(self, min=None, max=None): | |
return IntType(min=min, max=max) | |
Int = IntType() | |
class BoolType(Type): | |
""" Validates only ``bool``:: | |
>>> Bool.validate(False) | |
>>> Bool.validate(True) | |
>>> Bool.validate(1) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def validate(self, value): | |
if not isinstance(value, bool): | |
raise ValidationError() | |
def __call__(self): | |
return BoolType() | |
Bool = BoolType() | |
class StringType(Type): | |
""" Validates ``basestring`` type:: | |
>>> String.validate("string") | |
>>> String.validate(u"string") | |
>>> String.validate(1) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def validate(self, value): | |
if not isinstance(value, basestring): | |
raise ValidationError() | |
String = StringType() | |
class Sequence(Type): | |
""" Sequence represent iterable sequence. | |
>>> schema = Sequence(Int) | |
>>> schema.validate([1, 2, 3]) | |
>>> schema.validate([1, "2", 3]) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def __init__(self, node): | |
if isinstance(node, type) and issubclass(node, Type): | |
node = node() | |
self.node = node | |
def validate(self, value): | |
if not hasattr(value, "__iter__"): | |
raise ValidationError("%r is not iterable" % value) | |
for item in value: | |
self.node.validate(item) | |
class Mapping(Type): | |
""" Mapping represents disrete mapping, such as dictionary. | |
>>> schema = Mapping(a=Optional(Int), b=String) | |
>>> schema.validate({"a": 1, "b": "string"}) | |
>>> schema.validate({"b": "string"}) | |
>>> schema.validate({"a": 1}) | |
Traceback (most recent call last): | |
... | |
ValidationError: b is missing | |
>>> schema.validate({"b": 1}) | |
Traceback (most recent call last): | |
... | |
ValidationError | |
""" | |
def __init__(self, **nodes): | |
for name, node in nodes.items(): | |
if isinstance(node, type) and issubclass(node, Type): | |
node = node() | |
nodes[name] = node | |
self.nodes = nodes | |
def validate(self, value): | |
if not hasattr(value, "__getitem__"): | |
raise ValidationError("%r is not indexable" % value) | |
for name, node in self.nodes.items(): | |
try: | |
node_value = value[name] | |
except KeyError: | |
if not isinstance(node, Optional): | |
raise ValidationError("%s is missing" % name) | |
else: | |
node.validate(node_value) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment