A DataMapper adapter for REST Web Services
DataMapper REST Adapter provides a way to fetch and update resources from the web using the same DataMapper Resource classes you use when working with a database, or other type of repository. The only difference is that you will use the REST Adapter on your REST Resources.
The provided formats are XML and JSON.
For example, using the default configuration, a resource at “/posts/1.xml” can be fetched simply by using:
post = Post.get(1) # => GET /posts/1.xml post.title # => "A RESTful resource" post.title = "A new title" post.save # => PUT /posts/1.xml
If the resource does not exist, nil is returned, or if you used #get! DataMapper::ObjectNotFoundError is raised.
The following is an example of a Post model using XML, where the host settings simply point to localhost. In addition I have included a basic auth login which will be used if your REST service requires authentication:
DataMapper.setup(:default, { :adapter => 'rest', :format => 'xml', :scheme => 'https', :host => 'localhost', :port => 4000, :login => 'user', :password => 'verys3crit' })
Note that :scheme and :port are optional.
class Post include DataMapper::Resource property :id, Serial property :title, String property :body, Text end
If you notice this looks exactly like a normal DataMapper model. Every property you define will map itself with the XML returned or sent from/to the service.
Now for some code examples. DataMapper REST Adapter uses the same methods as DataMapper, including during creation. You can still add validations and all the usual DataMapper niceties to your models.
Post.first # => returns the object from the resource Post.get(1) # => returns the object from the resource p = Post.new( :title => "My awesome blog post", :body => "I really have nothing to say..." ) p.save # => saves the resource on the remote p.comments # => fetches post comments from the remote p.comments.get(7) # => fetches comment ID 7 from the remote Post.all(:category => "datamapper", :limit => 2) # => fetches the first two posts whose 'category' is "datamapper"
You can specify a non-standard extension (i.e. something other than “.json” or “.xml”) by passing the :extension option when setting up the connection.
DataMapper.setup(:default, { :adapter => 'rest', :format => 'xml', :extension => 'anything', ... })
If your URLs have no extension on them, just pass an empty string, or nil.
If your Resources map to a different path than the name of the model itself, you can specify the path fragment with #storage_names as with other adapters:
class Post include DataMapper::Resource storage_names[:default] = 'blog-posts' ... end Post.get(1) #=> GET /blog-posts/1.json
Likewise, if the fields in the XML/JSON are not the same as the attributes in your model, you can specify them with the :field option on your properties:
class Post include DataMapper::Resource property :id, Serial, :field => 'post_id' end
DataMapper REST Adapter has support for nested URL structures, where child relationships may be found nested under their parents in the URL. Take for example, fetching the comments from a Post:
/posts/1/comments.json
We can set this up as follows:
class Post include DataMapper::Resource property :id, Serial property :title, String property :body, Text has n, :comments, :nested => true end class Comment include DataMapper::Resource property :id, Serial property :body, String belongs_to :post end post = Post.get!(1) # => /posts/1.json comments = post.comments # => /posts/1/comments.json
DataMapper REST Adapter adds the :nested option to associations. Setting this to true will fetch the resource(s) at a subpath of the URL from which the parent resource was loaded. You may nest your resources more than one level deep.
Current limitation: Nesting currently only works for ‘has n’ and ‘has 1’ relationships. For example, you can’t map back to a parent as a nested resource (I’m not sure what the semantics of this would be if you then fetched the child from that nested resource again, and so on…)
RESTful web services, while convenient, are not optimized for query performance. For example,
Post.first
by default, actually fetches all Posts and then returns the first of the collection. There is not much that can be done about this and it just comes down to using the right tool for the job.
If you have control of the web service itself, it is possible to optimize for some of these scenarios on the service-side. Though DataMapper REST Adapter will filter responses that are returned from the service, it will provide hints via the query string. For example:
Post.first
actually queries for
/posts.xml?offset=0&limit=1
Similarly,
Post.all(:category => "datamapper")
will query
/posts.xml?category=datamapper
It is not a requirement that the remote service pays attention to these hints, but doing so will reduce the amount of work done to fetch resources. There are some obvious limitations as to what can be hinted at in the query string, such as, for example:
Post.all(:created_at.gte => 3.days.ago)
The currently expected serialization formats are:
For a collection of Posts, the XML must adhere to a format like this:
<posts> <post> <id type="integer">1</id> <created_at type="datetime">2011-06-16T02:38:42-07:00</created_at> <title>An awesome blog post</title> </post> <post> <id type="integer">2</id> .... </posts>
For a single Post:
<post> <id type="integer">1</id> <created_at type="datetime">2011-06-16T02:38:42-07:00</created_at> <title>An awesome blog post</title> </post>
The type attributes are ignored by DataMapper Rest Adapter and typecasting is done by the properties on the model as usual, so the values are somewhat flexible.
For a collection of Posts, the JSON must adhere to a format like this:
[ { "id" : 1, "created_at" : "2011-06-16T02:38:42-07:00", "title" : "An awesome blog post" }, { "id" : 2, ... } ]
For a single Post:
{ "id" : 1, "created_at" : "2011-06-16T02:38:42-07:00", "title" : "An awesome blog post" }
As with the XML format, the values themselves will be type-cast by the properties on your model.
* More flexible formatting conventions (custom Format implementation) * Embedded Resources (where a whole object-tree is returned in one response) * Request Logging?