Skip to content

Commit

Permalink
feat: Support ResponseSpec(..., examples=[...]) (#3100)
Browse files Browse the repository at this point in the history
feat: Support `ResponseSpec(..., examples=[...])` (#3068)

Allows to define custom examples for the responses via `ResponseSpec`.

The examples set this way are always generated locally, for each response:

```json
{
    "paths": {
    "/": {
        "get": {
        "responses": {
            "200": {
            "content": {
                "application/json": {
                "schema": {...},
                "examples": ...
```

Examples that go within the schema definition cannot be set by this.
  • Loading branch information
tuukkamustonen authored and provinzkraut committed Feb 13, 2024
1 parent 927aea8 commit 50826a2
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 20 deletions.
30 changes: 15 additions & 15 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
"name": "litestar-org/litestar",
"build": {
"dockerfile": "./Dockerfile",
"context": "."
"context": ".",
},
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "true",
"username": "vscode",
"userUid": "1000",
"userGid": "1000",
"upgradePackages": "true"
"upgradePackages": "true",
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers-contrib/features/pre-commit:2": {},
"ghcr.io/devcontainers/features/python:1": "none",
"ghcr.io/devcontainers/features/git:1": {
"version": "latest",
"ppa": "false"
}
"ppa": "false",
},
},
"customizations": {
"codespaces": {
"openFiles": ["CONTRIBUTING.rst"]
"openFiles": ["CONTRIBUTING.rst"],
},
"vscode": {
"extensions": [
Expand All @@ -31,7 +31,7 @@
"github.vscode-github-actions",
"ms-python.black-formatter",
"ms-python.mypy-type-checker",
"charliermarsh.ruff"
"charliermarsh.ruff",
],
"settings": {
"python.editor.defaultFormatter": "charliermarsh.ruff",
Expand All @@ -45,17 +45,17 @@
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"icon": "terminal-bash"
"icon": "terminal-bash",
},
"zsh": {
"path": "zsh"
"path": "zsh",
},
"fish": {
"path": "fish"
}
}
}
}
"path": "fish",
},
},
},
},
},
"forwardPorts": [8000],
"postCreateCommand": [
Expand All @@ -64,7 +64,7 @@
"--extras",
"full",
"--with",
"docs,lint"
"docs,lint",
],
"remoteUser": "vscode"
"remoteUser": "vscode",
}
14 changes: 10 additions & 4 deletions litestar/_openapi/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import TYPE_CHECKING, Any, Iterator

from litestar._openapi.schema_generation import SchemaCreator
from litestar._openapi.schema_generation.utils import get_formatted_examples
from litestar.enums import MediaType
from litestar.exceptions import HTTPException, ValidationException
from litestar.openapi.spec import Example, OpenAPIResponse
Expand Down Expand Up @@ -242,11 +243,16 @@ def create_additional_responses(self) -> Iterator[tuple[str, OpenAPIResponse]]:
)

content: dict[str, OpenAPIMediaType] | None
field_def = FieldDefinition.from_annotation(additional_response.data_container)
examples = (
dict(get_formatted_examples(field_def, additional_response.examples))
if additional_response.examples
else None
)

if additional_response.data_container is not None:
schema = schema_creator.for_field_definition(
FieldDefinition.from_annotation(additional_response.data_container)
)
content = {additional_response.media_type: OpenAPIMediaType(schema=schema)}
schema = schema_creator.for_field_definition(field_def)
content = {additional_response.media_type: OpenAPIMediaType(schema=schema, examples=examples)}
else:
content = None

Expand Down
3 changes: 3 additions & 0 deletions litestar/openapi/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from litestar.enums import MediaType

if TYPE_CHECKING:
from litestar.openapi.spec import Example
from litestar.types import DataContainerType


Expand All @@ -24,3 +25,5 @@ class ResponseSpec:
"""A description of the response."""
media_type: MediaType = field(default=MediaType.JSON)
"""Response media type."""
examples: list[Example] | None = field(default=None)
"""A list of Example models."""
24 changes: 23 additions & 1 deletion tests/unit/test_openapi/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from litestar.handlers import HTTPRouteHandler
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.datastructures import ResponseSpec
from litestar.openapi.spec import OpenAPIHeader, OpenAPIMediaType, Reference, Schema
from litestar.openapi.spec import Example, OpenAPIHeader, OpenAPIMediaType, Reference, Schema
from litestar.openapi.spec.enums import OpenAPIType
from litestar.response import File, Redirect, Stream, Template
from litestar.response.base import T
Expand Down Expand Up @@ -440,6 +440,28 @@ def handler() -> DataclassPerson:
assert responses["400"].description == "Overwritten response"


def test_additional_responses_with_custom_examples(create_factory: CreateFactoryFixture) -> None:
@get(responses={200: ResponseSpec(DataclassPerson, examples=[Example(value={"string": "example", "number": 1})])})
def handler() -> DataclassPerson:
return DataclassPersonFactory.build()

factory = create_factory(handler)
responses = factory.create_additional_responses()
status_code, response = next(responses)
assert response.content
assert response.content["application/json"].examples == {
"dataclassperson-example-1": Example(
value={
"string": "example",
"number": 1,
}
),
}

with pytest.raises(StopIteration):
next(responses)


def test_create_response_for_response_subclass(create_factory: CreateFactoryFixture) -> None:
class CustomResponse(Response[T]):
pass
Expand Down

0 comments on commit 50826a2

Please sign in to comment.