はっさんブログ

技術的なまとめ・話題になっているもの・やっていきを配信する

【Rails】ルーティングのリファクタリング【config/routes.rb】

参考になったらシェアいただけると幸いです!

問題となる背景

Railsアプリの開発を続けていくとconfig/routes.rbが肥大化して見辛くなります。

例えば...

# config/routes.rb

require 'sidekiq/web'

Rails.application.routes.draw do

  # devise not support omniauth with locale
  devise_for :user, only: [:omniauth_callbacks], controllers: {
    omniauth_callbacks: 'user/omniauth_callbacks'
  }

  scope '(:locale)', locale: /#{I18n.available_locales.map(&:to_s).join('|')}/ do
    root 'videos#index'

    devise_for :user, skip: [:sessions, :omniauth_callbacks], controllers: {
      passwords:          'user/passwords',
      registrations:      'user/registrations',
      confirmations:      'user/confirmations',
    }

    devise_scope :user do
      get    'login',  to: 'user/sessions#new',     as: :new_user_session
      post   'login',  to: 'user/sessions#create',  as: :user_session
      delete 'logout', to: 'user/sessions#destroy', as: :destroy_user_session

      get 'terms_of_service', to: 'user/registrations#terms_of_service', as: :terms_of_service
      get 'privacy', to: 'user/registrations#privacy', as: :privacy
    end

    resources :users,  only: [:update, :destroy]

    resource :user do
      resources :videos
      get '/support',  to: 'user#support'
      get '/mypage',   to: 'user#edit'
    end

    devise_for :admin, controllers: {
      sessions:      'admin/sessions'
    }

    resource :admin do
      resources :videos
    end

    resources :videos, only: [:show, :create, :destroy] do
      resource :good,    only: [:create, :destroy]
      resource :bad,     only: [:create, :destroy]
      resources :comments, only: [:create, :destroy, :update]
     
      # videos.idを伴わないパスを生成
      collection do
        resources :categories, only: [:index]
        get '/search',  to: 'videos#search', as: :search
      end
    end

    resource :opinion, only: [:create]

    mount FileUploader::UploadEndpoint => "/images" # s3 direct upload

    mount Sidekiq::Web => '/sidekiq'

    Sidekiq::Web.use Rack::Auth::Basic do |username, password|
      username == 'xxx' && password == 'xxx'
    end unless Rails.env.development?

    mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
  end
end

初見の体験がよろしくないですね。

リソースごとにファイルを切り分け、ルーティングを管理することはできないでしょうか?

解決策

Rubyのinstance_eval メソッドを用います。

instance_eval については以下の記事が参考になりました。

Rubyのinstance_evalメソッドについて分かってきたような気もするが疑問も増えた

insance_eval を用いると以下のように書くことができます。

require 'sidekiq/web'

Rails.application.routes.draw do
  def draw(resource_name)
    instance_eval(File.read(Rails.root.join("config/routes/#{resource_name}.rb")))
  end

  # devise not support omniauth with locale
  devise_for :user, only: [:omniauth_callbacks], controllers: {
    omniauth_callbacks: 'user/omniauth_callbacks'
  }

  scope '(:locale)', locale: /#{I18n.available_locales.map(&:to_s).join('|')}/ do
    root 'videos#index'

    draw :user
    draw :admin
    draw :video
    draw :third_party

    resource :opinion, only: [:create]
  end
end
# => 追加

config/routes/user.rb
config/routes/admin.rb
config/routes/video.rb
config/routes/third_party.rb

上記の例を用いてconfig/routes/videos.rbに移したものは以下です。

# config/routes/video.rb

resources :videos, only: [:show, :create, :destroy] do
  resource :good,    only: [:create, :destroy]
  resource :bad,     only: [:create, :destroy]
  resources :comments, only: [:create, :destroy, :update]
     
  # videos.idを伴わないパスを生成
  collection do
    resources :categories, only: [:index]
    get '/search',  to: 'videos#search', as: :search
  end
end

元々config/routes.rbに書いていたものをそのまま移せばOKです。

リソースを新規追加することになった際にはdraw: resource_nameを追加し、config/routes/resource_name.rbのファイルを作成してそちらにルーティングを書いていきます。

これにより、本体のconfig/routes.rb が肥大化していくことを抑えることができます。

ちなみにRails.application.draw

rails/actionpack/lib/action_dispatch/routing/route_set.rb

https://github.com/rails/rails/blob/ff7f4a70e1aaa9fd38e7a8ab457d09601f6a1d2d/actionpack/lib/action_dispatch/routing/route_set.rb#L406-L411

Railsで普段使っているようにパスとアプリのマッピングをしてくれています。

require 'action_dispatch'
header = {'Content-Type' => 'text/html'}
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
get 'hello', to: -> env { [200, header, ['hello']] }
get 'goodbye', to: -> env { [200, header, ['goobye']] }
root to: -> env { [200, header, ['Hiroshima Okayama rb']]}
end
Rack::Handler.default.run routes, Port: 3000
view raw app.rb hosted with ❤ by GitHub
gist.github.com

参考: http://blog.eiel.info/blog/2014/03/30/action-dispatch/

所感

Railsにおけるルーティングのリファクタリング方法を書きました。

あまり話題に上がらないテーマですが、皆さんはどのように管理しているのでしょうか。