Tuesday, January 30, 2007

Single Table Infuriation

Single Table Inheritance in Rails is an excellent pattern that works really well, except when it doesn't.


class Parent < ActiveRecord::Base
end

class Child < Parent
end

class GrandChild < Child
end


Each of the above classes are defined in their own files (app/models). There are times in development mode that I've noticed that Child.find(:all) will generate the following SQL:


SELECT * FROM parents WHERE (type='Child')


In Rails 1.2 the fix for this is perhaps a little obfuscated.


class Parent < ActiveRecord::Base
require_dependency 'child'
end

class Child < Parent
require_dependency 'grand_child'
end

class GrandChild < Child
end


Now, the other part of this solution is that you probably want to call require_dependency at the end of the class definition as there may be methods in the parent object that the child object needs. I suppose you could re-open the class at the end of the file.

Wednesday, January 24, 2007

capture, binding and other diabolical machinations

For the South Bend Ruby Group I volunteered to talk about form builders for February. At the time of volunteering, I knew close to nothing about form builders. I decided to take the upcoming meeting as a reason for learning about the builders. And now I'm hooked.

But that is not the point of this particular post. What I'm talking about is the bizarre magic involved in the form builder. In a project that I've been working on we have a need for rendering lists both in tables and in lists. However we'd really like to re-use the view logic in generating the list. Its pretty much a matter of hot-swapping the tags appropriately.


<% list_for(@list) do | l | %>
<% l.caption 'Hello World' %>
<% l.header_row(:id => 'list_head') do %>
<%= l.header_cell 'title' %>
<%= l.header_cell 'edit' %>
<%= l.header_cell 'delete' %>
<% end %>
<% l.row( :class => 'spam', :id => 'hot_dog') do | list_item | %>
<%= l.cell l.title %>
<%= l.cell 'edit_link' %>
<%= l.cell 'delete_link' %>
<% end %>
<% end %>


We opted to create a builder class. You call :list_for, and can optionally pass a builder class (like the one below). Of particular note there is use of bindings and captures. I'm still a bit dazzled by what all is going on (and wrote the tests to make sure it all works). Below is my understanding of what is going on.

1) There is the :concat method (as defined in the ActionView::Helpers::TextHelper module). We are taking the results of first parameter and appending it to the proc's binding. The proc's binding is the result of the stuff in that proc. In the case of the above :list_for its all of the text generated :list_for's block.

2) There is the :capture method (as defined in ActionView::Helpers::CaptureHelper module). This little critter packages up the input block as text. I tried using block.call but this didn't work as intended (I got double the block text). If I didn't want the option of passing options to each of the elements, I could conceptually simplify the method by doing a manual opening of a tag, block.call, then manually closing the tag. This could be a performance piece that I will implement at a later point (though I'll need a Hash#to_markup_attributes method).



def list_for(object, *args, &block)
raise ArgumentError, "Missing block" unless block_given?
options = args.last.is_a?(Hash) ? args.pop.symbolize_keys! : {}
builder = options.delete(:builder) || ListTableBuilder
builder.new(object, self, options, &block)
end

# <table>
# <caption>
# <tr>
# <th />
# </tr>
# <tr>
# <td />
# </tr>
# </table>
class ListTableBuilder

# Welcome to the insanely bizarre initialize function
def initialize(collection, template, options, &proc)
@collection, @template, @proc = collection, template, proc
@options = options.reverse_merge( :class => 'list', :id => 'list')
@template.concat( @template.content_tag('table', @template.capture(self, &proc) , @options ), @proc.binding)
end

# :options are applied to content_tag('td')
def cell(text, options = {})
@template.content_tag('td', text, options)
end

# :options are applied to content_tag('th')
def header_cell(title, options = {})
@template.content_tag('th', title, options)
end

# :options are applied to content_tag('caption')
def caption(text, options={})
@template.concat( @template.content_tag('caption', text, options), @proc.binding )
end

# :options are applied to content_tag('tr')
def header_row(options={}, &block)
raise ArgumentError, "Missing block" unless block_given?
@template.concat( @template.content_tag('tr', @template.capture(&block), options) , block.binding)
end

# Options
# The options will be added to each row.
# :class will append and not destroy the internal classes
# :id will be used as a prefix for the internal id generation
def row(options={}, &block)
raise ArgumentError, "Missing block" unless block_given?
options.symbolize_keys!
@collection.each do | c |
row_options = options.reverse_merge( :class => '' )
row_options[:class] += ' ' + @template.cycle('odd_row', 'even_row')
row_options[:id] += '_' + @template.send(:dom_id, c)
@template.concat( @template.content_tag('tr', @template.capture(c, &block ), row_options), block.binding )
end
end
end

Monday, January 22, 2007

Ruby Cookbook from O'Reilly

Ruby Cookbook: Recipes for Object-Oriented Scripting
by Lucas Carlson & Leonard Richardson

ISBN 0-596-52369-6

I haven't read this book cover-to-cover, but I have given it a very thorough inspection, skimming through sections to find points of interest and then stopping to read those recipes. So, actually, I've probably read a whole bunch of the book and didn't realize it.

The book is written in a very concise manner, with plenty of examples, lots of useful code, excellent cross-references and a very generous license that allows you to use the code from the book (albeit with some restrictions).

Each recipe is presented as a quartet of Problem, Solution, Discussion and See Also sections. Each problem is presented succinctly, and the solutions dive right into the required code (with code comments explaining some of the more complicated lines of code, but not explaining the self-documenting lines). The discussion section of each solution really digs into what is going on, code choices that were made, variations, and basically anything else that might be interesting. The see also section cross-references other recipes that may be pertinent to the given problem.

All told, this book does an amazing job of going through a wide range of problems, from Strings & Numbers to Meta-Programming & Rails and Graphics. This book is loaded with useful bits of information and I would highly recommend adding it to your collection.

Wednesday, January 17, 2007

Some Weird Weird Ruby (or nest Classes and Inheritence)

Thinking cap time. I am working on a CMS and the short of it is that a Page will have many PageBlock objects. The PageBlock is versioned and can have multiple things associated with it (i.e. Links or Files or Other Stuff, thankfully each page block is homogenous). I decided that the Links/Files will be serialized. So I wrote a serializing helper object. This serializing object


class PageBlock < ActiveRecord::Base
class << self
# I want to be able to configure the underlying DataSerializer
# in the containing class
delegate :set_valid_serialized_keys, :to => :data_serializing_class
def data_serializing_class
self::DataSerializer
end
end
class DataSerializer
class << self
def set_valid_serialized_keys( *attrs )
write_inheritable_attribute :valid_serialized_keys, attrs
class_inheritable_reader :valid_serialized_keys
end
end

def add_data(data = {})
data.assert_valid_keys(valid_serialized_keys)
... do the magic ...
end
end
end


Now

$ PageBlock.valid_serialized_keys
=> NoMethodError: undefined method `valid_serialized_keys' for PageBlock::DataSerializer:Class



class PageLinkBlock < PageBlock
set_valid_serialized_keys :path, :name
end



$ PageBlock.valid_serialized_keys
=> [:path, :name]


DOH!!! Man that is definitely not the behavior I want. I guess my nested Class has some behavior that I'm not aware of. I'm certain the behavior is intentional, I just didn't know about it

So the solution?


class PageBlock < ActiveRecord::Base
class << self
# I want to be able to configure the underlying DataSerializer
# in the containing class
delegate :set_valid_serialized_keys, :to => :data_serializing_class

def data_serializing_class
self::DataSerializer
end

def inherited(page_block_subclass)
page_block_subclass.module_eval <<-EOV
class DataSerializer < PageBlock::DataSerializer
end
EOV
super
end

end
class DataSerializer
class << self
def set_valid_serialized_keys( *attrs )
write_inheritable_attribute :valid_serialized_keys, attrs
class_inheritable_reader :valid_serialized_keys
end
end

def add_data(data = {})
data.assert_valid_keys(valid_serialized_keys)
... do the magic ...
end
end
end


Then I define PageLinkBlock and


$ PageBlock.valid_serialized_keys
=> NoMethodError: undefined method `valid_serialized_keys' for PageBlock::DataSerializer:Class


Alas, the behavior that I want. Time to dig out the Pickaxe Book to see if there is a reference to this.

Tuesday, January 16, 2007

Helper Module Testing

First and foremost


sudo gem install ZenTest


Then in your app/test/test_helper.rb add


require 'test/rails'


Then make a test class

class FooHelper
def print_hello_world
content_tag('p', 'Hello World!')
end
end

class FooHelperTest < Test::Rails::HelperTestCase
def test_method_goes_here
assert_equal '<p>Hello World</p>', print_hello_world
end
end


Of particular note, the name of the class that inherits from Test::Rails::HelperTestCase is very important. If the inheriting class name is MyHelperTest, make sure a module named MyHelper exists.

Wednesday, January 03, 2007

Dear RDoc,

One thing that I've quickly taken for granted is RDoc. Prior to RDoc, I lamented that the documentation that I wrote in code was impossible to discover, and thus the in-code documentation was useless for non-programmers. Thus, in order to "say" what the system did, additional documentation needed to be created. So now there were two sets of documents, both saying what the system did, but not communicating with each other.

Oh the humanity!

Enter RDoc. I can now rake that information and generate a useful document defining the system. It gets even better if I start using should-language, either in specify or test context (i.e. specify 'student should have a name' or def test_student_should_have_a_name). Then I can rake just the method names and get a document that says concisely what the system should do.

Of course this leads itself to writing test stubs earlier in the project process. Even better, it is possible for a project manager to write some of these specifications.

Suddenly, the tests, which everyone wants and needs, become useful not only for documentation but also for communication and um... testing. And better yet, the documentation is not discarded nor duplicated.

Only write what you need.

I don't recall where I read this, but someone once wrote that each line of code should be viewed as a liability.

At work, a colleague and I have begun referring to the test code as a contract; it describes the behavior of the system. The more tests, the tighter the contract. By extension, the more code in the system, the more restrictive the implicit contract. The implicit contract is the written, and all to often unwritten, expectations of the system.

So the less code needed to get the job done, the better.