Skip to content

Commit 8434430

Browse files
committed
track DRF openapi schema changes
- a bunch of private methods renamed to public - DRF now generates components
1 parent 4f4f26d commit 8434430

File tree

1 file changed

+61
-37
lines changed

1 file changed

+61
-37
lines changed

rest_framework_json_api/schemas/openapi.py

+61-37
Original file line numberDiff line numberDiff line change
@@ -271,43 +271,34 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
271271
Extend DRF's SchemaGenerator to implement jsonapi-flavored generateschema command
272272
"""
273273
def __init__(self, *args, **kwargs):
274+
self.openapi_schema = {}
274275
super().__init__(*args, **kwargs)
275276

276277
def get_schema(self, request=None, public=False):
277278
"""
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
279281
"""
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)
282285

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)
297289

298290
#: `expanded_endpoints` is like view_endpoints with one extra field tacked on:
299291
#: - 'action' copy of current view.action (list/fetch) as this gets reset for each request.
300292
# TODO: define an endpoint_inspector_cls that extends EndpointEnumerator
301293
# instead of doing it here.
302294
expanded_endpoints = []
303295
for path, method, view in view_endpoints:
304-
action = view.action if hasattr(view, 'action') else None
305296
if isinstance(view, RelationshipView):
306297
expanded_endpoints += self._expand_relationships(path, method, view)
307-
elif action == 'retrieve_related':
298+
elif view.action == 'retrieve_related':
308299
expanded_endpoints += self._expand_related(path, method, view, view_endpoints)
309300
else:
310-
expanded_endpoints.append((path, method, view, action))
301+
expanded_endpoints.append((path, method, view, view.action))
311302

312303
for path, method, view, action in expanded_endpoints:
313304
if not self.has_view_permissions(path, method, view):
@@ -320,21 +311,40 @@ def get_paths(self, request=None):
320311
current_action = view.action
321312
view.action = action
322313
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+
323324
if hasattr(view, 'action'):
324325
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']
329326
# Normalise path for any provided mount url.
330327
if path.startswith('/'):
331328
path = path[1:]
332329
path = urljoin(self.url or '/', path)
333330

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}
336336

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
338348

339349
def _expand_relationships(self, path, method, view):
340350
"""
@@ -423,27 +433,41 @@ class AutoSchema(drf_openapi.AutoSchema):
423433
"""
424434
content_types = ['application/vnd.api+json']
425435

426-
def __init__(self):
436+
def __init__(self, openapi_schema=None, **kwargs):
427437
"""
428438
Initialize the JSONAPI OAS schema generator
439+
:param openapi_schema: dict: OAS 3.0 document with initial values.
429440
"""
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}
431453

432454
def get_operation(self, path, method, action=None):
433455
""" basically a copy of AutoSchema.get_operation """
434456
operation = {}
435-
operation['operationId'] = self._get_operation_id(path, method)
457+
operation['operationId'] = self.get_operation_id(path, method)
436458
operation['description'] = self.get_description(path, method)
459+
# TODO: add security
460+
# operation['security'] = self.get_security(path, method)
437461

438462
parameters = []
439-
parameters += self._get_path_parameters(path, method)
463+
parameters += self.get_path_parameters(path, method)
440464
# pagination, filters only apply to GET/HEAD of collections and items
441465
if method in ['GET', 'HEAD']:
442466
parameters += self._get_include_parameters(path, method)
443467
parameters += self._get_fields_parameters(path, method)
444468
parameters += self._get_sort_parameters(path, method)
445469
parameters += self._get_pagination_parameters(path, method)
446-
parameters += self._get_filter_parameters(path, method)
470+
parameters += self.get_filter_parameters(path, method)
447471
operation['parameters'] = parameters
448472

449473
# get request and response code schemas
@@ -462,7 +486,7 @@ def get_operation(self, path, method, action=None):
462486
self._delete_item(operation, path, action)
463487
return operation
464488

465-
def _get_operation_id(self, path, method):
489+
def get_operation_id(self, path, method):
466490
""" create a unique operationId """
467491
# The DRF version creates non-unique operationIDs, especially when the same view is used
468492
# for different paths. Just make a simple concatenation of (mapped) method name and path.
@@ -564,7 +588,7 @@ def _get_item_schema(self, operation):
564588
'generated.'.format(view.__class__.__name__))
565589

566590
if isinstance(serializer, serializers.BaseSerializer):
567-
content = self._map_serializer(serializer)
591+
content = self.map_serializer(serializer)
568592
# No write_only fields for response.
569593
for name, schema in content['properties'].copy().items():
570594
if 'writeOnly' in schema:
@@ -633,7 +657,7 @@ def _get_request_body(self, path, method, action=None):
633657
if not isinstance(serializer, (serializers.BaseSerializer, )):
634658
return {}
635659

636-
content = self._map_serializer(serializer)
660+
content = self.map_serializer(serializer)
637661

638662
# 'type' and 'id' are both required for:
639663
# - all relationship operations
@@ -687,7 +711,7 @@ def _get_request_body(self, path, method, action=None):
687711
}
688712
}
689713

690-
def _map_serializer(self, serializer):
714+
def map_serializer(self, serializer):
691715
"""
692716
Custom map_serializer that serializes the schema using the jsonapi spec.
693717
Non-attributes like related and identity fields, are move to 'relationships' and 'links'.
@@ -713,7 +737,7 @@ def _map_serializer(self, serializer):
713737
if field.required:
714738
required.append(field.field_name)
715739

716-
schema = self._map_field(field)
740+
schema = self.map_field(field)
717741
if field.read_only:
718742
schema['readOnly'] = True
719743
if field.write_only:

0 commit comments

Comments
 (0)