refactoring can definition matching behavior

This commit is contained in:
Ryan Bates 2010-07-20 11:04:03 -07:00
parent 5d8f04363d
commit 60848143b7
4 changed files with 76 additions and 75 deletions

View File

@ -50,12 +50,10 @@ module CanCan
# Also see the RSpec Matchers to aid in testing. # Also see the RSpec Matchers to aid in testing.
def can?(action, subject, *extra_args) def can?(action, subject, *extra_args)
raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
matching_can_definition(action, subject) do |can_definition| match = relevant_can_definitions(action, subject).detect do |can_definition|
unless (can = can_definition.can?(action, subject, extra_args)) == :_NOT_MATCHED can_definition.matches_conditions?(action, subject, extra_args)
return can
end end
end match ? match.base_behavior : false
false
end end
# Convenience method which works the same as "can?" but returns the opposite value. # Convenience method which works the same as "can?" but returns the opposite value.
@ -200,12 +198,12 @@ module CanCan
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that. # determined from that.
def conditions(action, subject, options = {}) def conditions(action, subject, options = {})
matched = matching_can_definition(action, subject) relevant = relevant_can_definitions(action, subject)
unless matched.empty? unless relevant.empty?
if matched.any?{|can_definition| can_definition.only_block? } if relevant.any?{|can_definition| can_definition.only_block? }
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}"
end end
matched.map{|can_definition| relevant.map{|can_definition|
[can_definition.base_behavior, can_definition.conditions(options)] [can_definition.base_behavior, can_definition.conditions(options)]
} }
else else
@ -255,7 +253,7 @@ module CanCan
# Returns the associations used in conditions. This is usually used in the :joins option for a search. # Returns the associations used in conditions. This is usually used in the :joins option for a search.
# See ActiveRecordAdditions#accessible_by for use in Active Record. # See ActiveRecordAdditions#accessible_by for use in Active Record.
def association_joins(action, subject) def association_joins(action, subject)
can_definitions = matching_can_definition(action, subject) can_definitions = relevant_can_definitions(action, subject)
unless can_definitions.empty? unless can_definitions.empty?
if can_definitions.any?{|can_definition| can_definition.only_block? } if can_definitions.any?{|can_definition| can_definition.only_block? }
raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}"
@ -281,19 +279,12 @@ module CanCan
@can_definitions ||= [] @can_definitions ||= []
end end
def matching_can_definition(action, subject) # Returns an array of CanDefinition instances which match the action and subject
if block_given? # This does not take into consideration any hash conditions or block statements
can_definitions.reverse.each do |can_definition| def relevant_can_definitions(action, subject)
can_definitions.reverse.select do |can_definition|
can_definition.expanded_actions = expand_actions(can_definition.actions) can_definition.expanded_actions = expand_actions(can_definition.actions)
if can_definition.matches? action, subject can_definition.relevant? action, subject
yield can_definition
break if can_definition.definitive?
end
end
else
matched = []
matching_can_definition(action, subject){|can_definition| matched << can_definition}
matched
end end
end end

View File

@ -3,7 +3,7 @@ module CanCan
# it holds the information about a "can" call made on Ability and provides # it holds the information about a "can" call made on Ability and provides
# helpful methods to determine permission checking and conditions hash generation. # helpful methods to determine permission checking and conditions hash generation.
class CanDefinition # :nodoc: class CanDefinition # :nodoc:
attr_reader :conditions, :block, :base_behavior, :definitive attr_reader :conditions, :block, :base_behavior
include ActiveSupport::Inflector include ActiveSupport::Inflector
attr_reader :block attr_reader :block
attr_reader :actions attr_reader :actions
@ -21,14 +21,20 @@ module CanCan
@block = block @block = block
end end
def matches?(action, subject) # Matches both the subject and action, not necessarily the conditions
def relevant?(action, subject)
matches_action?(action) && matches_subject?(subject) matches_action?(action) && matches_subject?(subject)
end end
def can?(action, subject, extra_args) # Matches the block or conditions hash
result = can_without_base_behavior?(action, subject, extra_args) def matches_conditions?(action, subject, extra_args)
return :_NOT_MATCHED if result == :_NOT_MATCHED || !result if @block
@base_behavior ? result : !result call_block(action, subject, extra_args)
elsif @conditions.kind_of?(Hash) && subject.class != Class
matches_conditions_hash?(subject)
else
true
end
end end
# Returns a hash of conditions. If the ":tableize => true" option is passed # Returns a hash of conditions. If the ":tableize => true" option is passed
@ -45,10 +51,6 @@ module CanCan
end end
end end
def definitive?
conditions_empty? && @block.nil?
end
def only_block? def only_block?
conditions_empty? && !@block.nil? conditions_empty? && !@block.nil?
end end
@ -83,24 +85,14 @@ module CanCan
@subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) } @subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) }
end end
def can_without_base_behavior?(action, subject, extra_args) def matches_conditions_hash?(subject, conditions = @conditions)
if @block
call_block(action, subject, extra_args)
elsif @conditions.kind_of?(Hash) && subject.class != Class
matches_conditions?(subject)
else
true
end
end
def matches_conditions?(subject, conditions = @conditions)
conditions.all? do |name, value| conditions.all? do |name, value|
attribute = subject.send(name) attribute = subject.send(name)
if value.kind_of?(Hash) if value.kind_of?(Hash)
if attribute.kind_of? Array if attribute.kind_of? Array
attribute.any? { |element| matches_conditions? element, value } attribute.any? { |element| matches_conditions_hash? element, value }
else else
matches_conditions? attribute, value matches_conditions_hash? attribute, value
end end
elsif value.kind_of?(Array) || value.kind_of?(Range) elsif value.kind_of?(Array) || value.kind_of?(Range)
value.include? attribute value.include? attribute

View File

@ -16,56 +16,63 @@ describe CanCan::Ability do
@ability.can?(:foodfight, String).should be_false @ability.can?(:foodfight, String).should be_false
end end
it "should return what block returns on a can call, except for nil and false" do it "should pass true to `can?` when non false/nil is returned in block" do
@ability.can :read, :all @ability.can :read, :all
@ability.can :read, Symbol do |sym| @ability.can :read, Symbol do |sym|
[ sym ] "foo" # TODO test that sym is nil when no instance is passed
end end
@ability.can?(:read, Symbol).should == [ nil ] @ability.can?(:read, :some_symbol).should == true
@ability.can?(:read, :some_symbol).should == [ :some_symbol ]
end end
it "should pass to previous can definition, if block returns false or nil" do it "should pass to previous can definition, if block returns false or nil" do
@ability.can :read, :all @ability.can :read, Symbol
@ability.can :read, Symbol do |sym| @ability.can :read, Integer do |i|
sym i < 5
end end
@ability.can :read, Integer do |i| @ability.can :read, Integer do |i|
i > 0 i > 10
end end
@ability.can?(:read, Symbol).should be_true @ability.can?(:read, Symbol).should be_true
@ability.can?(:read, :some_symbol).should == :some_symbol @ability.can?(:read, 11).should be_true
@ability.can?(:read, 1).should be_true @ability.can?(:read, 1).should be_true
@ability.can?(:read, -1).should be_true @ability.can?(:read, 6).should be_false
end end
it "should pass class with object if :all objects are accepted" do it "should pass class with object if :all objects are accepted" do
@ability.can :preview, :all do |object_class, object| @ability.can :preview, :all do |object_class, object|
[object_class, object] object_class.should == Fixnum
object.should == 123
@block_called = true
end end
@ability.can?(:preview, 123).should == [Fixnum, 123] @ability.can?(:preview, 123)
@block_called.should be_true
end end
it "should pass class with no object if :all objects are accepted and class is passed directly" do it "should pass class with no object if :all objects are accepted and class is passed directly" do
@ability.can :preview, :all do |object_class, object| @ability.can :preview, :all do |object_class, object|
[object_class, object] object_class.should == Hash
object.should be_nil
@block_called = true
end end
@ability.can?(:preview, Hash).should == [Hash, nil] @ability.can?(:preview, Hash)
@block_called.should be_true
end end
it "should pass action and object for global manage actions" do it "should pass action and object for global manage actions" do
@ability.can :manage, Array do |action, object| @ability.can :manage, Array do |action, object|
[action, object] action.should == :stuff
object.should == [1, 2]
@block_called = true
end end
@ability.can?(:stuff, [1, 2]).should == [:stuff, [1, 2]] @ability.can?(:stuff, [1, 2]).should
@ability.can?(:stuff, Array).should == [:stuff, nil] @block_called.should be_true
end end
it "should alias update or destroy actions to modify action" do it "should alias update or destroy actions to modify action" do
@ability.alias_action :update, :destroy, :to => :modify @ability.alias_action :update, :destroy, :to => :modify
@ability.can(:modify, :all) { :modify_called } @ability.can :modify, :all
@ability.can?(:update, 123).should == :modify_called @ability.can?(:update, 123).should be_true
@ability.can?(:destroy, 123).should == :modify_called @ability.can?(:destroy, 123).should be_true
end end
it "should allow deeply nested aliased actions" do it "should allow deeply nested aliased actions" do
@ -77,10 +84,13 @@ describe CanCan::Ability do
it "should return block result for action, object_class, and object for any action" do it "should return block result for action, object_class, and object for any action" do
@ability.can :manage, :all do |action, object_class, object| @ability.can :manage, :all do |action, object_class, object|
[action, object_class, object] action.should == :foo
object_class.should == Fixnum
object.should == 123
@block_called = true
end end
@ability.can?(:foo, 123).should == [:foo, Fixnum, 123] @ability.can?(:foo, 123)
@ability.can?(:bar, Fixnum).should == [:bar, Fixnum, nil] @block_called.should be_true
end end
it "should automatically alias index and show into read calls" do it "should automatically alias index and show into read calls" do
@ -90,10 +100,10 @@ describe CanCan::Ability do
end end
it "should automatically alias new and edit into create and update respectively" do it "should automatically alias new and edit into create and update respectively" do
@ability.can(:create, :all) { :create_called } @ability.can :create, :all
@ability.can(:update, :all) { :update_called } @ability.can :update, :all
@ability.can?(:new, 123).should == :create_called @ability.can?(:new, 123).should be_true
@ability.can?(:edit, 123).should == :update_called @ability.can?(:edit, 123).should be_true
end end
it "should not respond to prepare (now using initialize)" do it "should not respond to prepare (now using initialize)" do
@ -260,7 +270,10 @@ describe CanCan::Ability do
@ability.cannot :read, SqlSanitizer @ability.cannot :read, SqlSanitizer
@ability.can :read, SqlSanitizer, :blocked => false, :user_id => 1 @ability.can :read, SqlSanitizer, :blocked => false, :user_id => 1
@ability.sql_conditions(:read, SqlSanitizer).should == 'blocked=false AND user_id=1' result = @ability.sql_conditions(:read, SqlSanitizer)
result.should include("blocked=false")
result.should include(" AND ")
result.should include("user_id=1")
end end
it "should return `true condition` for single `can` definition in front of default `can` condition" do it "should return `true condition` for single `can` definition in front of default `can` condition" do
@ -287,7 +300,11 @@ describe CanCan::Ability do
@ability.can :read, SqlSanitizer @ability.can :read, SqlSanitizer
@ability.cannot :read, SqlSanitizer, :blocked => true, :user_id => 1 @ability.cannot :read, SqlSanitizer, :blocked => true, :user_id => 1
@ability.sql_conditions(:read, SqlSanitizer).should == 'not (blocked=true AND user_id=1)' result = @ability.sql_conditions(:read, SqlSanitizer)
result.should include("not ")
result.should include("blocked=true")
result.should include(" AND ")
result.should include("user_id=1")
end end
it "should return appropriate sql conditions in complex case" do it "should return appropriate sql conditions in complex case" do

View File

@ -22,6 +22,7 @@ end
class Person class Person
end end
# Maybe we can use ActiveRecord directly here instead of duplicating the behavior
class SqlSanitizer class SqlSanitizer
def self.sanitize_sql(hash_cond) def self.sanitize_sql(hash_cond)
case hash_cond case hash_cond