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

Feature Request: Support for custom versioning classes #650

Closed
TauPan opened this issue Feb 8, 2022 · 10 comments
Closed

Feature Request: Support for custom versioning classes #650

TauPan opened this issue Feb 8, 2022 · 10 comments

Comments

@TauPan
Copy link

TauPan commented Feb 8, 2022

I have an application that uses AcceptHeaderVersioning in production but it has been migrated from no versioning at all.

In order to not break existing clients, we have the following REST_FRAMEWORK settings:

    'DEFAULT_VERSION': '1',
    'ALLOWED_VERSIONS': ('1', '2'),

Some endpoints are available in version 1 and 2 and some are only available in version 1 (or no version given) and if the client always sends version 2, it will receive version 1 for endpoints that only support version 1. (And yes, since you'll be asking, the plan is to migrate those version 1 views directly to version 3 in case we want to change them... So each client expects "their" api version.)

I've tried to migrate that application from drf-yasg to drf-spectacular, since we already use JSONSchema for some views so support for openapi 3 would be a definite win for us. (Our JSON Schemata are even displayed in the swagger ui with drf-spectacular, but at the end of the page, and not at all in redoc... I think some other issue points that out as well.)

Unfortunately setting DEFAULT_VERSION is not compatible with the fix for #637, as the OP has pointed out.

Since giving an Accept Header is pretty inconvient in the browser (in redoc as well as the browsable DRF API), we have a custom versioning class that accepts QueryParameterVersioning as well as AcceptHeaderVersioning although the latter is required in production. This just works in drf_yasg (although we had problems customising the documentation for other parts of the api which may be easier in drf-spectacular).

If I disable the custom class and just use AcceptHeaderVersioning, I can generate the correct schema for each version on the command line and in the schema view, if I monkey-patch modify_media_types_for_versioning to change the media type for the default version as well, however it's not clear to me how to switch versions in swagger or redoc. (But maybe I simply overlooked something.)

Also probably being able to override a workflow method on a subclass would be a cleaner approach than having to monkey patch a function in plumbing.py.

So what are your thoughts on this?

@tfranzel
Copy link
Owner

tfranzel commented Feb 8, 2022

Hi @TauPan

(Our JSON Schemata are even displayed in the swagger ui with drf-spectacular, but at the end of the page, and not at all in redoc... I think some other issue points that out as well.)

not quite sure why it is at the end of the page. query/accept versioning should not impact the endpoint ordering afaik.

there are 3 ways to achieve versioning with spectacular:

  • implicit versioning: SpectacularAPIView.as_view(versioning_class=XXXVersioning) or by DEFAULT_VERSIONING_CLASS
  • hard-coded versioning: SpectacularAPIView.as_view(api_version='v2')
  • explicit versioning: GET /api/schema?version=v2 to SpectacularAPIView.as_view() endpoint

redoc has no mechanics for this i think so you are stuck with

    path('api/schema-v1/', SpectacularAPIView.as_view(), name='schema-v1'),
    path('api/schema/redoc-v1/', SpectacularRedocView.as_view(url_name='schema-v1'), name='redoc'),
    path('api/schema-v2/', SpectacularAPIView.as_view(), name='schema-v2'),
    path('api/schema/redoc-v2/', SpectacularRedocView.as_view(url_name='schema-v2'), name='redoc'),

for SwaggerUI thinks are way better:

SPECTACULAR_SETTINGS = {
    "SWAGGER_UI_SETTINGS": """{
        deepLinking: true,
        displayOperationId: true,
        persistAuthorization: true,
        urls: [{url: "/api/schema/?version=v1", name: "version 1"}],
        presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
        layout: "StandaloneLayout",
    }""",
}

Just use the topbar layout like that and add more entries to urls by making use of "explicit versioning"

Unfortunately setting DEFAULT_VERSION is not compatible with the fix for #637, as the OP has pointed out.

why is that? I have not followed though because the OP was happy with it. Is it a bug? can you provide more context here?

Since giving an Accept Header is pretty inconvient in the browser (in redoc as well as the browsable DRF API)
[...] if I monkey-patch modify_media_types_for_versioning

i think we can relax the condition if that helps, i.e. view.versioning_class != versioning.AcceptHeaderVersioning to issubclass(view.versioning_class, versioning.AcceptHeaderVersioning). if you custom class is derived like that it should then work out of the box.

@TauPan
Copy link
Author

TauPan commented Feb 9, 2022

Thanks for your hints. They do help a great deal!

In fact if I derive my Versioning class from AcceptHeaderVersioning directly and use QueryParameterVersioning as an aggregate it just works so I can continue customising the replies.

With b535caa the "Try out" button would even work, I guess. (Currently it doesn't.) And since the version is set via urls.py or swagger, respectively, I don't need my workaround for the default version either, since in that case there will always be an explicit version.

In summary there doesn't seem to be a need to support custom version classes beyond what is already possible for me.

Sorry for being confused.

@TauPan TauPan closed this as completed Feb 9, 2022
@TauPan
Copy link
Author

TauPan commented Feb 9, 2022

As a posterity note:

Since I wanted to keep the way of passing version in the query parameters at least in the redoc view, I decided to override SpectacularRedocView as follows (i.e. adding version from query parameters):

from drf_spectacular.plumbing import get_relative_url, set_query_parameters
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from drf_spectacular.utils import extend_schema
from rest_framework.response import Response
from rest_framework.reverse import reverse
class MyRedocView(SpectacularRedocView):
    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
        schema_url = set_query_parameters(schema_url, lang=request.GET.get('lang'), version=request.GET.get('version'))
        return Response(
            data={
                'title': self.title,
                'dist': self._redoc_dist(),
                'schema_url': schema_url,
            },
            template_name=self.template_name
        )


urlpatterns += [
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    # Optional UI:
    path('doc/', MyRedocView.as_view(url_name='schema'), name='doc'),
]

I'm still deciding if I want to keep redoc or swagger so I might do something similar to adapt SpectacularSwaggerView to my needs as well.

@tfranzel
Copy link
Owner

tfranzel commented Feb 9, 2022

nice one. that should work.

you could do that for SpectacularSwaggerView but I think the config with the topbar version selector is quite nice. only downside is you need to explicitly list the versions in the config. No change required for SpectacularSchemaView then.

imho swagger-ui is superior to redoc. more customization options, a lot more features and finally it's also easier to read.

@TauPan
Copy link
Author

TauPan commented Feb 10, 2022

not quite sure why it is at the end of the page. query/accept versioning should not impact the endpoint ordering afaik.

I think I may have overlooked the "schema" tab in the endpoint. At least with b535caa it's there. All schemas are repeated again at the end of the page.

Also I came up with a more terse version of the code from #650 (comment) (boilerplate omitted):

class MyRedocView(SpectacularRedocView):
    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        ret = super().get(request, *args, **kwargs)
        ret.data['schema_url'] = set_query_parameters(
            ret.data['schema_url'],
            version=request.GET.get('version'))
        return ret

And this won't break things if a later version of drf-spectacular adds more parameters to the schema url.

(I'd personally still prefer adding a method get_schema_url which can be overridden in a subclass, but this does the job.)

tfranzel added a commit that referenced this issue Feb 10, 2022
eases subclassing redoc/swagger views with custom behavior
tfranzel added a commit that referenced this issue Feb 10, 2022
django will escape & if used without filter and thus breaks url params
concat inside JS contexts. For HTML this is not an issue.

"safe" filter ignored this but is likely too broad, which was the reason
for the change. "escapejs" is specifically for this usecase.
@tfranzel
Copy link
Owner

tfranzel commented Feb 10, 2022

All schemas are repeated again at the end of the page.

should never happen. maybe you have custom hooks that duplicate stuff?

factoring out get_schema_url is a good idea. added this for easier customization. also added version pass-through as there is not really a good reason why not.

this probably makes your custom view obsolete I suppose.

@TauPan
Copy link
Author

TauPan commented Feb 16, 2022

All schemas are repeated again at the end of the page.

should never happen. maybe you have custom hooks that duplicate stuff?

I'm migrating slowly to drf-spectacular in a branch and I still have several schemas from drf_yasg in place as well as JSONSchema which was added by hand (but was ignored by drf-yasg) so it's likely that there is still some duplicate stuff. I'll know when I've gone through the tedious manual work of converting all the annotations to drf-spectacular, which is feasible thanks to your detailed migration guide.

factoring out get_schema_url is a good idea. added this for easier customization. also added version pass-through as there is not really a good reason why not.

this probably makes your custom view obsolete I suppose.

It does, thanks! :)

I've noticed that the swagger view also gets the version passed through, but it seems to be ignored by swagger ui if I add ?version=2 at the end. The dropdown sets urls.primaryName=version%202 (i.e. what I set in SWAGGER_UI_SETTINGS["urls") but setting that as query parameter also has no effect.

@tfranzel
Copy link
Owner

our yasg migration was rather easy since the decorators are very closely related, but I can see that it takes longer the first time doing it. When more tricks from the yasg feature-set were used it makes this more complicated of course.

I've noticed that the swagger view also gets the version passed through, but it seems to be ignored by swagger ui

yes. swagger-ui ignores our prepared parameter schema_url when there are explicit urls (SWAGGER_UI_SETTINGS["urls"]). that's just a reasonable choice by swagger-ui. so either use the topbar preset with urls or pass version as query param. impossible to have both.

also make sure that the parameters (?) precede the fragment (#):

http://127.0.0.1:8000/api/schema/swagger-ui/?version=v3#/some-tag

I have tested both cases with the master branch and it works as expected for me.

@tfranzel
Copy link
Owner

ok small clarification: you can use the topbar. you just cannot pass urls in the settings, when you want to use query params, remove urls from the settings which gives you have a schema input field in the UI. It accepts the ?version=v2

@TauPan
Copy link
Author

TauPan commented Feb 16, 2022

ok small clarification: you can use the topbar. you just cannot pass urls in the settings, when you want to use query params, remove urls from the settings which gives you have a schema input field in the UI. It accepts the ?version=v2

Woah! That definitely works and the links to the versions I put in my description also work... Excellent! 🥳

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