From 3911c5534fab4bedf9e9ffc267dff21b5f58ac51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20L=C3=B6tvall?= Date: Mon, 21 Oct 2024 20:16:46 +0200 Subject: [PATCH] Fix lookup_value_regex handling of non string types --- drf_spectacular/openapi.py | 6 +- tests/contrib/test_drf_lookup_value.py | 38 +++++ tests/contrib/test_drf_lookup_value.yml | 194 +++++++++++++++++++++++ tests/contrib/test_drf_nested_routers.py | 4 - 4 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 tests/contrib/test_drf_lookup_value.py create mode 100644 tests/contrib/test_drf_lookup_value.yml diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py index ff0e5e36..a13b89ce 100644 --- a/drf_spectacular/openapi.py +++ b/drf_spectacular/openapi.py @@ -496,7 +496,7 @@ def _resolve_path_parameters(self, variables): resolved_parameter = resolve_django_path_parameter( self.path_regex, variable, self.map_renderers('format'), ) - if not resolved_parameter: + if not resolved_parameter and model is None: resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable) if resolved_parameter: @@ -519,6 +519,10 @@ def _resolve_path_parameters(self, variables): model_field_name = variable model_field = follow_model_field_lookup(model, model_field_name) schema = self._map_model_field(model_field, direction=None) + if 'type' in schema and schema['type'] == OpenApiTypes.STR: + regex_resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable) + if regex_resolved_parameter: + schema = regex_resolved_parameter['schema'] if 'description' not in schema and model_field.primary_key: description = get_pk_description(model, model_field) except django_exceptions.FieldError: diff --git a/tests/contrib/test_drf_lookup_value.py b/tests/contrib/test_drf_lookup_value.py new file mode 100644 index 00000000..61351899 --- /dev/null +++ b/tests/contrib/test_drf_lookup_value.py @@ -0,0 +1,38 @@ +import pytest +from django.db import models +from django.urls import include, re_path +from rest_framework import serializers, viewsets +from rest_framework.routers import SimpleRouter + +from tests import assert_schema, generate_schema + + +class Book(models.Model): + id = models.IntegerField(primary_key=True) + name = models.CharField(max_length=255) + +class BookSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Book + fields = ('name',) + +def _generate_simple_router_schema(viewset): + router = SimpleRouter() + router.register('books', viewset, basename='books') + urlpatterns = [ + re_path('', include(router.urls)), + ] + return generate_schema(None, patterns=urlpatterns) + +@pytest.mark.contrib('rest_framework_lookup_value') +def test_drf_lookup_value_regex_integer(no_warnings): + class BookViewSet(viewsets.ModelViewSet): + queryset = Book.objects.all() + serializer_class = BookSerializer + lookup_field = 'id' + lookup_value_regex = r'\d+' + + assert_schema( + _generate_simple_router_schema(BookViewSet), + 'tests/contrib/test_drf_lookup_value.yml', + ) diff --git a/tests/contrib/test_drf_lookup_value.yml b/tests/contrib/test_drf_lookup_value.yml new file mode 100644 index 00000000..44d27b54 --- /dev/null +++ b/tests/contrib/test_drf_lookup_value.yml @@ -0,0 +1,194 @@ +openapi: 3.0.3 +info: + title: '' + version: 0.0.0 +paths: + /books/: + get: + operationId: books_list + tags: + - books + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Book' + description: '' + post: + operationId: books_create + tags: + - books + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Book' + multipart/form-data: + schema: + $ref: '#/components/schemas/Book' + required: true + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + description: '' + /books/{id}/: + get: + operationId: books_retrieve + parameters: + - in: path + name: id + schema: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + description: A unique value identifying this book. + required: true + tags: + - books + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + description: '' + put: + operationId: books_update + parameters: + - in: path + name: id + schema: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + description: A unique value identifying this book. + required: true + tags: + - books + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Book' + multipart/form-data: + schema: + $ref: '#/components/schemas/Book' + required: true + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + description: '' + patch: + operationId: books_partial_update + parameters: + - in: path + name: id + schema: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + description: A unique value identifying this book. + required: true + tags: + - books + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBook' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBook' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBook' + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + description: '' + delete: + operationId: books_destroy + parameters: + - in: path + name: id + schema: + type: integer + maximum: 9223372036854775807 + minimum: -9223372036854775808 + format: int64 + description: A unique value identifying this book. + required: true + tags: + - books + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: No response body +components: + schemas: + Book: + type: object + properties: + name: + type: string + maxLength: 255 + required: + - name + PatchedBook: + type: object + properties: + name: + type: string + maxLength: 255 + securitySchemes: + basicAuth: + type: http + scheme: basic + cookieAuth: + type: apiKey + in: cookie + name: sessionid diff --git a/tests/contrib/test_drf_nested_routers.py b/tests/contrib/test_drf_nested_routers.py index a984e0eb..2347a25e 100644 --- a/tests/contrib/test_drf_nested_routers.py +++ b/tests/contrib/test_drf_nested_routers.py @@ -88,8 +88,4 @@ def get_queryset(self): assert_schema( _generate_nested_routers_schema(RootViewSet, ChildViewSet), 'tests/contrib/test_drf_nested_routers.yml', - reverse_transforms=[ - lambda x: x.replace('format: uuid', 'pattern: ^[0-9]+$'), - lambda x: x.replace('\n description: A UUID string identifying this root.', '') - ] )