aasm 4.12.3 → 5.1.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 (114) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.travis.yml +48 -18
  5. data/Appraisals +50 -26
  6. data/CHANGELOG.md +75 -3
  7. data/Dockerfile +44 -0
  8. data/Gemfile +2 -3
  9. data/README.md +216 -110
  10. data/aasm.gemspec +2 -0
  11. data/docker-compose.yml +40 -0
  12. data/gemfiles/norails.gemfile +10 -0
  13. data/gemfiles/rails_4.2.gemfile +9 -8
  14. data/gemfiles/rails_4.2_mongoid_5.gemfile +6 -5
  15. data/gemfiles/rails_4.2_nobrainer.gemfile +9 -0
  16. data/gemfiles/rails_5.0.gemfile +6 -6
  17. data/gemfiles/rails_5.0_nobrainer.gemfile +9 -0
  18. data/gemfiles/rails_5.1.gemfile +14 -0
  19. data/gemfiles/rails_5.2.gemfile +14 -0
  20. data/lib/aasm.rb +5 -2
  21. data/lib/aasm/aasm.rb +30 -27
  22. data/lib/aasm/base.rb +25 -7
  23. data/lib/aasm/core/event.rb +14 -24
  24. data/lib/aasm/core/invoker.rb +129 -0
  25. data/lib/aasm/core/invokers/base_invoker.rb +75 -0
  26. data/lib/aasm/core/invokers/class_invoker.rb +52 -0
  27. data/lib/aasm/core/invokers/literal_invoker.rb +47 -0
  28. data/lib/aasm/core/invokers/proc_invoker.rb +59 -0
  29. data/lib/aasm/core/state.rb +10 -9
  30. data/lib/aasm/core/transition.rb +7 -68
  31. data/lib/aasm/errors.rb +4 -3
  32. data/lib/aasm/instance_base.rb +16 -4
  33. data/lib/aasm/persistence.rb +3 -0
  34. data/lib/aasm/persistence/active_record_persistence.rb +25 -5
  35. data/lib/aasm/persistence/base.rb +1 -1
  36. data/lib/aasm/persistence/core_data_query_persistence.rb +2 -1
  37. data/lib/aasm/persistence/dynamoid_persistence.rb +1 -1
  38. data/lib/aasm/persistence/mongoid_persistence.rb +1 -1
  39. data/lib/aasm/persistence/no_brainer_persistence.rb +105 -0
  40. data/lib/aasm/persistence/orm.rb +23 -19
  41. data/lib/aasm/persistence/plain_persistence.rb +2 -1
  42. data/lib/aasm/persistence/redis_persistence.rb +1 -1
  43. data/lib/aasm/persistence/sequel_persistence.rb +0 -1
  44. data/lib/aasm/rspec/allow_event.rb +5 -1
  45. data/lib/aasm/rspec/allow_transition_to.rb +5 -1
  46. data/lib/aasm/rspec/transition_from.rb +5 -1
  47. data/lib/aasm/version.rb +1 -1
  48. data/lib/generators/aasm/orm_helpers.rb +6 -0
  49. data/lib/generators/active_record/aasm_generator.rb +3 -1
  50. data/lib/generators/nobrainer/aasm_generator.rb +28 -0
  51. data/lib/motion-aasm.rb +1 -0
  52. data/spec/database.rb +16 -1
  53. data/spec/en.yml +0 -3
  54. data/spec/generators/active_record_generator_spec.rb +6 -0
  55. data/spec/generators/no_brainer_generator_spec.rb +29 -0
  56. data/spec/{en_deprecated_style.yml → localizer_test_model_deprecated_style.yml} +0 -4
  57. data/spec/localizer_test_model_new_style.yml +5 -0
  58. data/spec/models/active_record/active_record_callback.rb +93 -0
  59. data/spec/models/active_record/instance_level_skip_validation_example.rb +19 -0
  60. data/spec/models/active_record/localizer_test_model.rb +3 -3
  61. data/spec/models/active_record/person.rb +23 -0
  62. data/spec/models/active_record/simple_new_dsl.rb +15 -0
  63. data/spec/models/active_record/work.rb +3 -0
  64. data/spec/models/callbacks/with_state_arg.rb +5 -1
  65. data/spec/models/callbacks/with_state_arg_multiple.rb +4 -1
  66. data/spec/models/default_state.rb +1 -1
  67. data/spec/models/nobrainer/complex_no_brainer_example.rb +36 -0
  68. data/spec/models/nobrainer/invalid_persistor_no_brainer.rb +39 -0
  69. data/spec/models/nobrainer/no_scope_no_brainer.rb +21 -0
  70. data/spec/models/nobrainer/nobrainer_relationships.rb +25 -0
  71. data/spec/models/nobrainer/silent_persistor_no_brainer.rb +39 -0
  72. data/spec/models/nobrainer/simple_new_dsl_nobrainer.rb +25 -0
  73. data/spec/models/nobrainer/simple_no_brainer.rb +23 -0
  74. data/spec/models/nobrainer/validator_no_brainer.rb +98 -0
  75. data/spec/models/simple_example.rb +8 -0
  76. data/spec/models/simple_example_with_guard_args.rb +17 -0
  77. data/spec/spec_helper.rb +15 -0
  78. data/spec/spec_helpers/active_record.rb +2 -1
  79. data/spec/spec_helpers/dynamoid.rb +7 -5
  80. data/spec/spec_helpers/mongoid.rb +20 -1
  81. data/spec/spec_helpers/nobrainer.rb +15 -0
  82. data/spec/spec_helpers/redis.rb +5 -2
  83. data/spec/spec_helpers/sequel.rb +1 -1
  84. data/spec/unit/abstract_class_spec.rb +27 -0
  85. data/spec/unit/api_spec.rb +4 -0
  86. data/spec/unit/callback_multiple_spec.rb +7 -3
  87. data/spec/unit/callbacks_spec.rb +32 -2
  88. data/spec/unit/complex_example_spec.rb +0 -1
  89. data/spec/unit/event_spec.rb +13 -0
  90. data/spec/unit/exception_spec.rb +1 -1
  91. data/spec/unit/inspection_multiple_spec.rb +9 -5
  92. data/spec/unit/inspection_spec.rb +7 -3
  93. data/spec/unit/invoker_spec.rb +189 -0
  94. data/spec/unit/invokers/base_invoker_spec.rb +72 -0
  95. data/spec/unit/invokers/class_invoker_spec.rb +95 -0
  96. data/spec/unit/invokers/literal_invoker_spec.rb +86 -0
  97. data/spec/unit/invokers/proc_invoker_spec.rb +86 -0
  98. data/spec/unit/localizer_spec.rb +9 -10
  99. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +4 -4
  100. data/spec/unit/persistence/active_record_persistence_spec.rb +109 -4
  101. data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +0 -4
  102. data/spec/unit/persistence/mongoid_persistence_spec.rb +0 -4
  103. data/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb +198 -0
  104. data/spec/unit/persistence/no_brainer_persistence_spec.rb +158 -0
  105. data/spec/unit/rspec_matcher_spec.rb +9 -0
  106. data/spec/unit/simple_example_spec.rb +15 -0
  107. data/spec/unit/state_spec.rb +23 -7
  108. data/spec/unit/transition_spec.rb +1 -1
  109. data/test/minitest_helper.rb +2 -2
  110. data/test/unit/minitest_matcher_test.rb +1 -1
  111. metadata +106 -12
  112. data/callbacks.txt +0 -51
  113. data/gemfiles/rails_3.2.gemfile +0 -13
  114. data/gemfiles/rails_4.0.gemfile +0 -15
data/aasm.gemspec CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency 'rspec', ">= 3"
24
24
  s.add_development_dependency 'generator_spec'
25
25
  s.add_development_dependency 'appraisal'
26
+ s.add_development_dependency "simplecov"
27
+ s.add_development_dependency "codecov", ">= 0.1.17", '< 0.1.20'
26
28
 
27
29
  # debugging
28
30
  # s.add_development_dependency 'debugger'
@@ -0,0 +1,40 @@
1
+ version: "2"
2
+
3
+ services:
4
+ aasm:
5
+ image: aasm/aasm
6
+ build: .
7
+ command: bash -c 'bundle exec appraisal install && bundle exec appraisal rspec'
8
+ environment:
9
+ - DYNAMODB_HOST=dynamodb
10
+ - DYNAMODB_PORT=8000
11
+ - MONGODB_HOST=mongo
12
+ - MONGODB_PORT=27017
13
+ - RAILS_ENV=development
14
+ - REDIS_HOST=redis
15
+ - REDIS_PORT=6379
16
+ - RETHINKDB_DB=rethinkdb_test
17
+ - RETHINKDB_HOST=rethinkdb
18
+ - RETHINKDB_PORT=28015
19
+ depends_on:
20
+ - dynamodb
21
+ - mongo
22
+ - redis
23
+ - rethinkdb
24
+ volumes:
25
+ - .:/application
26
+ volumes_from:
27
+ - bundle
28
+ bundle:
29
+ image: aasm/aasm
30
+ command: echo Bundler data container
31
+ volumes:
32
+ - /bundle
33
+ dynamodb:
34
+ image: cnadiminti/dynamodb-local:2017-02-16
35
+ mongo:
36
+ image: mongo:3.6.1
37
+ redis:
38
+ image: redis:4.0.6-alpine
39
+ rethinkdb:
40
+ image: rethinkdb:2.3.6
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3", ">= 1.3.5", platforms: :ruby
6
+ gem "rails", install_if: false
7
+ gem "sequel"
8
+ gem "redis-objects"
9
+
10
+ gemspec path: "../"
@@ -2,15 +2,16 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
7
6
  gem "rails", "4.2.5"
8
- gem "nokogiri", "1.6.8.1", :platforms => [:ruby_19]
9
- gem "mime-types", "~> 2", :platforms => [:ruby_19, :jruby]
10
- gem "mongoid", "~>4.0"
7
+ gem "nokogiri", "1.6.8.1", platforms: [:ruby_19]
8
+ gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
9
+ gem "mongoid", "~> 4.0"
11
10
  gem "sequel"
12
- gem "dynamoid", "~> 1", :platforms => :ruby
13
- gem "aws-sdk", "~>2", :platforms => :ruby
11
+ gem "dynamoid", "~> 1", platforms: :ruby
12
+ gem "aws-sdk", "~> 2", platforms: :ruby
14
13
  gem "redis-objects"
14
+ gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
15
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
15
16
 
16
- gemspec :path => "../"
17
+ gemspec path: "../"
@@ -2,10 +2,11 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
7
6
  gem "rails", "4.2.5"
8
- gem "mime-types", "~> 2", :platforms => [:ruby_19, :jruby]
9
- gem "mongoid", "~>5.0"
7
+ gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
8
+ gem "mongoid", "~> 5.0"
9
+ gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
10
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
10
11
 
11
- gemspec :path => "../"
12
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
+ gem "rails", "4.2.5"
7
+ gem "nobrainer", "~> 0.33.0"
8
+
9
+ gemspec path: "../"
@@ -2,13 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
7
6
  gem "rails", "5.0.0"
8
- gem "mongoid", "~>6.0"
7
+ gem "mongoid", "~> 6.0"
9
8
  gem "sequel"
10
- gem "dynamoid", "~> 1", :platforms => :ruby
11
- gem "aws-sdk", "~>2", :platforms => :ruby
9
+ gem "dynamoid", "~> 1.3", platforms: :ruby
10
+ gem "aws-sdk", "~> 2", platforms: :ruby
12
11
  gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
13
13
 
14
- gemspec :path => "../"
14
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
+ gem "rails", "5.0.0"
7
+ gem "nobrainer", "~> 0.33.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
+ gem "rails", "5.1"
7
+ gem "mongoid", "~>6.0"
8
+ gem "sequel"
9
+ gem "dynamoid", "~> 1.3", platforms: :ruby
10
+ gem "aws-sdk", "~>2", platforms: :ruby
11
+ gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
13
+
14
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
+ gem "rails", "5.2"
7
+ gem "mongoid", "~>6.0"
8
+ gem "sequel"
9
+ gem "dynamoid", "~>2.2", platforms: :ruby
10
+ gem "aws-sdk", "~>2", platforms: :ruby
11
+ gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
13
+
14
+ gemspec path: "../"
data/lib/aasm.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'ostruct'
2
-
3
1
  require 'aasm/version'
4
2
  require 'aasm/errors'
5
3
  require 'aasm/configuration'
@@ -9,6 +7,11 @@ require 'aasm/instance_base'
9
7
  require 'aasm/core/transition'
10
8
  require 'aasm/core/event'
11
9
  require 'aasm/core/state'
10
+ require 'aasm/core/invoker'
11
+ require 'aasm/core/invokers/base_invoker'
12
+ require 'aasm/core/invokers/class_invoker'
13
+ require 'aasm/core/invokers/literal_invoker'
14
+ require 'aasm/core/invokers/proc_invoker'
12
15
  require 'aasm/localizer'
13
16
  require 'aasm/state_machine_store'
14
17
  require 'aasm/state_machine'
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
@@ -54,13 +54,14 @@ module AASM
54
54
  # make sure to raise an error if no_direct_assignment is enabled
55
55
  # and attribute is directly assigned though
56
56
  aasm_name = @name
57
- klass.send :define_method, "#{@state_machine.config.column}=", ->(state_name) do
58
- if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
59
- raise AASM::NoDirectAssignmentError.new(
60
- 'direct assignment of AASM column has been disabled (see AASM configuration for this class)'
61
- )
62
- else
63
- super(state_name)
57
+
58
+ if @state_machine.config.no_direct_assignment
59
+ @klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
60
+ if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
61
+ raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)')
62
+ else
63
+ super(state_name)
64
+ end
64
65
  end
65
66
  end
66
67
  end
@@ -134,6 +135,8 @@ module AASM
134
135
  aasm_fire_event(aasm_name, event, {:persist => false}, *args, &block)
135
136
  end
136
137
 
138
+ skip_instance_level_validation(event, name, aasm_name, klass)
139
+
137
140
  # Create aliases for the event methods. Keep the old names to maintain backwards compatibility.
138
141
  if namespace?
139
142
  klass.send(:alias_method, "may_#{name}_#{namespace}?", "may_#{name}?")
@@ -249,5 +252,20 @@ module AASM
249
252
  end
250
253
  end
251
254
 
255
+ def skip_instance_level_validation(event, name, aasm_name, klass)
256
+ # Overrides the skip_validation config for an instance (If skip validation is set to false in original config) and
257
+ # restores it back to the original value after the event is fired.
258
+ safely_define_method klass, "#{name}_without_validation!", ->(*args, &block) do
259
+ original_config = AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save
260
+ begin
261
+ AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = true unless original_config
262
+ aasm(aasm_name).current_event = :"#{name}!"
263
+ aasm_fire_event(aasm_name, event, {:persist => true}, *args, &block)
264
+ ensure
265
+ AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = original_config
266
+ end
267
+ end
268
+ end
269
+
252
270
  end
253
271
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AASM::Core
2
4
  class Event
3
5
  include DslHelper
@@ -123,18 +125,19 @@ module AASM::Core
123
125
 
124
126
  def _fire(obj, options={}, to_state=::AASM::NO_VALUE, *args)
125
127
  result = options[:test_only] ? false : nil
128
+ clear_failed_callbacks
126
129
  transitions = @transitions.select { |t| t.from == obj.aasm(state_machine.name).current_state || t.from == nil}
127
130
  return result if transitions.size == 0
128
131
 
129
132
  if to_state == ::AASM::NO_VALUE
130
133
  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
134
+ elsif !(to_state.respond_to?(:to_sym) && transitions.map(&:to).flatten.include?(to_state.to_sym))
134
135
  # to_state is an argument
135
136
  args.unshift(to_state)
136
137
  to_state = nil
137
138
  end
139
+
140
+ # nop, to_state is a valid to-state
138
141
 
139
142
  transitions.each do |transition|
140
143
  next if to_state and !Array(transition.to).include?(to_state)
@@ -155,28 +158,15 @@ module AASM::Core
155
158
  result
156
159
  end
157
160
 
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
161
+ def clear_failed_callbacks
162
+ # https://github.com/aasm/aasm/issues/383, https://github.com/aasm/aasm/issues/599
163
+ transitions.each { |transition| transition.failures.clear }
164
+ end
176
165
 
177
- else
178
- false
179
- end
166
+ def invoke_callbacks(code, record, args)
167
+ Invoker.new(code, record, args)
168
+ .with_default_return_value(false)
169
+ .invoke
180
170
  end
181
171
  end
182
172
  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