Monday, June 3, 2013

Getting a Hierarchical URL structure going with DRF

Indians love hierarchy. Without hierarchy, our great nation will come to a stop. We wouldn't know what to do, whom to flatter and whom to diss. We carry over the same love of hierarchy to our technical architecture as well. If there is one thing you can be absolutely sure about DRF, it is that the author could not have been India. It is 2.3.4 and it still doesn't support hierarchical URLs out of box.

There are bunch of ways you can represent related resources in the API response. Out of them all, I liked the idea of returning a URL that you can use to fetch the related resource most appealing. So when you fetch a Publication, you get a list of URLs as part of it. Each URL represents one page. Now these URLs can be top level like:
/api/v1/pages/:pageid:

But since in our system, a page is always associated with a Publication, following seems more natural:
/api/v1/publications/:pubid:/pages/:pageid:

Trouble is that the built in HyperlinkedRelatedField cannot handle the second type of URL. Here is why. The URLs for related objects are generated by reverse lookup of views. Given a view_name, you can find the URL regex from URLconf. Then you fill in the captured parameter in the URL with the corresponding values for the given object and you have your URL.

However, HyperlinkedRelatedField only lets you specify only one captured argument and also mandates that it should be the same name as the corresponding field on the model. So if you are capturing the primary key which is the most common case, it would need to be called 'id' or 'pk'. This cannot work if you are capturing more than one primary keys from the URL since you cannot call both of them 'pk'. So we write our own MultilevelHyperlinkedField: 

class MultilevelHyperlinkedField(serializers.HyperlinkedRelatedField):
    '''
    Generate multilevel urls for foreign key relations
    '''
    def get_attr(self, obj, prop):
        '''
        Follow the dots
        '''
        value = obj
        for component in prop.split('.'):
            value = get_component(value, component)
            if value is None:
                break
        
        return value
    
    def get_url(self, obj, view_name, request, format):
        kwargs = {k: self.get_attr(obj, v) for k,v in self.lookup_field}
        logger.debug(kwargs)
        return reverse(view_name, kwargs=kwargs, request=request, format=format)
    
    def get_object(self, queryset, view_name, view_args, view_kwargs):
        qs_kwargs = {v: view_kwargs[k] for k,v in self.lookup_field}
        return queryset.get(**qs_kwargs)


Now we can build URLs as deep as we want:
pages = MultilevelHyperlinkedField(source='items', 
                                   many=True, 
                                   read_only = True, 
                                   view_name='page_details',
                                   lookup_field=[('pubid','publication.pk'),
                                                     ('pageid', 'pk')])

lookup_field, that can only be a single name in case of HyperlinkedRelatedField, is now a list of tuples. Each tuple provides a mapping between the name under which it is captured in the URL and the name of the field on the model. Notice that the name of the field on the model can be across models using the dot notation. This allows us to generate URLs as we like them.

Once we cross this hurdle, things start looking better for a while before they get ridiculous. I mean, how difficult would you imagine it would be, to serialize JSON? After all JSON is what we are serializing into! Turns out, not as easy as you would imagine. This sounds just too bureaucratic to be happening in my code. We hope to cut the red tape as soon as possible.

No comments:

Post a Comment