コード例 #1
0
ファイル: openapi.py プロジェクト: mdellweg/drf-spectacular
    def resolve_serializer(self, serializer, direction) -> ResolvedComponent:
        assert is_serializer(serializer), (
            f'internal assumption violated because we expected a serializer here and instead '
            f'got a "{serializer}". This may be the result of another app doing some unexpected '
            f'magic or an invalid internal call. Feel free to report this as a bug at '
            f'https://github.com/tfranzel/drf-spectacular/issues ')
        serializer = force_instance(serializer)

        component = ResolvedComponent(
            name=self._get_serializer_name(serializer, direction),
            type=ResolvedComponent.SCHEMA,
            object=serializer,
        )
        if component in self.registry:
            return self.registry[component]  # return component with schema

        self.registry.register(component)
        component.schema = self._map_serializer(serializer, direction)
        # 4 cases:
        #   1. polymorphic container component -> use
        #   2. concrete component with properties -> use
        #   3. concrete component without properties -> prob. transactional so discard
        #   4. explicit list component -> demultiplexed at usage location so discard
        keep_component = (any(nest_tag in component.schema
                              for nest_tag in ['oneOf', 'allOf', 'anyOf'])
                          or component.schema.get('properties', {}))
        if not keep_component:
            del self.registry[component]
            return ResolvedComponent(None, None)  # sentinel
        return component
コード例 #2
0
    def resolve_serializer(self, serializer, direction) -> ResolvedComponent:
        assert is_serializer(serializer)
        serializer = force_instance(serializer)

        component = ResolvedComponent(
            name=self._get_serializer_name(serializer, direction),
            type=ResolvedComponent.SCHEMA,
            object=serializer,
        )
        if component in self.registry:
            return self.registry[component]  # return component with schema

        self.registry.register(component)
        component.schema = self._map_serializer(serializer, direction)
        # 4 cases:
        #   1. polymorphic container component -> use
        #   2. concrete component with properties -> use
        #   3. concrete component without properties -> prob. transactional so discard
        #   4. explicit list component -> demultiplexed at usage location so discard
        keep_component = (
            any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf'])
            or component.schema.get('properties', {})
        )
        if not keep_component:
            del self.registry[component]
            return ResolvedComponent(None, None)  # sentinel
        return component
コード例 #3
0
ファイル: openapi.py プロジェクト: tsouvarev/drf-spectacular
    def get_auth(self, path, method):
        """
        Obtains authentication classes and permissions from view. If authentication
        is known, resolve security requirement for endpoint and security definition for
        the component section.
        For custom authentication subclass ``OpenApiAuthenticationExtension``.
        """
        auths = []

        for authenticator in self.view.get_authenticators():
            scheme = OpenApiAuthenticationExtension.get_match(authenticator)
            if not scheme:
                warn(
                    f'could not resolve authenticator {authenticator.__class__}. There '
                    f'was no OpenApiAuthenticationExtension registered for that class. '
                    f'Try creating one by subclassing it. Ignoring for now.')
                continue

            auths.append(scheme.get_security_requirement(self.view))
            component = ResolvedComponent(
                name=scheme.name,
                type=ResolvedComponent.SECURITY_SCHEMA,
                object=authenticator.__class__,
                schema=scheme.get_security_definition(self.view))
            if component not in self.registry:
                self.registry.register(component)

        perms = [p.__class__ for p in self.view.get_permissions()]
        if permissions.AllowAny in perms:
            auths.append({})
        elif permissions.IsAuthenticatedOrReadOnly in perms and method not in (
                'PUT', 'PATCH', 'POST'):
            auths.append({})
        return auths
コード例 #4
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': ''
        }
コード例 #5
0
ファイル: hooks.py プロジェクト: ToGoBananas/drf-spectacular
 def create_enum_component(name, schema):
     component = ResolvedComponent(
         name=name,
         type=ResolvedComponent.SCHEMA,
         schema=schema,
         object=name,
     )
     generator.registry.register_on_missing(component)
     return component
コード例 #6
0
ファイル: __init__.py プロジェクト: pradeepbbl/pulpcore
    def resolve_serializer(self, serializer, direction):
        """Serializer to component."""
        component_schema = self._map_serializer(serializer, direction)
        if not component_schema.get("properties", {}):
            component = ResolvedComponent(
                name=self._get_serializer_name(serializer, direction),
                type=ResolvedComponent.SCHEMA,
                object=serializer,
            )
            if component in self.registry:
                return self.registry[component]

            component.schema = component_schema
            self.registry.register(component)

        else:
            component = super().resolve_serializer(serializer, direction)

        return component
コード例 #7
0
ファイル: schema.py プロジェクト: goauthentik/authentik
 def create_component(name, schema, type_=ResolvedComponent.SCHEMA):
     """Register a component and return a reference to it."""
     component = ResolvedComponent(
         name=name,
         type=type_,
         schema=schema,
         object=name,
     )
     generator.registry.register_on_missing(component)
     return component
コード例 #8
0
ファイル: openapi.py プロジェクト: tsouvarev/drf-spectacular
    def resolve_serializer(self, method, serializer) -> ResolvedComponent:
        assert is_serializer(serializer)
        serializer = force_instance(serializer)

        component = ResolvedComponent(
            name=self._get_serializer_name(method, serializer),
            type=ResolvedComponent.SCHEMA,
            object=serializer,
        )
        if component in self.registry:
            return self.registry[component]  # return component with schema

        self.registry.register(component)
        component.schema = self._map_serializer(method, serializer)
        # 3 cases:
        #   1. polymorphic container component -> use
        #   2. concrete component with properties -> use
        #   3. concrete component without properties -> prob. transactional so discard
        if 'oneOf' not in component.schema and not component.schema[
                'properties']:
            del self.registry[component]
            return ResolvedComponent(None, None)  # sentinel
        return component
コード例 #9
0
ファイル: openapi.py プロジェクト: jorrete/drf-spectacular
    def get_auth(self):
        """
        Obtains authentication classes and permissions from view. If authentication
        is known, resolve security requirement for endpoint and security definition for
        the component section.
        For custom authentication subclass ``OpenApiAuthenticationExtension``.
        """
        auths = []

        for authenticator in self.view.get_authenticators():
            scheme = OpenApiAuthenticationExtension.get_match(authenticator)
            if not scheme:
                warn(
                    f'could not resolve authenticator {authenticator.__class__}. There '
                    f'was no OpenApiAuthenticationExtension registered for that class. '
                    f'Try creating one by subclassing it. Ignoring for now.'
                )
                continue

            security_requirements = scheme.get_security_requirement(self)
            if security_requirements is not None:
                auths.append(security_requirements)

            component = ResolvedComponent(
                name=scheme.name,
                type=ResolvedComponent.SECURITY_SCHEMA,
                object=authenticator.__class__,
                schema=scheme.get_security_definition(self)
            )
            self.registry.register_on_missing(component)

        if spectacular_settings.SECURITY:
            auths.extend(spectacular_settings.SECURITY)

        perms = [p.__class__ for p in self.view.get_permissions()]
        if permissions.AllowAny in perms:
            auths.append({})
        elif permissions.IsAuthenticatedOrReadOnly in perms and self.method in permissions.SAFE_METHODS:
            auths.append({})
        return auths
コード例 #10
0
    def map_serializer(self, auto_schema, direction):
        schema = super().map_serializer(auto_schema, direction)

        if isinstance(self, PolymorphicProxySerializerExtension):
            sub_serializers = self.target.serializers
        else:
            sub_serializers = [
                self.target._get_serializer_from_model_or_instance(sub_model)
                for sub_model in self.target.model_serializer_mapping
            ]

        resolved_sub_serializers = [
            auto_schema.resolve_serializer(sub, direction)
            for sub in sub_serializers
        ]
        # this will only be generated on return of map_serializer so mock it for now
        mocked_component = ResolvedComponent(
            name=auto_schema._get_serializer_name(self.target, direction),
            type=ResolvedComponent.SCHEMA,
            object=self.target,
            schema=schema)

        # hack for recursive models. at the time of extension execution, not all sub
        # serializer schema have been generated, so no rollup is possible.
        # by registering a local variable scoped postproc hook, we delay this
        # execution to the end where all schemas are present.
        def postprocessing_rollup_hook(generator, result, **kwargs):
            rollup_properties(mocked_component, resolved_sub_serializers)
            result['components'] = generator.registry.build({})
            return result

        # register postproc hook. must run before enum postproc due to rebuilding the registry
        spectacular_settings.POSTPROCESSING_HOOKS.insert(
            0, postprocessing_rollup_hook)
        # and do nothing for now
        return schema
コード例 #11
0
ファイル: geojson.py プロジェクト: maykinmedia/objects-api
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
from drf_spectacular.plumbing import ResolvedComponent
from rest_framework_gis.serializers import GeometryField

geometry = ResolvedComponent(
    name="Geometry",
    type=ResolvedComponent.SCHEMA,
    object="Geometry",
    schema={
        "type": "object",
        "title": "Geometry",
        "description": "GeoJSON geometry",
        "required": ["type"],
        "externalDocs": {"url": "https://tools.ietf.org/html/rfc7946#section-3.1"},
        "properties": {
            "type": {
                "type": "string",
                "description": "The geometry type",
            }
        },
    },
)
point_2d = ResolvedComponent(
    name="Point2D",
    type=ResolvedComponent.SCHEMA,
    object="Point2D",
    schema={
        "type": "array",
        "title": "Point2D",
        "description": "A 2D point",
        "items": {"type": "number"},
コード例 #12
0
ファイル: openapi.py プロジェクト: jorrete/drf-spectacular
    def _map_serializer_field(self, field, direction):
        meta = self._get_serializer_field_meta(field)

        if has_override(field, 'field'):
            override = get_override(field, 'field')
            if is_basic_type(override):
                schema = build_basic_type(override)
            elif isinstance(override, dict):
                schema = override
            else:
                schema = self._map_serializer_field(force_instance(override), direction)

            field_component_name = get_override(field, 'field_component_name')
            if field_component_name:
                component = ResolvedComponent(
                    name=field_component_name,
                    type=ResolvedComponent.SCHEMA,
                    schema=schema,
                    object=field,
                )
                self.registry.register_on_missing(component)
                return append_meta(component.ref, meta)
            else:
                return append_meta(schema, meta)

        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):
                    relation_field = field.parent.parent.Meta.model._meta.get_field(field.parent.source)
                else:
                    relation_field = field.parent.Meta.model._meta.get_field(field.source)
                model_field = relation_field.related_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)), meta)

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

        if isinstance(field, serializers.ListField):
            if isinstance(field.child, _UnvalidatedField):
                return append_meta(build_array_type({}), meta)
            elif 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)

        # 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)
コード例 #13
0
def postprocess_schema_enums(result, generator, **kwargs):
    """
    simple replacement of Enum/Choices that globally share the same name and have
    the same choices. Aids client generation to not generate a separate enum for
    every occurrence. only takes effect when replacement is guaranteed to be correct.
    """
    def iter_prop_containers(schema):
        if isinstance(schema, list):
            for item in schema:
                yield from iter_prop_containers(item)
        elif isinstance(schema, dict):
            if schema.get('properties'):
                yield schema['properties']
            yield from iter_prop_containers(schema.get('oneOf', []))
            yield from iter_prop_containers(schema.get('allOf', []))

    schemas = result.get('components', {}).get('schemas', {})

    overrides = load_enum_name_overrides()

    hash_mapping = defaultdict(set)
    # collect all enums, their names and choice sets
    for props in iter_prop_containers(list(schemas.values())):
        for prop_name, prop_schema in props.items():
            if 'enum' not in prop_schema:
                continue
            hash_mapping[prop_name].add(list_hash(prop_schema['enum']))

    # traverse all enum properties and generate a name for the choice set. naming collisions
    # are resolved and a warning is emitted. giving a choice set multiple names is technically
    # correct but potentially unwanted. also emit a warning there to make the user aware.
    enum_name_mapping = {}
    for prop_name, prop_hash_set in hash_mapping.items():
        for prop_hash in prop_hash_set:
            if prop_hash in overrides:
                enum_name = overrides[prop_hash]
            elif len(prop_hash_set) == 1:
                enum_name = f'{inflection.camelize(prop_name)}Enum'
            else:
                enum_name = f'{inflection.camelize(prop_name)}{prop_hash[:3].capitalize()}Enum'
                warn(
                    f'automatic enum naming encountered a collision for field "{prop_name}". the '
                    f'same name has been used for multiple choice sets. the collision was resolved '
                    f'with {enum_name}. add an entry to ENUM_NAME_OVERRIDES to fix the naming.'
                )
            if enum_name_mapping.get(prop_hash, enum_name) != enum_name:
                warn(
                    f'encountered multiple names for the same choice set ({enum_name}). this '
                    f'may be unwanted even though the generated schema is technically correct. '
                    f'add an entry to ENUM_NAME_OVERRIDES to fix the naming.')
                del enum_name_mapping[prop_hash]
            else:
                enum_name_mapping[prop_hash] = enum_name
            enum_name_mapping[(prop_hash, prop_name)] = enum_name

    # replace all enum occurrences with a enum schema component. cut out the
    # enum, replace it with a reference and add a corresponding component.
    for props in iter_prop_containers(list(schemas.values())):
        for prop_name, prop_schema in props.items():
            if 'enum' not in prop_schema:
                continue

            prop_hash = list_hash(prop_schema['enum'])
            # when choice sets are reused under multiple names, the generated name cannot be
            # resolved from the hash alone. fall back to prop_name and hash for resolution.
            enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[
                prop_hash, prop_name]

            enum_schema = {
                k: v
                for k, v in prop_schema.items() if k in ['type', 'enum']
            }
            prop_schema = {
                k: v
                for k, v in prop_schema.items() if k not in ['type', 'enum']
            }

            component = ResolvedComponent(
                name=enum_name,
                type=ResolvedComponent.SCHEMA,
                schema=enum_schema,
                object=enum_name,
            )
            if component not in generator.registry:
                generator.registry.register(component)
            prop_schema.update(component.ref)
            props[prop_name] = safe_ref(prop_schema)

    # sort again with additional components
    result['components'] = generator.registry.build(
        spectacular_settings.APPEND_COMPONENTS)
    return result