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,20 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(filename), binding, filename)
3
+ translations[:en][:activerecord] = translations[:en].delete(:activemodel)
4
+
5
+ # Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
6
+ # therefore possibly have I18n::VERSION available)
7
+ begin
8
+ require 'i18n/version'
9
+ rescue Exception => ex
10
+ end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
11
+
12
+ # Only i18n 0.4.0+ has the new %{key} syntax
13
+ if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
14
+ translations[:en][:activerecord][:errors][:messages].each do |key, message|
15
+ message.gsub!('%{', '{{')
16
+ message.gsub!('}', '}}')
17
+ end
18
+ end
19
+
20
+ translations
@@ -0,0 +1,143 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveRecord
4
+ version '2.x' do
5
+ def self.active?
6
+ ::ActiveRecord::VERSION::MAJOR == 2
7
+ end
8
+
9
+ def load_locale
10
+ super if defined?(I18n)
11
+ end
12
+
13
+ def create_scope(name, scope)
14
+ if owner_class.respond_to?(:named_scope)
15
+ name = name.to_sym
16
+ machine_name = self.name
17
+
18
+ # Since ActiveRecord does not allow direct access to the model
19
+ # being used within the evaluation of a dynamic named scope, the
20
+ # scope must be generated manually. It's necessary to have access
21
+ # to the model so that the state names can be translated to their
22
+ # associated values and so that inheritance is respected properly.
23
+ owner_class.named_scope(name)
24
+ owner_class.scopes[name] = lambda do |model, *states|
25
+ machine_states = model.state_machine(machine_name).states
26
+ values = states.flatten.map {|state| machine_states.fetch(state).value}
27
+
28
+ ::ActiveRecord::NamedScope::Scope.new(model, :conditions => scope.call(values))
29
+ end
30
+ end
31
+
32
+ # Prevent the Machine class from wrapping the scope
33
+ false
34
+ end
35
+
36
+ def invalidate(object, attribute, message, values = [])
37
+ if defined?(I18n)
38
+ super
39
+ else
40
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
41
+ end
42
+ end
43
+
44
+ def translate(klass, key, value)
45
+ if defined?(I18n)
46
+ super
47
+ else
48
+ value ? value.to_s.humanize.downcase : 'nil'
49
+ end
50
+ end
51
+
52
+ def supports_observers?
53
+ true
54
+ end
55
+
56
+ def supports_validations?
57
+ true
58
+ end
59
+
60
+ def supports_mass_assignment_security?
61
+ true
62
+ end
63
+
64
+ def i18n_scope(klass)
65
+ :activerecord
66
+ end
67
+
68
+ def action_hook
69
+ action == :save ? :create_or_update : super
70
+ end
71
+
72
+ def load_observer_extensions
73
+ super
74
+ ::ActiveRecord::Observer.class_eval do
75
+ include StateMachine::Integrations::ActiveModel::Observer
76
+ end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
77
+ end
78
+ end
79
+
80
+ version '2.0 - 2.2.x' do
81
+ def self.active?
82
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR < 3
83
+ end
84
+
85
+ def default_error_message_options(object, attribute, message)
86
+ {:default => @messages[message]}
87
+ end
88
+ end
89
+
90
+ version '2.0 - 2.3.1' do
91
+ def self.active?
92
+ ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 2)
93
+ end
94
+
95
+ def ancestors_for(klass)
96
+ klass.self_and_descendents_from_active_record
97
+ end
98
+ end
99
+
100
+ version '2.0.x' do
101
+ def self.active?
102
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 0
103
+ end
104
+
105
+ def supports_dirty_tracking?(object)
106
+ false
107
+ end
108
+ end
109
+
110
+ version '2.1.x - 2.3.x' do
111
+ def self.active?
112
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR > 0
113
+ end
114
+
115
+ def supports_dirty_tracking?(object)
116
+ object.respond_to?("#{attribute}_changed?")
117
+ end
118
+ end
119
+
120
+ version '2.3.2 - 2.3.x' do
121
+ def self.active?
122
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
123
+ end
124
+
125
+ def ancestors_for(klass)
126
+ klass.self_and_descendants_from_active_record
127
+ end
128
+ end
129
+
130
+ version '3.0.x' do
131
+ def self.active?
132
+ ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR == 0
133
+ end
134
+
135
+ def define_action_hook
136
+ # +around+ callbacks don't have direct access to results until AS 3.1
137
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
138
+ super
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,91 @@
1
+ module StateMachine
2
+ module Integrations
3
+ # Provides a set of base helpers for managing individual integrations
4
+ module Base
5
+ module ClassMethods
6
+ # The default options to use for state machines using this integration
7
+ attr_reader :defaults
8
+
9
+ # The name of the integration
10
+ def integration_name
11
+ @integration_name ||= begin
12
+ name = self.name.split('::').last
13
+ name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
14
+ name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
15
+ name.downcase!
16
+ name.to_sym
17
+ end
18
+ end
19
+
20
+ # Whether this integration is available for the current library. This
21
+ # is usually only true if the ORM that the integration is for is
22
+ # currently defined. Default is false.
23
+ def available?
24
+ false
25
+ end
26
+
27
+ # Whether the integration should be used for the given class. Default
28
+ # is false.
29
+ def matches?(klass)
30
+ false
31
+ end
32
+
33
+ # Tracks the various version overrides for an integration
34
+ def versions
35
+ @versions ||= []
36
+ end
37
+
38
+ # Creates a new version override for an integration. When this
39
+ # integration is activated, each version that is marked as active will
40
+ # also extend the integration.
41
+ #
42
+ # == Example
43
+ #
44
+ # module StateMachine
45
+ # module Integrations
46
+ # module ORMLibrary
47
+ # version '0.2.x - 0.3.x' do
48
+ # def self.active?
49
+ # ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
50
+ # end
51
+ #
52
+ # def invalidate(object, attribute, message, values = [])
53
+ # # Override here...
54
+ # end
55
+ # end
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # In the above example, a version override is defined for the ORMLibrary
61
+ # integration when the version is between 0.2.x and 0.3.x.
62
+ def version(name, &block)
63
+ versions << mod = Module.new(&block)
64
+ mod
65
+ end
66
+
67
+ # The path to the locale file containing translations for this
68
+ # integration. This file will only exist for integrations that actually
69
+ # support i18n.
70
+ def locale_path
71
+ path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
72
+ path if File.exists?(path)
73
+ end
74
+
75
+ # Extends the given object with any version overrides that are currently
76
+ # active
77
+ def extended(base)
78
+ versions.each do |version|
79
+ base.extend(version) if version.active?
80
+ end
81
+ end
82
+ end
83
+
84
+ extend ClassMethods
85
+
86
+ def self.included(base) #:nodoc:
87
+ base.class_eval { extend ClassMethods }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,392 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with DataMapper resources.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # DataMapper resource:
9
+ #
10
+ # class Vehicle
11
+ # include DataMapper::Resource
12
+ #
13
+ # property :id, Serial
14
+ # property :name, String
15
+ # property :state, String
16
+ #
17
+ # state_machine :initial => :parked do
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # The examples in the sections below will use the above class as a
25
+ # reference.
26
+ #
27
+ # == Actions
28
+ #
29
+ # By default, the action that will be invoked when a state is transitioned
30
+ # is the +save+ action. This will cause the resource to save the changes
31
+ # made to the state machine's attribute. *Note* that if any other changes
32
+ # were made to the resource prior to transition, then those changes will
33
+ # be saved as well.
34
+ #
35
+ # For example,
36
+ #
37
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
38
+ # vehicle.name = 'Ford Explorer'
39
+ # vehicle.ignite # => true
40
+ # vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
41
+ #
42
+ # == Events
43
+ #
44
+ # As described in StateMachine::InstanceMethods#state_machine, event
45
+ # attributes are created for every machine that allow transitions to be
46
+ # performed automatically when the object's action (in this case, :save)
47
+ # is called.
48
+ #
49
+ # In DataMapper, these automated events are run in the following order:
50
+ # * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
51
+ # * before save - If validation feature was skipped/not loaded, run before callbacks and persist new states, then save
52
+ # * after save - Run after callbacks
53
+ #
54
+ # For example,
55
+ #
56
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
57
+ # vehicle.state_event # => nil
58
+ # vehicle.state_event = 'invalid'
59
+ # vehicle.valid? # => false
60
+ # vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"state_event"=>["is invalid"]}>
61
+ #
62
+ # vehicle.state_event = 'ignite'
63
+ # vehicle.valid? # => true
64
+ # vehicle.save # => true
65
+ # vehicle.state # => "idling"
66
+ # vehicle.state_event # => nil
67
+ #
68
+ # Note that this can also be done on a mass-assignment basis:
69
+ #
70
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
71
+ # vehicle.state # => "idling"
72
+ #
73
+ # This technique is always used for transitioning states when the +save+
74
+ # action (which is the default) is configured for the machine.
75
+ #
76
+ # === Security implications
77
+ #
78
+ # Beware that public event attributes mean that events can be fired
79
+ # whenever mass-assignment is being used. If you want to prevent malicious
80
+ # users from tampering with events through URLs / forms, the attribute
81
+ # should be protected like so:
82
+ #
83
+ # class Vehicle
84
+ # include DataMapper::Resource
85
+ # ...
86
+ #
87
+ # state_machine do
88
+ # ...
89
+ # end
90
+ # protected :state_event
91
+ # end
92
+ #
93
+ # If you want to only have *some* events be able to fire via mass-assignment,
94
+ # you can build two state machines (one public and one protected) like so:
95
+ #
96
+ # class Vehicle
97
+ # include DataMapper::Resource
98
+ # ...
99
+ #
100
+ # state_machine do
101
+ # # Define private events here
102
+ # end
103
+ # protected :state_event= # Prevent access to events in the first machine
104
+ #
105
+ # # Allow both machines to share the same state
106
+ # state_machine :public_state, :attribute => :state do
107
+ # # Define public events here
108
+ # end
109
+ # end
110
+ #
111
+ # == Transactions
112
+ #
113
+ # By default, the use of transactions during an event transition is
114
+ # turned off to be consistent with DataMapper. This means that if
115
+ # changes are made to the database during a before callback, but the
116
+ # transition fails to complete, those changes will *not* be rolled back.
117
+ #
118
+ # For example,
119
+ #
120
+ # class Message
121
+ # include DataMapper::Resource
122
+ #
123
+ # property :id, Serial
124
+ # property :content, String
125
+ # end
126
+ #
127
+ # Vehicle.state_machine do
128
+ # before_transition do |transition|
129
+ # Message.create(:content => transition.inspect)
130
+ # throw :halt
131
+ # end
132
+ # end
133
+ #
134
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
135
+ # vehicle.ignite # => false
136
+ # Message.all.count # => 1
137
+ #
138
+ # To turn on transactions:
139
+ #
140
+ # class Vehicle < ActiveRecord::Base
141
+ # state_machine :initial => :parked, :use_transactions => true do
142
+ # ...
143
+ # end
144
+ # end
145
+ #
146
+ # If using the +save+ action for the machine, this option will be ignored as
147
+ # the transaction behavior will depend on the +save+ implementation within
148
+ # DataMapper.
149
+ #
150
+ # == Validation errors
151
+ #
152
+ # If an event fails to successfully fire because there are no matching
153
+ # transitions for the current record, a validation error is added to the
154
+ # record's state attribute to help in determining why it failed and for
155
+ # reporting via the UI.
156
+ #
157
+ # For example,
158
+ #
159
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
160
+ # vehicle.ignite # => false
161
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
162
+ #
163
+ # If an event fails to fire because of a validation error on the record and
164
+ # *not* because a matching transition was not available, no error messages
165
+ # will be added to the state attribute.
166
+ #
167
+ # == Scopes
168
+ #
169
+ # To assist in filtering models with specific states, a series of class
170
+ # methods are defined on the model for finding records with or without a
171
+ # particular set of states.
172
+ #
173
+ # These named scopes are the functional equivalent of the following
174
+ # definitions:
175
+ #
176
+ # class Vehicle
177
+ # include DataMapper::Resource
178
+ #
179
+ # property :id, Serial
180
+ # property :state, String
181
+ #
182
+ # class << self
183
+ # def with_states(*states)
184
+ # all(:state => states.flatten)
185
+ # end
186
+ # alias_method :with_state, :with_states
187
+ #
188
+ # def without_states(*states)
189
+ # all(:state.not => states.flatten)
190
+ # end
191
+ # alias_method :without_state, :without_states
192
+ # end
193
+ # end
194
+ #
195
+ # *Note*, however, that the states are converted to their stored values
196
+ # before being passed into the query.
197
+ #
198
+ # Because of the way scopes work in DataMapper, they can be chained like
199
+ # so:
200
+ #
201
+ # Vehicle.with_state(:parked).all(:order => [:id.desc])
202
+ #
203
+ # == Callbacks / Observers
204
+ #
205
+ # All before/after transition callbacks defined for DataMapper resources
206
+ # behave in the same way that other DataMapper hooks behave. Rather than
207
+ # passing in the record as an argument to the callback, the callback is
208
+ # instead bound to the object and evaluated within its context.
209
+ #
210
+ # For example,
211
+ #
212
+ # class Vehicle
213
+ # include DataMapper::Resource
214
+ #
215
+ # property :id, Serial
216
+ # property :state, String
217
+ #
218
+ # state_machine :initial => :parked do
219
+ # before_transition any => :idling do
220
+ # put_on_seatbelt
221
+ # end
222
+ #
223
+ # before_transition do |transition|
224
+ # # log message
225
+ # end
226
+ #
227
+ # event :ignite do
228
+ # transition :parked => :idling
229
+ # end
230
+ # end
231
+ #
232
+ # def put_on_seatbelt
233
+ # ...
234
+ # end
235
+ # end
236
+ #
237
+ # Note, also, that the transition can be accessed by simply defining
238
+ # additional arguments in the callback block.
239
+ #
240
+ # In addition to support for DataMapper-like hooks, there is additional
241
+ # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
242
+ # for more information.
243
+ module DataMapper
244
+ include Base
245
+
246
+ require 'state_machine/integrations/data_mapper/versions'
247
+
248
+ # The default options to use for state machines using this integration
249
+ class << self; attr_reader :defaults; end
250
+ @defaults = {:action => :save, :use_transactions => false}
251
+
252
+ # Whether this integration is available. Only true if DataMapper::Resource
253
+ # is defined.
254
+ def self.available?
255
+ defined?(::DataMapper::Resource)
256
+ end
257
+
258
+ # Should this integration be used for state machines in the given class?
259
+ # Classes that include DataMapper::Resource will automatically use the
260
+ # DataMapper integration.
261
+ def self.matches?(klass)
262
+ klass <= ::DataMapper::Resource
263
+ end
264
+
265
+ # Loads additional files specific to DataMapper
266
+ def self.extended(base) #:nodoc:
267
+ require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
268
+ super
269
+ end
270
+
271
+ # Forces the change in state to be recognized regardless of whether the
272
+ # state value actually changed
273
+ def write(object, attribute, value, *args)
274
+ result = super
275
+
276
+ if attribute == :state || attribute == :event && value
277
+ value = read(object, :state) if attribute == :event
278
+ mark_dirty(object, value)
279
+ end
280
+
281
+ result
282
+ end
283
+
284
+ # Adds a validation error to the given object
285
+ def invalidate(object, attribute, message, values = [])
286
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
287
+ end
288
+
289
+ # Resets any errors previously added when invalidating the given object
290
+ def reset(object)
291
+ object.errors.clear if supports_validations?
292
+ end
293
+
294
+ protected
295
+ # Initializes class-level extensions and defaults for this machine
296
+ def after_initialize
297
+ load_observer_extensions
298
+ end
299
+
300
+ # Loads extensions to DataMapper's Observers
301
+ def load_observer_extensions
302
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
303
+ end
304
+
305
+ # Is validation support currently loaded?
306
+ def supports_validations?
307
+ @supports_validations ||= ::DataMapper.const_defined?('Validate')
308
+ end
309
+
310
+ # Pluralizes the name using the built-in inflector
311
+ def pluralize(word)
312
+ ::DataMapper::Inflector.pluralize(word.to_s)
313
+ end
314
+
315
+ # Defines an initialization hook into the owner class for setting the
316
+ # initial state of the machine *before* any attributes are set on the
317
+ # object
318
+ def define_state_initializer
319
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
320
+ def initialize(*args)
321
+ self.class.state_machines.initialize_states(self) { super }
322
+ end
323
+ end_eval
324
+ end
325
+
326
+ # Skips defining reader/writer methods since this is done automatically
327
+ def define_state_accessor
328
+ owner_class.property(attribute, String) unless owner_class.properties.detect {|property| property.name == attribute}
329
+
330
+ if supports_validations?
331
+ name = self.name
332
+ owner_class.validates_with_block(attribute) do
333
+ machine = self.class.state_machine(name)
334
+ machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
335
+ end
336
+ end
337
+ end
338
+
339
+ # Adds hooks into validation for automatically firing events
340
+ def define_action_helpers
341
+ super
342
+
343
+ if action == :save && supports_validations?
344
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
345
+ def valid?(*)
346
+ self.class.state_machines.transitions(self, :save, :after => false).perform { super }
347
+ end
348
+ end_eval
349
+ end
350
+ end
351
+
352
+ # Uses internal save hooks if using the :save action
353
+ def action_hook
354
+ action == :save ? :save_self : super
355
+ end
356
+
357
+ # Creates a scope for finding records *with* a particular state or
358
+ # states for the attribute
359
+ def create_with_scope(name)
360
+ lambda {|resource, values| resource.all(attribute => values)}
361
+ end
362
+
363
+ # Creates a scope for finding records *without* a particular state or
364
+ # states for the attribute
365
+ def create_without_scope(name)
366
+ lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
367
+ end
368
+
369
+ # Runs a new database transaction, rolling back any changes if the
370
+ # yielded block fails (i.e. returns false).
371
+ def transaction(object)
372
+ object.class.transaction {|t| t.rollback unless yield}
373
+ end
374
+
375
+ # Creates a new callback in the callback chain, always ensuring that
376
+ # it's configured to bind to the object as this is the convention for
377
+ # DataMapper/Extlib callbacks
378
+ def add_callback(type, options, &block)
379
+ options[:bind_to_object] = true
380
+ super
381
+ end
382
+
383
+ # Marks the object's state as dirty so that the record will be saved
384
+ # even if no actual modifications have been made to the data
385
+ def mark_dirty(object, value)
386
+ object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) if object.persisted_state.is_a?(::DataMapper::Resource::State::Clean)
387
+ property = owner_class.properties[self.attribute]
388
+ object.persisted_state.original_attributes[property] = value unless object.persisted_state.original_attributes.include?(property)
389
+ end
390
+ end
391
+ end
392
+ end