Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Name Type Default Description
``APIFAIRY_TITLE`` String No title The API's title.
``APIFAIRY_VERSION`` String No version The API's version.
``APIFAIRY_APISPEC_PATH`` String */apispec.json* The URL path where the JSON OpenAPI specification for this project is served.
``APIFAIRY_APISPEC_VERSION`` String ``None`` The version of the OpenAPI specification to generate for this project.
``APIFAIRY_APISPEC_DECORATORS`` List [] A list of decorators to apply to the JSON OpenAPI endpoint.
``APIFAIRY_UI`` String redoc The documentation format to use. Supported formats are "redoc", "swagger_ui", "rapidoc" and "elements".
``APIFAIRY_UI_PATH`` String */docs* The URL path where the documentation is served.
Expand Down
21 changes: 16 additions & 5 deletions src/apifairy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
except ImportError: # pragma: no cover
HTTPBasicAuth = None
HTTPTokenAuth = None
from packaging.version import Version
from werkzeug.http import HTTP_STATUS_CODES

from apifairy.decorators import _webhooks
Expand Down Expand Up @@ -46,6 +47,7 @@ def init_app(self, app):
self.version = app.config.get('APIFAIRY_VERSION', 'No version')
self.apispec_path = app.config.get('APIFAIRY_APISPEC_PATH',
'/apispec.json')
self.apispec_version = app.config.get('APIFAIRY_APISPEC_VERSION', None)
self.apispec_decorators = app.config.get(
'APIFAIRY_APISPEC_DECORATORS', [])
self.ui = app.config.get('APIFAIRY_UI', 'redoc')
Expand Down Expand Up @@ -147,10 +149,19 @@ def resolver(schema):
tags[name] = tag
tag_list = [tags[name] for name in tag_names]
ma_plugin = MarshmallowPlugin(schema_name_resolver=resolver)
apispec_version = self.apispec_version
if apispec_version is None:
apispec_version = '3.1.0' if _webhooks else '3.0.3'
version = Version(apispec_version)
if version < Version('3.0.3'):
raise RuntimeError("Must use at openapi version '3.0.3' or newer")
elif version < Version('3.1.0') and _webhooks:
raise RuntimeError("Must use at least openapi version '3.1.0' "
'when using the @webhook decorator')
spec = APISpec(
title=self.title,
version=self.version,
openapi_version='3.1.0' if _webhooks else '3.0.3',
openapi_version=apispec_version,
plugins=[ma_plugin],
info=info,
servers=servers,
Expand Down Expand Up @@ -188,11 +199,11 @@ def resolver(schema):
else: # pragma: no cover
raise RuntimeError('Unknown authentication scheme')
if name in auth_names:
v = 2
new_name = f'{name}_{v}'
apispec_version = 2
new_name = f'{name}_{apispec_version}'
while new_name in auth_names: # pragma: no cover
v += 1
new_name = f'{name}_{v}'
apispec_version += 1
new_name = f'{name}_{apispec_version}'
name = new_name
auth_names.append(name)
security = {}
Expand Down
57 changes: 57 additions & 0 deletions tests/test_apifairy.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,50 @@ def test_no_apispec_path(self):
rv = client.get('/apispec.json')
assert rv.status_code == 404

def test_custom_apispec_version(self):
app, _ = self.create_app(config={'APIFAIRY_APISPEC_VERSION': '3.1.0'})

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 200
assert set(rv.json.keys()) == {
'openapi', 'info', 'servers', 'paths', 'tags'}
assert rv.json['openapi'] == '3.1.0'

def test_custom_apispec_default_version(self):
app, _ = self.create_app()

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 200
assert set(rv.json.keys()) == {
'openapi', 'info', 'servers', 'paths', 'tags'}
assert rv.json['openapi'] == '3.0.3'

def test_custom_apispec_invalid_version_old(self):
app, _ = self.create_app(
config={'APIFAIRY_APISPEC_VERSION': '3.0.2'})

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 500

def test_custom_apispec_invalid_version_new(self):
app, _ = self.create_app(
config={'APIFAIRY_APISPEC_VERSION': '6.1.0'})

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 500

def test_custom_apispec_non_semver_version(self):
app, _ = self.create_app(
config={'APIFAIRY_APISPEC_VERSION': 'invalid'})

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 500

def test_ui(self):
app, _ = self.create_app(config={'APIFAIRY_UI': 'swagger_ui'})

Expand Down Expand Up @@ -1040,6 +1084,19 @@ def blueprint_webhook():
assert 'blueprint_webhook' in rv.json['webhooks']
assert 'get' in rv.json['webhooks']['blueprint_webhook']

def test_webhook_invalid_apispec_version(self):
app, apifairy = self.create_app(
config={'APIFAIRY_APISPEC_VERSION': '3.0.3'})

@webhook
@body(Schema)
def unsupported_webhook():
pass

client = app.test_client()
rv = client.get('/apispec.json')
assert rv.status_code == 500

def test_webhook_duplicate(self):
app, apifairy = self.create_app()

Expand Down
Loading