trailblazer / roar-rails

Use Roar's representers in Rails.
http://roar.apotomo.de
MIT License
235 stars 65 forks source link

Consuming URLs for Relationships #89

Closed summera closed 10 years ago

summera commented 10 years ago

When updating or creating a resource in RESTful hypermedia APIs I have seen them designed in such a way where you relate resources via URLs instead of id's. To make up an example and follow your Fruit Bowl analogy, if I wanted to create a fruit as a part of a bowl I would do something along the lines of:

POST "http://fruits"
{ 
  "title": "Apple"
  "bowl": "http://bowls/1"
}

Behind the scenes, this would deserialize the passed in bowl url (http://bowls/1) to the bowl object with id 1 and relate the new fruit to the existing bowl.

Would really appreciate some insight on how I would go about this in roar-rails. :)

apotonick commented 10 years ago

You can use the :deserialize option from representable.

property :bowl, deserialize: lambda { |object, fragment, *| Bowl.find id_from_url(fragment) }
summera commented 10 years ago

Awesome! That's easy enough. Thanks :+1:

kookster commented 9 years ago

How does this work when it is a link in a HAL representer instead of a property? I don't see a way to change how links are deserialized so I can interpret them into ActiveRecord relations.

summera commented 9 years ago

@kookster Have you tried sending in your request with the url relation nested under _links?

Also I created this gem https://github.com/sweatshirtio/actionback for deserializing urls to ActiveRecord instances. There is an example in the README using roar-rails. The gem still needs work but it's on its way. :)

Hope this helps.

summera commented 9 years ago

@kookster I don't think you can deserialize with link

kookster commented 9 years ago

@summera thanks for the info, but that isn't what I am trying to figure out.

When the representer has a url value for a property, then you can specify the deserialize option on the property with a lambda to look up the model based on the url value. I see how your actionback gem helps with that.

The problem is that I see no way to specify the deserialize option for a link definition in the representer. This might look like the following:

  link rel: :bowl do
    {
      href: bowl_url(represented.bowl)
    }
  end

No way to specify in a deserialize option. Additional options besides rel are considered as attributes, not options for the Definition.

apotonick commented 9 years ago

When specifying a link using ::link, the representer will deserialise Hyperlink objects into the model's link field (with representer modules). https://github.com/apotonick/roar#consuming-hypermedia

In case you're using decorators, this is deserialised into the decorator's link field. If you still want it on the model, include HypermediaConsumer into your decorator: https://github.com/apotonick/roar/blob/master/lib/roar/decorator.rb

This will, when deserialising, call model.links= [link, link, link, ...].

kookster commented 9 years ago

@apotonick this is really, really, helpful. I am using decorators, I'll try that.

I won't get to use those links in the consume! call to immediately populate model relations, but at least I'll be able to access the link data, and I can do something with it after that.

apotonick commented 9 years ago

We can think about a block for #consume! that gives you the populated model, @kookster ?

kookster commented 9 years ago

@apotonick I would like to confine serialization logic to the representer, so adding a block to #consume!, while it would make this possible, wouldn't be my choice of where to put that logic.

If I pursue using hal-json links to add AR relations, my next line of thinking is to override links_definition_options or create_links_definition so I can define deserializer callbacks that will allow me to set the relations on the represented object.

I'm also thinking about abandoning using hal-json links for this, and treating them as read-only. That would basically make it so my API provides hal-json content for reading and navigating the data, but to add or change data it would only support regular json. Not the end of the world - perhaps hal-json just works better as a read-only standard.

One way to do this would be to do something like what @summera did above, and have write-only properties for any links that can be created or updated by the client, and add deserializer lambdas for those as per above. I could also then use actionback in the lambdas to generically handle this.

I wouldn't need to add these properties for all links; it seems to be a good pattern for (forgive my UML terminology) aggregation relationships where I need to have a way to express the relationship of the child to the parent, but not composition, where the parent is responsible for creating/managing the child.

With composition, I can express the link to the parent via the url that the new object is posted to: /parent/#{parent_id}/child

For aggregation, I can express it with a url attribute on the child, where the same parent_url from links (i.e. _links{ parent: { href: 'parent_url'}}) would be a write-only property of the child:

class ChildRepresenter < Roar::Decorator
    property :parent_url, readable: false
end

I would want to continue to have the parent link expressed as a url rather than the id, so the client wouldn't be parsing the url string to pull out the database key integer.

In talking with the engineer here who is working on the API client code, that seems to be their preferred approach.

apotonick commented 9 years ago

Cool, interesting thoughts!

Am I right assuming that you want to do the following?

  1. Deserialize incoming JSON-HAL document with hypermedia.
  2. Let representer populate the model's attributes accordingly, e.g.represented.title= "Roxanne" (so far, this is normal representable/Roar behaviour).
  3. Handle hypermedia using separate code and populate/update/change more model state here specific to the hypermedia state.
  4. Implement that hypermedia parsing in a generic, reusable way.

I see two ways for that.

Non-generic

This is pure pseudo-code.

model.from_json("{..}") do |mdl, links|
  mdl.items = Item.find links[:items].ids
end

Here, you could partly hook into deserialisation process and change state without having to worry about parsing shizzle.

Generic

link :items, parse_filter: lambda { .. } do 
  items_url
end

In this example, a new option :parse_filter could allow you to define parsing code per link. Is that what you're after?

The reason this is not implemented, yet, in Roar is because I wanted to wait for users to bring in some requirements. It's your chance to work something out with me now! :grimacing: