10.1. ActiveResource

Because requesting GET /articles.xml in your blog example produces XML data, you could interact with the REST service in this manner, without any wrapper and with "low-level" requests. But just like interacting with databases via low-level queries is more time consuming and error prone than using ActiveRecord, so is working with Web Services by sending handmade HTTP requests.

The idea behind ActiveResource is then very simple. ActiveRecord simplifies the process of interacting with databases, by providing object-relational mapping between relational data and business objects. ActiveResource does the same, only mapping REST resources. Both ActiveRecord and ActiveResource rely on a series of conventions and assumptions to be able to provide a wrapper to map structures containing the data (relational tables and resources, respectively) to model objects.

A script, desktop application, or Web application, can take advantage of ActiveResource, by simply defining a model for a given remote REST resource. When this prerequisite has been fulfilled, it will be possible to use that model through an API similar to ActiveRecord's one. Unlike ActiveRecord, which operates on database records and sends queries to the database, ActiveResource operates on remote HTTP resources by sending XML requests to the remote Web Service. Any XML received in response will then be opportunely serialized, so that you can continue to work with Ruby objects, as opposed to raw data.

Under the hood, the HTTP methods GET, POST, PUT, and DELETE are used to send the request, depending on the operation intended on the resource. For example, GET will be used to retrieve resources, POST to create new resources, PUT to update them, and finally, DELETE to, not surprisingly, delete resources.

Notice my use of the word "resource" as opposed to "record." In fact, though a resource will often represent actual database records, in which case ActiveResource indirectly provides an API to perform CRUD operations on a remote database, this doesn't have to be the case. There can be resources that do not represent database data.

10.1.1. Creating ActiveResource Models

The process of creating ActiveResource models is very similar to that for ActiveRecord models. When the client consuming a REST Web Service is another Rails application, an ActiveResource model is located in appmodels. The class is conventionally named after the name of the remote resource and inherits from ActiveResource::Base.

First start the blog sample application as usual with ruby script/server, so that your Web Service will be up and running.

Please use the blog application without authentication. If you've already added authentication to your blog app, you can use the blog version provided with Chapter 6 in the code download at wrox.com.

Next, create a new Rails application that will use it:

C:projects> rails blog_consumer
C:projects> cd blog_consumer

Proceed by creating an Article model. Create an article.rb file in appmodels with the following content:

class Article < ActiveResource::Base
  self.site = "http://localhost:3000/"
end

The highlighted line sets a class variable site, so that ActiveResource knows where to find the remote REST Web Service that needs to be invoked. In this particular case, you are running the blog application on localhost, so the model will be mapped to http://localhost:3000/articles, but the assigned value could be any valid URI.

Another useful class variable is timeout (for example, self.timeout = 4), which is used to express the timeout in seconds. When a request times out, the error ActiveResource::TimeoutError is raised. You can rescue it and decide how to proceed after each timeout. Generally speaking it's recommended that you keep the timeout value to a rather small number, to respect the Fail-fast principle (you can read more about it online at http://en.wikipedia.org/wiki/Fail-fast). The default value for timeout depends on the Ruby implementation that is running Rails, but it's usually 60 seconds.

Avoiding Duplicates

In this scenario you have an Article ActiveRecord model on a server, and an Article ActiveResource model on the client, so there is no conflict whatsoever. However, what happens if your Rails application that acts as a client for the Web Service exposed by another Rails application already has an existing article.rb file for ActiveRecord in appmodels?

When this happens, you can avoid duplication by simply setting the element_name for the ActiveResource model:

class ArticleResource < ActiveResource::Base
  self.site = "http://localhost:3000/"
  self.element_name = "article"
end


10.1.2. CRUD Operations

To experiment with this new model, you will work from the console. Open it by running:

C:projectslog_consumer> ruby script/console

Once inside the console, you'll proceed by performing a few CRUD operations. The whole process should appear familiar, because it's analogous to what you did with ActiveRecord, but don't forget that you are not interrogating a database but a remote REST service.

Again, for simplicity, I'm assuming you are using the blog application without authentication enabled, as per the code attached with Chapter 6.

If your Web Service has HTTP authentication in place, you can assign the credentials in the URL (for example, self.site = http://myuser:[email protected]/).

Alternatively, you can also use the class methods user= and password=, which are the only option when the username is an email address and as such cannot be included in the URL:

self.site = "https://mydomain.com/"
self.user = "[email protected]"
self.password = "secret"

It is recommended that in production you use SSL to encrypt the communication between the Web Service consumer and the server, so that the password will not be sent in clear text (for example, self.site = https://myuser:[email protected]/).

10.1.2.1. Read

Let's try to retrieve a resource by its id:

>> article = Article.find(1)
=> #<Article:0x61322c4 @prefix_options={}, @attributes={"updated_at"=>Thu Jul 17
03:18:28 UTC 2008, "body"=>"Hi from the
 body of an article. :)", "title"=>"Hello, Rails!", "published"=>false, "id"=>1,
"published_at"=>Fri Jul 11 09:24:00 UTC
 2008, "created_at"=>Fri Jul 11 09:32:41 UTC 2008}>

Assuming that the resource whose XML element id is 1 exists, ActiveResource will retrieve the XML document and instantiate an Article object for you. All the entries defined within @attributes will be available as attributes of the object:

>> article.published
=> false
>> article.title
=> "Hello, Rails!"

If an XML element contains other XML elements, this will be mapped as its own object (for example, article.complex_object.sub_element).

If the record doesn't exist, an ActiveResource::ResourceNotFound exception is raised:

>> Article.find(100)
ActiveResource::ResourceNotFound: Failed with 404 Not Found
        from d:/Ruby/lib/ruby/gems/1.8/gems/activeresource-2.2.2/lib/active_resource/
connection.rb:170:in 'handle_respon
se'
        from d:/Ruby/lib/ruby/gems/1.8/gems/activeresource-
2.2.2/lib/active_resource/connection.rb:151:in 'request'
        from d:/Ruby/lib/ruby/gems/1.8/gems/activeresource-
2.2.2/lib/active_resource/connection.rb:116:in 'get'
        from d:/Ruby/lib/ruby/gems/1.8/gems/activeresource-
2.2.2/lib/active_resource/base.rb:593:in 'find_single'
        from d:/Ruby/lib/ruby/gems/1.8/gems/activeresource-
2.2.2/lib/active_resource/base.rb:521:in 'find'
        from (irb):31

Note that although you can retrieve a collection of resources through find(:all), the alias methods all (as well as first and last) are not available for ActiveResource models.

Whenever the find method is invoked on your Article model, a GET request (for the XML format) is sent. For example, Article.find(1) generates a request on the server running the REST Web Service with the following parameters:

{ "format" => "xml", "action" => "show", "id" => "1", "controller" =>
"articles" }

Using JSON Rather Than XML

The default ActiveResource format is XML. If you'd like to set JSON as the format for your ARes models, add self.format = :json. Doing so changes the value of the parameter format from "xml" to "json." The Web Service will need to be able to deal with JSON requests or you'll get an ActiveResource::ClientError with a 406 Not Acceptable status code.


10.1.2.2. Create

Creating new resource instances is just as easy thanks to methods like save and create. These are the ActiveResource equivalents of the familiar methods you used for ActiveRecord.

Consider this example, which also employs the new? method to verify that an article resource hasn't been saved yet:

>> article = Article.new(:title => "Hi from ActiveResource", :body => "...some
 text...")
=> #<Article:0x611d018 @prefix_options={}, @attributes={"body"=>"...some text...",
"title"=>"Hi from ActiveResource"}>
>> article.new?
=> true
>> article.save
=> true
>> article.id
=> 6

Instead of new and save, you can also use the create method to reduce the creation process to a single method.

When article.save is executed, a POST request for http://localhost:3000/articles.xml is sent by the client. Notice that the resource id doesn't exist until you invoke article.save. When a new resource request is sent, the Web Service will try to create a new record (in our case) and if successful will return a 201 HTTP status code (Created), with a Location header like http://localhost:3000/articles/6. This is a RESTful URI that indicates the location of the resource you just created. Out of this URI the id is parsed and assigned to the receiver (for example, the object referenced by article).

Please note that server-side validations apply. If a validation fails, article.save will fail and return false. You can check the validity of a resource through the method valid? and read a list of errors by calling errors.full_messages on the object you tried to save.

You can define validations in your ActiveResource models to perform validations client side, in a similar manner to how ActiveRecord validations work server side. Check the documentation of ActiveResource::Validations for further information.

10.1.2.3. Update

To update a resource, you can modify its attributes and then invoke the save method:

>> article = Article.find(1)
=> #<Article:0x60d3f80 @prefix_options={}, @attributes={"updated_at"=>Thu Jul 17

03:18:28 UTC 2008, "body"=>"Hi from the
 body of an article. :)", "title"=>"Hello, Rails!", "published"=>false, "id"=>1,
 "published_at"=>Fri Jul 11 09:24:00 UTC
 2008, "created_at"=>Fri Jul 11 09:32:41 UTC 2008}>
>> article.title = "Hello!"
=> "Hello!"
>> article.save
=> true

Updating a resource sends a PUT request, in this case, for http://localhost:3000/articles/1.xml. Unlike creating a resource, successfully updating a resource returns an empty response with a 204 HTTP status code (No Content).

10.1.2.4. Delete

Deleting remote resources can be accomplished through the instance method destroy, or with the class method delete (by passing an id). These send a DELETE request for the resource location (including .xml which specifies the format) and returns an empty response with HTTP status code of 200 (OK).

The existence of a resource can be verified with the method exists?.

NOTE

Don't let the uncanny similarity between the basic CRUD methods defined by ActiveRecord and ActiveResource fool you. You won't be able to use many methods defined by ActiveRecord, including dynamic finders like find_by_title, with ActiveResource models, unless you define them yourself.

10.1.3. Beyond CRUD

The four basic CRUD operations and a few extra methods to verify the status of a model instance will fall short when trying to consume certain APIs. For this reason, ActiveResource enables you to use your own custom REST methods, through get, post, put, and delete.

For example, consider the following:

>> Article.get(:unpublished)
=> [{"updated_at"=>Sun Nov 30 19:25:43 UTC 2008, "title"=>"Hello!", "body"=>"Hi
from the body of an article. :)", "published"=>false, "id"=>1, "published_at"=>Fri
Jul 11 09:24:00 UTC 2008, "created_at"=>Fri Jul 11 09:32:41 UTC 2008}]

This translates to GET /articles/unpublished.xml request. Because you defined an unpublished REST method, you obtain the expected result.

The same is true for the other verbs as well. For example, the following by default will translate in a POST request for /books/new/add.xml, under the assumption that the Web Service has defined a custom REST method add:

Book.new(:title => "Advanced Calculus").post(:add)

Any extra argument passed to any of these methods will be interpreted as a parameter. For instance:

Book.find(1).put(:deposit, :overdue => true)

will issue a PUT /books/1/deposit.xml?overdue=true request.

Of course, you can go further and define add or deposit class methods within the ARes model, by taking advantage of post and put, respectively.

Note that the last two book examples are generic; I'm not referencing other book examples I made in Chapter 7.

10.1.4. Nested Resources

I mentioned that despite the similarity between the ActiveRecord and ActiveResource APIs, only selected methods are available for the latter. This also means that, while tempting, reaching for the methods that provide access to a collection of objects associated with a given object like you'd do in ActiveRecord is not going to work in ActiveResource.

ActiveResource, in fact, knows nothing about databases, tables, and records. The fact that a one-to-many relationship between the articles table and the comments table exists is absolutely irrelevant to ActiveResource. What ActiveResource minds is resources and their relationships. Because you specified that comments are nested within articles in config outes.rb, you are now able to work with nested resources from an ActiveResource client as well.

The comment.rb model in appmodels needs to look like this:

class Comment < ActiveResource::Base
  self.site = "http://localhost:3000/articles/:article_id"
end

Notice how you need to provide the suffix articles/:article_id because comments are nested within articles, so an article_id is always necessary in order to access comments.

With this model definition, you can then access a list of comments for a given article as follows:

Comment.find(:all, :params => {:article_id => 1})

You can also retrieve a particular comment resource, modify its attributes, and then request an update:

c = Comment.find(:last, :params => { :article_id => 1 })
c.name = "A different commenter"
c.save #=> true

Specifying the article_id through params is fundamental, otherwise the URI generated for a request like Comment.find(:last) will be the malformed /articles//comments.xml.

To help you determine the URI generated you can use the helper methods element_path and collection_path. Take a look at the following two examples of their usage:

>> Comment.element_path(3, { :article_id => 1 })
=> "/articles/1/comments/3.xml"
>> Comment.collection_path(:article_id => 2)
=> "/articles/2/comments.xml"

element_path is therefore used for retrieving the path for a single resource, whereas collection_path is for retrieving the path to a list of resource objects, like the list of comments for a particular article.

Please notice that element_path and collection_path do not accept a :params key.

10.1.5. Consuming and Publishing REST Web Services from .NET

REST is a relatively new technology, but it's gaining momentum with an increasing number of websites exposing RESTful APIs. Truth to be told, not all of the APIs called "RESTful" actually are, but those sites that genuinely provide a REST Web Service can be "consumed" client side by ActiveResource, whether the Web Service has been implemented in Ruby on Rails or in a different language/framework.

This is great news when you need to use these services from within Ruby. Whether you are writing a Rails application or a regular Ruby program, you can use ActiveResource to interact with the Web Service.

You may, however, find yourself in a different position. Perhaps you just created a nice RESTful Rails application and would now like to be able to consume the REST Web Service it exposes from your existing .NET infrastructure.

This is not such a farfetched scenario, given that it's one of the easiest ways to start introducing Rails into a company that is mainly .NET-based. The other way is consuming through ARes a REST Web Service implemented in .NET.

Because your client will be written in .NET code, you cannot use ActiveResource (short of tinkering with IronRuby), so face the challenge of consuming a REST Web Service.

The REST architecture is based on the HTTP protocol and there is very little voodoo about it. It is so straightforward that you could decide to formulate requests and parse the XML content retrieved on your own. You could, for example, use XmlDocument or the XElement class defined by System.Linq.XML for your GET requests, and use it along with the HttpWebRequest class when you need to specify a different HTTP verb.

If the Web Service returns JSON, rather than XML, you can use the class JavaScriptSerializer.

Finally, should you require to publish RESTful Web Services using .NET, you can take advantage of the Windows Communication Foundation (WCF) REST Starter Kit, which includes samples, code, templates, screencasts, and a wealth of information about working with REST in .NET. You can find it on MSDN at http://msdn.microsoft.com/wcf/rest.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.87.161