Browse Source

Use drf_yasg to generate swagger

drf_yasg provides more complete swagger output, allowing for generation
of usable clients.

Some custom work was needed to accommodate Netbox's custom field
serializers, and to provide x-nullable attributes where appropriate.
Dave Cameron 7 years ago
parent
commit
b83de7eb11
5 changed files with 102 additions and 5 deletions
  1. 23 1
      netbox/netbox/settings.py
  2. 17 3
      netbox/netbox/urls.py
  3. 60 0
      netbox/utilities/custom_inspectors.py
  4. 1 0
      old_requirements.txt
  5. 1 1
      requirements.txt

+ 23 - 1
netbox/netbox/settings.py

@@ -133,7 +133,6 @@ INSTALLED_APPS = (
     'django_tables2',
     'mptt',
     'rest_framework',
-    'rest_framework_swagger',
     'timezone_field',
     'circuits',
     'dcim',
@@ -144,6 +143,7 @@ INSTALLED_APPS = (
     'users',
     'utilities',
     'virtualization',
+    'drf_yasg',
 )
 
 # Middleware
@@ -246,6 +246,28 @@ REST_FRAMEWORK = {
     'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
 }
 
+# drf_yasg settings for Swagger
+SWAGGER_SETTINGS = {
+    'DEFAULT_FIELD_INSPECTORS': [
+        'utilities.custom_inspectors.NullableBooleanFieldInspector',
+        'utilities.custom_inspectors.CustomChoiceFieldInspector',
+        'drf_yasg.inspectors.CamelCaseJSONFilter',
+        'drf_yasg.inspectors.ReferencingSerializerInspector',
+        'drf_yasg.inspectors.RelatedFieldInspector',
+        'drf_yasg.inspectors.ChoiceFieldInspector',
+        'drf_yasg.inspectors.FileFieldInspector',
+        'drf_yasg.inspectors.DictFieldInspector',
+        'drf_yasg.inspectors.SimpleFieldInspector',
+        'drf_yasg.inspectors.StringDefaultFieldInspector',
+    ],
+    'DEFAULT_PAGINATOR_INSPECTORS': [
+        'utilities.custom_inspectors.NullablePaginatorInspector',
+        'drf_yasg.inspectors.DjangoRestResponsePagination',
+        'drf_yasg.inspectors.CoreAPICompatInspector',
+    ]
+}
+
+
 # Django debug toolbar
 INTERNAL_IPS = (
     '127.0.0.1',

+ 17 - 3
netbox/netbox/urls.py

@@ -4,12 +4,24 @@ from django.conf import settings
 from django.conf.urls import include, url
 from django.contrib import admin
 from django.views.static import serve
-from rest_framework_swagger.views import get_swagger_view
+from drf_yasg.views import get_schema_view
+from drf_yasg import openapi
 
 from netbox.views import APIRootView, HomeView, SearchView
 from users.views import LoginView, LogoutView
 
-swagger_view = get_swagger_view(title='NetBox API')
+schema_view = get_schema_view(
+    openapi.Info(
+        title="NetBox API",
+        default_version='v2',
+        description="API to access NetBox",
+        terms_of_service="https://github.com/digitalocean/netbox",
+        contact=openapi.Contact(email="netbox@digitalocean.com"),
+        license=openapi.License(name="Apache v2 License"),
+    ),
+    validators=['flex', 'ssv'],
+    public=True,
+)
 
 _patterns = [
 
@@ -40,7 +52,9 @@ _patterns = [
     url(r'^api/secrets/', include('secrets.api.urls')),
     url(r'^api/tenancy/', include('tenancy.api.urls')),
     url(r'^api/virtualization/', include('virtualization.api.urls')),
-    url(r'^api/docs/', swagger_view, name='api_docs'),
+    url(r'^api/docs/$', schema_view.with_ui('swagger', cache_timeout=None), name='api_docs'),
+    url(r'^api/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'),
+    url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'),
 
     # Serving static media in Django to pipe it through LoginRequiredMiddleware
     url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),

+ 60 - 0
netbox/utilities/custom_inspectors.py

@@ -0,0 +1,60 @@
+from drf_yasg import openapi
+from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector
+from rest_framework.fields import ChoiceField
+
+from extras.api.customfields import CustomFieldsSerializer
+from utilities.api import ChoiceFieldSerializer
+
+
+class CustomChoiceFieldInspector(FieldInspector):
+    def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
+        # this returns a callable which extracts title, description and other stuff
+        # https://drf-yasg.readthedocs.io/en/stable/_modules/drf_yasg/inspectors/base.html#FieldInspector._get_partial_types
+        SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
+
+        if isinstance(field, ChoiceFieldSerializer):
+            value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
+
+            if set([None] + list(field._choices.keys())) == {None, True, False}:
+                # Special case face and connection_status because the only keys for choices are True and False,
+                # but the underlying field is still a NullBooleanField
+                value_schema = openapi.Schema(type=openapi.TYPE_BOOLEAN)
+                value_schema['x-nullable'] = True
+
+            schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
+                "label": openapi.Schema(type=openapi.TYPE_STRING),
+                "value": value_schema
+            })
+
+            return schema
+
+        elif isinstance(field, CustomFieldsSerializer):
+            schema = SwaggerType(type=openapi.TYPE_OBJECT)
+            return schema
+
+        return NotHandled
+
+
+class NullableBooleanFieldInspector(FieldInspector):
+    def process_result(self, result, method_name, obj, **kwargs):
+
+        if isinstance(result, openapi.Schema) and isinstance(obj, ChoiceField) and result.type == 'boolean':
+            keys = obj.choices.keys()
+            if set(keys) == {None, True, False}:
+                result['x-nullable'] = True
+                result.type = 'boolean'
+
+        return result
+
+
+class NullablePaginatorInspector(PaginatorInspector):
+    def process_result(self, result, method_name, obj, **kwargs):
+        if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
+            next = result.properties['next']
+            if isinstance(next, openapi.Schema):
+                next['x-nullable'] = True
+            previous = result.properties['previous']
+            if isinstance(previous, openapi.Schema):
+                previous['x-nullable'] = True
+
+        return result

+ 1 - 0
old_requirements.txt

@@ -1,2 +1,3 @@
+django-rest-swagger
 psycopg2
 pycrypto

+ 1 - 1
requirements.txt

@@ -3,10 +3,10 @@ django-cors-headers>=2.1.0
 django-debug-toolbar>=1.9.0
 django-filter>=1.1.0
 django-mptt>=0.9.0
-django-rest-swagger>=2.1.0
 django-tables2>=1.19.0
 django-timezone-field>=2.0
 djangorestframework>=3.7.7
+drf-yasg>=1.4.4
 graphviz>=0.8.2
 Markdown>=2.6.11
 natsort>=5.2.0