8000 Implement dump on the OpenAPICodec. · waipbmtd/python-openapi-codec@9621035 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9621035

Browse files
committed
Implement dump on the OpenAPICodec.
1 parent 897ded7 commit 9621035

File tree

9 files changed

+233
-4
lines changed

9 files changed

+233
-4
lines changed

‎openapi_codec/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import json
2+
13
from coreapi.codecs.base import BaseCodec
2-
from coreapi.compat import urlparse
4+
from coreapi.compat import force_bytes, urlparse
35
from coreapi.document import Document, Link, Field
46
from coreapi.exceptions import ParseError
57
from openapi_codec.utils import _get_string, _get_dict, _get_list, _get_bool, get_strings, get_dicts
6-
import json
8+
from openapi_codec.converters import DocumentToOpenAPIConverter
79

810

911
__version__ = "0.0.3"
@@ -138,3 +140,7 @@ def load(self, bytes, base_url=None):
138140
raise ParseError('Top level node must be a document.')
139141

140142
return doc
143+
144+
def dump(self, document, **kwargs):
145+
converter = DocumentToOpenAPIConverter(document)
146+
return force_bytes(json.dumps(converter.convert()))
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from six.moves.urllib.parse import urlparse
2+
3+
4+
class DocumentToOpenAPIConverter(object):
5+
def __init__(self, document):
6+
self.document = document
7+
8+
def convert(self):
9+
return self._generate_swagger_object()
10+
11+
def _generate_swagger_object(self):
12+
"""
13+
Generates root of the Swagger spec.
14+
"""
15+
parsed_url = urlparse(self.document.url)
16+
17+
return {
18+
'swagger': '2.0',
19+
'info': self._get_info_object(),
20+
'paths': self._get_paths_object(),
21+
'host': parsed_url.netloc,
22+
}
23+
24+
def _get_info_object(self):
25+
return {
26+
'title': self.document.title,
27+
'version': '' # Required by the spec
28+
}
29+
30+
def _get_paths_object(self):
31+
paths = {}
32+
for tag, object_ in self.document.data.items():
33+
if not hasattr(object_, 'links'):
34+
continue
35+
36+
for link in object_.links.values():
37+
if link.url not in paths:
38+
paths[link.url] = {}
39+
40+
operation = self._get_operation(tag, link)
41+
paths[link.url].update({link.action: operation})
42+
43+
return paths
44+
45+
@classmethod
46+
def _get_operation(cls, tag, link):
47+
return {
48+
'tags': [tag],
49+
'description': link.description,
50+
'responses': cls._get_responses(link.action),
51+
'parameters': cls._get_parameters(link.fields)
52+
}
53+
54+
@classmethod
55+
def _get_parameters(cls, fields):
56+
"""
57+
Generates Swagger Parameter Item object.
58+
"""
59+
return [
60+
{
61+
'name': field.name,
62+
'required': field.required,
63+
'in': cls._convert_location_to_in(field.location),
64+
'description': field.description,
65+
'type': 'string'
66+
}
67+
for field in fields
68+
]
69+
70+
@classmethod
71+
def _convert_location_to_in(cls, location):
72+
"""
73+
Translates the CoreAPI field `location` into the Swagger `in`.
74+
The values are all the same with the exception of form -> formData.
75+
"""
76+
if location == 'form':
77+
return 'formData'
78+
79+
return location
80+
81+
@classmethod
82+
def _get_responses(cls, action):
83+
"""
84+
Returns minimally acceptable responses object based
85+
on action / method type.
86+
"""
87+
template = {'description': ''}
88+
if action == 'post':
89+
return {'201': template}
90+
if action == 'delete':
91+
return {'204': template}
92+
93+
return {'200': template}

‎requirements-unfrozen.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Package requirements
22
coreapi
3+
six
34

45
# Testing requirements
56
coverage

‎requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ pyflakes==1.2.3
1212
pytest==2.9.2
1313
requests==2.10.0
1414
simplejson==3.8.2
15+
six==1.10.0
1516
uritemplate==0.6

‎setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_package_data(package):
6161
author_email='tom@tomchristie.com',
6262
packages=get_packages('openapi_codec'),
6363
package_data=get_package_data('openapi_codec'),
64-
install_requires=[],
64+
install_requires=['six>=1.10.0'],
6565
classifiers=[
6666
'Intended Audience :: Developers',
6767
'License :: OSI Approved :: BSD License',

‎tests/__init__.py

Whitespace-only changes.

‎tests/compat.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
try:
2+
from unittest import mock
3+
except:
4+
import mock

‎tests/test_converters.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import uuid
2+
from unittest import TestCase
3+
4+
import six
5+
import coreapi
6+
from openapi_codec.converters import DocumentToOpenAPIConverter
7+
8+
from .compat import mock
9+
10+
11+
class TestGetInfoObject(TestCase):
12+
def setUp(self):
13+
self.document = coreapi.Document(title='Example API')
14+
converter = DocumentToOpenAPIConverter(self.document)
15+
self.sut = converter._get_info_object()
16+
17+
def test_title(self):
18+
self.assertDictContainsSubset({'title': self.document.title}, self.sut)
19+
20+
def test_version(self):
21+
"""
22+
Ensures that the version is provided since it is a required field.
23+
"""
24+
self.assertDictContainsSubset({'version': ''}, self.sut)
25+
26+
27+
class TestGetPathsObject(TestCase):
28+
def setUp(self):
29+
self.path = '/users'
30+
self.document = coreapi.Document(
31+
content={
32+
'users': {
33+
'create': coreapi.Link(
34+
action='post',
35+
url=self.path
36+
),
37+
'list': coreapi.Link(
38+
action='get',
39+
url=self.path
40+
)
41+
}
42+
}
43+
)
44+
self.sut = DocumentToOpenAPIConverter(self.document) \
45+
._get_paths_object()
46+
47+
def test_url_is_converted_to_key(self):
48+
self.assertIn(self.path, self.sut)
49+
50+
def test_actions_are_converted_to_keys_under_url(self):
51+
expected = [
52+
link.action for link in self.document.data['users'].values()
53+
]
54+
six.assertCountEqual(self, expected, self.sut[self.path].keys())
55+
56+
57+
class TestGetParameters(TestCase):
58+
def setUp(self):
59+
self.field = coreapi.Field(
60+
name='email',
61+
required='true',
62+
location='query',
63+
description='A valid email address.'
64+
)
65+
patcher = mock.patch.object(
66+
DocumentToOpenAPIConverter,
67+
'_convert_location_to_in'
68+
)
69+
self.location_mock = patcher.start()
70+
self.addCleanup(patcher.stop)
71+
72+
self.sut = DocumentToOpenAPIConverter \
73+
._get_parameters([self.field])[0]
74+
75+
def test_expected_fields(self):
76+
self.assertDictContainsSubset(
77+
{
78+
'name': self.field.name,
79+
'required': self.field.required,
80+
'in': self.location_mock.return_value,
81+
'description': self.field.description,
82+
'type': 'string' # Everything is a string for now.
83+
},
84+
self.sut
85+
)
86+
87+
88+
class TestConvertLocationToIn(TestCase):
89+
def setUp(self):
90+
self.sut = DocumentToOpenAPIConverter._convert_location_to_in
91+
92+
def test_form_is_converted_to_formdata(self):
93+
self.assertEqual('formData', self.sut('form'))
94+
95+
def test_random_string_is_returned_as_is(self):
96+
"""
97+
Asserts that any input (other than form) is returned as-is,
98+
since the Swagger Parameter object `in` property maps 1:1 with
99+
the Field.location property,
100+
"""
101+
expected = str(uuid.uuid4())
102+
self.assertEqual(expected, self.sut(expected))
103+
104+
105+
class TestGetResponses(TestCase):
106+
def setUp(self):
107+
self.sut = DocumentToOpenAPIConverter._get_responses
108+
109+
def test_post(self):
110+
self.assertDictEqual({'201': {'description': ''}}, self.sut('post'))
111+
112+
def test_delete(self):
113+
self.assertDictEqual({'201': {'description': ''}}, self.sut('post'))
114+
115+
def test_default(self):
116+
self.assertDictEqual(
117+
{'200': {'description': ''}},
118+
self.sut(uuid.uuid4())
119+
)

‎tox.ini

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
[tox]
2-
envlist = py34,py27
2+
envlist = py35,py34,py27
33
[testenv]
44
deps = -rrequirements.txt
55
commands = ./runtests
6+
[testenv:py27]
7+
deps =
8+
-rrequirements.txt
9+
mock
10+
commands = ./runtests

0 commit comments

Comments
 (0)
0