Skip to content

Commit 00ae628

Browse files
authored
Merge pull request #473 from p1c2u/refactor/validation-errors-refactor
Validation errors refactor
2 parents dcc431c + 02590a4 commit 00ae628

22 files changed

+419
-220
lines changed

openapi_core/contrib/django/handlers.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,14 @@
1313
from openapi_core.templating.paths.exceptions import OperationNotFound
1414
from openapi_core.templating.paths.exceptions import PathNotFound
1515
from openapi_core.templating.paths.exceptions import ServerNotFound
16-
from openapi_core.validation.exceptions import InvalidSecurity
17-
from openapi_core.validation.exceptions import MissingRequiredParameter
16+
from openapi_core.templating.security.exceptions import SecurityNotFound
1817

1918

2019
class DjangoOpenAPIErrorsHandler:
2120

22-
OPENAPI_ERROR_STATUS: Dict[Type[Exception], int] = {
23-
MissingRequiredParameter: 400,
21+
OPENAPI_ERROR_STATUS: Dict[Type[BaseException], int] = {
2422
ServerNotFound: 400,
25-
InvalidSecurity: 403,
23+
SecurityNotFound: 403,
2624
OperationNotFound: 405,
2725
PathNotFound: 404,
2826
MediaTypeNotFound: 415,
@@ -43,7 +41,9 @@ def handle(
4341
return JsonResponse(data, status=data_error_max["status"])
4442

4543
@classmethod
46-
def format_openapi_error(cls, error: Exception) -> Dict[str, Any]:
44+
def format_openapi_error(cls, error: BaseException) -> Dict[str, Any]:
45+
if error.__cause__ is not None:
46+
error = error.__cause__
4747
return {
4848
"title": str(error),
4949
"status": cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),

openapi_core/contrib/falcon/handlers.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,14 @@
1414
from openapi_core.templating.paths.exceptions import OperationNotFound
1515
from openapi_core.templating.paths.exceptions import PathNotFound
1616
from openapi_core.templating.paths.exceptions import ServerNotFound
17-
from openapi_core.validation.exceptions import InvalidSecurity
18-
from openapi_core.validation.exceptions import MissingRequiredParameter
17+
from openapi_core.templating.security.exceptions import SecurityNotFound
1918

2019

2120
class FalconOpenAPIErrorsHandler:
2221

23-
OPENAPI_ERROR_STATUS: Dict[Type[Exception], int] = {
24-
MissingRequiredParameter: 400,
22+
OPENAPI_ERROR_STATUS: Dict[Type[BaseException], int] = {
2523
ServerNotFound: 400,
26-
InvalidSecurity: 403,
24+
SecurityNotFound: 403,
2725
OperationNotFound: 405,
2826
PathNotFound: 404,
2927
MediaTypeNotFound: 415,
@@ -49,7 +47,9 @@ def handle(
4947
resp.complete = True
5048

5149
@classmethod
52-
def format_openapi_error(cls, error: Exception) -> Dict[str, Any]:
50+
def format_openapi_error(cls, error: BaseException) -> Dict[str, Any]:
51+
if error.__cause__ is not None:
52+
error = error.__cause__
5353
return {
5454
"title": str(error),
5555
"status": cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),

openapi_core/contrib/flask/handlers.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@
1212
from openapi_core.templating.paths.exceptions import OperationNotFound
1313
from openapi_core.templating.paths.exceptions import PathNotFound
1414
from openapi_core.templating.paths.exceptions import ServerNotFound
15+
from openapi_core.templating.security.exceptions import SecurityNotFound
1516

1617

1718
class FlaskOpenAPIErrorsHandler:
1819

19-
OPENAPI_ERROR_STATUS: Dict[Type[Exception], int] = {
20+
OPENAPI_ERROR_STATUS: Dict[Type[BaseException], int] = {
2021
ServerNotFound: 400,
22+
SecurityNotFound: 403,
2123
OperationNotFound: 405,
2224
PathNotFound: 404,
2325
MediaTypeNotFound: 415,
2426
}
2527

2628
@classmethod
27-
def handle(cls, errors: Iterable[Exception]) -> Response:
29+
def handle(cls, errors: Iterable[BaseException]) -> Response:
2830
data_errors = [cls.format_openapi_error(err) for err in errors]
2931
data = {
3032
"errors": data_errors,
@@ -36,7 +38,9 @@ def handle(cls, errors: Iterable[Exception]) -> Response:
3638
)
3739

3840
@classmethod
39-
def format_openapi_error(cls, error: Exception) -> Dict[str, Any]:
41+
def format_openapi_error(cls, error: BaseException) -> Dict[str, Any]:
42+
if error.__cause__ is not None:
43+
error = error.__cause__
4044
return {
4145
"title": str(error),
4246
"status": cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),

openapi_core/security/exceptions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from openapi_core.exceptions import OpenAPIError
22

33

4-
class SecurityError(OpenAPIError):
4+
class SecurityProviderError(OpenAPIError):
55
pass

openapi_core/security/providers.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import warnings
22
from typing import Any
33

4-
from openapi_core.security.exceptions import SecurityError
4+
from openapi_core.security.exceptions import SecurityProviderError
55
from openapi_core.spec import Spec
66
from openapi_core.validation.request.datatypes import RequestParameters
77

@@ -25,22 +25,26 @@ def __call__(self, parameters: RequestParameters) -> Any:
2525
location = self.scheme["in"]
2626
source = getattr(parameters, location)
2727
if name not in source:
28-
raise SecurityError("Missing api key parameter.")
28+
raise SecurityProviderError("Missing api key parameter.")
2929
return source[name]
3030

3131

3232
class HttpProvider(BaseProvider):
3333
def __call__(self, parameters: RequestParameters) -> Any:
3434
if "Authorization" not in parameters.header:
35-
raise SecurityError("Missing authorization header.")
35+
raise SecurityProviderError("Missing authorization header.")
3636
auth_header = parameters.header["Authorization"]
3737
try:
3838
auth_type, encoded_credentials = auth_header.split(" ", 1)
3939
except ValueError:
40-
raise SecurityError("Could not parse authorization header.")
40+
raise SecurityProviderError(
41+
"Could not parse authorization header."
42+
)
4143

4244
scheme = self.scheme["scheme"]
4345
if auth_type.lower() != scheme:
44-
raise SecurityError(f"Unknown authorization method {auth_type}")
46+
raise SecurityProviderError(
47+
f"Unknown authorization method {auth_type}"
48+
)
4549

4650
return encoded_credentials

openapi_core/templating/security/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from dataclasses import dataclass
2+
from typing import List
3+
4+
from openapi_core.exceptions import OpenAPIError
5+
6+
7+
class SecurityFinderError(OpenAPIError):
8+
"""Security finder error"""
9+
10+
11+
@dataclass
12+
class SecurityNotFound(SecurityFinderError):
13+
"""Find security error"""
14+
15+
schemes: List[List[str]]
16+
17+
def __str__(self) -> str:
18+
return f"Security not found. Schemes not valid for any requirement: {str(self.schemes)}"

openapi_core/validation/datatypes.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
from dataclasses import dataclass
33
from typing import Iterable
44

5+
from openapi_core.exceptions import OpenAPIError
6+
57

68
@dataclass
79
class BaseValidationResult:
8-
errors: Iterable[Exception]
10+
errors: Iterable[OpenAPIError]
911

1012
def raise_for_errors(self) -> None:
1113
for error in self.errors:

openapi_core/validation/decorators.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from functools import wraps
2+
from inspect import signature
3+
from typing import Any
4+
from typing import Callable
5+
from typing import Optional
6+
from typing import Type
7+
8+
from openapi_core.exceptions import OpenAPIError
9+
from openapi_core.unmarshalling.schemas.exceptions import ValidateError
10+
11+
OpenAPIErrorType = Type[OpenAPIError]
12+
13+
14+
class ValidationErrorWrapper:
15+
def __init__(
16+
self,
17+
err_cls: OpenAPIErrorType,
18+
err_validate_cls: Optional[OpenAPIErrorType] = None,
19+
err_cls_init: Optional[str] = None,
20+
**err_cls_kw: Any
21+
):
22+
self.err_cls = err_cls
23+
self.err_validate_cls = err_validate_cls or err_cls
24+
self.err_cls_init = err_cls_init
25+
self.err_cls_kw = err_cls_kw
26+
27+
def __call__(self, f: Callable[..., Any]) -> Callable[..., Any]:
28+
@wraps(f)
29+
def wrapper(*args: Any, **kwds: Any) -> Any:
30+
try:
31+
return f(*args, **kwds)
32+
except ValidateError as exc:
33+
self._raise_error(exc, self.err_validate_cls, f, *args, **kwds)
34+
except OpenAPIError as exc:
35+
self._raise_error(exc, self.err_cls, f, *args, **kwds)
36+
37+
return wrapper
38+
39+
def _raise_error(
40+
self,
41+
exc: OpenAPIError,
42+
cls: OpenAPIErrorType,
43+
f: Callable[..., Any],
44+
*args: Any,
45+
**kwds: Any
46+
) -> None:
47+
if isinstance(exc, self.err_cls):
48+
raise
49+
sig = signature(f)
50+
ba = sig.bind(*args, **kwds)
51+
kw = {
52+
name: ba.arguments[func_kw]
53+
for name, func_kw in self.err_cls_kw.items()
54+
}
55+
init = cls
56+
if self.err_cls_init is not None:
57+
init = getattr(cls, self.err_cls_init)
58+
raise init(**kw) from exc

openapi_core/validation/exceptions.py

+2-54
Original file line numberDiff line numberDiff line change
@@ -8,59 +8,7 @@ class ValidatorDetectError(OpenAPIError):
88
pass
99

1010

11-
class ValidationError(OpenAPIError):
12-
pass
13-
14-
15-
@dataclass
16-
class InvalidSecurity(ValidationError):
17-
def __str__(self) -> str:
18-
return "Security not valid for any requirement"
19-
20-
21-
class OpenAPIParameterError(OpenAPIError):
22-
pass
23-
24-
25-
class MissingParameterError(OpenAPIParameterError):
26-
"""Missing parameter error"""
27-
28-
29-
@dataclass
30-
class MissingParameter(MissingParameterError):
31-
name: str
32-
33-
def __str__(self) -> str:
34-
return f"Missing parameter (without default value): {self.name}"
35-
36-
3711
@dataclass
38-
class MissingRequiredParameter(MissingParameterError):
39-
name: str
40-
41-
def __str__(self) -> str:
42-
return f"Missing required parameter: {self.name}"
43-
44-
45-
class OpenAPIHeaderError(OpenAPIError):
46-
pass
47-
48-
49-
class MissingHeaderError(OpenAPIHeaderError):
50-
"""Missing header error"""
51-
52-
53-
@dataclass
54-
class MissingHeader(MissingHeaderError):
55-
name: str
56-
57-
def __str__(self) -> str:
58-
return f"Missing header (without default value): {self.name}"
59-
60-
61-
@dataclass
62-
class MissingRequiredHeader(MissingHeaderError):
63-
name: str
64-
12+
class ValidationError(OpenAPIError):
6513
def __str__(self) -> str:
66-
return f"Missing required header: {self.name}"
14+
return f"{self.__class__.__name__}: {self.__cause__}"

openapi_core/validation/request/exceptions.py

+57-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
from typing import Iterable
44

55
from openapi_core.exceptions import OpenAPIError
6+
from openapi_core.spec import Spec
7+
from openapi_core.unmarshalling.schemas.exceptions import ValidateError
8+
from openapi_core.validation.exceptions import ValidationError
69
from openapi_core.validation.request.datatypes import Parameters
7-
from openapi_core.validation.request.protocols import Request
810

911

1012
@dataclass
1113
class ParametersError(Exception):
1214
parameters: Parameters
13-
errors: Iterable[Exception]
15+
errors: Iterable[OpenAPIError]
1416

1517
@property
16-
def context(self) -> Iterable[Exception]:
18+
def context(self) -> Iterable[OpenAPIError]:
1719
warnings.warn(
1820
"context property of ParametersError is deprecated. "
1921
"Use errors instead.",
@@ -22,11 +24,20 @@ def context(self) -> Iterable[Exception]:
2224
return self.errors
2325

2426

25-
class OpenAPIRequestBodyError(OpenAPIError):
26-
pass
27+
class RequestError(ValidationError):
28+
"""Request error"""
29+
30+
31+
class RequestBodyError(RequestError):
32+
def __str__(self) -> str:
33+
return "Request body error"
2734

2835

29-
class MissingRequestBodyError(OpenAPIRequestBodyError):
36+
class InvalidRequestBody(RequestBodyError, ValidateError):
37+
"""Invalid request body"""
38+
39+
40+
class MissingRequestBodyError(RequestBodyError):
3041
"""Missing request body error"""
3142

3243

@@ -38,3 +49,43 @@ def __str__(self) -> str:
3849
class MissingRequiredRequestBody(MissingRequestBodyError):
3950
def __str__(self) -> str:
4051
return "Missing required request body"
52+
53+
54+
@dataclass
55+
class ParameterError(RequestError):
56+
name: str
57+
location: str
58+
59+
@classmethod
60+
def from_spec(cls, spec: Spec) -> "ParameterError":
61+
return cls(spec["name"], spec["in"])
62+
63+
def __str__(self) -> str:
64+
return f"{self.location.title()} parameter error: {self.name}"
65+
66+
67+
class InvalidParameter(ParameterError, ValidateError):
68+
def __str__(self) -> str:
69+
return f"Invalid {self.location} parameter: {self.name}"
70+
71+
72+
class MissingParameterError(ParameterError):
73+
"""Missing parameter error"""
74+
75+
76+
class MissingParameter(MissingParameterError):
77+
def __str__(self) -> str:
78+
return f"Missing {self.location} parameter: {self.name}"
79+
80+
81+
class MissingRequiredParameter(MissingParameterError):
82+
def __str__(self) -> str:
83+
return f"Missing required {self.location} parameter: {self.name}"
84+
85+
86+
class SecurityError(RequestError):
87+
pass
88+
89+
90+
class InvalidSecurity(SecurityError, ValidateError):
91+
"""Invalid security"""

0 commit comments

Comments
 (0)