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


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

```ruby: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オプションが渡されるブロックが実行される。

```ruby: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
```

```ruby: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などがある。

```ruby:actionpack/lib/action_dispatch/routing/mapper.rb
module HttpHelpers
  def get(*args, &block)
    map_method(:get, args, &block)
  end
  # 省略
end
```

```ruby: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に渡している。

```ruby: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が呼ばれる。


```ruby: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)させている。
３つある理由はそれぞれのクラスが担当する領域が異なるため。

```
# 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という構文解析を行っている。
パスを分解した構造を保持してこれでマッチさせるときに使う？もっと奥まで見る必要がある。

```ruby: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
```

```ruby: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クラスは作成される。

```ruby: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
```

```ruby: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メソッドのエイリアスが作成されている


```ruby: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メソッドが追加される。

```ruby: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
```

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

## 簡易クラス構造

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

```ruby:actionpack/lib/action_dispatch/routing/mapper.rb
Mapper
  Constraints < Routing::Endpoint
  Mapping
    Resource
    SingletonResource < Resource
  Scope
```

```ruby:actionpack/lib/action_dispatch/routing/route_set.rb
RouteSet
  Dispatcher < Routing::Endpoint
  StaticDispatcher < Dispatcher
  NamedRouteCollection
    UrlHelper
      OptimizedUrlHelper < UrlHelper
  Generator
```