Skip to content
This repository has been archived by the owner on Jan 11, 2021. It is now read-only.

Commit

Permalink
Extend OpenAPI codec to accept extra dict
Browse files Browse the repository at this point in the history
Fixes regression as of 2.0.6 which sees customizations added to the
Document, but ignored by the codec. This is a quick workaround to allow
passing in extra OpenAPI spec data until we can find a better solution.

(Fixes #561)
  • Loading branch information
marcgibbons committed Oct 16, 2016
1 parent fd1dd3c commit e0f5439
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 167 deletions.
51 changes: 22 additions & 29 deletions rest_framework_swagger/renderers.py
@@ -1,14 +1,27 @@

import coreapi
from coreapi.compat import force_bytes
from django.shortcuts import render, resolve_url
from openapi_codec import OpenAPICodec
from openapi_codec import OpenAPICodec as _OpenAPICodec
from openapi_codec.encode import generate_swagger_object
from rest_framework.renderers import BaseRenderer, JSONRenderer
from rest_framework import status
import simplejson as json

from .settings import swagger_settings


class OpenAPICodec(_OpenAPICodec):
def encode(self, document, extra=None, **options):
if not isinstance(document, coreapi.Document):
raise TypeError('Expected a `coreapi.Document` instance')

data = generate_swagger_object(document)
if isinstance(extra, dict):
data.update(extra)

return force_bytes(json.dumps(data))


class OpenAPIRenderer(BaseRenderer):
media_type = 'application/openapi+json'
charset = None
Expand All @@ -17,39 +30,19 @@ class OpenAPIRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
if renderer_context['response'].status_code != status.HTTP_200_OK:
return JSONRenderer().render(data)
extra = self.get_customizations()

assert isinstance(data, coreapi.Document), (
'Expected a coreapi.Document, but received %s instead.' %
type(data)
)
document = self.get_document(data, renderer_context)

return OpenAPICodec().encode(document)

def get_document(self, data, renderer_context):
title = data.title
url = data.url
content = dict(data)
self.add_customizations(content, renderer_context)

return coreapi.Document(title=title, url=url, content=content)
return OpenAPICodec().encode(data, extra=extra)

def add_customizations(self, data, renderer_context):
def get_customizations(self):
"""
Adds settings, overrides, etc. to the specification.
"""
self.add_security_definitions(data)
if not data.get('host'):
data['host'] = self.get_host(renderer_context)
data = {}
if swagger_settings.SECURITY_DEFINITIONS:
data['securityDefinitions'] = swagger_settings.SECURITY_DEFINITIONS

def add_security_definitions(self, data):
if not swagger_settings.SECURITY_DEFINITIONS:
return

data['securityDefinitions'] = swagger_settings.SECURITY_DEFINITIONS

def get_host(self, renderer_context):
return renderer_context['request'].get_host()
return data


class SwaggerUIRenderer(BaseRenderer):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -44,7 +44,7 @@
version=VERSION,
install_requires=[
'coreapi>=2.0.8',
'openapi-codec>=1.1.3',
'openapi-codec>=1.1.5',
'simplejson'
],
packages=['rest_framework_swagger'],
Expand Down
191 changes: 54 additions & 137 deletions tests/renderers/test_openapi_renderer.py
@@ -1,10 +1,49 @@
import coreapi
from coreapi.compat import force_bytes
from django.test import TestCase
from rest_framework_swagger import renderers
import simplejson as json

from ..compat.mock import MagicMock, patch


class TestOpenAPICodec(TestCase):
def setUp(self):
self.sut = renderers.OpenAPICodec().encode

def test_encode_without_document_instance_raises_assertion_error(self):
"""
Given that the data is not a CoreAPI Document instance,
an assertion error should be raised.
"""
with self.assertRaises(TypeError) as cx:
data = MagicMock()
self.sut(data)

expected = 'Expected a `coreapi.Document` instance'
self.assertEqual(expected, str(cx.exception))

def test_encode_generates_swagger_object_when_given_valid_document(self):
expected = {'fizz': 'buzz'}
with patch(
'rest_framework_swagger.renderers.generate_swagger_object',
return_value={'fizz': 'buzz'}
):
result = self.sut(coreapi.Document())

self.assertEqual(force_bytes(json.dumps(expected)), result)

def test_encode_adds_extra_data_provided_to_swagger_object(self):
expected = {'foo': 'bar'}
with patch(
'rest_framework_swagger.renderers.generate_swagger_object',
return_value={}
):
result = self.sut(coreapi.Document(), extra=expected)

self.assertEqual(force_bytes(json.dumps(expected)), result)


class TestOpenAPIRenderer(TestCase):
def setUp(self):
self.sut = renderers.OpenAPIRenderer()
Expand All @@ -21,19 +60,17 @@ def test_charset(self):
def test_format(self):
self.assertEqual('openapi', self.sut.format)

@patch('openapi_codec.OpenAPICodec.encode')
def test_render(self, encode_mock):
@patch('rest_framework_swagger.renderers.OpenAPICodec.encode')
def test_render_encodes_customizations(self, encode_mock):
data = coreapi.Document()
renderer_context = {
'request': MagicMock(),
'response': MagicMock(status_code=200)
}
with patch.object(self.sut, 'get_document') as mock:
result = self.sut.render(data, renderer_context=renderer_context)
with patch.object(self.sut, 'get_customizations') as mock:
self.sut.render(data, renderer_context=renderer_context)

mock.assert_called_once_with(data, renderer_context)
encode_mock.assert_called_once_with(mock.return_value)
self.assertEqual(result, encode_mock.return_value)
encode_mock.assert_called_once_with(data, extra=mock.return_value)

def test_render_if_response_is_not_200(self):
"""
Expand All @@ -47,144 +84,24 @@ def test_render_if_response_is_not_200(self):

self.assertEqual(expected, result)

def test_render_raises_assertion_error(self):
"""
Given that the data is not a CoreAPI Document instance,
an assertion error should be raised.
"""
renderer_context = {
'request': MagicMock(),
'response': MagicMock(status_code=200)
}
with self.assertRaises(AssertionError) as cx:
data = MagicMock()
self.sut.render(data, renderer_context=renderer_context)

expected = (
'Expected a coreapi.Document, but received %s instead.' %
type(data)
)
self.assertEqual(expected, str(cx.exception))


class TestGetDocument(TestCase):
def setUp(self):
self.data = coreapi.Document(
title='Vandelay Inustries',
url='http://seinfeld.wikia.com/wiki/Vandelay_Industries',
content={'fizz': 'buzz'}
)
self.renderer_context = MagicMock()

self.customizations = {'foo': 'bar'}

def add_customizations(data, renderer_context):
data.update(self.customizations)

with patch.object(
renderers.OpenAPIRenderer,
'add_customizations',
side_effect=add_customizations
) as self.add_customizations_mock:
self.sut = renderers.OpenAPIRenderer().get_document(
data=self.data,
renderer_context=self.renderer_context
)

def test_document_title(self):
self.assertEqual(self.data.title, self.sut.title)

def test_document_url(self):
self.assertEqual(self.data.url, self.sut.url)

def test_add_customizations_called(self):
expected = dict(self.data, **self.customizations)
self.add_customizations_mock.assert_called_once_with(
expected,
self.renderer_context
)

self.assertDictEqual(expected, dict(self.sut.data))


class TestAddSecurityDefinitons(TestCase):
class TestGetCustomizations(TestCase):
def setUp(self):
self.sut = renderers.OpenAPIRenderer()
self.sut = renderers.OpenAPIRenderer().get_customizations

settings_patcher = patch(
'rest_framework_swagger.renderers.swagger_settings'
)
self.swagger_settings = settings_patcher.start()
self.addCleanup(settings_patcher.stop)

def test_add_customizations_adds_security_definitions(self):
data = MagicMock()
renderer_context = {'request': MagicMock()}
with patch.object(self.sut, 'add_security_definitions') as mock:
self.sut.add_customizations(data, renderer_context)

mock.assert_called_once_with(data)
def test_security_definitions_included_when_defined(self):
self.swagger_settings.SECURITY_DEFINITIONS = {'foo': 'bar'}
expected = {
'securityDefinitions': self.swagger_settings.SECURITY_DEFINITIONS
}
self.assertDictContainsSubset(expected, self.sut())

def test_add_security_definitions_when_none(self):
"""
Given that SECURITY_DEFINITIONS is set to None, security definitions
should not be added.
"""
data = {}
def test_security_definitions_not_present_when_none(self):
self.swagger_settings.SECURITY_DEFINITIONS = None
self.sut.add_security_definitions(data)
self.assertNotIn('securityDefinitions', data)

def test_add_security_definitions_when_defined(self):
"""
Given that SECURITY_DEFINITIONS is defined, the value should be
added to the data dictionary.
"""
data = {}
expected = {'foo': 'bar'}
self.swagger_settings.SECURITY_DEFINITIONS = expected
self.sut.add_security_definitions(data)

self.assertDictContainsSubset(
{'securityDefinitions': expected},
data
)


class TestAddRequestHost(TestCase):
def setUp(self):
self.sut = renderers.OpenAPIRenderer()

def test_add_customizations_sets_hosts_when_falsey(self):
"""
Given that the `host` on the OpenAPI spec is falsey,
the host from the renderer's view request should be used as
the value for this property.
"""
data = {'host': ''}
with patch.object(self.sut, 'get_host') as mock:
self.sut.add_customizations(data, renderer_context=MagicMock())

self.assertEqual(mock.return_value, data['host'])

def test_add_customizations_preserves_host_when_truthy(self):
"""
Given that the `host` is already specified on the OpenAPI spec,
this value should be preserved.
"""
data = {'host': 'vandelayindustries.com'}
with patch.object(self.sut, 'get_host') as mock:
self.sut.add_customizations(data, renderer_context=MagicMock())
mock.assert_not_called()

self.assertEqual('vandelayindustries.com', data['host'])

def test_get_host(self):
expected = 'kramerica.org'
request = MagicMock()
request.get_host.return_value = expected

renderer_context = {'request': request}
result = self.sut.get_host(renderer_context)

self.assertEqual(expected, result)
self.assertNotIn('securityDefinitions', self.sut())

0 comments on commit e0f5439

Please sign in to comment.