Railsのroutes.rbにルーティングを設定するとrailsの内部ではどのような事が行われているのか調べてみました。
ルーティングには様々なオプションが存在するので、まずは一番シンプルなgetメソッドを使用したロジックを見ていきました。
Rails.application.routes.draw do
get "users/hello", to: "users#hello"
end
まずはdrawメソッドが呼ばれeval_blockへroutes.rbで定義されたルーティングのブロックがMapperオブジェクト内部でinstance_evalにて実行される。
今回だとgetメソッドに対してpathとtoオプションが渡されるブロックが実行される。
def draw(&block)
clear! unless @disable_clear_and_finalize
eval_block(block)
finalize! unless @disable_clear_and_finalize
nil
end
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などがある。
module HttpHelpers
def get(*args, &block)
map_method(:get, args, &block)
end
# 省略
end
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に渡している。
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が呼ばれる。
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という構文解析を行っている。
パスを分解した構造を保持してこれでマッチさせるときに使う?もっと奥まで見る必要がある。
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
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クラスは作成される。
def add_route(name, mapping)
route = mapping.make_route name, routes.length
routes << route
partition_route(route)
clear_cache!
route
end
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メソッドのエイリアスが作成されている
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メソッドが追加される。
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
これでルーティングに関するロジックは終わり。
ただ作成したルーティングをどのように保存して、リクエスト時に引っ掛けているかなどの謎はまだ残ってるので、
次はサーバ起動に関するコードか、リクエスト処理をするコードを追ってみようと思います。
簡易クラス構造
インナークラスをインデントで表現しています
Mapper
Constraints < Routing::Endpoint
Mapping
Resource
SingletonResource < Resource
Scope
RouteSet
Dispatcher < Routing::Endpoint
StaticDispatcher < Dispatcher
NamedRouteCollection
UrlHelper
OptimizedUrlHelper < UrlHelper
Generator