【Ruby on Rails: cancancan】canの旅 (ソースコードリーディング)第一回
ということで、cancancanのソースコードをちょっとづつ読んでいこうと思いまーす。
プロのコードは勉強になるよね、、、きっと、、、
下記でもcancancanは扱ったんですが、権限(このユーザはこのページをみれるけど、別のユーザは見れないなど)を設定できる、めっちゃ便利なgemです。ありがたやーーーー
たとえば下記のような、ユーザにお店が紐づいたデータベースの構造の場合、
user table: id, role, profile
shop table: id, name, user_id
下記のように書くことで、自分のuserレコードのときと、自分のshopレコードのときだけ操作可能になります。
can :manage, User, id: user.id
can :manage, Shop, user_id: user.id
今回扱うのは、権限設定(今回はcan method)してから、実際に権限チェックするまで、どんな旅をしているのかザックリ把握!
1.
まず入口になるメインのファイルはability.rbみたいですね。
cancancan/ability.rb at develop · CanCanCommunity/cancancan · GitHub
module Ability
include CanCan::Ability::Rules
include CanCan::Ability::Actions
include CanCan::UnauthorizedMessageResolver
include StrongParameterSupport(省略)
def can(action = nil, subject = nil, *attributes_and_conditions, &block)
add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
end
Abilityモジュールの中にcan メソッドが定義されてた!
Rubyでモジュールは、複数のクラスにまたがって使用されるものを定義する場所。
Cでいう、libraryのような立ち位置かなーって勝手に思ってます。
クラスだと一つのクラスしか継承できない(複数のクラスは継承できない)から、複数のクラスにまたがってmix-inされるものはモジュールに書くっていう認識。
can :manage, Shop, user_id: user.id の例だと、canメソッドの引数は
action = :manage,
subject = Shop,
*attributes_and_conditions = user_id: user.id って感じですね。
どうしてもメソッドには括弧をつけたくなるけど、
(test_function(a,b)みたいに。)
rubyは省くものは徹底的に省く!!括弧つけない!!リターンもかかない!!っていう思想みたいなので、、、慣れですかね。(括弧つけてもリターンつけても、動くは動く)
*attributes_and_conditionsのアスタリスク(*)は可変長引数っていうやつ。要するに、引数の数がいくつか分からないときにアスタリスク(*)を付けちゃえば、引数何個でもOKだよーってやつ。べんりーー
最後の引数である&blockがラスボス。実はcanメソッドは、ブロック(rubyでloop文の事)も渡すことができる!!下記のような使い方ができます。
can :read, Photo, Photo.left_joins(:groups).where(groups: { id: nil }) do |photo|
photo.groups.empty?
end
canメソッドの引数にブロックが入っていた時は、&blockに格納されてcan メソッドに渡されるってわけ。
それでは次に、can メソッド内で使用されているRuleクラスについて、見ていきます。
def can(action = nil, subject = nil, *attributes_and_conditions, &block)
add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
end
RuleクラスはRule.rbファイル内に定義されていました。
cancancan/rule.rb at develop · CanCanCommunity/cancancan · GitHub
class Rule
include ConditionsMatcher
include ParameterValidators
attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
attr_writer :expanded_actions, :conditions(省略)
def initialize(base_behavior, action, subject, *extra_args, &block)
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
condition_and_block_check(extra_args, block, action, subject)
@match_all = action.nil? && subject.nil?
raise Error, "Subject is required for #{action}" if action && subject.nil?@base_behavior = base_behavior
@actions = Array(action)
@subjects = Array(subject)
@attributes = Array(attributes)
@conditions = extra_args || {}
@block = block
end
include ConditionsMatcher
include ParameterValidators
の部分で、ConditionsMatcherモジュールとParameterValidatorsモジュールが読み込まれていますねー。
ちゃんと理解するには、このふたつのモジュールをコードリーディングする必要があるんだけど、、、めんどいので今回はスキップ!
attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
attr_writer :expanded_actions, :conditions
これらは生成されたオブジェクトから読めるインスタンス変数は「@base_behavior, @subjects、@actions、@conditions、@attributes」で、
オブジェクトから書き換えられるインスタンス変数は「@expanded_actions、@conditions」ですよーってこと。
まあ、cancancanを使うユーザ(私たち)はインスタンス変数は変更しないので、今のところ意識しなくてもいいんじゃないかな、、
おそらく、ほかのクラスがこれらのインスタンス変数を参照したり、書き換えたりできるように書いているんだと思います。
initializeメソッドを見て行きましょー!
initializeメソッドはnewが呼ばれてオブジェクトを生成するときに、必ず実行されるメソッド。C++とかでいうとコンストラクタですね。
何してるかなーー??ってみてみると、
def initialize(base_behavior, action, subject, *extra_args, &block)
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
condition_and_block_check(extra_args, block, action, subject)
@match_all = action.nil? && subject.nil?
raise Error, "Subject is required for #{action}" if action && subject.nil?@base_behavior = base_behavior
@actions = Array(action)
@subjects = Array(subject)
@attributes = Array(attributes)
@conditions = extra_args || {}
@block = block
end
さっきの例の、can :manage, Shop, user_id: user.idが
Rule.new(true, action, subject, *attributes_and_conditions, &block)
で渡ってきたと考えると、
base_behavior= true,
action=:manage,
subject=Shop,
*extra_args=user_id: user.id
&block=nil
になって、引数としてinitializeメソッドに入ってきました。
そしたら、、、下記に出会う、、、
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
condition_and_block_check(extra_args, block, action, subject)
これはめんどくさいってスキップしたConditionsMatcherモジュールとParameterValidatorsモジュール内のメソッド、、、
でも大丈夫!なんとなく予測でこんなことしてる!ってわかるぞー!
parse_attributes_from_extra_argsで、extra_argsにいっぱい入ってた引数を振り分けてる。第一戻り値がattributes、第二戻り値が extra_argsに格納される(予想です)
さっきは、*extra_args=user_id: user.idだったけど、たぶんattributes=user_id: user.idになって、extra_args=nilになるんじゃないかな
condition_and_block_check(extra_args, block, action, subject)
もなんかよくわかんないけども、おそらく引数がただしいかチェックして、だめだったらエラー出す、いわゆるバリデーションの部分なんじゃないかなー(予想です)
良いコードのいいところは、メソッド名で何するメソッドか見当がつくこと!
見習わねば、、、
@match_all = action.nil? && subject.nil?
raise Error, "Subject is required for #{action}" if action && subject.nil?
この部分では、actionとsubjectがどっちもnilなら@match_allにtrueが入る。
でも、今回はちゃんと
action=:manage,
subject=Shop,
で値が入ってるから、@match_allはfalseになるね!
raise Error, "Subject is required for #{action}" if action && subject.nil?
この部分ではif action && subject.nil? つまりactionもsubjectもnilならエラーを出してる。
なんで@match_allと同じことしてるのに@match_allをif文で使わないのか謎だけど、、、
@base_behavior = base_behavior
@actions = Array(action)
@subjects = Array(subject)
@attributes = Array(attributes)
@conditions = extra_args || {}
@block = block
この、initializeの最後の部分は、今まで取って来た引数をインスタンス変数に入れてるだけ。
( ´Д`)=3 フゥ 運動不足の自分には、そろそろ体力の限界が、、、
まだまだcanの旅は長そうだけど、ちょっとづつ進んでいけたらなと思います。
あと、ここの理解違うよーー!など、マスター様からのコメントやご意見お待ちしております。一人で旅するより、パーティの方が楽しい旅になると思うので。
おやすみなさいいいいいぃぃぃぃ