Prepare for 1.1.0 (#30)

* refactor the view inspection process to be more modular and allow recursive customization
* add operation_id argument to @swagger_auto_
* add inspections for min/max validators
* add support for URLPathVersioning and NamespaceVersioning
* integrate with djangorestframework-camel-case
* fix bugs, improve tests and documentation
This commit is contained in:
Cristi Vîjdea
2017-12-26 22:50:59 +01:00
committed by GitHub
parent 73adc49b2c
commit c89f96fcb0
52 changed files with 2167 additions and 854 deletions
+4 -3
View File
@@ -1,14 +1,15 @@
import copy
import json
import os
from collections import OrderedDict
import pytest
from django.contrib.auth.models import User
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
from ruamel import yaml
from drf_yasg import openapi, codecs
from drf_yasg.codecs import yaml_sane_load
from drf_yasg.generators import OpenAPISchemaGenerator
@@ -47,7 +48,7 @@ def swagger(mock_schema_request):
@pytest.fixture
def swagger_dict(swagger):
json_bytes = codec_json().encode(swagger)
return json.loads(json_bytes.decode('utf-8'))
return json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict)
@pytest.fixture
@@ -79,4 +80,4 @@ def redoc_settings(settings):
@pytest.fixture
def reference_schema():
with open(os.path.join(os.path.dirname(__file__), 'reference.yaml')) as reference:
return yaml.safe_load(reference)
return yaml_sane_load(reference)
+49 -18
View File
@@ -20,7 +20,7 @@ paths:
parameters:
- name: title
in: query
description: ''
description: Filter the returned list by title
required: false
type: string
- name: ordering
@@ -89,7 +89,7 @@ paths:
parameters:
- name: title
in: query
description: ''
description: Filter the returned list by title
required: false
type: string
- name: ordering
@@ -249,7 +249,7 @@ paths:
parameters: []
/snippets/:
get:
operationId: snippets_list
operationId: snippetsList
description: SnippetList classdoc
parameters: []
responses:
@@ -264,7 +264,7 @@ paths:
tags:
- snippets
post:
operationId: snippets_create
operationId: snippetsCreate
description: post method docstring
parameters:
- name: data
@@ -284,7 +284,7 @@ paths:
parameters: []
/snippets/{id}/:
get:
operationId: snippets_read
operationId: snippetsRead
description: SnippetDetail classdoc
parameters: []
responses:
@@ -297,7 +297,7 @@ paths:
tags:
- snippets
put:
operationId: snippets_update
operationId: snippetsUpdate
description: put class docstring
parameters:
- name: data
@@ -315,7 +315,7 @@ paths:
tags:
- snippets
patch:
operationId: snippets_partial_update
operationId: snippetsPartialUpdate
description: patch method docstring
parameters:
- name: data
@@ -333,7 +333,7 @@ paths:
tags:
- snippets
delete:
operationId: snippets_delete
operationId: snippetsDelete
description: delete method docstring
parameters: []
responses:
@@ -404,7 +404,7 @@ paths:
tags:
- users
patch:
operationId: users_partial_update
operationId: users_dummy
description: dummy operation
parameters: []
responses:
@@ -466,6 +466,7 @@ definitions:
title:
description: title model help_text
type: string
maxLength: 255
author:
description: The ID of the user that created this article; if none is provided,
defaults to the currently logged in user.
@@ -474,11 +475,13 @@ definitions:
body:
description: body serializer help_text
type: string
maxLength: 5000
slug:
description: slug model help_text
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 50
date_created:
type: string
format: date-time
@@ -509,14 +512,16 @@ definitions:
readOnly: true
Project:
required:
- project_name
- github_repo
- projectName
- githubRepo
type: object
properties:
project_name:
projectName:
title: Project name
description: Name of the project
type: string
github_repo:
githubRepo:
title: Github repo
description: Github repository of the project
type: string
Snippet:
@@ -526,29 +531,38 @@ definitions:
type: object
properties:
id:
title: Id
description: id serializer help text
type: integer
readOnly: true
owner:
title: Owner
description: The ID of the user that created this snippet; if none is provided,
defaults to the currently logged in user.
type: integer
default: 1
owner_as_string:
ownerAsString:
description: The ID of the user that created this snippet.
type: string
readOnly: true
title: Owner as string
title:
title: Title
type: string
maxLength: 100
code:
title: Code
type: string
linenos:
title: Linenos
type: boolean
language:
title: Language
description: Sample help text for language
type: object
properties:
name:
title: Name
description: The name of the programming language
type: string
enum:
@@ -988,6 +1002,7 @@ definitions:
- zephir
default: python
styles:
title: Styles
type: array
items:
type: string
@@ -1024,19 +1039,22 @@ definitions:
default:
- friendly
lines:
title: Lines
type: array
items:
type: integer
example_projects:
exampleProjects:
title: Example projects
type: array
items:
$ref: '#/definitions/Project'
readOnly: true
difficulty_factor:
difficultyFactor:
title: Difficulty factor
description: this is here just to test FloatField
type: number
default: 6.9
readOnly: true
default: 6.9
UserSerializerrr:
required:
- username
@@ -1045,42 +1063,55 @@ definitions:
type: object
properties:
id:
title: ID
type: integer
readOnly: true
username:
title: Username
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only.
type: string
pattern: ^[\w.@+-]+$
maxLength: 150
email:
title: Email address
type: string
format: email
maxLength: 254
articles:
title: Articles
type: array
items:
type: integer
uniqueItems: true
snippets:
title: Snippets
type: array
items:
type: integer
uniqueItems: true
last_connected_ip:
title: Last connected ip
description: i'm out of ideas
type: string
format: ipv4
readOnly: true
last_connected_at:
title: Last connected at
description: really?
type: string
format: date
readOnly: true
article_slugs:
title: Article slugs
type: array
items:
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+\Z
readOnly: true
uniqueItems: true
readOnly: true
uniqueItems: true
securityDefinitions:
basic:
type: basic
-17
View File
@@ -1,17 +0,0 @@
from drf_yasg import openapi
def test_operation_docstrings(swagger_dict):
users_list = swagger_dict['paths']['/users/']
assert users_list['get']['description'] == "UserList cbv classdoc"
assert users_list['post']['description'] == "apiview post description override"
users_detail = swagger_dict['paths']['/users/{id}/']
assert users_detail['get']['description'] == "user_detail fbv docstring"
assert users_detail['put']['description'] == "user_detail fbv docstring"
def test_parameter_docstrings(swagger_dict):
users_detail = swagger_dict['paths']['/users/{id}/']
assert users_detail['get']['parameters'][0]['description'] == "test manual param"
assert users_detail['put']['parameters'][0]['in'] == openapi.IN_BODY
-22
View File
@@ -1,22 +0,0 @@
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
@@ -1,29 +0,0 @@
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'] == "description from swagger_auto_schema via method_decorator"
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'] == "partial_update description override"
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 GET description override"
assert articles_image['post']['description'] == "image method docstring"
+37 -4
View File
@@ -1,13 +1,46 @@
from collections import OrderedDict
from datadiff.tools import assert_equal
from drf_yasg.codecs import yaml_sane_dump
from drf_yasg.inspectors import FieldInspector, SerializerInspector, PaginatorInspector, FilterInspector
def test_reference_schema(swagger_dict, reference_schema):
swagger_dict = dict(swagger_dict)
reference_schema = dict(reference_schema)
swagger_dict = OrderedDict(swagger_dict)
reference_schema = OrderedDict(reference_schema)
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
for attr in ignore:
swagger_dict.pop(attr, None)
reference_schema.pop(attr, None)
# formatted better than pytest diff
assert_equal(swagger_dict, reference_schema)
# print diff between YAML strings because it's prettier
assert_equal(yaml_sane_dump(swagger_dict, binary=False), yaml_sane_dump(reference_schema, binary=False))
class NoOpFieldInspector(FieldInspector):
pass
class NoOpSerializerInspector(SerializerInspector):
pass
class NoOpFilterInspector(FilterInspector):
pass
class NoOpPaginatorInspector(PaginatorInspector):
pass
def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema):
from drf_yasg import app_settings
def set_inspectors(inspectors, setting_name):
swagger_settings[setting_name] = inspectors + app_settings.SWAGGER_DEFAULTS[setting_name]
set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS')
set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS')
set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS')
test_reference_schema(swagger_dict, reference_schema)
+5 -4
View File
@@ -1,9 +1,10 @@
import json
from collections import OrderedDict
import pytest
from ruamel import yaml
from drf_yasg import openapi, codecs
from drf_yasg.codecs import yaml_sane_load
from drf_yasg.generators import OpenAPISchemaGenerator
@@ -35,12 +36,12 @@ def test_yaml_codec_roundtrip(codec_yaml, swagger, validate_schema):
yaml_bytes = codec_yaml.encode(swagger)
assert b'omap' not in yaml_bytes # ensure no ugly !!omap is outputted
assert b'&id' not in yaml_bytes and b'*id' not in yaml_bytes # ensure no YAML references are generated
validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8')))
validate_schema(yaml_sane_load(yaml_bytes.decode('utf-8')))
def test_yaml_and_json_match(codec_yaml, codec_json, swagger):
yaml_schema = yaml.safe_load(codec_yaml.encode(swagger).decode('utf-8'))
json_schema = json.loads(codec_json.encode(swagger).decode('utf-8'))
yaml_schema = yaml_sane_load(codec_yaml.encode(swagger).decode('utf-8'))
json_schema = json.loads(codec_json.encode(swagger).decode('utf-8'), object_pairs_hook=OrderedDict)
assert yaml_schema == json_schema
-2
View File
@@ -1,2 +0,0 @@
def test_paths_not_empty(swagger_dict):
assert len(swagger_dict['paths']) > 0
+5 -4
View File
@@ -2,7 +2,8 @@ import json
from collections import OrderedDict
import pytest
from ruamel import yaml
from drf_yasg.codecs import yaml_sane_load
def _validate_text_schema_view(client, validate_schema, path, loader):
@@ -22,10 +23,10 @@ def test_swagger_json(client, validate_schema):
def test_swagger_yaml(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load)
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml_sane_load)
def test_exception_middleware(client, swagger_settings):
def test_exception_middleware(client, swagger_settings, db):
swagger_settings['SECURITY_DEFINITIONS'] = {
'bad': {
'bad_attribute': 'should not be accepted'
@@ -70,5 +71,5 @@ def test_caching(client, validate_schema):
@pytest.mark.urls('urlconfs.non_public_urls')
def test_non_public(client):
response = client.get('/private/swagger.yaml')
swagger = yaml.safe_load(response.content.decode('utf-8'))
swagger = yaml_sane_load(response.content.decode('utf-8'))
assert len(swagger['paths']) == 0
+56
View File
@@ -0,0 +1,56 @@
import pytest
from drf_yasg.codecs import yaml_sane_load
def _get_versioned_schema(prefix, client, validate_schema):
response = client.get(prefix + 'swagger.yaml')
assert response.status_code == 200
swagger = yaml_sane_load(response.content.decode('utf-8'))
validate_schema(swagger)
assert prefix + 'snippets/' in swagger['paths']
return swagger
def _check_v1(swagger, prefix):
assert swagger['info']['version'] == '1.0'
versioned_post = swagger['paths'][prefix + 'snippets/']['post']
assert versioned_post['responses']['201']['schema']['$ref'] == '#/definitions/Snippet'
assert 'v2field' not in swagger['definitions']['Snippet']['properties']
def _check_v2(swagger, prefix):
assert swagger['info']['version'] == '2.0'
versioned_post = swagger['paths'][prefix + 'snippets/']['post']
assert versioned_post['responses']['201']['schema']['$ref'] == '#/definitions/SnippetV2'
assert 'v2field' in swagger['definitions']['SnippetV2']['properties']
v2field = swagger['definitions']['SnippetV2']['properties']['v2field']
assert v2field['description'] == 'version 2.0 field'
@pytest.mark.urls('urlconfs.url_versioning')
def test_url_v1(client, validate_schema):
prefix = '/versioned/url/v1.0/'
swagger = _get_versioned_schema(prefix, client, validate_schema)
_check_v1(swagger, prefix)
@pytest.mark.urls('urlconfs.url_versioning')
def test_url_v2(client, validate_schema):
prefix = '/versioned/url/v2.0/'
swagger = _get_versioned_schema(prefix, client, validate_schema)
_check_v2(swagger, prefix)
@pytest.mark.urls('urlconfs.ns_versioning')
def test_ns_v1(client, validate_schema):
prefix = '/versioned/ns/v1.0/'
swagger = _get_versioned_schema(prefix, client, validate_schema)
_check_v1(swagger, prefix)
@pytest.mark.urls('urlconfs.ns_versioning')
def test_ns_v2(client, validate_schema):
prefix = '/versioned/ns/v2.0/'
swagger = _get_versioned_schema(prefix, client, validate_schema)
_check_v2(swagger, prefix)
+26
View File
@@ -0,0 +1,26 @@
from django.conf.urls import url
from rest_framework import generics, versioning
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
class SnippetList(generics.ListCreateAPIView):
"""SnippetList classdoc"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
versioning_class = versioning.NamespaceVersioning
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def post(self, request, *args, **kwargs):
"""post method docstring"""
return super(SnippetList, self).post(request, *args, **kwargs)
app_name = 'test_ns_versioning'
urlpatterns = [
url(r"^$", SnippetList.as_view())
]
+23
View File
@@ -0,0 +1,23 @@
from django.conf.urls import url
from rest_framework import fields
from snippets.serializers import SnippetSerializer
from .ns_version1 import SnippetList as SnippetListV1
class SnippetSerializerV2(SnippetSerializer):
v2field = fields.IntegerField(help_text="version 2.0 field")
class Meta:
ref_name = 'SnippetV2'
class SnippetListV2(SnippetListV1):
serializer_class = SnippetSerializerV2
app_name = 'test_ns_versioning'
urlpatterns = [
url(r"^$", SnippetListV2.as_view())
]
+24
View File
@@ -0,0 +1,24 @@
from django.conf.urls import url, include
from rest_framework import versioning
from testproj.urls import SchemaView
from . import ns_version1, ns_version2
VERSION_PREFIX_NS = r"^versioned/ns/"
class VersionedSchemaView(SchemaView):
versioning_class = versioning.NamespaceVersioning
schema_patterns = [
url(r'swagger(?P<format>.json|.yaml)$', VersionedSchemaView.without_ui(), name='ns-schema')
]
urlpatterns = [
url(VERSION_PREFIX_NS + r"v1.0/snippets/", include(ns_version1, namespace='1.0')),
url(VERSION_PREFIX_NS + r"v2.0/snippets/", include(ns_version2, namespace='2.0')),
url(VERSION_PREFIX_NS + r'v1.0/', include((schema_patterns, '1.0'))),
url(VERSION_PREFIX_NS + r'v2.0/', include((schema_patterns, '2.0'))),
]
+48
View File
@@ -0,0 +1,48 @@
from django.conf.urls import url
from rest_framework import generics, versioning, fields
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from testproj.urls import SchemaView
class SnippetSerializerV2(SnippetSerializer):
v2field = fields.IntegerField(help_text="version 2.0 field")
class Meta:
ref_name = 'SnippetV2'
class SnippetList(generics.ListCreateAPIView):
"""SnippetList classdoc"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
versioning_class = versioning.URLPathVersioning
def get_serializer_class(self):
context = self.get_serializer_context()
request = context['request']
if int(float(request.version)) >= 2:
return SnippetSerializerV2
else:
return SnippetSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def post(self, request, *args, **kwargs):
"""post method docstring"""
return super(SnippetList, self).post(request, *args, **kwargs)
VERSION_PREFIX_URL = r"^versioned/url/v(?P<version>1.0|2.0)/"
class VersionedSchemaView(SchemaView):
versioning_class = versioning.URLPathVersioning
urlpatterns = [
url(VERSION_PREFIX_URL + r"snippets/$", SnippetList.as_view()),
url(VERSION_PREFIX_URL + r'swagger(?P<format>.json|.yaml)$', VersionedSchemaView.without_ui(), name='vschema-json'),
]