Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use REST with nested resources which are represented in XML?

My aim is to create nested resources via one REST request. The REST requests is represented via a XML document. That works fine for single resources but I could not manage it for nested ones. OK I'll give you a little example next.

First create a new rails project

rails forrest

Next we generate the scaffolds of two resources, the trees and the bird's nests.

./script/generate scaffold tree name:string
./script/generate scaffold bird_nest tree_id:integer bird_type:string eggs_count:integer

In the File ./forrest/app/models/tree.rb we insert the "has_many" line below because a tree can have many bird's nests :-)

class Tree < ActiveRecord::Base
  has_many :bird_nests
end

In the File ./forrest/app/models/bird_nest.rb we insert the "belongs_to" line below because every bird's nest should belong to a tree.

class BirdNest < ActiveRecord::Base
  belongs_to :tree
end

Afterwards we set up the database and start the server:

rake db:create
rake db:migrate
./script/server

Just copy and paste this XML sniplet to a file named "tree.xml"...

<tree>
  <name>Apple</name>
</tree>

...and post it to the service by cURL to create a new tree:

curl  -H 'Content-type: application/xml' -H 'Accept: application/xml' -d @tree.xml http://localhost:3000/trees/ -X POST

This works fine. Also for the bird's nest XML (file name "bird-nest.xml") separately. If we send this...

<bird-nest>
  <tree-id>1</tree-id>
  <bird-type>Sparrow</bird-type>
  <eggs-count>2</eggs-count>
</bird-nest>

...also via the following cURL statement. That resource is created properly!

curl  -H 'Content-type: application/xml' -H 'Accept: application/xml' -d @bird-nest.xml http://localhost:3000/bird_nests/ -X POST

OK everything is fine so far. Now comes the point where the rubber meets the road. We create both resources in one request. So here is the XML for our tree which contains one bird's nest:

<tree>
  <name>Cherry</name>
  <bird-nests>
    <bird-nest>
      <bird-type>Blackbird</bird-type>
      <eggs-count>2</eggs-count>
    </bird-nest>
  </bird-nests>
</tree>

We trigger the appropriate request by using cURL again...

curl  -H 'Content-type: application/xml' -H 'Accept: application/xml' -d @tree-and-bird_nest.xml http://localhost:3000/trees/ -X POST

...and now we'll get a server error in the (generated) "create" method of the tree's controller: AssociationTypeMismatch (BirdNest expected, got Array)

In my point of view this is the important part of the server's log regarding received attributes and error message:

Processing TreesController#create (for 127.0.0.1 at 2009-02-17 11:29:20) [POST]
  Session ID: 8373b8df7629332d4e251a18e844c7f9
  Parameters: {"action"=>"create", "controller"=>"trees", "tree"=>{"name"=>"Cherry", "bird_nests"=>{"bird_nest"=>{"bird_type"=>"Blackbird", "eggs_count"=>"2"}}}}
  SQL (0.000082)   SET NAMES 'utf8'
  SQL (0.000051)   SET SQL_AUTO_IS_NULL=0
  Tree Columns (0.000544)   SHOW FIELDS FROM `trees`


    ActiveRecord::AssociationTypeMismatch (BirdNest expected, got Array):
        /vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb:150:in `raise_on_type_mismatch'
        /vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:146:in `replace'
        /vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:146:in `each'
        /vendor/rails/activerecord/lib/active_record/associations/association_collection.rb:146:in `replace'
        /vendor/rails/activerecord/lib/active_record/associations.rb:1048:in `bird_nests='
        /vendor/rails/activerecord/lib/active_record/base.rb:2117:in `send'
        /vendor/rails/activerecord/lib/active_record/base.rb:2117:in `attributes='
        /vendor/rails/activerecord/lib/active_record/base.rb:2116:in `each'
        /vendor/rails/activerecord/lib/active_record/base.rb:2116:in `attributes='
        /vendor/rails/activerecord/lib/active_record/base.rb:1926:in `initialize'
        /app/controllers/trees_controller.rb:43:in `new'
        /app/controllers/trees_controller.rb:43:in `create'

So my question is what I'm doing wrong regarding the nesting of the XML resources. Which would be the right XML syntax? Or do I have to modify the tree's controller manually as this case is not covered by the generated one?

like image 814
Achim Tromm Avatar asked Feb 17 '09 10:02

Achim Tromm


People also ask

How do you represent resources in REST?

REST uses various representations to represent a resource where Text, JSON, XML. The most popular representations of resources are XML and JSON.

What is the recommended term used to refer to multiple resources in API?

In this case, we refer to these resources as singleton resources. Collections are themselves resources as well. Collections can exist globally, at the top level of an API, but can also be contained inside a single resource. In the latter case, we refer to these collections as sub-collections.

Which URL pattern is recommended when working with one resources and a collection of resources?

URL Structure The recommended convention for URLs is to use alternate collection / resource path segments, relative to the API entry point.

Which property would you use to include references to other resources in a JSON document?

This Account resource and every other JSON resource will always have an href property, which is the fully qualified canonical URL where that resource resides. Whenever you see an href property, you know you can access that resource by executing a GET request to the resource's URL.


2 Answers

One way you can accomplish this is to override the bird_nests= method on your tree model.

def bird_nests=(attrs_array)
  attrs_array.each do |attrs|
    bird_nests.build(attrs)
  end
end

The only issue here is that you lose the default behavior of the setter, which may or may not be an issue in your app.

If you're running a more recent version of Rails you can just turn on mass assignment as described here:

http://github.com/rails/rails/commit/e0750d6a5c7f621e4ca12205137c0b135cab444a

And here:

http://ryandaigle.com/articles/2008/7/19/what-s-new-in-edge-rails-nested-models

class Tree < ActiveRecord::Base
  has_many :bird_nests, :accessible => true
end

This is the preferred option.

like image 91
Patrick Ritchie Avatar answered Oct 01 '22 03:10

Patrick Ritchie


The overriding of the bird_nests= method of the tree model is a valid solution, referring to the previous post of Patrick Richie (thanks). So no changes on the controller are necessary. Here is the code in detail which will handle the given XML requests mentioned in the example above (also handling non-array nests):

  def bird_nests=(params)
    bird_nest=params[:bird_nest] 
    if !bird_nest.nil?
      if bird_nest.class==Array
        bird_nest.each do |attrs|
          bird_nests.build(attrs)
        end
      else 
        bird_nests.build(bird_nest)
      end
    end
  end
like image 30
Achim Tromm Avatar answered Oct 01 '22 02:10

Achim Tromm