Skip to content

Commit 32f6f67

Browse files
kigawasdbanty
andauthored
fix: Parsing requestBody with $ref (#633)
Fixes #595 Perhaps similar solution can be applied to #605 ## Description The requestBody with $ref was not handled. `requestBodies` in components should be injected into `_add_body` function. --------- Co-authored-by: Dylan Anthony <[email protected]>
1 parent 39b9db2 commit 32f6f67

File tree

16 files changed

+372
-220
lines changed

16 files changed

+372
-220
lines changed
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Support request body refs
6+
7+
You can now define and reuse bodies via refs, with a document like this:
8+
9+
```yaml
10+
paths:
11+
/something:
12+
post:
13+
requestBody:
14+
"$ref": "#/components/requestBodies/SharedBody"
15+
components:
16+
requestBodies:
17+
SharedBody:
18+
content:
19+
application/json:
20+
schema:
21+
type: string
22+
```
23+
24+
Thanks to @kigawas and @supermihi for initial implementations and @RockyMM for the initial request.
25+
26+
Closes #633, closes #664, resolves #595.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# serializer version: 1
2+
# name: test_documents_with_errors[circular-body-ref]
3+
'''
4+
Generating /test-documents-with-errors
5+
Warning(s) encountered while generating. Client was generated, but some pieces may be missing
6+
7+
WARNING parsing POST / within default. Endpoint will not be generated.
8+
9+
Circular $ref in request body
10+
11+
12+
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
13+
14+
'''
15+
# ---
16+
# name: test_documents_with_errors[missing-body-ref]
17+
'''
18+
Generating /test-documents-with-errors
19+
Warning(s) encountered while generating. Client was generated, but some pieces may be missing
20+
21+
WARNING parsing POST / within default. Endpoint will not be generated.
22+
23+
Could not resolve $ref #/components/requestBodies/body in request body
24+
25+
26+
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
27+
28+
'''
29+
# ---
30+
# name: test_documents_with_errors[optional-path-param]
31+
'''
32+
Generating /test-documents-with-errors
33+
Warning(s) encountered while generating. Client was generated, but some pieces may be missing
34+
35+
WARNING parsing GET /{optional} within default. Endpoint will not be generated.
36+
37+
Path parameter must be required
38+
39+
Parameter(name='optional', param_in=<ParameterLocation.PATH: 'path'>, 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=<DataType.STRING: 'string'>, 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)
40+
41+
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
42+
43+
'''
44+
# ---

end_to_end_tests/baseline_openapi_3.0.json

+31
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@
8787
}
8888
}
8989
},
90+
"/bodies/refs": {
91+
"post": {
92+
"tags": [
93+
"bodies"
94+
],
95+
"description": "Test request body defined via ref",
96+
"operationId": "refs",
97+
"requestBody": {
98+
"$ref": "#/components/requestBodies/NestedRef"
99+
},
100+
"responses": {
101+
"200": {
102+
"description": "OK"
103+
}
104+
}
105+
}
106+
},
90107
"/tests/": {
91108
"get": {
92109
"tags": [
@@ -2761,6 +2778,20 @@
27612778
"type": "string"
27622779
}
27632780
}
2781+
},
2782+
"requestBodies": {
2783+
"NestedRef": {
2784+
"$ref": "#/components/requestBodies/ARequestBody"
2785+
},
2786+
"ARequestBody": {
2787+
"content": {
2788+
"application/json": {
2789+
"schema": {
2790+
"$ref": "#/components/schemas/AModel"
2791+
}
2792+
}
2793+
}
2794+
}
27642795
}
27652796
}
27662797
}

end_to_end_tests/baseline_openapi_3.1.yaml

+27-4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,23 @@ info:
8383
}
8484
}
8585
},
86+
"/bodies/refs": {
87+
"post": {
88+
"tags": [
89+
"bodies"
90+
],
91+
"description": "Test request body defined via ref",
92+
"operationId": "refs",
93+
"requestBody": {
94+
"$ref": "#/components/requestBodies/NestedRef"
95+
},
96+
"responses": {
97+
"200": {
98+
"description": "OK"
99+
}
100+
}
101+
}
102+
},
86103
"/tests/": {
87104
"get": {
88105
"tags": [
@@ -1604,7 +1621,7 @@ info:
16041621
}
16051622
}
16061623
}
1607-
"components": {
1624+
"components":
16081625
"schemas": {
16091626
"AFormData": {
16101627
"type": "object",
@@ -2704,7 +2721,7 @@ info:
27042721
}
27052722
}
27062723
}
2707-
},
2724+
}
27082725
"parameters": {
27092726
"integer-param": {
27102727
"name": "integer param",
@@ -2772,5 +2789,11 @@ info:
27722789
}
27732790
}
27742791
}
2775-
}
2776-
2792+
requestBodies:
2793+
NestedRef:
2794+
"$ref": "#/components/requestBodies/ARequestBody"
2795+
ARequestBody:
2796+
content:
2797+
"application/json":
2798+
"schema":
2799+
"$ref": "#/components/schemas/AModel"

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import types
44

5-
from . import json_like, post_bodies_multiple
5+
from . import json_like, post_bodies_multiple, refs
66

77

88
class BodiesEndpoints:
@@ -19,3 +19,10 @@ def json_like(cls) -> types.ModuleType:
1919
A content type that works like json but isn't application/json
2020
"""
2121
return json_like
22+
23+
@classmethod
24+
def refs(cls) -> types.ModuleType:
25+
"""
26+
Test request body defined via ref
27+
"""
28+
return refs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: "Circular Body Ref"
4+
version: "0.1.0"
5+
paths:
6+
/:
7+
post:
8+
requestBody:
9+
$ref: "#/components/requestBodies/body"
10+
responses:
11+
"200":
12+
description: "Successful Response"
13+
content:
14+
"application/json":
15+
schema:
16+
const: "Why have a fixed response? I dunno"
17+
components:
18+
requestBodies:
19+
body:
20+
$ref: "#/components/requestBodies/body"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: "Trying to use a request body ref that does not exist"
4+
version: "0.1.0"
5+
paths:
6+
/:
7+
post:
8+
requestBody:
9+
$ref: "#/components/requestBodies/body"
10+
responses:
11+
"200":
12+
description: "Successful Response"
13+
content:
14+
"application/json":
15+
schema:
16+
const: "Why have a fixed response? I dunno"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from http import HTTPStatus
2+
from typing import Any, Dict, Optional, Union
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...models.a_model import AModel
9+
from ...types import Response
10+
11+
12+
def _get_kwargs(
13+
*,
14+
body: AModel,
15+
) -> Dict[str, Any]:
16+
headers: Dict[str, Any] = {}
17+
18+
_kwargs: Dict[str, Any] = {
19+
"method": "post",
20+
"url": "/bodies/refs",
21+
}
22+
23+
_body = body.to_dict()
24+
25+
_kwargs["json"] = _body
26+
headers["Content-Type"] = "application/json"
27+
28+
_kwargs["headers"] = headers
29+
return _kwargs
30+
31+
32+
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
33+
if response.status_code == HTTPStatus.OK:
34+
return None
35+
if client.raise_on_unexpected_status:
36+
raise errors.UnexpectedStatus(response.status_code, response.content)
37+
else:
38+
return None
39+
40+
41+
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
42+
return Response(
43+
status_code=HTTPStatus(response.status_code),
44+
content=response.content,
45+
headers=response.headers,
46+
parsed=_parse_response(client=client, response=response),
47+
)
48+
49+
50+
def sync_detailed(
51+
*,
52+
client: Union[AuthenticatedClient, Client],
53+
body: AModel,
54+
) -> Response[Any]:
55+
"""Test request body defined via ref
56+
57+
Args:
58+
body (AModel): A Model for testing all the ways custom objects can be used
59+
60+
Raises:
61+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
62+
httpx.TimeoutException: If the request takes longer than Client.timeout.
63+
64+
Returns:
65+
Response[Any]
66+
"""
67+
68+
kwargs = _get_kwargs(
69+
body=body,
70+
)
71+
72+
response = client.get_httpx_client().request(
73+
**kwargs,
74+
)
75+
76+
return _build_response(client=client, response=response)
77+
78+
79+
async def asyncio_detailed(
80+
*,
81+
client: Union[AuthenticatedClient, Client],
82+
body: AModel,
83+
) -> Response[Any]:
84+
"""Test request body defined via ref
85+
86+
Args:
87+
body (AModel): A Model for testing all the ways custom objects can be used
88+
89+
Raises:
90+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
91+
httpx.TimeoutException: If the request takes longer than Client.timeout.
92+
93+
Returns:
94+
Response[Any]
95+
"""
96+
97+
kwargs = _get_kwargs(
98+
body=body,
99+
)
100+
101+
response = await client.get_async_httpx_client().request(**kwargs)
102+
103+
return _build_response(client=client, response=response)

end_to_end_tests/test_end_to_end.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,18 @@ def test_bad_url():
222222
assert "Could not get OpenAPI document from provided URL" in result.stdout
223223

224224

225-
def test_invalid_document():
225+
ERROR_DOCUMENTS = [path for path in Path(__file__).parent.joinpath("documents_with_errors").iterdir() if path.is_file()]
226+
227+
228+
@pytest.mark.parametrize("document", ERROR_DOCUMENTS, ids=[path.stem for path in ERROR_DOCUMENTS])
229+
def test_documents_with_errors(snapshot, document):
226230
runner = CliRunner()
227-
path = Path(__file__).parent / "invalid_openapi.yaml"
228-
result = runner.invoke(app, ["generate", f"--path={path}", "--fail-on-warning"])
231+
output_path = Path.cwd() / "test-documents-with-errors"
232+
shutil.rmtree(output_path, ignore_errors=True)
233+
result = runner.invoke(app, ["generate", f"--path={document}", "--fail-on-warning", f"--output-path={output_path}"])
229234
assert result.exit_code == 1
230-
assert "Warning(s) encountered while generating" in result.stdout
235+
assert result.stdout.replace(str(output_path), "/test-documents-with-errors") == snapshot
236+
shutil.rmtree(output_path, ignore_errors=True)
231237

232238

233239
def test_custom_post_hooks():

0 commit comments

Comments
 (0)