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.
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