tapitapi’s blog

1日1杯タピオカ!エンジニア

【Ruby on Rails: cancancan】canの旅 (ソースコードリーディング)第一回

目指せ!ruby&railsマスター!

 

ということで、cancancanのソースコードをちょっとづつ読んでいこうと思いまーす。

プロのコードは勉強になるよね、、、きっと、、、

 

下記でもcancancanは扱ったんですが、権限(このユーザはこのページをみれるけど、別のユーザは見れないなど)を設定できる、めっちゃ便利なgemです。ありがたやーーーー

tapitapi.hatenadiary.com

 

たとえば下記のような、ユーザにお店が紐づいたデータベースの構造の場合、

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の旅は長そうだけど、ちょっとづつ進んでいけたらなと思います。

 

あと、ここの理解違うよーー!など、マスター様からのコメントやご意見お待ちしております。一人で旅するより、パーティの方が楽しい旅になると思うので。

 

おやすみなさいいいいいぃぃぃぃ