aasm 4.12.2 → 5.5.0

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 (218) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +1 -1
  3. data/README.md +393 -116
  4. data/lib/aasm/aasm.rb +30 -27
  5. data/lib/aasm/base.rb +64 -11
  6. data/lib/aasm/configuration.rb +6 -0
  7. data/lib/aasm/core/event.rb +26 -30
  8. data/lib/aasm/core/invoker.rb +129 -0
  9. data/lib/aasm/core/invokers/base_invoker.rb +75 -0
  10. data/lib/aasm/core/invokers/class_invoker.rb +52 -0
  11. data/lib/aasm/core/invokers/literal_invoker.rb +49 -0
  12. data/lib/aasm/core/invokers/proc_invoker.rb +59 -0
  13. data/lib/aasm/core/state.rb +16 -14
  14. data/lib/aasm/core/transition.rb +8 -69
  15. data/lib/aasm/dsl_helper.rb +24 -22
  16. data/lib/aasm/errors.rb +5 -3
  17. data/lib/aasm/instance_base.rb +34 -3
  18. data/lib/aasm/localizer.rb +13 -3
  19. data/lib/aasm/persistence/active_record_persistence.rb +25 -5
  20. data/lib/aasm/persistence/base.rb +14 -3
  21. data/lib/aasm/persistence/core_data_query_persistence.rb +2 -1
  22. data/lib/aasm/persistence/dynamoid_persistence.rb +1 -1
  23. data/lib/aasm/persistence/mongoid_persistence.rb +1 -1
  24. data/lib/aasm/persistence/no_brainer_persistence.rb +105 -0
  25. data/lib/aasm/persistence/orm.rb +26 -14
  26. data/lib/aasm/persistence/plain_persistence.rb +2 -1
  27. data/lib/aasm/persistence/redis_persistence.rb +1 -1
  28. data/lib/aasm/persistence/sequel_persistence.rb +0 -1
  29. data/lib/aasm/persistence.rb +3 -0
  30. data/lib/aasm/rspec/allow_event.rb +5 -1
  31. data/lib/aasm/rspec/allow_transition_to.rb +5 -1
  32. data/lib/aasm/rspec/transition_from.rb +5 -1
  33. data/lib/aasm/version.rb +1 -1
  34. data/lib/aasm.rb +5 -2
  35. data/lib/generators/aasm/orm_helpers.rb +7 -1
  36. data/lib/generators/active_record/aasm_generator.rb +3 -1
  37. data/lib/generators/active_record/templates/migration.rb +1 -1
  38. data/lib/generators/nobrainer/aasm_generator.rb +28 -0
  39. data/lib/motion-aasm.rb +1 -0
  40. metadata +42 -343
  41. data/.document +0 -6
  42. data/.gitignore +0 -20
  43. data/.travis.yml +0 -52
  44. data/API +0 -34
  45. data/Appraisals +0 -43
  46. data/CHANGELOG.md +0 -365
  47. data/CODE_OF_CONDUCT.md +0 -13
  48. data/CONTRIBUTING.md +0 -24
  49. data/Gemfile +0 -7
  50. data/HOWTO +0 -12
  51. data/PLANNED_CHANGES.md +0 -11
  52. data/README_FROM_VERSION_3_TO_4.md +0 -240
  53. data/Rakefile +0 -31
  54. data/TESTING.md +0 -25
  55. data/aasm.gemspec +0 -35
  56. data/callbacks.txt +0 -51
  57. data/gemfiles/rails_3.2.gemfile +0 -13
  58. data/gemfiles/rails_4.0.gemfile +0 -15
  59. data/gemfiles/rails_4.2.gemfile +0 -16
  60. data/gemfiles/rails_4.2_mongoid_5.gemfile +0 -11
  61. data/gemfiles/rails_5.0.gemfile +0 -14
  62. data/spec/database.rb +0 -44
  63. data/spec/database.yml +0 -3
  64. data/spec/en.yml +0 -12
  65. data/spec/en_deprecated_style.yml +0 -10
  66. data/spec/generators/active_record_generator_spec.rb +0 -47
  67. data/spec/generators/mongoid_generator_spec.rb +0 -31
  68. data/spec/models/active_record/basic_active_record_two_state_machines_example.rb +0 -25
  69. data/spec/models/active_record/complex_active_record_example.rb +0 -37
  70. data/spec/models/active_record/derivate_new_dsl.rb +0 -7
  71. data/spec/models/active_record/false_state.rb +0 -35
  72. data/spec/models/active_record/gate.rb +0 -39
  73. data/spec/models/active_record/invalid_persistor.rb +0 -29
  74. data/spec/models/active_record/localizer_test_model.rb +0 -34
  75. data/spec/models/active_record/no_direct_assignment.rb +0 -21
  76. data/spec/models/active_record/no_scope.rb +0 -21
  77. data/spec/models/active_record/persisted_state.rb +0 -12
  78. data/spec/models/active_record/provided_and_persisted_state.rb +0 -24
  79. data/spec/models/active_record/reader.rb +0 -7
  80. data/spec/models/active_record/readme_job.rb +0 -21
  81. data/spec/models/active_record/silent_persistor.rb +0 -29
  82. data/spec/models/active_record/simple_new_dsl.rb +0 -17
  83. data/spec/models/active_record/thief.rb +0 -29
  84. data/spec/models/active_record/transactor.rb +0 -99
  85. data/spec/models/active_record/transient.rb +0 -6
  86. data/spec/models/active_record/validator.rb +0 -118
  87. data/spec/models/active_record/with_enum.rb +0 -39
  88. data/spec/models/active_record/with_enum_without_column.rb +0 -38
  89. data/spec/models/active_record/with_false_enum.rb +0 -31
  90. data/spec/models/active_record/with_true_enum.rb +0 -39
  91. data/spec/models/active_record/worker.rb +0 -2
  92. data/spec/models/active_record/writer.rb +0 -6
  93. data/spec/models/basic_two_state_machines_example.rb +0 -25
  94. data/spec/models/callbacks/basic.rb +0 -98
  95. data/spec/models/callbacks/basic_multiple.rb +0 -75
  96. data/spec/models/callbacks/guard_within_block.rb +0 -67
  97. data/spec/models/callbacks/guard_within_block_multiple.rb +0 -66
  98. data/spec/models/callbacks/multiple_transitions_transition_guard.rb +0 -66
  99. data/spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb +0 -65
  100. data/spec/models/callbacks/private_method.rb +0 -44
  101. data/spec/models/callbacks/private_method_multiple.rb +0 -44
  102. data/spec/models/callbacks/with_args.rb +0 -62
  103. data/spec/models/callbacks/with_args_multiple.rb +0 -61
  104. data/spec/models/callbacks/with_state_arg.rb +0 -30
  105. data/spec/models/callbacks/with_state_arg_multiple.rb +0 -26
  106. data/spec/models/complex_example.rb +0 -222
  107. data/spec/models/conversation.rb +0 -93
  108. data/spec/models/default_state.rb +0 -12
  109. data/spec/models/double_definer.rb +0 -21
  110. data/spec/models/dynamoid/complex_dynamoid_example.rb +0 -37
  111. data/spec/models/dynamoid/dynamoid_multiple.rb +0 -18
  112. data/spec/models/dynamoid/dynamoid_simple.rb +0 -18
  113. data/spec/models/foo.rb +0 -106
  114. data/spec/models/foo_callback_multiple.rb +0 -45
  115. data/spec/models/guard_arguments_check.rb +0 -17
  116. data/spec/models/guard_with_params.rb +0 -24
  117. data/spec/models/guard_with_params_multiple.rb +0 -18
  118. data/spec/models/guardian.rb +0 -58
  119. data/spec/models/guardian_multiple.rb +0 -48
  120. data/spec/models/guardian_without_from_specified.rb +0 -18
  121. data/spec/models/initial_state_proc.rb +0 -31
  122. data/spec/models/mongoid/complex_mongoid_example.rb +0 -37
  123. data/spec/models/mongoid/invalid_persistor_mongoid.rb +0 -39
  124. data/spec/models/mongoid/mongoid_relationships.rb +0 -26
  125. data/spec/models/mongoid/no_scope_mongoid.rb +0 -21
  126. data/spec/models/mongoid/silent_persistor_mongoid.rb +0 -39
  127. data/spec/models/mongoid/simple_mongoid.rb +0 -23
  128. data/spec/models/mongoid/simple_new_dsl_mongoid.rb +0 -25
  129. data/spec/models/mongoid/validator_mongoid.rb +0 -100
  130. data/spec/models/multi_transitioner.rb +0 -34
  131. data/spec/models/multiple_transitions_that_differ_only_by_guard.rb +0 -31
  132. data/spec/models/namespaced_multiple_example.rb +0 -42
  133. data/spec/models/no_initial_state.rb +0 -25
  134. data/spec/models/not_auto_loaded/process.rb +0 -21
  135. data/spec/models/parametrised_event.rb +0 -42
  136. data/spec/models/parametrised_event_multiple.rb +0 -29
  137. data/spec/models/process_with_new_dsl.rb +0 -31
  138. data/spec/models/provided_state.rb +0 -24
  139. data/spec/models/redis/complex_redis_example.rb +0 -40
  140. data/spec/models/redis/redis_multiple.rb +0 -20
  141. data/spec/models/redis/redis_simple.rb +0 -20
  142. data/spec/models/sequel/complex_sequel_example.rb +0 -46
  143. data/spec/models/sequel/invalid_persistor.rb +0 -52
  144. data/spec/models/sequel/sequel_multiple.rb +0 -25
  145. data/spec/models/sequel/sequel_simple.rb +0 -26
  146. data/spec/models/sequel/silent_persistor.rb +0 -50
  147. data/spec/models/sequel/transactor.rb +0 -112
  148. data/spec/models/sequel/validator.rb +0 -93
  149. data/spec/models/sequel/worker.rb +0 -12
  150. data/spec/models/silencer.rb +0 -27
  151. data/spec/models/simple_custom_example.rb +0 -53
  152. data/spec/models/simple_example.rb +0 -15
  153. data/spec/models/simple_multiple_example.rb +0 -42
  154. data/spec/models/state_machine_with_failed_event.rb +0 -20
  155. data/spec/models/states_on_one_line_example.rb +0 -8
  156. data/spec/models/sub_class.rb +0 -41
  157. data/spec/models/sub_class_with_more_states.rb +0 -18
  158. data/spec/models/sub_classing.rb +0 -3
  159. data/spec/models/super_class.rb +0 -46
  160. data/spec/models/this_name_better_not_be_in_use.rb +0 -11
  161. data/spec/models/valid_state_name.rb +0 -23
  162. data/spec/spec_helper.rb +0 -26
  163. data/spec/spec_helpers/active_record.rb +0 -7
  164. data/spec/spec_helpers/dynamoid.rb +0 -33
  165. data/spec/spec_helpers/mongoid.rb +0 -7
  166. data/spec/spec_helpers/redis.rb +0 -15
  167. data/spec/spec_helpers/remove_warnings.rb +0 -1
  168. data/spec/spec_helpers/sequel.rb +0 -7
  169. data/spec/unit/api_spec.rb +0 -100
  170. data/spec/unit/basic_two_state_machines_example_spec.rb +0 -10
  171. data/spec/unit/callback_multiple_spec.rb +0 -300
  172. data/spec/unit/callbacks_spec.rb +0 -491
  173. data/spec/unit/complex_example_spec.rb +0 -84
  174. data/spec/unit/complex_multiple_example_spec.rb +0 -99
  175. data/spec/unit/edge_cases_spec.rb +0 -16
  176. data/spec/unit/event_multiple_spec.rb +0 -73
  177. data/spec/unit/event_naming_spec.rb +0 -16
  178. data/spec/unit/event_spec.rb +0 -381
  179. data/spec/unit/exception_spec.rb +0 -11
  180. data/spec/unit/guard_arguments_check_spec.rb +0 -9
  181. data/spec/unit/guard_multiple_spec.rb +0 -60
  182. data/spec/unit/guard_spec.rb +0 -89
  183. data/spec/unit/guard_with_params_multiple_spec.rb +0 -10
  184. data/spec/unit/guard_with_params_spec.rb +0 -14
  185. data/spec/unit/guard_without_from_specified_spec.rb +0 -10
  186. data/spec/unit/initial_state_multiple_spec.rb +0 -15
  187. data/spec/unit/initial_state_spec.rb +0 -12
  188. data/spec/unit/inspection_multiple_spec.rb +0 -201
  189. data/spec/unit/inspection_spec.rb +0 -149
  190. data/spec/unit/localizer_spec.rb +0 -78
  191. data/spec/unit/memory_leak_spec.rb +0 -38
  192. data/spec/unit/multiple_transitions_that_differ_only_by_guard_spec.rb +0 -14
  193. data/spec/unit/namespaced_multiple_example_spec.rb +0 -75
  194. data/spec/unit/new_dsl_spec.rb +0 -12
  195. data/spec/unit/override_warning_spec.rb +0 -94
  196. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +0 -618
  197. data/spec/unit/persistence/active_record_persistence_spec.rb +0 -721
  198. data/spec/unit/persistence/dynamoid_persistence_multiple_spec.rb +0 -135
  199. data/spec/unit/persistence/dynamoid_persistence_spec.rb +0 -84
  200. data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +0 -204
  201. data/spec/unit/persistence/mongoid_persistence_spec.rb +0 -169
  202. data/spec/unit/persistence/redis_persistence_multiple_spec.rb +0 -88
  203. data/spec/unit/persistence/redis_persistence_spec.rb +0 -53
  204. data/spec/unit/persistence/sequel_persistence_multiple_spec.rb +0 -148
  205. data/spec/unit/persistence/sequel_persistence_spec.rb +0 -368
  206. data/spec/unit/readme_spec.rb +0 -41
  207. data/spec/unit/reloading_spec.rb +0 -15
  208. data/spec/unit/rspec_matcher_spec.rb +0 -79
  209. data/spec/unit/simple_custom_example_spec.rb +0 -39
  210. data/spec/unit/simple_example_spec.rb +0 -42
  211. data/spec/unit/simple_multiple_example_spec.rb +0 -91
  212. data/spec/unit/state_spec.rb +0 -89
  213. data/spec/unit/states_on_one_line_example_spec.rb +0 -16
  214. data/spec/unit/subclassing_multiple_spec.rb +0 -74
  215. data/spec/unit/subclassing_spec.rb +0 -46
  216. data/spec/unit/transition_spec.rb +0 -436
  217. data/test/minitest_helper.rb +0 -57
  218. data/test/unit/minitest_matcher_test.rb +0 -80
data/lib/aasm/aasm.rb CHANGED
@@ -99,25 +99,10 @@ private
99
99
  begin
100
100
  old_state = aasm(state_machine_name).state_object_for_name(aasm(state_machine_name).current_state)
101
101
 
102
- event.fire_global_callbacks(
103
- :before_all_events,
104
- self,
105
- *process_args(event, aasm(state_machine_name).current_state, *args)
106
- )
107
-
108
- # new event before callback
109
- event.fire_callbacks(
110
- :before,
111
- self,
112
- *process_args(event, aasm(state_machine_name).current_state, *args)
113
- )
102
+ fire_default_callbacks(event, *process_args(event, aasm(state_machine_name).current_state, *args))
114
103
 
115
104
  if may_fire_to = event.may_fire?(self, *args)
116
- old_state.fire_callbacks(:before_exit, self,
117
- *process_args(event, aasm(state_machine_name).current_state, *args))
118
- old_state.fire_callbacks(:exit, self,
119
- *process_args(event, aasm(state_machine_name).current_state, *args))
120
-
105
+ fire_exit_callbacks(old_state, *process_args(event, aasm(state_machine_name).current_state, *args))
121
106
  if new_state_name = event.fire(self, {:may_fire => may_fire_to}, *args)
122
107
  aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args, &block)
123
108
  else
@@ -130,31 +115,51 @@ private
130
115
  event.fire_callbacks(:error, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
131
116
  event.fire_global_callbacks(:error_on_all_events, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
132
117
  raise(e)
118
+ false
133
119
  ensure
134
120
  event.fire_callbacks(:ensure, self, *process_args(event, aasm(state_machine_name).current_state, *args))
135
121
  event.fire_global_callbacks(:ensure_on_all_events, self, *process_args(event, aasm(state_machine_name).current_state, *args))
136
122
  end
137
123
  end
138
124
 
125
+ def fire_default_callbacks(event, *processed_args)
126
+ event.fire_global_callbacks(
127
+ :before_all_events,
128
+ self,
129
+ *processed_args
130
+ )
131
+
132
+ # new event before callback
133
+ event.fire_callbacks(
134
+ :before,
135
+ self,
136
+ *processed_args
137
+ )
138
+ end
139
+
140
+ def fire_exit_callbacks(old_state, *processed_args)
141
+ old_state.fire_callbacks(:before_exit, self, *processed_args)
142
+ old_state.fire_callbacks(:exit, self, *processed_args)
143
+ end
144
+
139
145
  def aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args)
140
146
  persist = options[:persist]
141
147
 
142
148
  new_state = aasm(state_machine_name).state_object_for_name(new_state_name)
149
+ callback_args = process_args(event, aasm(state_machine_name).current_state, *args)
143
150
 
144
- new_state.fire_callbacks(:before_enter, self,
145
- *process_args(event, aasm(state_machine_name).current_state, *args))
151
+ new_state.fire_callbacks(:before_enter, self, *callback_args)
146
152
 
147
- new_state.fire_callbacks(:enter, self,
148
- *process_args(event, aasm(state_machine_name).current_state, *args)) # TODO: remove for AASM 4?
153
+ new_state.fire_callbacks(:enter, self, *callback_args) # TODO: remove for AASM 4?
149
154
 
150
155
  persist_successful = true
151
156
  if persist
152
157
  persist_successful = aasm(state_machine_name).set_current_state_with_persistence(new_state_name)
153
158
  if persist_successful
154
159
  yield if block_given?
155
- event.fire_callbacks(:before_success, self)
160
+ event.fire_callbacks(:before_success, self, *callback_args)
156
161
  event.fire_transition_callbacks(self, *process_args(event, old_state.name, *args))
157
- event.fire_callbacks(:success, self)
162
+ event.fire_callbacks(:success, self, *callback_args)
158
163
  end
159
164
  else
160
165
  aasm(state_machine_name).current_state = new_state_name
@@ -167,10 +172,8 @@ private
167
172
  end
168
173
 
169
174
  if persist_successful
170
- old_state.fire_callbacks(:after_exit, self,
171
- *process_args(event, aasm(state_machine_name).current_state, *args))
172
- new_state.fire_callbacks(:after_enter, self,
173
- *process_args(event, aasm(state_machine_name).current_state, *args))
175
+ old_state.fire_callbacks(:after_exit, self, *callback_args)
176
+ new_state.fire_callbacks(:after_enter, self, *callback_args)
174
177
  event.fire_callbacks(
175
178
  :after,
176
179
  self,
data/lib/aasm/base.rb CHANGED
@@ -26,6 +26,9 @@ module AASM
26
26
  # raise if the model is invalid (in ActiveRecord)
27
27
  configure :whiny_persistence, false
28
28
 
29
+ # Use transactions (in ActiveRecord)
30
+ configure :use_transactions, true
31
+
29
32
  # use requires_new for nested transactions (in ActiveRecord)
30
33
  configure :requires_new_transaction, true
31
34
 
@@ -34,6 +37,9 @@ module AASM
34
37
  # string for a specific lock type i.e. FOR UPDATE NOWAIT
35
38
  configure :requires_lock, false
36
39
 
40
+ # automatically set `"#{state_name}_at" = ::Time.now` on state changes
41
+ configure :timestamps, false
42
+
37
43
  # set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
38
44
  configure :no_direct_assignment, false
39
45
 
@@ -48,18 +54,12 @@ module AASM
48
54
  # Configure a logger, with default being a Logger to STDERR
49
55
  configure :logger, Logger.new(STDERR)
50
56
 
57
+ # setup timestamp-setting callback if enabled
58
+ setup_timestamps(@name)
59
+
51
60
  # make sure to raise an error if no_direct_assignment is enabled
52
61
  # and attribute is directly assigned though
53
- aasm_name = @name
54
- klass.send :define_method, "#{@state_machine.config.column}=", ->(state_name) do
55
- if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
56
- raise AASM::NoDirectAssignmentError.new(
57
- 'direct assignment of AASM column has been disabled (see AASM configuration for this class)'
58
- )
59
- else
60
- super(state_name)
61
- end
62
- end
62
+ setup_no_direct_assignment(@name)
63
63
  end
64
64
 
65
65
  # This method is both a getter and a setter
@@ -131,6 +131,8 @@ module AASM
131
131
  aasm_fire_event(aasm_name, event, {:persist => false}, *args, &block)
132
132
  end
133
133
 
134
+ skip_instance_level_validation(event, name, aasm_name, klass)
135
+
134
136
  # Create aliases for the event methods. Keep the old names to maintain backwards compatibility.
135
137
  if namespace?
136
138
  klass.send(:alias_method, "may_#{name}_#{namespace}?", "may_#{name}?")
@@ -221,7 +223,20 @@ module AASM
221
223
  end
222
224
  end
223
225
 
224
- klass.send(:define_method, method_name, method_definition)
226
+ klass.send(:define_method, method_name, method_definition).tap do |sym|
227
+ apply_ruby2_keyword(klass, sym)
228
+ end
229
+ end
230
+
231
+ def apply_ruby2_keyword(klass, sym)
232
+ if RUBY_VERSION >= '2.7.1'
233
+ if klass.instance_method(sym).parameters.find { |type, _| type.to_s.start_with?('rest') }
234
+ # If there is a place where you are receiving in *args, do ruby2_keywords.
235
+ klass.module_eval do
236
+ ruby2_keywords sym
237
+ end
238
+ end
239
+ end
225
240
  end
226
241
 
227
242
  def namespace?
@@ -246,5 +261,43 @@ module AASM
246
261
  end
247
262
  end
248
263
 
264
+ def skip_instance_level_validation(event, name, aasm_name, klass)
265
+ # Overrides the skip_validation config for an instance (If skip validation is set to false in original config) and
266
+ # restores it back to the original value after the event is fired.
267
+ safely_define_method klass, "#{name}_without_validation!", ->(*args, &block) do
268
+ original_config = AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save
269
+ begin
270
+ AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = true unless original_config
271
+ aasm(aasm_name).current_event = :"#{name}!"
272
+ aasm_fire_event(aasm_name, event, {:persist => true}, *args, &block)
273
+ ensure
274
+ AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = original_config
275
+ end
276
+ end
277
+ end
278
+
279
+ def setup_timestamps(aasm_name)
280
+ return unless @state_machine.config.timestamps
281
+
282
+ after_all_transitions do
283
+ if self.class.aasm(:"#{aasm_name}").state_machine.config.timestamps
284
+ ts_setter = "#{aasm(aasm_name).to_state}_at="
285
+ respond_to?(ts_setter) && send(ts_setter, ::Time.now)
286
+ end
287
+ end
288
+ end
289
+
290
+ def setup_no_direct_assignment(aasm_name)
291
+ return unless @state_machine.config.no_direct_assignment
292
+
293
+ @klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
294
+ if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
295
+ raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)')
296
+ else
297
+ super(state_name)
298
+ end
299
+ end
300
+ end
301
+
249
302
  end
250
303
  end
@@ -15,12 +15,18 @@ module AASM
15
15
  # for ActiveRecord: store the new state even if the model is invalid and return true
16
16
  attr_accessor :skip_validation_on_save
17
17
 
18
+ # for ActiveRecord: use transactions
19
+ attr_accessor :use_transactions
20
+
18
21
  # for ActiveRecord: use requires_new for nested transactions?
19
22
  attr_accessor :requires_new_transaction
20
23
 
21
24
  # for ActiveRecord: use pessimistic locking
22
25
  attr_accessor :requires_lock
23
26
 
27
+ # automatically set `"#{state_name}_at" = ::Time.now` on state changes
28
+ attr_accessor :timestamps
29
+
24
30
  # forbid direct assignment in aasm_state column (in ActiveRecord)
25
31
  attr_accessor :no_direct_assignment
26
32
 
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AASM::Core
2
4
  class Event
3
- include DslHelper
5
+ include AASM::DslHelper
4
6
 
5
- attr_reader :name, :state_machine, :options
7
+ attr_reader :name, :state_machine, :options, :default_display_name
6
8
 
7
9
  def initialize(name, state_machine, options = {}, &block)
8
10
  @name = name
9
11
  @state_machine = state_machine
10
12
  @transitions = []
11
- @valid_transitions = {}
13
+ @valid_transitions = Hash.new { |h, k| h[k] = {} }
12
14
  @guards = Array(options[:guard] || options[:guards] || options[:if])
13
15
  @unless = Array(options[:unless]) #TODO: This could use a better name
16
+ @default_display_name = name.to_s.gsub(/_/, ' ').capitalize
14
17
 
15
18
  # from aasm4
16
19
  @options = options # QUESTION: .dup ?
@@ -76,8 +79,9 @@ module AASM::Core
76
79
 
77
80
  def fire_transition_callbacks(obj, *args)
78
81
  from_state = obj.aasm(state_machine.name).current_state
79
- transition = @valid_transitions[from_state]
80
- @valid_transitions[from_state].invoke_success_callbacks(obj, *args) if transition
82
+ transition = @valid_transitions[obj.object_id][from_state]
83
+ transition.invoke_success_callbacks(obj, *args) if transition
84
+ @valid_transitions.delete(obj.object_id)
81
85
  end
82
86
 
83
87
  def ==(event)
@@ -107,6 +111,10 @@ module AASM::Core
107
111
  transitions.flat_map(&:failures)
108
112
  end
109
113
 
114
+ def to_s
115
+ name.to_s
116
+ end
117
+
110
118
  private
111
119
 
112
120
  def attach_event_guards(definitions)
@@ -123,18 +131,19 @@ module AASM::Core
123
131
 
124
132
  def _fire(obj, options={}, to_state=::AASM::NO_VALUE, *args)
125
133
  result = options[:test_only] ? false : nil
134
+ clear_failed_callbacks
126
135
  transitions = @transitions.select { |t| t.from == obj.aasm(state_machine.name).current_state || t.from == nil}
127
136
  return result if transitions.size == 0
128
137
 
129
138
  if to_state == ::AASM::NO_VALUE
130
139
  to_state = nil
131
- elsif to_state.respond_to?(:to_sym) && transitions.map(&:to).flatten.include?(to_state.to_sym)
132
- # nop, to_state is a valid to-state
133
- else
140
+ elsif !(to_state.respond_to?(:to_sym) && transitions.map(&:to).flatten.include?(to_state.to_sym))
134
141
  # to_state is an argument
135
142
  args.unshift(to_state)
136
143
  to_state = nil
137
144
  end
145
+
146
+ # nop, to_state is a valid to-state
138
147
 
139
148
  transitions.each do |transition|
140
149
  next if to_state and !Array(transition.to).include?(to_state)
@@ -145,7 +154,7 @@ module AASM::Core
145
154
  result = transition
146
155
  else
147
156
  result = to_state || Array(transition.to).first
148
- Array(transition.to).each {|to| @valid_transitions[to] = transition }
157
+ Array(transition.to).each {|to| @valid_transitions[obj.object_id][to] = transition }
149
158
  transition.execute(obj, *args)
150
159
  end
151
160
 
@@ -155,28 +164,15 @@ module AASM::Core
155
164
  result
156
165
  end
157
166
 
158
- def invoke_callbacks(code, record, args)
159
- case code
160
- when Symbol, String
161
- unless record.respond_to?(code, true)
162
- raise NoMethodError.new("NoMethodError: undefined method `#{code}' for #{record.inspect}:#{record.class}")
163
- end
164
- arity = record.__send__(:method, code.to_sym).arity
165
- record.__send__(code, *(arity < 0 ? args : args[0...arity]))
166
- true
167
-
168
- when Proc
169
- arity = code.arity
170
- record.instance_exec(*(arity < 0 ? args : args[0...arity]), &code)
171
- true
172
-
173
- when Array
174
- code.each {|a| invoke_callbacks(a, record, args)}
175
- true
167
+ def clear_failed_callbacks
168
+ # https://github.com/aasm/aasm/issues/383, https://github.com/aasm/aasm/issues/599
169
+ transitions.each { |transition| transition.failures.clear }
170
+ end
176
171
 
177
- else
178
- false
179
- end
172
+ def invoke_callbacks(code, record, args)
173
+ Invoker.new(code, record, args)
174
+ .with_default_return_value(false)
175
+ .invoke
180
176
  end
181
177
  end
182
178
  end # AASM
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AASM
4
+ module Core
5
+ ##
6
+ # main invoker class which encapsulates the logic
7
+ # for invoking literal-based, proc-based, class-based
8
+ # and array-based callbacks for different entities.
9
+ class Invoker
10
+ DEFAULT_RETURN_VALUE = true
11
+
12
+ ##
13
+ # Initialize a new invoker instance.
14
+ # NOTE that invoker must be used per-subject/record
15
+ # (one instance per subject/record)
16
+ #
17
+ # ==Options:
18
+ #
19
+ # +subject+ - invoking subject, may be Proc,
20
+ # Class, String, Symbol or Array
21
+ # +record+ - invoking record
22
+ # +args+ - arguments which will be passed to the callback
23
+
24
+ def initialize(subject, record, args)
25
+ @subject = subject
26
+ @record = record
27
+ @args = args
28
+ @options = {}
29
+ @failures = []
30
+ @default_return_value = DEFAULT_RETURN_VALUE
31
+ end
32
+
33
+ ##
34
+ # Pass additional options to concrete invoker
35
+ #
36
+ # ==Options:
37
+ #
38
+ # +options+ - hash of options which will be passed to
39
+ # concrete invokers
40
+ #
41
+ # ==Example:
42
+ #
43
+ # with_options(guard: proc {...})
44
+
45
+ def with_options(options)
46
+ @options = options
47
+ self
48
+ end
49
+
50
+ ##
51
+ # Collect failures to a specified buffer
52
+ #
53
+ # ==Options:
54
+ #
55
+ # +failures+ - failures buffer to collect failures
56
+
57
+ def with_failures(failures)
58
+ @failures = failures
59
+ self
60
+ end
61
+
62
+ ##
63
+ # Change default return value of #invoke method
64
+ # if none of invokers processed the request.
65
+ #
66
+ # The default return value is #DEFAULT_RETURN_VALUE
67
+ #
68
+ # ==Options:
69
+ #
70
+ # +value+ - default return value for #invoke method
71
+
72
+ def with_default_return_value(value)
73
+ @default_return_value = value
74
+ self
75
+ end
76
+
77
+ ##
78
+ # Find concrete invoker for specified subject and invoker it,
79
+ # or return default value set by #DEFAULT_RETURN_VALUE or
80
+ # overridden by #with_default_return_value
81
+
82
+ # rubocop:disable Metrics/AbcSize
83
+ def invoke
84
+ return invoke_array if subject.is_a?(Array)
85
+ return literal_invoker.invoke if literal_invoker.may_invoke?
86
+ return proc_invoker.invoke if proc_invoker.may_invoke?
87
+ return class_invoker.invoke if class_invoker.may_invoke?
88
+ default_return_value
89
+ end
90
+ # rubocop:enable Metrics/AbcSize
91
+
92
+ private
93
+
94
+ attr_reader :subject, :record, :args, :options, :failures,
95
+ :default_return_value
96
+
97
+ def invoke_array
98
+ return subject.all? { |item| sub_invoke(item) } if options[:guard]
99
+ return subject.all? { |item| !sub_invoke(item) } if options[:unless]
100
+ subject.map { |item| sub_invoke(item) }
101
+ end
102
+
103
+ def sub_invoke(new_subject)
104
+ self.class.new(new_subject, record, args)
105
+ .with_failures(failures)
106
+ .with_options(options)
107
+ .invoke
108
+ end
109
+
110
+ def proc_invoker
111
+ @proc_invoker ||= Invokers::ProcInvoker
112
+ .new(subject, record, args)
113
+ .with_failures(failures)
114
+ end
115
+
116
+ def class_invoker
117
+ @class_invoker ||= Invokers::ClassInvoker
118
+ .new(subject, record, args)
119
+ .with_failures(failures)
120
+ end
121
+
122
+ def literal_invoker
123
+ @literal_invoker ||= Invokers::LiteralInvoker
124
+ .new(subject, record, args)
125
+ .with_failures(failures)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AASM
4
+ module Core
5
+ module Invokers
6
+ ##
7
+ # Base concrete invoker class which contain basic
8
+ # invoking and logging definitions
9
+ class BaseInvoker
10
+ attr_reader :failures, :subject, :record, :args, :result
11
+
12
+ ##
13
+ # Initialize a new concrete invoker instance.
14
+ # NOTE that concrete invoker must be used per-subject/record
15
+ # (one instance per subject/record)
16
+ #
17
+ # ==Options:
18
+ #
19
+ # +subject+ - invoking subject comparable with this invoker
20
+ # +record+ - invoking record
21
+ # +args+ - arguments which will be passed to the callback
22
+
23
+ def initialize(subject, record, args)
24
+ @subject = subject
25
+ @record = record
26
+ @args = args
27
+ @result = false
28
+ @failures = []
29
+ end
30
+
31
+ ##
32
+ # Collect failures to a specified buffer
33
+ #
34
+ # ==Options:
35
+ #
36
+ # +failures+ - failures buffer to collect failures
37
+
38
+ def with_failures(failures_buffer)
39
+ @failures = failures_buffer
40
+ self
41
+ end
42
+
43
+ ##
44
+ # Execute concrete invoker, log the error and return result
45
+
46
+ def invoke
47
+ return unless may_invoke?
48
+ log_failure unless invoke_subject
49
+ result
50
+ end
51
+
52
+ ##
53
+ # Check if concrete invoker may be invoked for a specified subject
54
+
55
+ def may_invoke?
56
+ raise NoMethodError, '"#may_invoke?" is not implemented'
57
+ end
58
+
59
+ ##
60
+ # Log failed invoking
61
+
62
+ def log_failure
63
+ raise NoMethodError, '"#log_failure" is not implemented'
64
+ end
65
+
66
+ ##
67
+ # Execute concrete invoker
68
+
69
+ def invoke_subject
70
+ raise NoMethodError, '"#invoke_subject" is not implemented'
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AASM
4
+ module Core
5
+ module Invokers
6
+ ##
7
+ # Class invoker which allows to use classes which respond to #call
8
+ # to be used as state/event/transition callbacks.
9
+ class ClassInvoker < BaseInvoker
10
+ def may_invoke?
11
+ subject.is_a?(Class) && subject.instance_methods.include?(:call)
12
+ end
13
+
14
+ def log_failure
15
+ return log_source_location if Method.method_defined?(:source_location)
16
+ log_method_info
17
+ end
18
+
19
+ def invoke_subject
20
+ @result = instance.call
21
+ end
22
+
23
+ private
24
+
25
+ def log_source_location
26
+ failures << instance.method(:call).source_location.join('#')
27
+ end
28
+
29
+ def log_method_info
30
+ failures << instance.method(:call)
31
+ end
32
+
33
+ def instance
34
+ @instance ||= retrieve_instance
35
+ end
36
+
37
+ # rubocop:disable Metrics/AbcSize
38
+ def retrieve_instance
39
+ return subject.new if subject_arity.zero?
40
+ return subject.new(record) if subject_arity == 1
41
+ return subject.new(record, *args) if subject_arity < 0
42
+ subject.new(record, *args[0..(subject_arity - 2)])
43
+ end
44
+ # rubocop:enable Metrics/AbcSize
45
+
46
+ def subject_arity
47
+ @arity ||= subject.instance_method(:initialize).arity
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AASM
4
+ module Core
5
+ module Invokers
6
+ ##
7
+ # Literal invoker which allows to use strings or symbols to call
8
+ # record methods as state/event/transition callbacks.
9
+ class LiteralInvoker < BaseInvoker
10
+ def may_invoke?
11
+ subject.is_a?(String) || subject.is_a?(Symbol)
12
+ end
13
+
14
+ def log_failure
15
+ failures << subject
16
+ end
17
+
18
+ def invoke_subject
19
+ @result = exec_subject
20
+ end
21
+
22
+ private
23
+
24
+ def subject_arity
25
+ @arity ||= record.__send__(:method, subject.to_sym).arity
26
+ end
27
+
28
+ # rubocop:disable Metrics/AbcSize
29
+ def exec_subject
30
+ raise(*record_error) unless record.respond_to?(subject, true)
31
+ return record.__send__(subject) if subject_arity.zero?
32
+ return record.__send__(subject, *args) if subject_arity < 0
33
+ req_args = args[0..(subject_arity - 1)]
34
+ return record.__send__(subject, **req_args[0]) if req_args[0].is_a?(Hash)
35
+ record.__send__(subject, *req_args)
36
+ end
37
+ # rubocop:enable Metrics/AbcSize
38
+
39
+ def record_error
40
+ [
41
+ NoMethodError,
42
+ 'NoMethodError: undefined method ' \
43
+ "`#{subject}' for #{record.inspect}:#{record.class}"
44
+ ]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end