RailsのMapperあたりを読んでみる

「Head First Rails」に続いて、「RailsによるアジャイルWebアプリケーション開発 第3版」をツラツラと読んでみた。ホビープログラマとしては、テストの話あたりが新鮮で面白かった。

まずURLのラウティング(ディスパッチ?)処理をやってるActionController::Routing::Routesがよく分からない。connectとか、resource、resourcesって何なのよ、結局! ということで、いっそソースコードを見てみようと思ってrouting.rbとかroute_set.rbを覗いてみたら、これが結構面白く、かつ、ああ、Rubyメタプログラミング方面がよくワカランなぁということが分かった。

rails/app/config/route.rbの記法に関連したキモの1つは、

def method_missing(route_name, *args, &proc) #:nodoc:
  super unless args.length >= 1 && proc.nil?
  @set.add_named_route(route_name, *args)
end

というところか。Mapperオブジェクトで定義されてるデフォルトのrouteは、rootだけなので、

map.login 'login', :controller => 'accounts', :action => 'login'

とかやると、

Mapper.method_missing(:login, {:controller => 'accounts', :action => 'login'})

て感じに呼ばれてるってことか。

ほかのCatalystとかDjangoでは、このへんどうなってるのかと思ってググッてみたら、少なくともAPI的には似たり寄ったりな感じのようで、なるほどなと思った。

やっぱりコードを読むといろいろと勉強になるもので、例えばActionController::Routing#normalize_pathsは、

 def normalize_paths(paths)
   # do the hokey-pokey of path normalization...
   paths = paths.collect do |path|
     path = path.
       gsub("//", "/").           # replace double / chars with a single
       gsub("\\\\", "\\").        # replace double \ chars with a single
       gsub(%r{(.)[\\/]$}, '\1')  # drop final / or \ if path ends with it

     # eliminate .. paths where possible
     re = %r{[^/\\]+[/\\]\.\.[/\\]}
     path.gsub!(re, "") while path.match(re)
     path
   end

   # start with longest path, first
   paths = paths.uniq.sort_by { |path| - path.length }
 end

なんてある。コレクションを破壊的に書き換えるとき、どういうスタイルがいいんだろうかと思ったりしていたけど、map系でブロックで渡して戻り値をそのまま代入するというスタイルでいいのか、というのが発見。何となくブロックを受け取るイテレータっぽいやつって戻り値とかじゃなくて、言語キーワード的に感じていたから、キーワードに戻り値なんてないような印象を持っていた、ような気がする。

スタイルにしても、改行の位置やgsubという同類のメソッドチェーンの並べ方なんかがスッキリ読みやすい。

似たような話で、

def generation_extraction
  segments.collect do |segment|
    segment.extraction_code
  end.compact * "\n"
end

というのも新鮮。endの後ろにメソッドをくっつけるのとか、良くないのかと思ってたけど、ぜんぜんふつうなのか。

もう1つナルホドと思ったのは、Routeオブジェクトを初期化が終わったら、freezeしてしまうというところだけど、

def to_s
  @to_s ||= begin
    segs = segments.inject("") { |str,s| str << s.to_s }
    "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
  end
end

# TODO: Route should be prepared and frozen on initialize
def freeze
  unless frozen?
    write_generation!
    write_recognition!
    prepare_matching!

    parameter_shell
    significant_keys
    defaults
    to_s
  end

  super
end

初期化のときに世代管理やらto_sをあらかじめ用意しておくとかをやっている。ここでも、to_sの値をメモしておく「@to_s ||= begin」とブロックの戻り値を代入してる。

それより以下のようなコードがよく分からないけど、なんだか面白そうだ。Moduleのメソッドを全部undefして、module_evalでゴソゴソとなにやら足している。こういうのがメタプログラミングなのかなと想像しつつ、やっぱりそのへん全然分かってないなぁという感じが強い。

class NamedRouteCollection #:nodoc:
  include Enumerable
  include ActionController::Routing::Optimisation
  attr_reader :routes, :helpers

  def initialize
    clear!
  end

  def clear!
    @routes = {}
    @helpers = []

    @module ||= Module.new
    @module.instance_methods.each do |selector|
      @module.class_eval { remove_method selector }
    end
  end

  def add(name, route)
    routes[name.to_sym] = route
    define_named_route_methods(name, route)
  end

  def get(name)
    routes[name.to_sym]
  end

  alias []=   add
  alias []    get
  alias clear clear!

  def each
    routes.each { |name, route| yield name, route }
    self
  end

  def names
    routes.keys
  end

  def length
    routes.length
  end

  def reset!
    old_routes = routes.dup
    clear!
    old_routes.each do |name, route|
      add(name, route)
    end
  end

  def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
    reset! if regenerate
    Array(destinations).each do |dest|
      dest.__send__(:include, @module)
    end
  end

  private
    def url_helper_name(name, kind = :url)
      :"#{name}_#{kind}"
    end

    def hash_access_name(name, kind = :url)
      :"hash_for_#{name}_#{kind}"
    end

    def define_named_route_methods(name, route)
      {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
        hash = route.defaults.merge(:use_route => name).merge(opts)
        define_hash_access route, name, kind, hash
        define_url_helper route, name, kind, hash
      end
    end

    def define_hash_access(route, name, kind, options)
      selector = hash_access_name(name, kind)
      @module.module_eval <<-end_eval # We use module_eval to avoid leaks
        def #{selector}(options = nil)
          options ? #{options.inspect}.merge(options) : #{options.inspect}
        end
        protected :#{selector}
      end_eval
      helpers << selector
    end

そういえば、RailsActiveSupport関連の約4000個あるファイルのうち2000個ほどにも目を通してみた。lessで順々に読んでいっただけだけど、何となく面倒なのでTextMateを買ってみた。Emacsと似たようなもんだろうと思ってたけど、結構いろいろ違う。