diff --git a/drf_spectacular/generators.py b/drf_spectacular/generators.py index 7f32d467..8c997a59 100644 --- a/drf_spectacular/generators.py +++ b/drf_spectacular/generators.py @@ -8,9 +8,9 @@ from drf_spectacular.extensions import OpenApiViewExtension from drf_spectacular.plumbing import ( - ComponentRegistry, alpha_operation_sorter, build_root_object, error, is_versioning_supported, - modify_for_versioning, normalize_result_object, operation_matches_version, - reset_generator_stats, warn, + ComponentRegistry, alpha_operation_sorter, build_root_object, camelize_operation, error, + is_versioning_supported, modify_for_versioning, normalize_result_object, + operation_matches_version, reset_generator_stats, warn, ) from drf_spectacular.settings import spectacular_settings @@ -154,6 +154,9 @@ def parse(self, request, public): path = path[1:] path = urljoin(self.url or '/', path) + if spectacular_settings.CAMELIZE_NAMES: + path, operation = camelize_operation(path, operation) + result.setdefault(path, {}) result[path][method.lower()] = operation diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py index 551035fb..094b58f5 100644 --- a/drf_spectacular/openapi.py +++ b/drf_spectacular/openapi.py @@ -292,9 +292,10 @@ def _resolve_path_parameters(self, variables): schema = resolved_parameter['schema'] elif not model: warn( - f'could not derive type of path parameter "{variable}" because ' - f'{self.view.__class__} has no queryset. consider annotating the ' - f'parameter type with @extend_schema. defaulting to "string".' + f'could not derive type of path parameter "{variable}" because because it ' + f'is untyped and {self.view.__class__} has no queryset. consider adding a ' + f'type to the path (e.g. ) or annotating the parameter ' + f'type with @extend_schema. defaulting to "string".' ) else: try: diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index d276e54e..576a67ca 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -10,6 +10,7 @@ from enum import Enum from typing import DefaultDict, Generic, List, Optional, Type, TypeVar, Union +import inflection import uritemplate from django import __version__ as DJANGO_VERSION from django.apps import apps @@ -700,3 +701,19 @@ def normalize_result_object(result): if isinstance(result, Promise): return str(result) return result + + +def camelize_operation(path, operation): + for path_variable in re.findall(r'\{(\w+)\}', path): + path = path.replace( + f'{{{path_variable}}}', + f'{{{inflection.camelize(path_variable, False)}}}' + ) + + for parameter in operation.get('parameters', []): + if parameter['in'] == OpenApiParameter.PATH: + parameter['name'] = inflection.camelize(parameter['name'], False) + + operation['operationId'] = inflection.camelize(operation['operationId'], False) + + return path, operation diff --git a/drf_spectacular/settings.py b/drf_spectacular/settings.py index 224a1309..ead8d2a7 100644 --- a/drf_spectacular/settings.py +++ b/drf_spectacular/settings.py @@ -18,7 +18,7 @@ # Split components into request and response parts where appropriate 'COMPONENT_SPLIT_REQUEST': False, # Aid client generator targets that have trouble with read-only properties. - "COMPONENT_NO_READ_ONLY_REQUIRED": False, + 'COMPONENT_NO_READ_ONLY_REQUIRED': False, # Configuration for serving the schema with SpectacularAPIView 'SERVE_URLCONF': None, @@ -50,6 +50,9 @@ # function that returns a list of all classes that should be excluded from doc string extraction 'GET_LIB_DOC_EXCLUDES': 'drf_spectacular.plumbing.get_lib_doc_excludes', + # Camelize names like operationId and path parameter names + 'CAMELIZE_NAMES': False, + # General schema metadata. Refer to spec for valid inputs # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#openapi-object 'TITLE': '', diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 3cf505ce..08e67d5a 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -909,3 +909,16 @@ def one_off(request, foo): '#/components/schemas/NestedInlineOneOff' ) assert len(one_off_nested['properties']) == 2 + + +@mock.patch('drf_spectacular.settings.spectacular_settings.CAMELIZE_NAMES', True) +def test_camelize_names(no_warnings): + @extend_schema(responses=OpenApiTypes.FLOAT) + @api_view(['GET']) + def view_func(request, format=None): + pass # pragma: no cover + + schema = generate_schema('/multi/step/path//', view_function=view_func) + operation = schema['paths']['/multi/step/path/{someName}/']['get'] + assert operation['parameters'][0]['name'] == 'someName' + assert operation['operationId'] == 'multiStepPathRetrieve'