hsume2-state_machine 1.0.1
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 +413 -0
- data/LICENSE +20 -0
- data/README.rdoc +717 -0
- data/Rakefile +77 -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 +448 -0
- data/lib/state_machine/alternate_machine.rb +79 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +224 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +86 -0
- data/lib/state_machine/event.rb +304 -0
- data/lib/state_machine/event_collection.rb +139 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/integrations.rb +110 -0
- data/lib/state_machine/integrations/active_model.rb +502 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +45 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_record.rb +424 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +143 -0
- data/lib/state_machine/integrations/base.rb +91 -0
- data/lib/state_machine/integrations/data_mapper.rb +392 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
- data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
- data/lib/state_machine/integrations/mongoid.rb +357 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
- data/lib/state_machine/integrations/sequel.rb +428 -0
- data/lib/state_machine/integrations/sequel/versions.rb +36 -0
- data/lib/state_machine/machine.rb +1873 -0
- data/lib/state_machine/machine_collection.rb +87 -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 +157 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +271 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +458 -0
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +27 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +11 -0
- data/test/functional/alternate_state_machine_test.rb +122 -0
- data/test/functional/state_machine_test.rb +993 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +890 -0
- data/test/unit/callback_test.rb +701 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +222 -0
- data/test/unit/event_collection_test.rb +358 -0
- data/test/unit/event_test.rb +985 -0
- data/test/unit/integrations/active_model_test.rb +1097 -0
- data/test/unit/integrations/active_record_test.rb +2021 -0
- data/test/unit/integrations/base_test.rb +99 -0
- data/test/unit/integrations/data_mapper_test.rb +1909 -0
- data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
- data/test/unit/integrations/mongoid_test.rb +1591 -0
- data/test/unit/integrations/sequel_test.rb +1523 -0
- data/test/unit/integrations_test.rb +61 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +77 -0
- data/test/unit/machine_collection_test.rb +599 -0
- data/test/unit/machine_test.rb +3043 -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 +217 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +310 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +924 -0
- data/test/unit/transition_collection_test.rb +2102 -0
- data/test/unit/transition_test.rb +1541 -0
- metadata +207 -0
@@ -0,0 +1,139 @@
|
|
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
|
+
# Valid requirement options:
|
11
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
12
|
+
# are specified, then this will be the object's current state.
|
13
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
14
|
+
# specified, then this will match any to state.
|
15
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
16
|
+
# are specified, then this will match any event.
|
17
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
18
|
+
# conditionals defined for each one. Default is true.
|
19
|
+
#
|
20
|
+
# == Examples
|
21
|
+
#
|
22
|
+
# class Vehicle
|
23
|
+
# state_machine :initial => :parked do
|
24
|
+
# event :park do
|
25
|
+
# transition :idling => :parked
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# event :ignite do
|
29
|
+
# transition :parked => :idling
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# events = Vehicle.state_machine(:state).events
|
35
|
+
#
|
36
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
37
|
+
# events.valid_for(vehicle) # => [#<StateMachine::Event name=:ignite transitions=[:parked => :idling]>]
|
38
|
+
#
|
39
|
+
# vehicle.state = 'idling'
|
40
|
+
# events.valid_for(vehicle) # => [#<StateMachine::Event name=:park transitions=[:idling => :parked]>]
|
41
|
+
def valid_for(object, requirements = {})
|
42
|
+
match(requirements).select {|event| event.can_fire?(object, requirements)}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gets the list of transitions that can be run on the given object.
|
46
|
+
#
|
47
|
+
# Valid requirement options:
|
48
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
49
|
+
# are specified, then this will be the object's current state.
|
50
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
51
|
+
# specified, then this will match any to state.
|
52
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
53
|
+
# are specified, then this will match any event.
|
54
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
55
|
+
# conditionals defined for each one. Default is true.
|
56
|
+
#
|
57
|
+
# == Examples
|
58
|
+
#
|
59
|
+
# class Vehicle
|
60
|
+
# state_machine :initial => :parked do
|
61
|
+
# event :park do
|
62
|
+
# transition :idling => :parked
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# event :ignite do
|
66
|
+
# transition :parked => :idling
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# events = Vehicle.state_machine.events
|
72
|
+
#
|
73
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
74
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
75
|
+
#
|
76
|
+
# vehicle.state = 'idling'
|
77
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
|
78
|
+
#
|
79
|
+
# # Search for explicit transitions regardless of the current state
|
80
|
+
# events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
81
|
+
def transitions_for(object, requirements = {})
|
82
|
+
match(requirements).map {|event| event.transition_for(object, requirements)}.compact
|
83
|
+
end
|
84
|
+
|
85
|
+
# Gets the transition that should be performed for the event stored in the
|
86
|
+
# given object's event attribute. This also takes an additional parameter
|
87
|
+
# for automatically invalidating the object if the event or transition are
|
88
|
+
# invalid. By default, this is turned off.
|
89
|
+
#
|
90
|
+
# *Note* that if a transition has already been generated for the event, then
|
91
|
+
# that transition will be used.
|
92
|
+
#
|
93
|
+
# == Examples
|
94
|
+
#
|
95
|
+
# class Vehicle < ActiveRecord::Base
|
96
|
+
# state_machine :initial => :parked do
|
97
|
+
# event :ignite do
|
98
|
+
# transition :parked => :idling
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
|
104
|
+
# events = Vehicle.state_machine.events
|
105
|
+
#
|
106
|
+
# vehicle.state_event = nil
|
107
|
+
# events.attribute_transition_for(vehicle) # => nil # Event isn't defined
|
108
|
+
#
|
109
|
+
# vehicle.state_event = 'invalid'
|
110
|
+
# events.attribute_transition_for(vehicle) # => false # Event is invalid
|
111
|
+
#
|
112
|
+
# vehicle.state_event = 'ignite'
|
113
|
+
# events.attribute_transition_for(vehicle) # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
114
|
+
def attribute_transition_for(object, invalidate = false)
|
115
|
+
return unless machine.action
|
116
|
+
|
117
|
+
result = machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
|
118
|
+
if event = self[event_name.to_sym, :name]
|
119
|
+
event.transition_for(object) || begin
|
120
|
+
# No valid transition: invalidate
|
121
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
|
122
|
+
false
|
123
|
+
end
|
124
|
+
else
|
125
|
+
# Event is unknown: invalidate
|
126
|
+
machine.invalidate(object, :event, :invalid) if invalidate
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
def match(requirements) #:nodoc:
|
136
|
+
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,149 @@
|
|
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
|
+
# Runs one or more events in parallel. All events will run through the
|
26
|
+
# following steps:
|
27
|
+
# * Before callbacks
|
28
|
+
# * Persist state
|
29
|
+
# * Invoke action
|
30
|
+
# * After callbacks
|
31
|
+
#
|
32
|
+
# For example, if two events (for state machines A and B) are run in
|
33
|
+
# parallel, the order in which steps are run is:
|
34
|
+
# * A - Before transition callbacks
|
35
|
+
# * B - Before transition callbacks
|
36
|
+
# * A - Persist new state
|
37
|
+
# * B - Persist new state
|
38
|
+
# * A - Invoke action
|
39
|
+
# * B - Invoke action (only if different than A's action)
|
40
|
+
# * A - After transition callbacks
|
41
|
+
# * B - After transition callbacks
|
42
|
+
#
|
43
|
+
# *Note* that multiple events on the same state machine / attribute cannot
|
44
|
+
# be run in parallel. If this is attempted, an ArgumentError will be
|
45
|
+
# raised.
|
46
|
+
#
|
47
|
+
# == Halting callbacks
|
48
|
+
#
|
49
|
+
# When running multiple events in parallel, special consideration should
|
50
|
+
# be taken with regard to how halting within callbacks affects the flow.
|
51
|
+
#
|
52
|
+
# For *before* callbacks, any <tt>:halt</tt> error that's thrown will
|
53
|
+
# immediately cancel the perform for all transitions. As a result, it's
|
54
|
+
# possible for one event's transition to affect the continuation of
|
55
|
+
# another.
|
56
|
+
#
|
57
|
+
# On the other hand, any <tt>:halt</tt> error that's thrown within an
|
58
|
+
# *after* callback with only affect that event's transition. Other
|
59
|
+
# transitions will continue to run their own callbacks.
|
60
|
+
#
|
61
|
+
# == Example
|
62
|
+
#
|
63
|
+
# class Vehicle
|
64
|
+
# state_machine :initial => :parked do
|
65
|
+
# event :ignite do
|
66
|
+
# transition :parked => :idling
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# event :park do
|
70
|
+
# transition :idling => :parked
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
|
75
|
+
# event :enable do
|
76
|
+
# transition all => :active
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# event :disable do
|
80
|
+
# transition all => :off
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
86
|
+
# vehicle.state # => "parked"
|
87
|
+
# vehicle.alarm_state # => "active"
|
88
|
+
#
|
89
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
90
|
+
# vehicle.state # => "idling"
|
91
|
+
# vehicle.alarm_state # => "off"
|
92
|
+
#
|
93
|
+
# # If any event fails, the entire event chain fails
|
94
|
+
# vehicle.fire_events(:ignite, :enable_alarm) # => false
|
95
|
+
# vehicle.state # => "idling"
|
96
|
+
# vehicle.alarm_state # => "off"
|
97
|
+
#
|
98
|
+
# # Exception raised on invalid event
|
99
|
+
# vehicle.fire_events(:park, :invalid) # => StateMachine::InvalidEvent: :invalid is an unknown event
|
100
|
+
# vehicle.state # => "idling"
|
101
|
+
# vehicle.alarm_state # => "off"
|
102
|
+
def fire_events(*events)
|
103
|
+
self.class.state_machines.fire_events(self, *events)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Run one or more events in parallel. If any event fails to run, then
|
107
|
+
# a StateMachine::InvalidTransition exception will be raised.
|
108
|
+
#
|
109
|
+
# See StateMachine::InstanceMethods#fire_events for more information.
|
110
|
+
#
|
111
|
+
# == Example
|
112
|
+
#
|
113
|
+
# class Vehicle
|
114
|
+
# state_machine :initial => :parked do
|
115
|
+
# event :ignite do
|
116
|
+
# transition :parked => :idling
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# event :park do
|
120
|
+
# transition :idling => :parked
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
125
|
+
# event :enable do
|
126
|
+
# transition all => :active
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# event :disable do
|
130
|
+
# transition all => :off
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
136
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
137
|
+
#
|
138
|
+
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachine::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
|
139
|
+
def fire_events!(*events)
|
140
|
+
run_action = [true, false].include?(events.last) ? events.pop : true
|
141
|
+
fire_events(*(events + [run_action])) || raise(StateMachine::InvalidParallelTransition.new(self, events))
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
def initialize_state_machines(options = {}, &block) #:nodoc:
|
146
|
+
self.class.state_machines.initialize_states(self, options, &block)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../../tasks/state_machine") if defined?(Merb::Plugins)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
if defined?(Rails)
|
2
|
+
# Track all of the applicable locales to load
|
3
|
+
locale_paths = []
|
4
|
+
StateMachine::Integrations.all.each do |integration|
|
5
|
+
locale_paths << integration.locale_path if integration.available? && integration.locale_path
|
6
|
+
end
|
7
|
+
|
8
|
+
if defined?(Rails::Engine)
|
9
|
+
# Rails 3.x
|
10
|
+
class StateMachine::RailsEngine < Rails::Engine
|
11
|
+
rake_tasks do
|
12
|
+
load 'tasks/state_machine.rb'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0
|
17
|
+
StateMachine::RailsEngine.paths.config.locales = locale_paths
|
18
|
+
else
|
19
|
+
StateMachine::RailsEngine.paths['config/locales'] = locale_paths
|
20
|
+
end
|
21
|
+
elsif defined?(I18n)
|
22
|
+
# Rails 2.x
|
23
|
+
I18n.load_path.unshift(*locale_paths)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Load each available integration
|
2
|
+
require 'state_machine/integrations/base'
|
3
|
+
Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
|
4
|
+
require "state_machine/integrations/#{File.basename(path)}"
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'state_machine/error'
|
8
|
+
|
9
|
+
module StateMachine
|
10
|
+
# An invalid integration was specified
|
11
|
+
class IntegrationNotFound < Error
|
12
|
+
def initialize(name)
|
13
|
+
super(nil, "#{name.inspect} is an invalid integration")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Integrations allow state machines to take advantage of features within the
|
18
|
+
# context of a particular library. This is currently most useful with
|
19
|
+
# database libraries. For example, the various database integrations allow
|
20
|
+
# state machines to hook into features like:
|
21
|
+
# * Saving
|
22
|
+
# * Transactions
|
23
|
+
# * Observers
|
24
|
+
# * Scopes
|
25
|
+
# * Callbacks
|
26
|
+
# * Validation errors
|
27
|
+
#
|
28
|
+
# This type of integration allows the user to work with state machines in a
|
29
|
+
# fashion similar to other object models in their application.
|
30
|
+
#
|
31
|
+
# The integration interface is loosely defined by various unimplemented
|
32
|
+
# methods in the StateMachine::Machine class. See that class or the various
|
33
|
+
# built-in integrations for more information about how to define additional
|
34
|
+
# integrations.
|
35
|
+
module Integrations
|
36
|
+
# Attempts to find an integration that matches the given class. This will
|
37
|
+
# look through all of the built-in integrations under the StateMachine::Integrations
|
38
|
+
# namespace and find one that successfully matches the class.
|
39
|
+
#
|
40
|
+
# == Examples
|
41
|
+
#
|
42
|
+
# class Vehicle
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class ActiveModelVehicle
|
46
|
+
# include ActiveModel::Dirty
|
47
|
+
# include ActiveModel::Observing
|
48
|
+
# include ActiveModel::Validations
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# class ActiveRecordVehicle < ActiveRecord::Base
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# class DataMapperVehicle
|
55
|
+
# include DataMapper::Resource
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# class MongoidVehicle
|
59
|
+
# include Mongoid::Document
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# class MongoMapperVehicle
|
63
|
+
# include MongoMapper::Document
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# class SequelVehicle < Sequel::Model
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# StateMachine::Integrations.match(Vehicle) # => nil
|
70
|
+
# StateMachine::Integrations.match(ActiveModelVehicle) # => StateMachine::Integrations::ActiveModel
|
71
|
+
# StateMachine::Integrations.match(ActiveRecordVehicle) # => StateMachine::Integrations::ActiveRecord
|
72
|
+
# StateMachine::Integrations.match(DataMapperVehicle) # => StateMachine::Integrations::DataMapper
|
73
|
+
# StateMachine::Integrations.match(MongoidVehicle) # => StateMachine::Integrations::Mongoid
|
74
|
+
# StateMachine::Integrations.match(MongoMapperVehicle) # => StateMachine::Integrations::MongoMapper
|
75
|
+
# StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
|
76
|
+
def self.match(klass)
|
77
|
+
all.detect {|integration| integration.available? && integration.matches?(klass)}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Finds an integration with the given name. If the integration cannot be
|
81
|
+
# found, then a NameError exception will be raised.
|
82
|
+
#
|
83
|
+
# == Examples
|
84
|
+
#
|
85
|
+
# StateMachine::Integrations.find_by_name(:active_record) # => StateMachine::Integrations::ActiveRecord
|
86
|
+
# StateMachine::Integrations.find_by_name(:active_model) # => StateMachine::Integrations::ActiveModel
|
87
|
+
# StateMachine::Integrations.find_by_name(:data_mapper) # => StateMachine::Integrations::DataMapper
|
88
|
+
# StateMachine::Integrations.find_by_name(:mongoid) # => StateMachine::Integrations::Mongoid
|
89
|
+
# StateMachine::Integrations.find_by_name(:mongo_mapper) # => StateMachine::Integrations::MongoMapper
|
90
|
+
# StateMachine::Integrations.find_by_name(:sequel) # => StateMachine::Integrations::Sequel
|
91
|
+
# StateMachine::Integrations.find_by_name(:invalid) # => StateMachine::IntegrationNotFound: :invalid is an invalid integration
|
92
|
+
def self.find_by_name(name)
|
93
|
+
all.detect {|integration| integration.integration_name == name} || raise(IntegrationNotFound.new(name))
|
94
|
+
end
|
95
|
+
|
96
|
+
# Gets a list of all of the available integrations for use. This will
|
97
|
+
# always list the ActiveModel integration last.
|
98
|
+
#
|
99
|
+
# == Example
|
100
|
+
#
|
101
|
+
# StateMachine::Integrations.all
|
102
|
+
# # => [StateMachine::Integrations::ActiveRecord, StateMachine::Integrations::DataMapper
|
103
|
+
# # StateMachine::Integrations::Mongoid, StateMachine::Integrations::MongoMapper,
|
104
|
+
# # StateMachine::Integrations::Sequel, StateMachine::Integrations::ActiveModel]
|
105
|
+
def self.all
|
106
|
+
constants = self.constants.map {|c| c.to_s}.select {|c| c != 'ActiveModel'}.sort << 'ActiveModel'
|
107
|
+
constants.map {|c| const_get(c)}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,502 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with ActiveModel classes.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# If using ActiveModel directly within your class, then any one of the
|
8
|
+
# following features need to be included in order for the integration to be
|
9
|
+
# detected:
|
10
|
+
# * ActiveModel::Dirty
|
11
|
+
# * ActiveModel::Observing
|
12
|
+
# * ActiveModel::Validations
|
13
|
+
#
|
14
|
+
# Below is an example of a simple state machine defined within an
|
15
|
+
# ActiveModel class:
|
16
|
+
#
|
17
|
+
# class Vehicle
|
18
|
+
# include ActiveModel::Dirty
|
19
|
+
# include ActiveModel::Observing
|
20
|
+
# include ActiveModel::Validations
|
21
|
+
#
|
22
|
+
# attr_accessor :state
|
23
|
+
# define_attribute_methods [:state]
|
24
|
+
#
|
25
|
+
# state_machine :initial => :parked do
|
26
|
+
# event :ignite do
|
27
|
+
# transition :parked => :idling
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# The examples in the sections below will use the above class as a
|
33
|
+
# reference.
|
34
|
+
#
|
35
|
+
# == Actions
|
36
|
+
#
|
37
|
+
# By default, no action will be invoked when a state is transitioned. This
|
38
|
+
# means that if you want to save changes when transitioning, you must
|
39
|
+
# define the action yourself like so:
|
40
|
+
#
|
41
|
+
# class Vehicle
|
42
|
+
# include ActiveModel::Validations
|
43
|
+
# attr_accessor :state
|
44
|
+
#
|
45
|
+
# state_machine :action => :save do
|
46
|
+
# ...
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def save
|
50
|
+
# # Save changes
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# == Validation errors
|
55
|
+
#
|
56
|
+
# In order to hook in validation support for your model, the
|
57
|
+
# ActiveModel::Validations feature must be included. If this is included
|
58
|
+
# and an event fails to successfully fire because there are no matching
|
59
|
+
# transitions for the object, a validation error is added to the object's
|
60
|
+
# state attribute to help in determining why it failed.
|
61
|
+
#
|
62
|
+
# For example,
|
63
|
+
#
|
64
|
+
# vehicle = Vehicle.new
|
65
|
+
# vehicle.ignite # => false
|
66
|
+
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
|
67
|
+
#
|
68
|
+
# === Security implications
|
69
|
+
#
|
70
|
+
# Beware that public event attributes mean that events can be fired
|
71
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
72
|
+
# users from tampering with events through URLs / forms, the attribute
|
73
|
+
# should be protected like so:
|
74
|
+
#
|
75
|
+
# class Vehicle
|
76
|
+
# include ActiveModel::MassAssignmentSecurity
|
77
|
+
# attr_accessor :state
|
78
|
+
#
|
79
|
+
# attr_protected :state_event
|
80
|
+
# # attr_accessible ... # Alternative technique
|
81
|
+
#
|
82
|
+
# state_machine do
|
83
|
+
# ...
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# If you want to only have *some* events be able to fire via mass-assignment,
|
88
|
+
# you can build two state machines (one public and one protected) like so:
|
89
|
+
#
|
90
|
+
# class Vehicle
|
91
|
+
# include ActiveModel::MassAssignmentSecurity
|
92
|
+
# attr_accessor :state
|
93
|
+
#
|
94
|
+
# attr_protected :state_event # Prevent access to events in the first machine
|
95
|
+
#
|
96
|
+
# state_machine do
|
97
|
+
# # Define private events here
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# # Public machine targets the same state as the private machine
|
101
|
+
# state_machine :public_state, :attribute => :state do
|
102
|
+
# # Define public events here
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# == Callbacks
|
107
|
+
#
|
108
|
+
# All before/after transition callbacks defined for ActiveModel models
|
109
|
+
# behave in the same way that other ActiveSupport callbacks behave. The
|
110
|
+
# object involved in the transition is passed in as an argument.
|
111
|
+
#
|
112
|
+
# For example,
|
113
|
+
#
|
114
|
+
# class Vehicle
|
115
|
+
# include ActiveModel::Validations
|
116
|
+
# attr_accessor :state
|
117
|
+
#
|
118
|
+
# state_machine :initial => :parked do
|
119
|
+
# before_transition any => :idling do |vehicle|
|
120
|
+
# vehicle.put_on_seatbelt
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# before_transition do |vehicle, transition|
|
124
|
+
# # log message
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# event :ignite do
|
128
|
+
# transition :parked => :idling
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# def put_on_seatbelt
|
133
|
+
# ...
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# Note, also, that the transition can be accessed by simply defining
|
138
|
+
# additional arguments in the callback block.
|
139
|
+
#
|
140
|
+
# == Observers
|
141
|
+
#
|
142
|
+
# In order to hook in observer support for your application, the
|
143
|
+
# ActiveModel::Observing feature must be included. Because of the way
|
144
|
+
# ActiveModel observers are designed, there is less flexibility around the
|
145
|
+
# specific transitions that can be hooked in. However, a large number of
|
146
|
+
# hooks *are* supported. For example, if a transition for a object's
|
147
|
+
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
148
|
+
# +ignite+ event, the following observer methods are supported:
|
149
|
+
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
150
|
+
# * before/after/after_failure_to-_ignite_from_parked
|
151
|
+
# * before/after/after_failure_to-_ignite_to_idling
|
152
|
+
# * before/after/after_failure_to-_ignite
|
153
|
+
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
154
|
+
# * before/after/after_failure_to-_transition_state_from_parked
|
155
|
+
# * before/after/after_failure_to-_transition_state_to_idling
|
156
|
+
# * before/after/after_failure_to-_transition_state
|
157
|
+
# * before/after/after_failure_to-_transition
|
158
|
+
#
|
159
|
+
# The following class shows an example of some of these hooks:
|
160
|
+
#
|
161
|
+
# class VehicleObserver < ActiveModel::Observer
|
162
|
+
# # Callback for :ignite event *before* the transition is performed
|
163
|
+
# def before_ignite(vehicle, transition)
|
164
|
+
# # log message
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# # Callback for :ignite event *after* the transition has been performed
|
168
|
+
# def after_ignite(vehicle, transition)
|
169
|
+
# # put on seatbelt
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# # Generic transition callback *before* the transition is performed
|
173
|
+
# def after_transition(vehicle, transition)
|
174
|
+
# Audit.log(vehicle, transition)
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# def after_failure_to_transition(vehicle, transition)
|
178
|
+
# Audit.error(vehicle, transition)
|
179
|
+
# end
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# More flexible transition callbacks can be defined directly within the
|
183
|
+
# model as described in StateMachine::Machine#before_transition
|
184
|
+
# and StateMachine::Machine#after_transition.
|
185
|
+
#
|
186
|
+
# To define a single observer for multiple state machines:
|
187
|
+
#
|
188
|
+
# class StateMachineObserver < ActiveModel::Observer
|
189
|
+
# observe Vehicle, Switch, Project
|
190
|
+
#
|
191
|
+
# def after_transition(object, transition)
|
192
|
+
# Audit.log(object, transition)
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# == Dirty Attribute Tracking
|
197
|
+
#
|
198
|
+
# In order to hook in validation support for your model, the
|
199
|
+
# ActiveModel::Validations feature must be included. If this is included
|
200
|
+
# then state attributes will always be properly marked as changed whether
|
201
|
+
# they were a callback or not.
|
202
|
+
#
|
203
|
+
# For example,
|
204
|
+
#
|
205
|
+
# class Vehicle
|
206
|
+
# include ActiveModel::Dirty
|
207
|
+
# attr_accessor :state
|
208
|
+
#
|
209
|
+
# state_machine :initial => :parked do
|
210
|
+
# event :park do
|
211
|
+
# transition :parked => :parked
|
212
|
+
# end
|
213
|
+
# end
|
214
|
+
# end
|
215
|
+
#
|
216
|
+
# vehicle = Vehicle.new
|
217
|
+
# vehicle.changed # => []
|
218
|
+
# vehicle.park # => true
|
219
|
+
# vehicle.changed # => ["state"]
|
220
|
+
#
|
221
|
+
# == Creating new integrations
|
222
|
+
#
|
223
|
+
# If you want to integrate state_machine with an ORM that implements parts
|
224
|
+
# or all of the ActiveModel API, the following features must be specified:
|
225
|
+
# * i18n scope (locale)
|
226
|
+
# * Machine defaults
|
227
|
+
#
|
228
|
+
# For example,
|
229
|
+
#
|
230
|
+
# module StateMachine::Integrations::MyORM
|
231
|
+
# include StateMachine::Integrations::ActiveModel
|
232
|
+
#
|
233
|
+
# @defaults = {:action = > :persist}
|
234
|
+
#
|
235
|
+
# def self.matches?(klass)
|
236
|
+
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# def self.extended(base)
|
240
|
+
# locale = "#{File.dirname(__FILE__)}/my_orm/locale.rb"
|
241
|
+
# I18n.load_path << locale unless I18n.load_path.include?(locale)
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
# protected
|
245
|
+
# def runs_validations_on_action?
|
246
|
+
# action == :persist
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# def i18n_scope
|
250
|
+
# :myorm
|
251
|
+
# end
|
252
|
+
# end
|
253
|
+
#
|
254
|
+
# If you wish to implement other features, such as attribute initialization
|
255
|
+
# with protected attributes, named scopes, or database transactions, you
|
256
|
+
# must add these independent of the ActiveModel integration. See the
|
257
|
+
# ActiveRecord implementation for examples of these customizations.
|
258
|
+
module ActiveModel
|
259
|
+
def self.included(base) #:nodoc:
|
260
|
+
base.versions.unshift(*versions)
|
261
|
+
end
|
262
|
+
|
263
|
+
include Base
|
264
|
+
extend ClassMethods
|
265
|
+
|
266
|
+
require 'state_machine/integrations/active_model/versions'
|
267
|
+
|
268
|
+
@defaults = {}
|
269
|
+
|
270
|
+
# Whether this integration is available. Only true if ActiveModel is
|
271
|
+
# defined.
|
272
|
+
def self.available?
|
273
|
+
defined?(::ActiveModel)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Should this integration be used for state machines in the given class?
|
277
|
+
# Classes that include ActiveModel::Dirty, ActiveModel::Observing, or
|
278
|
+
# ActiveModel::Validations will automatically use the ActiveModel
|
279
|
+
# integration.
|
280
|
+
def self.matches?(klass)
|
281
|
+
%w(Dirty Observing Validations).any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
|
282
|
+
end
|
283
|
+
|
284
|
+
# Forces the change in state to be recognized regardless of whether the
|
285
|
+
# state value actually changed
|
286
|
+
def write(object, attribute, value, *args)
|
287
|
+
result = super
|
288
|
+
|
289
|
+
if (attribute == :state || attribute == :event && value) && supports_dirty_tracking?(object) && !object.send("#{self.attribute}_changed?")
|
290
|
+
object.send("#{self.attribute}_will_change!")
|
291
|
+
end
|
292
|
+
|
293
|
+
result
|
294
|
+
end
|
295
|
+
|
296
|
+
# Adds a validation error to the given object
|
297
|
+
def invalidate(object, attribute, message, values = [])
|
298
|
+
if supports_validations?
|
299
|
+
attribute = self.attribute(attribute)
|
300
|
+
options = values.inject({}) do |options, (key, value)|
|
301
|
+
options[key] = value
|
302
|
+
options
|
303
|
+
end
|
304
|
+
|
305
|
+
default_options = default_error_message_options(object, attribute, message)
|
306
|
+
object.errors.add(attribute, message, options.merge(default_options))
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Resets any errors previously added when invalidating the given object
|
311
|
+
def reset(object)
|
312
|
+
object.errors.clear if supports_validations?
|
313
|
+
end
|
314
|
+
|
315
|
+
protected
|
316
|
+
# Whether observers are supported in the integration. Only true if
|
317
|
+
# ActiveModel::Observer is available.
|
318
|
+
def supports_observers?
|
319
|
+
defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
|
320
|
+
end
|
321
|
+
|
322
|
+
# Whether validations are supported in the integration. Only true if
|
323
|
+
# the ActiveModel feature is enabled on the owner class.
|
324
|
+
def supports_validations?
|
325
|
+
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
|
326
|
+
end
|
327
|
+
|
328
|
+
# Do validations run when the action configured this machine is
|
329
|
+
# invoked? This is used to determine whether to fire off attribute-based
|
330
|
+
# event transitions when the action is run.
|
331
|
+
def runs_validations_on_action?
|
332
|
+
false
|
333
|
+
end
|
334
|
+
|
335
|
+
# Whether change (dirty) tracking is supported in the integration.
|
336
|
+
# Only true if the ActiveModel feature is enabled on the owner class.
|
337
|
+
def supports_dirty_tracking?(object)
|
338
|
+
defined?(::ActiveModel::Dirty) && owner_class <= ::ActiveModel::Dirty && object.respond_to?("#{self.attribute}_changed?")
|
339
|
+
end
|
340
|
+
|
341
|
+
# Gets the terminator to use for callbacks
|
342
|
+
def callback_terminator
|
343
|
+
@terminator ||= lambda {|result| result == false}
|
344
|
+
end
|
345
|
+
|
346
|
+
# Determines the base scope to use when looking up translations
|
347
|
+
def i18n_scope(klass)
|
348
|
+
klass.i18n_scope
|
349
|
+
end
|
350
|
+
|
351
|
+
# The default options to use when generating messages for validation
|
352
|
+
# errors
|
353
|
+
def default_error_message_options(object, attribute, message)
|
354
|
+
{:message => @messages[message]}
|
355
|
+
end
|
356
|
+
|
357
|
+
# Translates the given key / value combo. Translation keys are looked
|
358
|
+
# up in the following order:
|
359
|
+
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
|
360
|
+
# * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}</tt>
|
361
|
+
# * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
|
362
|
+
#
|
363
|
+
# If no keys are found, then the humanized value will be the fallback.
|
364
|
+
def translate(klass, key, value)
|
365
|
+
ancestors = ancestors_for(klass)
|
366
|
+
group = key.to_s.pluralize
|
367
|
+
value = value ? value.to_s : 'nil'
|
368
|
+
|
369
|
+
# Generate all possible translation keys
|
370
|
+
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{name}.#{group}.#{value}"}
|
371
|
+
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
|
372
|
+
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
|
373
|
+
end
|
374
|
+
|
375
|
+
# Build a list of ancestors for the given class to use when
|
376
|
+
# determining which localization key to use for a particular string.
|
377
|
+
def ancestors_for(klass)
|
378
|
+
klass.lookup_ancestors
|
379
|
+
end
|
380
|
+
|
381
|
+
# Initializes class-level extensions and defaults for this machine
|
382
|
+
def after_initialize
|
383
|
+
load_locale
|
384
|
+
load_observer_extensions
|
385
|
+
add_default_callbacks
|
386
|
+
end
|
387
|
+
|
388
|
+
# Loads any locale files needed for translating validation errors
|
389
|
+
def load_locale
|
390
|
+
I18n.load_path.unshift(@integration.locale_path) unless I18n.load_path.include?(@integration.locale_path)
|
391
|
+
end
|
392
|
+
|
393
|
+
# Loads extensions to ActiveModel's Observers
|
394
|
+
def load_observer_extensions
|
395
|
+
require 'state_machine/integrations/active_model/observer'
|
396
|
+
end
|
397
|
+
|
398
|
+
# Adds a set of default callbacks that utilize the Observer extensions
|
399
|
+
def add_default_callbacks
|
400
|
+
if supports_observers?
|
401
|
+
callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
|
402
|
+
callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
|
403
|
+
callbacks[:failure] << Callback.new(:failure) {|object, transition| notify(:after_failure_to, object, transition)}
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Skips defining reader/writer methods since this is done automatically
|
408
|
+
def define_state_accessor
|
409
|
+
name = self.name
|
410
|
+
|
411
|
+
owner_class.validates_each(attribute) do |object, attr, value|
|
412
|
+
machine = object.class.state_machine(name)
|
413
|
+
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
|
414
|
+
end if supports_validations?
|
415
|
+
end
|
416
|
+
|
417
|
+
# Adds hooks into validation for automatically firing events
|
418
|
+
def define_action_helpers
|
419
|
+
super
|
420
|
+
define_validation_hook if runs_validations_on_action?
|
421
|
+
end
|
422
|
+
|
423
|
+
# Hooks into validations by defining around callbacks for the
|
424
|
+
# :validation event
|
425
|
+
def define_validation_hook
|
426
|
+
owner_class.set_callback(:validation, :around, self, :prepend => true)
|
427
|
+
end
|
428
|
+
|
429
|
+
# Runs state events around the object's validation process
|
430
|
+
def around_validation(object)
|
431
|
+
object.class.state_machines.transitions(object, action, :after => false).perform { yield }
|
432
|
+
end
|
433
|
+
|
434
|
+
# Creates a new callback in the callback chain, always inserting it
|
435
|
+
# before the default Observer callbacks that were created after
|
436
|
+
# initialization.
|
437
|
+
def add_callback(type, options, &block)
|
438
|
+
options[:terminator] = callback_terminator
|
439
|
+
|
440
|
+
if supports_observers?
|
441
|
+
@callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
|
442
|
+
add_states(callback.known_states)
|
443
|
+
callback
|
444
|
+
else
|
445
|
+
super
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Configures new states with the built-in humanize scheme
|
450
|
+
def add_states(new_states)
|
451
|
+
super.each do |state|
|
452
|
+
state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# Configures new event with the built-in humanize scheme
|
457
|
+
def add_events(new_events)
|
458
|
+
super.each do |event|
|
459
|
+
event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Notifies observers on the given object that a callback occurred
|
464
|
+
# involving the given transition. This will attempt to call the
|
465
|
+
# following methods on observers:
|
466
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}_to_#{to}</tt>
|
467
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}</tt>
|
468
|
+
# * <tt>#{type}_#{qualified_event}_to_#{to}</tt>
|
469
|
+
# * <tt>#{type}_#{qualified_event}</tt>
|
470
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}_to_#{to}</tt>
|
471
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}</tt>
|
472
|
+
# * <tt>#{type}_transition_#{machine_name}_to_#{to}</tt>
|
473
|
+
# * <tt>#{type}_transition_#{machine_name}</tt>
|
474
|
+
# * <tt>#{type}_transition</tt>
|
475
|
+
#
|
476
|
+
# This will always return true regardless of the results of the
|
477
|
+
# callbacks.
|
478
|
+
def notify(type, object, transition)
|
479
|
+
name = self.name
|
480
|
+
event = transition.qualified_event
|
481
|
+
from = transition.from_name
|
482
|
+
to = transition.to_name
|
483
|
+
|
484
|
+
# Machine-specific updates
|
485
|
+
["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
|
486
|
+
["_from_#{from}", nil].each do |from_segment|
|
487
|
+
["_to_#{to}", nil].each do |to_segment|
|
488
|
+
object.class.changed if object.class.respond_to?(:changed)
|
489
|
+
object.class.notify_observers([event_segment, from_segment, to_segment].join, object, transition)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Generic updates
|
495
|
+
object.class.changed if object.class.respond_to?(:changed)
|
496
|
+
object.class.notify_observers("#{type}_transition", object, transition)
|
497
|
+
|
498
|
+
true
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|