Skip to content
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

CLI detect spec version #171

Merged
merged 1 commit into from
Sep 2, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ or more pythonic way:
Examples
********

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

.. code:: python

Expand All @@ -92,9 +92,9 @@ By default, OpenAPI v3.1 syntax is expected. To validate an OpenAPI v3.1 spec:

In order to explicitly validate a:

* Swagger / OpenAPI 2.0 spec file, import ``validate_v2_spec``
* OpenAPI 3.0 spec file, import ``validate_v30_spec``
* OpenAPI 3.1 spec file, import ``validate_v31_spec``
* Swagger / OpenAPI 2.0 spec, import ``validate_v2_spec``
* OpenAPI 3.0 spec, import ``validate_v30_spec``
* OpenAPI 3.1 spec, import ``validate_v31_spec``

instead of ``validate_spec``.

Expand All @@ -117,9 +117,9 @@ You can also validate spec from url:

In order to explicitly validate a:

* Swagger / OpenAPI 2.0 spec file, import ``validate_v2_spec_url``
* OpenAPI 3.0 spec file, import ``validate_v30_spec_url``
* OpenAPI 3.1 spec file, import ``validate_v31_spec_url``
* Swagger / OpenAPI 2.0 spec url, import ``validate_v2_spec_url``
* OpenAPI 3.0 spec url, import ``validate_v30_spec_url``
* OpenAPI 3.1 spec url, import ``validate_v31_spec_url``

instead of ``validate_spec_url``.

Expand Down
20 changes: 4 additions & 16 deletions openapi_spec_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from jsonschema_spec.handlers import default_handlers

from openapi_spec_validator.shortcuts import validate_spec_detect_factory
from openapi_spec_validator.shortcuts import validate_spec_factory
from openapi_spec_validator.shortcuts import validate_spec_url_detect_factory
from openapi_spec_validator.shortcuts import validate_spec_url_factory
from openapi_spec_validator.validation import openapi_spec_validator_proxy
from openapi_spec_validator.validation import openapi_v2_spec_validator
from openapi_spec_validator.validation import openapi_v3_spec_validator
from openapi_spec_validator.validation import openapi_v30_spec_validator
Expand Down Expand Up @@ -33,20 +32,6 @@
]

# shortcuts
validate_spec = validate_spec_detect_factory(
{
("swagger", "2.0"): openapi_v2_spec_validator,
("openapi", "3.0"): openapi_v30_spec_validator,
("openapi", "3.1"): openapi_v31_spec_validator,
},
)
validate_spec_url = validate_spec_url_detect_factory(
{
("swagger", "2.0"): openapi_v2_spec_validator,
("openapi", "3.0"): openapi_v30_spec_validator,
("openapi", "3.1"): openapi_v31_spec_validator,
},
)
validate_v2_spec = validate_spec_factory(openapi_v2_spec_validator)
validate_v2_spec_url = validate_spec_url_factory(openapi_v2_spec_validator)

Expand All @@ -56,6 +41,9 @@
validate_v31_spec = validate_spec_factory(openapi_v31_spec_validator)
validate_v31_spec_url = validate_spec_url_factory(openapi_v31_spec_validator)

validate_spec = validate_spec_factory(openapi_spec_validator_proxy)
validate_spec_url = validate_spec_url_factory(openapi_spec_validator_proxy)

# aliases to the latest v3 version
validate_v3_spec = validate_v31_spec
validate_v3_spec_url = validate_v31_spec_url
22 changes: 13 additions & 9 deletions openapi_spec_validator/__main__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from argparse import ArgumentParser
import logging
import sys
from argparse import ArgumentParser
from typing import Optional
from typing import Sequence

from jsonschema.exceptions import best_match
from jsonschema.exceptions import ValidationError
from jsonschema.exceptions import best_match

from openapi_spec_validator import openapi_v2_spec_validator
from openapi_spec_validator import openapi_v30_spec_validator
from openapi_spec_validator import openapi_v31_spec_validator
from openapi_spec_validator.readers import read_from_filename
from openapi_spec_validator.readers import read_from_stdin
from openapi_spec_validator.validation import openapi_spec_validator_proxy
from openapi_spec_validator.validation import openapi_v2_spec_validator
from openapi_spec_validator.validation import openapi_v30_spec_validator
from openapi_spec_validator.validation import openapi_v31_spec_validator

logger = logging.getLogger(__name__)
logging.basicConfig(
Expand All @@ -20,7 +21,9 @@
)


def print_validationerror(exc: ValidationError, errors: str = "best-match") -> None:
def print_validationerror(
exc: ValidationError, errors: str = "best-match"
) -> None:
print("# Validation Error\n")
print(exc)
if exc.cause:
Expand Down Expand Up @@ -53,10 +56,10 @@ def main(args: Optional[Sequence[str]] = None) -> None:
)
parser.add_argument(
"--schema",
help="OpenAPI schema (default: 3.1.0)",
help="OpenAPI schema (default: detect)",
type=str,
choices=["2.0", "3.0.0", "3.1.0"],
default="3.1.0",
choices=["2.0", "3.0.0", "3.1.0", "detect"],
default="detect",
)
args_parsed = parser.parse_args(args)

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

Expand Down
4 changes: 0 additions & 4 deletions openapi_spec_validator/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
class OpenAPISpecValidatorError(Exception):
pass


class ValidatorDetectError(OpenAPISpecValidatorError):
pass
2 changes: 1 addition & 1 deletion openapi_spec_validator/readers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
from os import path
from pathlib import Path
import sys
from typing import Any
from typing import Hashable
from typing import Mapping
Expand Down
36 changes: 7 additions & 29 deletions openapi_spec_validator/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,24 @@
from typing import Callable
from typing import Hashable
from typing import Mapping
from typing import Tuple

from jsonschema_spec.handlers import all_urls_handler

from openapi_spec_validator.exceptions import ValidatorDetectError
from openapi_spec_validator.validation.validators import SpecValidator
from openapi_spec_validator.validation.protocols import SupportsValidation


def detect_validator(choices: Mapping[Tuple[str, str], SpecValidator], spec: Mapping[Hashable, Any]) -> SpecValidator:
for (key, value), validator in choices.items():
if key in spec and spec[key].startswith(value):
return validator
raise ValidatorDetectError("Spec schema version not detected")


def validate_spec_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[Mapping[Hashable, Any], str], None]:
def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None:
validator = detect_validator(choices, spec)
return validator.validate(spec, spec_url=spec_url)

return validate


def validate_spec_factory(validator: SpecValidator) -> Callable[[Mapping[Hashable, Any], str], None]:
def validate_spec_factory(
validator: SupportsValidation,
) -> Callable[[Mapping[Hashable, Any], str], None]:
def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None:
return validator.validate(spec, spec_url=spec_url)

return validate


def validate_spec_url_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[str], None]:
def validate(spec_url: str) -> None:
spec = all_urls_handler(spec_url)
validator = detect_validator(choices, spec)
return validator.validate(spec, spec_url=spec_url)

return validate


def validate_spec_url_factory(validator: SpecValidator) -> Callable[[str], None]:
def validate_spec_url_factory(
validator: SupportsValidation,
) -> Callable[[str], None]:
def validate(spec_url: str) -> None:
spec = all_urls_handler(spec_url)
return validator.validate(spec, spec_url=spec_url)
Expand Down
11 changes: 11 additions & 0 deletions openapi_spec_validator/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
from openapi_spec_validator.schemas import schema_v2
from openapi_spec_validator.schemas import schema_v30
from openapi_spec_validator.schemas import schema_v31
from openapi_spec_validator.validation.proxies import DetectValidatorProxy
from openapi_spec_validator.validation.validators import SpecValidator

__all__ = [
"openapi_v2_spec_validator",
"openapi_v3_spec_validator",
"openapi_v30_spec_validator",
"openapi_v31_spec_validator",
"openapi_spec_validator_proxy",
]

# v2.0 spec
Expand Down Expand Up @@ -59,3 +61,12 @@

# alias to the latest v3 version
openapi_v3_spec_validator = openapi_v31_spec_validator

# detect version spec
openapi_spec_validator_proxy = DetectValidatorProxy(
{
("swagger", "2.0"): openapi_v2_spec_validator,
("openapi", "3.0"): openapi_v30_spec_validator,
("openapi", "3.1"): openapi_v31_spec_validator,
},
)
6 changes: 6 additions & 0 deletions openapi_spec_validator/validation/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from jsonschema.exceptions import ValidationError

from openapi_spec_validator.exceptions import OpenAPISpecValidatorError


class ValidatorDetectError(OpenAPISpecValidatorError):
pass


class OpenAPIValidationError(ValidationError): # type: ignore
pass
Expand Down
34 changes: 34 additions & 0 deletions openapi_spec_validator/validation/protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING
from typing import Any
from typing import Hashable
from typing import Iterator
from typing import Mapping

if TYPE_CHECKING:
from typing_extensions import Protocol
from typing_extensions import runtime_checkable
else:
try:
from typing import Protocol
from typing import runtime_checkable
except ImportError:
from typing_extensions import Protocol
from typing_extensions import runtime_checkable

from openapi_spec_validator.validation.exceptions import OpenAPIValidationError


@runtime_checkable
class SupportsValidation(Protocol):
def is_valid(self, instance: Mapping[Hashable, Any]) -> bool:
...

def iter_errors(
self, instance: Mapping[Hashable, Any], spec_url: str = ""
) -> Iterator[OpenAPIValidationError]:
...

def validate(
self, instance: Mapping[Hashable, Any], spec_url: str = ""
) -> None:
...
40 changes: 40 additions & 0 deletions openapi_spec_validator/validation/proxies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""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 Tuple

from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
from openapi_spec_validator.validation.exceptions import ValidatorDetectError
from openapi_spec_validator.validation.validators import SpecValidator


class DetectValidatorProxy:

def __init__(self, choices: Mapping[Tuple[str, str], SpecValidator]):
self.choices = choices

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

def validate(
self, instance: Mapping[Hashable, Any], spec_url: str = ""
) -> None:
validator = self.detect(instance)
for err in validator.iter_errors(instance, spec_url=spec_url):
raise err

def is_valid(self, instance: Mapping[Hashable, Any]) -> bool:
validator = self.detect(instance)
error = next(validator.iter_errors(instance), None)
return error is None

def iter_errors(
self, instance: Mapping[Hashable, Any], spec_url: str = ""
) -> Iterator[OpenAPIValidationError]:
validator = self.detect(instance)
yield from validator.iter_errors(instance, spec_url=spec_url)