Skip to content

OpenAPI 3.1 support + Auto-detect proxies and request / response validator protocols #419

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
Show file tree
Hide file tree
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
18 changes: 16 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ About
#####

Openapi-core is a Python library that adds client-side and server-side support
for the `OpenAPI Specification v3 <https://github.com/OAI/OpenAPI-Specification>`__.
for the `OpenAPI v3.0 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md>`__
and `OpenAPI v3.1 <https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md>`__ specification.

Key features
************
Expand Down Expand Up @@ -57,7 +58,7 @@ Alternatively you can download the code and install from the repository:
Usage
#####

Firstly create your specification object:
Firstly create your specification object. By default, OpenAPI spec version is detected:

.. code-block:: python

Expand Down Expand Up @@ -132,6 +133,19 @@ and unmarshal response data from validation result

Response object should implement OpenAPI Response protocol (See `Integrations <https://openapi-core.readthedocs.io/en/latest/integrations.html>`__).

In order to explicitly validate a:

* OpenAPI 3.0 spec, import ``openapi_v30_request_validator`` or ``openapi_v30_response_validator``
* OpenAPI 3.1 spec, import ``openapi_v31_request_validator`` or ``openapi_v31_response_validator``

.. code:: python

from openapi_core.validation.response import openapi_v31_response_validator

result = openapi_v31_response_validator.validate(spec, request, response)

You can also explicitly import ``openapi_v3_request_validator`` or ``openapi_v3_response_validator`` which is a shortcut to the latest v3 release.

Related projects
################
* `bottle-openapi-3 <https://github.com/cope-systems/bottle-openapi-3>`__
Expand Down
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Welcome to openapi-core's documentation!
========================================

Openapi-core is a Python library that adds client-side and server-side support
for the `OpenAPI Specification v3 <https://github.com/OAI/OpenAPI-Specification>`__.
for the `OpenAPI v3.0 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md>`__
and `OpenAPI v3.1 <https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md>`__ specification.

Key features
------------
Expand Down
6 changes: 3 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Usage
=====

Firstly create your specification: object
Firstly create your specification object. By default, OpenAPI spec version is detected:

.. code-block:: python

Expand Down Expand Up @@ -46,7 +46,7 @@ and unmarshal request data from validation result
# get security data
validated_security = result.security

Request object should be instance of OpenAPIRequest class (See :doc:`integrations`).
Request object should implement OpenAPI Request protocol (See :doc:`integrations`).

Response
--------
Expand Down Expand Up @@ -75,7 +75,7 @@ and unmarshal response data from validation result
# get data
validated_data = result.data

Response object should be instance of OpenAPIResponse class (See :doc:`integrations`).
Response object should implement OpenAPI Response protocol (See :doc:`integrations`).

Security
--------
Expand Down
32 changes: 15 additions & 17 deletions openapi_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
"""OpenAPI core module"""
from openapi_core.spec import Spec
from openapi_core.validation.request.validators import RequestBodyValidator
from openapi_core.validation.request.validators import (
RequestParametersValidator,
from openapi_core.validation.request import openapi_request_body_validator
from openapi_core.validation.request import (
openapi_request_parameters_validator,
)
from openapi_core.validation.request.validators import RequestSecurityValidator
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.response.validators import ResponseDataValidator
from openapi_core.validation.response.validators import (
ResponseHeadersValidator,
)
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.request import openapi_request_security_validator
from openapi_core.validation.request import openapi_request_validator
from openapi_core.validation.response import openapi_response_data_validator
from openapi_core.validation.response import openapi_response_headers_validator
from openapi_core.validation.response import openapi_response_validator
from openapi_core.validation.shortcuts import validate_request
from openapi_core.validation.shortcuts import validate_response

Expand All @@ -24,11 +22,11 @@
"Spec",
"validate_request",
"validate_response",
"RequestValidator",
"ResponseValidator",
"RequestBodyValidator",
"RequestParametersValidator",
"RequestSecurityValidator",
"ResponseDataValidator",
"ResponseHeadersValidator",
"openapi_request_body_validator",
"openapi_request_parameters_validator",
"openapi_request_security_validator",
"openapi_request_validator",
"openapi_response_data_validator",
"openapi_response_headers_validator",
"openapi_response_validator",
]
4 changes: 2 additions & 2 deletions openapi_core/contrib/flask/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
from openapi_core.validation.processors import OpenAPIProcessor
from openapi_core.validation.request import openapi_request_validator
from openapi_core.validation.request.datatypes import RequestValidationResult
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.request.protocols import RequestValidator
from openapi_core.validation.response import openapi_response_validator
from openapi_core.validation.response.datatypes import ResponseValidationResult
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.response.protocols import ResponseValidator


class FlaskOpenAPIViewDecorator(OpenAPIProcessor):
Expand Down
14 changes: 14 additions & 0 deletions openapi_core/unmarshalling/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from openapi_schema_validator import OAS30Validator
from openapi_schema_validator import OAS31Validator

from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.factories import (
Expand All @@ -8,6 +9,9 @@
__all__ = [
"oas30_request_schema_unmarshallers_factory",
"oas30_response_schema_unmarshallers_factory",
"oas31_request_schema_unmarshallers_factory",
"oas31_response_schema_unmarshallers_factory",
"oas31_schema_unmarshallers_factory",
]

oas30_request_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
Expand All @@ -19,3 +23,13 @@
OAS30Validator,
context=UnmarshalContext.RESPONSE,
)

oas31_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
OAS31Validator,
)

# alias to v31 version (request/response are the same bcs no context needed)
oas31_request_schema_unmarshallers_factory = oas31_schema_unmarshallers_factory
oas31_response_schema_unmarshallers_factory = (
oas31_schema_unmarshallers_factory
)
4 changes: 4 additions & 0 deletions openapi_core/validation/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from openapi_core.exceptions import OpenAPIError


class ValidatorDetectError(OpenAPIError):
pass


class ValidationError(OpenAPIError):
pass

Expand Down
4 changes: 2 additions & 2 deletions openapi_core/validation/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from openapi_core.spec import Spec
from openapi_core.validation.request.datatypes import RequestValidationResult
from openapi_core.validation.request.protocols import Request
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.request.protocols import RequestValidator
from openapi_core.validation.response.datatypes import ResponseValidationResult
from openapi_core.validation.response.protocols import Response
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.response.protocols import ResponseValidator


class OpenAPIProcessor:
Expand Down
61 changes: 57 additions & 4 deletions openapi_core/validation/request/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from openapi_core.unmarshalling.schemas import (
oas30_request_schema_unmarshallers_factory,
)
from openapi_core.unmarshalling.schemas import (
oas31_schema_unmarshallers_factory,
)
from openapi_core.validation.request.proxies import DetectRequestValidatorProxy
from openapi_core.validation.request.validators import RequestBodyValidator
from openapi_core.validation.request.validators import (
RequestParametersValidator,
Expand All @@ -14,6 +18,14 @@
"openapi_v30_request_parameters_validator",
"openapi_v30_request_security_validator",
"openapi_v30_request_validator",
"openapi_v31_request_body_validator",
"openapi_v31_request_parameters_validator",
"openapi_v31_request_security_validator",
"openapi_v31_request_validator",
"openapi_v3_request_body_validator",
"openapi_v3_request_parameters_validator",
"openapi_v3_request_security_validator",
"openapi_v3_request_validator",
"openapi_request_body_validator",
"openapi_request_parameters_validator",
"openapi_request_security_validator",
Expand All @@ -33,8 +45,49 @@
schema_unmarshallers_factory=oas30_request_schema_unmarshallers_factory,
)

openapi_v31_request_body_validator = RequestBodyValidator(
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
)
openapi_v31_request_parameters_validator = RequestParametersValidator(
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
)
openapi_v31_request_security_validator = RequestSecurityValidator(
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
)
openapi_v31_request_validator = RequestValidator(
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
)

# alias to the latest v3 version
openapi_request_body_validator = openapi_v30_request_body_validator
openapi_request_parameters_validator = openapi_v30_request_parameters_validator
openapi_request_security_validator = openapi_v30_request_security_validator
openapi_request_validator = openapi_v30_request_validator
openapi_v3_request_body_validator = openapi_v31_request_body_validator
openapi_v3_request_parameters_validator = (
openapi_v31_request_parameters_validator
)
openapi_v3_request_security_validator = openapi_v31_request_security_validator
openapi_v3_request_validator = openapi_v31_request_validator

# detect version spec
openapi_request_body_validator = DetectRequestValidatorProxy(
{
("openapi", "3.0"): openapi_v30_request_body_validator,
("openapi", "3.1"): openapi_v31_request_body_validator,
},
)
openapi_request_parameters_validator = DetectRequestValidatorProxy(
{
("openapi", "3.0"): openapi_v30_request_parameters_validator,
("openapi", "3.1"): openapi_v31_request_parameters_validator,
},
)
openapi_request_security_validator = DetectRequestValidatorProxy(
{
("openapi", "3.0"): openapi_v30_request_security_validator,
("openapi", "3.1"): openapi_v31_request_security_validator,
},
)
openapi_request_validator = DetectRequestValidatorProxy(
{
("openapi", "3.0"): openapi_v30_request_validator,
("openapi", "3.1"): openapi_v31_request_validator,
},
)
13 changes: 13 additions & 0 deletions openapi_core/validation/request/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
from typing_extensions import Protocol
from typing_extensions import runtime_checkable

from openapi_core.spec import Spec
from openapi_core.validation.request.datatypes import RequestParameters
from openapi_core.validation.request.datatypes import RequestValidationResult


@runtime_checkable
Expand Down Expand Up @@ -85,3 +87,14 @@ class SupportsPathPattern(Protocol):
@property
def path_pattern(self) -> str:
...


@runtime_checkable
class RequestValidator(Protocol):
def validate(
self,
spec: Spec,
request: Request,
base_url: Optional[str] = None,
) -> RequestValidationResult:
...
57 changes: 57 additions & 0 deletions openapi_core/validation/request/proxies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""OpenAPI spec validator validation proxies module."""
from typing import Any
from typing import Hashable
from typing import Iterator
from typing import Mapping
from typing import Optional
from typing import Tuple

from openapi_core.exceptions import OpenAPIError
from openapi_core.spec import Spec
from openapi_core.validation.exceptions import ValidatorDetectError
from openapi_core.validation.request.datatypes import RequestValidationResult
from openapi_core.validation.request.protocols import Request
from openapi_core.validation.request.validators import BaseRequestValidator


class DetectRequestValidatorProxy:
def __init__(
self, choices: Mapping[Tuple[str, str], BaseRequestValidator]
):
self.choices = choices

def detect(self, spec: Spec) -> BaseRequestValidator:
for (key, value), validator in self.choices.items():
if key in spec and spec[key].startswith(value):
return validator
raise ValidatorDetectError("Spec schema version not detected")

def validate(
self,
spec: Spec,
request: Request,
base_url: Optional[str] = None,
) -> RequestValidationResult:
validator = self.detect(spec)
return validator.validate(spec, request, base_url=base_url)

def is_valid(
self,
spec: Spec,
request: Request,
base_url: Optional[str] = None,
) -> bool:
validator = self.detect(spec)
error = next(
validator.iter_errors(spec, request, base_url=base_url), None
)
return error is None

def iter_errors(
self,
spec: Spec,
request: Request,
base_url: Optional[str] = None,
) -> Iterator[Exception]:
validator = self.detect(spec)
yield from validator.iter_errors(spec, request, base_url=base_url)
11 changes: 11 additions & 0 deletions openapi_core/validation/request/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import warnings
from typing import Any
from typing import Dict
from typing import Iterator
from typing import Optional

from openapi_core.casting.schemas import schema_casters_factory
Expand All @@ -20,6 +21,7 @@
from openapi_core.deserializing.parameters.factories import (
ParameterDeserializersFactory,
)
from openapi_core.exceptions import OpenAPIError
from openapi_core.security import security_provider_factory
from openapi_core.security.exceptions import SecurityError
from openapi_core.security.factories import SecurityProviderFactory
Expand Down Expand Up @@ -64,6 +66,15 @@ def __init__(
)
self.security_provider_factory = security_provider_factory

def iter_errors(
self,
spec: Spec,
request: Request,
base_url: Optional[str] = None,
) -> Iterator[Exception]:
result = self.validate(spec, request, base_url=base_url)
yield from result.errors

def validate(
self,
spec: Spec,
Expand Down
Loading