Recipe 14.6. Extending Active Record with acts_as


Problem

You may have used the Active Record acts extensions that ship with Rails, such as acts_as_list, or those added by plug-ins, such as acts_as_versioned. But you really need your own acts functionality. For example, you would like each object of a Word model to have a method called define that returns that word's definition. You want to create acts_as_dictionary.

Solution

To create a custom plug-in, use the plug-in generator. The generator creates a number of files and directories that form the base for a distributable plug-in. Note that not all of these files have to be included.

$ ./script/generate plugin acts_as_dictionary create vendor/plugins/acts_as_dictionary/lib create vendor/plugins/acts_as_dictionary/tasks create vendor/plugins/acts_as_dictionary/test create vendor/plugins/acts_as_dictionary/README create vendor/plugins/acts_as_dictionary/Rakefile create vendor/plugins/acts_as_dictionary/init.rb create vendor/plugins/acts_as_dictionary/install.rb create vendor/plugins/acts_as_dictionary/lib/acts_as_dictionary.rb create vendor/plugins/acts_as_dictionary/tasks/acts_as_dictionary_tasks.rake create vendor/plugins/acts_as_dictionary/test/acts_as_dictionary_test.rb

Now, add the following to init.rb to load lib/acts_as_dictionary.rb when you restart your application:

vendor/plugins/acts_as_dictionary/init.rb:

require 'acts_as_dictionary' ActiveRecord::Base.send(:include, ActiveRecord::Acts::Dictionary)

To make the acts_as_dictionary method add methods to a model and its instance objects, you must open the module definitions of Rails and add your own method definitions. Add a define instance method and a dictlist class method to all models that are to act as dictionaries by adding the following module definitions to acts_as_dictionary.rb:

vendor/plugins/acts_as_dictionary/lib/acts_as_dictionary.rb:

require 'active_record' require 'rexml/document' require 'net/http' require 'uri' module Cookbook   module Acts     module Dictionary       def self.included(mod)         mod.extend(ClassMethods)       end       module ClassMethods         def acts_as_dictionary           class_eval do             extend Cookbook::Acts::Dictionary::SingletonMethods           end           include Cookbook::Acts::Dictionary::InstanceMethods         end       end       module SingletonMethods         def dictlist           base = "http://services.aonaware.com"           url = "#{base}/DictService/DictService.asmx/DictionaryList?"           begin             dict_xml = Net::HTTP.get URI.parse(url)             doc = REXML::Document.new(dict_xml)             dictionaries = []             hash = {}             doc.elements.each("//Dictionary/*") do |elem|               if elem.name == "Id"                  if !hash.empty?                   dictionaries << hash                    hash = {}                 end                 hash[:id] = elem.text               else                 hash[:name] = elem.text               end             end             dictionaries           rescue             "error"           end         end       end       module InstanceMethods         def define(dict='foldoc')           base = "http://services.aonaware.com"           url = "#{base}/DictService/DictService.asmx/DefineInDict"           url << "?dictId=#{dict}&word=#{self.name}"           begin             dict_xml = Net::HTTP.get URI.parse(url)             REXML::XPath.first(REXML::Document.new(dict_xml),                '//Definition/WordDefinition').text.gsub(/(\n|\s+)/,' ')           rescue             "no definition found"           end         end       end     end   end end ActiveRecord::Base.class_eval do   include Cookbook::Acts::Dictionary end

To demonstrate that the plug-in works, create a words table with a migration that simply contains a name column. Next, generate the Word model for this table:

db/migrate/001_create_words.rb:

class CreateWords < ActiveRecord::Migration   def self.up     create_table :words do |t|       t.column :name, :string      end   end   def self.down     drop_table :words   end end

Now add your custom method to the Word class by calling acts_as_dictionary in the model class definition just as you would with the built-in acts:

app/models/word.rb:

class Word < ActiveRecord::Base   acts_as_dictionary end

Calling Word.dictlist returns an array of hashes containing all of the service's available dictionaries of the web service DictService (http://services.aonaware.com/DictService/DictService.asmx). Word objects can be defined by calling their define method, which takes a dictionary ID (from the results of dictlist) as an optional parameter.

Discussion

There's a lot of idiomatic Ruby happening in acts_as_dictionary.rb. The basic premise behind extending Ruby in this way is the concept of open classes: the fact that a Ruby class can be extended at any time.

The module starts out by including active_record and several other libraries used for HTTP requests and XML manipulation. Three module definitions are then opened to set up a namespace:

module Cookbook   module Acts     module Dictionary

Next, the included method is defined. This method is a callback method that gets invoked whenever the receiver is included in another module (or class).

def self.included(mod)   mod.extend(ClassMethods) end

In this case, included extends ActiveRecord::Base to include the ClassMethods module. In turn, the call to class_eval at the end of the file makes sure that ActiveRecord::Base includes Cookbook::Acts::Dictionary:

ActiveRecord::Base.class_eval do   include Cookbook::Acts::Dictionary end

The ClassMethods module defines the acts_as_dictionary method that you'll use to attach the dictionary behavior to the models of your Rails application:

module ClassMethods   def acts_as_dictionary     class_eval do       extend Cookbook::Acts::Dictionary::SingletonMethods     end     include Cookbook::Acts::Dictionary::InstanceMethods   end end

The first part of the acts_as_dictionary method definition evaluates a call to extend. This makes all of the methods of the Cookbook::Acts::Dictionary::SingletonMethods module class methods of the receiver of acts_as_dictionary. The next line simply includes the methods in Cookbook::Acts::Dictionary::InstanceMethods as instance methods of the receiving model. The end result is that a model that acts as dictionary gets a class method, dictlist and an instance method, define. dictlist by polling a dictionary web service and calling its DictionaryList. This action returns a list of available dictionaries. The define method take the ID of a dictionary (as returned from dictlist) and returns the definition of the word, if found.

Here's the result of calling the dictlist method of the Word class, which returns an array of hashes, and printing the hashes out in somewhat nicer format:

>> Word.dictlist.each {|d| puts "ID: " + d[:id], "NAME: " + d[:name], "" } ID: gcide NAME: The Collaborative International Dictionary of English v.0.48 ID: wn NAME: WordNet (r) 2.0 ID: moby-thes NAME: Moby Thesaurus II by Grady Ward, 1.0 ID: elements NAME: Elements database 20001107 ID: vera NAME: Virtual Entity of Relevant Acronyms (Version 1.9, June 2002) ID: jargon NAME: Jargon File (4.3.1, 29 Jun 2001) ID: foldoc NAME: The Free On-line Dictionary of Computing (27 SEP 03)

To look up a word in the dictionary, create a Word object with a :name of "Berkelium", an element from the periodic table. To display the definition, call define on the Word object and explicitly specify the 'elements' dictionary:

>> w = Word.create(:name => 'Berkelium') => #<Word:0x239ce18 @errors=#<ActiveRecord::Errors:0x239b784 @errors={},  @base=#<Word:0x239ce18 ...>>, @attributes={"name"=>"Berkelium", "id"=>11},  @new_record=false> >> w.define('elements') => "berkelium Symbol: Bk Atomic number: 97 Atomic weight: (247) Radioactive  metallic transuranic element. Belongs to actinoid series. Eight known isotopes,  the most common Bk-247, has a half-life of 1.4*10^3 years. First produced by  Glenn T. Seaborg and associates in 1949 by bombarding americium-241 with alpha  particles."

From the Rails console, you can inspect the class and instance methods of the module:

>> ActiveRecord::Acts::Dictionary::InstanceMethods::\                                         ClassMethods.public_instance_methods => ["dictlist"] >> ActiveRecord::Acts::Dictionary::InstanceMethods.public_instance_methods => ["define"]

See Also

  • The acts_as_treemap plug-in home page, http://blog.tupleshop.com/2006/7/27/treemap-on-rails

  • Section 14.10"




Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net