"""This module contains response processors.""" from collections import defaultdict from django.utils import six from rest_framework.serializers import ListSerializer from rest_framework.utils.serializer_helpers import ReturnDict from dynamic_rest.conf import settings from dynamic_rest.tagged import TaggedDict POST_PROCESSORS = {} def register_post_processor(func): """ Register a post processor function to be run as the final step in serialization. The data passed in will already have gone through the sideloading processor. Usage: @register_post_processor def my_post_processor(data): # do stuff with `data` return data """ global POST_PROCESSORS key = func.__name__ POST_PROCESSORS[key] = func return func def post_process(data): """Apply registered post-processors to data.""" for post_processor in POST_PROCESSORS.values(): data = post_processor(data) return data class SideloadingProcessor(object): """A processor that sideloads serializer data. Sideloaded records are returned under top-level response keys and produces responses that are typically smaller than their nested equivalent. """ def __init__(self, serializer, data): """Initializes and runs the processor. Arguments: serializer: a DREST serializer data: the serializer's representation """ if isinstance(serializer, ListSerializer): serializer = serializer.child self.data = {} self.seen = defaultdict(set) self.plural_name = serializer.get_plural_name() self.name = serializer.get_name() # process the data, optionally sideloading self.process(data) # add the primary resource data into the response data resource_name = self.name if isinstance( data, dict ) else self.plural_name self.data[resource_name] = data def is_dynamic(self, data): """Check whether the given data dictionary is a DREST structure. Arguments: data: A dictionary representation of a DRF serializer. """ return isinstance(data, TaggedDict) def process(self, obj, parent=None, parent_key=None, depth=0): """Recursively process the data for sideloading. Converts the nested representation into a sideloaded representation. """ if isinstance(obj, list): for key, o in enumerate(obj): # traverse into lists of objects self.process(o, parent=obj, parent_key=key, depth=depth) elif isinstance(obj, dict): dynamic = self.is_dynamic(obj) returned = isinstance(obj, ReturnDict) if dynamic or returned: # recursively check all fields for key, o in six.iteritems(obj): if isinstance(o, list) or isinstance(o, dict): # lists or dicts indicate a relation self.process( o, parent=obj, parent_key=key, depth=depth + 1 ) if not dynamic or getattr(obj, 'embed', False): return serializer = obj.serializer name = serializer.get_plural_name() instance = getattr(obj, 'instance', serializer.instance) instance_pk = instance.pk if instance else None pk = getattr(obj, 'pk_value', instance_pk) or instance_pk # For polymorphic relations, `pk` can be a dict, so use the # string representation (dict isn't hashable). pk_key = repr(pk) # sideloading seen = True # if this object has not yet been seen if pk_key not in self.seen[name]: seen = False self.seen[name].add(pk_key) # prevent sideloading the primary objects if depth == 0: return # TODO: spec out the exact behavior for secondary instances of # the primary resource # if the primary resource is embedded, add it to a prefixed key if name == self.plural_name: name = '%s%s' % ( settings.ADDITIONAL_PRIMARY_RESOURCE_PREFIX, name ) if not seen: # allocate a top-level key in the data for this resource # type if name not in self.data: self.data[name] = [] # move the object into a new top-level bucket # and mark it as seen self.data[name].append(obj) else: # obj sideloaded, but maybe with other fields for o in self.data.get(name, []): if o.instance.pk == pk: o.update(obj) break # replace the object with a reference if parent is not None and parent_key is not None: parent[parent_key] = pk