Skip to content

Commit

Permalink
Merge pull request #54 from ghazi-git/compatibility-with-drf-spectacu…
Browse files Browse the repository at this point in the history
…lar-0.27

compatibility-with-drf-spectacular-0.27
  • Loading branch information
ghazi-git authored Jan 1, 2024
2 parents 213dafb + dee0073 commit 12a8308
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 32 deletions.
25 changes: 25 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [UNRELEASED]
### Changed
- If you're using drf-spectacular 0.27.0 or newer, update `ENUM_NAME_OVERRIDES` entries to reference `choices`
rather than `values`. The list of overrides specific to this package should become like this:
```python
SPECTACULAR_SETTINGS = {
# other settings
"ENUM_NAME_OVERRIDES": {
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.choices",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.choices",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.choices",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.choices",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.choices",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.choices",
# other overrides
},
}
```

### Added
- add compatibility with drf-spectacular 0.27

## [0.12.6] - 2023-10-25
### Added
Expand Down
24 changes: 12 additions & 12 deletions docs/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ by drf-spectacular due to the same set of error codes appearing in multiple oper
SPECTACULAR_SETTINGS = {
# other settings
"ENUM_NAME_OVERRIDES": {
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.values",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.values",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.values",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.values",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.values",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.values",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.values",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.values",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.values",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.values",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.values",
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.choices",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.choices",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.choices",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.choices",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.choices",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.choices",
},
}
```
Expand Down Expand Up @@ -243,7 +243,7 @@ SPECTACULAR_SETTINGS = {
# other settings
"ENUM_NAME_OVERRIDES": {
# to avoid warnings raised by drf-spectacular, add the next line
"ErrorCode503Enum": "path.to.ErrorCode503Enum.values",
"ErrorCode503Enum": "path.to.ErrorCode503Enum.choices",
},
}
```
Expand Down
52 changes: 43 additions & 9 deletions drf_standardized_errors/openapi_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
from collections import defaultdict

from drf_spectacular.hooks import postprocess_schema_enum_id_removal
from drf_spectacular.plumbing import (
ResolvedComponent,
list_hash,
Expand Down Expand Up @@ -62,6 +63,16 @@ def create_enum_component(name, schema):
generator.registry.register_on_missing(component)
return component

def extract_hash(schema):
if "x-spec-enum-id" in schema:
# try to use the injected enum hash first as it generated from (name, value) tuples,
# which prevents collisions on choice sets only differing in labels not values.
return schema["x-spec-enum-id"]
else:
# fall back to actual list hashing when we encounter enums not generated by us.
# remove blank/null entry for hashing. will be reconstructed in the last step
return list_hash([(i, i) for i in schema["enum"] if i not in ("", None)])

schemas = result.get("components", {}).get("schemas", {})

overrides = load_enum_name_overrides()
Expand All @@ -75,10 +86,8 @@ def create_enum_component(name, schema):
prop_schema = prop_schema.get("items", {})
if "enum" not in prop_schema:
continue
# remove blank/null entry for hashing. will be reconstructed in the last step
prop_enum_cleaned_hash = list_hash(
[i for i in prop_schema["enum"] if i not in ["", None]]
)

prop_enum_cleaned_hash = extract_hash(prop_schema)
prop_hash_mapping[prop_name].add(prop_enum_cleaned_hash)
hash_name_mapping[prop_enum_cleaned_hash].add((component_name, prop_name))

Expand Down Expand Up @@ -131,7 +140,7 @@ def create_enum_component(name, schema):
prop_schema["enum"] = [
i for i in prop_schema["enum"] if i not in ["", None]
]
prop_hash = list_hash(prop_schema["enum"])
prop_hash = extract_hash(prop_schema)
# 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 = (
Expand All @@ -144,19 +153,40 @@ def create_enum_component(name, 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"]
k: v
for k, v in prop_schema.items()
if k not in ["type", "enum", "x-spec-enum-id"]
}

# separate actual description from name-value tuples
if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION:
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)]
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 spectacular_settings.OAS_VERSION.startswith("3.1"):
components.append(
create_enum_component("NullEnum", schema={"type": "null"})
)
else:
components.append(
create_enum_component("NullEnum", schema={"enum": [None]})
)

# undo OAS 3.1 type list NULL construction as we cover this in a separate component already
if spectacular_settings.OAS_VERSION.startswith("3.1") and isinstance(
enum_schema["type"], list
):
enum_schema["type"] = [t for t in enum_schema["type"] if t != "null"][0]

if len(components) == 1:
prop_schema.update(components[0].ref)
Expand All @@ -172,4 +202,8 @@ def create_enum_component(name, schema):
result["components"] = generator.registry.build(
spectacular_settings.APPEND_COMPONENTS
)

# remove remaining ids that were not part of this hook (operation parameters mainly)
postprocess_schema_enum_id_removal(result, generator)

return result
22 changes: 11 additions & 11 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"ENUM_NAME_OVERRIDES": {
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.values",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.values",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.values",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.values",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.values",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.values",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.values",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.values",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.values",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.values",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.values",
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.choices",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.choices",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.choices",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.choices",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.choices",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.choices",
},
"POSTPROCESSING_HOOKS": [
"drf_standardized_errors.openapi_hooks.postprocess_schema_enums"
Expand Down

0 comments on commit 12a8308

Please sign in to comment.