@@ -271,43 +271,34 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
271
271
Extend DRF's SchemaGenerator to implement jsonapi-flavored generateschema command
272
272
"""
273
273
def __init__ (self , * args , ** kwargs ):
274
+ self .openapi_schema = {}
274
275
super ().__init__ (* args , ** kwargs )
275
276
276
277
def get_schema (self , request = None , public = False ):
277
278
"""
278
- Generate a JSONAPI OpenAPI schema.
279
+ Generate a JSONAPI OpenAPI schema. Merge in standard JSONAPI components
280
+ based on a copy of drf's get_schema because I need to muck with paths and endpoints
279
281
"""
280
- schema = super ().get_schema (request , public )
281
- return {** schema , 'components' : JSONAPI_COMPONENTS }
282
+ self ._initialise_endpoints ()
283
+ components_schemas = {}
284
+ components_schemas .update (JSONAPI_COMPONENTS )
282
285
283
- def get_paths (self , request = None ):
284
- """
285
- **Replacement** for rest_framework.schemas.openapi.SchemaGenerator.get_paths():
286
- - expand the paths for RelationshipViews and retrieve_related actions:
287
- {related_field} gets replaced by the related field names.
288
- - Merges in any openapi_schema initializer that the view has.
289
- """
290
- result = {}
291
-
292
- paths , view_endpoints = self ._get_paths_and_endpoints (request )
293
-
294
- # Only generate the path prefix for paths that will be included
295
- if not paths :
296
- return None
286
+ # Iterate endpoints generating per method path operations.
287
+ paths = {}
288
+ _ , view_endpoints = self ._get_paths_and_endpoints (None if public else request )
297
289
298
290
#: `expanded_endpoints` is like view_endpoints with one extra field tacked on:
299
291
#: - 'action' copy of current view.action (list/fetch) as this gets reset for each request.
300
292
# TODO: define an endpoint_inspector_cls that extends EndpointEnumerator
301
293
# instead of doing it here.
302
294
expanded_endpoints = []
303
295
for path , method , view in view_endpoints :
304
- action = view .action if hasattr (view , 'action' ) else None
305
296
if isinstance (view , RelationshipView ):
306
297
expanded_endpoints += self ._expand_relationships (path , method , view )
307
- elif action == 'retrieve_related' :
298
+ elif view . action == 'retrieve_related' :
308
299
expanded_endpoints += self ._expand_related (path , method , view , view_endpoints )
309
300
else :
310
- expanded_endpoints .append ((path , method , view , action ))
301
+ expanded_endpoints .append ((path , method , view , view . action ))
311
302
312
303
for path , method , view , action in expanded_endpoints :
313
304
if not self .has_view_permissions (path , method , view ):
@@ -320,21 +311,40 @@ def get_paths(self, request=None):
320
311
current_action = view .action
321
312
view .action = action
322
313
operation = view .schema .get_operation (path , method , action )
314
+ components = view .schema .get_components (path , method )
315
+ for k in components .keys ():
316
+ if k not in components_schemas :
317
+ continue
318
+ if components_schemas [k ] == components [k ]:
319
+ continue
320
+ warnings .warn ('Schema component "{}" has been overriden with a different value.' .format (k ))
321
+
322
+ components_schemas .update (components )
323
+
323
324
if hasattr (view , 'action' ):
324
325
view .action = current_action
325
-
326
- if 'responses' in operation and '200' in operation ['responses' ]:
327
- # TODO: Still a TODO in DRF 3.11 as well
328
- operation ['responses' ]['200' ]['description' ] = operation ['operationId' ]
329
326
# Normalise path for any provided mount url.
330
327
if path .startswith ('/' ):
331
328
path = path [1 :]
332
329
path = urljoin (self .url or '/' , path )
333
330
334
- result .setdefault (path , {})
335
- result [path ][method .lower ()] = operation
331
+ paths .setdefault (path , {})
332
+ paths [path ][method .lower ()] = operation
333
+ if hasattr (view .schema , 'openapi_schema' ): # TODO: still needed?
334
+ # TODO: shallow or deep merge?
335
+ self .openapi_schema = {** self .openapi_schema , ** view .schema .openapi_schema }
336
336
337
- return result
337
+ self .check_duplicate_operation_id (paths )
338
+
339
+ # Compile final schema.
340
+ schema = {
341
+ 'openapi' : '3.0.2' ,
342
+ 'info' : self .get_info (),
343
+ 'paths' : paths ,
344
+ 'components' : components_schemas ,
345
+ }
346
+
347
+ return schema
338
348
339
349
def _expand_relationships (self , path , method , view ):
340
350
"""
@@ -423,27 +433,41 @@ class AutoSchema(drf_openapi.AutoSchema):
423
433
"""
424
434
content_types = ['application/vnd.api+json' ]
425
435
426
- def __init__ (self ):
436
+ def __init__ (self , openapi_schema = None , ** kwargs ):
427
437
"""
428
438
Initialize the JSONAPI OAS schema generator
439
+ :param openapi_schema: dict: OAS 3.0 document with initial values.
429
440
"""
430
- super ().__init__ ()
441
+ super ().__init__ (** kwargs )
442
+ #: allow initialization of OAS schema doc TODO: still needed?
443
+ if openapi_schema is None :
444
+ openapi_schema = {}
445
+ self .openapi_schema = openapi_schema
446
+ # static JSONAPI fields that get $ref'd to in the view mappings
447
+ jsonapi_ref = {
448
+ 'components' : JSONAPI_COMPONENTS
449
+ }
450
+ # merge in our reference data on top of anything provided by the init.
451
+ # TODO: shallow or deep merge?
452
+ self .openapi_schema = {** self .openapi_schema , ** jsonapi_ref }
431
453
432
454
def get_operation (self , path , method , action = None ):
433
455
""" basically a copy of AutoSchema.get_operation """
434
456
operation = {}
435
- operation ['operationId' ] = self ._get_operation_id (path , method )
457
+ operation ['operationId' ] = self .get_operation_id (path , method )
436
458
operation ['description' ] = self .get_description (path , method )
459
+ # TODO: add security
460
+ # operation['security'] = self.get_security(path, method)
437
461
438
462
parameters = []
439
- parameters += self ._get_path_parameters (path , method )
463
+ parameters += self .get_path_parameters (path , method )
440
464
# pagination, filters only apply to GET/HEAD of collections and items
441
465
if method in ['GET' , 'HEAD' ]:
442
466
parameters += self ._get_include_parameters (path , method )
443
467
parameters += self ._get_fields_parameters (path , method )
444
468
parameters += self ._get_sort_parameters (path , method )
445
469
parameters += self ._get_pagination_parameters (path , method )
446
- parameters += self ._get_filter_parameters (path , method )
470
+ parameters += self .get_filter_parameters (path , method )
447
471
operation ['parameters' ] = parameters
448
472
449
473
# get request and response code schemas
@@ -462,7 +486,7 @@ def get_operation(self, path, method, action=None):
462
486
self ._delete_item (operation , path , action )
463
487
return operation
464
488
465
- def _get_operation_id (self , path , method ):
489
+ def get_operation_id (self , path , method ):
466
490
""" create a unique operationId """
467
491
# The DRF version creates non-unique operationIDs, especially when the same view is used
468
492
# for different paths. Just make a simple concatenation of (mapped) method name and path.
@@ -564,7 +588,7 @@ def _get_item_schema(self, operation):
564
588
'generated.' .format (view .__class__ .__name__ ))
565
589
566
590
if isinstance (serializer , serializers .BaseSerializer ):
567
- content = self ._map_serializer (serializer )
591
+ content = self .map_serializer (serializer )
568
592
# No write_only fields for response.
569
593
for name , schema in content ['properties' ].copy ().items ():
570
594
if 'writeOnly' in schema :
@@ -633,7 +657,7 @@ def _get_request_body(self, path, method, action=None):
633
657
if not isinstance (serializer , (serializers .BaseSerializer , )):
634
658
return {}
635
659
636
- content = self ._map_serializer (serializer )
660
+ content = self .map_serializer (serializer )
637
661
638
662
# 'type' and 'id' are both required for:
639
663
# - all relationship operations
@@ -687,7 +711,7 @@ def _get_request_body(self, path, method, action=None):
687
711
}
688
712
}
689
713
690
- def _map_serializer (self , serializer ):
714
+ def map_serializer (self , serializer ):
691
715
"""
692
716
Custom map_serializer that serializes the schema using the jsonapi spec.
693
717
Non-attributes like related and identity fields, are move to 'relationships' and 'links'.
@@ -713,7 +737,7 @@ def _map_serializer(self, serializer):
713
737
if field .required :
714
738
required .append (field .field_name )
715
739
716
- schema = self ._map_field (field )
740
+ schema = self .map_field (field )
717
741
if field .read_only :
718
742
schema ['readOnly' ] = True
719
743
if field .write_only :
0 commit comments