From 3d3ce264a918c98176f7340e43d00aa140d1b9f8 Mon Sep 17 00:00:00 2001 From: Hat Dao Date: Mon, 8 Apr 2019 11:04:51 +0700 Subject: [PATCH 1/2] Support multiple versions swagger --- flask_apispec/extension.py | 33 ++++++++++++++++++++++--- flask_apispec/templates/swagger-ui.html | 16 ++++++------ tests/test_extension.py | 27 ++++++++++++++++++++ 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 68636b1..3430ec1 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -36,14 +36,17 @@ def get_pet(pet_id): :param Flask app: App associated with API documentation :param APISpec spec: apispec specification associated with API documentation + :param bool support_multiple_version: support multiple version swaggers + by register doc to 'swagger-{api_version}' url """ - def __init__(self, app=None): + def __init__(self, app=None, support_multiple_version=False): self._deferred = [] self.app = app self.view_converter = None self.resource_converter = None self.spec = None + self.support_multiple_version = support_multiple_version if app: self.init_app(app) @@ -67,9 +70,15 @@ def _defer(self, callable, *args, **kwargs): if self.app: bound() + @property + def blueprint_name(self): + if self.support_multiple_version: + return 'flask-apispec-' + self.spec.version + return 'flask-apispec' + def add_swagger_routes(self): blueprint = flask.Blueprint( - 'flask-apispec', + self.blueprint_name, __name__, static_folder='./static', template_folder='./templates', @@ -78,19 +87,37 @@ def add_swagger_routes(self): json_url = self.app.config.get('APISPEC_SWAGGER_URL', '/swagger/') if json_url: + if self.support_multiple_version: + json_url = self.make_url_with_suffix_version(json_url) + blueprint.add_url_rule(json_url, 'swagger-json', self.swagger_json) ui_url = self.app.config.get('APISPEC_SWAGGER_UI_URL', '/swagger-ui/') if ui_url: + if self.support_multiple_version: + ui_url = self.make_url_with_suffix_version(ui_url) blueprint.add_url_rule(ui_url, 'swagger-ui', self.swagger_ui) self.app.register_blueprint(blueprint) + def make_url_with_suffix_version(self, url): + # adding version suffix + if url.endswith('/'): + url = url[:-1] + '-' + self.spec.version + '/' + elif url.endswith('.json') or url.endswith('.html'): + # support extension url + url = url.replace('.json', '-' + self.spec.version + '.json') \ + .replace('.html', '-' + self.spec.version + '.html') + else: + url += '-' + self.spec.version + return url + def swagger_json(self): return flask.jsonify(self.spec.to_dict()) def swagger_ui(self): - return flask.render_template('swagger-ui.html') + return flask.render_template('swagger-ui.html', + blueprint_name=self.blueprint_name) def register_existing_resources(self): for name, rule in self.app.view_functions.items(): diff --git a/flask_apispec/templates/swagger-ui.html b/flask_apispec/templates/swagger-ui.html index 3752e62..7a89942 100644 --- a/flask_apispec/templates/swagger-ui.html +++ b/flask_apispec/templates/swagger-ui.html @@ -3,19 +3,19 @@ Swagger UI - - - + + +
 
- - + + diff --git a/tests/test_extension.py b/tests/test_extension.py index f1f5a7a..2243cc9 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import random import pytest from flask import Blueprint @@ -98,3 +99,29 @@ def test_apispec_config(self, app): assert docs.spec.title == 'test-extension' assert docs.spec.version == '2.1' assert docs.spec.openapi_version == '2.0' + + def test_make_url_with_suffix_version_with_version(self, app): + app.config['APISPEC_VERSION'] = 'v2' + docs = FlaskApiSpec(app, support_multiple_version=True) + + assert docs.make_url_with_suffix_version('/swagger/') == '/swagger-v2/' + assert docs.make_url_with_suffix_version('/swagger-ui/') == '/swagger-ui-v2/' + assert docs.make_url_with_suffix_version('/swagger.json') == '/swagger-v2.json' + assert docs.make_url_with_suffix_version('/swagger.html') == '/swagger-v2.html' + assert docs.make_url_with_suffix_version('/swagger') == '/swagger-v2' + + def test_urls_with_support_multiple_version(self, app, client): + app.config['APISPEC_VERSION'] = 'v2' + docs = FlaskApiSpec(app, support_multiple_version=True) + res = client.get('/swagger-v2/') + assert res.json == docs.spec.to_dict() + client.get('/swagger-ui-v2/') + + def test_support_multiple_version_by_changing_blueprint_name(self, app): + version = 'v{}'.format(random.randint(1, 5)) + app.config['APISPEC_VERSION'] = version + docs = FlaskApiSpec(app, support_multiple_version=True) + assert docs.blueprint_name == 'flask-apispec-' + version + + docs_without_support_multiple_version = FlaskApiSpec(app) + assert docs_without_support_multiple_version.blueprint_name == 'flask-apispec' From 6aae40e1cf84aadfa6a25ca989a78ad5c4d6a673 Mon Sep 17 00:00:00 2001 From: Hat Dao Date: Mon, 8 Apr 2019 11:32:00 +0700 Subject: [PATCH 2/2] Fix base test cases --- tests/test_openapi.py | 2 +- tests/test_views.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 11752b6..afa00fa 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -154,7 +154,7 @@ def path(self, app, spec, function_view): return spec._paths['/bands/{band_id}/'] def test_responses(self, schemas, path): - response = path['delete']['responses'][204] + response = path['delete']['responses']['204'] assert response['description'] == 'a deleted band' assert response['schema'] == {} diff --git a/tests/test_views.py b/tests/test_views.py index 23e8365..cc12a1f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -271,8 +271,15 @@ def test_schemas_none(self, app, client, models, schemas): class ConcreteResource(MethodResource): @marshal_with(None, code=204) def delete(self, **kwargs): - return make_response('', 204) + from flask import Response + response = Response(None, 204) + + # remove content-type as webtest raises error for this + # https://github.com/Pylons/webtest/blob/master/webtest/lint.py#L543 + response.headers.pop('Content-Type', None) + return response app.add_url_rule('//', view_func=ConcreteResource.as_view('concrete')) res = client.delete('/5/') + assert res.status_code == 204 assert res.body == b''