1from __future__ import division
2
3import contextlib
4import json
5import numbers
6
7try:
8    import requests
9except ImportError:
10    requests = None
11
12from jsonschema import _utils, _validators
13from jsonschema.compat import (
14    Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen,
15    str_types, int_types, iteritems, lru_cache,
16)
17from jsonschema.exceptions import ErrorTree  # Backwards compat  # noqa: F401
18from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType
19
20
21_unset = _utils.Unset()
22
23validators = {}
24meta_schemas = _utils.URIDict()
25
26
27def validates(version):
28    """
29    Register the decorated validator for a ``version`` of the specification.
30
31    Registered validators and their meta schemas will be considered when
32    parsing ``$schema`` properties' URIs.
33
34    Arguments:
35
36        version (str):
37
38            An identifier to use as the version's name
39
40    Returns:
41
42        callable: a class decorator to decorate the validator with the version
43
44    """
45
46    def _validates(cls):
47        validators[version] = cls
48        if u"id" in cls.META_SCHEMA:
49            meta_schemas[cls.META_SCHEMA[u"id"]] = cls
50        return cls
51    return _validates
52
53
54def create(meta_schema, validators=(), version=None, default_types=None):  # noqa: C901, E501
55    if default_types is None:
56        default_types = {
57            u"array": list, u"boolean": bool, u"integer": int_types,
58            u"null": type(None), u"number": numbers.Number, u"object": dict,
59            u"string": str_types,
60        }
61
62    class Validator(object):
63        VALIDATORS = dict(validators)
64        META_SCHEMA = dict(meta_schema)
65        DEFAULT_TYPES = dict(default_types)
66
67        def __init__(
68            self, schema, types=(), resolver=None, format_checker=None,
69        ):
70            self._types = dict(self.DEFAULT_TYPES)
71            self._types.update(types)
72
73            if resolver is None:
74                resolver = RefResolver.from_schema(schema)
75
76            self.resolver = resolver
77            self.format_checker = format_checker
78            self.schema = schema
79
80        @classmethod
81        def check_schema(cls, schema):
82            for error in cls(cls.META_SCHEMA).iter_errors(schema):
83                raise SchemaError.create_from(error)
84
85        def iter_errors(self, instance, _schema=None):
86            if _schema is None:
87                _schema = self.schema
88
89            scope = _schema.get(u"id")
90            if scope:
91                self.resolver.push_scope(scope)
92            try:
93                ref = _schema.get(u"$ref")
94                if ref is not None:
95                    validators = [(u"$ref", ref)]
96                else:
97                    validators = iteritems(_schema)
98
99                for k, v in validators:
100                    validator = self.VALIDATORS.get(k)
101                    if validator is None:
102                        continue
103
104                    errors = validator(self, v, instance, _schema) or ()
105                    for error in errors:
106                        # set details if not already set by the called fn
107                        error._set(
108                            validator=k,
109                            validator_value=v,
110                            instance=instance,
111                            schema=_schema,
112                        )
113                        if k != u"$ref":
114                            error.schema_path.appendleft(k)
115                        yield error
116            finally:
117                if scope:
118                    self.resolver.pop_scope()
119
120        def descend(self, instance, schema, path=None, schema_path=None):
121            for error in self.iter_errors(instance, schema):
122                if path is not None:
123                    error.path.appendleft(path)
124                if schema_path is not None:
125                    error.schema_path.appendleft(schema_path)
126                yield error
127
128        def validate(self, *args, **kwargs):
129            for error in self.iter_errors(*args, **kwargs):
130                raise error
131
132        def is_type(self, instance, type):
133            if type not in self._types:
134                raise UnknownType(type, instance, self.schema)
135            pytypes = self._types[type]
136
137            # bool inherits from int, so ensure bools aren't reported as ints
138            if isinstance(instance, bool):
139                pytypes = _utils.flatten(pytypes)
140                is_number = any(
141                    issubclass(pytype, numbers.Number) for pytype in pytypes
142                )
143                if is_number and bool not in pytypes:
144                    return False
145            return isinstance(instance, pytypes)
146
147        def is_valid(self, instance, _schema=None):
148            error = next(self.iter_errors(instance, _schema), None)
149            return error is None
150
151    if version is not None:
152        Validator = validates(version)(Validator)
153        Validator.__name__ = version.title().replace(" ", "") + "Validator"
154
155    return Validator
156
157
158def extend(validator, validators, version=None):
159    all_validators = dict(validator.VALIDATORS)
160    all_validators.update(validators)
161    return create(
162        meta_schema=validator.META_SCHEMA,
163        validators=all_validators,
164        version=version,
165        default_types=validator.DEFAULT_TYPES,
166    )
167
168
169Draft3Validator = create(
170    meta_schema=_utils.load_schema("draft3"),
171    validators={
172        u"$ref": _validators.ref,
173        u"additionalItems": _validators.additionalItems,
174        u"additionalProperties": _validators.additionalProperties,
175        u"dependencies": _validators.dependencies,
176        u"disallow": _validators.disallow_draft3,
177        u"divisibleBy": _validators.multipleOf,
178        u"enum": _validators.enum,
179        u"extends": _validators.extends_draft3,
180        u"format": _validators.format,
181        u"items": _validators.items,
182        u"maxItems": _validators.maxItems,
183        u"maxLength": _validators.maxLength,
184        u"maximum": _validators.maximum,
185        u"minItems": _validators.minItems,
186        u"minLength": _validators.minLength,
187        u"minimum": _validators.minimum,
188        u"multipleOf": _validators.multipleOf,
189        u"pattern": _validators.pattern,
190        u"patternProperties": _validators.patternProperties,
191        u"properties": _validators.properties_draft3,
192        u"type": _validators.type_draft3,
193        u"uniqueItems": _validators.uniqueItems,
194    },
195    version="draft3",
196)
197
198Draft4Validator = create(
199    meta_schema=_utils.load_schema("draft4"),
200    validators={
201        u"$ref": _validators.ref,
202        u"additionalItems": _validators.additionalItems,
203        u"additionalProperties": _validators.additionalProperties,
204        u"allOf": _validators.allOf_draft4,
205        u"anyOf": _validators.anyOf_draft4,
206        u"dependencies": _validators.dependencies,
207        u"enum": _validators.enum,
208        u"format": _validators.format,
209        u"items": _validators.items,
210        u"maxItems": _validators.maxItems,
211        u"maxLength": _validators.maxLength,
212        u"maxProperties": _validators.maxProperties_draft4,
213        u"maximum": _validators.maximum,
214        u"minItems": _validators.minItems,
215        u"minLength": _validators.minLength,
216        u"minProperties": _validators.minProperties_draft4,
217        u"minimum": _validators.minimum,
218        u"multipleOf": _validators.multipleOf,
219        u"not": _validators.not_draft4,
220        u"oneOf": _validators.oneOf_draft4,
221        u"pattern": _validators.pattern,
222        u"patternProperties": _validators.patternProperties,
223        u"properties": _validators.properties_draft4,
224        u"required": _validators.required_draft4,
225        u"type": _validators.type_draft4,
226        u"uniqueItems": _validators.uniqueItems,
227    },
228    version="draft4",
229)
230
231
232class RefResolver(object):
233    """
234    Resolve JSON References.
235
236    Arguments:
237
238        base_uri (str):
239
240            The URI of the referring document
241
242        referrer:
243
244            The actual referring document
245
246        store (dict):
247
248            A mapping from URIs to documents to cache
249
250        cache_remote (bool):
251
252            Whether remote refs should be cached after first resolution
253
254        handlers (dict):
255
256            A mapping from URI schemes to functions that should be used
257            to retrieve them
258
259        urljoin_cache (functools.lru_cache):
260
261            A cache that will be used for caching the results of joining
262            the resolution scope to subscopes.
263
264        remote_cache (functools.lru_cache):
265
266            A cache that will be used for caching the results of
267            resolved remote URLs.
268
269    """
270
271    def __init__(
272        self,
273        base_uri,
274        referrer,
275        store=(),
276        cache_remote=True,
277        handlers=(),
278        urljoin_cache=None,
279        remote_cache=None,
280    ):
281        if urljoin_cache is None:
282            urljoin_cache = lru_cache(1024)(urljoin)
283        if remote_cache is None:
284            remote_cache = lru_cache(1024)(self.resolve_from_url)
285
286        self.referrer = referrer
287        self.cache_remote = cache_remote
288        self.handlers = dict(handlers)
289
290        self._scopes_stack = [base_uri]
291        self.store = _utils.URIDict(
292            (id, validator.META_SCHEMA)
293            for id, validator in iteritems(meta_schemas)
294        )
295        self.store.update(store)
296        self.store[base_uri] = referrer
297
298        self._urljoin_cache = urljoin_cache
299        self._remote_cache = remote_cache
300
301    @classmethod
302    def from_schema(cls, schema, *args, **kwargs):
303        """
304        Construct a resolver from a JSON schema object.
305
306        Arguments:
307
308            schema:
309
310                the referring schema
311
312        Returns:
313
314            :class:`RefResolver`
315
316        """
317
318        return cls(schema.get(u"id", u""), schema, *args, **kwargs)
319
320    def push_scope(self, scope):
321        self._scopes_stack.append(
322            self._urljoin_cache(self.resolution_scope, scope),
323        )
324
325    def pop_scope(self):
326        try:
327            self._scopes_stack.pop()
328        except IndexError:
329            raise RefResolutionError(
330                "Failed to pop the scope from an empty stack. "
331                "`pop_scope()` should only be called once for every "
332                "`push_scope()`"
333            )
334
335    @property
336    def resolution_scope(self):
337        return self._scopes_stack[-1]
338
339    @property
340    def base_uri(self):
341        uri, _ = urldefrag(self.resolution_scope)
342        return uri
343
344    @contextlib.contextmanager
345    def in_scope(self, scope):
346        self.push_scope(scope)
347        try:
348            yield
349        finally:
350            self.pop_scope()
351
352    @contextlib.contextmanager
353    def resolving(self, ref):
354        """
355        Context manager which resolves a JSON ``ref`` and enters the
356        resolution scope of this ref.
357
358        Arguments:
359
360            ref (str):
361
362                The reference to resolve
363
364        """
365
366        url, resolved = self.resolve(ref)
367        self.push_scope(url)
368        try:
369            yield resolved
370        finally:
371            self.pop_scope()
372
373    def resolve(self, ref):
374        url = self._urljoin_cache(self.resolution_scope, ref)
375        return url, self._remote_cache(url)
376
377    def resolve_from_url(self, url):
378        url, fragment = urldefrag(url)
379        try:
380            document = self.store[url]
381        except KeyError:
382            try:
383                document = self.resolve_remote(url)
384            except Exception as exc:
385                raise RefResolutionError(exc)
386
387        return self.resolve_fragment(document, fragment)
388
389    def resolve_fragment(self, document, fragment):
390        """
391        Resolve a ``fragment`` within the referenced ``document``.
392
393        Arguments:
394
395            document:
396
397                The referrant document
398
399            fragment (str):
400
401                a URI fragment to resolve within it
402
403        """
404
405        fragment = fragment.lstrip(u"/")
406        parts = unquote(fragment).split(u"/") if fragment else []
407
408        for part in parts:
409            part = part.replace(u"~1", u"/").replace(u"~0", u"~")
410
411            if isinstance(document, Sequence):
412                # Array indexes should be turned into integers
413                try:
414                    part = int(part)
415                except ValueError:
416                    pass
417            try:
418                document = document[part]
419            except (TypeError, LookupError):
420                raise RefResolutionError(
421                    "Unresolvable JSON pointer: %r" % fragment
422                )
423
424        return document
425
426    def resolve_remote(self, uri):
427        """
428        Resolve a remote ``uri``.
429
430        If called directly, does not check the store first, but after
431        retrieving the document at the specified URI it will be saved in
432        the store if :attr:`cache_remote` is True.
433
434        .. note::
435
436            If the requests_ library is present, ``jsonschema`` will use it to
437            request the remote ``uri``, so that the correct encoding is
438            detected and used.
439
440            If it isn't, or if the scheme of the ``uri`` is not ``http`` or
441            ``https``, UTF-8 is assumed.
442
443        Arguments:
444
445            uri (str):
446
447                The URI to resolve
448
449        Returns:
450
451            The retrieved document
452
453        .. _requests: http://pypi.python.org/pypi/requests/
454
455        """
456
457        scheme = urlsplit(uri).scheme
458
459        if scheme in self.handlers:
460            result = self.handlers[scheme](uri)
461        elif (
462            scheme in [u"http", u"https"] and
463            requests and
464            getattr(requests.Response, "json", None) is not None
465        ):
466            # Requests has support for detecting the correct encoding of
467            # json over http
468            if callable(requests.Response.json):
469                result = requests.get(uri).json()
470            else:
471                result = requests.get(uri).json
472        else:
473            # Otherwise, pass off to urllib and assume utf-8
474            result = json.loads(urlopen(uri).read().decode("utf-8"))
475
476        if self.cache_remote:
477            self.store[uri] = result
478        return result
479
480
481def validator_for(schema, default=_unset):
482    if default is _unset:
483        default = Draft4Validator
484    return meta_schemas.get(schema.get(u"$schema", u""), default)
485
486
487def validate(instance, schema, cls=None, *args, **kwargs):
488    """
489    Validate an instance under the given schema.
490
491        >>> validate([2, 3, 4], {"maxItems": 2})
492        Traceback (most recent call last):
493            ...
494        ValidationError: [2, 3, 4] is too long
495
496    :func:`validate` will first verify that the provided schema is itself
497    valid, since not doing so can lead to less obvious error messages and fail
498    in less obvious or consistent ways. If you know you have a valid schema
499    already or don't care, you might prefer using the
500    :meth:`~IValidator.validate` method directly on a specific validator
501    (e.g. :meth:`Draft4Validator.validate`).
502
503
504    Arguments:
505
506        instance:
507
508            The instance to validate
509
510        schema:
511
512            The schema to validate with
513
514        cls (:class:`IValidator`):
515
516            The class that will be used to validate the instance.
517
518    If the ``cls`` argument is not provided, two things will happen in
519    accordance with the specification. First, if the schema has a
520    :validator:`$schema` property containing a known meta-schema [#]_ then the
521    proper validator will be used.  The specification recommends that all
522    schemas contain :validator:`$schema` properties for this reason. If no
523    :validator:`$schema` property is found, the default validator class is
524    :class:`Draft4Validator`.
525
526    Any other provided positional and keyword arguments will be passed on when
527    instantiating the ``cls``.
528
529    Raises:
530
531        :exc:`ValidationError` if the instance is invalid
532
533        :exc:`SchemaError` if the schema itself is invalid
534
535    .. rubric:: Footnotes
536    .. [#] known by a validator registered with :func:`validates`
537    """
538    if cls is None:
539        cls = validator_for(schema)
540    cls.check_schema(schema)
541    cls(schema, *args, **kwargs).validate(instance)
542