Pinto公開に向けて #4 ― コントローラの呼び出し処理を実装した
あらすじ
いじり始めたばかりのRubyで、Pintoというソーシャルブックマークサービスを作ろうとしています。前回はAddressableというライブラリを使って、ルーティング周りを実装しました。
ルーティングというぐらいなのでコントローラ名が動的に決まるわけですが、そのコントローラをどのようにしたら呼び出せるのか分からず、宿題にしました。
クラス名が動的に決まる状況でそのクラスを使う方法
RubyのFAQを見ると、ずばり書いてありました。
- eval(classname).new
- 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みたいに組み込みクラスにメソッドが追加できたりしてギガントタノシスですなあ。