diff --git a/.changeset/support_request_body_refs.md b/.changeset/support_request_body_refs.md new file mode 100644 index 000000000..f21006e63 --- /dev/null +++ b/.changeset/support_request_body_refs.md @@ -0,0 +1,26 @@ +--- +default: minor +--- + +# Support request body refs + +You can now define and reuse bodies via refs, with a document like this: + +```yaml +paths: + /something: + post: + requestBody: + "$ref": "#/components/requestBodies/SharedBody" +components: + requestBodies: + SharedBody: + content: + application/json: + schema: + type: string +``` + +Thanks to @kigawas and @supermihi for initial implementations and @RockyMM for the initial request. + +Closes #633, closes #664, resolves #595. diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr new file mode 100644 index 000000000..dac0127aa --- /dev/null +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -0,0 +1,44 @@ +# serializer version: 1 +# name: test_documents_with_errors[circular-body-ref] + ''' + Generating /test-documents-with-errors + Warning(s) encountered while generating. Client was generated, but some pieces may be missing + + WARNING parsing POST / within default. Endpoint will not be generated. + + Circular $ref in request body + + + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose + + ''' +# --- +# name: test_documents_with_errors[missing-body-ref] + ''' + Generating /test-documents-with-errors + Warning(s) encountered while generating. Client was generated, but some pieces may be missing + + WARNING parsing POST / within default. Endpoint will not be generated. + + Could not resolve $ref #/components/requestBodies/body in request body + + + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose + + ''' +# --- +# name: test_documents_with_errors[optional-path-param] + ''' + Generating /test-documents-with-errors + Warning(s) encountered while generating. Client was generated, but some pieces may be missing + + WARNING parsing GET /{optional} within default. Endpoint will not be generated. + + Path parameter must be required + + Parameter(name='optional', param_in=, description=None, required=False, deprecated=False, allowEmptyValue=False, style=None, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None) + + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose + + ''' +# --- diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 7f6acd095..f34f27366 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -87,6 +87,23 @@ } } }, + "/bodies/refs": { + "post": { + "tags": [ + "bodies" + ], + "description": "Test request body defined via ref", + "operationId": "refs", + "requestBody": { + "$ref": "#/components/requestBodies/NestedRef" + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -2761,6 +2778,20 @@ "type": "string" } } + }, + "requestBodies": { + "NestedRef": { + "$ref": "#/components/requestBodies/ARequestBody" + }, + "ARequestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AModel" + } + } + } + } } } } diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 53389e525..13d267b77 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -83,6 +83,23 @@ info: } } }, + "/bodies/refs": { + "post": { + "tags": [ + "bodies" + ], + "description": "Test request body defined via ref", + "operationId": "refs", + "requestBody": { + "$ref": "#/components/requestBodies/NestedRef" + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -1604,7 +1621,7 @@ info: } } } -"components": { +"components": "schemas": { "AFormData": { "type": "object", @@ -2704,7 +2721,7 @@ info: } } } - }, + } "parameters": { "integer-param": { "name": "integer param", @@ -2772,5 +2789,11 @@ info: } } } -} - + requestBodies: + NestedRef: + "$ref": "#/components/requestBodies/ARequestBody" + ARequestBody: + content: + "application/json": + "schema": + "$ref": "#/components/schemas/AModel" diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py index 92367f620..89304dde0 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py @@ -2,7 +2,7 @@ import types -from . import json_like, post_bodies_multiple +from . import json_like, post_bodies_multiple, refs class BodiesEndpoints: @@ -19,3 +19,10 @@ def json_like(cls) -> types.ModuleType: A content type that works like json but isn't application/json """ return json_like + + @classmethod + def refs(cls) -> types.ModuleType: + """ + Test request body defined via ref + """ + return refs diff --git a/end_to_end_tests/documents_with_errors/circular-body-ref.yaml b/end_to_end_tests/documents_with_errors/circular-body-ref.yaml new file mode 100644 index 000000000..98761a35d --- /dev/null +++ b/end_to_end_tests/documents_with_errors/circular-body-ref.yaml @@ -0,0 +1,20 @@ +openapi: "3.1.0" +info: + title: "Circular Body Ref" + version: "0.1.0" +paths: + /: + post: + requestBody: + $ref: "#/components/requestBodies/body" + responses: + "200": + description: "Successful Response" + content: + "application/json": + schema: + const: "Why have a fixed response? I dunno" +components: + requestBodies: + body: + $ref: "#/components/requestBodies/body" \ No newline at end of file diff --git a/end_to_end_tests/documents_with_errors/missing-body-ref.yaml b/end_to_end_tests/documents_with_errors/missing-body-ref.yaml new file mode 100644 index 000000000..bf02ba6b1 --- /dev/null +++ b/end_to_end_tests/documents_with_errors/missing-body-ref.yaml @@ -0,0 +1,16 @@ +openapi: "3.1.0" +info: + title: "Trying to use a request body ref that does not exist" + version: "0.1.0" +paths: + /: + post: + requestBody: + $ref: "#/components/requestBodies/body" + responses: + "200": + description: "Successful Response" + content: + "application/json": + schema: + const: "Why have a fixed response? I dunno" \ No newline at end of file diff --git a/end_to_end_tests/invalid_openapi.yaml b/end_to_end_tests/documents_with_errors/optional-path-param.yaml similarity index 100% rename from end_to_end_tests/invalid_openapi.yaml rename to end_to_end_tests/documents_with_errors/optional-path-param.yaml diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py new file mode 100644 index 000000000..8d00169e4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py @@ -0,0 +1,103 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.a_model import AModel +from ...types import Response + + +def _get_kwargs( + *, + body: AModel, +) -> Dict[str, Any]: + headers: Dict[str, Any] = {} + + _kwargs: Dict[str, Any] = { + "method": "post", + "url": "/bodies/refs", + } + + _body = body.to_dict() + + _kwargs["json"] = _body + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: + if response.status_code == HTTPStatus.OK: + return None + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: Union[AuthenticatedClient, Client], + body: AModel, +) -> Response[Any]: + """Test request body defined via ref + + Args: + body (AModel): A Model for testing all the ways custom objects can be used + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: Union[AuthenticatedClient, Client], + body: AModel, +) -> Response[Any]: + """Test request body defined via ref + + Args: + body (AModel): A Model for testing all the ways custom objects can be used + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index f101152f3..621d8ecc4 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -222,12 +222,18 @@ def test_bad_url(): assert "Could not get OpenAPI document from provided URL" in result.stdout -def test_invalid_document(): +ERROR_DOCUMENTS = [path for path in Path(__file__).parent.joinpath("documents_with_errors").iterdir() if path.is_file()] + + +@pytest.mark.parametrize("document", ERROR_DOCUMENTS, ids=[path.stem for path in ERROR_DOCUMENTS]) +def test_documents_with_errors(snapshot, document): runner = CliRunner() - path = Path(__file__).parent / "invalid_openapi.yaml" - result = runner.invoke(app, ["generate", f"--path={path}", "--fail-on-warning"]) + output_path = Path.cwd() / "test-documents-with-errors" + shutil.rmtree(output_path, ignore_errors=True) + result = runner.invoke(app, ["generate", f"--path={document}", "--fail-on-warning", f"--output-path={output_path}"]) assert result.exit_code == 1 - assert "Warning(s) encountered while generating" in result.stdout + assert result.stdout.replace(str(output_path), "/test-documents-with-errors") == snapshot + shutil.rmtree(output_path, ignore_errors=True) def test_custom_post_hooks(): diff --git a/openapi_python_client/parser/bodies.py b/openapi_python_client/parser/bodies.py index 6b8e4ad72..8c2c86f30 100644 --- a/openapi_python_client/parser/bodies.py +++ b/openapi_python_client/parser/bodies.py @@ -1,5 +1,5 @@ import sys -from typing import List, Tuple, Union +from typing import Dict, List, Tuple, Union import attr @@ -44,15 +44,19 @@ def body_from_data( *, data: oai.Operation, schemas: Schemas, + request_bodies: Dict[str, Union[oai.RequestBody, oai.Reference]], config: Config, endpoint_name: str, ) -> Tuple[List[Union[Body, ParseError]], Schemas]: """Adds form or JSON body to Endpoint if included in data""" - if data.request_body is None or isinstance(data.request_body, oai.Reference): + body = _resolve_reference(data.request_body, request_bodies) + if isinstance(body, ParseError): + return [body], schemas + if body is None: return [], schemas bodies: List[Union[Body, ParseError]] = [] - body_content = data.request_body.content + body_content = body.content prefix_type_names = len(body_content) > 1 for content_type, media_type in body_content.items(): @@ -61,7 +65,7 @@ def body_from_data( bodies.append( ParseError( detail="Invalid content type", - data=data.request_body, + data=body, level=ErrorLevel.WARNING, ) ) @@ -71,7 +75,7 @@ def body_from_data( bodies.append( ParseError( detail="Missing schema", - data=data.request_body, + data=body, level=ErrorLevel.WARNING, ) ) @@ -88,7 +92,7 @@ def body_from_data( bodies.append( ParseError( detail=f"Unsupported content type {simplified_content_type}", - data=data.request_body, + data=body, level=ErrorLevel.WARNING, ) ) @@ -123,3 +127,19 @@ def body_from_data( ) return bodies, schemas + + +def _resolve_reference( + body: Union[oai.RequestBody, oai.Reference, None], request_bodies: Dict[str, Union[oai.RequestBody, oai.Reference]] +) -> Union[oai.RequestBody, ParseError, None]: + if body is None: + return None + references_seen = [] + while isinstance(body, oai.Reference) and body.ref not in references_seen: + references_seen.append(body.ref) + body = request_bodies.get(body.ref.split("/")[-1]) + if isinstance(body, oai.Reference): + return ParseError(detail="Circular $ref in request body", data=body) + if body is None and references_seen: + return ParseError(detail=f"Could not resolve $ref {references_seen[-1]} in request body") + return body diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 015966224..0d6a7424b 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -48,6 +48,7 @@ def from_data( data: Dict[str, oai.PathItem], schemas: Schemas, parameters: Parameters, + request_bodies: Dict[str, Union[oai.RequestBody, oai.Reference]], config: Config, ) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: """Parse the openapi paths data to get EndpointCollections by tag""" @@ -69,6 +70,7 @@ def from_data( tag=tag, schemas=schemas, parameters=parameters, + request_bodies=request_bodies, config=config, ) # Add `PathItem` parameters @@ -392,6 +394,7 @@ def from_data( tag: str, schemas: Schemas, parameters: Parameters, + request_bodies: Dict[str, Union[oai.RequestBody, oai.Reference]], config: Config, ) -> Tuple[Union["Endpoint", ParseError], Schemas, Parameters]: """Construct an endpoint from the OpenAPI data""" @@ -423,7 +426,9 @@ def from_data( result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config) if isinstance(result, ParseError): return result, schemas, parameters - bodies, schemas = body_from_data(data=data, schemas=schemas, config=config, endpoint_name=result.name) + bodies, schemas = body_from_data( + data=data, schemas=schemas, config=config, endpoint_name=result.name, request_bodies=request_bodies + ) body_errors = [] for body in bodies: if isinstance(body, ParseError): @@ -507,8 +512,9 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", parameters=parameters, config=config, ) + request_bodies = (openapi.components and openapi.components.requestBodies) or {} endpoint_collections_by_tag, schemas, parameters = EndpointCollection.from_data( - data=openapi.paths, schemas=schemas, parameters=parameters, config=config + data=openapi.paths, schemas=schemas, parameters=parameters, request_bodies=request_bodies, config=config ) enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty)) diff --git a/pdm.lock b/pdm.lock index 5353a0bde..dc916ea06 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:8c81482bbbefbab7b565283f94bc1ea7bff579c3019f9957be1ad0450483ecc9" +content_hash = "sha256:44cf9eee85acb131ec5257c492b0d97fdd7261fdf8e1f0b70f771211adc49ee5" [[package]] name = "annotated-types" @@ -836,6 +836,20 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "syrupy" +version = "4.6.1" +requires_python = ">=3.8.1,<4" +summary = "Pytest Snapshot Test Utility" +groups = ["dev"] +dependencies = [ + "pytest<9.0.0,>=7.0.0", +] +files = [ + {file = "syrupy-4.6.1-py3-none-any.whl", hash = "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133"}, + {file = "syrupy-4.6.1.tar.gz", hash = "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"}, +] + [[package]] name = "taskipy" version = "1.12.2" diff --git a/pyproject.toml b/pyproject.toml index 73005bf46..6eb121a5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ authors = [ { name = "Dylan Anthony", email = "contact@dylananthony.com" }, ] license = { text = "MIT" } -requires-python = ">=3.8,<4.0" +requires-python = ">=3.8.1,<4.0" dependencies = [ "jinja2>=3.0.0,<4.0.0", "typer>0.6,<0.13", @@ -84,8 +84,6 @@ ignore_missing_imports = true [tool.pytest.ini_options] junit_family = "xunit2" - - [tool.pdm.dev-dependencies] dev = [ "pytest", @@ -98,6 +96,7 @@ dev = [ "types-certifi<2021.10.9,>=2020.0.0", "types-python-dateutil<3.0.0,>=2.0.0", "ruamel-yaml-string>=0.1.1", + "syrupy>=4", ] [tool.pdm.build] diff --git a/tests/test_parser/test_bodies.py b/tests/test_parser/test_bodies.py index 699ed00cf..0956d11f6 100644 --- a/tests/test_parser/test_bodies.py +++ b/tests/test_parser/test_bodies.py @@ -32,7 +32,9 @@ def test_errors(config): responses={}, ) - errs, _ = body_from_data(data=operation, schemas=Schemas(), config=config, endpoint_name="this will not succeed") + errs, _ = body_from_data( + data=operation, schemas=Schemas(), config=config, endpoint_name="this will not succeed", request_bodies={} + ) assert len(errs) == len(operation.request_body.content) assert all(isinstance(err, ParseError) for err in errs) diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 43a9e8c42..75eea1b47 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -14,62 +14,6 @@ class TestGeneratorData: - def test_from_dict(self, mocker, model_property_factory, enum_property_factory): - from openapi_python_client.parser.properties import Schemas - - build_schemas = mocker.patch(f"{MODULE_NAME}.build_schemas") - build_parameters = mocker.patch(f"{MODULE_NAME}.build_parameters") - EndpointCollection = mocker.patch(f"{MODULE_NAME}.EndpointCollection") - schemas = mocker.MagicMock() - schemas.classes_by_name = { - "Model": model_property_factory(), - "Enum": enum_property_factory(), - } - parameters = Parameters() - - endpoints_collections_by_tag = mocker.MagicMock() - EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas, parameters) - OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") - openapi = OpenAPI.model_validate.return_value - openapi.openapi = mocker.MagicMock(major=3) - config = mocker.MagicMock() - in_dict = mocker.MagicMock() - - from openapi_python_client.parser.openapi import GeneratorData - - generator_data = GeneratorData.from_dict(in_dict, config=config) - - OpenAPI.model_validate.assert_called_once_with(in_dict) - build_schemas.assert_called_once_with(components=openapi.components.schemas, config=config, schemas=Schemas()) - build_parameters.assert_called_once_with( - components=openapi.components.parameters, - parameters=parameters, - config=config, - ) - EndpointCollection.from_data.assert_called_once_with( - data=openapi.paths, - schemas=build_schemas.return_value, - parameters=build_parameters.return_value, - config=config, - ) - assert generator_data.title == openapi.info.title - assert generator_data.description == openapi.info.description - assert generator_data.version == openapi.info.version - assert generator_data.endpoint_collections_by_tag == endpoints_collections_by_tag - assert generator_data.errors == schemas.errors + parameters.errors - assert list(generator_data.models) == [schemas.classes_by_name["Model"]] - assert list(generator_data.enums) == [schemas.classes_by_name["Enum"]] - - # Test no components - openapi.components = None - build_schemas.reset_mock() - build_parameters.reset_mock() - - GeneratorData.from_dict(in_dict, config=config) - - build_schemas.assert_not_called() - build_parameters.assert_not_called() - def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") config = mocker.MagicMock() @@ -532,6 +476,7 @@ def test_from_data_bad_params(self, mocker, config): schemas=initial_schemas, parameters=parameters, config=config, + request_bodies={}, ) assert result == (parse_error, return_schemas, return_parameters) @@ -566,6 +511,7 @@ def test_from_data_bad_responses(self, mocker, config): schemas=initial_schemas, parameters=initial_parameters, config=config, + request_bodies={}, ) assert result == (parse_error, response_schemas, return_parameters) @@ -605,6 +551,7 @@ def test_from_data_standard(self, mocker, config): schemas=initial_schemas, parameters=initial_parameters, config=config, + request_bodies={}, ) add_parameters.assert_called_once_with( @@ -647,8 +594,15 @@ def test_from_data_no_operation_id(self, mocker, config): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) parameters = mocker.MagicMock() - endpoint, return_schemas, return_params = Endpoint.from_data( - data=data, path=path, method=method, tag="default", schemas=schemas, parameters=parameters, config=config + endpoint, _, return_params = Endpoint.from_data( + data=data, + path=path, + method=method, + tag="default", + schemas=schemas, + parameters=parameters, + config=config, + request_bodies={}, ) add_parameters.assert_called_once_with( @@ -695,7 +649,14 @@ def test_from_data_no_security(self, mocker, config): parameters = mocker.MagicMock() Endpoint.from_data( - data=data, path=path, method=method, tag="a", schemas=schemas, parameters=parameters, config=config + data=data, + path=path, + method=method, + tag="a", + schemas=schemas, + parameters=parameters, + config=config, + request_bodies={}, ) add_parameters.assert_called_once_with( @@ -737,6 +698,7 @@ def test_from_data_some_bad_bodies(self, config): tag="tag", path="/", method="get", + request_bodies={}, ) assert isinstance(endpoint, Endpoint) @@ -759,6 +721,7 @@ def test_from_data_all_bodies_bad(self, config): tag="tag", path="/", method="get", + request_bodies={}, ) assert isinstance(endpoint, ParseError) @@ -799,79 +762,6 @@ def test_import_string_from_reference_with_prefix(self, mocker): class TestEndpointCollection: - def test_from_data(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint, EndpointCollection - - path_1_put = oai.Operation.model_construct() - path_1_post = oai.Operation.model_construct(tags=["tag_2", "tag_3"]) - path_2_get = oai.Operation.model_construct() - data = { - "path_1": oai.PathItem.model_construct(post=path_1_post, put=path_1_put), - "path_2": oai.PathItem.model_construct(get=path_2_get), - } - endpoint_1 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"1", "2"}, path="path_1") - endpoint_2 = mocker.MagicMock(autospec=Endpoint, tag="tag_2", relative_imports={"2"}, path="path_1") - endpoint_3 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"2", "3"}, path="path_2") - schemas_1 = mocker.MagicMock() - schemas_2 = mocker.MagicMock() - schemas_3 = mocker.MagicMock() - parameters_1 = mocker.MagicMock() - parameters_2 = mocker.MagicMock() - parameters_3 = mocker.MagicMock() - endpoint_from_data = mocker.patch.object( - Endpoint, - "from_data", - side_effect=[ - (endpoint_1, schemas_1, parameters_1), - (endpoint_2, schemas_2, parameters_2), - (endpoint_3, schemas_3, parameters_3), - ], - ) - schemas = mocker.MagicMock() - parameters = mocker.MagicMock() - - result = EndpointCollection.from_data(data=data, schemas=schemas, parameters=parameters, config=config) - - endpoint_from_data.assert_has_calls( - [ - mocker.call( - data=path_1_put, - path="path_1", - method="put", - tag="default", - schemas=schemas, - parameters=parameters, - config=config, - ), - mocker.call( - data=path_1_post, - path="path_1", - method="post", - tag="tag_2", - schemas=schemas_1, - parameters=parameters_1, - config=config, - ), - mocker.call( - data=path_2_get, - path="path_2", - method="get", - tag="default", - schemas=schemas_2, - parameters=parameters_2, - config=config, - ), - ], - ) - assert result == ( - { - "default": EndpointCollection("default", endpoints=[endpoint_1, endpoint_3]), - "tag_2": EndpointCollection("tag_2", endpoints=[endpoint_2]), - }, - schemas_3, - parameters_3, - ) - def test_from_data_overrides_path_item_params_with_operation_params(self, config): data = { "/": oai.PathItem.model_construct( @@ -896,6 +786,7 @@ def test_from_data_overrides_path_item_params_with_operation_params(self, config schemas=Schemas(), parameters=Parameters(), config=config, + request_bodies={}, ) collection: EndpointCollection = collections["default"] assert isinstance(collection.endpoints[0].query_parameters[0], IntProperty) @@ -916,7 +807,7 @@ def test_from_data_errors(self, mocker, config): parameters_1 = mocker.MagicMock() parameters_2 = mocker.MagicMock() parameters_3 = mocker.MagicMock() - endpoint_from_data = mocker.patch.object( + mocker.patch.object( Endpoint, "from_data", side_effect=[ @@ -929,40 +820,13 @@ def test_from_data_errors(self, mocker, config): parameters = mocker.MagicMock() result, result_schemas, result_parameters = EndpointCollection.from_data( - data=data, schemas=schemas, config=config, parameters=parameters + data=data, + schemas=schemas, + config=config, + parameters=parameters, + request_bodies={}, ) - endpoint_from_data.assert_has_calls( - [ - mocker.call( - data=path_1_put, - path="path_1", - method="put", - tag="default", - schemas=schemas, - parameters=parameters, - config=config, - ), - mocker.call( - data=path_1_post, - path="path_1", - method="post", - tag="tag_2", - schemas=schemas_1, - parameters=parameters_1, - config=config, - ), - mocker.call( - data=path_2_get, - path="path_2", - method="get", - tag="default", - schemas=schemas_2, - parameters=parameters_2, - config=config, - ), - ], - ) assert result["default"].parse_errors[0].data == "1" assert result["default"].parse_errors[1].data == "3" assert result["tag_2"].parse_errors[0].data == "2" @@ -989,7 +853,7 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker, config): parameters_1 = mocker.MagicMock() parameters_2 = mocker.MagicMock() parameters_3 = mocker.MagicMock() - endpoint_from_data = mocker.patch.object( + mocker.patch.object( Endpoint, "from_data", side_effect=[ @@ -1001,39 +865,10 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker, config): schemas = mocker.MagicMock() parameters = mocker.MagicMock() - result = EndpointCollection.from_data(data=data, schemas=schemas, parameters=parameters, config=config) - - endpoint_from_data.assert_has_calls( - [ - mocker.call( - data=path_1_put, - path="path_1", - method="put", - tag="default", - schemas=schemas, - parameters=parameters, - config=config, - ), - mocker.call( - data=path_1_post, - path="path_1", - method="post", - tag="amf_subscription_info_document", - schemas=schemas_1, - parameters=parameters_1, - config=config, - ), - mocker.call( - data=path_2_get, - path="path_2", - method="get", - tag="tag3_abc", - schemas=schemas_2, - parameters=parameters_2, - config=config, - ), - ], + result = EndpointCollection.from_data( + data=data, schemas=schemas, parameters=parameters, config=config, request_bodies={} ) + assert result == ( { "default": EndpointCollection("default", endpoints=[endpoint_1]),