summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Berman <Julian+git@GrayVines.com>2011-12-29 22:36:53 -0500
committerJulian Berman <Julian+git@GrayVines.com>2011-12-29 22:36:53 -0500
commit2c25dc77051fbdb10b7e5552f317edb72bb6bcc4 (patch)
treef069c809ea17f4a0df097900294b1aaa0c6d0e86
downloadjsonschema-2c25dc77051fbdb10b7e5552f317edb72bb6bcc4.tar.gz
Initial commit
-rw-r--r--.gitignore19
-rw-r--r--COPYING19
-rw-r--r--jsons.py179
-rw-r--r--test_jsons.py224
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
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d8338a3
--- /dev/null
+++ b/COPYING
@@ -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)