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