Skip to content

Commit

Permalink
Feature: Make Arrays first-class wrapper objects.
Browse files Browse the repository at this point in the history
Prior to now, arrays objects were handled a little weirdly in that
they were stored as native python lists with an associated validator
object. This resulted in all sorts of unexpected behavior, with
nested arrays being returned without their validators under certain
circumstances.

This resolves the issue by converting all arrays to first-class object
wrappers (and renaming the wrapper to ArrayWrapper instead of
ArrayValidator).
  • Loading branch information
cwacek committed Nov 22, 2016
1 parent 53f576e commit edfbb67
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 245 deletions.
37 changes: 22 additions & 15 deletions python_jsonschema_objects/classbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import sys

import logging

import python_jsonschema_objects.wrapper_types

logger = logging.getLogger(__name__)

logger.addHandler(logging.NullHandler())
Expand Down Expand Up @@ -153,7 +156,6 @@ def __init__(self, **props):
six.moves.xrange(len(self.__prop_names__))]))

for prop in props:

try:
logger.debug(util.lazy_format("Setting value for '{0}' to {1}", prop, props[prop]))
setattr(self, prop, props[prop])
Expand Down Expand Up @@ -318,7 +320,11 @@ def __init__(self, value, typ=None):
:value: @todo
"""
self._value = value
if isinstance(value, LiteralValue):
self._value = value._value
else:
self._value = value

self.validate()

def as_dict(self):
Expand Down Expand Up @@ -456,7 +462,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,),**kw):
elif clsdata.get('type') == 'array' and 'items' in clsdata:
clsdata_copy = {}
clsdata_copy.update(clsdata)
self.resolved[uri] = validators.ArrayValidator.create(
self.resolved[uri] = python_jsonschema_objects.wrapper_types.ArrayWrapper.create(
uri,
item_constraint=clsdata_copy.pop('items'),
classbuilder=self,
Expand Down Expand Up @@ -581,7 +587,7 @@ def _build_object(self, nm, clsdata, parents,**kw):
typ = self.construct(uri, detail['items'])
propdata = {
'type': 'array',
'validator': validators.ArrayValidator.create(
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(
uri,
item_constraint=typ)}
else:
Expand All @@ -602,14 +608,14 @@ def _build_object(self, nm, clsdata, parents,**kw):
else:
typ = self.construct(uri, detail['items'])
propdata = {'type': 'array',
'validator': validators.ArrayValidator.create(uri, item_constraint=typ,
addl_constraints=detail)}
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(uri, item_constraint=typ,
addl_constraints=detail)}
except NotImplementedError:
typ = detail['items']
propdata = {'type': 'array',
'validator': validators.ArrayValidator.create(uri,
item_constraint=typ,
addl_constraints=detail)}
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(uri,
item_constraint=typ,
addl_constraints=detail)}

props[prop] = make_property(prop,
propdata,
Expand Down Expand Up @@ -725,7 +731,7 @@ def setprop(self, val):
val.validate()
ok = True
break
elif util.safe_issubclass(typ, validators.ArrayValidator):
elif util.safe_issubclass(typ, python_jsonschema_objects.wrapper_types.ArrayWrapper):
try:
val = typ(val)
except Exception as e:
Expand All @@ -743,13 +749,14 @@ def setprop(self, val):
"Object must be one of {0}: \n{1}".format(info['type'], errstr))

elif info['type'] == 'array':
instance = info['validator'](val)
val = instance.validate()
val = info['validator'](val)
val.validate()

elif util.safe_issubclass(info['type'], validators.ArrayValidator):
elif util.safe_issubclass(info['type'],
python_jsonschema_objects.wrapper_types.ArrayWrapper):
# An array type may have already been converted into an ArrayValidator
instance = info['type'](val)
val = instance.validate()
val = info['type'](val)
val.validate()

elif getattr(info['type'], 'isLiteralClass', False) is True:
if not isinstance(val, info['type']):
Expand Down
5 changes: 4 additions & 1 deletion python_jsonschema_objects/pattern_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import collections

import logging

import python_jsonschema_objects.wrapper_types

logger = logging.getLogger(__name__)

PatternDef = collections.namedtuple('PatternDef', 'pattern schema_type')
Expand Down Expand Up @@ -70,7 +73,7 @@ def _make_type(self, typ, val):
if util.safe_issubclass(typ, cb.ProtocolBase):
return typ(**util.coerce_for_expansion(val))

if util.safe_issubclass(typ, validators.ArrayValidator):
if util.safe_issubclass(typ, python_jsonschema_objects.wrapper_types.ArrayWrapper):
return typ(val)

raise validators.ValidationError(
Expand Down
4 changes: 2 additions & 2 deletions python_jsonschema_objects/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ class ProtocolJSONEncoder(json.JSONEncoder):

def default(self, obj):
from python_jsonschema_objects import classbuilder
from python_jsonschema_objects import validators
from python_jsonschema_objects import wrapper_types

if isinstance(obj, classbuilder.LiteralValue):
return obj._value
if isinstance(obj, validators.ArrayValidator):
if isinstance(obj, wrapper_types.ArrayWrapper):
return obj.for_json()
if isinstance(obj, classbuilder.ProtocolBase):
props = {}
Expand Down
220 changes: 3 additions & 217 deletions python_jsonschema_objects/validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import six
from python_jsonschema_objects import util
import collections
import logging

import six

logger = logging.getLogger(__name__)

SCHEMA_TYPE_MAPPING = (
Expand Down Expand Up @@ -167,217 +167,3 @@ def check_type(param, value, type_data):
type_check(param, value, type_data)


class ArrayValidator(object):

def __init__(self, ary):
if isinstance(ary, (list, tuple, collections.Sequence)):
self.data = ary
elif isinstance(ary, ArrayValidator):
self.data = ary.data
else:
raise TypeError("Invalid value given to array validator: {0}"
.format(ary))

@classmethod
def from_json(cls, jsonmsg):
import json
msg = json.loads(jsonmsg)
obj = cls(msg)
obj.validate()
return obj

def serialize(self):
d = self.validate()
enc = util.ProtocolJSONEncoder()
return enc.encode(d)

def for_json(self):
return self.validate()

def validate(self):
converted = self.validate_items()
self.validate_length()
self.validate_uniqueness()
return converted

def validate_uniqueness(self):
from python_jsonschema_objects import classbuilder

if getattr(self, 'uniqueItems', None) is not None:
testset = set(self.data)
if len(testset) != len(self.data):
raise ValidationError(
"{0} has duplicate elements, but uniqueness required"
.format(self.data))

def validate_length(self):
from python_jsonschema_objects import classbuilder

if getattr(self, 'minItems', None) is not None:
if len(self.data) < self.minItems:
raise ValidationError(
"{1} has too few elements. Wanted {0}."
.format(self.minItems, self.data))

if getattr(self, 'maxItems', None) is not None:
if len(self.data) > self.maxItems:
raise ValidationError(
"{1} has too few elements. Wanted {0}."
.format(self.maxItems, self.data))

def validate_items(self):
from python_jsonschema_objects import classbuilder

if self.__itemtype__ is None:
return

type_checks = self.__itemtype__
if not isinstance(type_checks, (tuple, list)):
# we were given items = {'type': 'blah'} ; thus ensure the type for all data.
type_checks = [type_checks] * len(self.data)
elif len(type_checks) > len(self.data):
raise ValidationError(
"{1} does not have sufficient elements to validate against {0}"
.format(self.__itemtype__, self.data))

typed_elems = []
for elem, typ in zip(self.data, type_checks):
if isinstance(typ, dict):
for param, paramval in six.iteritems(typ):
validator = registry(param)
if validator is not None:
validator(paramval, elem, typ)
typed_elems.append(elem)

elif util.safe_issubclass(typ, classbuilder.LiteralValue):
val = typ(elem)
val.validate()
typed_elems.append(val)
elif util.safe_issubclass(typ, classbuilder.ProtocolBase):
if not isinstance(elem, typ):
try:
if isinstance(elem, (six.string_types, six.integer_types, float)):
val = typ(elem)
else:
val = typ(**util.coerce_for_expansion(elem))
except TypeError as e:
raise ValidationError("'{0}' is not a valid value for '{1}': {2}"
.format(elem, typ, e))
else:
val = elem
val.validate()
typed_elems.append(val)

elif util.safe_issubclass(typ, ArrayValidator):
val = typ(elem)
val.validate()
typed_elems.append(val)

elif isinstance(typ, classbuilder.TypeProxy):
try:
if isinstance(elem, (six.string_types, six.integer_types, float)):
val = typ(elem)
else:
val = typ(**util.coerce_for_expansion(elem))
except TypeError as e:
raise ValidationError("'{0}' is not a valid value for '{1}': {2}"
.format(elem, typ, e))
else:
val.validate()
typed_elems.append(val)

return typed_elems

@staticmethod
def create(name, item_constraint=None, **addl_constraints):
""" Create an array validator based on the passed in constraints.
If item_constraint is a tuple, it is assumed that tuple validation
is being performed. If it is a class or dictionary, list validation
will be performed. Classes are assumed to be subclasses of ProtocolBase,
while dictionaries are expected to be basic types ('string', 'number', ...).
addl_constraints is expected to be key-value pairs of any of the other
constraints permitted by JSON Schema v4.
"""
from python_jsonschema_objects import classbuilder
klassbuilder = addl_constraints.pop("classbuilder", None)
props = {}

if item_constraint is not None:
if isinstance(item_constraint, (tuple, list)):
for i, elem in enumerate(item_constraint):
isdict = isinstance(elem, (dict,))
isklass = isinstance( elem, type) and util.safe_issubclass(
elem, (classbuilder.ProtocolBase, classbuilder.LiteralValue))

if not any([isdict, isklass]):
raise TypeError(
"Item constraint (position {0}) is not a schema".format(i))
elif isinstance(item_constraint, classbuilder.TypeProxy):
pass
elif util.safe_issubclass(item_constraint, ArrayValidator):
pass
else:
isdict = isinstance(item_constraint, (dict,))
isklass = isinstance( item_constraint, type) and util.safe_issubclass(
item_constraint, (classbuilder.ProtocolBase, classbuilder.LiteralValue))

if not any([isdict, isklass]):
raise TypeError("Item constraint is not a schema")

if isdict and '$ref' in item_constraint:
if klassbuilder is None:
raise TypeError("Cannot resolve {0} without classbuilder"
.format(item_constraint['$ref']))

uri = item_constraint['$ref']
if uri in klassbuilder.resolved:
logger.debug(util.lazy_format(
"Using previously resolved object for {0}", uri))
else:
logger.debug(util.lazy_format("Resolving object for {0}", uri))

with klassbuilder.resolver.resolving(uri) as resolved:
# Set incase there is a circular reference in schema definition
klassbuilder.resolved[uri] = None
klassbuilder.resolved[uri] = klassbuilder.construct(
uri,
resolved,
(classbuilder.ProtocolBase,))

item_constraint = klassbuilder.resolved[uri]

elif isdict and item_constraint.get('type') == 'array':
# We need to create a sub-array validator.
item_constraint = ArrayValidator.create(name + "#sub",
item_constraint=item_constraint[
'items'],
addl_constraints=item_constraint)
elif isdict and 'oneOf' in item_constraint:
# We need to create a TypeProxy validator
uri = "{0}_{1}".format(name, "<anonymous_list_type>")
type_array = []
for i, item_detail in enumerate(item_constraint['oneOf']):
if '$ref' in item_detail:
subtype = klassbuilder.construct(
util.resolve_ref_uri(
klassbuilder.resolver.resolution_scope,
item_detail['$ref']),
item_detail)
else:
subtype = klassbuilder.construct(
uri + "_%s" % i, item_detail)

type_array.append(subtype)

item_constraint = classbuilder.TypeProxy(type_array)

props['__itemtype__'] = item_constraint

props.update(addl_constraints)

validator = type(str(name), (ArrayValidator,), props)

return validator

Loading

0 comments on commit edfbb67

Please sign in to comment.