Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Lines changed: 61 additions & 37 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /