Skip to content

Commit 1ed28ff

Browse files
authored
Merge pull request #171 from p1c2u/feature/cli-detect-spec-version
CLI detect spec version
2 parents 1194aac + 7bd456d commit 1ed28ff

File tree

13 files changed

+174
-83
lines changed

13 files changed

+174
-83
lines changed

README.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ or more pythonic way:
7272
Examples
7373
********
7474

75-
By default, OpenAPI v3.1 syntax is expected. To validate an OpenAPI v3.1 spec:
75+
By default, OpenAPI spec version is detected. To validate spec:
7676

7777
.. code:: python
7878
@@ -92,9 +92,9 @@ By default, OpenAPI v3.1 syntax is expected. To validate an OpenAPI v3.1 spec:
9292
9393
In order to explicitly validate a:
9494

95-
* Swagger / OpenAPI 2.0 spec file, import ``validate_v2_spec``
96-
* OpenAPI 3.0 spec file, import ``validate_v30_spec``
97-
* OpenAPI 3.1 spec file, import ``validate_v31_spec``
95+
* Swagger / OpenAPI 2.0 spec, import ``validate_v2_spec``
96+
* OpenAPI 3.0 spec, import ``validate_v30_spec``
97+
* OpenAPI 3.1 spec, import ``validate_v31_spec``
9898

9999
instead of ``validate_spec``.
100100

@@ -117,9 +117,9 @@ You can also validate spec from url:
117117
118118
In order to explicitly validate a:
119119

120-
* Swagger / OpenAPI 2.0 spec file, import ``validate_v2_spec_url``
121-
* OpenAPI 3.0 spec file, import ``validate_v30_spec_url``
122-
* OpenAPI 3.1 spec file, import ``validate_v31_spec_url``
120+
* Swagger / OpenAPI 2.0 spec url, import ``validate_v2_spec_url``
121+
* OpenAPI 3.0 spec url, import ``validate_v30_spec_url``
122+
* OpenAPI 3.1 spec url, import ``validate_v31_spec_url``
123123

124124
instead of ``validate_spec_url``.
125125

openapi_spec_validator/__init__.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from jsonschema_spec.handlers import default_handlers
22

3-
from openapi_spec_validator.shortcuts import validate_spec_detect_factory
43
from openapi_spec_validator.shortcuts import validate_spec_factory
5-
from openapi_spec_validator.shortcuts import validate_spec_url_detect_factory
64
from openapi_spec_validator.shortcuts import validate_spec_url_factory
5+
from openapi_spec_validator.validation import openapi_spec_validator_proxy
76
from openapi_spec_validator.validation import openapi_v2_spec_validator
87
from openapi_spec_validator.validation import openapi_v3_spec_validator
98
from openapi_spec_validator.validation import openapi_v30_spec_validator
@@ -33,20 +32,6 @@
3332
]
3433

3534
# shortcuts
36-
validate_spec = validate_spec_detect_factory(
37-
{
38-
("swagger", "2.0"): openapi_v2_spec_validator,
39-
("openapi", "3.0"): openapi_v30_spec_validator,
40-
("openapi", "3.1"): openapi_v31_spec_validator,
41-
},
42-
)
43-
validate_spec_url = validate_spec_url_detect_factory(
44-
{
45-
("swagger", "2.0"): openapi_v2_spec_validator,
46-
("openapi", "3.0"): openapi_v30_spec_validator,
47-
("openapi", "3.1"): openapi_v31_spec_validator,
48-
},
49-
)
5035
validate_v2_spec = validate_spec_factory(openapi_v2_spec_validator)
5136
validate_v2_spec_url = validate_spec_url_factory(openapi_v2_spec_validator)
5237

@@ -56,6 +41,9 @@
5641
validate_v31_spec = validate_spec_factory(openapi_v31_spec_validator)
5742
validate_v31_spec_url = validate_spec_url_factory(openapi_v31_spec_validator)
5843

44+
validate_spec = validate_spec_factory(openapi_spec_validator_proxy)
45+
validate_spec_url = validate_spec_url_factory(openapi_spec_validator_proxy)
46+
5947
# aliases to the latest v3 version
6048
validate_v3_spec = validate_v31_spec
6149
validate_v3_spec_url = validate_v31_spec_url

openapi_spec_validator/__main__.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
from argparse import ArgumentParser
21
import logging
32
import sys
3+
from argparse import ArgumentParser
44
from typing import Optional
55
from typing import Sequence
66

7-
from jsonschema.exceptions import best_match
87
from jsonschema.exceptions import ValidationError
8+
from jsonschema.exceptions import best_match
99

10-
from openapi_spec_validator import openapi_v2_spec_validator
11-
from openapi_spec_validator import openapi_v30_spec_validator
12-
from openapi_spec_validator import openapi_v31_spec_validator
1310
from openapi_spec_validator.readers import read_from_filename
1411
from openapi_spec_validator.readers import read_from_stdin
12+
from openapi_spec_validator.validation import openapi_spec_validator_proxy
13+
from openapi_spec_validator.validation import openapi_v2_spec_validator
14+
from openapi_spec_validator.validation import openapi_v30_spec_validator
15+
from openapi_spec_validator.validation import openapi_v31_spec_validator
1516

1617
logger = logging.getLogger(__name__)
1718
logging.basicConfig(
@@ -20,7 +21,9 @@
2021
)
2122

2223

23-
def print_validationerror(exc: ValidationError, errors: str = "best-match") -> None:
24+
def print_validationerror(
25+
exc: ValidationError, errors: str = "best-match"
26+
) -> None:
2427
print("# Validation Error\n")
2528
print(exc)
2629
if exc.cause:
@@ -53,10 +56,10 @@ def main(args: Optional[Sequence[str]] = None) -> None:
5356
)
5457
parser.add_argument(
5558
"--schema",
56-
help="OpenAPI schema (default: 3.1.0)",
59+
help="OpenAPI schema (default: detect)",
5760
type=str,
58-
choices=["2.0", "3.0.0", "3.1.0"],
59-
default="3.1.0",
61+
choices=["2.0", "3.0.0", "3.1.0", "detect"],
62+
default="detect",
6063
)
6164
args_parsed = parser.parse_args(args)
6265

@@ -77,6 +80,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:
7780
"2.0": openapi_v2_spec_validator,
7881
"3.0.0": openapi_v30_spec_validator,
7982
"3.1.0": openapi_v31_spec_validator,
83+
"detect": openapi_spec_validator_proxy,
8084
}
8185
validator = validators[args_parsed.schema]
8286

openapi_spec_validator/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
class OpenAPISpecValidatorError(Exception):
22
pass
3-
4-
5-
class ValidatorDetectError(OpenAPISpecValidatorError):
6-
pass

openapi_spec_validator/readers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import sys
12
from os import path
23
from pathlib import Path
3-
import sys
44
from typing import Any
55
from typing import Hashable
66
from typing import Mapping

openapi_spec_validator/shortcuts.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,24 @@
33
from typing import Callable
44
from typing import Hashable
55
from typing import Mapping
6-
from typing import Tuple
76

87
from jsonschema_spec.handlers import all_urls_handler
98

10-
from openapi_spec_validator.exceptions import ValidatorDetectError
11-
from openapi_spec_validator.validation.validators import SpecValidator
9+
from openapi_spec_validator.validation.protocols import SupportsValidation
1210

1311

14-
def detect_validator(choices: Mapping[Tuple[str, str], SpecValidator], spec: Mapping[Hashable, Any]) -> SpecValidator:
15-
for (key, value), validator in choices.items():
16-
if key in spec and spec[key].startswith(value):
17-
return validator
18-
raise ValidatorDetectError("Spec schema version not detected")
19-
20-
21-
def validate_spec_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[Mapping[Hashable, Any], str], None]:
22-
def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None:
23-
validator = detect_validator(choices, spec)
24-
return validator.validate(spec, spec_url=spec_url)
25-
26-
return validate
27-
28-
29-
def validate_spec_factory(validator: SpecValidator) -> Callable[[Mapping[Hashable, Any], str], None]:
12+
def validate_spec_factory(
13+
validator: SupportsValidation,
14+
) -> Callable[[Mapping[Hashable, Any], str], None]:
3015
def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None:
3116
return validator.validate(spec, spec_url=spec_url)
3217

3318
return validate
3419

3520

36-
def validate_spec_url_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[str], None]:
37-
def validate(spec_url: str) -> None:
38-
spec = all_urls_handler(spec_url)
39-
validator = detect_validator(choices, spec)
40-
return validator.validate(spec, spec_url=spec_url)
41-
42-
return validate
43-
44-
45-
def validate_spec_url_factory(validator: SpecValidator) -> Callable[[str], None]:
21+
def validate_spec_url_factory(
22+
validator: SupportsValidation,
23+
) -> Callable[[str], None]:
4624
def validate(spec_url: str) -> None:
4725
spec = all_urls_handler(spec_url)
4826
return validator.validate(spec, spec_url=spec_url)

openapi_spec_validator/validation/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
from openapi_spec_validator.schemas import schema_v2
1313
from openapi_spec_validator.schemas import schema_v30
1414
from openapi_spec_validator.schemas import schema_v31
15+
from openapi_spec_validator.validation.proxies import DetectValidatorProxy
1516
from openapi_spec_validator.validation.validators import SpecValidator
1617

1718
__all__ = [
1819
"openapi_v2_spec_validator",
1920
"openapi_v3_spec_validator",
2021
"openapi_v30_spec_validator",
2122
"openapi_v31_spec_validator",
23+
"openapi_spec_validator_proxy",
2224
]
2325

2426
# v2.0 spec
@@ -59,3 +61,12 @@
5961

6062
# alias to the latest v3 version
6163
openapi_v3_spec_validator = openapi_v31_spec_validator
64+
65+
# detect version spec
66+
openapi_spec_validator_proxy = DetectValidatorProxy(
67+
{
68+
("swagger", "2.0"): openapi_v2_spec_validator,
69+
("openapi", "3.0"): openapi_v30_spec_validator,
70+
("openapi", "3.1"): openapi_v31_spec_validator,
71+
},
72+
)

openapi_spec_validator/validation/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from jsonschema.exceptions import ValidationError
22

3+
from openapi_spec_validator.exceptions import OpenAPISpecValidatorError
4+
5+
6+
class ValidatorDetectError(OpenAPISpecValidatorError):
7+
pass
8+
39

410
class OpenAPIValidationError(ValidationError): # type: ignore
511
pass
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import TYPE_CHECKING
2+
from typing import Any
3+
from typing import Hashable
4+
from typing import Iterator
5+
from typing import Mapping
6+
7+
if TYPE_CHECKING:
8+
from typing_extensions import Protocol
9+
from typing_extensions import runtime_checkable
10+
else:
11+
try:
12+
from typing import Protocol
13+
from typing import runtime_checkable
14+
except ImportError:
15+
from typing_extensions import Protocol
16+
from typing_extensions import runtime_checkable
17+
18+
from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
19+
20+
21+
@runtime_checkable
22+
class SupportsValidation(Protocol):
23+
def is_valid(self, instance: Mapping[Hashable, Any]) -> bool:
24+
...
25+
26+
def iter_errors(
27+
self, instance: Mapping[Hashable, Any], spec_url: str = ""
28+
) -> Iterator[OpenAPIValidationError]:
29+
...
30+
31+
def validate(
32+
self, instance: Mapping[Hashable, Any], spec_url: str = ""
33+
) -> None:
34+
...
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""OpenAPI spec validator validation proxies module."""
2+
from typing import Any
3+
from typing import Hashable
4+
from typing import Iterator
5+
from typing import Mapping
6+
from typing import Tuple
7+
8+
from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
9+
from openapi_spec_validator.validation.exceptions import ValidatorDetectError
10+
from openapi_spec_validator.validation.validators import SpecValidator
11+
12+
13+
class DetectValidatorProxy:
14+
15+
def __init__(self, choices: Mapping[Tuple[str, str], SpecValidator]):
16+
self.choices = choices
17+
18+
def detect(self, instance: Mapping[Hashable, Any]) -> SpecValidator:
19+
for (key, value), validator in self.choices.items():
20+
if key in instance and instance[key].startswith(value):
21+
return validator
22+
raise ValidatorDetectError("Spec schema version not detected")
23+
24+
def validate(
25+
self, instance: Mapping[Hashable, Any], spec_url: str = ""
26+
) -> None:
27+
validator = self.detect(instance)
28+
for err in validator.iter_errors(instance, spec_url=spec_url):
29+
raise err
30+
31+
def is_valid(self, instance: Mapping[Hashable, Any]) -> bool:
32+
validator = self.detect(instance)
33+
error = next(validator.iter_errors(instance), None)
34+
return error is None
35+
36+
def iter_errors(
37+
self, instance: Mapping[Hashable, Any], spec_url: str = ""
38+
) -> Iterator[OpenAPIValidationError]:
39+
validator = self.detect(instance)
40+
yield from validator.iter_errors(instance, spec_url=spec_url)

0 commit comments

Comments
 (0)