Use jsonschema for validation (#936)

* jsonschema validation

* doh, exceptions should be raised

* internalize OpenAPI specs

from 6d17b631ff

* enforce that each default value validates against the schema in which it resides

* Fix import order

* Remove unused imports

* Move _validate_spec() to Specification base class

Co-authored-by: Patrick Wang <patrickkwang@users.noreply.github.com>
This commit is contained in:
Patrick Wang
2021-07-31 16:47:09 -04:00
committed by GitHub
parent 2229831e80
commit e3e851dd31
4 changed files with 3294 additions and 18 deletions

View File

@@ -1,2 +1,3 @@
include *.txt include *.txt
include *.rst include *.rst
recursive-include connexion/resources *.json

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,54 @@ This module defines Python interfaces for OpenAPI specifications.
import abc import abc
import copy import copy
import json
import pathlib import pathlib
from collections.abc import Mapping from collections.abc import Mapping
from urllib.parse import urlsplit from urllib.parse import urlsplit
import jinja2 import jinja2
import jsonschema
import pkg_resources
import yaml import yaml
from openapi_spec_validator.exceptions import OpenAPIValidationError from jsonschema import Draft4Validator
from jsonschema.validators import extend as extend_validator
from .exceptions import InvalidSpecification from .exceptions import InvalidSpecification
from .json_schema import resolve_refs from .json_schema import resolve_refs
from .operations import OpenAPIOperation, Swagger2Operation from .operations import OpenAPIOperation, Swagger2Operation
from .utils import deep_get from .utils import deep_get
validate_properties = Draft4Validator.VALIDATORS["properties"]
def validate_defaults(validator, properties, instance, schema):
"""Validate `properties` subschema.
Enforcing each default value validates against the schema in which it resides.
"""
valid = True
for error in validate_properties(
validator, properties, instance, schema,
):
valid = False
yield error
# Validate default only when the subschema has validated successfully
# and only when an instance validator is available.
if not valid or not hasattr(validator, 'instance_validator'):
return
if isinstance(instance, dict) and 'default' in instance:
for error in validator.instance_validator.iter_errors(
instance['default'],
instance
):
yield error
OpenApiValidator = extend_validator(
Draft4Validator, {"properties": validate_defaults},
)
NO_SPEC_VERSION_ERR_MSG = """Unable to get the spec version. NO_SPEC_VERSION_ERR_MSG = """Unable to get the spec version.
You are missing either '"swagger": "2.0"' or '"openapi": "3.0.0"' You are missing either '"swagger": "2.0"' or '"openapi": "3.0.0"'
from the top level of your spec.""" from the top level of your spec."""
@@ -44,10 +79,15 @@ class Specification(Mapping):
""" """
@classmethod @classmethod
@abc.abstractmethod
def _validate_spec(cls, spec): def _validate_spec(cls, spec):
""" validate spec against schema """ validate spec against schema
""" """
try:
validator = OpenApiValidator(cls.openapi_schema)
validator.instance_validator = Draft4Validator(spec)
validator.validate(spec)
except jsonschema.exceptions.ValidationError as e:
raise InvalidSpecification.create_from(e)
def get_path_params(self, path): def get_path_params(self, path):
return deep_get(self._spec, ["paths", path]).get("parameters", []) return deep_get(self._spec, ["paths", path]).get("parameters", [])
@@ -164,6 +204,9 @@ class Swagger2Specification(Specification):
yaml_name = 'swagger.yaml' yaml_name = 'swagger.yaml'
operation_cls = Swagger2Operation operation_cls = Swagger2Operation
schema_string = pkg_resources.resource_string('connexion', 'resources/schemas/v2.0/schema.json')
openapi_schema = json.loads(schema_string.decode('utf-8'))
@classmethod @classmethod
def _set_defaults(cls, spec): def _set_defaults(cls, spec):
spec.setdefault('produces', []) spec.setdefault('produces', [])
@@ -206,14 +249,6 @@ class Swagger2Specification(Specification):
self._raw_spec['basePath'] = base_path self._raw_spec['basePath'] = base_path
self._spec['basePath'] = base_path self._spec['basePath'] = base_path
@classmethod
def _validate_spec(cls, spec):
from openapi_spec_validator import validate_v2_spec as validate_spec
try:
validate_spec(spec)
except OpenAPIValidationError as e:
raise InvalidSpecification.create_from(e)
class OpenAPISpecification(Specification): class OpenAPISpecification(Specification):
"""Python interface for an OpenAPI 3 specification.""" """Python interface for an OpenAPI 3 specification."""
@@ -221,6 +256,9 @@ class OpenAPISpecification(Specification):
yaml_name = 'openapi.yaml' yaml_name = 'openapi.yaml'
operation_cls = OpenAPIOperation operation_cls = OpenAPIOperation
schema_string = pkg_resources.resource_string('connexion', 'resources/schemas/v3.0/schema.json')
openapi_schema = json.loads(schema_string.decode('utf-8'))
@classmethod @classmethod
def _set_defaults(cls, spec): def _set_defaults(cls, spec):
spec.setdefault('components', {}) spec.setdefault('components', {})
@@ -233,14 +271,6 @@ class OpenAPISpecification(Specification):
def components(self): def components(self):
return self._spec['components'] return self._spec['components']
@classmethod
def _validate_spec(cls, spec):
from openapi_spec_validator import validate_v3_spec as validate_spec
try:
validate_spec(spec)
except OpenAPIValidationError as e:
raise InvalidSpecification.create_from(e)
@property @property
def base_path(self): def base_path(self):
servers = self._spec.get('servers', []) servers = self._spec.get('servers', [])