2
2
from django .http import HttpRequest
3
3
4
4
from rest_framework import viewsets
5
+ from rest_framework .serializers import BaseSerializer
5
6
6
7
from .introspectors import APIViewIntrospector , \
7
8
ViewSetIntrospector , BaseMethodIntrospector , IntrospectorHelper , \
8
- get_resolved_value
9
+ get_resolved_value , YAMLDocstringParser
9
10
10
11
11
12
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
+
12
22
def generate (self , apis ):
13
23
"""
14
- Returns documentaion for a list of APIs
24
+ Returns documentation for a list of APIs
15
25
"""
16
26
api_docs = []
17
27
for api in apis :
@@ -43,21 +53,37 @@ def get_operations(self, api):
43
53
method_introspector .get_http_method () == "OPTIONS" :
44
54
continue # No one cares. I impose JSON.
45
55
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 )
48
64
49
65
operation = {
50
- 'httpMethod ' : method_introspector .get_http_method (),
66
+ 'method ' : method_introspector .get_http_method (),
51
67
'summary' : method_introspector .get_summary (),
52
68
'nickname' : method_introspector .get_nickname (),
53
69
'notes' : method_introspector .get_notes (),
54
- 'responseClass ' : serializer_name ,
70
+ 'type ' : response_type ,
55
71
}
56
72
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 :
59
82
operation ['parameters' ] = parameters
60
83
84
+ if response_messages :
85
+ operation ['responseMessages' ] = response_messages
86
+
61
87
operations .append (operation )
62
88
63
89
return operations
@@ -68,19 +94,120 @@ def get_models(self, apis):
68
94
DRF serializers and their fields
69
95
"""
70
96
serializers = self ._get_serializer_set (apis )
97
+ serializers .update (self .explicit_serializers )
98
+ serializers .update (
99
+ self ._find_field_serializers (serializers )
100
+ )
71
101
72
102
models = {}
73
103
74
104
for serializer in serializers :
75
- properties = self ._get_serializer_fields (serializer )
105
+ data = self ._get_serializer_fields (serializer )
76
106
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 ,
80
124
}
81
125
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 )
82
148
return models
83
149
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
+
84
211
def _get_serializer_set (self , apis ):
85
212
"""
86
213
Returns a set of serializer classes for a provided list
@@ -95,30 +222,95 @@ def _get_serializer_set(self, apis):
95
222
96
223
return serializers
97
224
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
+
98
238
def _get_serializer_fields (self , serializer ):
99
239
"""
100
240
Returns serializer fields in the Swagger MODEL format
101
241
"""
102
242
if serializer is None :
103
243
return
104
244
105
- fields = serializer ().get_fields ()
245
+ if hasattr (serializer , '__call__' ):
246
+ fields = serializer ().get_fields ()
247
+ else :
248
+ fields = serializer .get_fields ()
106
249
107
- data = {}
250
+ data = {
251
+ 'fields' : {},
252
+ 'required' : [],
253
+ 'write_only' : [],
254
+ 'read_only' : [],
255
+ }
108
256
for name , field in fields .items ():
257
+ if getattr (field , 'write_only' , False ):
258
+ data ['write_only' ].append (name )
109
259
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 ),
120
280
}
121
281
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
+
122
314
return data
123
315
124
316
def _get_serializer_class (self , callback ):
0 commit comments