axnsan12 / drf-yasg

Automated generation of real Swagger/OpenAPI 2.0 schemas from Django REST Framework code.
https://drf-yasg.readthedocs.io/en/stable/
Other
3.37k stars 433 forks source link

Add a way to specify common API path prefix #146

Closed phihag closed 6 years ago

phihag commented 6 years ago

Currently, the documentation states that in order to set an API prefix, one should set the Django option FORCE_SCRIPT_NAME. But FORCE_SCRIPT_NAME has huge implications on the rest of the project and is thus not an option for larger projects. It is also a classic case of using a global setting to fix a local problem, which should be avoided at all costs.

There should be a way to set a prefix just for one swaggerized API, and not for the whole project.

For reference, I have a project whose URL structure is like this:

# service/myservice/urls/api.py
urlpatterns = [
    # ...
    url(r'^v2/', include(api2.urlpatterns)),
]

# service/myservice/urls/api2.py
urlpatterns = [
    url(r'^auth/login$', api2.login, name='api2_auth_login'),
    url(r'^user$', api2.user, name='api2_user'),
    url(r'^doc(?P<format>\.json|\.yaml)$', api2.schema_view.without_ui(cache_timeout=0), name='schema-json'),
    url(r'^doc/$', api2.schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]

We are using django-hosts to support multiple hostnames, but the service.myservice.urls.api is the root url configuration for the current endpoint.

Thus, a typical URL would be https://api.prod.example.net/v2/auth/login. In swagger, I want to show the base URL as https://api.prod.example.net/v2/ and the route as /auth/login. How do I do that with drf_yasg?

axnsan12 commented 6 years ago

Thus, a typical URL would be https://api.prod.example.net/v2/auth/login. In swagger, I want to show the base URL as https://api.prod.example.net/v2/ and the route as /auth/login. How do I do that with drf_yasg?

I don't understand. Doesn't that happen by default?

phihag commented 6 years ago

I forgot one more piece of the problem: I'm passing in

urlconf='service.myservice.urls.api2',

to get_schema_view. Hence, when the path is determined, it's just the local component after /v2/, e.g. just '/auth/login'.

But that means just modifying my urlconf so that the total route is defined in service.myservice.urls.api2

# service/myservice/urls/api.py
urlpatterns = [
    # ...
] + api2.url_patterns

# service/myservice/urls/api2.py
api2_urlpatterns = [
    url(r'^auth/login$', api2.login, name='api2_auth_login'),
    url(r'^user$', api2.user, name='api2_user'),
    url(r'^doc(?P<format>\.json|\.yaml)$', api2.schema_view.without_ui(cache_timeout=0), name='schema-json'),
    url(r'^doc/$', api2.schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]
urlpatterns = [url(r'^v2/', include(api2_urlpatterns)]

solves the problem for me.

Although I think that having the ability to add a prefix - preferably simply by adding a path to the base URL - is a good idea in general, and would be necessary in a more complicated setup than mine, this is a valid workaround I can employ, and I am thus clothing this issue.

Thanks for your help!

axnsan12 commented 6 years ago

Although I think that having the ability to add a prefix - preferably simply by adding a path to the base URL - is a good idea in general, and would be necessary in a more complicated setup than mine

The full request path in Django is always determined by SCRIPT_NAME (external path prefix outside of Django's control, e.g. reverse proxies) plus the full Django route. This was the reasoning behind giving no such "force" mechanism in drf-yasg, since there is already a "force" mechanism in Django for external paths (FORCE_SCRIPT_NAME), and full internal routes are always available to be passed into the schema view at an appropriate location.

I might reconsider this if someone provides an example of such a setup where passing full urlpatterns is not possible or desirable, but for now I would consider the implicit mechanisms sufficient.

alissonmuller commented 5 years ago

Alternatively, you can override get_schema in a custom generator, and set the basePath attribute:

from drf_yasg.generators import OpenAPISchemaGenerator

class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
    def get_schema(self, *args, **kwargs):
        schema = super().get_schema(*args, **kwargs)
        schema.basePath = '/v2' # API prefix
        return schema

schema_view = get_schema_view(
    ... other arguments ...
    generator_class=CustomOpenAPISchemaGenerator,
)
kushalsht commented 5 years ago

Thanks

nseetim commented 2 years ago

Great job you guys are doing on this I must confess,

Assuming I have a URL api.example.com/users/login, and the prefix /login is just one of many in the users app

In the root url.py file we have something like:

urlpatterns = [
    path('admin/', admin.site.urls),

    # urls for the user on-boarding and user related functions
    path('users/', include('users.api.urls')),
    path('podcasts/', include('podcast.api.urls')),
    path('audiostream/', include('audiostreaming.api.urls')),]

in the users app we have something like:

from users.api.views import (signup_user_api, login_api,
                             email_verification_request, login_api,
                             logout_api, activate_api, subscribe, unsubscribe
                            )

app_name = 'users'

urlpatterns = [
    path('signup', signup_user_api, name='signup_user_api'),
    path('login', login_api, name='login_api'),]

so each app URL is routed to it in the fashion above, when using the URL parameter in the _get_schemaview it doesn't display the full path with the prefix as I supply, it trims off the path component, and using the FORCE_SCRIPT_NAME solution won't work here as it would affect the whole project and changing the URL structure would cause a lot of breaking changes, so I was wondering if it would be possible to add a means of stating what URL the documentation would use on an individual app basis just as it's supplied to it

currently the URL produced is api.example.co/login ommiting the /users path component

if there's a way you would recommend that won't cause breaking changes I would be very much open to it

mohitbestpeers commented 2 years ago

Alternatively, you can override get_schema in a custom generator, and set the basePath attribute:

from drf_yasg.generators import OpenAPISchemaGenerator

class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
    def get_schema(self, *args, **kwargs):
        schema = super().get_schema(*args, **kwargs)
        schema.basePath = '/v2' # API prefix
        return schema

schema_view = get_schema_view(
    ... other arguments ...
    generator_class=CustomOpenAPISchemaGenerator,
)

Please can you help me.

I want to prepend /v1/ to all the URLs in schema. how can I do that.

nseetim commented 1 year ago

@mohitbestpeers I dont know if you have figured out how to do this yet but adapting from the answer given by @alissonmuller what I did was to create a helper function somewhere, in my case i have a utilities file,

#utilities.py
from drf_yasg.generators import OpenAPISchemaGenerator

def my_custom_openapi_schema_generator(path_prefix:str):
    if not isinstance(path_prefix, str):
        raise TypeError('The supplied path_prefix must be of type str')
    class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
        def get_schema(self, *args, **kwargs):
            schema = super().get_schema(*args, **kwargs)
            schema.basePath = path_prefix # API prefix
            return schema
    return CustomOpenAPISchemaGenerator

Then you can use this as I have done in any of your urls and specify the path_prefix you want for the urls in that file or so

#urls.py
from utilities import my_custom_openapi_schema_generator

from drf_yasg.views import get_schema_view
from drf_yasg import openapi

custom_generator_class = my_custom_openapi_schema_generator(path_prefix='/user')

schema_view = get_schema_view(
   openapi.Info(
      title="Users API",
      default_version='v1',
      description='This is the official documentation for all userAPIs',
   ),
   generator_class=custom_generator_class,
   url= f'{API_BASE_URL}',
   urlconf='users.api.urls',
)