1
+ import re
1
2
import warnings
2
3
from collections import OrderedDict
3
4
from decimal import Decimal
@@ -39,16 +40,21 @@ def get_schema(self, request=None, public=False):
39
40
Generate a OpenAPI schema.
40
41
"""
41
42
self ._initialise_endpoints ()
43
+ components_schemas = {}
42
44
43
45
# Iterate endpoints generating per method path operations.
44
- # TODO: …and reference components.
45
46
paths = {}
46
47
_ , view_endpoints = self ._get_paths_and_endpoints (None if public else request )
47
48
for path , method , view in view_endpoints :
48
49
if not self .has_view_permissions (path , method , view ):
49
50
continue
50
51
51
52
operation = view .schema .get_operation (path , method )
53
+ component = view .schema .get_components (path , method )
54
+
55
+ if component is not None :
56
+ components_schemas .update (component )
57
+
52
58
# Normalise path for any provided mount url.
53
59
if path .startswith ('/' ):
54
60
path = path [1 :]
@@ -64,6 +70,11 @@ def get_schema(self, request=None, public=False):
64
70
'paths' : paths ,
65
71
}
66
72
73
+ if len (components_schemas ) > 0 :
74
+ schema ['components' ] = {
75
+ 'schemas' : components_schemas
76
+ }
77
+
67
78
return schema
68
79
69
80
# View Inspectors
@@ -101,6 +112,34 @@ def get_operation(self, path, method):
101
112
102
113
return operation
103
114
115
+ def _get_serializer_component_name (self , serializer ):
116
+ if not hasattr (serializer , 'Meta' ):
117
+ return None
118
+
119
+ if hasattr (serializer .Meta , 'schema_component_name' ):
120
+ return serializer .Meta .schema_component_name
121
+
122
+ # If the serializer has no Meta.schema_component_name, we use
123
+ # the serializer's class name as the component name.
124
+ component_name = serializer .__class__ .__name__
125
+ # We remove the "serializer" string from the class name.
126
+ pattern = re .compile ("serializer" , re .IGNORECASE )
127
+ return pattern .sub ("" , component_name )
128
+
129
+ def get_components (self , path , method ):
130
+ serializer = self ._get_serializer (path , method )
131
+
132
+ if not isinstance (serializer , serializers .Serializer ):
133
+ return None
134
+
135
+ component_name = self ._get_serializer_component_name (serializer )
136
+
137
+ if component_name is None :
138
+ return None
139
+
140
+ content = self ._map_serializer (serializer )
141
+ return {component_name : content }
142
+
104
143
def _get_operation_id (self , path , method ):
105
144
"""
106
145
Compute an operation ID from the model, serializer or view name.
@@ -491,6 +530,10 @@ def _get_serializer(self, path, method):
491
530
.format (view .__class__ .__name__ , method , path ))
492
531
return None
493
532
533
+ def _get_reference (self , serializer ):
534
+ component_name = self ._get_serializer_component_name (serializer )
535
+ return {'$ref' : '#/components/schemas/{}' .format (component_name )}
536
+
494
537
def _get_request_body (self , path , method ):
495
538
if method not in ('PUT' , 'PATCH' , 'POST' ):
496
539
return {}
@@ -500,20 +543,30 @@ def _get_request_body(self, path, method):
500
543
serializer = self ._get_serializer (path , method )
501
544
502
545
if not isinstance (serializer , serializers .Serializer ):
503
- return {}
504
-
505
- content = self ._map_serializer (serializer )
506
- # No required fields for PATCH
507
- if method == 'PATCH' :
508
- content .pop ('required' , None )
509
- # No read_only fields for request.
510
- for name , schema in content ['properties' ].copy ().items ():
511
- if 'readOnly' in schema :
512
- del content ['properties' ][name ]
546
+ item_schema = {}
547
+ elif hasattr (serializer , 'Meta' ):
548
+ # If possible, the serializer should use a reference
549
+ item_schema = self ._get_reference (serializer )
550
+ else :
551
+ # There is no model, we'll map the serializer's fields
552
+ item_schema = self ._map_serializer (serializer )
553
+ # No required fields for PATCH
554
+ if method == 'PATCH' :
555
+ item_schema .pop ('required' , None )
556
+ # No read_only fields for request.
557
+ # No write_only fields for response.
558
+ for name , schema in item_schema ['properties' ].copy ().items ():
559
+ if 'writeOnly' in schema :
560
+ del item_schema ['properties' ][name ]
561
+ if 'required' in item_schema :
562
+ item_schema ['required' ] = [f for f in item_schema ['required' ] if f != name ]
563
+ for name , schema in item_schema ['properties' ].copy ().items ():
564
+ if 'readOnly' in schema :
565
+ del item_schema ['properties' ][name ]
513
566
514
567
return {
515
568
'content' : {
516
- ct : {'schema' : content }
569
+ ct : {'schema' : item_schema }
517
570
for ct in self .request_media_types
518
571
}
519
572
}
@@ -529,10 +582,15 @@ def _get_responses(self, path, method):
529
582
530
583
self .response_media_types = self .map_renderers (path , method )
531
584
532
- item_schema = {}
533
585
serializer = self ._get_serializer (path , method )
534
586
535
- if isinstance (serializer , serializers .Serializer ):
587
+ if not isinstance (serializer , serializers .Serializer ):
588
+ item_schema = {}
589
+ elif hasattr (serializer , 'Meta' ) and hasattr (serializer .Meta , 'model' ):
590
+ # If the serializer uses a model, we should use a reference
591
+ item_schema = self ._get_reference (serializer )
592
+ else :
593
+ # There is no model, we'll map the serializer's fields
536
594
item_schema = self ._map_serializer (serializer )
537
595
# No write_only fields for response.
538
596
for name , schema in item_schema ['properties' ].copy ().items ():
0 commit comments