Ruby Duck Typing Goodness
Posted by Guy Naor Fri, 12 Jan 2007 22:48:00 GMT
Coming over from C++ to Ruby, duck typing was one of the really cool features I learned about. You don't use it every day, but when you need it, it's an amazing tool.
Case in point: writing the admin interface to the Famundo help app (you can currently see the public interface at Famundo's Help. (A new version is coming out shortly, and I intend to Open Source it soon.) I used AjaxScaffold for the interface, but wanted to also manage file uploads. I wanted to keep it the same interface as the rest of the tables, as it gives me a nice UI, sorting, searching, etc...
To make that work, I duck typed a class that uses the filesystem, but acts like an ActiveRecord class. I then pointed AjaxScaffold at it and as far as the user experience goes, it's just like managing a database table. Simple and intuitive.
I didn't duck type everything in ActiveRecord, just the stuff that AjaxScaffold needed. Of course, if the need comes, adding more methods is very easy.
Following is the class I created. The $STORAGE_DIR global points to where the storage dir is. Usually something under public so that it's easy to serve the files. But you can also use x-send-file or some other trick and put it someplace else.
class StoredFile
attr_accessor :size, :filename, :modified_time
# Trick the id...
alias_method :id, :filename
alias_method :id=, :filename=
# All the methods we need to overwrite from active_record...
def self.table_name
'stored_files'
end
def self.primary_key
'filename'
end
def filename_before_type_cast
filename
end
def self.count(*args)
options = args.last.is_a?(Hash) ? args.pop : {} # Taken from the rails source!
fltr = options[:conditions] || ''
Dir.entries($STORAGE_DIR).delete_if{|i| i[0..0] == '.' || (fltr.is_a?(Regexp) ? i !~ fltr : !i.downcase.include?(fltr.to_s))}.size
end
def self.find(*args)
options = args.last.is_a?(Hash) ? args.pop : {} # Taken from the rails source!
# See if it's a single file find, like ID in rails tables
if args.first.is_a?(String)
fname = args.first.gsub(/[^\w\.\-]/, '_')
raise "File not found #{args.first}" if !File.exist?("#{$STORAGE_DIR}#{fname}")
return StoredFile.init_from_file(fname)
end
fltr = options[:conditions] || ''
flist = Dir.entries($STORAGE_DIR).delete_if{|i| i[0..0] == '.' || (fltr.is_a?(Regexp) ? i !~ fltr : !i.downcase.include?(fltr.to_s))}.collect{|f| StoredFile.init_from_file f }
# This is now an unsorted list of files. Now we can sort and apply limits/offsets
if options[:order]
options[:order] =~ /^(.+) (asc|desc)?$/i
reverse_ord = !$2.nil? && ($2.downcase == "desc")
case $1
when 'filename' : flist.sort! {|a,b| a.filename <=> b.filename }
when 'modified_time': flist.sort! {|a,b| a.modified_time <=> b.modified_time }
when 'size' : flist.sort! {|a,b| a.size <=> b.size }
end
flist.reverse! if reverse_ord
end
idx = (options[:offset] ? options[:offset] : 0).to_i
len = (options[:limit] ? options[:limit ] : flist.size).to_i
flist[idx,len]
end
def errors
[]
end
def self.destroy fname
FileUtils.rm_f "#{$STORAGE_DIR}#{fname}"
fname
end
def destroy
StoredFile.destroy self.id
end
protected
def self.init_from_file f
fstat = File::stat "#{$STORAGE_DIR}#{f}"
sf = StoredFile.new
sf.filename, sf.modified_time, sf.size = f, fstat.mtime, fstat.size
sf
end
end

















In the case of code such as:
def id filename end
def id=(the_id) filename = the_id end
You may wish to consider using the alias_method method.
alias_method :id, :filename alias_method :id=, :filename=
Good catch Chris. I was adding the methods one by one based on tests, so I missed this as being the same :-).
It's double good, as it cuts a few more lines of code from the help app I'm writing. It's already under 600 LOC, but the less the better.
I updated the post to reflect this change.