Example #1
0
    def _get_response_for_code(self, serializer):
        serializer = force_instance(serializer)

        if not serializer:
            return {'description': _('No response body')}
        elif isinstance(serializer, serializers.ListSerializer):
            if is_serializer(serializer.child):
                schema = self.resolve_serializer(serializer.child,
                                                 'response').ref
            else:
                schema = self._map_serializer_field(serializer.child,
                                                    'response')
        elif is_serializer(serializer):
            component = self.resolve_serializer(serializer, 'response')
            if not component.schema:
                return {'description': _('No response body')}
            schema = component.ref
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
        elif isinstance(serializer, dict):
            # bypass processing and use given schema directly
            schema = serializer
        else:
            warn(
                f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either '
                f'a serializer or some supported override mechanism. defaulting to '
                f'generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = _('Unspecified response body')

        if self._is_list_view(serializer) and not get_override(
                serializer, 'many') is False:
            schema = build_array_type(schema)
            paginator = self._get_paginator()

            if paginator and is_serializer(serializer):
                paginated_name = f'Paginated{self._get_serializer_name(serializer, "response")}List'
                component = ResolvedComponent(
                    name=paginated_name,
                    type=ResolvedComponent.SCHEMA,
                    schema=paginator.get_paginated_response_schema(schema),
                    object=paginated_name,
                )
                self.registry.register(component)
                schema = component.ref
            elif paginator:
                schema = paginator.get_paginated_response_schema(schema)

        return {
            'content': {
                mt: {
                    'schema': schema
                }
                for mt in self.map_renderers('media_type')
            },
            # Description is required by spec, but descriptions for each response code don't really
            # fit into our model. Description is therefore put into the higher level slots.
            # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject
            'description': ''
        }
Example #2
0
    def resolve_filter_field(self, auto_schema, model, filterset_class,
                             field_name, filter_field):
        from django_filters.rest_framework import filters

        if isinstance(filter_field, filters.OrderingFilter):
            # only here filter_field.field_name is not the model field name/path
            schema = build_basic_type(OpenApiTypes.STR)
        elif filter_field.method:
            if callable(filter_field.method):
                filter_method = filter_field.method
            else:
                filter_method = getattr(filterset_class, filter_field.method)

            try:
                filter_method_hints = typing.get_type_hints(filter_method)
            except:  # noqa: E722
                filter_method_hints = {}

            if 'value' in filter_method_hints and is_basic_type(
                    filter_method_hints['value']):
                schema = build_basic_type(filter_method_hints['value'])
            else:
                schema = self.map_filter_field(filter_field)
        else:
            path = filter_field.field_name.split('__')
            model_field = follow_field_source(model, path)

            if isinstance(model_field, models.Field):
                schema = auto_schema._map_model_field(model_field,
                                                      direction=None)
            else:
                schema = self.map_filter_field(filter_field)

        enum = schema.pop('enum', None)
        if 'choices' in filter_field.extra:
            enum = [c for c, _ in filter_field.extra['choices']]
        if enum:
            schema['enum'] = sorted(enum, key=str)

        description = schema.pop('description', None)
        if filter_field.extra.get('help_text', None):
            description = filter_field.extra['help_text']
        elif filter_field.label is not None:
            description = filter_field.label

        if isinstance(filter_field, filters.BaseCSVFilter):
            schema = build_array_type(schema)
            explode = False
            style = 'form'
        else:
            explode = None
            style = None

        return build_parameter_type(name=field_name,
                                    required=filter_field.extra['required'],
                                    location=OpenApiParameter.QUERY,
                                    description=description,
                                    schema=schema,
                                    explode=explode,
                                    style=style)
Example #3
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer(
            ).build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and isinstance(field, serializers.PrimaryKeyRelatedField):
            # special case handling only for _resolve_path_parameters() where neither queryset nor
            # parent is set by build_field. patch in queryset as _map_serializer_field requires it
            if not field.queryset:
                field.queryset = model_field.related_model.objects.none()
            return self._map_serializer_field(field, direction)
        elif field and not anyisinstance(
                field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, 'JSONField') and isinstance(
                model_field, models.JSONField):
            # fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping
            return build_basic_type(OpenApiTypes.OBJECT)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping.get(
                internal_type)
            if not field_cls:
                warn(
                    f'model field "{model_field.get_internal_type()}" has no mapping in '
                    f'ModelSerializer. it may be a deprecated field. defaulting to "string"'
                )
                return build_basic_type(OpenApiTypes.STR)
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"')
            return build_basic_type(OpenApiTypes.STR)
Example #4
0
    def resolve_filter_field(self, auto_schema, model, filterset_class,
                             field_name, filter_field):
        """
        Generate proper OAS for ObjectTypeFilter
        """
        if isinstance(filter_field, ObjectTypeFilter):
            schema = build_basic_type(OpenApiTypes.URI)
            if "max_length" in filter_field.extra:
                schema["maxLength"] = filter_field.extra.get("max_length")
            if "min_length" in filter_field.extra:
                schema["minLength"] = filter_field.extra["min_length"]

            description = filter_field.extra["help_text"]

            return [
                build_parameter_type(
                    name=field_name,
                    required=filter_field.extra["required"],
                    location=OpenApiParameter.QUERY,
                    description=description,
                    schema=schema,
                )
            ]
        return super().resolve_filter_field(auto_schema, model,
                                            filterset_class, field_name,
                                            filter_field)
Example #5
0
    def _build_filter_method_type(self, filterset_class, filter_field):
        if callable(filter_field.method):
            filter_method = filter_field.method
        else:
            filter_method = getattr(filterset_class, filter_field.method)

        try:
            filter_method_hints = typing.get_type_hints(filter_method)
        except:  # noqa: E722
            filter_method_hints = {}

        if 'value' in filter_method_hints and is_basic_type(
                filter_method_hints['value']):
            return build_basic_type(filter_method_hints['value'])
        else:
            return build_basic_type(OpenApiTypes.STR)
Example #6
0
    def resolve_filter_field(self, auto_schema, model, filterset_class,
                             filter_field):
        if filter_field.method:
            filter_method = getattr(filterset_class, filter_field.method)
            filter_method_hints = typing.get_type_hints(filter_method)

            if 'value' in filter_method_hints and is_basic_type(
                    filter_method_hints['value']):
                schema = build_basic_type(filter_method_hints['value'])
            else:
                schema = self.map_filter_field(filter_field)
        else:
            path = filter_field.field_name.split('__')
            model_field = follow_field_source(model, path)

            if isinstance(model_field, models.Field):
                schema = auto_schema._map_model_field(model_field,
                                                      direction=None)
            else:
                schema = self.map_filter_field(filter_field)

        enum = schema.pop('enum', None)

        if 'choices' in filter_field.extra:
            enum = [c for c, _ in filter_field.extra['choices']]

        description = schema.pop('description', None)

        if filter_field.extra.get('help_text', None):
            description = filter_field.extra['help_text']
        elif filter_field.label is not None:
            description = filter_field.label

        return schema, description, enum
Example #7
0
 def _process_override_parameters(self):
     result = []
     for parameter in self.get_override_parameters():
         if isinstance(parameter, OpenApiParameter):
             if is_basic_type(parameter.type):
                 schema = build_basic_type(parameter.type)
             elif is_serializer(parameter.type):
                 schema = self.resolve_serializer(parameter.type, direction=None).ref
             else:
                 schema = parameter.type
             result.append(build_parameter_type(
                 name=parameter.name,
                 schema=schema,
                 location=parameter.location,
                 required=parameter.required,
                 description=parameter.description,
                 enum=parameter.enum,
                 deprecated=parameter.deprecated,
             ))
         elif is_serializer(parameter):
             # explode serializer into separate parameters. defaults to QUERY location
             mapped = self._map_serializer(parameter, direction=None)
             for property_name, property_schema in mapped['properties'].items():
                 result.append(build_parameter_type(
                     name=property_name,
                     schema=property_schema,
                     location=OpenApiParameter.QUERY,
                     required=property_name in mapped.get('required', [])
                 ))
         else:
             warn(f'could not resolve parameter annotation {parameter}. skipping.')
     return result
Example #8
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer().build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and not anyisinstance(field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping[internal_type]
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"'
            )
            return build_basic_type(OpenApiTypes.STR)
Example #9
0
    def _get_response_bodies(self, path, method):
        response_serializers = self.get_response_serializers(path, method)

        if is_serializer(response_serializers) or is_basic_type(
                response_serializers):
            if method == 'DELETE':
                return {'204': {'description': 'No response body'}}
            return {
                '200':
                self._get_response_for_code(path, method, response_serializers)
            }
        elif isinstance(response_serializers, dict):
            # custom handling for overriding default return codes with @extend_schema
            return {
                str(code): self._get_response_for_code(path, method,
                                                       serializer)
                for code, serializer in response_serializers.items()
            }
        else:
            warn(
                f'could not resolve "{response_serializers}" for {method} {path}. '
                f'Expected either a serializer or some supported override mechanism. '
                f'defaulting to generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = 'Unspecified response body'
            return {'200': self._get_response_for_code(path, method, schema)}
Example #10
0
    def _get_response_bodies(self):
        response_serializers = self.get_response_serializers()

        if is_serializer(response_serializers) or is_basic_type(response_serializers):
            if self.method == 'DELETE':
                return {'204': {'description': _('No response body')}}
            if self.method == 'POST' and getattr(self.view, 'action', None) == 'create':
                return {'201': self._get_response_for_code(response_serializers, '201')}
            return {'200': self._get_response_for_code(response_serializers, '200')}
        elif isinstance(response_serializers, dict):
            # custom handling for overriding default return codes with @extend_schema
            responses = {}
            for code, serializer in response_serializers.items():
                if isinstance(code, tuple):
                    code, media_types = str(code[0]), code[1:]
                else:
                    code, media_types = str(code), None
                content_response = self._get_response_for_code(serializer, code, media_types)
                if code in responses:
                    responses[code]['content'].update(content_response['content'])
                else:
                    responses[code] = content_response
            return responses
        else:
            warn(
                f'could not resolve "{response_serializers}" for {self.method} {self.path}. '
                f'Expected either a serializer or some supported override mechanism. '
                f'defaulting to generic free-form object.'
            )
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = _('Unspecified response body')
            return {'200': self._get_response_for_code(schema, '200')}
Example #11
0
    def _get_request_body(self):
        # only unsafe methods can have a body
        if self.method not in ('PUT', 'PATCH', 'POST'):
            return None

        serializer = force_instance(self.get_request_serializer())

        request_body_required = False
        if is_list_serializer(serializer):
            if is_serializer(serializer.child):
                component = self.resolve_serializer(serializer.child,
                                                    'request')
                schema = build_array_type(component.ref)
            else:
                schema = build_array_type(
                    self._map_serializer_field(serializer.child, 'request'))
            request_body_required = True
        elif is_serializer(serializer):
            if self.method == 'PATCH':
                serializer.partial = True
            component = self.resolve_serializer(serializer, 'request')
            if not component.schema:
                # serializer is empty so skip content enumeration
                return None
            schema = component.ref
            # request body is only required if any required property is not read-only
            readonly_props = [
                p for p, s in component.schema.get('properties', {}).items()
                if s.get('readOnly')
            ]
            required_props = component.schema.get('required', [])
            request_body_required = any(req not in readonly_props
                                        for req in required_props)
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
            if not schema:
                return None
        else:
            warn(
                f'could not resolve request body for {self.method} {self.path}. defaulting to generic '
                'free-form object. (maybe annotate a Serializer class?)')
            schema = build_object_type(
                additionalProperties={},
                description='Unspecified request body',
            )

        request_body = {
            'content': {
                media_type: build_media_type_object(
                    schema,
                    self._get_examples(serializer, 'request', media_type))
                for media_type in self.map_parsers()
            }
        }

        if request_body_required:
            request_body['required'] = request_body_required

        return request_body
Example #12
0
    def _get_response_for_code(self, path, method, serializer):
        serializer = force_instance(serializer)

        if not serializer:
            return {'description': 'No response body'}
        elif isinstance(serializer, serializers.ListSerializer):
            schema = self.resolve_serializer(method, serializer.child).ref
        elif is_serializer(serializer):
            component = self.resolve_serializer(method, serializer)
            if not component:
                return {'description': 'No response body'}
            schema = component.ref
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
        elif isinstance(serializer, dict):
            # bypass processing and use given schema directly
            schema = serializer
        else:
            warn(
                f'could not resolve "{serializer}" for {method} {path}. Expected either '
                f'a serializer or some supported override mechanism. defaulting to '
                f'generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = 'Unspecified response body'

        if isinstance(serializer, serializers.ListSerializer) or is_list_view(
                path, method, self.view):
            # TODO i fear is_list_view is not covering all the cases
            schema = build_array_type(schema)
            paginator = self._get_paginator()
            if paginator:
                schema = paginator.get_paginated_response_schema(schema)

        return {
            'content': {
                mt: {
                    'schema': schema
                }
                for mt in self.map_renderers(path, method)
            },
            # Description is required by spec, but descriptions for each response code don't really
            # fit into our model. Description is therefore put into the higher level slots.
            # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject
            'description': ''
        }
Example #13
0
 def map_serializer_field(self, auto_schema, direction):
     schema = build_basic_type(OpenApiTypes.URI)
     schema.update(
         {
             "minLength": 1,
             "maxLength": 1000,
         }
     )
     return schema
Example #14
0
    def _map_type_hint(self, method):
        hint = getattr(method, '_spectacular_annotation', None) or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint))
        elif is_basic_type(hint):
            return build_basic_type(hint)
        elif getattr(hint, '__origin__', None) is typing.Union:
            if type(None) == hint.__args__[1] and len(hint.__args__) == 2:
                schema = build_basic_type(hint.__args__[0])
                schema['nullable'] = True
                return schema
            else:
                warn(f'type hint {hint} not supported yet. defaulting to "string"')
                return build_basic_type(OpenApiTypes.STR)
        else:
            warn(f'type hint for function "{method.__name__}" is unknown. defaulting to string.')
            return build_basic_type(OpenApiTypes.STR)
Example #15
0
 def _get_format_parameters(self):
     parameters = []
     formats = self.map_renderers('format')
     if api_settings.URL_FORMAT_OVERRIDE and len(formats) > 1:
         parameters.append(
             build_parameter_type(name=api_settings.URL_FORMAT_OVERRIDE,
                                  schema=build_basic_type(OpenApiTypes.STR),
                                  location=OpenApiParameter.QUERY,
                                  enum=formats))
     return parameters
Example #16
0
    def _map_response_type_hint(self, method):
        hint = get_override(method, 'field') or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint), 'response')
        elif is_basic_type(hint, allow_none=False):
            return build_basic_type(hint)
        elif getattr(hint, '__origin__', None) is typing.Union:
            if type(None) == hint.__args__[1] and len(hint.__args__) == 2:
                schema = build_basic_type(hint.__args__[0])
                schema['nullable'] = True
                return schema
            else:
                warn(f'type hint {hint} not supported yet. defaulting to "string"')
                return build_basic_type(OpenApiTypes.STR)
        else:
            warn(
                f'type hint for function "{method.__name__}" is unknown. consider using '
                f'a type hint or @extend_schema_field. defaulting to string.'
            )
            return build_basic_type(OpenApiTypes.STR)
Example #17
0
    def map_serializer_field(self, auto_schema, direction):
        schema = build_basic_type(OpenApiTypes.URI)
        schema.update(
            {
                "minLength": 1,
                "maxLength": 1000,
                "description": "URL reference to this object. "
                "This is the unique identification and location of this object.",
            }
        )

        return schema
Example #18
0
    def _resolve_path_parameters(self, variables):
        """
        Resolve path parameters.

        Extended to omit undesired warns.
        """
        model = getattr(getattr(self.view, "queryset", None), "model", None)
        parameters = []

        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ""

            resolved_parameter = resolve_django_path_parameter(
                self.path_regex,
                variable,
                self.map_renderers("format"),
            )
            if not resolved_parameter:
                resolved_parameter = resolve_regex_path_parameter(
                    self.path_regex, variable)

            if resolved_parameter:
                schema = resolved_parameter["schema"]
            elif model:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    # strip irrelevant meta data
                    irrelevant_field_meta = [
                        "readOnly", "writeOnly", "nullable", "default"
                    ]
                    schema = {
                        k: v
                        for k, v in schema.items()
                        if k not in irrelevant_field_meta
                    }
                    if "description" not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except FieldDoesNotExist:
                    pass

            parameters.append(
                build_parameter_type(
                    name=variable,
                    location=OpenApiParameter.PATH,
                    description=description,
                    schema=schema,
                ))

        return parameters
Example #19
0
    def _resolve_path_parameters(self, variables):
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        parameters = []

        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''

            resolved_parameter = resolve_regex_path_parameter(
                self.path_regex,
                variable,
                self.map_renderers('format'),
            )

            if resolved_parameter:
                schema = resolved_parameter['schema']
            elif not model:
                warn(
                    f'could not derive type of path parameter "{variable}" because because it '
                    f'is untyped and {self.view.__class__} has no queryset. consider adding a '
                    f'type to the path (e.g. <int:{variable}>) or annotating the parameter '
                    f'type with @extend_schema. defaulting to "string".')
            else:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    # strip irrelevant meta data
                    irrelevant_field_meta = [
                        'readOnly', 'writeOnly', 'nullable', 'default'
                    ]
                    schema = {
                        k: v
                        for k, v in schema.items()
                        if k not in irrelevant_field_meta
                    }
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append(
                build_parameter_type(name=variable,
                                     location=OpenApiParameter.PATH,
                                     description=description,
                                     schema=schema))

        return parameters
Example #20
0
    def _map_response_type_hint(self, method):
        hint = get_override(method, 'field') or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint), 'response')

        try:
            return resolve_type_hint(hint)
        except UnableToProceedError:
            warn(
                f'unable to resolve type hint for function "{method.__name__}". consider '
                f'using a type hint or @extend_schema_field. defaulting to string.'
            )
            return build_basic_type(OpenApiTypes.STR)
Example #21
0
    def _map_model_field(self, field):
        assert isinstance(field, models.Field)
        drf_mapping = serializers.ModelSerializer.serializer_field_mapping

        if field.__class__ in drf_mapping:
            # use DRF native field resolution - taken from ModelSerializer.get_fields()
            return self._map_serializer_field(drf_mapping[field.__class__]())
        elif isinstance(field, models.ForeignKey):
            return self._map_model_field(field.target_field)
        else:
            error(
                f'could not resolve model field "{field}" due to missing mapping.'
                'either your field is custom and not based on a known subclasses '
                'or we missed something. let us know.'
            )
            return build_basic_type(OpenApiTypes.STR)
Example #22
0
    def _map_model_field(self, field):
        assert isinstance(field, models.Field)
        drf_mapping = serializers.ModelSerializer.serializer_field_mapping

        if field.__class__ in drf_mapping:
            # use DRF native field resolution - taken from ModelSerializer.get_fields()
            # TODO maybe init the field with args
            return self._map_serializer_field(drf_mapping[field.__class__]())
        elif isinstance(field, models.OneToOneField):
            return self._map_model_field(
                get_field_from_model(field.model, field.model.id))
        else:
            warn(
                f'could not resolve model field "{field}" due to missing mapping.'
                'either your field is custom and not based on a known subclasses '
                'or we missed something. let us know.')
            return build_basic_type(OpenApiTypes.STR)
Example #23
0
    def _resolve_path_parameters(self, variables):
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        parameters = []

        for idx, variable in enumerate(variables):
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''
            required = True

            resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable)

            if resolved_parameter:
                schema, required = resolved_parameter['schema'], resolved_parameter['required']
            elif not model:
                warn(
                    f'could not derive type of path parameter "{variable}" because '
                    f'{self.view.__class__} has no queryset. consider annotating the '
                    f'parameter type with @extend_schema. defaulting to "string".'
                )
            else:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    # strip irrelevant meta data
                    irrelevant_field_meta = ['readOnly', 'writeOnly', 'nullable', 'default']
                    schema = {k: v for k, v in schema.items() if k not in irrelevant_field_meta}
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append({
                "name": variable,
                "in": "path",
                "required": required,
                "description": description,
                'schema': schema,
            })

        return parameters
Example #24
0
    def _get_request_body(self):
        # only unsafe methods can have a body
        if self.method not in ('PUT', 'PATCH', 'POST'):
            return None

        serializer = force_instance(self.get_request_serializer())

        request_body_required = False
        if isinstance(serializer, serializers.ListSerializer):
            component = self.resolve_serializer(serializer.child, 'request')
            schema = build_array_type(component.ref)
            request_body_required = True
        elif is_serializer(serializer):
            component = self.resolve_serializer(serializer, 'request')
            if not component:
                # serializer is empty so skip content enumeration
                return None
            schema = component.ref
            if component.schema.get('required', []):
                request_body_required = True
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
            if not schema:
                return None
        else:
            warn(
                f'could not resolve request body for {self.method} {self.path}. defaulting to generic '
                'free-form object. (maybe annotate a Serializer class?)'
            )
            schema = {
                'type': 'object',
                'additionalProperties': {},  # https://github.com/swagger-api/swagger-codegen/issues/1318
                'description': 'Unspecified request body',
            }

        request_body = {
            'content': {
                request_media_types: {'schema': schema} for request_media_types in self.map_parsers()
            }
        }
        if request_body_required:
            request_body['required'] = request_body_required

        return request_body
Example #25
0
    def _resolve_path_parameters(self, variables):
        parameters = []
        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''

            resolved_parameter = resolve_regex_path_parameter(
                self.path_regex,
                variable,
                self.map_renderers('format'),
            )

            if resolved_parameter:
                schema = resolved_parameter['schema']
            elif get_view_model(self.view) is None:
                warn(
                    f'could not derive type of path parameter "{variable}" because because it '
                    f'is untyped and obtaining queryset from {self.view.__class__} failed. '
                    f'consider adding a type to the path (e.g. <int:{variable}>) or annotating '
                    f'the parameter type with @extend_schema. defaulting to "string".'
                )
            else:
                try:
                    model = get_view_model(self.view)
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append(
                build_parameter_type(name=variable,
                                     location=OpenApiParameter.PATH,
                                     description=description,
                                     schema=schema))

        return parameters
Example #26
0
    def _process_override_parameters(self):
        result = {}
        for parameter in self.get_override_parameters():
            if isinstance(parameter, OpenApiParameter):
                if is_basic_type(parameter.type):
                    schema = build_basic_type(parameter.type)
                elif is_serializer(parameter.type):
                    schema = self.resolve_serializer(parameter.type, 'request').ref
                else:
                    schema = parameter.type

                if parameter.exclude:
                    result[parameter.name, parameter.location] = None
                else:
                    result[parameter.name, parameter.location] = build_parameter_type(
                        name=parameter.name,
                        schema=schema,
                        location=parameter.location,
                        required=parameter.required,
                        description=parameter.description,
                        enum=parameter.enum,
                        deprecated=parameter.deprecated,
                        style=parameter.style,
                        explode=parameter.explode,
                        default=parameter.default,
                        examples=build_examples_list(parameter.examples),
                    )
            elif is_serializer(parameter):
                # explode serializer into separate parameters. defaults to QUERY location
                mapped = self._map_serializer(parameter, 'request')
                for property_name, property_schema in mapped['properties'].items():
                    result[property_name, OpenApiParameter.QUERY] = build_parameter_type(
                        name=property_name,
                        schema=property_schema,
                        description=property_schema.pop('description', None),
                        location=OpenApiParameter.QUERY,
                        required=property_name in mapped.get('required', []),
                    )
            else:
                warn(f'could not resolve parameter annotation {parameter}. skipping.')
        return result
Example #27
0
 def map_filter_field(self, filter_field):
     from django_filters.rest_framework import filters
     mapping = {
         filters.CharFilter: OpenApiTypes.STR,
         filters.BooleanFilter: OpenApiTypes.BOOL,
         filters.DateFilter: OpenApiTypes.DATE,
         filters.DateTimeFilter: OpenApiTypes.DATETIME,
         filters.TimeFilter: OpenApiTypes.TIME,
         filters.UUIDFilter: OpenApiTypes.UUID,
         filters.NumberFilter: OpenApiTypes.NUMBER,
         # TODO underspecified filters. fallback to STR
         # filters.ChoiceFilter: None,
         # filters.TypedChoiceFilter: None,
         # filters.MultipleChoiceFilter: None,
         # filters.DurationFilter: None,
         # filters.NumericRangeFilter: None,
         # filters.RangeFilter: None,
         # filters.BaseCSVFilter: None,
         # filters.LookupChoiceFilter: None,
     }
     return build_basic_type(
         mapping.get(filter_field.__class__, OpenApiTypes.STR))
Example #28
0
    def _resolve_path_parameters(self, variables):
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        parameters = []

        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''

            if not model:
                warn(
                    f'could not derive type of path parameter "{variable}" because '
                    f'{self.view.__class__} has no queryset. consider annotating the '
                    f'parameter type with @extend_schema. defaulting to "string".'
                )
            else:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field)
                    if model_field.help_text:
                        description = force_str(model_field.help_text)
                    elif model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append({
                "name": variable,
                "in": "path",
                "required": True,
                "description": description,
                'schema': schema,
            })

        return parameters
Example #29
0
    def _map_serializer_field(self, field, direction):
        if has_override(field, 'field'):
            override = get_override(field, 'field')
            if is_basic_type(override):
                return build_basic_type(override)
            else:
                return self._map_serializer_field(override, direction)

        meta = self._get_serializer_field_meta(field)

        serializer_field_extension = OpenApiSerializerFieldExtension.get_match(
            field)
        if serializer_field_extension:
            schema = serializer_field_extension.map_serializer_field(
                self, direction)
            return append_meta(schema, meta)

        # nested serializer
        if isinstance(field, serializers.Serializer):
            component = self.resolve_serializer(field, direction)
            return append_meta(component.ref, meta) if component else None

        # nested serializer with many=True gets automatically replaced with ListSerializer
        if isinstance(field, serializers.ListSerializer):
            if is_serializer(field.child):
                component = self.resolve_serializer(field.child, direction)
                return append_meta(build_array_type(component.ref),
                                   meta) if component else None
            else:
                schema = self._map_serializer_field(field.child, direction)
                return append_meta(build_array_type(schema), meta)

        # Related fields.
        if isinstance(field, serializers.ManyRelatedField):
            schema = self._map_serializer_field(field.child_relation,
                                                direction)
            # remove hand-over initkwargs applying only to outer scope
            schema.pop('description', None)
            schema.pop('readOnly', None)
            return append_meta(build_array_type(schema), meta)

        if isinstance(field, serializers.PrimaryKeyRelatedField):
            # read_only fields do not have a Manager by design. go around and get field
            # from parent. also avoid calling Manager. __bool__ as it might be customized
            # to hit the database.
            if getattr(field, 'queryset', None) is not None:
                model_field = field.queryset.model._meta.pk
            else:
                if isinstance(field.parent, serializers.ManyRelatedField):
                    model_field = field.parent.parent.Meta.model._meta.pk
                else:
                    model_field = field.parent.Meta.model._meta.pk

            # primary keys are usually non-editable (readOnly=True) and map_model_field correctly
            # signals that attribute. however this does not apply in the context of relations.
            schema = self._map_model_field(model_field, direction)
            schema.pop('readOnly', None)
            return append_meta(schema, meta)

        if isinstance(field, serializers.StringRelatedField):
            return append_meta(build_basic_type(OpenApiTypes.STR), meta)

        if isinstance(field, serializers.SlugRelatedField):
            return append_meta(build_basic_type(OpenApiTypes.STR), meta)

        if isinstance(field, serializers.HyperlinkedIdentityField):
            return append_meta(build_basic_type(OpenApiTypes.URI), meta)

        if isinstance(field, serializers.HyperlinkedRelatedField):
            return append_meta(build_basic_type(OpenApiTypes.URI), meta)

        if isinstance(field, serializers.MultipleChoiceField):
            return append_meta(
                build_array_type(build_choice_field(field.choices)), meta)

        if isinstance(field, serializers.ChoiceField):
            return append_meta(build_choice_field(field.choices), meta)

        if isinstance(field, serializers.ListField):
            schema = build_array_type({})
            # TODO check this
            if not isinstance(field.child, _UnvalidatedField):
                map_field = self._map_serializer_field(field.child, direction)
                items = {"type": map_field.get('type')}
                if 'format' in map_field:
                    items['format'] = map_field.get('format')
                schema['items'] = items
            return append_meta(schema, meta)

        # DateField and DateTimeField type is string
        if isinstance(field, serializers.DateField):
            return append_meta(build_basic_type(OpenApiTypes.DATE), meta)

        if isinstance(field, serializers.DateTimeField):
            return append_meta(build_basic_type(OpenApiTypes.DATETIME), meta)

        if isinstance(field, serializers.TimeField):
            return append_meta(build_basic_type(OpenApiTypes.TIME), meta)

        if isinstance(field, serializers.EmailField):
            return append_meta(build_basic_type(OpenApiTypes.EMAIL), meta)

        if isinstance(field, serializers.URLField):
            return append_meta(build_basic_type(OpenApiTypes.URI), meta)

        if isinstance(field, serializers.UUIDField):
            return append_meta(build_basic_type(OpenApiTypes.UUID), meta)

        if isinstance(field, serializers.DurationField):
            return append_meta(build_basic_type(OpenApiTypes.STR), meta)

        if isinstance(field, serializers.IPAddressField):
            # TODO this might be a DRF bug. protocol is not propagated to serializer although it
            #  should have been. results in always 'both' (thus no format)
            if 'ipv4' == field.protocol.lower():
                schema = build_basic_type(OpenApiTypes.IP4)
            elif 'ipv6' == field.protocol.lower():
                schema = build_basic_type(OpenApiTypes.IP6)
            else:
                schema = build_basic_type(OpenApiTypes.STR)
            return append_meta(schema, meta)

        # DecimalField has multipleOf based on decimal_places
        if isinstance(field, serializers.DecimalField):
            if getattr(field, 'coerce_to_string',
                       api_settings.COERCE_DECIMAL_TO_STRING):
                content = {
                    **build_basic_type(OpenApiTypes.STR), 'format': 'decimal'
                }
            else:
                content = build_basic_type(OpenApiTypes.DECIMAL)

            if field.max_whole_digits:
                content['maximum'] = int(field.max_whole_digits * '9') + 1
                content['minimum'] = -content['maximum']
            self._map_min_max(field, content)
            return append_meta(content, meta)

        if isinstance(field, serializers.FloatField):
            content = build_basic_type(OpenApiTypes.FLOAT)
            self._map_min_max(field, content)
            return append_meta(content, meta)

        if isinstance(field, serializers.IntegerField):
            content = build_basic_type(OpenApiTypes.INT)
            self._map_min_max(field, content)
            # 2147483647 is max for int32_size, so we use int64 for format
            if int(content.get('maximum', 0)) > 2147483647 or int(
                    content.get('minimum', 0)) > 2147483647:
                content['format'] = 'int64'
            return append_meta(content, meta)

        if isinstance(field, serializers.FileField):
            if spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'request':
                content = build_basic_type(OpenApiTypes.BINARY)
            else:
                use_url = getattr(field, 'use_url',
                                  api_settings.UPLOADED_FILES_USE_URL)
                content = build_basic_type(
                    OpenApiTypes.URI if use_url else OpenApiTypes.STR)
            return append_meta(content, meta)

        if isinstance(field, serializers.SerializerMethodField):
            method = getattr(field.parent, field.method_name)
            return append_meta(self._map_response_type_hint(method), meta)

        if anyisinstance(
                field,
            [serializers.BooleanField, serializers.NullBooleanField]):
            return append_meta(build_basic_type(OpenApiTypes.BOOL), meta)

        if isinstance(field, serializers.JSONField):
            return append_meta(build_basic_type(OpenApiTypes.OBJECT), meta)

        if anyisinstance(field,
                         [serializers.DictField, serializers.HStoreField]):
            content = build_basic_type(OpenApiTypes.OBJECT)
            if not isinstance(field.child, _UnvalidatedField):
                content['additionalProperties'] = self._map_serializer_field(
                    field.child, direction)
            return append_meta(content, meta)

        if isinstance(field, serializers.CharField):
            return append_meta(build_basic_type(OpenApiTypes.STR), meta)

        if isinstance(field, serializers.ReadOnlyField):
            # direct source from the serializer
            assert field.source_attrs, f'ReadOnlyField "{field}" needs a proper source'
            target = follow_field_source(field.parent.Meta.model,
                                         field.source_attrs)

            if callable(target):
                schema = self._map_response_type_hint(target)
            elif isinstance(target, models.Field):
                schema = self._map_model_field(target, direction)
            else:
                assert False, f'ReadOnlyField target "{field}" must be property or model field'
            return append_meta(schema, meta)

        # DRF was not able to match the model field to an explicit SerializerField and therefore
        # used its generic fallback serializer field that simply wraps the model field.
        if isinstance(field, serializers.ModelField):
            schema = self._map_model_field(field.model_field, direction)
            return append_meta(schema, meta)

        warn(
            f'could not resolve serializer field "{field}". defaulting to "string"'
        )
        return append_meta(build_basic_type(OpenApiTypes.STR), meta)
Example #30
0
    def _map_serializer_field(self, method, field):
        if hasattr(field, '_spectacular_annotation'):
            if is_basic_type(field._spectacular_annotation):
                return build_basic_type(field._spectacular_annotation)
            else:
                return self._map_serializer_field(
                    method, field._spectacular_annotation)

        # nested serializer
        if isinstance(field, serializers.Serializer):
            return self.resolve_serializer(method, field).ref

        # nested serializer with many=True gets automatically replaced with ListSerializer
        if isinstance(field, serializers.ListSerializer):
            return build_array_type(
                self.resolve_serializer(method, field.child).ref)

        # Related fields.
        if isinstance(field, serializers.ManyRelatedField):
            return build_array_type(
                self._map_serializer_field(method, field.child_relation))

        if isinstance(field, serializers.PrimaryKeyRelatedField):
            # read_only fields do not have a Manager by design. go around and get field
            # from parent. also avoid calling Manager. __bool__ as it might be customized
            # to hit the database.
            if getattr(field, 'queryset', None) is not None:
                return self._map_model_field(field.queryset.model._meta.pk)
            else:
                model = field.parent.Meta.model
                return self._map_model_field(
                    get_field_from_model(model, model.id))

        if isinstance(field, serializers.StringRelatedField):
            return build_basic_type(OpenApiTypes.STR)

        if isinstance(field, serializers.SlugRelatedField):
            return build_basic_type(OpenApiTypes.STR)

        if isinstance(field, serializers.HyperlinkedIdentityField):
            return build_basic_type(OpenApiTypes.URI)

        if isinstance(field, serializers.HyperlinkedRelatedField):
            return build_basic_type(OpenApiTypes.URI)

        # ChoiceFields (single and multiple).
        # Q:
        # - Is 'type' required?
        # - can we determine the TYPE of a choicefield?
        if isinstance(field, serializers.MultipleChoiceField):
            return build_array_type(self._map_choicefield(field))

        if isinstance(field, serializers.ChoiceField):
            return self._map_choicefield(field)

        if isinstance(field, serializers.ListField):
            schema = build_array_type({})
            # TODO check this
            if not isinstance(field.child, _UnvalidatedField):
                map_field = self._map_serializer_field(method, field.child)
                items = {"type": map_field.get('type')}
                if 'format' in map_field:
                    items['format'] = map_field.get('format')
                schema['items'] = items
            return schema

        # DateField and DateTimeField type is string
        if isinstance(field, serializers.DateField):
            return build_basic_type(OpenApiTypes.DATE)

        if isinstance(field, serializers.DateTimeField):
            return build_basic_type(OpenApiTypes.DATETIME)

        if isinstance(field, serializers.EmailField):
            return build_basic_type(OpenApiTypes.EMAIL)

        if isinstance(field, serializers.URLField):
            return build_basic_type(OpenApiTypes.URI)

        if isinstance(field, serializers.UUIDField):
            return build_basic_type(OpenApiTypes.UUID)

        if isinstance(field, serializers.IPAddressField):
            # TODO this might be a DRF bug. protocol is not propagated to serializer although it
            #  should have been. results in always 'both' (thus no format)
            if 'ipv4' == field.protocol.lower():
                return build_basic_type(OpenApiTypes.IP4)
            elif 'ipv6' == field.protocol.lower():
                return build_basic_type(OpenApiTypes.IP6)
            else:
                return build_basic_type(OpenApiTypes.STR)

        # DecimalField has multipleOf based on decimal_places
        if isinstance(field, serializers.DecimalField):
            content = {'type': 'number'}
            if field.decimal_places:
                content['multipleOf'] = float('.' +
                                              (field.decimal_places - 1) *
                                              '0' + '1')
            if field.max_whole_digits:
                content['maximum'] = int(field.max_whole_digits * '9') + 1
                content['minimum'] = -content['maximum']
            self._map_min_max(field, content)
            return content

        if isinstance(field, serializers.FloatField):
            content = build_basic_type(OpenApiTypes.FLOAT)
            self._map_min_max(field, content)
            return content

        if isinstance(field, serializers.IntegerField):
            content = build_basic_type(OpenApiTypes.INT)
            self._map_min_max(field, content)
            # 2147483647 is max for int32_size, so we use int64 for format
            if int(content.get('maximum', 0)) > 2147483647 or int(
                    content.get('minimum', 0)) > 2147483647:
                content['format'] = 'int64'
            return content

        if isinstance(field, serializers.FileField):
            # TODO returns filename. but does it accept binary data on upload?
            return build_basic_type(OpenApiTypes.STR)

        if isinstance(field, serializers.SerializerMethodField):
            method = getattr(field.parent, field.method_name)
            return self._map_type_hint(method)

        if isinstance(field, serializers.BooleanField):
            return build_basic_type(OpenApiTypes.BOOL)

        if anyisinstance(field, [
                serializers.JSONField, serializers.DictField,
                serializers.HStoreField
        ]):
            return build_basic_type(OpenApiTypes.OBJECT)

        if isinstance(field, serializers.CharField):
            return build_basic_type(OpenApiTypes.STR)

        if isinstance(field, serializers.ReadOnlyField):
            # direct source from the serializer
            assert field.source_attrs, 'ReadOnlyField needs a proper source'
            target = follow_field_source(field.parent.Meta.model,
                                         field.source_attrs)

            if callable(target):
                return self._map_type_hint(target)
            elif isinstance(target, models.Field):
                return self._map_model_field(target)

        warn(
            f'could not resolve serializer field {field}. defaulting to "string"'
        )
        return build_basic_type(OpenApiTypes.STR)