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

ability to configure Enum Name Generation in postprocess_schema_enums via SPECTACULAR_SETTINGS #1277

Open
sergerdn opened this issue Aug 15, 2024 · 4 comments

Comments

@sergerdn
Copy link

sergerdn commented Aug 15, 2024

I am currently working on a Django project and have configured the SPECTACULAR_SETTINGS as shown:

SPECTACULAR_SETTINGS = {
    "POSTPROCESSING_HOOKS": [
        "drf_spectacular.hooks.postprocess_schema_enums",
        "app.my_cool_app.hooks.postprocess_schema_enums", # my hooks for debugging
    ],
}

I've noted that the postprocess_schema_enums function does not prepend the component name to the enum name by default https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/hooks.py#L90.

I'd like to request an enhancement to include a configuration setting that would allow for prepending the component name to the enum name.

For instance:

enum_name = f'{camelize(component_name)}{camelize(prop_name)}{enum_suffix}' # ComponentNameSchemaSomeEnum

We could then have a flag in the SPECTACULAR_SETTINGS like so:

SPECTACULAR_SETTINGS = {
    "ALWAYS_PREPEND_COMPONENT_NAME_TO_ENUM": True  # an example
}

This gives users additional control over how the enum names are generated, providing a higher level of customization and flexibility.

If there's already an existing feature or technique to achieve this result, kindly let me know.

@tfranzel
Copy link
Owner

Hi,

so this functionality is basically already there, however it is only activated as a graceful fallback when there is no better alternative (i.e. there is a collision in naming that cannot be resolved otherwise):

enum_name = f'{camelize(component_name)}{camelize(prop_name)}{enum_suffix}'

Imho it makes no sense enabling this all the time. What if you have 2 serializers (components) that use the same enum? Then you have have the same enum represented under 2 names. If you guard for that duplication and only use the first components name you see, then will likely be confusing using enum name A on component B. And finally this will also mess up generated code.

Of course you can patch the post hook for yourself, but this change would be a major regression overall.

@sergerdn
Copy link
Author

sergerdn commented Aug 16, 2024

@tfranzel

Currently the names of the generated enums in my component are absolutely unpredictable for me.

I have looked at the code and I'm not sure that I understand it correctly, but if I do, it appears that the generated enums depend on several parameters. In this case, the name of the generated enums is unpredictable because it depends on many factors, including other activated applications, e.g. if other apps have the same enums or not.

Maybe I chose the wrong way to reach my main goal - having predictable generated enums where the logic of generation does not depend on other enums in the same component or different components, e.g., Django apps or something else.

In this situation, I think the idea of having the component name in the enum, serving as a prefix, is a viable one.

So, I would like to ask, what path should I follow to make the_names_of_my enums predictable and independent?

P.S.
In other words: When I create a serializer, it always generates a predictable OpenAPI schema (e.g., enums) all the time. Also, I would like to know that the OpenAPI schema (e.g., enums) of that serializer will not change.

@tfranzel
Copy link
Owner

tfranzel commented Aug 16, 2024

Yes, the downside of this approach is that the names change depending on the general context. Usually django apps/projects are not wildly changing ever so often and so it is usually not an issue after the project has settled.

Creating 10 identical CatStatusEnum, DogStatusEnum, ... instead of StatusEnum is not a good solution imho (especially with code generators). All that complicated code makes sure we reuse commonality and subtype differences where possible.

The rules are not that complicated:

  • prop_name (e.g. status) has been used exclusively for one choice set (best case) -> use field name
  • prop_name (e.g. status) has multiple choice sets overall, but each choice set occurrence is limited to one component only -> prefix with component name
  • prop_name (e.g. status) is used in multiple serializers each with different choice sets and at least one choice set is reused in one or more serializer -> resolve collision with a 3 digit hash and emit warning

You can influence enum naming and fix names for good with the ENUM_NAME_OVERRIDES setting. It has names for keys and the enum/list/import path as value. Items in the overrides will be manually set and are not subject to the automatic name generation.

# enum name overrides. dict with keys "YourEnum" and their choice values "field.choices"
# e.g. {'SomeEnum': ['A', 'B'], 'OtherEnum': 'import.path.to.choices'}
'ENUM_NAME_OVERRIDES': {},

@sergerdn
Copy link
Author

Yes, the downside of this approach is that the names change depending on the general context.

Yes, this is my main issue now.

You can influence enum naming and fix names for good with the ENUM_NAME_OVERRIDES setting. It has names for keys and the enum/list/import path as value. Items in the overrides will be manually set and are not subject to the automatic name generation.

# enum name overrides. dict with keys "YourEnum" and their choice values "field.choices"
# e.g. {'SomeEnum': ['A', 'B'], 'OtherEnum': 'import.path.to.choices'}
'ENUM_NAME_OVERRIDES': {},

Yes, I have read about it in the docs, but in my opinion, it doesn't seem like the best workaround because it's also unpredictable. I need to write complete Django app, see what happens with enums, and then fix it manually through the settings.

I think about some logic like:

class SomeMainSerializer(serializers.ModelSerializer):
    class Meta:
        model = SomeDjangoModel
        fields = [
            "id",
            "status",  # choice field
            "created_at",
            "updated_at",
        ]
        extra_kwargs = {"status": {"prepend_enum_name": "SomeMainPre"}}

Looks good to me; it shouldn't break any existing logic for both old and future code.

Can the Meta class be accessed in the current hook implementation?

If you have any ideas on how to implement any logic/solution/etc to ensure predictable enums, that would be great. I can provide a PR to target my goals.

However, if you'd prefer not to change your current codebase, that's fine too, as I can implement the logic in my own hook. Any suggestions from you would be much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants