ProblemYou 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. SolutionTo 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. DiscussionThere'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
|