From f774f04fb840397196747a405033a0c01e6723ac Mon Sep 17 00:00:00 2001 From: Igor Davydenko Date: Sun, 12 May 2019 22:56:21 +0200 Subject: [PATCH] feature: Support passing custom OpenAPI versions. Previously `AiohttpApiSpec` instantiate `ApiSpec` only for `openapi_version == '2.0'`, this commit changes it by allowing pass `openapi_version` to `setup_aiohttp_apispec` function and then use given value, while instantiating `ApiSpec` instance. Fixes #46 --- .gitignore | 1 + aiohttp_apispec/aiohttp_apispec.py | 17 +- tests/conftest.py | 46 +++-- tests/test_documentation.py | 268 +++++++++++++++-------------- 4 files changed, 185 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index a35ca22..5e5819c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,6 @@ *.egg-info /dist /venv +/.venv .mypy_cache/ .DS_Store diff --git a/aiohttp_apispec/aiohttp_apispec.py b/aiohttp_apispec/aiohttp_apispec.py index e719370..2fd2ead 100644 --- a/aiohttp_apispec/aiohttp_apispec.py +++ b/aiohttp_apispec/aiohttp_apispec.py @@ -24,12 +24,12 @@ def __init__( app=None, request_data_name="data", swagger_path=None, - static_path='/static/swagger', + static_path="/static/swagger", **kwargs ): self.plugin = MarshmallowPlugin() - self.spec = APISpec(plugins=(self.plugin,), openapi_version="2.0", **kwargs) + self.spec = APISpec(plugins=(self.plugin,), **kwargs) self.url = url self.swagger_path = swagger_path @@ -61,7 +61,9 @@ async def swagger_handler(request): if self.swagger_path is not None: self.add_swagger_web_page(app, self.static_path, self.swagger_path) - def add_swagger_web_page(self, app: web.Application, static_path: str, view_path: str): + def add_swagger_web_page( + self, app: web.Application, static_path: str, view_path: str + ): static_files = Path(__file__).parent / "static" app.router.add_static(static_path, static_files) @@ -69,9 +71,7 @@ def add_swagger_web_page(self, app: web.Application, static_path: str, view_path tmp = Template(swg_tmp.read()).render(path=self.url, static=static_path) async def swagger_view(_): - return web.Response( - text=tmp, content_type="text/html" - ) + return web.Response(text=tmp, content_type="text/html") app.router.add_route("GET", view_path, swagger_view) @@ -148,10 +148,11 @@ def setup_aiohttp_apispec( *, title: str = "API documentation", version: str = "0.0.1", + openapi_version: str = "2.0", url: str = "/api/docs/swagger.json", request_data_name: str = "data", swagger_path: str = None, - static_path: str = '/static/swagger', + static_path: str = "/static/swagger", **kwargs ) -> None: """ @@ -195,6 +196,7 @@ async def index(request): :param Application app: aiohttp web app :param str title: API title :param str version: API version + :param str openapi_version: OpenAPI version to use. By default: ``'2.0'`` :param str url: url for swagger spec in JSON format :param str request_data_name: name of the key in Request object where validated data will be placed by @@ -211,6 +213,7 @@ async def index(request): request_data_name, title=title, version=version, + openapi_version=openapi_version, swagger_path=swagger_path, static_path=static_path, **kwargs diff --git a/tests/conftest.py b/tests/conftest.py index 02cc132..83eae06 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,12 +13,12 @@ def pytest_report_header(config): return """ - . . . -,-. . ,-. |-. |- |- ,-. ,-. ,-. . ,-. ,-. ,-. ,-. -,-| | | | | | | | | | -- ,-| | | | `-. | | |-' | -`-^ ' `-' ' ' `' `' |-' `-^ |-' ' `-' |-' `-' `-' - | | | - ' ' ' + . . . +,-. . ,-. |-. |- |- ,-. ,-. ,-. . ,-. ,-. ,-. ,-. +,-| | | | | | | | | | -- ,-| | | | `-. | | |-' | +`-^ ' `-' ' ' `' `' |-' `-^ |-' ' `-' |-' `-' `-' + | | | + ' ' ' """ @@ -55,10 +55,22 @@ class ResponseSchema(Schema): @pytest.fixture( params=[ - ({"locations": ["query"]}, True), - ({"location": "query"}, True), - ({"locations": ["query"]}, False), - ({"location": "query"}, False), + (None, {"locations": ["query"]}, True), + (None, {"location": "query"}, True), + (None, {"locations": ["query"]}, False), + (None, {"location": "query"}, False), + ("2.0", {"locations": ["query"]}, True), + ("2.0", {"location": "query"}, True), + ("2.0", {"locations": ["query"]}, False), + ("2.0", {"location": "query"}, False), + ("2.1", {"locations": ["query"]}, True), + ("2.1", {"location": "query"}, True), + ("2.1", {"locations": ["query"]}, False), + ("2.1", {"location": "query"}, False), + ("3.0", {"locations": ["query"]}, True), + ("3.0", {"location": "query"}, True), + ("3.0.2", {"locations": ["query"]}, True), + ("3.0.2", {"location": "query"}, True), ] ) def aiohttp_app( @@ -69,7 +81,7 @@ def aiohttp_app( aiohttp_client, request, ): - locations, nested = request.param + openapi_version, locations, nested = request.param @docs( tags=["mytag"], @@ -127,10 +139,18 @@ async def other(request): return web.Response() app = web.Application() + kwargs = {} + if openapi_version: + kwargs["openapi_version"] = openapi_version + if nested: v1 = web.Application() setup_aiohttp_apispec( - app=v1, title="API documentation", version="0.0.1", url="/api/docs/api-docs" + app=v1, + title="API documentation", + version="0.0.1", + url="/api/docs/api-docs", + **kwargs, ) v1.router.add_routes( [ @@ -147,7 +167,7 @@ async def other(request): v1.middlewares.append(validation_middleware) app.add_subapp("/v1/", v1) else: - setup_aiohttp_apispec(app=app, url="/v1/api/docs/api-docs") + setup_aiohttp_apispec(app=app, url="/v1/api/docs/api-docs", **kwargs) app.router.add_routes( [ web.get("/v1/test", handler_get), diff --git a/tests/test_documentation.py b/tests/test_documentation.py index aed9315..1e2a0ca 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -2,6 +2,123 @@ from yarl import URL +OPENAPI_20_V1_TEST_SCHEMA = { + "parameters": [ + {"in": "query", "name": "bool_field", "required": False, "type": "boolean"}, + { + "format": "int32", + "in": "query", + "name": "id", + "required": False, + "type": "integer", + }, + { + "collectionFormat": "multi", + "in": "query", + "items": {"format": "int32", "type": "integer"}, + "name": "list_field", + "required": False, + "type": "array", + }, + { + "description": "name", + "in": "query", + "name": "name", + "required": False, + "type": "string", + }, + ], + "responses": { + "200": { + "description": "Success response", + "schema": {"$ref": "#/definitions/Response"}, + }, + "404": {"description": "Not Found"}, + }, + "tags": ["mytag"], + "summary": "Test method summary", + "description": "Test method description", + "produces": ["application/json"], +} + +OPENAPI_20_V1_CLASS_ECHO_SCHEMA = { + "parameters": [ + {"in": "query", "name": "bool_field", "required": False, "type": "boolean"}, + { + "format": "int32", + "in": "query", + "name": "id", + "required": False, + "type": "integer", + }, + { + "collectionFormat": "multi", + "in": "query", + "items": {"format": "int32", "type": "integer"}, + "name": "list_field", + "required": False, + "type": "array", + }, + { + "description": "name", + "in": "query", + "name": "name", + "required": False, + "type": "string", + }, + ], + "responses": {}, + "tags": ["mytag"], + "summary": "View method summary", + "description": "View method description", + "produces": ["application/json"], +} + +OPENAPI_20_DEFINITIONS_SCHEMA = { + "Response": { + "type": "object", + "properties": {"msg": {"type": "string"}, "data": {"type": "object"}}, + }, + "Request": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "name"}, + "bool_field": {"type": "boolean"}, + "list_field": { + "type": "array", + "items": {"type": "integer", "format": "int32"}, + }, + "id": {"type": "integer", "format": "int32"}, + }, + }, + "Request1": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "name"}, + "bool_field": {"type": "boolean"}, + "list_field": { + "type": "array", + "items": {"type": "integer", "format": "int32"}, + }, + "id": {"type": "integer", "format": "int32"}, + }, + }, +} + +SCHEMAS_MAPPING = { + "2.0": ( + OPENAPI_20_V1_TEST_SCHEMA, + OPENAPI_20_V1_CLASS_ECHO_SCHEMA, + OPENAPI_20_DEFINITIONS_SCHEMA, + ), + "2.1": ( + OPENAPI_20_V1_TEST_SCHEMA, + OPENAPI_20_V1_CLASS_ECHO_SCHEMA, + OPENAPI_20_DEFINITIONS_SCHEMA, + ), +} + + def test_app_swagger_url(aiohttp_app): def safe_url_for(route): try: @@ -16,133 +133,30 @@ def safe_url_for(route): async def test_app_swagger_json(aiohttp_app): resp = await aiohttp_app.get("/v1/api/docs/api-docs") docs = await resp.json() + assert docs["info"]["title"] == "API documentation" assert docs["info"]["version"] == "0.0.1" - docs["paths"]["/v1/test"]["get"]["parameters"] = sorted( - docs["paths"]["/v1/test"]["get"]["parameters"], key=lambda x: x["name"] - ) - assert json.dumps(docs["paths"]["/v1/test"]["get"], sort_keys=True) == json.dumps( - { - "parameters": [ - { - "in": "query", - "name": "bool_field", - "required": False, - "type": "boolean", - }, - { - "format": "int32", - "in": "query", - "name": "id", - "required": False, - "type": "integer", - }, - { - "collectionFormat": "multi", - "in": "query", - "items": {"format": "int32", "type": "integer"}, - "name": "list_field", - "required": False, - "type": "array", - }, - { - "description": "name", - "in": "query", - "name": "name", - "required": False, - "type": "string", - }, - ], - "responses": { - "200": { - "description": "Success response", - "schema": {"$ref": "#/definitions/Response"}, - }, - "404": {"description": "Not Found"}, - }, - "tags": ["mytag"], - "summary": "Test method summary", - "description": "Test method description", - "produces": ["application/json"], - }, - sort_keys=True, - ) - docs["paths"]["/v1/class_echo"]["get"]["parameters"] = sorted( - docs["paths"]["/v1/class_echo"]["get"]["parameters"], key=lambda x: x["name"] - ) - assert json.dumps( - docs["paths"]["/v1/class_echo"]["get"], sort_keys=True - ) == json.dumps( - { - "parameters": [ - { - "in": "query", - "name": "bool_field", - "required": False, - "type": "boolean", - }, - { - "format": "int32", - "in": "query", - "name": "id", - "required": False, - "type": "integer", - }, - { - "collectionFormat": "multi", - "in": "query", - "items": {"format": "int32", "type": "integer"}, - "name": "list_field", - "required": False, - "type": "array", - }, - { - "description": "name", - "in": "query", - "name": "name", - "required": False, - "type": "string", - }, - ], - "responses": {}, - "tags": ["mytag"], - "summary": "View method summary", - "description": "View method description", - "produces": ["application/json"], - }, - sort_keys=True, - ) - assert json.dumps(docs["definitions"], sort_keys=True) == json.dumps( - { - "Response": { - "type": "object", - "properties": {"msg": {"type": "string"}, "data": {"type": "object"}}, - }, - "Request": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "name"}, - "bool_field": {"type": "boolean"}, - "list_field": { - "type": "array", - "items": {"type": "integer", "format": "int32"}, - }, - "id": {"type": "integer", "format": "int32"}, - }, - }, - "Request1": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "name"}, - "bool_field": {"type": "boolean"}, - "list_field": { - "type": "array", - "items": {"type": "integer", "format": "int32"}, - }, - "id": {"type": "integer", "format": "int32"}, - }, - }, - }, - sort_keys=True, - ) + openapi_version = docs.get("openapi") or "2.0" + if openapi_version in SCHEMAS_MAPPING: + test_schema, class_echo_schema, definitions_schema = SCHEMAS_MAPPING[ + openapi_version + ] + docs["paths"]["/v1/test"]["get"]["parameters"] = sorted( + docs["paths"]["/v1/test"]["get"]["parameters"], key=lambda x: x["name"] + ) + assert json.dumps( + docs["paths"]["/v1/test"]["get"], sort_keys=True + ) == json.dumps(test_schema, sort_keys=True) + + docs["paths"]["/v1/class_echo"]["get"]["parameters"] = sorted( + docs["paths"]["/v1/class_echo"]["get"]["parameters"], + key=lambda x: x["name"], + ) + assert json.dumps( + docs["paths"]["/v1/class_echo"]["get"], sort_keys=True + ) == json.dumps(class_echo_schema, sort_keys=True) + + assert json.dumps(docs["definitions"], sort_keys=True) == json.dumps( + definitions_schema, sort_keys=True + )