Skip to content

Commit

Permalink
add support for custom converters and coverter override #502
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Sep 19, 2021
1 parent c0a9605 commit 5890bd6
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 11 deletions.
47 changes: 37 additions & 10 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.sql.query import Query
from django.urls.converters import get_converters
from django.urls.resolvers import ( # type: ignore
_PATH_PARAMETER_COMPONENT_RE, RegexPattern, Resolver404, RoutePattern, URLPattern, URLResolver,
get_resolver,
Expand Down Expand Up @@ -733,11 +734,19 @@ def resolve_django_path_parameter(path_regex, variable, available_formats):
"""
convert django style path parameters to OpenAPI parameters.
"""
registered_converters = get_converters()
for match in _PATH_PARAMETER_COMPONENT_RE.finditer(path_regex):
converter, parameter = match.group('converter'), match.group('parameter')
enum_values = None

if converter and converter.startswith('drf_format_suffix_'):
if api_settings.SCHEMA_COERCE_PATH_PK and parameter == 'pk':
parameter = 'id'

if not converter or parameter != variable:
continue

# special handling for drf_format_suffix
if converter.startswith('drf_format_suffix_'):
explicit_formats = converter[len('drf_format_suffix_'):].split('_')
enum_values = [
f'.{suffix}' for suffix in explicit_formats if suffix in available_formats
Expand All @@ -746,16 +755,34 @@ def resolve_django_path_parameter(path_regex, variable, available_formats):
elif converter == 'drf_format_suffix':
enum_values = [f'.{suffix}' for suffix in available_formats]

if api_settings.SCHEMA_COERCE_PATH_PK and parameter == 'pk':
parameter = 'id'
if converter in spectacular_settings.PATH_CONVERTER_OVERRIDES:
override = spectacular_settings.PATH_CONVERTER_OVERRIDES[converter]
if is_basic_type(override):
schema = build_basic_type(override)
elif isinstance(override, dict):
schema = dict(override)
else:
warn(
f'Unable to use path converter override for "{converter}". '
f'Please refer to the documentation on how to use this.'
)
return None
elif converter in DJANGO_PATH_CONVERTER_MAPPING:
schema = build_basic_type(DJANGO_PATH_CONVERTER_MAPPING[converter])
elif converter in registered_converters:
# gracious fallback for custom converters that have no override specified.
schema = build_basic_type(OpenApiTypes.STR)
schema['pattern'] = registered_converters[converter].regex
else:
error(f'Encountered path converter "{converter}" that is unknown to Django.')
return None

if parameter == variable and converter in DJANGO_PATH_CONVERTER_MAPPING:
return build_parameter_type(
name=parameter,
schema=build_basic_type(DJANGO_PATH_CONVERTER_MAPPING[converter]),
location=OpenApiParameter.PATH,
enum=enum_values,
)
return build_parameter_type(
name=parameter,
schema=schema,
location=OpenApiParameter.PATH,
enum=enum_values,
)

return None

Expand Down
6 changes: 6 additions & 0 deletions drf_spectacular/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
# allowed values are 'dict', 'bool', None
'GENERIC_ADDITIONAL_PROPERTIES': 'dict',

# Path converter schema overrides (e.g. <int:foo>). Can be used to either modify default
# behavior or provide a schema for custom converters registered with register_converter(...).
# Takes converter labels as keys and either basic python types, OpenApiType, or raw schemas
# as values. Example: {'aint': OpenApiTypes.INT, 'bint': str, 'cint': {'type': ...}}
'PATH_CONVERTER_OVERRIDES': {},

# Determines whether operation parameters should be sorted alphanumerically or just in
# the order they arrived. Accepts either True, False, or a callable for sort's key arg.
'SORT_OPERATION_PARAMETERS': True,
Expand Down
42 changes: 41 additions & 1 deletion tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from django.core import validators
from django.db import models
from django.db.models import fields
from django.urls import path, re_path
from django.urls import path, re_path, register_converter
from django.urls.converters import StringConverter
from rest_framework import (
filters, generics, mixins, pagination, parsers, renderers, routers, serializers, views,
viewsets,
Expand Down Expand Up @@ -2371,3 +2372,42 @@ class XAPIView(generics.RetrieveAPIView):
@pytest.mark.parametrize('import_string', IMPORT_STRINGS)
def test_import_strings_in_default_settings(import_string):
assert import_string in SPECTACULAR_DEFAULTS


@mock.patch(
'drf_spectacular.settings.spectacular_settings.PATH_CONVERTER_OVERRIDES', {
'int': str, # override default behavior
'signed_int': {'type': 'integer', 'format': 'signed'},
}
)
def test_path_converter_override(no_warnings):
@extend_schema(responses=OpenApiTypes.FLOAT)
@api_view(['GET'])
def pi(request, foo):
pass # pragma: no cover

class SignedIntConverter(StringConverter):
regex = r'\-[0-9]+'

class HexConverter(StringConverter):
regex = r'[a-f0-9]+'

register_converter(SignedIntConverter, 'signed_int')
register_converter(HexConverter, 'hex')

urlpatterns = [
path('/a/<int:var>/', pi),
path('/b/<signed_int:var>/', pi),
path('/c/<hex:var>/', pi),
]
schema = generate_schema(None, patterns=urlpatterns)

assert schema['paths']['/a/{var}/']['get']['parameters'][0]['schema'] == {
'type': 'string',
}
assert schema['paths']['/b/{var}/']['get']['parameters'][0]['schema'] == {
'type': 'integer', 'format': 'signed'
}
assert schema['paths']['/c/{var}/']['get']['parameters'][0]['schema'] == {
'type': 'string', 'pattern': '[a-f0-9]+'
}
15 changes: 15 additions & 0 deletions tests/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,18 @@ class XViewSet(viewsets.ModelViewSet):
'could not derive type of path parameter "non_existent_field" because model '
'"tests.models.SimpleModel" contained no such field.'
) in stderr


@mock.patch(
'drf_spectacular.settings.spectacular_settings.PATH_CONVERTER_OVERRIDES', {'int': object}
)
def test_invalid_path_converter_override(capsys):
@extend_schema(responses=OpenApiTypes.FLOAT)
@api_view(['GET'])
def pi(request, foo):
pass # pragma: no cover

urlpatterns = [path('/a/<int:var>/', pi)]
generate_schema(None, patterns=urlpatterns)
stderr = capsys.readouterr().err
assert 'Unable to use path converter override for "int".' in stderr

0 comments on commit 5890bd6

Please sign in to comment.