岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

Pinto公開に向けて #4 ― コントローラの呼び出し処理を実装した

あらすじ

いじり始めたばかりのRubyで、Pintoというソーシャルブックマークサービスを作ろうとしています。前回Addressableというライブラリを使って、ルーティング周りを実装しました。

ルーティングというぐらいなのでコントローラ名が動的に決まるわけですが、そのコントローラをどのようにしたら呼び出せるのか分からず、宿題にしました。

クラス名が動的に決まる状況でそのクラスを使う方法

RubyのFAQを見ると、ずばり書いてありました。

  1. eval(classname).new
  2. Object.const_get(classname).new

1 は簡単ですしネストしたクラス (Net::HTTP など) にも対応できますが、 CGI 環境などで迂闊に使うと危険です。

一方、2 の方法ではネストしたクラスに対応できません。次のようにするとネストしたクラスも扱えるようになります。

# Ruby 1.8 以降でいいなら
c = classname.split(/::/).inject(Object) {|c,name| c.const_get(name) }
c.new

セキュリティを考慮して2を採用することにします。また、コントローラはPinto::Controller::Hogehogeのようにしたいので、ネストも考慮する必要があります。

というわけで、ディスパッチャを書き直しました

# lib/pinto/dispatcher.rb

require 'addressable/uri'
require 'pinto/pathname'
require 'pinto/uri/extract_processor'
require 'rack'

module Pinto
  class Dispatcher
    def call(env)
      uri_templates = {
        'top' =>
          'http://pinto.jp/',
        'index' =>
          'http://pinto.jp/index{lang}',
        'signup_openid' =>
          'http://pinto.jp/signup_forms/openid{lang}',
        'signup_auth' =>
          'http://pinto.jp/signup_forms/auth{lang}{query}',
        'signup_account' =>
          'http://pinto.jp/signup_forms/account{lang}{query}',
        'users' =>
          'http://pinto.jp/users{lang}',
        'user' =>
          'http://pinto.jp/users/{username}{lang}'
      }

      request = Rack::Request.new(env)
      controller, request[:uri_map] = extract(uri_templates, request.url)
      run_controller(controller, request)
    end

    def extract(uri_templates, request_uri)
      uri = Addressable::URI.parse(request_uri)
      uri_templates.each do |controller, template|
        param = uri.extract_mapping(template, Pinto::URI::ExtractProcessor)
        return [controller, param] unless param.nil?
      end
      ['not_found', {}]
    end

    # ここです
    def run_controller(controller, request)
      path = Pathname.new("pinto/controller/#{controller}")
      require path
      path.get_class.run(request)
    end
  end
end

run_controllerでコントローラを呼び出しています。Pathnameインスタンスメソッドget_classを追加しているのが見どころ。

Pathname#get_class

とはいえ、FAQのコードをちょっといじっただけですが。

# lib/pinto/pathname.rb

require 'pathname'
require 'pinto/string'

class Pathname
  def get_class
    self.to_classname.split('::').inject(Object) do |parent, child|
       parent.const_get(child)
    end
  end

  def to_classname
    self.to_str.split('/').map {|part| part.camelize}.join('::')
  end
end
# lib/pinto/string.rb

class String
  def camelize
    self.split('_').map{|word| word.capitalize}.join
  end
end

JavaScriptみたいに組み込みクラスにメソッドが追加できたりしてギガントタノシスですなあ。

それはそれとして

今回はついでに、ディスパッチャの役割を明確にしました。URIからコントローラを決定し、リクエストオブジェクト(Rack::Request)を渡すだけ。それ以外はコントローラに任せようという魂胆です。

次のアクション

  • トップページを表示させる

多言語対応なんかはまだ気にしないで、ほぼ静的なページを表示させるつもりです。ビューの扱いを決めたり、Erubisを触ってみたりするのが目的。