ActiveResource::HttpMock doesn’t mock hard enough

November 19th, 2009

I’ve got a family of (ARes) resources that are available through an external web service.  These resources are used pervasively throughout my site -they appear on about 75% of my site’s pages.  When testing, of course I don’t want to hit the web service and endure the vagaries of performance and availability.  The nice people developing ActiveResource seem to have accommodated this desire by providing the HttpMock class.

But…  HttpMock is very exacting in its demands for matching requests -it’s all or nothing.  That means that if I make fifty different requests, all identical save for a tiny change in the URL params, I need to make fifty different mocks.  Ouch.

So I decided to mock the mocker.  No, really.  Using mocha, I mock the HttpMock::Request#== method to always return true.  Now, regardless of the request path, HttpMock always sees a match and returns my mock data.

The denouement, using the Rails docs as a starting point:

def setup
  @matz  = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
  ActiveResource::HttpMock.respond_to do |mock|
    mock.post   "/people.xml",   {}, @matz, 201, "Location" => "/people/1.xml"
    mock.get    "/people/1.xml", {}, @matz
    mock.put    "/people/1.xml", {}, nil, 204
    mock.delete "/people/1.xml", {}, nil, 200
    req_res = mock.get "/", {}, "Constant Data Goes Here", 200
    req_res.last.first.stubs(:==).returns(true)
 end
end

git bisect… taste great when served with Edge Rails.

November 2nd, 2009

So I’ve got a couple of projects that I like to claim run on “Edge Rails”.  But the reality is that I let them slip for a month or two before playing catch up.  Today I decided to update an application that had somehow gotten behind 2-3-stable by hundreds of commits.  Being bold, I jumped straight to HEAD and was met with a shower of sparks running rake.  Where to start?

In a word or two: git bisect

I started the bisection run and told git that head was bad and my current commit was good.  Note: I use a git submodule to track Rails…

[…vendor/rails]# git bisect start

[…vendor/rails]# git bisect good

[…vendor/rails]# git bisect bad 2-3-stable

(and in another terminal window)

[myapp] # rake

Then it was just a matter of marking each auto-selected commit as good or bad with git bisect good or git bisect bad.  Worked great.  Next time, I’m going to automate the whole thing with git bisect run.

Borked gems and when cache is not cache

October 9th, 2009

Somehow, someway my gem installation became subtly corrupt.  Specifically, many of my gems are not available in the cache directory.  That’s not a very intuitive erroneous situation, but there are gem commands that clearly expect a gem file in the cache directory -and they can’t recover the situation if the file is missing.  Doesn’t sound like my definition of a cache, but it’s time to move on.

Symptom:
1. whiny errors while attempting to unpack a gem like:

[cch1@bimota:~/Documents/Development/t]$ gem unpack exifr
ERROR:  While executing gem … (Gem::Exception)
Cannot load gem at [/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/cache/exifr-0.10.7.gem] in /Users/cch1/Documents/Development/t

or

[cch1@bimota:~/Documents/Development/t]$ gem pristine exifr
Restoring gem(s) to pristine condition…
ERROR:  Cached gem for exifr-0.10.7 not found, use `gem install` to restore

2. Indeed, no .gem file exists in the referenced cache directory.

Note that in OSX Leopard, gems are typically installed in several locations:

[cch1@bimota:~/Documents/Development/t]$ gem env paths
/Users/cch1/.gem/ruby/1.8:/Library/Ruby/Gems/1.8:/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8

My theory is that earlier versions of gem (such as the one I got with my distribution of Leopard in early 2008) had problems installing and updating gems other than those originally packaged with the system.  Using the gems by requiring the library file typically worked fine.  But by not building the cache file (or perhaps building it in the wrong one of the multiple gem directories), problems crept up months later trying to unpack the gem.

Sadly, the only solution I can find is to re-install the gem:

[cch1@bimota:~/Documents/Development/t]$ gem list exifr

*** LOCAL GEMS ***

exifr (0.10.7)
[cch1@bimota:~/Documents/Development/t]$ sudo gem install exifr –version ‘=0.10.7′
Password:
Successfully installed exifr-0.10.7
1 gem installed
Installing ri documentation for exifr-0.10.7…
Installing RDoc documentation for exifr-0.10.7…

Now the commands that depend on the “cached” version will work:

[cch1@bimota:~/Documents/Development/t]$ gem unpack exifr
Unpacked gem: ‘/Users/cch1/Documents/Development/t/exifr-0.10.7′

Aptana RadRails and the test_helper.rb LoadError

July 17th, 2008

Since moving to more recent versions of Rails, I’ve noticed that the auto-generated Rails tests assume that the myapp/test directory is on the load path.  And the bundled Rake testing tasks do indeed take care of that requirement.  But the Aptana RadRails plugins for Eclipse don’t.  The result is that whenever you run a test, either with the test buttons or the Run As | Test::UnitTest context menu, you get something like the following:

./test/unit/user_test.rb:1:in `require': no such file to load -- test_helper (LoadError)

There is an open issue on the Aptana issue tracker, but it doesn’t seem to be resolved yet.  Some people have suggested prefixing your requires with the test directory when requiring the test_helper file at the start of your test files -but I don’t think that’s a good solution, especially if you’re working with multiple developers.  Frustrated by not getting my daily green bar fix, I found this work-around:

  1. From the Eclipse Preferences option, choose Ruby | Installed Interpreters
  2. Select your interpreter (I use the Standard VM default interpreter named usr) and choose ‘Edit’. 
  3. Add -Itest to the Default VM Arguments option.  Don’t forget the leading dash!
  4. Click ‘OK’. 

You should now be able to run your tests without having to edit each one to include the test directory.  The downside is that you now have an ‘extra’ directory in your load path which will give you a (tiny) performance hit to all your ruby ops -not just tests.  Conceivably this could also introduce an incompatibility if you have some conflicting stuff in the test directory -extremely unlikely if you only have test_helper.rb in there.

Tested against Edge Rails (d37e6413366c9a3fafa02c4298a2946dc8327a42), Aptana RadRails 1.0.3.200807071913NGT, Ruby 1.8.6 patchlevel 114 running under Darwin 9.4.0

Eclipse Git plugin installation

June 27th, 2008

Like a lot of Rails developers who have been spoiled by the excellent Eclipse plugins for Subversion, I was disappointed by the lack of equivalent plugins for Git.  In fact, the only one that I found was Egit.  Unfortunately, its documentation is a bit weak.  And for someone who is not a Java guru, Eclipse guru and/or a mind reader, the installation instructions are really weak.  But I was desperate.  And once I dove in, it turned out to not be that difficult.

Assumptions (most directly from Egit’s sparse installation guide):

  • You have git installed and working correctly
    • I have version 1.5.5.4
    • You can see your version from the command line with git –version
  • You have Eclipse installed and working correctly
    • I use Europa, version 3.3.2
    • From within Eclipse you can see your version with Eclipse | About Eclipse SDK (OSX) or Help | About (Windows).
  • Java Runtime Environment (JRE) version 6 or 1.5.0_11 or later available
    • My MacBook Pro came pre-installed with version 1.5.0_13
    • You can see which version you have from the command line with java -showversion -it’s at the top.  But that only shows one JRE version, and…
    • Because it’s possible to have multiple JREs installed, you should also check your Eclipse preferences to make sure you have an appropriate version available from within Eclipse.  You can see the JRE Interpreters with Java > Installed JREs.  The checked JRE is the default, but the Egit plugin only cares that a suitable version is available.

The installation steps are pretty straightforward:

  1. Clone (or otherwise acquire) the Egit source and put it somewhere.  I used this command: git clone git://repo.or.cz/egit.git
  2. Inside the Egit source are several Eclipse projects, each in a directory whose name starts with “org.spearce.”  You need to add these projects into your Eclipse environment -but only temporarily.  You can add them from the File | Import menu option in Eclipse.  Then select General > Existing Projects into Workspace.  From the dialog box browse to the directory holding the Egit source that you acquired in step one above.  You should then see the eight Eclipse projects “org.spearce.<something>.” Make sure they are all selected and choose “Finish.”
  3. Each project should build automatically, but in case you have somehow disabled automatic building, manually build the Egit projects from the Project menu.
  4. Next, you’re going to create an Eclipse plugin from the built projects:
    1. From the Project Explorer (or Ruby Explorer) view, select (highlight) all eight Egit projects
    2. Choose File | Export from the Eclipse menu
    3. Choose Plug-in Development > Deployable plug-ins and fragments.  If you don’t see the Plug-In Development section,you need to enable the Development capability in the Eclipse Preferences (General > Capabilities).
    4. Select “Next” and in the resulting “Export” dialog, make sure you have selected all five available Plug-ins and fragments
    5. Browse to the root directory of your Eclipse installation.  On OSX, the directory is typically Applications/Eclipse.  Under Windows, I would expect something like C:\Program Files\Eclipse or C:\Eclipse.
    6. Select “Finish”
  5. Now that you have added the Egit plugin, you need to restart Eclipse to enable it.

You’re done.

Add Git features to an existing project from the “Team” menu.

Issues:

  1. Where are all the pretty decoration icons for git status?  All I have are nasty ‘>’ characters.

Ruby, Rails and MySQL with Leopard 10.5.2 and XCode 3.0

March 19th, 2008

I’m pretty new to Macintosh development but I’ve been working in Ruby for a couple of years under Win32 and Linux. I was excited by the concept of Ruby and Rails being supported “out-of-the-box” on Leopard with the installation of XCode 3.0. But it didn’t take long for the luster to wear off.I first knew things were not going to be easy when I tried to install MySQL. Since I have a recent MacBook Pro with a Core 2 Duo processor, I went for the x86_64 disk image package (ominous background music starts now). It installed without too many difficulties (but not painless w.r.t the system preference pane). I then went to install the C-based ruby mysql bindings gem:

bimota:lib cch1$ sudo gem install mysql
Building native extensions.  This could take a while...
ERROR:  Error installing mysql:	ERROR: Failed to build gem native extension.
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb install mysql
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lm... yes
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lz... yes
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lsocket... no
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lnsl... no
checking for mysql_query() in -lmysqlclient... no
Gem files will remain installed in /Library/Ruby/Gems/1.8/gems/mysql-2.7 for inspection.
Results logged to /Library/Ruby/Gems/1.8/gems/mysql-2.7/gem_make.out

…and thus began the disenchantment.The failure above is caused by the gem command attempting to compile the native MySQL extensions but not being able to find the necessary library and header files. This can be caused by several more fundamental problems:

  1. The path used by default to find the MySQL library files (/usr/local/lib/mysql) does not match the default MySQL installation directory (/usr/local/mysql) on Leopard. This could be remedied by passing command line options to the gem command which would ultimately passed to the configurator (extconf.rb).
  2. The default gem installation tries to build a fat binary with code for four architectures (ppc, ppc64, i386 and i86_64). But, assuming you installed the x86_64 MySQL package, the library file /usr/local/mysql/lib/libmysqlclient.dylib only supports i86_64. This can be remedied either by setting the ARCHFLAGS environment variable before starting the gem command, or by helping the configurator learn the architecture with another command line option.

Both of the above problems can apparently be solved nicely with one command line option that helps the configurator learn the appropriate build options directly from the MySQL installation:

bimota:mysql cch1$ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

But you would be foolish to believe that it actually works. Run the same command with the verbose option enabled, and you can see that there is trouble on the horizon despite the apparent success:

bimota:mysql cch1$ sudo gem install -V mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Installing gem mysql-2.7/Library/Ruby/Gems/1.8/gems/mysql-2.7/COPYING/Library/Ruby/Gems/1.8/gems/mysql-2.7/COPYING.ja/Library/Ruby/Gems/1.8/gems/mysql-2.7/README.html/Library/Ruby/Gems/1.8/gems/mysql-2.7/README_ja.html/Library/Ruby/Gems/1.8/gems/mysql-2.7/extconf.rb/Library/Ruby/Gems/1.8/gems/mysql-2.7/mysql.c.in/Library/Ruby/Gems/1.8/gems/mysql-2.7/test.rb/Library/Ruby/Gems/1.8/gems/mysql-2.7/tommy.css/Library/Ruby/Gems/1.8/gems/mysql-2.7/mysql.gemspec
Building native extensions.  This could take a while...
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb install -V mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
checking for mysql_ssl_set()... no
checking for mysql.h... yes
creating Makefile
makegcc -I. -I. -I/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0 -I. -DHAVE_MYSQL_H  -I/usr/local/mysql/include -Os -arch x86_64 -fno-common -fno-common -arch ppc -arch i386 -Os -pipe -fno-common  -c mysql.ccc -arch ppc -arch i386 -pipe -bundle -undefined dynamic_lookup -o mysql.bundle mysql.o -L"." -L"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib" -L. -arch ppc -arch i386    -lruby -L/usr/local/mysql/lib -lmysqlclient -lz -lm  -lpthread -ldl -lm
ld: warning in /usr/local/mysql/lib/libmysqlclient.dylib, file is not of required architecture
ld: warning in /usr/local/mysql/lib/libmysqlclient.dylib, file is not of required architecture
make install/usr/bin/install -c -m 0755 mysql.bundle /Library/Ruby/Gems/1.8/gems/mysql-2.7/libSuccessfully installed mysql-2.71 gem installed

Indeed, when you go to use (not just ‘require’) the gem, you are likely to see this nasty error:

bimota:mmweb cch1$ rake db:version(in /Users/cch1/Documents/Development/mmweb)dyld: lazy symbol binding failed: Symbol not found: _mysql_init  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle  Expected in: dynamic lookup
dyld: Symbol not found: _mysql_init  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle  Expected in: dynamic lookup
Trace/BPT trap

I’m still have no idea what causes this specific failure, but I suspect it’s related to the compiler options suggested by mysql_config being ignored/munged by the configurator. The gcc command above tries to build for three different architectures (ppc, i386, x86_64) despite mysql_config’s “recommendation” of just x86_64:

bimota:mysql cch1$ /usr/local/mysql/bin/mysql_config
Usage: /usr/local/mysql/bin/mysql_config [OPTIONS]
Options:
        --cflags         [-I/usr/local/mysql/include -Os -arch x86_64 -fno-common]
        --include        [-I/usr/local/mysql/include]
        --libs           [-L/usr/local/mysql/lib -lmysqlclient -lz -lm]
        --libs_r         [-L/usr/local/mysql/lib -lmysqlclient_r -lz -lm]
        --socket         [/tmp/mysql.sock]
        --port           [3306]
        --version        [5.0.51a]
        --libmysqld-libs [-L/usr/local/mysql/lib -lmysqld -lz -lm]

I then try forcing the issue by setting the environment variable as well:

bimota:lib cch1$ sudo env ARCHFLAGS="-arch x86_64" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_configBuilding native extensions.  This could take a while...Successfully installed mysql-2.71 gem installed

Again, it looks promising. But now when Rails asks Ruby (1.8.6 from the default XCode 3.0 install) to load the mysql gem, the OS generates a load error due to some kind of a mismatch:

bimota:mmweb cch1$ script/console
Loading development environment (Rails 2.0.2)
>> require_library_or_gem 'mysql'
LoadError: dlopen(/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle, 9): no suitable image found.  Did find:
	/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle: mach-o, but wrong architecture - /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
	from /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
	from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:32:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:496:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:342:in `new_constants_in'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:496:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/requires.rb:7:in `require_library_or_gem'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/reporting.rb:11:in `silence_warnings'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/requires.rb:5:in `require_library_or_gem'
	from (irb):1
>> ^Dbimota:mmweb cch1$

Why the mismatch? Some googling led me to this post that notes that the Ruby interpreter bundled in XCode 3.0 is in fact only compiled as a 32-bit i386 executable. That’s right: the latest and greatest Macs (with Intel Core 2 Duo processors) running the latest and greatest OS (Leopard 10.5.2) are shipping with a neutered Ruby interpreter.At this point, I see two solutions:1. Install the i386/32-bit only version of MySQL (boo!). I’ve confirmed that this allows a nice working mysql binding to be built with the following command:

bimota:mysql cch1$ sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...Successfully installed mysql-2.71 gem installedbimota:mysql cch1$

And not only does it compile, it actually works.2. Recompile Ruby with x86_64 support. That sounds like a bigger job -but there are web sites that show you how to get started. For now though, I’m leaving XCode alone until I know what works -that way, when it breaks, I can assign the failure to the 64-bit ruby interpreter.

Validating the presence of exactly one of a list of attributes

November 8th, 2007

I read Eric’s blog on creating an ActiveRecord validation for an exactly-one-of predicate.  I myself had just recently gone through a similar exercise and found a straightforward way of expressing it using the validate method:

validate :exactly_one_of?

def exactly_one_of?
  attrs = [:url, :file, :blob]
  returning (attrs.inject(false){|m, o| m^self[o]}) do |bool|
    self.errors.add(:base, “Exactly one of #{attrs} must be set.”) unless bool
  end
end

While this worked nicely for me, Eric went to the trouble of building a semantically clean validation method that mimics the options of the standard validations.  I prefer using Ruby’s Exclusive OR operator combined with inject instead of the looping in Eric’s method, but I like the semantic style he defined.  Combining his semantics with my logic yields something slick and DRY like this:

def self.validate_exactly_one_of(*attrs)
  options = attrs.last.is_a?(Hash) ? attrs.pop : {}
  configuration = { :message => “one of #{attrs.to_sentence :connector => ‘or’} must be set”, :on => :save }
  configuration.update(options)

  send(validation_method(options[:on] || :save)) do |record|
    returning (attrs.inject(false){|m, o| m^record[o]}) do |bool|
      self.errors.add(:base, configuration[:message]) unless bool
    end
  end
end

I also fiddled the options logic because I think otherwise it would have been possible to have an attribute listed in the error message list that was in fact an option.

Factory methods and scoping with ActiveRecord inheritance hierarchies

October 24th, 2007

Today I ran into a problem with a factory pattern for an ActiveRecord inheritance hierarchy and scoping: has_many association members were being created without references to the “parent” model instance.  For example, my parent class looks something like this:

class Post < ActiveRecord::Base
  has_many :attachments, :as => :attachee
end

In my AttachmentsController, I was trying to create an attachment like this:

@post.attachments.create(attributes)

This approach is bog standard Rails and works fine.  The next iteration involved subclassing the Attachment model class and making Attachment#new method be a smart factory returning an instance of a subclass based on the initialization attributes:

class Attachment < ActiveRecord::Base
  validates_presence_of :attachee_id

  def new(attributes, &block)
    if self == Attachment
      return AttachmentURL if attributes[:url]

      return AttachmentBlob if (attributes[:blob] or attributes[:file])

    end

    super

  end
end

class AttachmentURL < Attachment
end

class AttachmentBlob < Attachment
end

Exercising this code in the console showed the right behavior:

Attachment.new({:url => ‘http://cho.hapgoods.com’})   # => AttachmentURL

But things started looking a bit squirrely when I started to use associations: my validation for the presence of the attachee_id reference was failing:

@post.attachments.create({:url => ‘http://cho.hapgoods.com’})

How could Rails be leaving out the critical association reference?!?  It turns out that association methods like create use :create scoping to set attribute values.  In ActiveRecord::Base.initialize, the :create scope is examined and all scoped attributes are assigned values from the :create scope.  My problem was that the :create scope was being lost when I switched to a subclass in the Attachments#new constructor.  This is the Important Fact:  A scoped ActiveRecord model does not imply scoping for subclasses.

I solved the problem by “transferring” the scope from the base class to the subclasses.  It really wasn’t that complicated and I ended with a really spiffy pattern for subclassed AR model factories.  Here is what my factory method looks like now:

class Attachment < ActiveRecord::Base
  def self.new(attributes, &block)
    if self == Attachment
      klass = AttachmentURL if attributes[:url]
      klass = AttachmentBlob if attributes[:blob] or attributes[:file]
      raise ‘Unknown Attachment type’ unless klass
      klass.send(:with_scope, :create => (scope(:create) || {})) do  # Scope subclass to same create attributes as this class.
        return klass.new(attributes, &block)
      end
    end
    super
  end
end

nota bene: It so happens that I was using Single Table Inheritance (STI) for this hierarchy, but I suspect the problem and solution would be identical with true abstract classes and arbitrary table mapping.  Also, the fact that I was using a polymorphic has_many association is almost certainly irrelevant.

Technorati Tags: , , ,

More on Full Circle REST

September 18th, 2007

Some time ago I frothed on (hip people call this blogging) about how Rails was missing an opportunity to exploit the modelling work done in routes.rb. You can see my post here.

Anyway, in the intervening month or so, I have been working with Ian White’s nifty ResourcesController plugin. As is my wont, I bitch and moan a lot about what is wrong with ResourcesController (RC). The nice thing is that Ian listens and crafts some pretty damn nice code -a lot of my vision of Full-circle REST is starting to appear in RC. In fact, RC is so promising I have abandoned my own plugin and jumped into bed with Ian (in a manner of speaking).

RC can identify many of the RESTful resources of a concrete request with an increasingly concise invocation. It can handle singleton resources, arbitrary nesting and polymorphic nesting. For these features alone, I suggest you look into it.

RC identifies the abstract resources of the URI by examining the inbound request (it has evolved from doing this in a purely textual fashion to examining the matched route’s structure). But it could be better. It could identify the route (when the request arrives) more efficiently. It could link this concrete route directly and unambiguously to the abstract resource that generated it (keeping RC nice and DRY w.r.t routes.rb). And critically, the abstract resource could refer to its parents in the hierarchy of resources in routes.rb, allowing even more direct determination of the complete chain of abstract resources that is being supplied by the URI. Then we could have full-circle REST.

But at this point, RC is hampered by Rails itself. Three patches to edge are required to get us there. I’ve written the first two, and the third shouldn’t be to hard (volunteers?). I’m presenting them in reverse order because I think they’re easier to understand that way.

Patch #3 - Store the hierarchy of abstract REST resources that generate named routes. Currently, only a few strings (name_prefix) provide circumstantial evidence of the parents. Let’s be explicit and store a reference to the the parent in each ActionController::Resources::Resource instance. Then the abstract REST resources will be represented by a forest of trees -and that is pretty close to ideal.

Patch #2 - Store the generating abstract resource in the generated named route. I’ve written the code to do this, but I’m not proud. In fact, I think it is kinda ugly.

module ActionController
  module Resources
      def action_options_for(action, resource, method = nil)
        default_options = { :action => action.to_s, :resource => resource }
        require_id = !resource.kind_of?(SingletonResource)
        case default_options[:action]
          when “index”, “new” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
          when “create”       : default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
          when “show”, “edit” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
          when “update”       : default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
          when “destroy”      : default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
          else                  default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements)
        end
      end
  end
end

module ActionController
  module Routing
    class Route
      attr_accessor :resource
    end
    class RouteBuilder      # Construct and return a route with the given path and options (including the resource option)
      def build(path, options)        # Wrap the path with slashes
        path = “/#{path}” unless path[0] == ?/
        path = “#{path}/” unless path[-1] == ?/    

        path = “/#{options[:path_prefix]}#{path}” if options[:path_prefix]

        resource = options.delete(:resource)
        segments = segments_for_route_path(path)
        defaults, requirements, conditions = divide_route_options(segments, options)
        requirements = assign_route_options(segments, defaults, requirements)

        route = Route.new
        route.segments = segments
        route.requirements = requirements
        route.conditions = conditions
        route.resource = resource

        if !route.significant_keys.include?(:action) && !route.requirements[:action]
          route.requirements[:action] = “index”
          route.significant_keys << :action
        end

        if !route.significant_keys.include?(:controller)
          raise ArgumentError, “Illegal route: the :controller must be specified!”
        end

        route
      end
    end
  end
end

The actual code change is minimal, but it is not very clean in RouteBuilder -in fact, that code really should not have been required. FYI, this code was working about a month ago and I assume it should still work. Still, beware!

Patch #1 - Store the recognized route in the abstract request. Testing aside (see below), this one is simple:

module ActionController
  class AbstractRequest
    attr_accessor :recognized_route
  end

  module Routing
    class RouteSet
      def recognize(request)
        params, recognized_route = recognize_path(request.path, extract_request_environment(request))
        request.path_parameters = params.with_indifferent_access
	request.recognized_route = recognized_route
        "#{params[:controller].camelize}Controller".constantize
      end

      def recognize_path(path, environment={})
        routes.each do |route|
          result = route.recognize(path, environment) and return result, route
        end

        allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }

        if environment[:method] && !HTTP_METHODS.include?(environment[:method])
          raise NotImplemented.new(*allows)
        elsif !allows.empty?
          raise MethodNotAllowed.new(*allows)
        else
          raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
        end
      end
    end
  end
end

I’ve changed the return signature of recognize_path so there may be some undesired side-effects. But I haven’t found any yet. Also, the TestRequest object would need to be modified as well to set the recognized_route attribute appropriately because recognize is not invoked in testing (another volunteer?).

So, a quick recap: Rails invites us to model our REST resources (in routes.rb) and uses that information for recognizing inbound routes and generating routes in views. But there is a much value to our modeling work to be found in the controllers themselves. And to harvest that value, we need visibility into the abstract resources that generated the invoked route. Rails needs to evolve in this direction I call full-circle REST.

Technorati Tags:

Why can’t stinit get the attention it deserves?

September 14th, 2007

I’ve been frustrated by tape drives for years.  They are the most finicky, unreliable, least-documented and most expensive components of most of the computer systems I have had the pleasure of administering.  I’m no longer an “up-to-date” sysadmin, but I still try to take care of my own stuff and, not surprisingly, tape drives still are the the top of my bitch list.

Today’s bitch is brought to you by stinit -a great idea that needs some love.

With stinit, you have a means of enforcing a configuration set on a device.  Since each tape drive (in the default Linux st configuration) is represented by four devices, you can easily define four different configuration modes for each drive.  Simply by using a different device (/dev/st0, /dev/st0a, /dev/st0l, /dev/st0m, for example) you can ensure your apps see the tape drive just so.

Unfortunately, stinit is weakly documented (just try the mode= syntax) and full of bugs that make tracking down simple errors costly.  Here’s the one I hit today…see if you can tell what the hell is wrong.

[Background: I have two tape drives connected to this server, one is a Compaq SuperDLT, the other an ancient Archive DDS drive for which I have no documentation]

[root@aprilia amanda]# stinit
Warning: No modes in definition for (’COMPAQ’, ‘SuperDLT1′, ”).
Can’t find defaults for tape number 1.
Initialized 1 tape device.

Hmmm…seems like I left out the modes in the definition of Compaq drive.  Let’s go to the file:


# Archive Python
manufacturer=ARCHIVE model=”Python 28388-XXX” {
        timeout=3600
}

# Compaq SuperDLT1
manufacturer=COMPAQ model=”SuperDLT1″ {
        timeout=3600
        mode1 blocksize=0
        mode2 blocksize=0
        mode3 blocksize=0
        mode4 blocksize=0
}

Puzzling.  The modes are clearly in there.  I try the stinit command again, this time with the verbose option:


[root@aprilia amanda]# stinit -v

stinit, processing tape 0
The manufacturer is ‘COMPAQ’, product is ‘SuperDLT1′, and revision ‘5F5F’.

stinit, processing tape 1
The manufacturer is ‘ARCHIVE’, product is ‘Python 28388-XXX’, and revision ‘4.45′.
Warning: No modes in definition for (’COMPAQ’, ‘SuperDLT1′, ”).
Can’t find defaults for tape number 1.
Initialized 1 tape device.

Interesting that the error message appears after stinit trys to initialize the Archive drive.  After a little googling, I find that an stinit bug was reported in the debian mailing lists (here) for the numbering of the modes.  On a hunch that the programmer couldn’t get his zero-based and one-based numbering straight for the tape drives either, I add a garbage mode for the Archive drive

# Archive Python
manufacturer=ARCHIVE model=”Python 28388-XXX” {
        timeout=3600
        mode1 blocksize=0
}

# Compaq SuperDLT1
manufacturer=COMPAQ model=”SuperDLT1″ {
        timeout=3600
        mode1 blocksize=0
        mode2 blocksize=0
        mode3 blocksize=0
        mode4 blocksize=0
}

…and ABRACADABRA, the problem goes away:

[root@aprilia amanda]# stinit
Initialized 2 tape devices.