diff --git a/.github/workflows/oas.yml b/.github/workflows/oas.yml index e0c22557..a020720a 100644 --- a/.github/workflows/oas.yml +++ b/.github/workflows/oas.yml @@ -26,3 +26,4 @@ jobs: openapi-to-postman-version: '^5.0.0' postman-artifact-name: objects-api-postman-collection openapi-generator-version: '^2.20.0' + spectral-ruleset: https://static.developer.overheid.nl/adr/2.1/ruleset.yaml diff --git a/docs/manual/logging.rst b/docs/manual/logging.rst index bda4d9d4..3b8e3f64 100644 --- a/docs/manual/logging.rst +++ b/docs/manual/logging.rst @@ -63,6 +63,7 @@ Objects API * ``search_failed_for_datastore``: attempted to perform ``jsonpath`` search for a backend that does not support this operation. Additional context: ``exc_info``. * ``object_created``: created an ``Object`` via the API. Additional context: ``object_uuid``, ``objecttype_uuid``, ``objecttype_version``, ``token_identifier``, ``token_application``. * ``object_updated``: updated an ``Object`` via the API. Additional context: ``object_uuid``, ``objecttype_uuid``, ``objecttype_version``, ``token_identifier``, ``token_application``. +* ``deprecated_endpoint_called``: a deprecated endpoint was called. Additional context: ``endpoint``. Objecttypes API ~~~~~~~~~~~~~~~ @@ -73,6 +74,7 @@ Objecttypes API * ``object_version_created``: created an ``Object_version`` via the API. Additional context: ``version``, ``objecttype_uuid``, ``token_identifier``, ``token_application``. * ``object_version_updated``: updated an ``Object_version`` via the API. Additional context: ``version``, ``objecttype_uuid``, ``token_identifier``, ``token_application``. * ``object_version_deleted``: deleted an ``Object_version`` via the API. Additional context: ``version``, ``objecttype_uuid``, ``token_identifier``, ``token_application``. +* ``deprecated_endpoint_called``: a deprecated endpoint was called. Additional context: ``endpoint``. Setup configuration ~~~~~~~~~~~~~~~~~~~ diff --git a/src/objects/api/v2/openapi.yaml b/src/objects/api/v2/openapi.yaml index 3fc1b553..8ca8d0c0 100644 --- a/src/objects/api/v2/openapi.yaml +++ b/src/objects/api/v2/openapi.yaml @@ -70,6 +70,8 @@ info: `objecten` channel. contact: url: https://github.com/maykinmedia/objects-api + email: support@maykinmedia.nl + name: Maykin Media license: name: EUPL-1.2 paths: @@ -234,6 +236,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' X-Unauthorized-Fields: schema: type: string @@ -297,6 +304,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -345,6 +357,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' X-Unauthorized-Fields: schema: type: string @@ -415,6 +432,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -480,6 +502,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -502,6 +529,12 @@ paths: - tokenAuth: [] responses: '204': + headers: + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' description: No response body /objects/{uuid}/{index}: get: @@ -543,6 +576,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -596,6 +634,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -763,6 +806,11 @@ paths: description: 'The ''Coordinate Reference System'' (CRS) of the request data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is the same as WGS84).' + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: @@ -792,6 +840,12 @@ paths: - {} responses: '200': + headers: + API-version: + schema: + type: string + description: 'Geeft een specifieke API-versie aan in de context van + een specifieke aanroep. Voorbeeld: 1.2.1.' content: application/json: schema: diff --git a/src/objects/api/v2/urls.py b/src/objects/api/v2/urls.py index c93f41af..d618a549 100644 --- a/src/objects/api/v2/urls.py +++ b/src/objects/api/v2/urls.py @@ -1,12 +1,16 @@ from django.urls import include, path from drf_spectacular.views import ( - SpectacularJSONAPIView, SpectacularRedocView, - SpectacularYAMLAPIView, ) from rest_framework import routers +from objects.utils.oas_extensions.views import ( + DeprecationRedirectView, + SpectacularJSONAPIView, + SpectacularYAMLAPIView, +) + from .views import ObjectViewSet, PermissionViewSet router = routers.DefaultRouter(trailing_slash=False) @@ -16,20 +20,27 @@ app_name = "v2" urlpatterns = [ - path("", SpectacularJSONAPIView.as_view(), name="schema-json"), path( "/", include( [ - # schema path( "schema/openapi.yaml", + DeprecationRedirectView.as_view(pattern_name="v2:schema-yaml"), + ), + path( + "openapi.yaml", SpectacularYAMLAPIView.as_view(), - name="schema", + name="schema-yaml", + ), + path( + "openapi.json", + SpectacularJSONAPIView.as_view(), + name="schema-json", ), path( "schema/", - SpectacularRedocView.as_view(url_name="schema"), + SpectacularRedocView.as_view(url_name="schema-yaml"), name="schema-redoc", ), # actual endpoints diff --git a/src/objects/conf/api.py b/src/objects/conf/api.py index 30411899..69d54ec4 100644 --- a/src/objects/conf/api.py +++ b/src/objects/conf/api.py @@ -94,6 +94,8 @@ "SERVE_INCLUDE_SCHEMA": False, "CONTACT": { "url": "https://github.com/maykinmedia/objects-api", + "email": "support@maykinmedia.nl", + "name": "Maykin Media", }, "LICENSE": {"name": "EUPL-1.2"}, "EXTERNAL_DOCS": { diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index a72c460c..766df05d 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -43,7 +43,9 @@ "objects.utils", ] - +MIDDLEWARE += [ + "vng_api_common.middleware.APIVersionHeaderMiddleware", +] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ diff --git a/src/objects/tests/v2/test_schema.py b/src/objects/tests/v2/test_schema.py index e0b07335..88d00f4b 100644 --- a/src/objects/tests/v2/test_schema.py +++ b/src/objects/tests/v2/test_schema.py @@ -5,6 +5,6 @@ class APISchemaTest(APITestCase): - def test_schema_endoint(self): + def test_schema_endpoint(self): response = self.client.get(reverse("schema-redoc")) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/objects/utils/autoschema.py b/src/objects/utils/autoschema.py index 13e39177..1e9a2a97 100644 --- a/src/objects/utils/autoschema.py +++ b/src/objects/utils/autoschema.py @@ -5,6 +5,7 @@ from drf_spectacular.openapi import AutoSchema as _AutoSchema from drf_spectacular.plumbing import build_parameter_type, get_view_model from drf_spectacular.utils import OpenApiParameter +from vng_api_common.constants import VERSION_HEADER from vng_api_common.geo import DEFAULT_CRS, HEADER_ACCEPT, HEADER_CONTENT from vng_api_common.schema import HTTP_STATUS_CODE_TITLES @@ -18,6 +19,14 @@ class AutoSchema(_AutoSchema): + def get_response_serializers( + self, + ): + if self.method == "DELETE": + return {204: None} + + return super().get_response_serializers() + def get_operation_id(self): """ Use model name as a base for operation_id @@ -29,10 +38,15 @@ def get_operation_id(self): def get_override_parameters(self): """Add request GEO headers""" + params = super().get_override_parameters() + geo_headers = self.get_geo_headers() content_type_headers = self.get_content_type_headers() + version_headers = self.get_version_headers() field_params = self.get_fields_params() - return geo_headers + content_type_headers + field_params + return ( + params + geo_headers + content_type_headers + version_headers + field_params + ) def _get_filter_parameters(self): """remove filter parameters from all actions except LIST""" @@ -121,6 +135,20 @@ def get_content_type_headers(self) -> list: ) ] + def get_version_headers(self) -> list[OpenApiParameter]: + return [ + OpenApiParameter( + name=VERSION_HEADER, + type=str, + location=OpenApiParameter.HEADER, + description=_( + "Geeft een specifieke API-versie aan in de context van " + "een specifieke aanroep. Voorbeeld: 1.2.1." + ), + response=True, + ) + ] + def get_fields_params(self) -> list[OpenApiParameter]: if self.method != "GET": return [] diff --git a/src/objects/utils/oas_extensions/views.py b/src/objects/utils/oas_extensions/views.py new file mode 100644 index 00000000..78da619c --- /dev/null +++ b/src/objects/utils/oas_extensions/views.py @@ -0,0 +1,33 @@ +from django.views.generic import RedirectView + +import structlog +from drf_spectacular.views import ( + SpectacularJSONAPIView as _SpectacularJSONAPIView, + SpectacularYAMLAPIView as _SpectacularYAMLAPIView, +) + +logger = structlog.stdlib.get_logger(__name__) + + +class AllowAllOriginsMixin: + def dispatch(self, request, *args, **kwargs): + response = super().dispatch(request, *args, **kwargs) + response["Access-Control-Allow-Origin"] = "*" + return response + + +class SpectacularYAMLAPIView(AllowAllOriginsMixin, _SpectacularYAMLAPIView): + """Spectacular YAML API view with Access-Control-Allow-Origin set to allow all""" + + +class SpectacularJSONAPIView(AllowAllOriginsMixin, _SpectacularJSONAPIView): + """Spectacular JSON API view with Access-Control-Allow-Origin set to allow all""" + + +class DeprecationRedirectView(RedirectView): + def get(self, request, *args, **kwargs): + logger.warning( + "deprecated_endpoint_called", + endpoint=request.path, + ) + return super().get(request, *args, **kwargs)