From b352a8f6d998c85fba872d23450ce06915a7ac97 Mon Sep 17 00:00:00 2001 From: tarsil Date: Fri, 14 Jul 2023 13:34:18 +0100 Subject: [PATCH] Update tests and openapi responses doc --- README.md | 5 +++- docs/index.md | 3 ++ docs/release-notes.md | 11 ++++++- docs/responses.md | 13 +++++++++ esmerald/__init__.py | 2 +- esmerald/openapi/datastructures.py | 12 +++++++- pyproject.toml | 2 +- tests/openapi/test_raise_value_error.py | 39 +++++++++++++++++++++++++ 8 files changed, 82 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ed33f83..08a153f4 100644 --- a/README.md +++ b/README.md @@ -507,9 +507,12 @@ To access the OpenAPI, simply start your local development and access: * **Swagger** - `/docs/swagger`. * **Redoc** - `/docs/redoc`. -There are more details about [how to configure the OpenAPIConfig](https://esmerald.dev/configurations/openapi/config.md) +There are more details about [how to configure the OpenAPIConfig](https://esmerald.dev/configurations/openapi/config) within the documentation. +There is also a good explanation on how to use the [OpenAPIResponse](https://esmerald.dev/responses#openapi-responses) +as well. + ## Notes This is just a very high-level demonstration of how to start quickly and what Esmerald can do. diff --git a/docs/index.md b/docs/index.md index 3c4bb4d6..6281f435 100644 --- a/docs/index.md +++ b/docs/index.md @@ -424,6 +424,9 @@ To access the OpenAPI, simply start your local development and access: There are more details about [how to configure the OpenAPIConfig](./configurations/openapi/config.md) within this documentation. +There is also a good explanation on how to use the [OpenAPIResponse](./responses.md#openapi-responses) +as well. + ## Notes This is just a very high-level demonstration of how to start quickly and what Esmerald can do. diff --git a/docs/release-notes.md b/docs/release-notes.md index d8484076..8e874d22 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,14 @@ # Release Notes +## 2.0.1 + +This is a small fix into the parser of lists for the OpenAPI specification. + +### Fixed + +- [OpenAPIResponse](https://esmerald.dev/responses#openapi-responses) now allows proper parse of +lists as definition. [More details](http://esmerald.dev/responses/#lists). + ## 2.0.0 !!! Warning @@ -166,7 +175,7 @@ across main application and children. - Brand new support for [Saffier](https://saffier.tarsild.io). A brand new ORM running on the top of SQLAlchemy in an async fashion. - New `base_user` and `middleware` support for Saffier with Esmerald. -- New docs regarding the [Saffier](https://esmerald.dymmond.com/databases/saffier/motivation/) integration. +- New docs regarding the [Saffier](https://esmerald.dev/databases/saffier/motivation/) integration. Those include also an example how to use it. ### Changed diff --git a/docs/responses.md b/docs/responses.md index 984fa457..1d9fe21f 100644 --- a/docs/responses.md +++ b/docs/responses.md @@ -341,6 +341,19 @@ async def get_items() -> Union[None, UserOut]: As you could notice, we simply added `[]` in the model to reflect a list in the OpenAPI specification. That simple. +#### Errors + +A `ValueError` is raised in the following scenarios: + +* You try to pass a model than one pydantic model into a list. The OpenAPIResponse is a mere +representation of a response, so be compliant. +* You try to pass a model that is not a subclass of a Pydantic `BaseModel`. +* You try to pass a list of non Pydantic `BaseModels`. + +When one of these scenarios occur, the following error will be raised. + +> The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel]) + ## Other responses There are other responses you can have that does not necessessarily have to be the ones provided here. Every case is diff --git a/esmerald/__init__.py b/esmerald/__init__.py index c4c3e4bf..2e851b3d 100644 --- a/esmerald/__init__.py +++ b/esmerald/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" from starlette import status diff --git a/esmerald/openapi/datastructures.py b/esmerald/openapi/datastructures.py index dcb4e24c..98bfade5 100644 --- a/esmerald/openapi/datastructures.py +++ b/esmerald/openapi/datastructures.py @@ -1,6 +1,6 @@ from typing import List, Optional, Type, Union -from pydantic import BaseModel +from pydantic import BaseModel, field_validator from esmerald.enums import MediaType @@ -10,3 +10,13 @@ class OpenAPIResponse(BaseModel): description: str = "Additional response" media_type: MediaType = MediaType.JSON status_text: Optional[str] = None + + @field_validator("model", mode="before") + def validate_model( + cls, model: Union[Type[BaseModel], List[Type[BaseModel]]] + ) -> Union[Type[BaseModel], List[Type[BaseModel]]]: + if isinstance(model, list) and len(model) > 1: + raise ValueError( + "The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel])." + ) + return model diff --git a/pyproject.toml b/pyproject.toml index b47d15f9..7e937503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ test = [ "polyfactory>=2.5.0,<3.0.0", "python-jose>=3.3.0,<4", "orjson>=3.8.5,<4.0.0", - "saffier[postgres]>=0.14.0", + "saffier[postgres]>=0.15.0", "requests>=2.28.2,<3.0.0", "ruff>=0.0.256,<1.0.0", "ujson>=5.7.0,<6", diff --git a/tests/openapi/test_raise_value_error.py b/tests/openapi/test_raise_value_error.py index 81dd31c4..6e0135f5 100644 --- a/tests/openapi/test_raise_value_error.py +++ b/tests/openapi/test_raise_value_error.py @@ -12,6 +12,11 @@ class Error(BaseModel): detail: str +class DummyErrorModel(BaseModel): + status: int + detail: str + + @dataclass class DummyErrorDataclass: status: int @@ -61,3 +66,37 @@ def test_openapi_response_value_for_class_as_list(test_client_factory, model): ) async def read_item(id: str) -> None: ... + + +def test_openapi_response_value_for_class_as_list_multiple_models(test_client_factory): + with pytest.raises(ValueError) as raised: + + @get( + "/item/{id}", + responses={422: OpenAPIResponse(model=[Error, DummyErrorModel], description="Error")}, + ) + async def read_item(id: str) -> None: + ... + + assert ( + raised.value.errors()[0]["ctx"]["error"] + == "The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel])." + ) + + +def xtest_openapi_response_value_for_class_as_list_multiple(test_client_factory): + with pytest.raises(ValueError) as raised: + + @get( + "/item/{id}", + responses={ + 422: OpenAPIResponse(model=[DummyErrorDataclass, DummyError], description="Error") + }, + ) + async def read_item(id: str) -> None: + ... + + assert ( + raised.value.errors()[0]["ctx"]["error"] + == "The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel])." + )