|
|
|
"""
|
|
|
|
This module give access to OpenAPI specifications schemas
|
|
|
|
and allows to validate specs against them.
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
|
|
|
"""
|
|
|
|
import io
|
|
|
|
import json
|
|
|
|
|
|
|
|
import importlib_resources
|
|
|
|
|
|
|
|
from collections.abc import Mapping
|
|
|
|
from jsonschema import Draft4Validator
|
|
|
|
|
|
|
|
from flask_restx import errors
|
|
|
|
|
|
|
|
|
|
|
|
class SchemaValidationError(errors.ValidationError):
|
|
|
|
"""
|
|
|
|
Raised when specification is not valid
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, msg, errors=None):
|
|
|
|
super(SchemaValidationError, self).__init__(msg)
|
|
|
|
self.errors = errors
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
msg = [self.msg]
|
|
|
|
for error in sorted(self.errors, key=lambda e: e.path):
|
|
|
|
path = ".".join(error.path)
|
|
|
|
msg.append("- {}: {}".format(path, error.message))
|
|
|
|
for suberror in sorted(error.context, key=lambda e: e.schema_path):
|
|
|
|
path = ".".join(suberror.schema_path)
|
|
|
|
msg.append(" - {}: {}".format(path, suberror.message))
|
|
|
|
return "\n".join(msg)
|
|
|
|
|
|
|
|
__unicode__ = __str__
|
|
|
|
|
|
|
|
|
|
|
|
class LazySchema(Mapping):
|
|
|
|
"""
|
|
|
|
A thin wrapper around schema file lazy loading the data on first access
|
|
|
|
|
|
|
|
:param filename str: The package relative json schema filename
|
|
|
|
:param validator: The jsonschema validator class version
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, filename, validator=Draft4Validator):
|
|
|
|
super(LazySchema, self).__init__()
|
|
|
|
self.filename = filename
|
|
|
|
self._schema = None
|
|
|
|
self._validator = validator
|
|
|
|
|
|
|
|
def _load(self):
|
|
|
|
if not self._schema:
|
|
|
|
ref = importlib_resources.files(__name__) / self.filename
|
|
|
|
|
|
|
|
with io.open(ref) as infile:
|
|
|
|
self._schema = json.load(infile)
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
self._load()
|
|
|
|
return self._schema.__getitem__(key)
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
self._load()
|
|
|
|
return self._schema.__iter__()
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
self._load()
|
|
|
|
return self._schema.__len__()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def validator(self):
|
|
|
|
"""The jsonschema validator to validate against"""
|
|
|
|
return self._validator(self)
|
|
|
|
|
|
|
|
|
|
|
|
#: OpenAPI 2.0 specification schema
|
|
|
|
OAS_20 = LazySchema("oas-2.0.json")
|
|
|
|
|
|
|
|
#: Map supported OpenAPI versions to their JSON schema
|
|
|
|
VERSIONS = {
|
|
|
|
"2.0": OAS_20,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def validate(data):
|
|
|
|
"""
|
|
|
|
Validate an OpenAPI specification.
|
|
|
|
|
|
|
|
Supported OpenAPI versions: 2.0
|
|
|
|
|
|
|
|
:param data dict: The specification to validate
|
|
|
|
:returns boolean: True if the specification is valid
|
|
|
|
:raises SchemaValidationError: when the specification is invalid
|
|
|
|
:raises flask_restx.errors.SpecsError: when it's not possible to determinate
|
|
|
|
the schema to validate against
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
|
|
|
"""
|
|
|
|
if "swagger" not in data:
|
|
|
|
raise errors.SpecsError("Unable to determinate OpenAPI schema version")
|
|
|
|
|
|
|
|
version = data["swagger"]
|
|
|
|
if version not in VERSIONS:
|
|
|
|
raise errors.SpecsError('Unknown OpenAPI schema version "{}"'.format(version))
|
|
|
|
|
|
|
|
validator = VERSIONS[version].validator
|
|
|
|
|
|
|
|
validation_errors = list(validator.iter_errors(data))
|
|
|
|
if validation_errors:
|
|
|
|
raise SchemaValidationError(
|
|
|
|
"OpenAPI {} validation failed".format(version), errors=validation_errors
|
|
|
|
)
|
|
|
|
return True
|