diff options
-rw-r--r-- | .gitignore | 19 | ||||
-rw-r--r-- | COPYING | 19 | ||||
-rw-r--r-- | jsons.py | 179 | ||||
-rw-r--r-- | test_jsons.py | 224 |
4 files changed, 441 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2deb6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.DS_Store + +*.pyc +*.pyo + +*.egg-info +_build +build +dist +MANIFEST + +.coverage +.coveragerc +coverage +htmlcov + +_trial_temp + +.tox @@ -0,0 +1,19 @@ +Copyright (c) 2011 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jsons.py b/jsons.py new file mode 100644 index 0000000..51b2d39 --- /dev/null +++ b/jsons.py @@ -0,0 +1,179 @@ +from __future__ import unicode_literals + +import numbers +import re +import types + + +_PYTYPES = { + "array" : list, "boolean" : bool, "integer" : int, + "null" : types.NoneType, "number" : numbers.Number, + "object" : dict, "string" : unicode +} + +_PYTYPES["any"] = tuple(_PYTYPES.values()) + + +class SchemaError(Exception): + pass + + +class ValidationError(Exception): + pass + + +class Validator(object): + + # required and dependencies are handled in validate_properties + # exclusive Minium and Maximum are handled in validate_minimum + SKIPPED = { + "dependencies", "required", "exclusiveMinimum", "exclusiveMaximum" + } + + def is_valid(self, instance, schema): + try: + self.validate(instance, schema) + return True + except ValidationError: + return False + + def validate(self, instance, schema): + for k, v in schema.iteritems(): + if k in self.SKIPPED: + continue + + validator = getattr(self, "validate_%s" % k, None) + + if validator is None: + raise SchemaError("'%s' is not a known schema property" % k) + + validator(v, instance, schema) + + def validate_type(self, types, instance, schema): + types = _(types) + + for type in types: + if ( + isinstance(type, dict) and + isinstance(instance, dict) and + self.is_valid(instance, type) + ): + return + + elif isinstance(type, unicode): + type = _PYTYPES.get(type) + + if type is None: + raise SchemaError("'%s' is not a known type" % type) + + # isinstance(a_bool, int) will make us even sadder here, so + # let's be even dirtier than we would otherwise be. + + elif ( + isinstance(instance, type) and + (not isinstance(instance, bool) or + type is bool or types == ["any"]) + ): + return + else: + raise ValidationError("'%s' is not of type %s" % (instance, types)) + + def validate_properties(self, properties, instance, schema): + for property, subschema in properties.iteritems(): + if property in instance: + dependencies = _(subschema.get("dependencies", [])) + if isinstance(dependencies, dict): + self.validate(instance, dependencies) + else: + missing = (d for d in dependencies if d not in instance) + first = next(missing, None) + if first is not None: + raise ValidationError( + "'%s' is a dependency of '%s'" % (first, property) + ) + + self.validate(instance[property], subschema) + elif subschema.get("required", False): + raise ValidationError("'%s' is a required property" % property) + + def validate_patternProperties(self, patternProperties, instance, schema): + for pattern, subschema in patternProperties.iteritems(): + for k, v in instance.iteritems(): + if re.match(pattern, k): + self.validate(v, subschema) + + def validate_additionalProperties(self, aP, instance, schema): + extras = instance.viewkeys() - schema.get("properties", {}).viewkeys() + + if isinstance(aP, dict): + for extra in extras: + self.validate(instance[extra], aP) + elif not aP and extras: + raise ValidationError("Additional properties are not allowed") + + def validate_items(self, items, instance, schema): + if isinstance(items, dict): + for item in instance: + self.validate(item, items) + else: + for item, subschema in zip(instance, items): + self.validate(item, subschema) + + def validate_additionalItems(self, aI, instance, schema): + if isinstance(aI, dict): + for item in instance[len(schema):]: + self.validate(item, aI) + elif not aI and len(instance) > len(schema): + raise ValidationError("Additional items are not allowed") + + def validate_minimum(self, minimum, instance, schema): + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + raise ValidationError( + "%s is %s the minimum (%s)" % (instance, cmp, minimum) + ) + + def validate_maximum(self, maximum, instance, schema): + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + raise ValidationError( + "%s is %s the maximum (%s)" % (instance, cmp, maximum) + ) + + def validate_minItems(self, mI, instance, schema): + if len(instance) < mI: + raise ValidationError("'%s' is too short" % (instance,)) + + def validate_maxItems(self, mI, instance, schema): + if len(instance) > mI: + raise ValidationError("'%s' is too long" % (instance,)) + + def validate_minLength(self, mL, instance, schema): + if len(instance) < mL: + raise ValidationError("'%s' is too short" % (instance,)) + + def validate_maxLength(self, mL, instance, schema): + if len(instance) > mL: + raise ValidationError("'%s' is too long" % (instance,)) + + +def _(thing): + if isinstance(thing, unicode): + return [thing] + return thing + + +_default_validator = Validator() +validate = _default_validator.validate diff --git a/test_jsons.py b/test_jsons.py new file mode 100644 index 0000000..cbbc43b --- /dev/null +++ b/test_jsons.py @@ -0,0 +1,224 @@ +from __future__ import unicode_literals + +import unittest + +from jsons import ValidationError, validate + + +class TestValidate(unittest.TestCase): + def validate_test(self, valids=(), invalids=(), **schema): + for valid in valids: + validate(valid, schema) + + for invalid in invalids: + with self.assertRaises(ValidationError): + validate(invalid, schema) + + def type_test(self, type, valids, invalids): + self.validate_test(valids=valids, invalids=invalids, type=type) + + def test_type_integer(self): + self.type_test("integer", [1], [1.1, "foo", {}, [], True, None]) + + def test_type_number(self): + self.type_test("number", [1, 1.1], ["foo", {}, [], True, None]) + + def test_type_string(self): + self.type_test("string", ["foo"], [1, 1.1, {}, [], True, None]) + + def test_type_object(self): + self.type_test("object", [{}], [1, 1.1, "foo", [], True, None]) + + def test_type_array(self): + self.type_test("array", [[]], [1, 1.1, "foo", {}, True, None]) + + def test_type_boolean(self): + self.type_test( + "boolean", [True, False], [1, 1.1, "foo", {}, [], None] + ) + + def test_type_null(self): + self.type_test("null", [None], [1, 1.1, "foo", {}, [], True]) + + def test_type_any(self): + self.type_test("any", [1, 1.1, "foo", {}, [], True, None], []) + + def test_multiple_types(self): + self.type_test( + ["integer", "string"], [1, "foo"], [1.1, {}, [], True, None] + ) + + def test_multiple_types_subschema(self): + self.type_test( + ["array", {"type" : "object"}], + [[1, 2], {"foo" : "bar"}], + [1.1, True, None] + ) + + self.type_test( + ["integer", {"properties" : {"foo" : {"type" : "null"}}}], + [1, {"foo" : None}], + [{"foo" : 1}, {"foo" : 1.1}], + ) + + def test_properties(self): + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + + valids = [ + {"foo" : 1, "bar" : "baz"}, + {"foo" : 1, "bar" : "baz", "quux" : 42}, + ] + + self.validate_test(valids, [{"foo" : 1, "bar" : []}], **schema) + + def test_patternProperties(self): + self.validate_test( + [{"foo" : 1}, {"foo" : 1, "fah" : 2, "bar" : "baz"}], + [{"foo" : "bar"}, {"foo" : 1, "fah" : "bar"}], + patternProperties={"f.*" : {"type" : "integer"}}, + ) + + def test_multiple_patternProperties(self): + pattern = {"a*" : {"type" : "integer"}, "aaa*" : {"maximum" : 20}} + self.validate_test( + [{"a" : 1}, {"a" : 21}, {"aaaa" : 18}], + [{"aaa" : "foo"}, {"aaaa" : 31}], + patternProperties=pattern, + ) + + def test_additionalProperties(self): + ex = {"foo" : 1, "bar" : "baz", "quux" : False} + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + + validate(ex, schema) + + with self.assertRaises(ValidationError): + validate(ex, dict(additionalProperties=False, **schema)) + + invalids = [{"foo" : 1, "bar" : "baz", "quux" : "boom"}] + additional = {"type" : "boolean"} + + self.validate_test( + [ex], invalids, additionalProperties=additional, **schema + ) + + def test_items(self): + validate([1, "foo", False], {"type" : "array"}) + self.validate_test([[1, 2, 3]], [[1, "x"]], items={"type" : "integer"}) + + def test_items_tuple_typing(self): + items = [{"type" : "integer"}, {"type" : "string"}] + self.validate_test([[1, "foo"]], [["foo", 1], [1, False]], items=items) + + def test_additionalItems(self): + schema = {"items" : [{"type" : "integer"}, {"type" : "string"}]} + + validate([1, "foo", False], schema) + + self.validate_test( + [[1, "foo"]], [[1, "foo", False]], additionalItems=False, **schema + ) + + self.validate_test( + [[1, "foo", 3]], + [[1, "foo", "bar"]], + additionalItems={"type" : "integer"}, + **schema + ) + + def test_required(self): + schema = { + "properties" : { + "foo" : {"type" : "number"}, + "bar" : {"type" : "string"}, + } + } + + validate({"foo" : 1}, schema) + + schema["properties"]["foo"]["required"] = False + + validate({"foo" : 1}, schema) + + schema["properties"]["foo"]["required"] = True + schema["properties"]["bar"]["required"] = True + + with self.assertRaises(ValidationError): + validate({"foo" : 1}, schema) + + def test_dependencies(self): + schema = {"properties" : {"bar" : {"dependencies" : "foo"}}} + self.validate_test( + [{}, {"foo" : 1}, {"foo" : 1, "bar" : 2}], [{"bar" : 2}], **schema + ) + + def test_multiple_dependencies(self): + schema = { + "properties" : { + "quux" : {"dependencies" : ["foo", "bar"]} + } + } + + valids = [ + {}, + {"foo" : 1}, + {"foo" : 1, "bar" : 2}, + {"foo" : 1, "bar" : 2, "quux" : 3}, + ] + + invalids = [ + {"foo" : 1, "quux" : 2}, + {"bar" : 1, "quux" : 2}, + {"quux" : 1}, + ] + + self.validate_test(valids, invalids, **schema) + + def test_multiple_dependencies_subschema(self): + dependencies = { + "properties" : { + "foo" : {"type" : "integer"}, + "bar" : {"type" : "integer"}, + } + } + + schema = {"properties" : {"bar" : {"dependencies" : dependencies}}} + + self.validate_test( + [{"foo" : 1, "bar" : 2}], [{"foo" : "quux", "bar" : 2}], **schema + ) + + def test_minimum(self): + self.validate_test([2.6], [.6], minimum=1.2) + self.validate_test(invalids=[1.2], minimum=1.2, exclusiveMinimum=True) + + def test_maximum(self): + self.validate_test([2.7], [3.5], maximum=3.0) + self.validate_test(invalids=[3.0], maximum=3.0, exclusiveMaximum=True) + + def test_minItems(self): + self.validate_test([[1, 2], [1]], [[]], minItems=1) + + def test_maxItems(self): + self.validate_test([[1, 2], [1], []], [[1, 2, 3]], maxItems=2) + + def test_uniqueItems(self): + pass + + def test_minLength(self): + self.validate_test(["foo"], ["f"], minLength=2) + + def test_maxLength(self): + self.validate_test(["f"], ["foo"], maxLength=2) + + # Test that only the types that are json-loaded validate (e.g. bytestrings) |