Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reference enums in path parameters #732

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 46 additions & 35 deletions drf_spectacular/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,46 +97,57 @@ def create_enum_component(name, schema):
enum_name_mapping[prop_hash] = enum_name
enum_name_mapping[(prop_hash, prop_name)] = enum_name

def replace_enum(component, path, schema):
is_array = schema.get('type') == 'array'
if is_array:
schema = schema.get('items', {})

if 'enum' not in schema:
return

prop_enum_original_list = schema['enum']
schema['enum'] = [i for i in schema['enum'] if i not in ['', None]]
prop_hash = list_hash(schema['enum'])
# when choice sets are reused under multiple names, the generated name cannot be
# resolved from the hash alone. fall back to prop_name and hash for resolution.
enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping.get((prop_hash, prop_name))
if not enum_name:
return

# split property into remaining property and enum component parts
enum_schema = {k: v for k, v in schema.items() if k in ['type', 'enum']}
schema = {k: v for k, v in schema.items() if k not in ['type', 'enum']}

components = [
create_enum_component(enum_name, schema=enum_schema)
]
if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE:
if '' in prop_enum_original_list:
components.append(create_enum_component('BlankEnum', schema={'enum': ['']}))
if None in prop_enum_original_list:
components.append(create_enum_component('NullEnum', schema={'enum': [None]}))

if len(components) == 1:
schema.update(components[0].ref)
else:
schema.update({'oneOf': [c.ref for c in components]})

if is_array:
component[path]['items'] = safe_ref(schema)
else:
component[path] = safe_ref(schema)

# replace all enum occurrences with a enum schema component. cut out the
# enum, replace it with a reference and add a corresponding component.
for _, props in iter_prop_containers(schemas):
for prop_name, prop_schema in props.items():
is_array = prop_schema.get('type') == 'array'
if is_array:
prop_schema = prop_schema.get('items', {})

if 'enum' not in prop_schema:
continue

prop_enum_original_list = prop_schema['enum']
prop_schema['enum'] = [i for i in prop_schema['enum'] if i not in ['', None]]
prop_hash = list_hash(prop_schema['enum'])
# when choice sets are reused under multiple names, the generated name cannot be
# resolved from the hash alone. fall back to prop_name and hash for resolution.
enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[prop_hash, prop_name]

# split property into remaining property and enum component parts
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']}

components = [
create_enum_component(enum_name, schema=enum_schema)
]
if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE:
if '' in prop_enum_original_list:
components.append(create_enum_component('BlankEnum', schema={'enum': ['']}))
if None in prop_enum_original_list:
components.append(create_enum_component('NullEnum', schema={'enum': [None]}))

if len(components) == 1:
prop_schema.update(components[0].ref)
else:
prop_schema.update({'oneOf': [c.ref for c in components]})
replace_enum(props, prop_name, prop_schema)

if is_array:
props[prop_name]['items'] = safe_ref(prop_schema)
else:
props[prop_name] = safe_ref(prop_schema)
# replace all parameter enum occurrences with a enum schema, if one exists
for path in result['paths'].values():
for operation in path.values():
for parameter in operation.get('parameters', []):
replace_enum(parameter, 'schema', parameter['schema'])

# sort again with additional components
result['components'] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
Expand Down
25 changes: 5 additions & 20 deletions tests/contrib/test_django_filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ paths:
- in: query
name: category
schema:
type: string
enum:
- A
- B
$ref: '#/components/schemas/CategoryEnum'
description: some category description
- in: query
name: custom_filter
Expand All @@ -34,10 +31,7 @@ paths:
schema:
type: array
items:
type: string
enum:
- A
- B
$ref: '#/components/schemas/CategoryEnum'
description: Multiple values may be separated by commas.
explode: false
style: form
Expand All @@ -51,10 +45,7 @@ paths:
schema:
type: array
items:
type: string
enum:
- A
- B
$ref: '#/components/schemas/CategoryEnum'
description: Multiple values may be separated by commas.
explode: false
style: form
Expand Down Expand Up @@ -93,10 +84,7 @@ paths:
schema:
type: array
items:
type: string
enum:
- A
- B
$ref: '#/components/schemas/CategoryEnum'
description: some category description
explode: true
style: form
Expand All @@ -111,10 +99,7 @@ paths:
- in: query
name: model_single_cat
schema:
type: string
enum:
- A
- B
$ref: '#/components/schemas/CategoryEnum'
description: some category description
- in: query
name: number_id
Expand Down