Skip to content

Specification validation as part of shortcuts #686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 13, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions docs/customizations.rst
Original file line number Diff line number Diff line change
@@ -4,18 +4,17 @@ Customizations
Specification validation
------------------------

By default, the provided specification is validated on ``Spec`` object creation time.
By default, the specified specification is also validated.

If you know you have a valid specification already, disabling the validator can improve the performance.

.. code-block:: python
:emphasize-lines: 5
:emphasize-lines: 4
from openapi_core import Spec
spec = Spec.from_dict(
spec_dict,
validator=None,
validate_request(
request,
spec=spec,
spec_validator_cls=None,
)
Media type deserializers
8 changes: 8 additions & 0 deletions openapi_core/shortcuts.py
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ def unmarshal_apicall_request(
if not issubclass(cls, RequestUnmarshaller):
raise TypeError("'cls' argument is not type of RequestUnmarshaller")
v = cls(spec, base_url=base_url, **unmarshaller_kwargs)
v.check_spec(spec)
result = v.unmarshal(request)
result.raise_for_errors()
return result
@@ -134,6 +135,7 @@ def unmarshal_webhook_request(
"'cls' argument is not type of WebhookRequestUnmarshaller"
)
v = cls(spec, base_url=base_url, **unmarshaller_kwargs)
v.check_spec(spec)
result = v.unmarshal(request)
result.raise_for_errors()
return result
@@ -198,6 +200,7 @@ def unmarshal_apicall_response(
if not issubclass(cls, ResponseUnmarshaller):
raise TypeError("'cls' argument is not type of ResponseUnmarshaller")
v = cls(spec, base_url=base_url, **unmarshaller_kwargs)
v.check_spec(spec)
result = v.unmarshal(request, response)
result.raise_for_errors()
return result
@@ -227,6 +230,7 @@ def unmarshal_webhook_response(
"'cls' argument is not type of WebhookResponseUnmarshaller"
)
v = cls(spec, base_url=base_url, **unmarshaller_kwargs)
v.check_spec(spec)
result = v.unmarshal(request, response)
result.raise_for_errors()
return result
@@ -378,6 +382,7 @@ def validate_apicall_request(
if not issubclass(cls, RequestValidator):
raise TypeError("'cls' argument is not type of RequestValidator")
v = cls(spec, base_url=base_url, **validator_kwargs)
v.check_spec(spec)
return v.validate(request)


@@ -402,6 +407,7 @@ def validate_webhook_request(
"'cls' argument is not type of WebhookRequestValidator"
)
v = cls(spec, base_url=base_url, **validator_kwargs)
v.check_spec(spec)
return v.validate(request)


@@ -425,6 +431,7 @@ def validate_apicall_response(
if not issubclass(cls, ResponseValidator):
raise TypeError("'cls' argument is not type of ResponseValidator")
v = cls(spec, base_url=base_url, **validator_kwargs)
v.check_spec(spec)
return v.validate(request, response)


@@ -452,4 +459,5 @@ def validate_webhook_response(
"'cls' argument is not type of WebhookResponseValidator"
)
v = cls(spec, base_url=base_url, **validator_kwargs)
v.check_spec(spec)
return v.validate(request, response)
6 changes: 6 additions & 0 deletions openapi_core/unmarshalling/request/protocols.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@ class RequestUnmarshaller(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def unmarshal(
self,
request: Request,
@@ -27,6 +30,9 @@ class WebhookRequestUnmarshaller(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def unmarshal(
self,
request: WebhookRequest,
4 changes: 4 additions & 0 deletions openapi_core/unmarshalling/request/unmarshallers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional

from jsonschema_path import SchemaPath
from openapi_spec_validator.validation.types import SpecValidatorType

from openapi_core.casting.schemas import schema_casters_factory
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -88,6 +89,7 @@ def __init__(
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
spec_validator_cls: Optional[SpecValidatorType] = None,
format_validators: Optional[FormatValidatorsDict] = None,
extra_format_validators: Optional[FormatValidatorsDict] = None,
extra_media_type_deserializers: Optional[
@@ -108,6 +110,7 @@ def __init__(
style_deserializers_factory=style_deserializers_factory,
media_type_deserializers_factory=media_type_deserializers_factory,
schema_validators_factory=schema_validators_factory,
spec_validator_cls=spec_validator_cls,
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
@@ -123,6 +126,7 @@ def __init__(
style_deserializers_factory=style_deserializers_factory,
media_type_deserializers_factory=media_type_deserializers_factory,
schema_validators_factory=schema_validators_factory,
spec_validator_cls=spec_validator_cls,
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
6 changes: 6 additions & 0 deletions openapi_core/unmarshalling/response/protocols.py
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ class ResponseUnmarshaller(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def unmarshal(
self,
request: Request,
@@ -33,6 +36,9 @@ class WebhookResponseUnmarshaller(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def unmarshal(
self,
request: WebhookRequest,
3 changes: 3 additions & 0 deletions openapi_core/unmarshalling/unmarshallers.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from typing import Tuple

from jsonschema_path import SchemaPath
from openapi_spec_validator.validation.types import SpecValidatorType

from openapi_core.casting.schemas import schema_casters_factory
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -42,6 +43,7 @@ def __init__(
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
spec_validator_cls: Optional[SpecValidatorType] = None,
format_validators: Optional[FormatValidatorsDict] = None,
extra_format_validators: Optional[FormatValidatorsDict] = None,
extra_media_type_deserializers: Optional[
@@ -64,6 +66,7 @@ def __init__(
style_deserializers_factory=style_deserializers_factory,
media_type_deserializers_factory=media_type_deserializers_factory,
schema_validators_factory=schema_validators_factory,
spec_validator_cls=spec_validator_cls,
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
6 changes: 6 additions & 0 deletions openapi_core/validation/request/protocols.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@ class RequestValidator(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def iter_errors(
self,
request: Request,
@@ -33,6 +36,9 @@ class WebhookRequestValidator(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def iter_errors(
self,
request: WebhookRequest,
17 changes: 17 additions & 0 deletions openapi_core/validation/request/validators.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,9 @@
from typing import Optional

from jsonschema_path import SchemaPath
from openapi_spec_validator import OpenAPIV30SpecValidator
from openapi_spec_validator import OpenAPIV31SpecValidator
from openapi_spec_validator.validation.types import SpecValidatorType

from openapi_core.casting.schemas import schema_casters_factory
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -70,6 +73,7 @@ def __init__(
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
spec_validator_cls: Optional[SpecValidatorType] = None,
format_validators: Optional[FormatValidatorsDict] = None,
extra_format_validators: Optional[FormatValidatorsDict] = None,
extra_media_type_deserializers: Optional[
@@ -84,6 +88,7 @@ def __init__(
style_deserializers_factory=style_deserializers_factory,
media_type_deserializers_factory=media_type_deserializers_factory,
schema_validators_factory=schema_validators_factory,
spec_validator_cls=spec_validator_cls,
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
@@ -387,53 +392,65 @@ def iter_errors(self, request: WebhookRequest) -> Iterator[Exception]:


class V30RequestBodyValidator(APICallRequestBodyValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_write_schema_validators_factory


class V30RequestParametersValidator(APICallRequestParametersValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_write_schema_validators_factory


class V30RequestSecurityValidator(APICallRequestSecurityValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_write_schema_validators_factory


class V30RequestValidator(APICallRequestValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_write_schema_validators_factory


class V31RequestBodyValidator(APICallRequestBodyValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31RequestParametersValidator(APICallRequestParametersValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31RequestSecurityValidator(APICallRequestSecurityValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31RequestValidator(APICallRequestValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
path_finder_cls = WebhookPathFinder


class V31WebhookRequestBodyValidator(WebhookRequestBodyValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
path_finder_cls = WebhookPathFinder


class V31WebhookRequestParametersValidator(WebhookRequestParametersValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
path_finder_cls = WebhookPathFinder


class V31WebhookRequestSecurityValidator(WebhookRequestSecurityValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
path_finder_cls = WebhookPathFinder


class V31WebhookRequestValidator(WebhookRequestValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
path_finder_cls = WebhookPathFinder
6 changes: 6 additions & 0 deletions openapi_core/validation/response/protocols.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@ class ResponseValidator(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def iter_errors(
self,
request: Request,
@@ -36,6 +39,9 @@ class WebhookResponseValidator(Protocol):
def __init__(self, spec: SchemaPath, base_url: Optional[str] = None):
...

def check_spec(self, spec: SchemaPath) -> None:
...

def iter_errors(
self,
request: WebhookRequest,
11 changes: 11 additions & 0 deletions openapi_core/validation/response/validators.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
from typing import Mapping

from jsonschema_path import SchemaPath
from openapi_spec_validator import OpenAPIV30SpecValidator
from openapi_spec_validator import OpenAPIV31SpecValidator

from openapi_core.exceptions import OpenAPIError
from openapi_core.protocols import Request
@@ -330,36 +332,45 @@ def iter_errors(


class V30ResponseDataValidator(APICallResponseDataValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_read_schema_validators_factory


class V30ResponseHeadersValidator(APICallResponseHeadersValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_read_schema_validators_factory


class V30ResponseValidator(APICallResponseValidator):
spec_validator_cls = OpenAPIV30SpecValidator
schema_validators_factory = oas30_read_schema_validators_factory


class V31ResponseDataValidator(APICallResponseDataValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31ResponseHeadersValidator(APICallResponseHeadersValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31ResponseValidator(APICallResponseValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31WebhookResponseDataValidator(WebhookResponseDataValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31WebhookResponseHeadersValidator(WebhookResponseHeadersValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory


class V31WebhookResponseValidator(WebhookResponseValidator):
spec_validator_cls = OpenAPIV31SpecValidator
schema_validators_factory = oas31_schema_validators_factory
11 changes: 11 additions & 0 deletions openapi_core/validation/validators.py
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
from urllib.parse import urljoin

from jsonschema_path import SchemaPath
from openapi_spec_validator.validation.types import SpecValidatorType

from openapi_core.casting.schemas import schema_casters_factory
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -43,6 +44,7 @@

class BaseValidator:
schema_validators_factory: SchemaValidatorsFactory = NotImplemented
spec_validator_cls: Optional[SpecValidatorType] = None

def __init__(
self,
@@ -52,6 +54,7 @@ def __init__(
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
spec_validator_cls: Optional[SpecValidatorType] = None,
format_validators: Optional[FormatValidatorsDict] = None,
extra_format_validators: Optional[FormatValidatorsDict] = None,
extra_media_type_deserializers: Optional[
@@ -73,10 +76,18 @@ def __init__(
raise NotImplementedError(
"schema_validators_factory is not assigned"
)
self.spec_validator_cls = spec_validator_cls or self.spec_validator_cls
self.format_validators = format_validators
self.extra_format_validators = extra_format_validators
self.extra_media_type_deserializers = extra_media_type_deserializers

def check_spec(self, spec: SchemaPath) -> None:
if self.spec_validator_cls is None:
return

validator = self.spec_validator_cls(spec)
validator.validate()

def _find_media_type(
self, content: SchemaPath, mimetype: Optional[str] = None
) -> MediaType:
27 changes: 5 additions & 22 deletions poetry.lock
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ isodate = "*"
more-itertools = "*"
parse = "*"
openapi-schema-validator = "^0.6.0"
openapi-spec-validator = ">=0.6.0,<0.8.0"
openapi-spec-validator = "^0.7.1"
requests = {version = "*", optional = true}
werkzeug = "*"
jsonschema-path = "^0.3.1"
13 changes: 11 additions & 2 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -4,12 +4,21 @@

@pytest.fixture
def spec_v30():
return SchemaPath.from_dict({"openapi": "3.0"})
return SchemaPath.from_dict({"openapi": "3.0.0"})


@pytest.fixture
def spec_v31():
return SchemaPath.from_dict({"openapi": "3.1"})
return SchemaPath.from_dict(
{
"openapi": "3.1.0",
"info": {
"title": "Spec",
"version": "0.0.1",
},
"paths": {},
}
)


@pytest.fixture
36 changes: 36 additions & 0 deletions tests/unit/test_shortcuts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import mock

import pytest
from openapi_spec_validator import OpenAPIV31SpecValidator

from openapi_core import unmarshal_apicall_request
from openapi_core import unmarshal_apicall_response
@@ -46,6 +47,7 @@


class MockClass:
spec_validator_cls = None
schema_validators_factory = None
schema_unmarshallers_factory = None

@@ -586,6 +588,23 @@ def test_cls_apicall(self, spec_v31):
(request,),
]

def test_cls_apicall_with_spec_validator_cls(self, spec_v31):
request = mock.Mock(spec=Request)
TestAPICallReq = type(
"TestAPICallReq",
(MockReqValidator, APICallRequestValidator),
{
"spec_validator_cls": OpenAPIV31SpecValidator,
},
)

result = validate_request(request, spec=spec_v31, cls=TestAPICallReq)

assert result is None
assert TestAPICallReq.validate_calls == [
(request,),
]

def test_cls_webhook(self, spec_v31):
request = mock.Mock(spec=Request)
TestWebhookReq = type(
@@ -601,6 +620,23 @@ def test_cls_webhook(self, spec_v31):
(request,),
]

def test_cls_webhook_with_spec_validator_cls(self, spec_v31):
request = mock.Mock(spec=Request)
TestWebhookReq = type(
"TestWebhookReq",
(MockReqValidator, WebhookRequestValidator),
{
"spec_validator_cls": OpenAPIV31SpecValidator,
},
)

result = validate_request(request, spec=spec_v31, cls=TestWebhookReq)

assert result is None
assert TestWebhookReq.validate_calls == [
(request,),
]

def test_webhook_cls(self, spec_v31):
request = mock.Mock(spec=WebhookRequest)
TestWebhookReq = type(