tfranzel / drf-spectacular

Sane and flexible OpenAPI 3 schema generation for Django REST framework.
https://drf-spectacular.readthedocs.io
BSD 3-Clause "New" or "Revised" License
2.39k stars 264 forks source link

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

Open sergerdn opened 2 months ago

sergerdn commented 2 months ago

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 commented 2 months ago

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):

https://github.com/tfranzel/drf-spectacular/blob/f74189904878a878f2b06fb7e609e6349c34c330/drf_spectacular/hooks.py#L94

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 commented 2 months ago

@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_ofmy 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 commented 2 months ago

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:

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.

https://github.com/tfranzel/drf-spectacular/blob/f74189904878a878f2b06fb7e609e6349c34c330/drf_spectacular/settings.py#L116-L118

sergerdn commented 2 months ago

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.

https://github.com/tfranzel/drf-spectacular/blob/f74189904878a878f2b06fb7e609e6349c34c330/drf_spectacular/settings.py#L116-L118

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.