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
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
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
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': '' }
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
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
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
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
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
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
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"},
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)
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