Skip to content

Commit a0c4d4e

Browse files
author
Marc Gibbons
committed
Merge branch 'feature/yaml-docs' of github.com:pySilver/django-rest-swagger into feature/yaml
Conflicts: README.md rest_framework_swagger/__init__.py rest_framework_swagger/static/rest_framework_swagger/index.html rest_framework_swagger/static/rest_framework_swagger/lib/jquery.cookie.js rest_framework_swagger/templates/rest_framework_swagger/index.html rest_framework_swagger/urlparser.py setup.py
2 parents 558678b + b9401b6 commit a0c4d4e

35 files changed

+3634
-1768
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ Many thanks to Tom Christie & all the contributors who have developed [Django RE
121121
* Darren Thompson (@WhiteDawn)
122122
* Lukasz Balcerzak (@lukaszb)
123123
* David Newgas (@davidn)
124+
<<<<<<< HEAD
124125
* Bozidar Benko (@bbenko)
126+
=======
127+
>>>>>>> b9401b6cb4d500600df632d6819fea5389eea402
125128
126129

127130
### Django REST Framework Docs contributors:

cigar_example/cigar_example/settings.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Django settings for cigar_example project.
2+
from os.path import dirname, abspath, join
23

34
DEBUG = True
45
TEMPLATE_DEBUG = DEBUG
@@ -8,10 +9,13 @@
89

910
MANAGERS = ADMINS
1011

12+
DJANGO_ROOT = dirname(dirname(abspath(__file__)))
13+
root = lambda *x: abspath(join(abspath(DJANGO_ROOT), *x))
14+
1115
DATABASES = {
1216
'default': {
1317
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
14-
'NAME': 'db.sql', # Or path to database file if using sqlite3.
18+
'NAME': root('db.sql'), # Or path to database file if using sqlite3.
1519
'USER': '', # Not used with sqlite3.
1620
'PASSWORD': '', # Not used with sqlite3.
1721
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.

cigar_example/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Django==1.5.5
22
PyYAML==3.10
33
argh==0.23.2
4-
argparse==1.2.1
4+
argparse==1.1
55
coverage==3.6
66
distribute==0.6.49
77
django-nose==1.2

rest_framework_swagger/docgenerator.py

+216-24
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@
22
from django.http import HttpRequest
33

44
from rest_framework import viewsets
5+
from rest_framework.serializers import BaseSerializer
56

67
from .introspectors import APIViewIntrospector, \
78
ViewSetIntrospector, BaseMethodIntrospector, IntrospectorHelper, \
8-
get_resolved_value
9+
get_resolved_value, YAMLDocstringParser
910

1011

1112
class DocumentationGenerator(object):
13+
# Serializers defined in docstrings
14+
explicit_serializers = set()
15+
16+
# Serializers defined in fields
17+
fields_serializers = set()
18+
19+
# Response classes defined in docstrings
20+
explicit_response_types = dict()
21+
1222
def generate(self, apis):
1323
"""
14-
Returns documentaion for a list of APIs
24+
Returns documentation for a list of APIs
1525
"""
1626
api_docs = []
1727
for api in apis:
@@ -43,21 +53,37 @@ def get_operations(self, api):
4353
method_introspector.get_http_method() == "OPTIONS":
4454
continue # No one cares. I impose JSON.
4555

46-
serializer = method_introspector.get_serializer_class()
47-
serializer_name = IntrospectorHelper.get_serializer_name(serializer)
56+
doc_parser = YAMLDocstringParser(
57+
docstring=method_introspector.get_docs())
58+
59+
serializer = self._get_method_serializer(
60+
doc_parser, method_introspector)
61+
62+
response_type = self._get_method_response_type(
63+
doc_parser, serializer, introspector, method_introspector)
4864

4965
operation = {
50-
'httpMethod': method_introspector.get_http_method(),
66+
'method': method_introspector.get_http_method(),
5167
'summary': method_introspector.get_summary(),
5268
'nickname': method_introspector.get_nickname(),
5369
'notes': method_introspector.get_notes(),
54-
'responseClass': serializer_name,
70+
'type': response_type,
5571
}
5672

57-
parameters = method_introspector.get_parameters()
58-
if len(parameters) > 0:
73+
if doc_parser.yaml_error is not None:
74+
operation['notes'] += "<pre>YAMLError:\n {err}</pre>".format(
75+
err=doc_parser.yaml_error)
76+
77+
response_messages = doc_parser.get_response_messages()
78+
parameters = doc_parser.discover_parameters(
79+
inspector=method_introspector)
80+
81+
if parameters:
5982
operation['parameters'] = parameters
6083

84+
if response_messages:
85+
operation['responseMessages'] = response_messages
86+
6187
operations.append(operation)
6288

6389
return operations
@@ -68,19 +94,120 @@ def get_models(self, apis):
6894
DRF serializers and their fields
6995
"""
7096
serializers = self._get_serializer_set(apis)
97+
serializers.update(self.explicit_serializers)
98+
serializers.update(
99+
self._find_field_serializers(serializers)
100+
)
71101

72102
models = {}
73103

74104
for serializer in serializers:
75-
properties = self._get_serializer_fields(serializer)
105+
data = self._get_serializer_fields(serializer)
76106

77-
models[serializer.__name__] = {
78-
'id': serializer.__name__,
79-
'properties': properties,
107+
# Register 2 models with different subset of properties suitable
108+
# for data reading and writing.
109+
# i.e. rest framework does not output write_only fields in response
110+
# or require read_only fields in complex input.
111+
112+
serializer_name = IntrospectorHelper.get_serializer_name(serializer)
113+
# Writing
114+
# no readonly fields
115+
w_name = "Write{serializer}".format(serializer=serializer_name)
116+
117+
w_properties = dict((k, v) for k, v in data['fields'].items()
118+
if k not in data['read_only'])
119+
120+
models[w_name] = {
121+
'id': w_name,
122+
'required': [i for i in data['required'] if i in w_properties.keys()],
123+
'properties': w_properties,
80124
}
81125

126+
# Reading
127+
# no write_only fields
128+
r_name = serializer_name
129+
130+
r_properties = dict((k, v) for k, v in data['fields'].items()
131+
if k not in data['write_only'])
132+
133+
models[r_name] = {
134+
'id': r_name,
135+
'required': [i for i in r_properties.keys()],
136+
'properties': r_properties,
137+
}
138+
139+
# Enable original model for testing purposes
140+
# models[serializer_name] = {
141+
# 'id': serializer_name,
142+
# 'required': data['required'],
143+
# 'properties': data['fields'],
144+
# }
145+
146+
models.update(self.explicit_response_types)
147+
models.update(self.fields_serializers)
82148
return models
83149

150+
def _get_method_serializer(self, doc_parser, method_inspector):
151+
"""
152+
Returns serializer used in method.
153+
Registers custom serializer from docstring in scope.
154+
155+
Serializer might be ignored if explicitly told in docstring
156+
"""
157+
serializer = method_inspector.get_serializer_class()
158+
159+
docstring_serializer = doc_parser.get_serializer_class(
160+
callback=method_inspector.callback
161+
)
162+
163+
if doc_parser.get_response_type() is not None:
164+
# Custom response class detected
165+
return None
166+
167+
if docstring_serializer is not None:
168+
self.explicit_serializers.add(docstring_serializer)
169+
serializer = docstring_serializer
170+
171+
if doc_parser.should_omit_serializer():
172+
serializer = None
173+
174+
return serializer
175+
176+
def _get_method_response_type(self, doc_parser, serializer,
177+
view_inspector, method_inspector):
178+
"""
179+
Returns response type for method.
180+
This might be custom `type` from docstring or discovered
181+
serializer class name.
182+
183+
Once custom `type` found in docstring - it'd be
184+
registered in a scope
185+
"""
186+
response_type = doc_parser.get_response_type()
187+
if response_type is not None:
188+
# Register class in scope
189+
view_name = view_inspector.callback.__name__
190+
view_name = view_name.replace('ViewSet', '')
191+
view_name = view_name.replace('APIView', '')
192+
view_name = view_name.replace('View', '')
193+
response_type_name = "{view}{method}Response".format(
194+
view=view_name,
195+
method=method_inspector.method.title().replace('_', '')
196+
)
197+
self.explicit_response_types.update({
198+
response_type_name: {
199+
"id": response_type_name,
200+
"properties": response_type
201+
}
202+
})
203+
return response_type_name
204+
else:
205+
serializer_name = IntrospectorHelper.get_serializer_name(serializer)
206+
if serializer_name is not None:
207+
return serializer_name
208+
209+
return None
210+
84211
def _get_serializer_set(self, apis):
85212
"""
86213
Returns a set of serializer classes for a provided list
@@ -95,30 +222,95 @@ def _get_serializer_set(self, apis):
95222

96223
return serializers
97224

225+
def _find_field_serializers(self, serializers):
226+
"""
227+
Returns set of serializers discovered from fields
228+
"""
229+
serializers_set = set()
230+
for serializer in serializers:
231+
fields = serializer().get_fields()
232+
for name, field in fields.items():
233+
if isinstance(field, BaseSerializer):
234+
serializers_set.add(field)
235+
236+
return serializers_set
237+
98238
def _get_serializer_fields(self, serializer):
99239
"""
100240
Returns serializer fields in the Swagger MODEL format
101241
"""
102242
if serializer is None:
103243
return
104244

105-
fields = serializer().get_fields()
245+
if hasattr(serializer, '__call__'):
246+
fields = serializer().get_fields()
247+
else:
248+
fields = serializer.get_fields()
106249

107-
data = {}
250+
data = {
251+
'fields': {},
252+
'required': [],
253+
'write_only': [],
254+
'read_only': [],
255+
}
108256
for name, field in fields.items():
257+
if getattr(field, 'write_only', False):
258+
data['write_only'].append(name)
109259

110-
data[name] = {
111-
'type': field.type_label,
112-
'required': getattr(field, 'required', None),
113-
'allowableValues': {
114-
'min': getattr(field, 'min_length', None),
115-
'max': getattr(field, 'max_length', None),
116-
'defaultValue': get_resolved_value(field, 'default', None),
117-
'readOnly': getattr(field, 'read_only', None),
118-
'valueType': 'RANGE',
119-
}
260+
if getattr(field, 'read_only', False):
261+
data['read_only'].append(name)
262+
263+
if getattr(field, 'required', False):
264+
data['required'].append(name)
265+
266+
data_type = field.type_label
267+
268+
# guess format
269+
data_format = 'string'
270+
if data_type in BaseMethodIntrospector.PRIMITIVES:
271+
data_format = BaseMethodIntrospector.PRIMITIVES.get(data_type)[0]
272+
273+
f = {
274+
'description': getattr(field, 'help_text', ''),
275+
'type': data_type,
276+
'format': data_format,
277+
'required': getattr(field, 'required', False),
278+
'defaultValue': get_resolved_value(field, 'default'),
279+
'readOnly': getattr(field, 'read_only', None),
120280
}
121281

282+
# Min/Max values
283+
max_val = getattr(field, 'max_val', None)
284+
min_val = getattr(field, 'min_val', None)
285+
if max_val is not None and data_type == 'integer':
286+
f['minimum'] = min_val
287+
288+
if max_val is not None and data_type == 'integer':
289+
f['maximum'] = max_val
290+
291+
# ENUM options
292+
if field.type_label == 'multiple choice' \
293+
and isinstance(field.choices, list):
294+
f['enum'] = [k for k, v in field.choices]
295+
296+
# Support for complex types
297+
if isinstance(field, BaseSerializer):
298+
field_serializer = IntrospectorHelper.get_serializer_name(field)
299+
300+
if getattr(field, 'write_only', False):
301+
field_serializer = "Write{}".format(field_serializer)
302+
303+
f['type'] = field_serializer
304+
if field.many:
305+
f['type'] = 'array'
306+
if data_type in BaseMethodIntrospector.PRIMITIVES:
307+
f['items'] = {'type': data_type}
308+
else:
309+
f['items'] = {'$ref': field_serializer}
310+
311+
# memorize discovered field
312+
data['fields'][name] = f
313+
122314
return data
123315

124316
def _get_serializer_class(self, callback):

0 commit comments

Comments
 (0)