axnsan12 / drf-yasg

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

Provide a drf-hal-json integration #191

Open webjunkie opened 5 years ago

webjunkie commented 5 years ago

I'm using https://github.com/Artory/drf-hal-json/ that basically provides custom base serializers that add dynamically fields like _links into the response via the to_representation method.

I tried to somehow get that into the schema as well, but I'm getting stuck. Are there any directions on what I need to provide/subclass/overwrite, so to make it work?

I imagine I could inspect my serializer objects, since they have a property which dynamic fields need to be added the the schema.

Any help or ideas where to look would be appreciated. Thanks!

webjunkie commented 5 years ago

I solved this now in a same fashion as suggested in https://github.com/axnsan12/drf-yasg/issues/92#issuecomment-378539214

Here is a sample:

class HalModelSerializerInspector(FieldInspector):
    def process_result(self, result: openapi.Schema, method_name: str, obj: Serializer,
                       **kwargs: Dict[Any, Any]) -> openapi.Schema:
        if isinstance(obj, serializers.HalModelSerializer) and method_name == 'field_to_swagger_object':
            model_response = self.formatted_model_result(result, obj)
            if obj.parent is None and self.view.action != 'list':
                # It will be top level object not in list, decorate with data
                return model_response
            return model_response

        return result

    @staticmethod
    def generate_link(field: Field) -> Tuple[str, openapi.Schema]:
        field_schema = openapi.Schema(
            title='Relationship object',
            type=openapi.TYPE_OBJECT,
            properties=OrderedDict((
                ('href', openapi.Schema(
                    type=openapi.TYPE_STRING,
                    title='Link to related resource',
                )),
            ))
        )
        return field.field_name, field_schema

    @staticmethod
    def _is_link_field(field: Field) -> bool:
        # ManyRelatedField could wrap any type so we need to analyze the underlying type
        if isinstance(field, ManyRelatedField):
            field = field.child_relation
        return isinstance(field, serializers.HalIncludeInLinksMixin)

    @staticmethod
    def _is_embedded_field(field: Field) -> bool:
        return isinstance(field, BaseSerializer)

    @staticmethod
    def grab_fields_from_schema(field_names: Set[str], schema: openapi.Schema,
                                title: str) -> Optional[openapi.Schema]:
        subset_fields = [(key, val) for key, val in schema.properties.items() if key in field_names]
        if subset_fields:
            return openapi.Schema(title=title, type=openapi.TYPE_OBJECT, properties=OrderedDict(subset_fields))
        return None

    def generate_links(self, obj: Serializer) -> Optional[openapi.Schema]:
        relationships_properties = []
        for field in obj.fields.values():
            if self._is_link_field(field):
                relationships_properties.append(self.generate_link(field))
        if relationships_properties:
            return openapi.Schema(
                title='Relationships of object',
                type=openapi.TYPE_OBJECT,
                properties=OrderedDict(relationships_properties),
            )
        return None

    def formatted_model_result(self, result: openapi.Schema, obj: Serializer) -> openapi.Schema:
        schema = openapi.resolve_ref(result, self.components)
        field_names_embeds = set(p for p in obj.fields.keys() if self._is_embedded_field(obj.fields[p]))
        field_names_links = set(p for p in obj.fields.keys() if self._is_link_field(obj.fields[p]))

        if getattr(schema, 'properties', {}):
            real_properties = OrderedDict(
                (key, val)
                for key, val in schema.properties.items() if key not in field_names_embeds | field_names_links
            )

            embeds = self.grab_fields_from_schema(field_names=field_names_embeds, schema=schema,
                                                  title='Embedded objects')
            if embeds:
                real_properties.update({'_embedded': embeds})
                real_properties.move_to_end('_embedded', last=False)

            links = self.generate_links(obj)
            if links:
                real_properties.update({'_links': links})
                real_properties.move_to_end('_links', last=False)

            schema.properties = real_properties

            if getattr(schema, 'required', []):
                req = list(filter(lambda x: x not in field_names_embeds | field_names_links, schema.required))
                if req:
                    schema.required = req

        return result

How would you add compatibility between the two libraries?

I co-maintain Artory/drf-hal-json, so we could add the inspectors there?

axnsan12 commented 5 years ago

How would you add compatibility between the two libraries?

I co-maintain Artory/drf-hal-json, so we could add the inspectors there?

I'm not generally opposed to adding third-party specific glue code (see djangorestframework-camel-case and django-rest-framework-recursive).

However, it's probably best to have it in your codebase, so any functional changes can be kept in sync with the schema.