Skip to content

Commit

Permalink
Add tests for request body refs and handle nested refs
Browse files Browse the repository at this point in the history
  • Loading branch information
dbanty committed Jun 15, 2024
1 parent 813513d commit cdc5f78
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 214 deletions.
31 changes: 31 additions & 0 deletions end_to_end_tests/baseline_openapi_3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -2761,6 +2778,20 @@
"type": "string"
}
}
},
"requestBodies": {
"NestedRef": {
"$ref": "#/components/requestBodies/ARequestBody"
},
"ARequestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AModel"
}
}
}
}
}
}
}
31 changes: 27 additions & 4 deletions end_to_end_tests/baseline_openapi_3.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -1604,7 +1621,7 @@ info:
}
}
}
"components": {
"components":
"schemas": {
"AFormData": {
"type": "object",
Expand Down Expand Up @@ -2704,7 +2721,7 @@ info:
}
}
}
},
}
"parameters": {
"integer-param": {
"name": "integer param",
Expand Down Expand Up @@ -2772,5 +2789,11 @@ info:
}
}
}
}

requestBodies:
NestedRef:
"$ref": "#/components/requestBodies/ARequestBody"
ARequestBody:
content:
"application/json":
"schema":
"$ref": "#/components/schemas/AModel"
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import types

from . import json_like, post_bodies_multiple
from . import json_like, post_bodies_multiple, refs


class BodiesEndpoints:
Expand All @@ -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
103 changes: 103 additions & 0 deletions end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 26 additions & 6 deletions openapi_python_client/parser/bodies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from typing import List, Tuple, Union
from typing import Dict, List, Tuple, Union

import attr

Expand Down Expand Up @@ -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():
Expand All @@ -61,7 +65,7 @@ def body_from_data(
bodies.append(
ParseError(
detail="Invalid content type",
data=data.request_body,
data=body,
level=ErrorLevel.WARNING,
)
)
Expand All @@ -71,7 +75,7 @@ def body_from_data(
bodies.append(
ParseError(
detail="Missing schema",
data=data.request_body,
data=body,
level=ErrorLevel.WARNING,
)
)
Expand All @@ -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,
)
)
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,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):
Expand Down Expand Up @@ -510,9 +512,7 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData",
parameters=parameters,
config=config,
)
request_bodies = {}
if openapi.components and openapi.components.requestBodies:
request_bodies = openapi.components.requestBodies
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, request_bodies=request_bodies, config=config
)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_parser/test_bodies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading

0 comments on commit cdc5f78

Please sign in to comment.