From b591de2ace297c1da537e50f64b4066f96553247 Mon Sep 17 00:00:00 2001 From: mikaello <2505178+mikaello@users.noreply.github.com> Date: Sun, 14 Jun 2020 15:38:29 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20OpenAPI=20ser?= =?UTF-8?q?vers=20metadata=20(#1547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add servers option for OpenAPI Closes #872 * ✨ Use dicts for OpenAPI servers * ♻️ Update OpenAPI Server model to support relative URLs * ✅ Add tests for OpenAPI servers * ♻️ Re-order parameter location of servers for OpenAPI * 🎨 Format code Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- fastapi/applications.py | 3 ++ fastapi/openapi/models.py | 2 +- fastapi/openapi/utils.py | 9 ++++-- tests/test_openapi_servers.py | 60 +++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/test_openapi_servers.py diff --git a/fastapi/applications.py b/fastapi/applications.py index 3306aab3d..c21087911 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -38,6 +38,7 @@ class FastAPI(Starlette): version: str = "0.1.0", openapi_url: Optional[str] = "/openapi.json", openapi_tags: Optional[List[Dict[str, Any]]] = None, + servers: Optional[List[Dict[str, Union[str, Any]]]] = None, default_response_class: Type[Response] = JSONResponse, docs_url: Optional[str] = "/docs", redoc_url: Optional[str] = "/redoc", @@ -70,6 +71,7 @@ class FastAPI(Starlette): self.title = title self.description = description self.version = version + self.servers = servers self.openapi_url = openapi_url self.openapi_tags = openapi_tags # TODO: remove when discarding the openapi_prefix parameter @@ -106,6 +108,7 @@ class FastAPI(Starlette): routes=self.routes, openapi_prefix=openapi_prefix, tags=self.openapi_tags, + servers=self.servers, ) return self.openapi_schema diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index a7c4460fa..13dc59f18 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -63,7 +63,7 @@ class ServerVariable(BaseModel): class Server(BaseModel): - url: AnyUrl + url: Union[AnyUrl, str] description: Optional[str] = None variables: Optional[Dict[str, ServerVariable]] = None diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index b6221ca20..5a0c89a89 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -86,7 +86,7 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L def get_openapi_operation_parameters( *, all_route_params: Sequence[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str] + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: @@ -112,7 +112,7 @@ def get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str] + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], ) -> Optional[Dict]: if not body_field: return None @@ -318,12 +318,15 @@ def get_openapi( description: str = None, routes: Sequence[BaseRoute], openapi_prefix: str = "", - tags: Optional[List[Dict[str, Any]]] = None + tags: Optional[List[Dict[str, Any]]] = None, + servers: Optional[List[Dict[str, Union[str, Any]]]] = None, ) -> Dict: info = {"title": title, "version": version} if description: info["description"] = description output: Dict[str, Any] = {"openapi": openapi_version, "info": info} + if servers: + output["servers"] = servers components: Dict[str, Dict] = {} paths: Dict[str, Dict] = {} flat_models = get_flat_models_from_routes(routes) diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py new file mode 100644 index 000000000..a210154f6 --- /dev/null +++ b/tests/test_openapi_servers.py @@ -0,0 +1,60 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app = FastAPI( + servers=[ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ] +) + + +@app.get("/foo") +def foo(): + return {"message": "Hello World"} + + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi_servers(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_app(): + response = client.get("/foo") + assert response.status_code == 200, response.text