Rewrite schema generation (#1)

* Completeley rewritten schema generation
* Added support for python 2.7 and 3.4
* Restructured testing and build configuration
* Added nested request schemas

This rewrite completely replaces the public interface of the django rest schema generation library, so 
further changes will be needed to re-enable and further extend the customization points one might want.
This commit is contained in:
Cristi Vîjdea
2017-12-05 19:46:02 +01:00
committed by GitHub
parent 5658910711
commit dce00156d5
55 changed files with 1512 additions and 346 deletions
+28 -2
View File
@@ -1,13 +1,14 @@
import pytest
from ruamel import yaml
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
from ruamel import yaml
@pytest.fixture
def generator():
return OpenAPISchemaGenerator(
info=openapi.Info("Test generator", "v1"),
info=openapi.Info(title="Test generator", default_version="v1"),
version="v2",
)
@@ -27,3 +28,28 @@ def swagger_dict():
swagger = generator().get_schema(None, True)
json_bytes = codec_yaml().encode(swagger)
return yaml.safe_load(json_bytes.decode('utf-8'))
@pytest.fixture
def validate_schema():
def validate_schema(swagger):
from flex.core import parse as validate_flex
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
validate_flex(swagger)
validate_ssv(swagger)
return validate_schema
@pytest.fixture
def bad_settings():
from drf_swagger.app_settings import swagger_settings, SWAGGER_DEFAULTS
bad_security = {
'bad': {
'bad_attribute': 'should not be accepted'
}
}
SWAGGER_DEFAULTS['SECURITY_DEFINITIONS'].update(bad_security)
yield swagger_settings
del SWAGGER_DEFAULTS['SECURITY_DEFINITIONS']['bad']
+6
View File
@@ -0,0 +1,6 @@
def test_operation_docstrings(swagger_dict):
users_list = swagger_dict['paths']['/users/']
assert users_list['get']['description'] == "UserList cbv classdoc"
users_detail = swagger_dict['paths']['/users/{id}/']
assert users_detail['get']['description'] == "user_detail fbv docstring"
+22
View File
@@ -0,0 +1,22 @@
def test_appropriate_status_codes(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert '200' in snippets_list['get']['responses']
assert '201' in snippets_list['post']['responses']
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert '200' in snippets_detail['get']['responses']
assert '200' in snippets_detail['put']['responses']
assert '200' in snippets_detail['patch']['responses']
assert '204' in snippets_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert snippets_list['get']['description'] == "SnippetList classdoc"
assert snippets_list['post']['description'] == "post method docstring"
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
assert snippets_detail['put']['description'] == "put class docstring"
assert snippets_detail['patch']['description'] == "patch method docstring"
assert snippets_detail['delete']['description'] == "delete method docstring"
+29
View File
@@ -0,0 +1,29 @@
def test_appropriate_status_codes(swagger_dict):
articles_list = swagger_dict['paths']['/articles/']
assert '200' in articles_list['get']['responses']
assert '201' in articles_list['post']['responses']
articles_detail = swagger_dict['paths']['/articles/{slug}/']
assert '200' in articles_detail['get']['responses']
assert '200' in articles_detail['put']['responses']
assert '200' in articles_detail['patch']['responses']
assert '204' in articles_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
articles_list = swagger_dict['paths']['/articles/']
assert articles_list['get']['description'] == "ArticleViewSet class docstring"
assert articles_list['post']['description'] == "ArticleViewSet class docstring"
articles_detail = swagger_dict['paths']['/articles/{slug}/']
assert articles_detail['get']['description'] == "retrieve class docstring"
assert articles_detail['put']['description'] == "update method docstring"
assert articles_detail['patch']['description'] == "ArticleViewSet class docstring"
assert articles_detail['delete']['description'] == "destroy method docstring"
articles_today = swagger_dict['paths']['/articles/today/']
assert articles_today['get']['description'] == "ArticleViewSet class docstring"
articles_image = swagger_dict['paths']['/articles/{slug}/image/']
assert articles_image['get']['description'] == "image method docstring"
assert articles_image['post']['description'] == "image method docstring"
+6 -12
View File
@@ -1,17 +1,10 @@
import json
import pytest
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
from ruamel import yaml
def validate_schema(swagger):
from flex.core import parse as validate_flex
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
validate_flex(swagger)
validate_ssv(swagger)
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
def test_schema_generates_without_errors(generator):
@@ -24,9 +17,10 @@ def test_schema_is_valid(generator, codec_yaml):
def test_invalid_schema_fails(codec_json):
# noinspection PyTypeChecker
bad_generator = OpenAPISchemaGenerator(
info=openapi.Info(
"Test generator", "v1",
title="Test generator", default_version="v1",
contact=openapi.Contact(name=69, email=[])
),
version="v2",
@@ -37,13 +31,13 @@ def test_invalid_schema_fails(codec_json):
codec_json.encode(swagger)
def test_json_codec_roundtrip(codec_json, generator):
def test_json_codec_roundtrip(codec_json, generator, validate_schema):
swagger = generator.get_schema(None, True)
json_bytes = codec_json.encode(swagger)
validate_schema(json.loads(json_bytes.decode('utf-8')))
def test_yaml_codec_roundtrip(codec_yaml, generator):
def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
swagger = generator.get_schema(None, True)
json_bytes = codec_yaml.encode(swagger)
validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
+1 -23
View File
@@ -1,24 +1,2 @@
def test_paths_not_empty(swagger_dict):
assert bool(swagger_dict['paths'])
def test_appropriate_status_codes(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert '200' in snippets_list['get']['responses']
assert '201' in snippets_list['post']['responses']
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert '200' in snippets_detail['get']['responses']
assert '200' in snippets_detail['put']['responses']
assert '200' in snippets_detail['patch']['responses']
assert '204' in snippets_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert snippets_list['get']['description'] == "SnippetList classdoc"
assert snippets_list['post']['description'] == "post method docstring"
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
assert snippets_detail['put']['description'] == "put class docstring"
assert snippets_detail['patch']['description'] == "patch method docstring"
assert snippets_detail['delete']['description'] == "delete method docstring"
assert len(swagger_dict['paths']) > 0
+39
View File
@@ -0,0 +1,39 @@
import json
from ruamel import yaml
def _validate_text_schema_view(client, validate_schema, path, loader):
response = client.get(path)
assert response.status_code == 200
validate_schema(loader(response.content.decode('utf-8')))
def _validate_ui_schema_view(client, path, string):
response = client.get(path)
assert response.status_code == 200
assert string in response.content.decode('utf-8')
def test_swagger_json(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.json", json.loads)
def test_swagger_yaml(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load)
def test_exception_middleware(client, bad_settings):
response = client.get('/swagger.json')
assert response.status_code == 500
assert 'errors' in json.loads(response.content.decode('utf-8'))
def test_swagger_ui(client, validate_schema):
_validate_ui_schema_view(client, '/swagger/', 'swagger-ui-dist/swagger-ui-bundle.js')
_validate_text_schema_view(client, validate_schema, '/swagger/?format=openapi', json.loads)
def test_redoc(client, validate_schema):
_validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js')
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
+53
View File
@@ -0,0 +1,53 @@
from drf_swagger import openapi
def test_vendor_extensions():
"""Any attribute starting with x_ should map to a vendor property of the form x-camelCase"""
sd = openapi.SwaggerDict(x_vendor_ext_1='test')
sd.x_vendor_ext_2 = 'test'
assert 'x-vendorExt1' in sd
assert sd.x_vendor_ext_1 == 'test'
assert sd['x-vendorExt2'] == 'test'
del sd.x_vendor_ext_1
assert 'x-vendorExt1' not in sd
def test_ref():
"""The attribute 'ref' maps to the swagger key '$ref'"""
sd = openapi.SwaggerDict(ref='reftest')
assert '$ref' in sd
assert sd['$ref'] == sd.ref == 'reftest'
del sd['$ref']
assert not hasattr(sd, 'ref')
def test_leading_underscore_ignored():
"""Attributes with a leading underscore are set on the object as-is and are not added to its dict form"""
sd = openapi.SwaggerDict(_private_attr_1='not_camelised')
initial_len = len(sd)
sd._nope = 'not camelised either'
assert len(sd) == initial_len
assert 'privateAttr1' not in sd
assert sd._private_attr_1 == 'not_camelised'
assert '_private_attr_1' not in sd
assert hasattr(sd, '_nope')
del sd._nope
assert not hasattr(sd, '_nope')
def test_trailing_underscore_stripped():
"""Trailing underscores are stripped when converting attribute names.
This allows, for example, python keywords to function as SwaggerDict attributes."""
sd = openapi.SwaggerDict(trailing_underscore_='trailing')
sd.in_ = 'trailing'
assert 'in' in sd
assert 'trailingUnderscore' in sd
assert sd.trailing_underscore == sd['in']
assert hasattr(sd, 'in___')
del sd.in_
assert 'in' not in sd
assert not hasattr(sd, 'in__')