2007/4/23

An Insanely Handy Table Generator in Ruby

Recently we have been developing an application that displays a slew of information. Most of the items displayed are in the form of table. So there we are, first writing each table component by hand, then we get tired of it. It's just simply... not DRY. So like every other sane developer, we started writing our own table tag helpers, to save our life (and our poor fingers) to generate those pesky <table> tags.

With the help of some good Ruby, the table generator can be an all-season, yet still very terse, helper.

The Code

  def generate_table(array_object, titles=[], columns=[], callbacks={})
    html = ""
    html += '<table class="auto">'
    html += "<tr>"
    titles.each { |t| html += "<th>#{h(t)}</th>"}
    html += "</tr>"
    
    row_number = 0
    
    array_object.each do |row|
      html += "<tr>"
      columns.each do |col|
        if col == :_row_number
          data = row_number
        else
          if row.class == Hash
            data = (col.class == Array ? col.collect {|c| row[c]} : row[col])
          else
            data = (col.class == Array ? col.collect {|c| row.send(c)} : row.send(col))
          end
        end
        
        if callbacks[col]
          if data.class == Date || data.class == Time || data.class == DateTime
            str = callbacks[col].call(data)
          else
            str = callbacks[col].call(*data)
          end
        else
          str = h(data)
        end
        
        html += "<td>#{str}</td>"
      end
      html += "</tr>"
      
      row_number += 1
    end
    
    html += "</table>"
  end

Here is the code of the generator. We made it into a Rails helper class (ApplicationHelper), but this snippet is not just good for Rails.

Usage

This generator expects the table data source to be in either an array of hases, of an array of ActiveRecord-like objects, ie. objects that have attribute accessors, such as student.name. So say we want to generate a table of students, we do:

  generate_table Students.find(:all), 
    ["Name", "Address", "Mobile", "Homepage"],
    [:name, :address, :mobile, :homepage],
    {
      :homepage => lambda { |hp| link_to "visit the homepage", hp }
    }

This helper has three required arguments and one optional. The first three are an array of objects, an array of titles, an array of attributes to be fetched. The optional argument is an hash of callbacks.

In our case, the generator iterates the array of Student objects, and retrieves each's name, address, mobile phone number and homepage. The column homepage will be post-processed by a callback (lambda), which in turn yields the URL with a title "visit the homepage".

Array of hashes fits in the picture well. The secret is, the generator knows if the row object is a hash. If it is, it uses Hash key-value accessor Hash#[] to retrieve the column, instead of sending messages to the row object.

Fancier Usage Is Just a Hack Away

In some cases, we want to reuse the same attribute in different columns, say we want to use student id to fetch another data. The problem being, since the callback is a hash, an attribute can have exactly only one callback. Or does it have to be? Once again, some Ruby comes to help:

  generate_table Student.find(:all), 
    ["Student ID", "Extra Info"],
    [:id, [:id, :name]], 
    {
      [:id, :name] => lambda{ |i, n| retrieve_extra(i) }
    }

The point is that arrays can actually be fine hash keys, and when the callback of such column is given exact the same number of arguments. In this case, whether you want to use the attribute name is irrelevant. We just use this "virtual" attribute to let us get by.

... And One More Thing

An attribute, :_row_number, yields the current row number, starting from zero. This doubles as a counter, and can be useful in items such as a ranked list.

Possible Extensions

There are dozens of ways to extend the generator. Styling options, even/odd row handler, more hidden attributes (:_row_object anyone?) and column-span support all come to mind. For the time being at least, the snippet works perfectly for me, and I love it to stay the way it is.

Ruby Tuesday Taiwan

So that's so much for today! If you happen to be living in northern Taiwan and are interested in Ruby and/or Rails, we are having a regular meet-up at OP Café in Hsinchu City. The next meet-up will be on Tuesday, May 1, 2007, starting from 7:30 pm. I'll be talking about the sorry state of affair of the login thingy--how we end up rewriting our own login system each time for each app, and some possible remedies. See you there!

2007/4/13

Asynapse 初版釋出

在經過了兩天的黑客鬆之後,將釋出 Asynapse 的方式定了下來,於是率先釋出 Perl 版的 Asynapse。有興趣的讀者可前往 Asynapse 專案下載頁,或是利用 CPAN 下載、安裝。

更新:JavaScript 版本也已經釋出,可前往 JSAN 下載

此釋出版為開發者版本,相關的文件請參詳 Asynapse::Record Perl 模組的文件。

2007/4/10

Localizing Early Is Good Even for Monolingual Sites

Localizing your application is a good thing even if it's monolingual. By using a localization ("l10n") package, you separate the work of "putting some text up there" from "serious copywriting"--which usually is best left to good writers or done when you are not in the coding mode.

Modern localization packages force developers to wear some straitjacket. Yes, the infamous "_" symbol that litters here and there isn't pretty. Singulars and plurals are grammar headaches. Why the heck the world can't just think like we programmers, to whom everything starts with 0 and "copied 5 file(s)" is both formal and polite?

The problem is, as much as we emphasize refactoring, we don't pay equal attention to the quality of writing. Then when someone who really cares shows up and asks you to revise it, believe me, fiddling 50 HTML files scattered in 10 directories is absolutely no fun. If you had used packages such as gettext, the only thing you would need to do is open up the wounded .po file and clean it up, or leave it to others (its format is simple enough for average muggles to understand--far better than those things you do in .jsp/.asp/.php/.rhtml/whatever).

l10n the Pure Ruby Way

gettext is your friend if you're developing a real thing. It's ubiquitous, available to practically every programming language on earth, and most important of all, it is established--which means you don't have to reinvent l10n tools (the obvious thing) and l10n workflows (the less obvious thing), since there are thousands of people who have done that before you do.

Sometimes, however, I find gettext an overkill for some one-shot projects that simply need some localization help but not all the perks that gettext offers. This is when this localization plug-in comes in handy. It's relatively unknown, but I love it. It's really just a string look-up thingy, and does no form validation messages (one sine qua non for more serious Rails app). On the other hand, installation is only a "script/plugin install" away, and its documentation is easy and short (making gettext for Ruby/Rails work is an ordeal partly because of scant documentation and no-brainer how-to's).

Two things to bear in mind, though. First, this plug-in doesn't scale up well--it does not have a string collector as powerful as gettext's. Second, it allows the use of Ruby lambda for more delicate string handling, which is addictive. The first thing lets you know when you need to use gettext, and the second reminds you that you need to think about the migration path if your project eventually grows. Refraining from the lambda magic will make things a little bit easier.

2007/4/8

YAPC::Asia 2007 紀行

YAPC Asia 207 圓滿落幕,感謝 Shibuya.pm 出人出力辨了這麼好的 研討會。我所演講的主題:Asynapse,雖然仍是個有點模糊、難以界定的概 念,不過,在提供了適當的 Demo 的情況之下,也得到了 Catalyst 的 Jonathan Rockway、Jifty 的 Jesse Vincent、Amazon EC 的 Emerson Mills 等人於口頭上的肯定。

Asynapse 目前還仍在實驗當中,不過其各項目的算是已經成功地證實其可 行。像是與以 REST 實做與 ActiveRecord 相仿的資料介面「AsynapseRecord」, 以 JSON 做為載體來進行 REST API 呼叫的「AsynapseTextDiff」,以及以 [Interface Builder] 做為刻畫 WebUI 工具的「AsynapseInterface」。這些實 驗都在近日成功完成,順利的話,亦將進行包裝,釋出給開發人員所使用的版本。

讀者如果有興趣,除了 Handlino Blog 之外,亦可持續追蹤 Asynapse 專 案頁面,將不定期出現各更新的訊息。