state_machine 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +26 -0
- data/README.rdoc +254 -46
- data/Rakefile +29 -3
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.jpg +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/lib/state_machine.rb +161 -116
- data/lib/state_machine/assertions.rb +21 -0
- data/lib/state_machine/callback.rb +168 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +135 -101
- data/lib/state_machine/extensions.rb +83 -0
- data/lib/state_machine/guard.rb +115 -0
- data/lib/state_machine/integrations/active_record.rb +242 -0
- data/lib/state_machine/integrations/data_mapper.rb +198 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +153 -0
- data/lib/state_machine/integrations/sequel.rb +169 -0
- data/lib/state_machine/machine.rb +746 -352
- data/lib/state_machine/transition.rb +104 -212
- data/test/active_record.log +34865 -0
- data/test/classes/switch.rb +11 -0
- data/test/data_mapper.log +14015 -0
- data/test/functional/state_machine_test.rb +249 -15
- data/test/sequel.log +3835 -0
- data/test/test_helper.rb +3 -12
- data/test/unit/assertions_test.rb +13 -0
- data/test/unit/callback_test.rb +189 -0
- data/test/unit/eval_helpers_test.rb +92 -0
- data/test/unit/event_test.rb +247 -113
- data/test/unit/guard_test.rb +420 -0
- data/test/unit/integrations/active_record_test.rb +515 -0
- data/test/unit/integrations/data_mapper_test.rb +407 -0
- data/test/unit/integrations/sequel_test.rb +244 -0
- data/test/unit/invalid_transition_test.rb +1 -1
- data/test/unit/machine_test.rb +1056 -98
- data/test/unit/state_machine_test.rb +14 -113
- data/test/unit/transition_test.rb +269 -495
- metadata +44 -30
- data/test/app_root/app/models/auto_shop.rb +0 -34
- data/test/app_root/app/models/car.rb +0 -19
- data/test/app_root/app/models/highway.rb +0 -3
- data/test/app_root/app/models/motorcycle.rb +0 -3
- data/test/app_root/app/models/switch.rb +0 -23
- data/test/app_root/app/models/switch_observer.rb +0 -20
- data/test/app_root/app/models/toggle_switch.rb +0 -2
- data/test/app_root/app/models/vehicle.rb +0 -78
- data/test/app_root/config/environment.rb +0 -7
- data/test/app_root/db/migrate/001_create_switches.rb +0 -12
- data/test/app_root/db/migrate/002_create_auto_shops.rb +0 -13
- data/test/app_root/db/migrate/003_create_highways.rb +0 -11
- data/test/app_root/db/migrate/004_create_vehicles.rb +0 -16
- data/test/factory.rb +0 -77
@@ -0,0 +1,67 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Provides a set of helper methods for evaluating methods within the context
|
3
|
+
# of an object.
|
4
|
+
module EvalHelpers
|
5
|
+
# Evaluates one of several different types of methods within the context
|
6
|
+
# of the given object. Methods can be one of the following types:
|
7
|
+
# * Symbol
|
8
|
+
# * Method / Proc
|
9
|
+
# * String
|
10
|
+
#
|
11
|
+
# == Examples
|
12
|
+
#
|
13
|
+
# Below are examples of the various ways that a method can be evaluated
|
14
|
+
# on an object:
|
15
|
+
#
|
16
|
+
# class Person
|
17
|
+
# def initialize(name)
|
18
|
+
# @name = name
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def name
|
22
|
+
# @name
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class PersonCallback
|
27
|
+
# def self.run(person)
|
28
|
+
# person.name
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# person = Person.new('John Smith')
|
33
|
+
#
|
34
|
+
# evaluate_method(person, :name) # => "John Smith"
|
35
|
+
# evaluate_method(person, PersonCallback.method(:run)) # => "John Smith"
|
36
|
+
# evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith"
|
37
|
+
# evaluate_method(person, lambda {|person| person.name}) # => "John Smith"
|
38
|
+
# evaluate_method(person, '@name') # => "John Smith"
|
39
|
+
#
|
40
|
+
# == Additional arguments
|
41
|
+
#
|
42
|
+
# Additional arguments can be passed to the methods being evaluated. If
|
43
|
+
# the method defines additional arguments other than the object context,
|
44
|
+
# then all arguments are required.
|
45
|
+
#
|
46
|
+
# For example,
|
47
|
+
#
|
48
|
+
# person = Person.new('John Smith')
|
49
|
+
#
|
50
|
+
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
51
|
+
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
52
|
+
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
53
|
+
def evaluate_method(object, method, *args)
|
54
|
+
case method
|
55
|
+
when Symbol
|
56
|
+
method = object.method(method)
|
57
|
+
method.arity == 0 ? method.call : method.call(*args)
|
58
|
+
when String
|
59
|
+
eval(method, object.instance_eval {binding})
|
60
|
+
when Proc, Method
|
61
|
+
method.arity == 1 ? method.call(object) : method.call(object, *args)
|
62
|
+
else
|
63
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a string to be evaluated, or a block to be invoked'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/state_machine/event.rb
CHANGED
@@ -1,116 +1,150 @@
|
|
1
1
|
require 'state_machine/transition'
|
2
|
+
require 'state_machine/guard'
|
3
|
+
require 'state_machine/assertions'
|
2
4
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
module StateMachine
|
6
|
+
# An event defines an action that transitions an attribute from one state to
|
7
|
+
# another. The state that an attribute is transitioned to depends on the
|
8
|
+
# guards configured for the event.
|
9
|
+
class Event
|
10
|
+
include Assertions
|
11
|
+
|
12
|
+
# The state machine for which this event is defined
|
13
|
+
attr_accessor :machine
|
14
|
+
|
15
|
+
# The name of the action that fires the event
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
# The list of guards that determine what state this event transitions
|
19
|
+
# objects to when fired
|
20
|
+
attr_reader :guards
|
21
|
+
|
22
|
+
# A list of all of the states known to this event using the configured
|
23
|
+
# guards/transitions as the source.
|
24
|
+
attr_reader :known_states
|
25
|
+
|
26
|
+
# Creates a new event within the context of the given machine
|
27
|
+
def initialize(machine, name) #:nodoc:
|
28
|
+
@machine = machine
|
29
|
+
@name = name
|
30
|
+
@guards = []
|
31
|
+
@known_states = []
|
10
32
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
33
|
+
add_actions
|
34
|
+
end
|
35
|
+
|
36
|
+
# Creates a copy of this event in addition to the list of associated
|
37
|
+
# guards to prevent conflicts across different events.
|
38
|
+
def initialize_copy(orig) #:nodoc:
|
39
|
+
super
|
40
|
+
@guards = @guards.dup
|
41
|
+
@known_states = @known_states.dup
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates a new transition that will be evaluated when the event is fired.
|
45
|
+
#
|
46
|
+
# Configuration options:
|
47
|
+
# * +to+ - The state that being transitioned to. If not specified, then the transition will not change the state.
|
48
|
+
# * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
|
49
|
+
# * +except_from+ - A state or array of states that *cannot* be transitioned from.
|
50
|
+
# * +if+ - Specifies a method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
|
51
|
+
# * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
|
52
|
+
#
|
53
|
+
# == Order of operations
|
54
|
+
#
|
55
|
+
# Transitions are evaluated in the order in which they're defined. As a
|
56
|
+
# result, if more than one transition applies to a given object, then the
|
57
|
+
# first transition that matches will be performed.
|
58
|
+
#
|
59
|
+
# == Dynamic states
|
60
|
+
#
|
61
|
+
# There is limited support for using dynamically generated values for the
|
62
|
+
# +to+ state in transitions. This is especially useful for times where
|
63
|
+
# the machine attribute represents a Time object. In order to have a
|
64
|
+
# a transition be made to the current time, a lambda block can be passed
|
65
|
+
# in representing the state, such as:
|
66
|
+
#
|
67
|
+
# transition :to => lambda {Time.now}
|
68
|
+
#
|
69
|
+
# == Examples
|
70
|
+
#
|
71
|
+
# transition :from => %w(first_gear reverse)
|
72
|
+
# transition :except_from => 'parked'
|
73
|
+
# transition :to => 'parked'
|
74
|
+
# transition :to => lambda {Time.now}
|
75
|
+
# transition :to => 'parked', :from => 'first_gear'
|
76
|
+
# transition :to => 'parked', :from => %w(first_gear reverse)
|
77
|
+
# transition :to => 'parked', :from => 'first_gear', :if => :moving?
|
78
|
+
# transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
|
79
|
+
# transition :to => 'parked', :except_from => 'parked'
|
80
|
+
def transition(options)
|
81
|
+
assert_valid_keys(options, :to, :from, :except_from, :if, :unless)
|
57
82
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
83
|
+
guards << guard = Guard.new(options)
|
84
|
+
@known_states |= guard.known_states
|
85
|
+
guard
|
86
|
+
end
|
87
|
+
|
88
|
+
# Determines whether any transitions can be performed for this event based
|
89
|
+
# on the current state of the given object.
|
90
|
+
#
|
91
|
+
# If the event can't be fired, then this will return false, otherwise true.
|
92
|
+
def can_fire?(object)
|
93
|
+
!next_transition(object).nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Finds and builds the next transition that can be performed on the given
|
97
|
+
# object. If no transitions can be made, then this will return nil.
|
98
|
+
def next_transition(object)
|
99
|
+
from = object.send(machine.attribute)
|
65
100
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
101
|
+
if guard = guards.find {|guard| guard.matches?(object, :from => from)}
|
102
|
+
# Guard allows for the transition to occur
|
103
|
+
to = guard.requirements[:to] || from
|
104
|
+
to = to.call if to.is_a?(Proc)
|
105
|
+
Transition.new(object, machine, name, from, to)
|
70
106
|
end
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
107
|
+
end
|
108
|
+
|
109
|
+
# Attempts to perform the next available transition on the given object.
|
110
|
+
# If no transitions can be made, then this will return false, otherwise
|
111
|
+
# true.
|
112
|
+
def fire(object, *args)
|
113
|
+
if transition = next_transition(object)
|
114
|
+
transition.perform(*args)
|
115
|
+
else
|
116
|
+
false
|
77
117
|
end
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
# Add the various instance methods that can transition the object using
|
122
|
+
# the current event
|
123
|
+
def add_actions
|
124
|
+
attribute = machine.attribute
|
125
|
+
name = self.name
|
126
|
+
|
127
|
+
machine.owner_class.class_eval do
|
128
|
+
# Checks whether the event can be fired on the current object
|
129
|
+
define_method("can_#{name}?") do
|
130
|
+
self.class.state_machines[attribute].events[name].can_fire?(self)
|
131
|
+
end
|
85
132
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
define_method("can_#{name}?") {self.class.state_machines[attribute].events[name].can_fire?(self)}
|
133
|
+
# Gets the next transition that would be performed if the event were to be fired now
|
134
|
+
define_method("next_#{name}_transition") do
|
135
|
+
self.class.state_machines[attribute].events[name].next_transition(self)
|
90
136
|
end
|
91
|
-
end
|
92
|
-
|
93
|
-
# Attempts to find a transition that can be performed for this event.
|
94
|
-
#
|
95
|
-
# +bang+ indicates whether +perform+ or <tt>perform!</tt> will be
|
96
|
-
# invoked on transitions.
|
97
|
-
def run(record, bang)
|
98
|
-
result = false
|
99
137
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
result = bang ? transition.perform!(record) : transition.perform(record)
|
104
|
-
break
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# Rollback any changes if the transition failed
|
109
|
-
raise ActiveRecord::Rollback unless result
|
138
|
+
# Fires the event
|
139
|
+
define_method(name) do |*args|
|
140
|
+
self.class.state_machines[attribute].events[name].fire(self, *args)
|
110
141
|
end
|
111
142
|
|
112
|
-
|
143
|
+
# Fires the event, raising an exception if it fails to transition
|
144
|
+
define_method("#{name}!") do |*args|
|
145
|
+
send(name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition via :#{name} from #{send(attribute).inspect}")
|
146
|
+
end
|
113
147
|
end
|
114
|
-
|
148
|
+
end
|
115
149
|
end
|
116
150
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module ClassMethods
|
3
|
+
def self.extended(base) #:nodoc:
|
4
|
+
base.class_eval do
|
5
|
+
@state_machines = {}
|
6
|
+
|
7
|
+
# method_added may get defined by the class, so instead it's chained
|
8
|
+
class << self
|
9
|
+
alias_method :method_added_without_state_machine, :method_added
|
10
|
+
alias_method :method_added, :method_added_with_state_machine
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Ensures that the +initialize+ hook defined in StateMachine::InstanceMethods
|
16
|
+
# remains there even if the class defines its own +initialize+ method
|
17
|
+
# *after* the state machine has been defined. For example,
|
18
|
+
#
|
19
|
+
# class Switch
|
20
|
+
# state_machine do
|
21
|
+
# ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def initialize(attributes = {})
|
25
|
+
# ...
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
def method_added_with_state_machine(method) #:nodoc:
|
29
|
+
method_added_without_state_machine(method)
|
30
|
+
|
31
|
+
# Aliasing the +initialize+ method also invokes +method_added+, so
|
32
|
+
# alias processing is tracked to prevent an infinite loop
|
33
|
+
if !@skip_initialize_hook && [:initialize, :initialize_with_state_machine].include?(method)
|
34
|
+
@skip_initialize_hook = true
|
35
|
+
|
36
|
+
# +define_method+ is used to prevent it from showing up in #instance_methods
|
37
|
+
alias_method :initialize_without_state_machine, :initialize
|
38
|
+
class_eval <<-end_eval, __FILE__, __LINE__
|
39
|
+
def initialize(*args, &block)
|
40
|
+
initialize_with_state_machine(*args, &block)
|
41
|
+
end
|
42
|
+
end_eval
|
43
|
+
|
44
|
+
@skip_initialize_hook = false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Gets the current list of state machines defined for this class. This
|
49
|
+
# class-level attribute acts like an inheritable attribute. The attribute
|
50
|
+
# is available to each subclass, each subclass having a copy of its
|
51
|
+
# superclass's attribute.
|
52
|
+
#
|
53
|
+
# The hash of state machines maps +name+ => +machine+, e.g.
|
54
|
+
#
|
55
|
+
# Vehicle.state_machines # => {"state" => #<StateMachine::Machine:0xb6f6e4a4 ...>
|
56
|
+
def state_machines
|
57
|
+
@state_machines ||= superclass.state_machines.dup
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
def self.included(base) #:nodoc:
|
63
|
+
# Methods added from an included module don't invoke +method_added+,
|
64
|
+
# triggering the initialize alias, so it's done explicitly
|
65
|
+
base.method_added(:initialize_with_state_machine)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Defines the initial values for state machine attributes. The values
|
69
|
+
# will be set *after* the original initialize method is invoked. This is
|
70
|
+
# necessary in order to ensure that the object is initialized before
|
71
|
+
# dynamic initial attributes are evaluated.
|
72
|
+
def initialize_with_state_machine(*args, &block)
|
73
|
+
initialize_without_state_machine(*args, &block)
|
74
|
+
|
75
|
+
self.class.state_machines.each do |attribute, machine|
|
76
|
+
# Set the initial value of the machine's attribute unless it already
|
77
|
+
# exists (which must mean the defaults are being skipped)
|
78
|
+
value = send(attribute)
|
79
|
+
send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'state_machine/eval_helpers'
|
2
|
+
require 'state_machine/assertions'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# Represents a set of requirements that must be met in order for a transition
|
6
|
+
# or callback to occur. Guards verify that the event, from state, and to
|
7
|
+
# state of the transition match, in addition to if/unless conditionals for
|
8
|
+
# an object's state.
|
9
|
+
class Guard
|
10
|
+
include Assertions
|
11
|
+
include EvalHelpers
|
12
|
+
|
13
|
+
# The transition/conditional options that must be met in order for the
|
14
|
+
# guard to match
|
15
|
+
attr_reader :requirements
|
16
|
+
|
17
|
+
# A list of all of the states known to this guard. This will pull state
|
18
|
+
# names from the following requirements:
|
19
|
+
# * +to+
|
20
|
+
# * +from+
|
21
|
+
# * +except_to+
|
22
|
+
# * +except_from+
|
23
|
+
attr_reader :known_states
|
24
|
+
|
25
|
+
# Creates a new guard with the given requirements
|
26
|
+
def initialize(requirements = {}) #:nodoc:
|
27
|
+
assert_valid_keys(requirements, :to, :from, :on, :except_to, :except_from, :except_on, :if, :unless)
|
28
|
+
|
29
|
+
@requirements = requirements
|
30
|
+
@known_states = [:to, :from, :except_to, :except_from].inject([]) {|states, option| states |= Array(requirements[option])}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determines whether the given object / query matches the requirements
|
34
|
+
# configured for this guard. In addition to matching the event, from state,
|
35
|
+
# and to state, this will also check whether the configured :if/:unless
|
36
|
+
# conditionals pass on the given object.
|
37
|
+
#
|
38
|
+
# Query options:
|
39
|
+
# * +to+ - One or more states being transitioned to. If none are specified, then this will always match.
|
40
|
+
# * +from+ - One or more states being transitioned from. If none are specified, then this will always match.
|
41
|
+
# * +on+ - One or more events that fired the transition. If none are specified, then this will always match.
|
42
|
+
# * +except_to+ - One more states *not* being transitioned to
|
43
|
+
# * +except_from+ - One or more states *not* being transitioned from
|
44
|
+
# * +except_on+ - One or more events that *did not* fire the transition.
|
45
|
+
#
|
46
|
+
# == Examples
|
47
|
+
#
|
48
|
+
# guard = StateMachine::Guard.new(:on => 'ignite', :from => 'parked', :to => 'idling')
|
49
|
+
#
|
50
|
+
# # Successful
|
51
|
+
# guard.matches?(object, :on => 'ignite') # => true
|
52
|
+
# guard.matches?(object, :from => 'parked') # => true
|
53
|
+
# guard.matches?(object, :to => 'idling') # => true
|
54
|
+
# guard.matches?(object, :from => 'parked', :to => 'idling') # => true
|
55
|
+
# guard.matches?(object, :on => 'ignite', :from => 'parked', :to => 'idling') # => true
|
56
|
+
#
|
57
|
+
# # Unsuccessful
|
58
|
+
# guard.matches?(object, :on => 'park') # => false
|
59
|
+
# guard.matches?(object, :from => 'idling') # => false
|
60
|
+
# guard.matches?(object, :to => 'first_gear') # => false
|
61
|
+
# guard.matches?(object, :from => 'parked', :to => 'first_gear') # => false
|
62
|
+
# guard.matches?(object, :on => 'park', :from => 'parked', :to => 'idling') # => false
|
63
|
+
def matches?(object, query = {})
|
64
|
+
matches_query?(object, query) && matches_conditions?(object)
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
# Verify that the from state, to state, and event match the query
|
69
|
+
def matches_query?(object, query)
|
70
|
+
(!query || query.empty?) ||
|
71
|
+
find_match(query[:from], requirements[:from], requirements[:except_from]) &&
|
72
|
+
find_match(query[:to], requirements[:to], requirements[:except_to]) &&
|
73
|
+
find_match(query[:on], requirements[:on], requirements[:except_on])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Verify that the conditionals for this guard evaluate to true for the
|
77
|
+
# given object
|
78
|
+
def matches_conditions?(object)
|
79
|
+
if requirements[:if]
|
80
|
+
evaluate_method(object, requirements[:if])
|
81
|
+
elsif requirements[:unless]
|
82
|
+
!evaluate_method(object, requirements[:unless])
|
83
|
+
else
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Attempts to find the given value in either a whitelist of values or
|
89
|
+
# a blacklist of values. The whitelist will always be used first if it
|
90
|
+
# is specified. If neither lists are specified or the value is blank,
|
91
|
+
# then this will always find a match and return true.
|
92
|
+
#
|
93
|
+
# == Examples
|
94
|
+
#
|
95
|
+
# find_match(nil, %w(parked idling), nil) # => true
|
96
|
+
# find_match('parked', nil, nil) # => true
|
97
|
+
# find_match('parked', %w(parked idling), nil) # => true
|
98
|
+
# find_match('first_gear', %w(parked idling, nil) # => false
|
99
|
+
# find_match('parked', nil, %w(parked idling)) # => false
|
100
|
+
# find_match('first_gear', nil, %w(parked idling)) # => true
|
101
|
+
def find_match(value, whitelist, blacklist)
|
102
|
+
if value
|
103
|
+
if whitelist
|
104
|
+
Array(whitelist).include?(value)
|
105
|
+
elsif blacklist
|
106
|
+
!Array(blacklist).include?(value)
|
107
|
+
else
|
108
|
+
true
|
109
|
+
end
|
110
|
+
else
|
111
|
+
true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|