11import warnings
22from urllib .parse import urljoin
33
4- from django .db .models .fields import related_descriptors as rd
54from django .utils .module_loading import import_string as import_class_from_dotted_path
65from rest_framework .fields import empty
76from rest_framework .relations import ManyRelatedField
87from rest_framework .schemas import openapi as drf_openapi
98from rest_framework .schemas .utils import is_list_view
109
11- from rest_framework_json_api import serializers
12- from rest_framework_json_api .views import RelationshipView
10+ from rest_framework_json_api import serializers , views
1311
1412
1513class SchemaGenerator (drf_openapi .SchemaGenerator ):
@@ -29,19 +27,6 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
2927 },
3028 'additionalProperties' : False
3129 },
32- 'ResourceIdentifierObject' : {
33- 'type' : 'object' ,
34- 'required' : ['type' , 'id' ],
35- 'additionalProperties' : False ,
36- 'properties' : {
37- 'type' : {
38- '$ref' : '#/components/schemas/type'
39- },
40- 'id' : {
41- '$ref' : '#/components/schemas/id'
42- },
43- },
44- },
4530 'resource' : {
4631 'type' : 'object' ,
4732 'required' : ['type' , 'id' ],
@@ -133,6 +118,18 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
133118 'items' : {'$ref' : '#/components/schemas/linkage' },
134119 'uniqueItems' : True
135120 },
121+ # A RelationshipView uses a ResourceIdentifierObjectSerializer (hence the name
122+ # ResourceIdentifierObject returned by get_component_name()) which serializes type and
123+ # id. These can be lists or individual items depending on whether the relationship is
124+ # toMany or toOne so offer both options since we are not iterating over all the
125+ # possible {related_field}'s but rather rendering one path schema which may represent
126+ # toMany and toOne relationships.
127+ 'ResourceIdentifierObject' : {
128+ 'oneOf' : [
129+ {'$ref' : '#/components/schemas/relationshipToOne' },
130+ {'$ref' : '#/components/schemas/relationshipToMany' }
131+ ]
132+ },
136133 'linkage' : {
137134 'type' : 'object' ,
138135 'description' : "the 'type' and 'id'" ,
@@ -302,24 +299,23 @@ def get_schema(self, request=None, public=False):
302299 #: - 'action' copy of current view.action (list/fetch) as this gets reset for each request.
303300 expanded_endpoints = []
304301 for path , method , view in view_endpoints :
305- if isinstance (view , RelationshipView ):
306- expanded_endpoints += self ._expand_relationships (path , method , view )
307- elif hasattr (view , 'action' ) and view .action == 'retrieve_related' :
302+ if hasattr (view , 'action' ) and view .action == 'retrieve_related' :
308303 expanded_endpoints += self ._expand_related (path , method , view , view_endpoints )
309304 else :
310305 expanded_endpoints .append ((path , method , view , getattr (view , 'action' , None )))
311306
312307 for path , method , view , action in expanded_endpoints :
313308 if not self .has_view_permissions (path , method , view ):
314309 continue
315- # kludge to preserve view.action as it changes "globally" for the same ViewSet
316- # whether it is used for a collection, item or related serializer. _expand_related
317- # sets it based on whether the related field is a toMany collection or toOne item.
310+ # kludge to preserve view.action as it is 'list' for the parent ViewSet
311+ # but the related viewset that was expanded may be either 'fetch' (to_one) or 'list'
312+ # (to_many). This patches the view.action appropriately so that
313+ # view.schema.get_operation() "does the right thing" for fetch vs. list.
318314 current_action = None
319315 if hasattr (view , 'action' ):
320316 current_action = view .action
321317 view .action = action
322- operation = view .schema .get_operation (path , method , action )
318+ operation = view .schema .get_operation (path , method )
323319 components = view .schema .get_components (path , method )
324320 for k in components .keys ():
325321 if k not in components_schemas :
@@ -350,28 +346,6 @@ def get_schema(self, request=None, public=False):
350346
351347 return schema
352348
353- def _expand_relationships (self , path , method , view ):
354- """
355- Expand path containing .../{id}/relationships/{related_field} into list of related fields.
356- :return:list[tuple(path, method, view, action)]
357- """
358- queryset = view .get_queryset ()
359- if not queryset .model :
360- return [(path , method , view , getattr (view , 'action' , '' )), ]
361- result = []
362- # TODO: what about serializer-only (non-model) fields?
363- # Shouldn't this be iterating over serializer fields rather than model fields?
364- # Look at parent view's serializer to get the list of fields.
365- # OR maybe like _expand_related?
366- m = queryset .model
367- for field in [f for f in dir (m ) if not f .startswith ('_' )]:
368- attr = getattr (m , field )
369- if isinstance (attr , (rd .ReverseManyToOneDescriptor , rd .ForwardOneToOneDescriptor )):
370- action = 'rels' if isinstance (attr , rd .ReverseManyToOneDescriptor ) else 'rel'
371- result .append ((path .replace ('{related_field}' , field ), method , view , action ))
372-
373- return result
374-
375349 def _expand_related (self , path , method , view , view_endpoints ):
376350 """
377351 Expand path containing .../{id}/{related_field} into list of related fields
@@ -439,16 +413,12 @@ class AutoSchema(drf_openapi.AutoSchema):
439413 #: ignore all the media types and only generate a JSONAPI schema.
440414 content_types = ['application/vnd.api+json' ]
441415
442- def get_operation (self , path , method , action = None ):
416+ def get_operation (self , path , method ):
443417 """
444418 JSONAPI adds some standard fields to the API response that are not in upstream DRF:
445419 - some that only apply to GET/HEAD methods.
446420 - collections
447- - special handling for POST, PATCH, DELETE:
448-
449- :param action: One of the usual actions for a conventional path (list, retrieve, update,
450- partial_update, destroy) or special case 'rel' or 'rels' for a singular or
451- plural relationship.
421+ - special handling for POST, PATCH, DELETE
452422 """
453423 operation = {}
454424 operation ['operationId' ] = self .get_operation_id (path , method )
@@ -472,13 +442,13 @@ def get_operation(self, path, method, action=None):
472442 else :
473443 self ._add_get_item_response (operation )
474444 elif method == 'POST' :
475- self ._add_post_item_response (operation , path , action )
445+ self ._add_post_item_response (operation , path )
476446 elif method == 'PATCH' :
477- self ._add_patch_item_response (operation , path , action )
447+ self ._add_patch_item_response (operation , path )
478448 elif method == 'DELETE' :
479449 # should only allow deleting a resource, not a collection
480450 # TODO: implement delete of a relationship in future release.
481- self ._add_delete_item_response (operation , path , action )
451+ self ._add_delete_item_response (operation , path )
482452 return operation
483453
484454 def get_operation_id (self , path , method ):
@@ -591,11 +561,11 @@ def _get_toplevel_200_response(self, operation, collection=True):
591561 }
592562 }
593563
594- def _add_post_item_response (self , operation , path , action ):
564+ def _add_post_item_response (self , operation , path ):
595565 """
596566 add response for POST of an item to operation
597567 """
598- operation ['requestBody' ] = self .get_request_body (path , 'POST' , action )
568+ operation ['requestBody' ] = self .get_request_body (path , 'POST' )
599569 operation ['responses' ] = {
600570 '201' : self ._get_toplevel_200_response (operation , collection = False )
601571 }
@@ -610,95 +580,74 @@ def _add_post_item_response(self, operation, path, action):
610580 }
611581 self ._add_post_4xx_responses (operation )
612582
613- def _add_patch_item_response (self , operation , path , action ):
583+ def _add_patch_item_response (self , operation , path ):
614584 """
615585 Add PATCH response for an item to operation
616586 """
617- operation ['requestBody' ] = self .get_request_body (path , 'PATCH' , action )
587+ operation ['requestBody' ] = self .get_request_body (path , 'PATCH' )
618588 operation ['responses' ] = {
619589 '200' : self ._get_toplevel_200_response (operation , collection = False )
620590 }
621591 self ._add_patch_4xx_responses (operation )
622592
623- def _add_delete_item_response (self , operation , path , action ):
593+ def _add_delete_item_response (self , operation , path ):
624594 """
625595 add DELETE response for item or relationship(s) to operation
626596 """
627597 # Only DELETE of relationships has a requestBody
628- if action in [ 'rels' , 'rel' ] :
629- operation ['requestBody' ] = self .get_request_body (path , 'DELETE' , action )
598+ if isinstance ( self . view , views . RelationshipView ) :
599+ operation ['requestBody' ] = self .get_request_body (path , 'DELETE' )
630600 self ._add_delete_responses (operation )
631601
632- def get_request_body (self , path , method , action = None ):
602+ def get_request_body (self , path , method ):
633603 """
634604 A request body is required by jsonapi for POST, PATCH, and DELETE methods.
635- This has an added parameter which is not in upstream DRF:
636-
637- :param action: None for conventional path; 'rel' or 'rels' for a singular or plural
638- relationship of a related path, respectively.
639605 """
640606 serializer = self .get_serializer (path , method )
641607 if not isinstance (serializer , (serializers .BaseSerializer , )):
642608 return {}
609+ is_relationship = isinstance (self .view , views .RelationshipView )
643610
644- # DRF uses a $ref to the component definition, but this
611+ # DRF uses a $ref to the component schema definition, but this
645612 # doesn't work for jsonapi due to the different required fields based on
646613 # the method, so make those changes and inline another copy of the schema.
647- # TODO: A future improvement could make this DRYer with multiple components?
648- item_schema = self .map_serializer (serializer )
649-
650- # 'type' and 'id' are both required for:
651- # - all relationship operations
652- # - regular PATCH or DELETE
653- # Only 'type' is required for POST: system may assign the 'id'.
654- if action in ['rels' , 'rel' ]:
655- item_schema ['required' ] = ['type' , 'id' ]
656- elif method in ['PATCH' , 'DELETE' ]:
657- item_schema ['required' ] = ['type' , 'id' ]
658- elif method == 'POST' :
659- item_schema ['required' ] = ['type' ]
614+ # TODO: A future improvement could make this DRYer with multiple component schemas:
615+ # A base schema for each viewset that has no required fields
616+ # One subclassed from the base that requires some fields (`type` but not `id` for POST)
617+ # Another subclassed from base with required type/id but no required attributes (PATCH)
660618
661- if 'attributes' in item_schema ['properties' ]:
619+ if is_relationship :
620+ item_schema = {'$ref' : '#/components/schemas/ResourceIdentifierObject' }
621+ else :
622+ item_schema = self .map_serializer (serializer )
623+ if method == 'POST' :
624+ # 'type' and 'id' are both required for:
625+ # - all relationship operations
626+ # - regular PATCH or DELETE
627+ # Only 'type' is required for POST: system may assign the 'id'.
628+ item_schema ['required' ] = ['type' ]
629+
630+ if 'properties' in item_schema and 'attributes' in item_schema ['properties' ]:
662631 # No required attributes for PATCH
663632 if method in ['PATCH' , 'PUT' ] and 'required' in item_schema ['properties' ]['attributes' ]:
664633 del item_schema ['properties' ]['attributes' ]['required' ]
665634 # No read_only fields for request.
666635 for name , schema in item_schema ['properties' ]['attributes' ]['properties' ].copy ().items (): # noqa E501
667636 if 'readOnly' in schema :
668637 del item_schema ['properties' ]['attributes' ]['properties' ][name ]
669- # relationships special case: plural request body (data is array of items)
670- if action == 'rels' :
671- return {
672- 'content' : {
673- ct : {
674- 'schema' : {
675- 'required' : ['data' ],
676- 'properties' : {
677- 'data' : {
678- 'type' : 'array' ,
679- 'items' : item_schema
680- }
681- }
682- }
683- }
684- for ct in self .content_types
685- }
686- }
687- # singular request body for all other cases
688- else :
689- return {
690- 'content' : {
691- ct : {
692- 'schema' : {
693- 'required' : ['data' ],
694- 'properties' : {
695- 'data' : item_schema
696- }
638+ return {
639+ 'content' : {
640+ ct : {
641+ 'schema' : {
642+ 'required' : ['data' ],
643+ 'properties' : {
644+ 'data' : item_schema
697645 }
698646 }
699- for ct in self .content_types
700647 }
648+ for ct in self .content_types
701649 }
650+ }
702651
703652 def map_serializer (self , serializer ):
704653 """
0 commit comments