diff --git a/CHANGELOG.md b/CHANGELOG.md index f27b54e7e..00a64107a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 0.8.0 - Unreleased ### Additions @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `setup` will generate a pyproject.toml with no Poetry information, and instead create a `setup.py` with the project info. - `none` will not create a project folder at all, only the inner package folder (which won't be inner anymore) +- Attempt to detect and alert users if they are using an unsupported version of OpenAPI (#281). ### Changes diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 3053ee305..d4ab60244 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -260,12 +260,22 @@ class GeneratorData: enums: Dict[str, EnumProperty] @staticmethod - def from_dict(d: Dict[str, Dict[str, Any]]) -> Union["GeneratorData", GeneratorError]: + def from_dict(d: Dict[str, Any]) -> Union["GeneratorData", GeneratorError]: """ Create an OpenAPI from dict """ try: openapi = oai.OpenAPI.parse_obj(d) except ValidationError as e: - return GeneratorError(header="Failed to parse OpenAPI document", detail=str(e)) + detail = str(e) + if "swagger" in d: + detail = ( + "You may be trying to use a Swagger document; this is not supported by this project.\n\n" + detail + ) + return GeneratorError(header="Failed to parse OpenAPI document", detail=detail) + if openapi.openapi.major != 3: + return GeneratorError( + header="openapi-python-client only supports OpenAPI 3.x", + detail=f"The version of the provided document was {openapi.openapi}", + ) if openapi.components is None or openapi.components.schemas is None: schemas = Schemas() else: diff --git a/openapi_python_client/schema/__init__.py b/openapi_python_client/schema/__init__.py index 9edb7d3d9..39ba1f456 100644 --- a/openapi_python_client/schema/__init__.py +++ b/openapi_python_client/schema/__init__.py @@ -1,69 +1,50 @@ -""" -OpenAPI v3.0.3 schema types, created according to the specification: -https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md - -The type orders are according to the contents of the specification: -https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#table-of-contents -""" - __all__ = [ - "Components", - "Contact", - "Discriminator", - "Encoding", - "Example", - "ExternalDocumentation", - "Header", - "Info", - "License", - "Link", "MediaType", - "OAuthFlow", - "OAuthFlows", "OpenAPI", "Operation", "Parameter", "PathItem", - "Paths", "Reference", "RequestBody", "Response", "Responses", "Schema", - "SecurityRequirement", - "SecurityScheme", - "Server", - "ServerVariable", - "Tag", - "XML", ] -from .components import Components -from .contact import Contact -from .discriminator import Discriminator -from .encoding import Encoding -from .example import Example -from .external_documentation import ExternalDocumentation -from .header import Header -from .info import Info -from .license import License -from .link import Link -from .media_type import MediaType -from .oauth_flow import OAuthFlow -from .oauth_flows import OAuthFlows -from .open_api import OpenAPI -from .operation import Operation -from .parameter import Parameter -from .path_item import PathItem -from .paths import Paths -from .reference import Reference -from .request_body import RequestBody -from .response import Response -from .responses import Responses -from .schema import Schema -from .security_requirement import SecurityRequirement -from .security_scheme import SecurityScheme -from .server import Server -from .server_variable import ServerVariable -from .tag import Tag -from .xml import XML + +import re +from typing import Callable, Iterator + +from .openapi_schema_pydantic import MediaType +from .openapi_schema_pydantic import OpenAPI as _OpenAPI +from .openapi_schema_pydantic import Operation, Parameter, PathItem, Reference, RequestBody, Response, Responses, Schema + +regex = re.compile(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)") + + +class SemVer: + def __init__(self, str_value: str) -> None: + self.str_value = str_value + if not isinstance(str_value, str): + raise TypeError("string required") + m = regex.fullmatch(str_value) + if not m: + raise ValueError("invalid semantic versioning format") + self.major = int(m.group(1)) + self.minor = int(m.group(2)) + self.patch = int(m.group(3)) + + @classmethod + def __get_validators__(cls) -> Iterator[Callable[[str], "SemVer"]]: + yield cls.validate + + @classmethod + def validate(cls, v: str) -> "SemVer": + return cls(v) + + def __str__(self) -> str: + return self.str_value + + +class OpenAPI(_OpenAPI): + openapi: SemVer diff --git a/openapi_python_client/schema/LICENSE b/openapi_python_client/schema/openapi_schema_pydantic/LICENSE similarity index 100% rename from openapi_python_client/schema/LICENSE rename to openapi_python_client/schema/openapi_schema_pydantic/LICENSE diff --git a/openapi_python_client/schema/README.md b/openapi_python_client/schema/openapi_schema_pydantic/README.md similarity index 86% rename from openapi_python_client/schema/README.md rename to openapi_python_client/schema/openapi_schema_pydantic/README.md index d5fae02d5..0e4d40146 100644 --- a/openapi_python_client/schema/README.md +++ b/openapi_python_client/schema/openapi_schema_pydantic/README.md @@ -1,4 +1,4 @@ -# OpenAPI v3.0.3 schema classes +Everything in this directory (including the rest of this file after this paragraph) is a vendored copy of [openapi-schem-pydantic](https://github.com/kuimono/openapi-schema-pydantic) and is licensed under the LICENSE file in this directory. ## Alias diff --git a/openapi_python_client/schema/openapi_schema_pydantic/__init__.py b/openapi_python_client/schema/openapi_schema_pydantic/__init__.py new file mode 100644 index 000000000..9edb7d3d9 --- /dev/null +++ b/openapi_python_client/schema/openapi_schema_pydantic/__init__.py @@ -0,0 +1,69 @@ +""" +OpenAPI v3.0.3 schema types, created according to the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md + +The type orders are according to the contents of the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#table-of-contents +""" + +__all__ = [ + "Components", + "Contact", + "Discriminator", + "Encoding", + "Example", + "ExternalDocumentation", + "Header", + "Info", + "License", + "Link", + "MediaType", + "OAuthFlow", + "OAuthFlows", + "OpenAPI", + "Operation", + "Parameter", + "PathItem", + "Paths", + "Reference", + "RequestBody", + "Response", + "Responses", + "Schema", + "SecurityRequirement", + "SecurityScheme", + "Server", + "ServerVariable", + "Tag", + "XML", +] + +from .components import Components +from .contact import Contact +from .discriminator import Discriminator +from .encoding import Encoding +from .example import Example +from .external_documentation import ExternalDocumentation +from .header import Header +from .info import Info +from .license import License +from .link import Link +from .media_type import MediaType +from .oauth_flow import OAuthFlow +from .oauth_flows import OAuthFlows +from .open_api import OpenAPI +from .operation import Operation +from .parameter import Parameter +from .path_item import PathItem +from .paths import Paths +from .reference import Reference +from .request_body import RequestBody +from .response import Response +from .responses import Responses +from .schema import Schema +from .security_requirement import SecurityRequirement +from .security_scheme import SecurityScheme +from .server import Server +from .server_variable import ServerVariable +from .tag import Tag +from .xml import XML diff --git a/openapi_python_client/schema/components.py b/openapi_python_client/schema/openapi_schema_pydantic/components.py similarity index 100% rename from openapi_python_client/schema/components.py rename to openapi_python_client/schema/openapi_schema_pydantic/components.py diff --git a/openapi_python_client/schema/contact.py b/openapi_python_client/schema/openapi_schema_pydantic/contact.py similarity index 100% rename from openapi_python_client/schema/contact.py rename to openapi_python_client/schema/openapi_schema_pydantic/contact.py diff --git a/openapi_python_client/schema/discriminator.py b/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py similarity index 100% rename from openapi_python_client/schema/discriminator.py rename to openapi_python_client/schema/openapi_schema_pydantic/discriminator.py diff --git a/openapi_python_client/schema/encoding.py b/openapi_python_client/schema/openapi_schema_pydantic/encoding.py similarity index 100% rename from openapi_python_client/schema/encoding.py rename to openapi_python_client/schema/openapi_schema_pydantic/encoding.py diff --git a/openapi_python_client/schema/example.py b/openapi_python_client/schema/openapi_schema_pydantic/example.py similarity index 100% rename from openapi_python_client/schema/example.py rename to openapi_python_client/schema/openapi_schema_pydantic/example.py diff --git a/openapi_python_client/schema/external_documentation.py b/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py similarity index 100% rename from openapi_python_client/schema/external_documentation.py rename to openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py diff --git a/openapi_python_client/schema/header.py b/openapi_python_client/schema/openapi_schema_pydantic/header.py similarity index 100% rename from openapi_python_client/schema/header.py rename to openapi_python_client/schema/openapi_schema_pydantic/header.py diff --git a/openapi_python_client/schema/info.py b/openapi_python_client/schema/openapi_schema_pydantic/info.py similarity index 100% rename from openapi_python_client/schema/info.py rename to openapi_python_client/schema/openapi_schema_pydantic/info.py diff --git a/openapi_python_client/schema/license.py b/openapi_python_client/schema/openapi_schema_pydantic/license.py similarity index 100% rename from openapi_python_client/schema/license.py rename to openapi_python_client/schema/openapi_schema_pydantic/license.py diff --git a/openapi_python_client/schema/link.py b/openapi_python_client/schema/openapi_schema_pydantic/link.py similarity index 100% rename from openapi_python_client/schema/link.py rename to openapi_python_client/schema/openapi_schema_pydantic/link.py diff --git a/openapi_python_client/schema/media_type.py b/openapi_python_client/schema/openapi_schema_pydantic/media_type.py similarity index 100% rename from openapi_python_client/schema/media_type.py rename to openapi_python_client/schema/openapi_schema_pydantic/media_type.py diff --git a/openapi_python_client/schema/oauth_flow.py b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py similarity index 100% rename from openapi_python_client/schema/oauth_flow.py rename to openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py diff --git a/openapi_python_client/schema/oauth_flows.py b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py similarity index 100% rename from openapi_python_client/schema/oauth_flows.py rename to openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py diff --git a/openapi_python_client/schema/open_api.py b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py similarity index 83% rename from openapi_python_client/schema/open_api.py rename to openapi_python_client/schema/openapi_schema_pydantic/open_api.py index abec6cc01..dd480a8f5 100644 --- a/openapi_python_client/schema/open_api.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py @@ -14,14 +14,6 @@ class OpenAPI(BaseModel): """This is the root document object of the OpenAPI document.""" - openapi: str = "3.0.3" - """ - **REQUIRED**. This string MUST be the [semantic version number](https://semver.org/spec/v2.0.0.html) - of the [OpenAPI Specification version](#versions) that the OpenAPI document uses. - The `openapi` field SHOULD be used by tooling specifications and clients to interpret the OpenAPI document. - This is *not* related to the API [`info.version`](#infoVersion) string. - """ - info: Info """ **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling as required. diff --git a/openapi_python_client/schema/operation.py b/openapi_python_client/schema/openapi_schema_pydantic/operation.py similarity index 100% rename from openapi_python_client/schema/operation.py rename to openapi_python_client/schema/openapi_schema_pydantic/operation.py diff --git a/openapi_python_client/schema/parameter.py b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py similarity index 100% rename from openapi_python_client/schema/parameter.py rename to openapi_python_client/schema/openapi_schema_pydantic/parameter.py diff --git a/openapi_python_client/schema/path_item.py b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py similarity index 100% rename from openapi_python_client/schema/path_item.py rename to openapi_python_client/schema/openapi_schema_pydantic/path_item.py diff --git a/openapi_python_client/schema/paths.py b/openapi_python_client/schema/openapi_schema_pydantic/paths.py similarity index 100% rename from openapi_python_client/schema/paths.py rename to openapi_python_client/schema/openapi_schema_pydantic/paths.py diff --git a/openapi_python_client/schema/reference.py b/openapi_python_client/schema/openapi_schema_pydantic/reference.py similarity index 100% rename from openapi_python_client/schema/reference.py rename to openapi_python_client/schema/openapi_schema_pydantic/reference.py diff --git a/openapi_python_client/schema/request_body.py b/openapi_python_client/schema/openapi_schema_pydantic/request_body.py similarity index 100% rename from openapi_python_client/schema/request_body.py rename to openapi_python_client/schema/openapi_schema_pydantic/request_body.py diff --git a/openapi_python_client/schema/response.py b/openapi_python_client/schema/openapi_schema_pydantic/response.py similarity index 100% rename from openapi_python_client/schema/response.py rename to openapi_python_client/schema/openapi_schema_pydantic/response.py diff --git a/openapi_python_client/schema/responses.py b/openapi_python_client/schema/openapi_schema_pydantic/responses.py similarity index 100% rename from openapi_python_client/schema/responses.py rename to openapi_python_client/schema/openapi_schema_pydantic/responses.py diff --git a/openapi_python_client/schema/schema.py b/openapi_python_client/schema/openapi_schema_pydantic/schema.py similarity index 100% rename from openapi_python_client/schema/schema.py rename to openapi_python_client/schema/openapi_schema_pydantic/schema.py diff --git a/openapi_python_client/schema/security_requirement.py b/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py similarity index 100% rename from openapi_python_client/schema/security_requirement.py rename to openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py diff --git a/openapi_python_client/schema/security_scheme.py b/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py similarity index 100% rename from openapi_python_client/schema/security_scheme.py rename to openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py diff --git a/openapi_python_client/schema/server.py b/openapi_python_client/schema/openapi_schema_pydantic/server.py similarity index 100% rename from openapi_python_client/schema/server.py rename to openapi_python_client/schema/openapi_schema_pydantic/server.py diff --git a/openapi_python_client/schema/server_variable.py b/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py similarity index 100% rename from openapi_python_client/schema/server_variable.py rename to openapi_python_client/schema/openapi_schema_pydantic/server_variable.py diff --git a/openapi_python_client/schema/tag.py b/openapi_python_client/schema/openapi_schema_pydantic/tag.py similarity index 100% rename from openapi_python_client/schema/tag.py rename to openapi_python_client/schema/openapi_schema_pydantic/tag.py diff --git a/openapi_python_client/schema/xml.py b/openapi_python_client/schema/openapi_schema_pydantic/xml.py similarity index 100% rename from openapi_python_client/schema/xml.py rename to openapi_python_client/schema/openapi_schema_pydantic/xml.py diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index e3caafa9e..5c2caa3cc 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -14,6 +14,7 @@ def test_from_dict(self, mocker): EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas) OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") openapi = OpenAPI.parse_obj.return_value + openapi.openapi = mocker.MagicMock(major=3) in_dict = mocker.MagicMock() @@ -54,16 +55,63 @@ def test_from_dict_invalid_schema(self, mocker): assert generator_data == GeneratorError( header="Failed to parse OpenAPI document", detail=( - "2 validation errors for OpenAPI\n" + "3 validation errors for OpenAPI\n" "info\n" " field required (type=value_error.missing)\n" "paths\n" + " field required (type=value_error.missing)\n" + "openapi\n" " field required (type=value_error.missing)" ), ) Schemas.build.assert_not_called() Schemas.assert_not_called() + def test_swagger_document_invalid_schema(self, mocker): + Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") + + in_dict = {"swagger": "2.0"} + + from openapi_python_client.parser.openapi import GeneratorData + + generator_data = GeneratorData.from_dict(in_dict) + + assert generator_data == GeneratorError( + header="Failed to parse OpenAPI document", + detail=( + "You may be trying to use a Swagger document; this is not supported by this project.\n\n" + "3 validation errors for OpenAPI\n" + "info\n" + " field required (type=value_error.missing)\n" + "paths\n" + " field required (type=value_error.missing)\n" + "openapi\n" + " field required (type=value_error.missing)" + ), + ) + Schemas.build.assert_not_called() + Schemas.assert_not_called() + + def test_from_dict_invalid_version(self, mocker): + Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") + + OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") + openapi = OpenAPI.parse_obj.return_value + openapi.openapi = oai.SemVer("2.1.3") + + in_dict = mocker.MagicMock() + + from openapi_python_client.parser.openapi import GeneratorData + + generator_data = GeneratorData.from_dict(in_dict) + + assert generator_data == GeneratorError( + header="openapi-python-client only supports OpenAPI 3.x", + detail="The version of the provided document was 2.1.3", + ) + Schemas.build.assert_not_called() + Schemas.assert_not_called() + class TestEndpoint: def test_parse_request_form_body(self, mocker): diff --git a/tests/test_schema/test_open_api.py b/tests/test_schema/test_open_api.py new file mode 100644 index 000000000..e332e4fca --- /dev/null +++ b/tests/test_schema/test_open_api.py @@ -0,0 +1,16 @@ +import pytest +from pydantic import ValidationError + +from openapi_python_client.schema import OpenAPI + + +@pytest.mark.parametrize( + "version, valid", [("abc", False), ("1", False), ("2.0", False), ("3.0.0", True), ("3.1.0-b.3", False), (1, False)] +) +def test_validate_version(version, valid): + data = {"openapi": version, "info": {"title": "test", "version": ""}, "paths": {}} + if valid: + OpenAPI.parse_obj(data) + else: + with pytest.raises(ValidationError): + OpenAPI.parse_obj(data)