Class: Credentials::Rule

Inherits:
Object
  • Object
show all
Defined in:
lib/credentials/rule.rb

Overview

Specifies an individual ‘line’ in a Rulebook. Trivially subclassed as Credentials::Rule::AllowRule and Credentials::Rule::DenyRule, but this is just to allow you to create your own, more complex rule types.

Direct Known Subclasses

AllowRule, DenyRule

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Rule

Returns a new instance of Rule.



10
11
12
13
# File 'lib/credentials/rule.rb', line 10

def initialize(*args)
  self.options = args.last.is_a?(Hash) ? args.pop : {}
  self.parameters = args
end

Instance Attribute Details

#optionsObject

Returns the value of attribute options.



8
9
10
# File 'lib/credentials/rule.rb', line 8

def options
  @options
end

#parametersObject

Returns the value of attribute parameters.



7
8
9
# File 'lib/credentials/rule.rb', line 7

def parameters
  @parameters
end

Instance Method Details

#arityObject

Returns the number of arguments expected in a test to this rule. This is really basic, but allows a quick first-pass filtering of the rules.



18
19
20
# File 'lib/credentials/rule.rb', line 18

def arity
  parameters.length
end

#evaluate_condition(conditions, op, *args) ⇒ Object

Evaluates an if or unless condition.

conditions

One or more conditions to evaluate. Can be symbols or procs.

op

Operator used to combine the results (+|+ for if, & for unless).

args

List of arguments to test with/against



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/credentials/rule.rb', line 75

def evaluate_condition(conditions, op, *args)
  receiver = args.shift
  args.reject! { |arg| arg.is_a? Symbol }
  Array(conditions).inject(op == :| ? false : true) do |memo, condition|
    memo = memo.send op, case condition
    when Symbol
      return false unless receiver.respond_to? condition
      !!receiver.send(condition, *args[0, receiver.method(condition).arity])
    when Proc
      raise ArgumentError, "wrong number of arguments to condition (#{args.size + 1} to #{condition.arity})" unless args.size + 1 == condition.arity
      !!condition.call(receiver, *args)
    else
      raise ArgumentError, "invalid :if or :unless option (expected Symbol or Proc, or array thereof; got #{condition.class})"
    end
  end
end

#evaluate_preposition(object, expected, actual) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/credentials/rule.rb', line 92

def evaluate_preposition(object, expected, actual)
  return true if expected.nil?
  return false if actual.nil?
  return true if expected === actual
  
  single = expected.to_s
  plural = single.respond_to?(:pluralize) ? single.pluralize : single + "s"
  
  lclass, rclass = [ object.class.name, actual.class.name ].map do |s|
    s.gsub(/^.*::/, '').
      gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
      gsub(/([a-z\d])([A-Z])/,'\1_\2').
      tr("-", "_").
      downcase
  end

  if object.respond_to?(:id) && actual.respond_to?(:"#{lclass}_id")
    return true if actual.send(:"#{lclass}_id").to_i == object.id.to_i
  end
  
  if actual.respond_to?(:id) && object.respond_to?(:"#{rclass}_id")
    return true if object.send(:"#{rclass}_id").to_i == actual.id.to_i
  end
  
  (object.respond_to?(single) && (object.send(single) == actual)) || (object.respond_to?(plural) && (object.send(plural).include?(actual)))
end

#match?(*args) ⇒ Boolean

Returns true if the given arguments match this rule. Rules mostly use the same matching criteria as Ruby’s case statement: that is, the === operator. Remember:

User === User.new   # a class matches an instance
/\w+/ === "abc"     # a RegExp matches a valid string
(1..5) === 3        # a Range matches a number
:foo === :foo       # anything matches itself

There are two exceptions to this behaviour. Firstly, if the rule specifies an array, then the argument will match any element of that array:

class User
  credentials do |user|
    user.can [ :punch, :fight ], [ :shatner, :gandhi ]
  end
end

user.can? :fight, :gandhi  # => true

Secondly, specifying :self in a rule is a nice shorthand for specifying an object’s permissions on itself:

class User
  credentials do |user|
    user.can :fight, :self  # SPOILER ALERT
  end
end

Returns:

  • (Boolean)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/credentials/rule.rb', line 49

def match?(*args)
  values = args.last.is_a?(Hash) ? args.pop : {}
  
  return false unless arity == args.length

  parameters.zip(args).each do |expected, actual|
    case expected
    when :self then return false unless actual == args.first
    when Array then return false unless expected.any? { |item| (item === actual) || (item == :self && actual == args.first) }
    else return false unless expected == actual || expected === actual || (expected.is_a?(Class) && actual.is_a?(Class) && actual.ancestors.include?(expected))
    end
  end
  
  result = true
  result = result && (options.keys & Credentials::Prepositions).inject(true) { |memo, key| memo && evaluate_preposition(args.first, options[key], values[key]) }
  result = result && evaluate_condition(options[:if], :|, *args) unless options[:if].nil?
  result = result && !evaluate_condition(options[:unless], :&, *args) unless options[:unless].nil?
  result
end