diff --git a/CHANGELOG.md b/CHANGELOG.md index 9502f476..63865fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ any parts of the framework not mentioned in the documentation should generally b * Removed support for Python 3.8. * Removed support for Django REST framework 3.14. * Removed support for Django 5.0. - +* Removed built-in support for generating OpenAPI schema. Use [drf-spectacular-json-api](https://github.com/jokiefer/drf-spectacular-json-api/) instead. ## [7.1.0] - 2024-10-25 diff --git a/README.rst b/README.rst index c0e95a19..05c01c04 100644 --- a/README.rst +++ b/README.rst @@ -114,7 +114,6 @@ Install using ``pip``... $ # for optional package integrations $ pip install djangorestframework-jsonapi['django-filter'] $ pip install djangorestframework-jsonapi['django-polymorphic'] - $ pip install djangorestframework-jsonapi['openapi'] or from source... @@ -156,8 +155,6 @@ installed and activated: Browse to * http://localhost:8000 for the list of available collections (in a non-JSON:API format!), -* http://localhost:8000/swagger-ui/ for a Swagger user interface to the dynamic schema view, or -* http://localhost:8000/openapi for the schema view's OpenAPI specification document. ----- diff --git a/docs/getting-started.md b/docs/getting-started.md index 4052450b..81040e8e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -69,7 +69,6 @@ Install using `pip`... # for optional package integrations pip install djangorestframework-jsonapi['django-filter'] pip install djangorestframework-jsonapi['django-polymorphic'] - pip install djangorestframework-jsonapi['openapi'] or from source... @@ -100,8 +99,6 @@ and add `rest_framework_json_api` to your `INSTALLED_APPS` setting below `rest_f Browse to * [http://localhost:8000](http://localhost:8000) for the list of available collections (in a non-JSON:API format!), -* [http://localhost:8000/swagger-ui/](http://localhost:8000/swagger-ui/) for a Swagger user interface to the dynamic schema view, or -* [http://localhost:8000/openapi](http://localhost:8000/openapi) for the schema view's OpenAPI specification document. ## Running Tests diff --git a/docs/usage.md b/docs/usage.md index 1a2cb195..00c12ee8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1054,139 +1054,6 @@ The `prefetch_related` case will issue 4 queries, but they will be small and fas ### Errors --> -## Generating an OpenAPI Specification (OAS) 3.0 schema document - -DRF has a [OAS schema functionality](https://www.django-rest-framework.org/api-guide/schemas/) to generate an -[OAS 3.0 schema](https://www.openapis.org/) as a YAML or JSON file. - -DJA extends DRF's schema support to generate an OAS schema in the JSON:API format. - ---- - -**Deprecation notice:** - -REST framework's built-in support for generating OpenAPI schemas is -**deprecated** in favor of 3rd party packages that can provide this -functionality instead. Therefore we have also deprecated the schema support in -Django REST framework JSON:API. The built-in support will be retired over the -next releases. - -As a full-fledged replacement, we recommend the [drf-spectacular-json-api] package. - ---- - -### AutoSchema Settings - -In order to produce an OAS schema that properly represents the JSON:API structure -you have to either add a `schema` attribute to each view class or set the `REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS']` -to DJA's version of AutoSchema. - -#### View-based - -```python -from rest_framework_json_api.schemas.openapi import AutoSchema - -class MyViewset(ModelViewSet): - schema = AutoSchema - ... -``` - -#### Default schema class - -```python -REST_FRAMEWORK = { - # ... - 'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema', -} -``` - -### Adding additional OAS schema content - -You can extend the OAS schema document by subclassing -[`SchemaGenerator`](https://www.django-rest-framework.org/api-guide/schemas/#schemagenerator) -and extending `get_schema`. - - -Here's an example that adds OAS `info` and `servers` objects. - -```python -from rest_framework_json_api.schemas.openapi import SchemaGenerator as JSONAPISchemaGenerator - - -class MySchemaGenerator(JSONAPISchemaGenerator): - """ - Describe my OAS schema info in detail (overriding what DRF put in) and list the servers where it can be found. - """ - def get_schema(self, request, public): - schema = super().get_schema(request, public) - schema['info'] = { - 'version': '1.0', - 'title': 'my demo API', - 'description': 'A demonstration of [OAS 3.0](https://www.openapis.org)', - 'contact': { - 'name': 'my name' - }, - 'license': { - 'name': 'BSD 2 clause', - 'url': 'https://github.com/django-json-api/django-rest-framework-json-api/blob/main/LICENSE', - } - } - schema['servers'] = [ - {'url': 'http://localhost/v1', 'description': 'local docker'}, - {'url': 'http://localhost:8000/v1', 'description': 'local dev'}, - {'url': 'https://api.example.com/v1', 'description': 'demo server'}, - {'url': '{serverURL}', 'description': 'provide your server URL', - 'variables': {'serverURL': {'default': 'http://localhost:8000/v1'}}} - ] - return schema -``` - -### Generate a Static Schema on Command Line - -See [DRF documentation for generateschema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-static-schema-with-the-generateschema-management-command) -To generate a static OAS schema document, using the `generateschema` management command, you **must override DRF's default** `generator_class` with the DJA-specific version: - -```text -$ ./manage.py generateschema --generator_class rest_framework_json_api.schemas.openapi.SchemaGenerator -``` - -You can then use any number of OAS tools such as -[swagger-ui-watcher](https://www.npmjs.com/package/swagger-ui-watcher) -to render the schema: -```text -$ swagger-ui-watcher myschema.yaml -``` - -Note: Swagger-ui-watcher will complain that "DELETE operations cannot have a requestBody" -but it will still work. This [error](https://github.com/OAI/OpenAPI-Specification/pull/2117) -in the OAS specification will be fixed when [OAS 3.1.0](https://www.openapis.org/blog/2020/06/18/openapi-3-1-0-rc0-its-here) -is published. - -([swagger-ui](https://www.npmjs.com/package/swagger-ui) will work silently.) - -### Generate a Dynamic Schema in a View - -See [DRF documentation for a Dynamic Schema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-dynamic-schema-with-schemaview). - -```python -from rest_framework.schemas import get_schema_view - -urlpatterns = [ - ... - path('openapi', get_schema_view( - title="Example API", - description="API for all things …", - version="1.0.0", - generator_class=MySchemaGenerator, - ), name='openapi-schema'), - path('swagger-ui/', TemplateView.as_view( - template_name='swagger-ui.html', - extra_context={'schema_url': 'openapi-schema'} - ), name='swagger-ui'), - ... -] -``` - ## Third Party Packages ### About Third Party Packages diff --git a/example/settings/dev.py b/example/settings/dev.py index 7b40e61f..05cab4d1 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -83,7 +83,6 @@ "rest_framework_json_api.renderers.BrowsableAPIRenderer", ), "DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata", - "DEFAULT_SCHEMA_CLASS": "rest_framework_json_api.schemas.openapi.AutoSchema", "DEFAULT_FILTER_BACKENDS": ( "rest_framework_json_api.filters.OrderingFilter", "rest_framework_json_api.django_filters.DjangoFilterBackend", diff --git a/example/templates/swagger-ui.html b/example/templates/swagger-ui.html deleted file mode 100644 index 29776491..00000000 --- a/example/templates/swagger-ui.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - Swagger - - - - - -
- - - - \ No newline at end of file diff --git a/example/tests/__snapshots__/test_openapi.ambr b/example/tests/__snapshots__/test_openapi.ambr deleted file mode 100644 index f72c6ff8..00000000 --- a/example/tests/__snapshots__/test_openapi.ambr +++ /dev/null @@ -1,1414 +0,0 @@ -# serializer version: 1 -# name: test_delete_request - ''' - { - "description": "", - "operationId": "destroy/authors/{id}", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/onlymeta" - } - } - }, - "description": "[OK](https://jsonapi.org/format/#crud-deleting-responses-200)" - }, - "202": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/datum" - } - } - }, - "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)" - }, - "204": { - "description": "[no content](https://jsonapi.org/format/#crud-deleting-responses-204)" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Resource does not exist](https://jsonapi.org/format/#crud-deleting-responses-404)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_patch_request - ''' - { - "description": "", - "operationId": "update/authors/{id}", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - } - }, - "required": [ - "data" - ] - } - } - } - }, - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/Author" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "update/authors/{id}" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "403": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-updating-responses-404)" - }, - "409": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Conflict]([Conflict](https://jsonapi.org/format/#crud-updating-responses-409)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_path_with_id_parameter - ''' - { - "description": "", - "operationId": "retrieve/authors/{id}/", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/AuthorDetail" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "retrieve/authors/{id}/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_path_without_parameters - ''' - { - "description": "", - "operationId": "List/authors/", - "parameters": [ - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "A page number within the paginated result set.", - "in": "query", - "name": "page[number]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "Number of results to return per page.", - "in": "query", - "name": "page[size]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/AuthorList" - }, - "type": "array" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "List/authors/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_post_request - ''' - { - "description": "", - "operationId": "create/authors/", - "parameters": [], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "required": [ - "name", - "fullName", - "email" - ], - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "required": [ - "bio", - "entries", - "comments" - ], - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type" - ], - "type": "object" - } - }, - "required": [ - "data" - ] - } - } - } - }, - "responses": { - "201": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/Author" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-201). Assigned `id` and/or any other changes are in this response." - }, - "202": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/datum" - } - } - }, - "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)" - }, - "204": { - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-204) with the supplied `id`. No other changes from what was POSTed." - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "403": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-creating-responses-404)" - }, - "409": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_schema_construction - ''' - { - "components": { - "parameters": { - "fields": { - "description": "[sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets).\nUse fields[\\]=field1,field2,...,fieldN", - "explode": true, - "in": "query", - "name": "fields", - "required": false, - "schema": { - "type": "object" - }, - "style": "deepObject" - }, - "include": { - "description": "[list of included related resources](https://jsonapi.org/format/#fetching-includes)", - "in": "query", - "name": "include", - "required": false, - "schema": { - "type": "string" - }, - "style": "form" - } - }, - "schemas": { - "AuthorList": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "initials": { - "readOnly": true, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "required": [ - "name", - "fullName", - "email" - ], - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "required": [ - "bio", - "entries", - "comments" - ], - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "ResourceIdentifierObject": { - "oneOf": [ - { - "$ref": "#/components/schemas/relationshipToOne" - }, - { - "$ref": "#/components/schemas/relationshipToMany" - } - ] - }, - "datum": { - "description": "singular item", - "properties": { - "data": { - "$ref": "#/components/schemas/resource" - } - } - }, - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "id": { - "type": "string" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "source": { - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - }, - "parameter": { - "description": "A string indicating which query parameter caused the error.", - "type": "string" - }, - "pointer": { - "description": "A [JSON Pointer](https://tools.ietf.org/html/rfc6901) to the associated entity in the request document [e.g. `/data` for a primary data object, or `/data/attributes/title` for a specific attribute.", - "type": "string" - } - }, - "type": "object" - }, - "status": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "type": "object" - }, - "errors": { - "items": { - "$ref": "#/components/schemas/error" - }, - "type": "array", - "uniqueItems": true - }, - "failure": { - "properties": { - "errors": { - "$ref": "#/components/schemas/errors" - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "required": [ - "errors" - ], - "type": "object" - }, - "id": { - "description": "Each resource object\u2019s type and id pair MUST [identify](https://jsonapi.org/format/#document-resource-object-identification) a single, unique resource.", - "type": "string" - }, - "include": { - "additionalProperties": false, - "properties": { - "attributes": { - "additionalProperties": true, - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "relationships": { - "additionalProperties": true, - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "jsonapi": { - "additionalProperties": false, - "description": "The server's implementation", - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - }, - "version": { - "type": "string" - } - }, - "type": "object" - }, - "link": { - "oneOf": [ - { - "description": "a string containing the link's URL", - "format": "uri-reference", - "type": "string" - }, - { - "properties": { - "href": { - "description": "a string containing the link's URL", - "format": "uri-reference", - "type": "string" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "required": [ - "href" - ], - "type": "object" - } - ] - }, - "linkage": { - "description": "the 'type' and 'id'", - "properties": { - "id": { - "$ref": "#/components/schemas/id" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "links": { - "additionalProperties": { - "$ref": "#/components/schemas/link" - }, - "type": "object" - }, - "meta": { - "additionalProperties": true, - "type": "object" - }, - "nulltype": { - "default": null, - "nullable": true, - "type": "object" - }, - "onlymeta": { - "additionalProperties": false, - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - } - } - }, - "pageref": { - "oneOf": [ - { - "format": "uri-reference", - "type": "string" - }, - { - "$ref": "#/components/schemas/nulltype" - } - ] - }, - "pagination": { - "properties": { - "first": { - "$ref": "#/components/schemas/pageref" - }, - "last": { - "$ref": "#/components/schemas/pageref" - }, - "next": { - "$ref": "#/components/schemas/pageref" - }, - "prev": { - "$ref": "#/components/schemas/pageref" - } - }, - "type": "object" - }, - "relationshipLinks": { - "additionalProperties": true, - "description": "optional references to other resource objects", - "properties": { - "related": { - "$ref": "#/components/schemas/link" - }, - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationshipToMany": { - "description": "An array of objects each containing the 'type' and 'id' for to-many relationships", - "items": { - "$ref": "#/components/schemas/linkage" - }, - "type": "array", - "uniqueItems": true - }, - "relationshipToOne": { - "anyOf": [ - { - "$ref": "#/components/schemas/nulltype" - }, - { - "$ref": "#/components/schemas/linkage" - } - ], - "description": "reference to other resource in a to-one relationship" - }, - "reltomany": { - "description": "a multiple 'to-many' relationship", - "properties": { - "data": { - "$ref": "#/components/schemas/relationshipToMany" - }, - "links": { - "$ref": "#/components/schemas/relationshipLinks" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "type": "object" - }, - "reltoone": { - "description": "a singular 'to-one' relationship", - "properties": { - "data": { - "$ref": "#/components/schemas/relationshipToOne" - }, - "links": { - "$ref": "#/components/schemas/relationshipLinks" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "type": "object" - }, - "resource": { - "additionalProperties": false, - "properties": { - "attributes": { - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "relationships": { - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "type": { - "description": "The [type](https://jsonapi.org/format/#document-resource-object-identification) member is used to describe resource objects that share common attributes and relationships.", - "type": "string" - } - } - }, - "info": { - "title": "", - "version": "" - }, - "openapi": "3.0.2", - "paths": { - "/authors/": { - "get": { - "description": "", - "operationId": "List/authors/", - "parameters": [ - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "A page number within the paginated result set.", - "in": "query", - "name": "page[number]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "Number of results to return per page.", - "in": "query", - "name": "page[size]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/AuthorList" - }, - "type": "array" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "List/authors/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - } - } - } - ''' -# --- diff --git a/example/tests/test_openapi.py b/example/tests/test_openapi.py deleted file mode 100644 index fa2f9c73..00000000 --- a/example/tests/test_openapi.py +++ /dev/null @@ -1,230 +0,0 @@ -# largely based on DRF's test_openapi -import json - -import pytest -from django.test import RequestFactory, override_settings -from django.urls import re_path -from rest_framework.request import Request - -from rest_framework_json_api.schemas.openapi import AutoSchema, SchemaGenerator - -from example import views - -pytestmark = pytest.mark.filterwarnings("ignore:Built-in support") - - -def create_request(path): - factory = RequestFactory() - request = Request(factory.get(path)) - return request - - -def create_view_with_kw(view_cls, method, request, initkwargs): - generator = SchemaGenerator() - view = generator.create_view(view_cls.as_view(initkwargs), method, request) - return view - - -def test_path_without_parameters(snapshot): - path = "/authors/" - method = "GET" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"get": "list"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_path_with_id_parameter(snapshot): - path = "/authors/{id}/" - method = "GET" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"get": "retrieve"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_post_request(snapshot): - method = "POST" - path = "/authors/" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"post": "create"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_patch_request(snapshot): - method = "PATCH" - path = "/authors/{id}" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"patch": "update"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_delete_request(snapshot): - method = "DELETE" - path = "/authors/{id}" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"delete": "delete"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -@override_settings( - REST_FRAMEWORK={ - "DEFAULT_SCHEMA_CLASS": "rest_framework_json_api.schemas.openapi.AutoSchema" - } -) -def test_schema_construction(snapshot): - """Construction of the top level dictionary.""" - patterns = [ - re_path("^authors/?$", views.AuthorViewSet.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - assert snapshot == json.dumps(schema, indent=2, sort_keys=True) - - -def test_schema_id_field(): - """ID field is only included in the root, not the attributes.""" - patterns = [ - re_path("^companies/?$", views.CompanyViewset.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - company_properties = schema["components"]["schemas"]["Company"]["properties"] - assert company_properties["id"] == {"$ref": "#/components/schemas/id"} - assert "id" not in company_properties["attributes"]["properties"] - - -def test_schema_subserializers(): - """Schema for child Serializers reflects the actual response structure.""" - patterns = [ - re_path( - "^questionnaires/?$", views.QuestionnaireViewset.as_view({"get": "list"}) - ), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - assert { - "type": "object", - "properties": { - "metadata": { - "type": "object", - "properties": { - "author": {"type": "string"}, - "producer": {"type": "string"}, - }, - "required": ["author"], - }, - "questions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "text": {"type": "string"}, - "required": {"type": "boolean", "default": False}, - }, - "required": ["text"], - }, - }, - "name": {"type": "string", "maxLength": 100}, - }, - "required": ["name", "questions", "metadata"], - } == schema["components"]["schemas"]["Questionnaire"]["properties"]["attributes"] - - -def test_schema_parameters_include(): - """Include paramater is only used when serializer defines included_serializers.""" - patterns = [ - re_path("^authors/?$", views.AuthorViewSet.as_view({"get": "list"})), - re_path("^project-types/?$", views.ProjectTypeViewset.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - include_ref = {"$ref": "#/components/parameters/include"} - assert include_ref in schema["paths"]["/authors/"]["get"]["parameters"] - assert include_ref not in schema["paths"]["/project-types/"]["get"]["parameters"] - - -def test_schema_serializer_method_resource_related_field(): - """SerializerMethodResourceRelatedField fieds have the correct relation ref.""" - patterns = [ - re_path("^entries/?$", views.EntryViewSet.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = Request(RequestFactory().get("/", {"include": "featured"})) - schema = generator.get_schema(request=request) - - entry_schema = schema["components"]["schemas"]["Entry"] - entry_relationships = entry_schema["properties"]["relationships"]["properties"] - - rel_to_many_ref = {"$ref": "#/components/schemas/reltomany"} - assert entry_relationships["suggested"] == rel_to_many_ref - assert entry_relationships["suggestedHyperlinked"] == rel_to_many_ref - - rel_to_one_ref = {"$ref": "#/components/schemas/reltoone"} - assert entry_relationships["featured"] == rel_to_one_ref - assert entry_relationships["featuredHyperlinked"] == rel_to_one_ref - - -def test_schema_related_serializers(): - """ - Confirm that paths are generated for related fields. For example: - /authors/{pk}/{related_field>} - /authors/{id}/comments/ - /authors/{id}/entries/ - /authors/{id}/first_entry/ - and confirm that the schema for the related field is properly rendered - """ - generator = SchemaGenerator() - request = create_request("/") - schema = generator.get_schema(request=request) - # make sure the path's relationship and related {related_field}'s got expanded - assert "/authors/{id}/relationships/{related_field}" in schema["paths"] - assert "/authors/{id}/comments/" in schema["paths"] - assert "/authors/{id}/entries/" in schema["paths"] - assert "/authors/{id}/first_entry/" in schema["paths"] - first_get = schema["paths"]["/authors/{id}/first_entry/"]["get"]["responses"]["200"] - first_schema = first_get["content"]["application/vnd.api+json"]["schema"] - first_props = first_schema["properties"]["data"] - assert "$ref" in first_props - assert first_props["$ref"] == "#/components/schemas/Entry" diff --git a/example/tests/unit/test_filter_schema_params.py b/example/tests/unit/test_filter_schema_params.py deleted file mode 100644 index d7cb4fb8..00000000 --- a/example/tests/unit/test_filter_schema_params.py +++ /dev/null @@ -1,107 +0,0 @@ -from rest_framework import filters as drf_filters - -from rest_framework_json_api import filters as dja_filters -from rest_framework_json_api.django_filters import backends - -from example.views import EntryViewSet - - -class DummyEntryViewSet(EntryViewSet): - filter_backends = ( - dja_filters.QueryParameterValidationFilter, - dja_filters.OrderingFilter, - backends.DjangoFilterBackend, - drf_filters.SearchFilter, - ) - filterset_fields = { - "id": ("exact",), - "headline": ("exact", "contains"), - "blog__name": ("contains",), - } - - def __init__(self, **kwargs): - # dummy up self.request since PreloadIncludesMixin expects it to be defined - self.request = None - super().__init__(**kwargs) - - -def test_filters_get_schema_params(): - """ - test all my filters for `get_schema_operation_parameters()` - """ - # list of tuples: (filter, expected result) - filters = [ - (dja_filters.QueryParameterValidationFilter, []), - ( - backends.DjangoFilterBackend, - [ - { - "name": "filter[id]", - "required": False, - "in": "query", - "description": "id", - "schema": {"type": "string"}, - }, - { - "name": "filter[headline]", - "required": False, - "in": "query", - "description": "headline", - "schema": {"type": "string"}, - }, - { - "name": "filter[headline.contains]", - "required": False, - "in": "query", - "description": "headline__contains", - "schema": {"type": "string"}, - }, - { - "name": "filter[blog.name.contains]", - "required": False, - "in": "query", - "description": "blog__name__contains", - "schema": {"type": "string"}, - }, - ], - ), - ( - dja_filters.OrderingFilter, - [ - { - "name": "sort", - "required": False, - "in": "query", - "description": "[list of fields to sort by]" - "(https://jsonapi.org/format/#fetching-sorting)", - "schema": {"type": "string"}, - } - ], - ), - ( - drf_filters.SearchFilter, - [ - { - "name": "filter[search]", - "required": False, - "in": "query", - "description": "A search term.", - "schema": {"type": "string"}, - } - ], - ), - ] - view = DummyEntryViewSet() - - for c, expected in filters: - f = c() - result = f.get_schema_operation_parameters(view) - assert len(result) == len(expected) - if len(result) == 0: - continue - # py35: the result list/dict ordering isn't guaranteed - for res_item in result: - assert "name" in res_item - for exp_item in expected: - if res_item["name"] == exp_item["name"]: - assert res_item == exp_item diff --git a/example/urls.py b/example/urls.py index 413d058d..471fbe81 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,9 +1,5 @@ from django.urls import include, path, re_path -from django.views.generic import TemplateView from rest_framework import routers -from rest_framework.schemas import get_schema_view - -from rest_framework_json_api.schemas.openapi import SchemaGenerator from example.views import ( AuthorRelationshipView, @@ -87,22 +83,4 @@ AuthorRelationshipView.as_view(), name="author-relationships", ), - path( - "openapi", - get_schema_view( - title="Example API", - description="API for all things …", - version="1.0.0", - generator_class=SchemaGenerator, - ), - name="openapi-schema", - ), - path( - "swagger-ui/", - TemplateView.as_view( - template_name="swagger-ui.html", - extra_context={"schema_url": "openapi-schema"}, - ), - name="swagger-ui", - ), ] diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 589636e6..3db600e2 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -3,5 +3,3 @@ django-filter==24.3 # should be set to pinned version again # see https://github.com/django-polymorphic/django-polymorphic/pull/541 django-polymorphic@git+https://github.com/django-polymorphic/django-polymorphic@master # pyup: ignore -pyyaml==6.0.2 -uritemplate==4.1.1 diff --git a/rest_framework_json_api/django_filters/backends.py b/rest_framework_json_api/django_filters/backends.py index c0044839..70e543c1 100644 --- a/rest_framework_json_api/django_filters/backends.py +++ b/rest_framework_json_api/django_filters/backends.py @@ -4,7 +4,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings -from rest_framework_json_api.utils import format_field_name, undo_format_field_name +from rest_framework_json_api.utils import undo_format_field_name class DjangoFilterBackend(DjangoFilterBackend): @@ -129,18 +129,3 @@ def get_filterset_kwargs(self, request, queryset, view): "request": request, "filter_keys": filter_keys, } - - def get_schema_operation_parameters(self, view): - """ - Convert backend filter `name` to JSON:API-style `filter[name]`. - For filters that are relationship paths, rewrite ORM-style `__` to our preferred `.`. - For example: `blog__name__contains` becomes `filter[blog.name.contains]`. - - This is basically the reverse of `get_filterset_kwargs` above. - """ - result = super().get_schema_operation_parameters(view) - for res in result: - if "name" in res: - name = format_field_name(res["name"].replace("__", ".")) - res["name"] = f"filter[{name}]" - return result diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py deleted file mode 100644 index 6892e991..00000000 --- a/rest_framework_json_api/schemas/openapi.py +++ /dev/null @@ -1,905 +0,0 @@ -import warnings -from urllib.parse import urljoin - -from rest_framework.fields import empty -from rest_framework.relations import ManyRelatedField -from rest_framework.schemas import openapi as drf_openapi -from rest_framework.schemas.utils import is_list_view - -from rest_framework_json_api import serializers, views -from rest_framework_json_api.relations import ManySerializerMethodResourceRelatedField -from rest_framework_json_api.utils import format_field_name - - -class SchemaGenerator(drf_openapi.SchemaGenerator): - """ - Extend DRF's SchemaGenerator to implement JSON:API flavored generateschema command. - """ - - #: These JSON:API component definitions are referenced by the generated OAS schema. - #: If you need to add more or change these static component definitions, extend this dict. - jsonapi_components = { - "schemas": { - "jsonapi": { - "type": "object", - "description": "The server's implementation", - "properties": { - "version": {"type": "string"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - "additionalProperties": False, - }, - "resource": { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "attributes": { - "type": "object", - # ... - }, - "relationships": { - "type": "object", - # ... - }, - "links": {"$ref": "#/components/schemas/links"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "include": { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "attributes": { - "type": "object", - "additionalProperties": True, - # ... - }, - "relationships": { - "type": "object", - "additionalProperties": True, - # ... - }, - "links": {"$ref": "#/components/schemas/links"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "link": { - "oneOf": [ - { - "description": "a string containing the link's URL", - "type": "string", - "format": "uri-reference", - }, - { - "type": "object", - "required": ["href"], - "properties": { - "href": { - "description": "a string containing the link's URL", - "type": "string", - "format": "uri-reference", - }, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - ] - }, - "links": { - "type": "object", - "additionalProperties": {"$ref": "#/components/schemas/link"}, - }, - "reltoone": { - "description": "a singular 'to-one' relationship", - "type": "object", - "properties": { - "links": {"$ref": "#/components/schemas/relationshipLinks"}, - "data": {"$ref": "#/components/schemas/relationshipToOne"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "relationshipToOne": { - "description": "reference to other resource in a to-one relationship", - "anyOf": [ - {"$ref": "#/components/schemas/nulltype"}, - {"$ref": "#/components/schemas/linkage"}, - ], - }, - "reltomany": { - "description": "a multiple 'to-many' relationship", - "type": "object", - "properties": { - "links": {"$ref": "#/components/schemas/relationshipLinks"}, - "data": {"$ref": "#/components/schemas/relationshipToMany"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "relationshipLinks": { - "description": "optional references to other resource objects", - "type": "object", - "additionalProperties": True, - "properties": { - "self": {"$ref": "#/components/schemas/link"}, - "related": {"$ref": "#/components/schemas/link"}, - }, - }, - "relationshipToMany": { - "description": "An array of objects each containing the " - "'type' and 'id' for to-many relationships", - "type": "array", - "items": {"$ref": "#/components/schemas/linkage"}, - "uniqueItems": True, - }, - # A RelationshipView uses a ResourceIdentifierObjectSerializer (hence the name - # ResourceIdentifierObject returned by get_component_name()) which serializes type - # and id. These can be lists or individual items depending on whether the - # relationship is toMany or toOne so offer both options since we are not iterating - # over all the possible {related_field}'s but rather rendering one path schema - # which may represent toMany and toOne relationships. - "ResourceIdentifierObject": { - "oneOf": [ - {"$ref": "#/components/schemas/relationshipToOne"}, - {"$ref": "#/components/schemas/relationshipToMany"}, - ] - }, - "linkage": { - "type": "object", - "description": "the 'type' and 'id'", - "required": ["type", "id"], - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "pagination": { - "type": "object", - "properties": { - "first": {"$ref": "#/components/schemas/pageref"}, - "last": {"$ref": "#/components/schemas/pageref"}, - "prev": {"$ref": "#/components/schemas/pageref"}, - "next": {"$ref": "#/components/schemas/pageref"}, - }, - }, - "pageref": { - "oneOf": [ - {"type": "string", "format": "uri-reference"}, - {"$ref": "#/components/schemas/nulltype"}, - ] - }, - "failure": { - "type": "object", - "required": ["errors"], - "properties": { - "errors": {"$ref": "#/components/schemas/errors"}, - "meta": {"$ref": "#/components/schemas/meta"}, - "jsonapi": {"$ref": "#/components/schemas/jsonapi"}, - "links": {"$ref": "#/components/schemas/links"}, - }, - }, - "errors": { - "type": "array", - "items": {"$ref": "#/components/schemas/error"}, - "uniqueItems": True, - }, - "error": { - "type": "object", - "additionalProperties": False, - "properties": { - "id": {"type": "string"}, - "status": {"type": "string"}, - "links": {"$ref": "#/components/schemas/links"}, - "code": {"type": "string"}, - "title": {"type": "string"}, - "detail": {"type": "string"}, - "source": { - "type": "object", - "properties": { - "pointer": { - "type": "string", - "description": ( - "A [JSON Pointer](https://tools.ietf.org/html/rfc6901) " - "to the associated entity in the request document " - "[e.g. `/data` for a primary data object, or " - "`/data/attributes/title` for a specific attribute." - ), - }, - "parameter": { - "type": "string", - "description": "A string indicating which query parameter " - "caused the error.", - }, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - }, - }, - "onlymeta": { - "additionalProperties": False, - "properties": {"meta": {"$ref": "#/components/schemas/meta"}}, - }, - "meta": {"type": "object", "additionalProperties": True}, - "datum": { - "description": "singular item", - "properties": {"data": {"$ref": "#/components/schemas/resource"}}, - }, - "nulltype": {"type": "object", "nullable": True, "default": None}, - "type": { - "type": "string", - "description": "The [type]" - "(https://jsonapi.org/format/#document-resource-object-identification) " - "member is used to describe resource objects that share common attributes " - "and relationships.", - }, - "id": { - "type": "string", - "description": "Each resource object’s type and id pair MUST " - "[identify]" - "(https://jsonapi.org/format/#document-resource-object-identification) " - "a single, unique resource.", - }, - }, - "parameters": { - "include": { - "name": "include", - "in": "query", - "description": "[list of included related resources]" - "(https://jsonapi.org/format/#fetching-includes)", - "required": False, - "style": "form", - "schema": {"type": "string"}, - }, - # TODO: deepObject not well defined/supported: - # https://github.com/OAI/OpenAPI-Specification/issues/1706 - "fields": { - "name": "fields", - "in": "query", - "description": "[sparse fieldsets]" - "(https://jsonapi.org/format/#fetching-sparse-fieldsets).\n" - "Use fields[\\]=field1,field2,...,fieldN", - "required": False, - "style": "deepObject", - "schema": { - "type": "object", - }, - "explode": True, - }, - }, - } - - def get_schema(self, request=None, public=False): - """ - Generate a JSON:API OpenAPI schema. - Overrides upstream DRF's get_schema. - """ - # TODO: avoid copying so much of upstream get_schema() - schema = super().get_schema(request, public) - - components_schemas = {} - - # Iterate endpoints generating per method path operations. - paths = {} - _, view_endpoints = self._get_paths_and_endpoints(None if public else request) - - #: `expanded_endpoints` is like view_endpoints with one extra field tacked on: - #: - 'action' copy of current view.action (list/fetch) as this gets reset for - # each request. - expanded_endpoints = [] - for path, method, view in view_endpoints: - if hasattr(view, "action") and view.action == "retrieve_related": - expanded_endpoints += self._expand_related( - path, method, view, view_endpoints - ) - else: - expanded_endpoints.append( - (path, method, view, getattr(view, "action", None)) - ) - - for path, method, view, action in expanded_endpoints: - if not self.has_view_permissions(path, method, view): - continue - # kludge to preserve view.action as it is 'list' for the parent ViewSet - # but the related viewset that was expanded may be either 'fetch' (to_one) or 'list' - # (to_many). This patches the view.action appropriately so that - # view.schema.get_operation() "does the right thing" for fetch vs. list. - current_action = None - if hasattr(view, "action"): - current_action = view.action - view.action = action - operation = view.schema.get_operation(path, method) - components = view.schema.get_components(path, method) - for k in components.keys(): - if k not in components_schemas: - continue - if components_schemas[k] == components[k]: - continue - warnings.warn( - f'Schema component "{k}" has been overriden with a different value.', - stacklevel=1, - ) - - components_schemas.update(components) - - if hasattr(view, "action"): - view.action = current_action - # Normalise path for any provided mount url. - if path.startswith("/"): - path = path[1:] - path = urljoin(self.url or "/", path) - - paths.setdefault(path, {}) - paths[path][method.lower()] = operation - - self.check_duplicate_operation_id(paths) - - # Compile final schema, overriding stuff from super class. - schema["paths"] = paths - schema["components"] = self.jsonapi_components - schema["components"]["schemas"].update(components_schemas) - - return schema - - def _expand_related(self, path, method, view, view_endpoints): - """ - Expand path containing .../{id}/{related_field} into list of related fields - and **their** views, making sure toOne relationship's views are a 'fetch' and toMany - relationship's are a 'list'. - :param path - :param method - :param view - :param view_endpoints - :return:list[tuple(path, method, view, action)] - """ - result = [] - serializer = view.get_serializer() - # It's not obvious if it's allowed to have both included_ and related_ serializers, - # so just merge both dicts. - serializers = {} - if hasattr(serializer, "included_serializers"): - serializers = {**serializers, **serializer.included_serializers} - if hasattr(serializer, "related_serializers"): - serializers = {**serializers, **serializer.related_serializers} - related_fields = [fs for fs in serializers.items()] - - for field, related_serializer in related_fields: - related_view = self._find_related_view( - view_endpoints, related_serializer, view - ) - if related_view: - action = self._field_is_one_or_many(field, view) - result.append( - ( - path.replace("{related_field}", field), - method, - related_view, - action, - ) - ) - - return result - - def _find_related_view(self, view_endpoints, related_serializer, parent_view): - """ - For a given related_serializer, try to find it's "parent" view instance. - - :param view_endpoints: list of all view endpoints - :param related_serializer: the related serializer for a given related field - :param parent_view: the parent view (used to find toMany vs. toOne). - TODO: not actually used. - :return:view - """ - for _path, _method, view in view_endpoints: - view_serializer = view.get_serializer() - if isinstance(view_serializer, related_serializer): - return view - - return None - - def _field_is_one_or_many(self, field, view): - serializer = view.get_serializer() - if isinstance(serializer.fields[field], ManyRelatedField): - return "list" - else: - return "fetch" - - -class AutoSchema(drf_openapi.AutoSchema): - """ - Extend DRF's openapi.AutoSchema for JSON:API serialization. - """ - - #: ignore all the media types and only generate a JSON:API schema. - content_types = ["application/vnd.api+json"] - - def get_operation(self, path, method): - """ - JSON:API adds some standard fields to the API response that are not in upstream DRF: - - some that only apply to GET/HEAD methods. - - collections - - special handling for POST, PATCH, DELETE - """ - - warnings.warn( - DeprecationWarning( - "Built-in support for generating OpenAPI schema is deprecated. " - "Use drf-spectacular-json-api instead see " - "https://github.com/jokiefer/drf-spectacular-json-api/" - ), - stacklevel=2, - ) - - operation = {} - operation["operationId"] = self.get_operation_id(path, method) - operation["description"] = self.get_description(path, method) - - serializer = self.get_response_serializer(path, method) - - parameters = [] - parameters += self.get_path_parameters(path, method) - # pagination, filters only apply to GET/HEAD of collections and items - if method in ["GET", "HEAD"]: - parameters += self._get_include_parameters(path, method, serializer) - parameters += self._get_fields_parameters(path, method) - parameters += self.get_pagination_parameters(path, method) - parameters += self.get_filter_parameters(path, method) - operation["parameters"] = parameters - operation["tags"] = self.get_tags(path, method) - - # get request and response code schemas - if method == "GET": - if is_list_view(path, method, self.view): - self._add_get_collection_response(operation, path) - else: - self._add_get_item_response(operation, path) - elif method == "POST": - self._add_post_item_response(operation, path) - elif method == "PATCH": - self._add_patch_item_response(operation, path) - elif method == "DELETE": - # should only allow deleting a resource, not a collection - # TODO: implement delete of a relationship in future release. - self._add_delete_item_response(operation, path) - return operation - - def get_operation_id(self, path, method): - """ - The upstream DRF version creates non-unique operationIDs, because the same view is - used for the main path as well as such as related and relationships. - This concatenates the (mapped) method name and path as the spec allows most any - """ - method_name = getattr(self.view, "action", method.lower()) - if is_list_view(path, method, self.view): - action = "List" - elif method_name not in self.method_mapping: - action = method_name - else: - action = self.method_mapping[method.lower()] - return action + path - - def _get_include_parameters(self, path, method, serializer): - """ - includes parameter: https://jsonapi.org/format/#fetching-includes - """ - if getattr(serializer, "included_serializers", {}): - return [{"$ref": "#/components/parameters/include"}] - return [] - - def _get_fields_parameters(self, path, method): - """ - sparse fieldsets https://jsonapi.org/format/#fetching-sparse-fieldsets - """ - # TODO: See if able to identify the specific types for fields[type]=... and return this: - # name: fields - # in: query - # description: '[sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets)' # noqa: B950 - # required: true - # style: deepObject - # schema: - # type: object - # properties: - # hello: - # type: string # noqa F821 - # world: - # type: string # noqa F821 - # explode: true - return [{"$ref": "#/components/parameters/fields"}] - - def _add_get_collection_response(self, operation, path): - """ - Add GET 200 response for a collection to operation - """ - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "GET", collection=True - ) - } - self._add_get_4xx_responses(operation) - - def _add_get_item_response(self, operation, path): - """ - add GET 200 response for an item to operation - """ - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "GET", collection=False - ) - } - self._add_get_4xx_responses(operation) - - def _get_toplevel_200_response(self, operation, path, method, collection=True): - """ - return top-level JSON:API GET 200 response - - :param collection: True for collections; False for individual items. - - Uses a $ref to the components.schemas. component definition. - """ - if collection: - data = { - "type": "array", - "items": self.get_reference(self.get_response_serializer(path, method)), - } - else: - data = self.get_reference(self.get_response_serializer(path, method)) - - return { - "description": operation["operationId"], - "content": { - "application/vnd.api+json": { - "schema": { - "type": "object", - "required": ["data"], - "properties": { - "data": data, - "included": { - "type": "array", - "uniqueItems": True, - "items": {"$ref": "#/components/schemas/include"}, - }, - "links": { - "description": "Link members related to primary data", - "allOf": [ - {"$ref": "#/components/schemas/links"}, - {"$ref": "#/components/schemas/pagination"}, - ], - }, - "jsonapi": {"$ref": "#/components/schemas/jsonapi"}, - }, - } - } - }, - } - - def _add_post_item_response(self, operation, path): - """ - add response for POST of an item to operation - """ - operation["requestBody"] = self.get_request_body(path, "POST") - operation["responses"] = { - "201": self._get_toplevel_200_response( - operation, path, "POST", collection=False - ) - } - operation["responses"]["201"]["description"] = ( - "[Created](https://jsonapi.org/format/#crud-creating-responses-201). " - "Assigned `id` and/or any other changes are in this response." - ) - self._add_async_response(operation) - operation["responses"]["204"] = { - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-204) " - "with the supplied `id`. No other changes from what was POSTed." - } - self._add_post_4xx_responses(operation) - - def _add_patch_item_response(self, operation, path): - """ - Add PATCH response for an item to operation - """ - operation["requestBody"] = self.get_request_body(path, "PATCH") - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "PATCH", collection=False - ) - } - self._add_patch_4xx_responses(operation) - - def _add_delete_item_response(self, operation, path): - """ - add DELETE response for item or relationship(s) to operation - """ - # Only DELETE of relationships has a requestBody - if isinstance(self.view, views.RelationshipView): - operation["requestBody"] = self.get_request_body(path, "DELETE") - self._add_delete_responses(operation) - - def get_request_body(self, path, method): - """ - A request body is required by JSON:API for POST, PATCH, and DELETE methods. - """ - serializer = self.get_request_serializer(path, method) - if not isinstance(serializer, (serializers.BaseSerializer,)): - return {} - is_relationship = isinstance(self.view, views.RelationshipView) - - # DRF uses a $ref to the component schema definition, but this - # doesn't work for JSON:API due to the different required fields based on - # the method, so make those changes and inline another copy of the schema. - - # TODO: A future improvement could make this DRYer with multiple component schemas: - # A base schema for each viewset that has no required fields - # One subclassed from the base that requires some fields (`type` but not `id` for POST) - # Another subclassed from base with required type/id but no required attributes (PATCH) - - if is_relationship: - item_schema = {"$ref": "#/components/schemas/ResourceIdentifierObject"} - else: - item_schema = self.map_serializer(serializer) - if method == "POST": - # 'type' and 'id' are both required for: - # - all relationship operations - # - regular PATCH or DELETE - # Only 'type' is required for POST: system may assign the 'id'. - item_schema["required"] = ["type"] - - if "properties" in item_schema and "attributes" in item_schema["properties"]: - # No required attributes for PATCH - if ( - method in ["PATCH", "PUT"] - and "required" in item_schema["properties"]["attributes"] - ): - del item_schema["properties"]["attributes"]["required"] - # No read_only fields for request. - for name, schema in ( - item_schema["properties"]["attributes"]["properties"].copy().items() - ): # noqa E501 - if "readOnly" in schema: - del item_schema["properties"]["attributes"]["properties"][name] - - if "properties" in item_schema and "relationships" in item_schema["properties"]: - # No required relationships for PATCH - if ( - method in ["PATCH", "PUT"] - and "required" in item_schema["properties"]["relationships"] - ): - del item_schema["properties"]["relationships"]["required"] - - return { - "content": { - ct: { - "schema": { - "required": ["data"], - "properties": {"data": item_schema}, - } - } - for ct in self.content_types - } - } - - def map_serializer(self, serializer): - """ - Custom map_serializer that serializes the schema using the JSON:API spec. - - Non-attributes like related and identity fields, are moved to 'relationships' - and 'links'. - """ - # TODO: remove attributes, etc. for relationshipView?? - if isinstance( - serializer.parent, (serializers.ListField, serializers.BaseSerializer) - ): - # Return plain non-JSON:API serializer schema for serializers nested inside - # a Serializer or a ListField, as those don't use the full JSON:API - # serializer schemas. - return super().map_serializer(serializer) - - required = [] - attributes = {} - relationships_required = [] - relationships = {} - - for field in serializer.fields.values(): - if isinstance(field, serializers.HyperlinkedIdentityField): - # the 'url' is not an attribute but rather a self.link, so don't map it here. - continue - if isinstance(field, serializers.HiddenField): - continue - if isinstance( - field, - ( - serializers.ManyRelatedField, - ManySerializerMethodResourceRelatedField, - ), - ): - if field.required: - relationships_required.append(format_field_name(field.field_name)) - relationships[format_field_name(field.field_name)] = { - "$ref": "#/components/schemas/reltomany" - } - continue - if isinstance(field, serializers.RelatedField): - if field.required: - relationships_required.append(format_field_name(field.field_name)) - relationships[format_field_name(field.field_name)] = { - "$ref": "#/components/schemas/reltoone" - } - continue - if field.field_name == "id": - # ID is always provided in the root of JSON:API and removed from the - # attributes in JSONRenderer. - continue - - if field.required: - required.append(format_field_name(field.field_name)) - - schema = self.map_field(field) - if field.read_only: - schema["readOnly"] = True - if field.write_only: - schema["writeOnly"] = True - if field.allow_null: - schema["nullable"] = True - if field.default and field.default != empty and not callable(field.default): - schema["default"] = field.default - if field.help_text: - # Ensure django gettext_lazy is rendered correctly - schema["description"] = str(field.help_text) - self.map_field_validators(field, schema) - - attributes[format_field_name(field.field_name)] = schema - - result = { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "links": { - "type": "object", - "properties": {"self": {"$ref": "#/components/schemas/link"}}, - }, - }, - } - if attributes: - result["properties"]["attributes"] = { - "type": "object", - "properties": attributes, - } - if required: - result["properties"]["attributes"]["required"] = required - - if relationships: - result["properties"]["relationships"] = { - "type": "object", - "properties": relationships, - } - if relationships_required: - result["properties"]["relationships"][ - "required" - ] = relationships_required - return result - - def _add_async_response(self, operation): - """ - Add async response to operation - """ - operation["responses"]["202"] = { - "description": "Accepted for [asynchronous processing]" - "(https://jsonapi.org/recommendations/#asynchronous-processing)", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/datum"} - } - }, - } - - def _failure_response(self, reason): - """ - Return failure response reason as the description - """ - return { - "description": reason, - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/failure"} - } - }, - } - - def _add_generic_failure_responses(self, operation): - """ - Add generic failure response(s) to operation - """ - for code, reason in [ - ("400", "bad request"), - ("401", "not authorized"), - ("429", "too many requests"), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_get_4xx_responses(self, operation): - """ - Add generic 4xx GET responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [("404", "not found")]: - operation["responses"][code] = self._failure_response(reason) - - def _add_post_4xx_responses(self, operation): - """ - Add POST 4xx error responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "403", - "[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)", - ), - ( - "404", - "[Related resource does not exist]" - "(https://jsonapi.org/format/#crud-creating-responses-404)", - ), - ( - "409", - "[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_patch_4xx_responses(self, operation): - """ - Add PATCH 4xx error responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "403", - "[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)", - ), - ( - "404", - "[Related resource does not exist]" - "(https://jsonapi.org/format/#crud-updating-responses-404)", - ), - ( - "409", - "[Conflict]([Conflict]" - "(https://jsonapi.org/format/#crud-updating-responses-409)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_delete_responses(self, operation): - """ - Add generic DELETE responses to operation - """ - # the 2xx statuses: - operation["responses"] = { - "200": { - "description": "[OK](https://jsonapi.org/format/#crud-deleting-responses-200)", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/onlymeta"} - } - }, - } - } - self._add_async_response(operation) - operation["responses"]["204"] = { - "description": "[no content](https://jsonapi.org/format/#crud-deleting-responses-204)", # noqa: B950 - } - # the 4xx errors: - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "404", - "[Resource does not exist]" - "(https://jsonapi.org/format/#crud-deleting-responses-404)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) diff --git a/setup.cfg b/setup.cfg index 4230dcbb..92606700 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,9 +63,6 @@ DJANGO_SETTINGS_MODULE=example.settings.test filterwarnings = error::DeprecationWarning error::PendingDeprecationWarning - # Django filter schema generation. Can be removed once we remove - # schema support - ignore:Built-in schema generation is deprecated. testpaths = example tests diff --git a/setup.py b/setup.py index de61b0d1..0b88f4c9 100755 --- a/setup.py +++ b/setup.py @@ -112,7 +112,6 @@ def get_package_data(package): extras_require={ "django-polymorphic": ["django-polymorphic>=3.0"], "django-filter": ["django-filter>=2.4"], - "openapi": ["pyyaml>=5.4", "uritemplate>=3.0.1"], }, setup_requires=wheel, python_requires=">=3.9", diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py deleted file mode 100644 index 427f18fc..00000000 --- a/tests/schemas/test_openapi.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework_json_api.schemas.openapi import AutoSchema -from tests.serializers import CallableDefaultSerializer - - -class TestAutoSchema: - def test_schema_callable_default(self): - inspector = AutoSchema() - result = inspector.map_serializer(CallableDefaultSerializer()) - assert result["properties"]["attributes"]["properties"]["field"] == { - "type": "string", - } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy