closeio / flask-mongorest

Restful API framework wrapped around MongoEngine
Other
522 stars 88 forks source link

Handling different Hypermedia formats #27

Open xogeny opened 11 years ago

xogeny commented 11 years ago

This blog post has a great discussion about hypermedia formats like HAL and Siren.

Personally, I'm quite interested in exploring these formats and supporting one or more of them in my API. The issue I see is that the current approach expects each dispatch_request method to return a dict and then it marshals that through mimerender into either JSON or XML.

But the issue I see in supporting things like HAL or Siren is that these are effectively different content types (application/hal+json and application/vnd.siren+json, respectively). In both cases, you could get by with returning different dictionaries, but the structure of the dictionaries wouldn't be the same and it depends on the contents of the underlying Document. So mimerender can't make this call, it has to come from something resource specific.

You can make something like this:

class HALResource(Resource):
    serialize_embedded = True

    def serialize_field(self, obj, **kwargs):
        if self.serialize_embedded:
            return self.serialize(obj, **kwargs)
        else:
            return self._url(str(obj.id))

    def serialize(self, obj, **kwargs):
        # Get the normal serialized version
        rep = super(HALResource,self).serialize(obj, **kwargs)

        # Pull the database ID out of this and then remove that field
        myid = rep["id"]
        rep.pop("id")

        # Compute the URI for this resource (including host name)
        myuri = self._url(str(myid))

        # Populate the initial _links and _embedded field
        links = {"self": myuri}
        embedded = {}

        # Added related resources to embedded if they have
        # a uri_prefix
        for k in self.related_resources:
            r = self.related_resources[k]()
            if r.uri_prefix!=None:
                v = rep[k]
                rep.pop(k)
                embedded[k] = v

        # Add special fields
        rep["_links"] = links
        if len(embedded)>0:
            rep["_embedded"] = embedded

        # Return the HAL version
        return rep

But this links the Resource with the content type in an ugly way. What if you want to support both formats? It seems to me that what you need is to have the serialize method handle this somehow. Ideally, it would be nice to have renderers for different formats, e.g.


class HALJSonRenderer(SerializeRenderer):
  content_type = 'application/hal+json`
  def serialize(obj, **kwargs):
    ...

class MyResource(Resource):
  renderers = [DefaultJSONRenderer, DefaultXMLRender, HALJSonRenderer, HALXMLRenderer, SirenRenderer, JSONCollectionRenderer]

and then the Resource.serialize method could simply look at the requested content type and call the appropriate renderer. The default value for renderers should include at least [DefaultJSONRenderer, DefaultXMLRenderer] (which would capture the current approach) but could also be extended to include other supported content types without any backward compatibility issues.

Does this seem like a reasonable approach? If so, I could take a shot at making a backward compatible pull request to add this functionality.

xogeny commented 11 years ago

In case anybody is interested and following this ticket, I've got patches that processes the "Accept" header on the request and can match it against different output formats, pick the correct one and provides the correct content type on the response.

For example, with this code you can say "If I get a request with Accept: application/json", then use this code to generate and render the JSON and return a Content-Type: application/json.

But "If I get a request with Accept: application/hal+json", use different code to formulate the response (dict can be organized differently, which would be required for HAL), render the JSON and return Content-Type: application/hal+json.

Now, you may not care about HAL, but the point is that you can have an API where the clients can request different formats (various JSON serializations, XML, YAML, etc) and you can plug-in code to deal with these requests without having to change your Resource definitions.

I'm trying to put together a pull request, but the changes are involved. :-(

pezon commented 9 years ago

@xogeny Do you know what the general steps might be to support CSV with content-type application/csv ?

xogeny commented 9 years ago

@pezon Sorry, I've been away from flask-mongorest for a long time. I really don't have any suggestions. Sorry.