Hatena::Groupdann

dann's blog このページをアンテナに追加 RSSフィード

Fork me on GitHub

2008-12-01

今日の結論

今日の結論 - dann's blog を含むブックマーク はてなブックマーク - 今日の結論 - dann's blog 今日の結論 - dann's blog のブックマークコメント

Routeの実装をみたときの絶望感はすごいですね。にわかperlerが踏み込んではいけない領域な気がします。これrubyistな人は読んでるんですかね...

今日の結論は、railsは表面的なクラス構造は綺麗だけど、深層に入るとカオス過ぎ。

rails--

Railsで学ぶフレームワーク作り - Dispatcher編 その4 Routeの構築部分

| Railsで学ぶフレームワーク作り - Dispatcher編 その4 Routeの構築部分 - dann's blog を含むブックマーク はてなブックマーク - Railsで学ぶフレームワーク作り - Dispatcher編 その4 Routeの構築部分 - dann's blog Railsで学ぶフレームワーク作り - Dispatcher編 その4 Routeの構築部分 - dann's blog のブックマークコメント

config/routes.rbをみると、以下のようなブロックで囲まれています。ここがディスパッチルールの設定部分ですね。

    ActionController::Routing:Routes.draw do |map|
      map.connect ':controller/:action/:id', :controller => 'blog'
    end

RoutesはActiveController::Routing::RouteSetのインスタンスであることは前に説明しました。では、このクラスのdrawメソッドを読んでいきましょう。

drawメソッドは、以下のようになっているので、config/routes.rb のブロック引数 map は Mapper クラスのインスタンスということになりますね。

      def draw
        yield Mapper.new(self)
        install_helpers
      end

ということは、map.connectをよぶと、Routes(RouteSetのインスタンス)に、以下のようにrouteが追加されていくということになりますね。

        def connect(path, options = {}) 
          @set.add_route(path, options)
        end

add_routeの実装をみると、builder(RouteBuilder)によって、routeがbuildされて、RouteSet の routesインスタンス変数に追加されるいう形になってます。

      def add_route(path, options = {}) 
        route = builder.build(path, options)
        routes << route
        route
      end 

RouteBuilderでpathとかの解析やってますね。各種Segmenterで文字列分解してroute作ってます。

      # Construct and return a route with the given path and options.
      def build(path, options)
        # Wrap the path with slashes
        path = "/#{path}" unless path[0] == ?/
        path = "#{path}/" unless path[-1] == ?/

        path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]

        segments = segments_for_route_path(path)
        defaults, requirements, conditions = divide_route_options(segments, options)
        requirements = assign_route_options(segments, defaults, requirements)

        # TODO: Segments should be frozen on initialize
        segments.each { |segment| segment.freeze }

        route = Route.new(segments, requirements, conditions)

        if !route.significant_keys.include?(:controller)
          raise ArgumentError, "Illegal route: the :controller must be specified!"
        end

        route.freeze
      end 

で、ここはさほど難しくないんですが、route.freezeの中身がかなり黒魔術的で、やる気をごっそりもってかれてしまっていたわけです。

routeは、ActionController::Routing::Routeで、

actionpack/lib/action_controller/routing/route.rb

で定義されています。

freezeメソッドの中の実装は、かなり黒魔術的ですが、段々と本丸のDispatch部分に近づいてきたので、めげずに少し何をやっているかくらいはみていきましょう。ここは次回に。

Railsで学ぶフレームワーク作り - Dispatcher編 その3 route設定の初期化部分

| Railsで学ぶフレームワーク作り - Dispatcher編 その3 route設定の初期化部分 - dann's blog を含むブックマーク はてなブックマーク - Railsで学ぶフレームワーク作り - Dispatcher編 その3 route設定の初期化部分 - dann's blog Railsで学ぶフレームワーク作り - Dispatcher編 その3 route設定の初期化部分 - dann's blog のブックマークコメント

route設定の初期化してるところから読んできますか。routing, initあたりでackで調べると、それっぽいメソッドありますね。以下のメソッドのようですね

railties/lib/initializer.rb

    def initialize_routing
      return unless configuration.frameworks.include?(:action_controller)

      ActionController::Routing.controller_paths += configuration.controller_paths
      ActionController::Routing::Routes.add_configuration_file(configuration.routes_configuration_file)
      ActionController::Routing::Routes.reload
    end  

Routesなんてクラスない!と思ったら、

actionpack/lib/action_controller/routing.rb

371:    Routes = RouteSet.new

ActionController::Routing::RouteSetのようですね。

reloadメソッド探すと、以下のようになってます。

routesが更新されてるかチェックしてからloadメソッドよんでますね。

      def reload
        if configuration_files.any? && @routes_last_modified
          if routes_changed_at == @routes_last_modified
            return # routes didn't change, don't reload
          else
            @routes_last_modified = routes_changed_at
          end
        end

        load!
      end 

load読んでみると、最終的にload_routes!呼んでますね。これが肝ですかね。

      def load!
        Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
        clear!
        load_routes!
      end 

ようやくconfiguration fileをloadするところまできました。

      def load_routes!
        if configuration_files.any?
          configuration_files.each { |config| load(config) }
          @routes_last_modified = routes_changed_at
        else
          add_route ":controller/:action/:id"
        end
      end

Railsで学ぶフレームワーク作り - Dispatcher編 その2 Routing::Routes

Railsで学ぶフレームワーク作り - Dispatcher編 その2 Routing::Routes - dann's blog を含むブックマーク はてなブックマーク - Railsで学ぶフレームワーク作り - Dispatcher編 その2 Routing::Routes - dann's blog Railsで学ぶフレームワーク作り - Dispatcher編 その2 Routing::Routes - dann's blog のブックマークコメント

前回DispatcherだとわかったRouting::Routesのrecognizeメソッドの実装をみていきましょう。

      def recognize(request)
        params = recognize_path(request.path, extract_request_environment(request))
        request.path_parameters = params.with_indifferent_access
        "#{params[:controller].camelize}Controller".constantize
      end

request.pathはまぁ普通のフレームワークと変わらないですね。

extract_request_environmentはメソッドを取り出しているだけ。

      def extract_request_environment(request)
        { :method => request.method }
      end

その後、pathの解析結果からcontrollerのクラス名を取り出して返すのような実装ですね。

recognize_pathの実装は、以下のような感じになってます。

actionpack/lib/action_controller/routing/recognition_optimisation.rb

    class RouteSet
      def recognize_path(path, environment={})
        result = recognize_optimized(path, environment) and return result

        # Route was not recognized. Try to find out why (maybe wrong verb).
        allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } } 

        if environment[:method] && !HTTP_METHODS.include?(environment[:method])
          raise NotImplemented.new(*allows)
        elsif !allows.empty?
          raise MethodNotAllowed.new(*allows)
        else
          raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
        end
      end 

recognize_optimizedメソッドを読もうかと思ったんですが、この周辺のコードはひどく黒魔術ですね... うーん... なんかこれは踏み込んではいけないような...

一旦、Routesの設定の読み込みあたりからにしたほうがよさそうですね。ということで、次はRoute設定の読み込み部分を。

Railsで学ぶフレームワーク作り - Dispatcher編 その1

Railsで学ぶフレームワーク作り - Dispatcher編 その1 - dann's blog を含むブックマーク はてなブックマーク - Railsで学ぶフレームワーク作り - Dispatcher編 その1 - dann's blog Railsで学ぶフレームワーク作り - Dispatcher編 その1 - dann's blog のブックマークコメント

フレームワーク作りの参考にRailsのコードを読んでみる事にしました。

Rails使ってたのは1.0のころなので、もうすっかり忘れてますが。

actionpack/lib/action_controller/dispatcher.rb がいかにもって感じなので、そこから読んでみることに。

確かRailsRack対応なんですよね、ってことで読んでみるとcallメソッドがありますね。callがRackの入り口ですね。Rackのrequest, response作ってdispatchすると。

    def call(env)
      @request = RackRequest.new(env)
      @response = RackResponse.new(@request)
      dispatch
    end

dispatch部分は、一応マルチスレッド対応のコードがあるようだけど、

ここはパスして、dispatch_unlockedに。

    def dispatch
      if ActionController::Base.allow_concurrency
        dispatch_unlocked
      else
        @@guard.synchronize do
          dispatch_unlocked
        end
      end
    end

dispatch前後にcallbackを用意してますね。何に使うのかはまだわからないですが、これはまたあとで。handle_requestで、request処理をと。


    def dispatch_unlocked
      begin
        run_callbacks :before_dispatch
        handle_request
      rescue Exception => exception
        failsafe_rescue exception
      ensure
        run_callbacks :after_dispatch, :enumerator => :reverse_each
      end
    end

handle_requestを見ると、Routing::RoutesがDispatcherになってるみたいですね。

Routes系っぽく、Controllerを見つけて、見つけたControllerに処理委譲してみたいですね。このRequestがResponseが

    protected
      def handle_request
        @controller = Routing::Routes.recognize(@request)
        @controller.process(@request, @response).out(@output)
      end 

なんだか、HTTP::Engineでそのまま処理できそうな気がしてきますよねー。

では次は、いかにもDispatcherなRouting::Routesみていきます。

これがHTTP::Routerが参考にしてるDispatcherってことでしょうね、多分。