REST Adapter for DataMapper
Ruby
Pull request Compare This branch is 50 commits ahead, 11 commits behind datamapper:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
spec
tasks
.gitignore
Gemfile
LICENSE
README.rdoc
Rakefile
VERSION
dm-rest-adapter.gemspec

README.rdoc

dm-rest-adapter

A DataMapper adapter for REST Web Services

Usage

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.

Code

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"

Additional Configuration

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

Nested Resources

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…)

Exercise Caution

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)

Expected Formats for Legacy Applications

The currently expected serialization formats are:

XML

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.

JSON

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.

TODO:

* More flexible formatting conventions (custom Format implementation)
* Embedded Resources (where a whole object-tree is returned in one response)
* Request Logging?