railsのpath名の決まり方

結論から言え!

railsでroute.rbにresourcesを使ってrouteを生やすときにcollectもしくはmemberを使う場合、 path名にcreate new show update destroyのどれかを使うとpathの名前が狂う

困ったこと

かなーりレアケースなのですがこの間、resourcesで定義してあるroutingに新たにリソース作成のrouteを追加したい、と言う状況になりました

使用するメソッドは普通の場合と同じでcreateなので create と言うrouteをcollectionを使って新たに作成しました

config/routes.rb

Rails.application.routes.draw do
  resources :users
end

もともとこうなっていたものを

config/routes.rb

Rails.application.routes.draw do
  resources :users do
    collection do
      post 'create', action: 'create'
    end
  end
end

これで /users/create にpostすると新たにuserが作成できるようになるはず

と思って rake routes してroutingを確認するとpath名がおかしなことになっていました

f:id:poul8et6:20191207181710p:plain

、、ん?

一番上がこうなってます

Prefix  Verb     URI Pattern            Controller#Action
users   POST   /users/create(.:format)  users#create

新たに作成した /users/create と言うpathにusersって言うprefixがついてる、、

つまりこれは redirect_to users_path とか書くと /users/create をpostしにいきます、非常にまずい

原因

routingのprefixがどこで呼ばれてるのか調べてみました

rails内の name_for_action と言うmethodでprefixは作られています

github.com

def name_for_action(as, action)
   prefix = prefix_name_for_action(as, action)
   name_prefix = @scope[:as]

   if parent_resource
       return nil unless as || action

       collection_name = parent_resource.collection_name
       member_name = parent_resource.member_name
   end

   action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
   candidate = action_name.select(&:present?).join("_")

   unless candidate.empty?
       # If a name was not explicitly given, we check if it is valid
       # and return nil in case it isn't. Otherwise, we pass the invalid name
       # forward so the underlying router engine treats it and raises an exception.
       if as.nil?
           candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
       else
           candidate
       end
   end
end

resourcesでprefixが作られる流れとしては

  1. CANONICAL_ACTIONS = %w(index create new show update destroy)の要素が一つづつactionとしてname_for_actionメソッドに渡される

  2. prefix_name_for_actionにasとactionが渡され、

    • as があればprefixに代入される
    • as がない場合actionがCANONICAL_ACTIONSに含まれていなければprefixに代入される
  3. collection_nameとmember_nameにresourcesに渡したmodel名の複数形と単数形がそれぞれ代入される

  4. action_nameメソッドが呼ばれ、scope_levelによってどの変数を組み合わせてprefixを作るかが決定する action_nameメソッドは以下の通り

  5. action_nameメソッドで組み合わせが決まったので、それを-で繋いでprefixとする。すでに同じ名前のものがあれば先に作られたものを優先する

def action_name(name_prefix, prefix, collection_name, member_name)
    case scope_level
        when :nested
            [name_prefix, prefix]
        when :collection
            [prefix, name_prefix, collection_name]
        when :new
            [prefix, :new, name_prefix, member_name]
        when :member
            [prefix, name_prefix, member_name]
        when :root
            [name_prefix, collection_name, prefix]
        else
           [name_prefix, member_name, prefix]
    end
end

です。

で、なぜ 'users/create' に users-path が紐づいてしまったかと言うと

  1. CANONICAL_ACTIONSよりも先にcollectionで指定された 'create' アクションがname_for_actionに渡される

  2. 'create'はCANONICAL_ACTIONSに含まれるのでprefixはnullになる

  3. collection_nameはusers、member_nameはuser

  4. action_nameメソッド内ではwhen :collectionに分岐し、[prefix, name_prefix, collection_name]でprefixを作成

  5. prefix = nil、name_prefix = nil, collection_name = usersなのでここでprefix: users, uri : /users/create, action: createの謎のroutingが爆誕する

と言う訳でした

解決策

要はaction名が'create'なのがいけないので'/create'にしてみました

/はprefix_name_for_actionの一番最後で外されるので新たに作成したrouteは

create-users-path と言う名前になります

f:id:poul8et6:20191207191032p:plain

これはダメ、users_pathがmethod: post, url: 'users/create', action: 'create'になる

resources :users do collection do post 'create', action: 'create' end end

これはOK、users_pathがmethod: get, url: 'users', action: 'index'になる resources :users do collection do post '/create', action: 'create' end end

rails/mapper.rb at 97b08334589cf15e86b5c89e13b62ac39e910d34 · rails/rails · GitHub

resourcesで作られるroute

CANONICAL_ACTIONS = %w(index create new show update destroy)

が順番にactionとして

name_name_for_actions(as, action) メソッドに渡される

めでたしめでたし