Rails で cancancan と action_args の2つの gem を共存して使う方法

こんにちは、Forkwell 事業部の正徳です。

タイトルにもあるように、Forkwell Jobs の開発では cancancanaction_args の2つの gem を使用しています。この2つの gem を一緒に使う際に問題が起きましたので「問題の紹介」と「解決するコード」を紹介したいと思います。

各 gem の簡単な紹介

知らない方もいると思いますので、各 gem の概要を書いておきます。

cancancan

コードの各所に散らばりがちな権限を Ability という1つのファイルで管理できるようになります。

# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  load_and_authorize_resource

  def create
    if @user.save
      # do something
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :age)
  end
end

action_args

コントローラーで使用する値を引数に定義することで、 params を使わずにコードを書けるようになります。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  # white-lists User model's attributes
  permits :name, :age

  # the given `user` parameter would be automatically permitted by ActionArgs
  def create(user)
    @user = User.new(user)
  end
end

2つの gem を使うときの問題点

前述したコードからも分かるように、それぞれの gem で Strong Parameters に対応する方法が異なります。このため、両方のいいとこ取りをするような下記のコードはエラーになってしまいます。

class UsersController < ApplicationController
  load_and_authorize_resource

  permits :name, :age

  def create
    if @user.save
      # do something
    else
      render :new
    end
  end

  def update(user)
    if @user.update(user)
      # do something
    else
      render :edit
    end
  end
end

こんなコードが書けるようにしたいですよね!というわけで、共存させるようなコードを書きました。

コード

# refs
# https://github.com/CanCanCommunity/cancancan/blob/v1.13.1/lib/cancan/controller_resource.rb#L79
# https://github.com/asakusarb/action_args/blob/v2.0.0/lib/action_args/params_handler.rb#L45
module CanCan
  module ActionArgsEx
    def permits(*attributes)
      super
      UsingActionArgs.include_once(self)
    end
  end

  module UsingActionArgs
    def self.included(klass)
      klass.singleton_class.prepend(ActionArgsEx)
    end

    def self.include_once(klass)
      method_name = "#{klass.controller_name.singularize}_params".to_sym
      return if respond_to?(method_name)

      m = Module.new do
        define_method method_name do
          model_name = klass.instance_variable_get(:@permitting_model_name)
          permitted_attributes = klass.instance_variable_get(:@permitted_attributes)
          key = (model_name || klass.controller_name).singularize.underscore.to_sym
          params[key].permit(*permitted_attributes)
        end
      end
      klass.include(m)
    end
  end
end

Ruby のメタプログラミング力が上がりそうなコードですね!この Module がやっている概要は下記の通りです。

  1. include したクラスの self.permits を上書きする
  2. permits が呼ばれたら、一度だけ 3. 以降の処理を実行する
  3. コントローラーのクラス名から xxx_params というメソッド名を作る
  4. xxx_params のメソッドを持つ Module を動的に生成し、コントローラーで include する

これにより、 load_and_authorize_resourcexxx_params のメソッドを呼び出すことができるため、エラーが起きなくなります。

ちなみに、使い方はコントローラーで include するだけです。

class UsersController < ApplicationController
  include CanCan::UsingActionArgs

  load_and_authorize_resource

  permits :name, :age

  def create
    if @user.save
      # do something
    else
      render :new
    end
  end

  def update(user)
    if @user.update(user)
      # do something
    else
      render :edit
    end
  end
end

cancancan と action_args という2つの gem を組み合わせて使う際の解決方法をご紹介しましたが、いかがだったでしょうか?

このコードが cancancan と action_args を両方とも使っている Rails プロジェクトの役に立てば幸いです。