LoginSignup
22
15

More than 1 year has passed since last update.

Railsのコードを読む ルーティングについて

Last updated at Posted at 2016-11-13

Railsのroutes.rbにルーティングを設定するとrailsの内部ではどのような事が行われているのか調べてみました。
ルーティングには様々なオプションが存在するので、まずは一番シンプルなgetメソッドを使用したロジックを見ていきました。

routes.rb
Rails.application.routes.draw do
  get "users/hello", to: "users#hello"
end

まずはdrawメソッドが呼ばれeval_blockへroutes.rbで定義されたルーティングのブロックがMapperオブジェクト内部でinstance_evalにて実行される。
今回だとgetメソッドに対してpathとtoオプションが渡されるブロックが実行される。

actionpack/lib/action_dispatch/routing/route_set.rb
def draw(&block)
  clear! unless @disable_clear_and_finalize
  eval_block(block)
  finalize! unless @disable_clear_and_finalize
  nil
end
actionpack/lib/action_dispatch/routing/route_set.rb
def eval_block(block)
  mapper = Mapper.new(self)
  if default_scope
    mapper.with_default_scope(default_scope, &block)
  else
    mapper.instance_exec(&block)
  end
end

class Mapper
  # 省略
  def initialize(set)
    @set = set
    @scope = Scope.new(path_names: @set.resources_path_names)
    @concerns = {}
  end
end

HttpHelpersではHTTPプロトコルに対応したメソッドが定義されている。
getの他にpost delete put patchなどがある。

actionpack/lib/action_dispatch/routing/mapper.rb
module HttpHelpers
  def get(*args, &block)
    map_method(:get, args, &block)
  end
  # 省略
end
actionpack/lib/action_dispatch/routing/mapper.rb
def map_method(method, args, &block)
  options = args.extract_options!
  options[:via] = method
  if options.key?(:defaults)
    defaults(options.delete(:defaults)) { match(*args, options, &block) }
  else
    match(*args, options, &block)
  end
  self
end

[1] pry(#<ActionDispatch::Routing::Mapper>)> args
=> ["users/hello"]
[2] pry(#<ActionDispatch::Routing::Mapper>)> options
=> {:to=>"users#hello", :via=>:get}

どのHTTPプロトコルも最終的にはこの汎用的にルーティングを設定出来るmatchメソッドにて作成される
長いメソッドなので、今回使ってないオプション用のロジックや通らない箇所は省略
matchでは指定されたパス、アクション、オプションを設定してからdecomposed_matchに渡している。

actionpack/lib/action_dispatch/routing/mapper.rb
def match(path, *rest)
  if rest.empty? && Hash === path
    # 省略
  else
    options = rest.pop || {}
    paths = [path] + rest
  end

  # pry(#<ActionDispatch::Routing::Mapper>)> options
  # => {:to=>"users#hello", :via=>:get}
  # pry(#<ActionDispatch::Routing::Mapper>)> paths
  # => ["users/hello"]

  # 省略

  controller = options.delete(:controller) || @scope[:controller]
  option_path = options.delete :path
  to = options.delete :to
  via = Mapping.check_via Array(options.delete(:via) {
    @scope[:via]
  })
  formatted = options.delete(:format) { @scope[:format] }
  anchor = options.delete(:anchor) { true }
  options_constraints = options.delete(:constraints) || {}
  path_types = paths.group_by(&:class)

  # pry(#<ActionDispatch::Routing::Mapper>)> controller
  # => nil
  # pry(#<ActionDispatch::Routing::Mapper>)> option_path
  # => nil
  # pry(#<ActionDispatch::Routing::Mapper>)> via
  # => [:get]
  # pry(#<ActionDispatch::Routing::Mapper>)> formatted
  # => nil
  # pry(#<ActionDispatch::Routing::Mapper>)> anchor
  # => true
  # pry(#<ActionDispatch::Routing::Mapper>)> options_constraints
  # => {}
  # pry(#<ActionDispatch::Routing::Mapper>)> path_types
  # => {String=>["users/hello"]}

  path_types.fetch(String, []).each do |_path|
    route_options = options.dup
    if _path && option_path
      # pathオプションを使用した時の非推奨メソッド警告処理
    end
    to = get_to_from_path(_path, to, route_options[:action])
    decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
  end

  # 省略

  self
end

getメソッドから呼び出しているので@scope.scope_levelはnilのため、add_routeが呼ばれる。

actionpack/lib/action_dispatch/routing/mapper.rb
def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
  # pry(#<ActionDispatch::Routing::Mapper>)> path
  # => "users/hello"
  # pry(#<ActionDispatch::Routing::Mapper>)> controller
  # => nil
  # pry(#<ActionDispatch::Routing::Mapper>)> options
  # => {}
  # pry(#<ActionDispatch::Routing::Mapper>)> _path
  # => "users/hello"
  # pry(#<ActionDispatch::Routing::Mapper>)> to
  # => "users#hello"
  # pry(#<ActionDispatch::Routing::Mapper>)> via
  # => [:get]
  # pry(#<ActionDispatch::Routing::Mapper>)> formatted
  # => nil
  # pry(#<ActionDispatch::Routing::Mapper>)> anchor
  # => true
  # pry(#<ActionDispatch::Routing::Mapper>)> options_constraints
  # => {}

  if on = options.delete(:on)
    send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
  else
    case @scope.scope_level
    when :resources
      nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
    when :resource
      member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
    else
      add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
    end
  end
end

Mapping.normalize_pathではMapper.normalize_path(path)、Journey::Router::Utils.normalize_path(path)の順で呼び出してる。
基本的には末尾のスラッシュ削除やパスのフォーマットをここで統一?(normalize)させている。
3つある理由はそれぞれのクラスが担当する領域が異なるため。

# format付きのpathに変換する
# '/users/path' => '/users/path(.:format)'
Mapping.normalize_path(path, format)

# スラッシュをオプション扱いにするためpath最後のスラッシュをフォーマット内へスラッシュを正規表現でgsubで移動している
# /users/hello(.:locale) => /users/hello(/.:locale)
Mapper.normalize_path(path)

# ルートのパスや末尾のスラッシュは削除するなどのフォーマットを整える
# '/users/path/' => '/users/path'
# 'users' => '/users'
# '' => '/'
Journey::Router::Utils.normalize_path(path)

Journey::Parser.parserではraccという構文解析を行っている。
パスを分解した構造を保持してこれでマッチさせるときに使う?もっと奥まで見る必要がある。

actionpack/lib/action_dispatch/routing/mapper.rb
def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
  path = path_for_action(action, _path)
  raise ArgumentError, "path is required" if path.blank?

  action = action.to_s

  default_action = options.delete(:action) || @scope[:action]

  if action =~ /^[\w\-\/]+$/
    default_action ||= action.tr("-", "_") unless action.include?("/")
  else
    action = nil
  end

  as = if !options.fetch(:as, true) # if it's set to nil or false
    options.delete(:as)
  else
    name_for_action(options.delete(:as), action)
  end

  path = Mapping.normalize_path URI.parser.escape(path), formatted
  ast = Journey::Parser.parse path

  mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
  @set.add_route(mapping, ast, as, anchor)
end
actionpack/lib/action_dispatch/routing/route_set.rb
def add_route(mapping, path_ast, name, anchor)
  # 省略

  route = @set.add_route(name, mapping)
  named_routes[name] = route if name

  # 省略

  route
end

mappingクラスはdispatcherやcontroller,actionなどを生成しているのでpathと処理するController actionの情報を保持するRouteクラスを紐付けるためのクラスかな。
気になるのは一つの記述で複数pathを生成するresourcesを作成した時にget post delete patch putの5つが生成されるのかどうか。
おそらく5つRouteクラスは作成される。

actionpack/lib/action_displatch/journey/routes.rb
def add_route(name, mapping)
  route = mapping.make_route name, routes.length
  routes << route
  partition_route(route)
  clear_cache!
  route
end
actionpack/lib/action_dispatch/routing/mapper.rb
def make_route(name, precedence)
  route = Journey::Route.new(name,
                    application,
                    path,
                    conditions,
                    required_defaults,
                    defaults,
                    request_method,
                    precedence,
                    @internal)

  route
end

add_routeの処理が終わると,named_routesにRouteオブジェクトを代入している。
named_routesにはNamedRouteCollectionオブジェクトが代入されており、[]=にaddメソッドのエイリアスが作成されている

actionpack/lib/action_dispatch/routing/route_set.rb
def add_route(mapping, path_ast, name, anchor)
  # 省略

  route = @set.add_route(name, mapping)
  named_routes[name] = route if name

  # 省略

  route
end

define_url_helperでroutes.rbで定義したルーティングのxxx_pathメソッドが追加される。

actionpack/lib/action_dispatch/routing/route_set.rb
def add(name, route)
  key       = name.to_sym
  path_name = :"#{name}_path"
  url_name  = :"#{name}_url"

  if routes.key? key
    @path_helpers_module.send :undef_method, path_name
    @url_helpers_module.send  :undef_method, url_name
  end
  routes[key] = route
  define_url_helper @path_helpers_module, route, path_name, route.defaults, name, path
  define_url_helper @url_helpers_module,  route, url_name,  route.defaults, name, unknown

  @path_helpers << path_name
  @url_helpers << url_name
end

これでルーティングに関するロジックは終わり。
ただ作成したルーティングをどのように保存して、リクエスト時に引っ掛けているかなどの謎はまだ残ってるので、
次はサーバ起動に関するコードか、リクエスト処理をするコードを追ってみようと思います。

簡易クラス構造

インナークラスをインデントで表現しています

actionpack/lib/action_dispatch/routing/mapper.rb
Mapper
  Constraints < Routing::Endpoint
  Mapping
    Resource
    SingletonResource < Resource
  Scope
actionpack/lib/action_dispatch/routing/route_set.rb
RouteSet
  Dispatcher < Routing::Endpoint
  StaticDispatcher < Dispatcher
  NamedRouteCollection
    UrlHelper
      OptimizedUrlHelper < UrlHelper
  Generator
22
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
15