pluginaweek-state_machine 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- metadata +161 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Represents a collection of events in a state machine
|
3
|
+
class EventCollection < NodeCollection
|
4
|
+
def initialize(machine) #:nodoc:
|
5
|
+
super(machine, :index => [:name, :qualified_name])
|
6
|
+
end
|
7
|
+
|
8
|
+
# Gets the list of events that can be fired on the given object.
|
9
|
+
#
|
10
|
+
# == Examples
|
11
|
+
#
|
12
|
+
# class Vehicle
|
13
|
+
# state_machine :initial => :parked do
|
14
|
+
# event :park do
|
15
|
+
# transition :idling => :parked
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# event :ignite do
|
19
|
+
# transition :parked => :idling
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# events = Vehicle.state_machine(:state).events
|
25
|
+
#
|
26
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
27
|
+
# events.valid_for(vehicle) # => [#<StateMachine::Event name=:ignite transitions=[:parked => :idling]>]
|
28
|
+
#
|
29
|
+
# vehicle.state = 'idling'
|
30
|
+
# events.valid_for(vehicle) # => [#<StateMachine::Event name=:park transitions=[:idling => :parked]>]
|
31
|
+
def valid_for(object)
|
32
|
+
select {|event| event.can_fire?(object)}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gets the list of transitions that can be run on the given object.
|
36
|
+
#
|
37
|
+
# == Examples
|
38
|
+
#
|
39
|
+
# class Vehicle
|
40
|
+
# state_machine :initial => :parked do
|
41
|
+
# event :park do
|
42
|
+
# transition :idling => :parked
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# event :ignite do
|
46
|
+
# transition :parked => :idling
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# events = Vehicle.state_machine.events
|
52
|
+
#
|
53
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
54
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
55
|
+
#
|
56
|
+
# vehicle.state = 'idling'
|
57
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
|
58
|
+
def transitions_for(object)
|
59
|
+
map {|event| event.transition_for(object)}.compact
|
60
|
+
end
|
61
|
+
|
62
|
+
# Gets the transition that should be performed for the event stored in the
|
63
|
+
# given object's event attribute. This also takes an additional parameter
|
64
|
+
# for automatically invalidating the object if the event or transition
|
65
|
+
# are invalid. By default, this is turned off.
|
66
|
+
#
|
67
|
+
# *Note* that if a transition has already been generated for the event,
|
68
|
+
# then that transition will be used.
|
69
|
+
#
|
70
|
+
# == Examples
|
71
|
+
#
|
72
|
+
# class Vehicle < ActiveRecord::Base
|
73
|
+
# state_machine :initial => :parked do
|
74
|
+
# event :ignite do
|
75
|
+
# transition :parked => :idling
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
|
81
|
+
# events = Vehicle.state_machine.events
|
82
|
+
#
|
83
|
+
# vehicle.state_event = nil
|
84
|
+
# events.attribute_transition_for(vehicle) # => nil # Event isn't defined
|
85
|
+
#
|
86
|
+
# vehicle.state_event = 'invalid'
|
87
|
+
# events.attribute_transition_for(vehicle) # => false # Event is invalid
|
88
|
+
#
|
89
|
+
# vehicle.state_event = 'ignite'
|
90
|
+
# events.attribute_transition_for(vehicle) # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
91
|
+
def attribute_transition_for(object, invalidate = false)
|
92
|
+
return unless machine.action
|
93
|
+
|
94
|
+
result = nil
|
95
|
+
|
96
|
+
if event_name = machine.read(object, :event)
|
97
|
+
if event = self[event_name.to_sym, :name]
|
98
|
+
unless result = machine.read(object, :event_transition) || event.transition_for(object)
|
99
|
+
# No valid transition: invalidate
|
100
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
|
101
|
+
result = false
|
102
|
+
end
|
103
|
+
else
|
104
|
+
# Event is unknown: invalidate
|
105
|
+
machine.invalidate(object, :event, :invalid) if invalidate
|
106
|
+
result = false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'state_machine/machine_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
module ClassMethods
|
5
|
+
def self.extended(base) #:nodoc:
|
6
|
+
base.class_eval do
|
7
|
+
@state_machines = MachineCollection.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Gets the current list of state machines defined for this class. This
|
12
|
+
# class-level attribute acts like an inheritable attribute. The attribute
|
13
|
+
# is available to each subclass, each having a copy of its superclass's
|
14
|
+
# attribute.
|
15
|
+
#
|
16
|
+
# The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
|
17
|
+
#
|
18
|
+
# Vehicle.state_machines # => {:state => #<StateMachine::Machine:0xb6f6e4a4 ...>}
|
19
|
+
def state_machines
|
20
|
+
@state_machines ||= superclass.state_machines.dup
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
# Defines the initial values for state machine attributes. The values
|
26
|
+
# will be set *after* the original initialize method is invoked. This is
|
27
|
+
# necessary in order to ensure that the object is initialized before
|
28
|
+
# dynamic initial attributes are evaluated.
|
29
|
+
def initialize(*args, &block)
|
30
|
+
super
|
31
|
+
initialize_state_machines
|
32
|
+
end
|
33
|
+
|
34
|
+
# Runs one or more events in parallel. All events will run through the
|
35
|
+
# following steps:
|
36
|
+
# * Before callbacks
|
37
|
+
# * Persist state
|
38
|
+
# * Invoke action
|
39
|
+
# * After callbacks
|
40
|
+
#
|
41
|
+
# For example, if two events (for state machines A and B) are run in
|
42
|
+
# parallel, the order in which steps are run is:
|
43
|
+
# * A - Before transition callbacks
|
44
|
+
# * B - Before transition callbacks
|
45
|
+
# * A - Persist new state
|
46
|
+
# * B - Persist new state
|
47
|
+
# * A - Invoke action
|
48
|
+
# * B - Invoke action (only if different than A's action)
|
49
|
+
# * A - After transition callbacks
|
50
|
+
# * B - After transition callbacks
|
51
|
+
#
|
52
|
+
# *Note* that multiple events on the same state machine / attribute cannot
|
53
|
+
# be run in parallel. If this is attempted, an ArgumentError will be
|
54
|
+
# raised.
|
55
|
+
#
|
56
|
+
# == Halting callbacks
|
57
|
+
#
|
58
|
+
# When running multiple events in parallel, special consideration should
|
59
|
+
# be taken with regard to how halting within callbacks affects the flow.
|
60
|
+
#
|
61
|
+
# For *before* callbacks, any <tt>:halt</tt> error that's thrown will
|
62
|
+
# immediately cancel the perform for all transitions. As a result, it's
|
63
|
+
# possible for one event's transition to affect the continuation of
|
64
|
+
# another.
|
65
|
+
#
|
66
|
+
# On the other hand, any <tt>:halt</tt> error that's thrown within an
|
67
|
+
# *after* callback with only affect that event's transition. Other
|
68
|
+
# transitions will continue to run their own callbacks.
|
69
|
+
#
|
70
|
+
# == Example
|
71
|
+
#
|
72
|
+
# class Vehicle
|
73
|
+
# state_machine :initial => :parked do
|
74
|
+
# event :ignite do
|
75
|
+
# transition :parked => :idling
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# event :park do
|
79
|
+
# transition :idling => :parked
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
|
84
|
+
# event :enable do
|
85
|
+
# transition all => :active
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# event :disable do
|
89
|
+
# transition all => :off
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
95
|
+
# vehicle.state # => "parked"
|
96
|
+
# vehicle.alarm_state # => "active"
|
97
|
+
#
|
98
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
99
|
+
# vehicle.state # => "idling"
|
100
|
+
# vehicle.alarm_state # => "off"
|
101
|
+
#
|
102
|
+
# # If any event fails, the entire event chain fails
|
103
|
+
# vehicle.fire_events(:ignite, :enable_alarm) # => false
|
104
|
+
# vehicle.state # => "idling"
|
105
|
+
# vehicle.alarm_state # => "off"
|
106
|
+
#
|
107
|
+
# # Exception raised on invalid event
|
108
|
+
# vehicle.fire_events(:park, :invalid) # => StateMachine::InvalidEvent: :invalid is an unknown event
|
109
|
+
# vehicle.state # => "idling"
|
110
|
+
# vehicle.alarm_state # => "off"
|
111
|
+
def fire_events(*events)
|
112
|
+
self.class.state_machines.fire_events(self, *events)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Run one or more events in parallel. If any event fails to run, then
|
116
|
+
# a StateMachine::InvalidTransition exception will be raised.
|
117
|
+
#
|
118
|
+
# See StateMachine::InstanceMethods#fire_events for more information.
|
119
|
+
#
|
120
|
+
# == Example
|
121
|
+
#
|
122
|
+
# class Vehicle
|
123
|
+
# state_machine :initial => :parked do
|
124
|
+
# event :ignite do
|
125
|
+
# transition :parked => :idling
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# event :park do
|
129
|
+
# transition :idling => :parked
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
134
|
+
# event :enable do
|
135
|
+
# transition all => :active
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# event :disable do
|
139
|
+
# transition all => :off
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
145
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
146
|
+
#
|
147
|
+
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachine::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
|
148
|
+
def fire_events!(*events)
|
149
|
+
run_action = [true, false].include?(events.last) ? events.pop : true
|
150
|
+
fire_events(*(events + [run_action])) || raise(StateMachine::InvalidTransition, "Cannot run events in parallel: #{events * ', '}")
|
151
|
+
end
|
152
|
+
|
153
|
+
protected
|
154
|
+
def initialize_state_machines #:nodoc:
|
155
|
+
self.class.state_machines.initialize_states(self)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'state_machine/matcher'
|
2
|
+
require 'state_machine/eval_helpers'
|
3
|
+
require 'state_machine/assertions'
|
4
|
+
|
5
|
+
module StateMachine
|
6
|
+
# Represents a set of requirements that must be met in order for a transition
|
7
|
+
# or callback to occur. Guards verify that the event, from state, and to
|
8
|
+
# state of the transition match, in addition to if/unless conditionals for
|
9
|
+
# an object's state.
|
10
|
+
class Guard
|
11
|
+
include Assertions
|
12
|
+
include EvalHelpers
|
13
|
+
|
14
|
+
# The condition that must be met on an object
|
15
|
+
attr_reader :if_condition
|
16
|
+
|
17
|
+
# The condition that must *not* be met on an object
|
18
|
+
attr_reader :unless_condition
|
19
|
+
|
20
|
+
# The requirement for verifying the event being guarded
|
21
|
+
attr_reader :event_requirement
|
22
|
+
|
23
|
+
# One or more requirements for verifying the states being guarded. All
|
24
|
+
# requirements contain a mapping of {:from => matcher, :to => matcher}.
|
25
|
+
attr_reader :state_requirements
|
26
|
+
|
27
|
+
# A list of all of the states known to this guard. This will pull states
|
28
|
+
# from the following options (in the same order):
|
29
|
+
# * +from+ / +except_from+
|
30
|
+
# * +to+ / +except_to+
|
31
|
+
attr_reader :known_states
|
32
|
+
|
33
|
+
# Creates a new guard
|
34
|
+
def initialize(options = {}) #:nodoc:
|
35
|
+
# Build conditionals
|
36
|
+
@if_condition = options.delete(:if)
|
37
|
+
@unless_condition = options.delete(:unless)
|
38
|
+
|
39
|
+
# Build event requirement
|
40
|
+
@event_requirement = build_matcher(options, :on, :except_on)
|
41
|
+
|
42
|
+
if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
|
43
|
+
# Explicit from/to requirements specified
|
44
|
+
@state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
|
45
|
+
else
|
46
|
+
# Separate out the event requirement
|
47
|
+
options.delete(:on)
|
48
|
+
options.delete(:except_on)
|
49
|
+
|
50
|
+
# Implicit from/to requirements specified
|
51
|
+
@state_requirements = options.collect do |from, to|
|
52
|
+
from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
|
53
|
+
to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
|
54
|
+
{:from => from, :to => to}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Track known states. The order that requirements are iterated is based
|
59
|
+
# on the priority in which tracked states should be added.
|
60
|
+
@known_states = []
|
61
|
+
@state_requirements.each do |state_requirement|
|
62
|
+
[:from, :to].each {|option| @known_states |= state_requirement[option].values}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Determines whether the given object / query matches the requirements
|
67
|
+
# configured for this guard. In addition to matching the event, from state,
|
68
|
+
# and to state, this will also check whether the configured :if/:unless
|
69
|
+
# conditions pass on the given object.
|
70
|
+
#
|
71
|
+
# == Examples
|
72
|
+
#
|
73
|
+
# guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
|
74
|
+
#
|
75
|
+
# # Successful
|
76
|
+
# guard.matches?(object, :on => :ignite) # => true
|
77
|
+
# guard.matches?(object, :from => nil) # => true
|
78
|
+
# guard.matches?(object, :from => :parked) # => true
|
79
|
+
# guard.matches?(object, :to => :idling) # => true
|
80
|
+
# guard.matches?(object, :from => :parked, :to => :idling) # => true
|
81
|
+
# guard.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true
|
82
|
+
#
|
83
|
+
# # Unsuccessful
|
84
|
+
# guard.matches?(object, :on => :park) # => false
|
85
|
+
# guard.matches?(object, :from => :idling) # => false
|
86
|
+
# guard.matches?(object, :to => :first_gear) # => false
|
87
|
+
# guard.matches?(object, :from => :parked, :to => :first_gear) # => false
|
88
|
+
# guard.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
|
89
|
+
def matches?(object, query = {})
|
90
|
+
!match(object, query).nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Attempts to match the given object / query against the set of requirements
|
94
|
+
# configured for this guard. In addition to matching the event, from state,
|
95
|
+
# and to state, this will also check whether the configured :if/:unless
|
96
|
+
# conditions pass on the given object.
|
97
|
+
#
|
98
|
+
# If a match is found, then the event/state requirements that the query
|
99
|
+
# passed successfully will be returned. Otherwise, nil is returned if there
|
100
|
+
# was no match.
|
101
|
+
#
|
102
|
+
# Query options:
|
103
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
104
|
+
# are specified, then this will always match.
|
105
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
106
|
+
# specified, then this will always match.
|
107
|
+
# * <tt>:on</tt> - One or more events that fired the transition. If none
|
108
|
+
# are specified, then this will always match.
|
109
|
+
#
|
110
|
+
# == Examples
|
111
|
+
#
|
112
|
+
# guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
|
113
|
+
#
|
114
|
+
# guard.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
|
115
|
+
# guard.match(object, :on => :park) # => nil
|
116
|
+
def match(object, query = {})
|
117
|
+
if (match = match_query(query)) && matches_conditions?(object)
|
118
|
+
match
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Draws a representation of this guard on the given graph. This will draw
|
123
|
+
# an edge between every state this guard matches *from* to either the
|
124
|
+
# configured to state or, if none specified, then a loopback to the from
|
125
|
+
# state.
|
126
|
+
#
|
127
|
+
# For example, if the following from states are configured:
|
128
|
+
# * +idling+
|
129
|
+
# * +first_gear+
|
130
|
+
# * +backing_up+
|
131
|
+
#
|
132
|
+
# ...and the to state is +parked+, then the following edges will be created:
|
133
|
+
# * +idling+ -> +parked+
|
134
|
+
# * +first_gear+ -> +parked+
|
135
|
+
# * +backing_up+ -> +parked+
|
136
|
+
#
|
137
|
+
# Each edge will be labeled with the name of the event that would cause the
|
138
|
+
# transition.
|
139
|
+
#
|
140
|
+
# The collection of edges generated on the graph will be returned.
|
141
|
+
def draw(graph, event, valid_states)
|
142
|
+
state_requirements.inject([]) do |edges, state_requirement|
|
143
|
+
# From states determined based on the known valid states
|
144
|
+
from_states = state_requirement[:from].filter(valid_states)
|
145
|
+
|
146
|
+
# If a to state is not specified, then it's a loopback and each from
|
147
|
+
# state maps back to itself
|
148
|
+
if state_requirement[:to].values.empty?
|
149
|
+
loopback = true
|
150
|
+
else
|
151
|
+
to_state = state_requirement[:to].values.first
|
152
|
+
to_state = to_state ? to_state.to_s : 'nil'
|
153
|
+
loopback = false
|
154
|
+
end
|
155
|
+
|
156
|
+
# Generate an edge between each from and to state
|
157
|
+
from_states.each do |from_state|
|
158
|
+
from_state = from_state ? from_state.to_s : 'nil'
|
159
|
+
edges << graph.add_edge(from_state, loopback ? from_state : to_state, :label => event.to_s)
|
160
|
+
end
|
161
|
+
|
162
|
+
edges
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
protected
|
167
|
+
# Builds a matcher strategy to use for the given options. If neither a
|
168
|
+
# whitelist nor a blacklist option is specified, then an AllMatcher is
|
169
|
+
# built.
|
170
|
+
def build_matcher(options, whitelist_option, blacklist_option)
|
171
|
+
assert_exclusive_keys(options, whitelist_option, blacklist_option)
|
172
|
+
|
173
|
+
if options.include?(whitelist_option)
|
174
|
+
WhitelistMatcher.new(options[whitelist_option])
|
175
|
+
elsif options.include?(blacklist_option)
|
176
|
+
BlacklistMatcher.new(options[blacklist_option])
|
177
|
+
else
|
178
|
+
AllMatcher.instance
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Verifies that all configured requirements (event and state) match the
|
183
|
+
# given query. If a match is found, then a hash containing the
|
184
|
+
# event/state requirements that passed will be returned; otherwise, nil.
|
185
|
+
def match_query(query)
|
186
|
+
query ||= {}
|
187
|
+
|
188
|
+
if match_event(query) && (state_requirement = match_states(query))
|
189
|
+
state_requirement.merge(:on => event_requirement)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Verifies that the event requirement matches the given query
|
194
|
+
def match_event(query)
|
195
|
+
matches_requirement?(query, :on, event_requirement)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Verifies that the state requirements match the given query. If a
|
199
|
+
# matching requirement is found, then it is returned.
|
200
|
+
def match_states(query)
|
201
|
+
state_requirements.detect do |state_requirement|
|
202
|
+
[:from, :to].all? {|option| matches_requirement?(query, option, state_requirement[option])}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Verifies that an option in the given query matches the values required
|
207
|
+
# for that option
|
208
|
+
def matches_requirement?(query, option, requirement)
|
209
|
+
!query.include?(option) || requirement.matches?(query[option], query)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Verifies that the conditionals for this guard evaluate to true for the
|
213
|
+
# given object
|
214
|
+
def matches_conditions?(object)
|
215
|
+
Array(if_condition).all? {|condition| evaluate_method(object, condition)} &&
|
216
|
+
!Array(unless_condition).any? {|condition| evaluate_method(object, condition)}
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|