Skip to content

Commit

Permalink
add enum key/value list to description string #337 #403 #105 #563
Browse files Browse the repository at this point in the history
Thanks to @valentijnscholten for the first attempt in #403
  • Loading branch information
tfranzel committed Mar 2, 2023
1 parent bc58f8f commit b084643
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 12 deletions.
40 changes: 34 additions & 6 deletions drf_spectacular/contrib/django_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from drf_spectacular.drainage import add_trace_message, get_override, has_override, warn
from drf_spectacular.extensions import OpenApiFilterExtension
from drf_spectacular.plumbing import (
build_array_type, build_basic_type, build_parameter_type, follow_field_source, get_manager,
get_type_hints, get_view_model, is_basic_type,
build_array_type, build_basic_type, build_choice_description_list, build_parameter_type,
follow_field_source, get_manager, get_type_hints, get_view_model, is_basic_type,
)
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter

Expand Down Expand Up @@ -154,10 +155,8 @@ def resolve_filter_field(self, auto_schema, model, filterset_class, field_name,
enum = filter_choices

description = schema.pop('description', None)
if filter_field.extra.get('help_text', None):
description = filter_field.extra['help_text']
elif filter_field.label is not None:
description = filter_field.label
if not schema_from_override:
description = self._get_field_description(filter_field, description)

# parameter style variations based on filter base class
if isinstance(filter_field, filters.BaseCSVFilter):
Expand Down Expand Up @@ -248,6 +247,35 @@ def _get_schema_from_model_field(self, auto_schema, filter_field, model):
model_field = qs.query.annotations[filter_field.field_name].field
return auto_schema._map_model_field(model_field, direction=None)

def _get_field_description(self, filter_field, description):
# Try to improve description beyond auto-generated model description
if filter_field.extra.get('help_text', None):
description = filter_field.extra['help_text']
elif filter_field.label is not None:
description = filter_field.label

choices = filter_field.extra.get('choices')
if choices and callable(choices):
# remove auto-generated enum list, since choices come from a callable
if '\n\n*' in (description or ''):
description, _, _ = description.partition('\n\n*')
return description

choice_description = ''
if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION and choices and not callable(choices):
choice_description = build_choice_description_list(choices)

if not choices:
return description

if description:
# replace or append model choice description
if '\n\n*' in description:
description, _, _ = description.partition('\n\n*')
return description + '\n\n' + choice_description
else:
return choice_description

@classmethod
def _is_gis(cls, field):
if not getattr(cls, '_has_gis', True):
Expand Down
7 changes: 7 additions & 0 deletions drf_spectacular/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ def create_enum_component(name, schema):
enum_schema = {k: v for k, v in prop_schema.items() if k in ['type', 'enum']}
prop_schema = {k: v for k, v in prop_schema.items() if k not in ['type', 'enum']}

# separate actual description from name-value tuples
if prop_schema.get('description', '').startswith('*'):
enum_schema['description'] = prop_schema.pop('description')
elif '\n\n*' in prop_schema.get('description', ''):
_, _, post = prop_schema['description'].partition('\n\n*')
enum_schema['description'] = '*' + post

components = [
create_enum_component(enum_name, schema=enum_schema)
]
Expand Down
5 changes: 4 additions & 1 deletion drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,10 @@ def _map_serializer_field(self, field, direction, bypass_extensions=False):
return append_meta(build_array_type(build_choice_field(field)), meta)

if isinstance(field, serializers.ChoiceField):
return append_meta(build_choice_field(field), meta)
schema = build_choice_field(field)
if 'description' in meta:
meta['description'] = meta['description'] + '\n\n' + schema.pop('description')
return append_meta(schema, meta)

if isinstance(field, serializers.ListField):
if isinstance(field.child, _UnvalidatedField):
Expand Down
8 changes: 8 additions & 0 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,17 @@ def build_choice_field(field):
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
if type:
schema['type'] = type

if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION:
schema['description'] = build_choice_description_list(field.choices.items())

return schema


def build_choice_description_list(choices) -> str:
return '\n'.join(f'* `{value}` - {label}' for value, label in choices)


def build_bearer_security_scheme_object(header_name, token_prefix, bearer_format=None):
""" Either build a bearer scheme or a fallback due to OpenAPI 3.0.3 limitations """
# normalize Django header quirks
Expand Down
2 changes: 2 additions & 0 deletions drf_spectacular/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
'ENUM_NAME_OVERRIDES': {},
# Adds "blank" and "null" enum choices where appropriate. disable on client generation issues
'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': True,
# Add/Append a list of (``choice value`` - choice name) to the enum description string.
'ENUM_GENERATE_CHOICE_DESCRIPTION': True,

# 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',
Expand Down
37 changes: 32 additions & 5 deletions tests/contrib/test_django_filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ paths:
enum:
- A
- B
description: some category description
description: |-
some category description
* `A` - aaa
* `B` - b
- in: query
name: choice_field_enum_override
schema:
Expand Down Expand Up @@ -101,7 +105,11 @@ paths:
enum:
- A
- B
description: some category description
description: |-
some category description
* `A` - aaa
* `B` - b
explode: true
style: form
- in: query
Expand All @@ -119,7 +127,11 @@ paths:
enum:
- A
- B
description: some category description
description: |-
some category description
* `A` - aaa
* `B` - b
- in: query
name: number_id
schema:
Expand All @@ -140,7 +152,13 @@ paths:
- -price
- in_stock
- price
description: Ordering
description: |-
Ordering
* `price` - Price
* `-price` - Price (descending)
* `in_stock` - in stock
* `-in_stock` - in stock (descending)
explode: false
style: form
- in: query
Expand Down Expand Up @@ -191,6 +209,7 @@ paths:
type: integer
enum:
- 1
description: '* `1` - one'
- in: query
name: untyped_multiple_choice_field_method_with_explicit_choices
schema:
Expand All @@ -199,6 +218,7 @@ paths:
type: integer
enum:
- 1
description: '* `1` - one'
explode: true
style: form
tags:
Expand Down Expand Up @@ -246,6 +266,9 @@ components:
- A
- B
type: string
description: |-
* `A` - aaa
* `B` - b
Product:
type: object
properties:
Expand All @@ -255,7 +278,11 @@ components:
category:
allOf:
- $ref: '#/components/schemas/CategoryEnum'
description: some category description
description: |-
some category description
* `A` - aaa
* `B` - b
in_stock:
type: boolean
price:
Expand Down
3 changes: 3 additions & 0 deletions tests/test_basic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ components:
- POP
- ROCK
type: string
description: |-
* `POP` - Pop
* `ROCK` - Rock
PatchedAlbum:
type: object
properties:
Expand Down
11 changes: 11 additions & 0 deletions tests/test_extend_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ paths:
- b
- c
type: string
description: |-
* `a` - a
* `b` - b
* `c` - c
default:
- a
- in: query
Expand Down Expand Up @@ -367,6 +371,9 @@ components:
- a
- b
type: string
description: |-
* `a` - a
* `b` - b
GammaEpsilon:
type: object
properties:
Expand Down Expand Up @@ -394,6 +401,10 @@ components:
- b
- c
type: string
description: |-
* `a` - a
* `b` - b
* `c` - c
Query:
type: object
properties:
Expand Down
9 changes: 9 additions & 0 deletions tests/test_postprocessing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ components:
- ru
- cn
type: string
description: |-
* `en` - en
* `es` - es
* `ru` - ru
* `cn` - cn
NullEnum:
enum:
- null
Expand All @@ -79,6 +84,10 @@ components:
- 0
- -1
type: integer
description: |-
* `1` - Positive
* `0` - Neutral
* `-1` - Negative
securitySchemes:
basicAuth:
type: http
Expand Down

0 comments on commit b084643

Please sign in to comment.