hsume2-state_machine 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. 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,4 @@
1
+ # Load each application initializer
2
+ Dir["#{File.dirname(__FILE__)}/initializers/*.rb"].sort.each do |path|
3
+ require "state_machine/initializers/#{File.basename(path)}"
4
+ 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