aasm 5.0.5 → 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 (53) 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 +26 -23
  5. data/Appraisals +6 -10
  6. data/CHANGELOG.md +30 -0
  7. data/Dockerfile +1 -6
  8. data/Gemfile +1 -1
  9. data/README.md +63 -12
  10. data/aasm.gemspec +2 -0
  11. data/gemfiles/norails.gemfile +1 -1
  12. data/gemfiles/rails_4.2.gemfile +1 -0
  13. data/gemfiles/rails_4.2_mongoid_5.gemfile +1 -0
  14. data/gemfiles/rails_5.0.gemfile +1 -0
  15. data/gemfiles/rails_5.1.gemfile +1 -0
  16. data/gemfiles/rails_5.2.gemfile +1 -0
  17. data/lib/aasm.rb +0 -2
  18. data/lib/aasm/aasm.rb +29 -27
  19. data/lib/aasm/base.rb +19 -0
  20. data/lib/aasm/core/event.rb +3 -3
  21. data/lib/aasm/instance_base.rb +16 -4
  22. data/lib/aasm/persistence/active_record_persistence.rb +18 -0
  23. data/lib/aasm/persistence/orm.rb +23 -19
  24. data/lib/aasm/rspec/transition_from.rb +5 -1
  25. data/lib/aasm/version.rb +1 -1
  26. data/spec/database.rb +6 -1
  27. data/spec/en.yml +0 -3
  28. data/spec/{en_deprecated_style.yml → localizer_test_model_deprecated_style.yml} +0 -4
  29. data/spec/localizer_test_model_new_style.yml +5 -0
  30. data/spec/models/active_record/active_record_callback.rb +93 -0
  31. data/spec/models/active_record/instance_level_skip_validation_example.rb +19 -0
  32. data/spec/models/active_record/localizer_test_model.rb +3 -3
  33. data/spec/models/callbacks/with_state_arg.rb +5 -1
  34. data/spec/models/callbacks/with_state_arg_multiple.rb +4 -1
  35. data/spec/models/default_state.rb +1 -1
  36. data/spec/models/simple_example.rb +6 -0
  37. data/spec/spec_helper.rb +15 -0
  38. data/spec/unit/api_spec.rb +4 -0
  39. data/spec/unit/callback_multiple_spec.rb +4 -0
  40. data/spec/unit/callbacks_spec.rb +4 -0
  41. data/spec/unit/complex_example_spec.rb +0 -1
  42. data/spec/unit/event_spec.rb +13 -0
  43. data/spec/unit/inspection_multiple_spec.rb +9 -5
  44. data/spec/unit/inspection_spec.rb +7 -3
  45. data/spec/unit/localizer_spec.rb +9 -10
  46. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +4 -4
  47. data/spec/unit/persistence/active_record_persistence_spec.rb +93 -4
  48. data/spec/unit/rspec_matcher_spec.rb +3 -0
  49. data/spec/unit/simple_example_spec.rb +15 -0
  50. data/spec/unit/state_spec.rb +21 -5
  51. metadata +50 -11
  52. data/callbacks.txt +0 -51
  53. data/gemfiles/rails_3.2.gemfile +0 -14
@@ -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'
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", "~> 1.3.5", platforms: :ruby
5
+ gem "sqlite3", "~> 1.3", ">= 1.3.5", platforms: :ruby
6
6
  gem "rails", install_if: false
7
7
  gem "sequel"
8
8
  gem "redis-objects"
@@ -12,5 +12,6 @@ gem "dynamoid", "~> 1", platforms: :ruby
12
12
  gem "aws-sdk", "~> 2", platforms: :ruby
13
13
  gem "redis-objects"
14
14
  gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
15
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
15
16
 
16
17
  gemspec path: "../"
@@ -7,5 +7,6 @@ gem "rails", "4.2.5"
7
7
  gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
8
8
  gem "mongoid", "~> 5.0"
9
9
  gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
10
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
10
11
 
11
12
  gemspec path: "../"
@@ -9,5 +9,6 @@ gem "sequel"
9
9
  gem "dynamoid", "~> 1.3", platforms: :ruby
10
10
  gem "aws-sdk", "~> 2", platforms: :ruby
11
11
  gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
12
13
 
13
14
  gemspec path: "../"
@@ -9,5 +9,6 @@ gem "sequel"
9
9
  gem "dynamoid", "~> 1.3", platforms: :ruby
10
10
  gem "aws-sdk", "~>2", platforms: :ruby
11
11
  gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
12
13
 
13
14
  gemspec path: "../"
@@ -9,5 +9,6 @@ gem "sequel"
9
9
  gem "dynamoid", "~>2.2", platforms: :ruby
10
10
  gem "aws-sdk", "~>2", platforms: :ruby
11
11
  gem "redis-objects"
12
+ gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
12
13
 
13
14
  gemspec path: "../"
@@ -1,5 +1,3 @@
1
- require 'ostruct'
2
-
3
1
  require 'aasm/version'
4
2
  require 'aasm/errors'
5
3
  require 'aasm/configuration'
@@ -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
@@ -137,25 +122,44 @@ private
137
122
  end
138
123
  end
139
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
+
140
145
  def aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args)
141
146
  persist = options[:persist]
142
147
 
143
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)
144
150
 
145
- new_state.fire_callbacks(:before_enter, self,
146
- *process_args(event, aasm(state_machine_name).current_state, *args))
151
+ new_state.fire_callbacks(:before_enter, self, *callback_args)
147
152
 
148
- new_state.fire_callbacks(:enter, self,
149
- *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?
150
154
 
151
155
  persist_successful = true
152
156
  if persist
153
157
  persist_successful = aasm(state_machine_name).set_current_state_with_persistence(new_state_name)
154
158
  if persist_successful
155
159
  yield if block_given?
156
- event.fire_callbacks(:before_success, self)
160
+ event.fire_callbacks(:before_success, self, *callback_args)
157
161
  event.fire_transition_callbacks(self, *process_args(event, old_state.name, *args))
158
- event.fire_callbacks(:success, self)
162
+ event.fire_callbacks(:success, self, *callback_args)
159
163
  end
160
164
  else
161
165
  aasm(state_machine_name).current_state = new_state_name
@@ -168,10 +172,8 @@ private
168
172
  end
169
173
 
170
174
  if persist_successful
171
- old_state.fire_callbacks(:after_exit, self,
172
- *process_args(event, aasm(state_machine_name).current_state, *args))
173
- new_state.fire_callbacks(:after_enter, self,
174
- *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)
175
177
  event.fire_callbacks(
176
178
  :after,
177
179
  self,
@@ -59,6 +59,8 @@ module AASM
59
59
  @klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
60
60
  if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
61
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)
62
64
  end
63
65
  end
64
66
  end
@@ -133,6 +135,8 @@ module AASM
133
135
  aasm_fire_event(aasm_name, event, {:persist => false}, *args, &block)
134
136
  end
135
137
 
138
+ skip_instance_level_validation(event, name, aasm_name, klass)
139
+
136
140
  # Create aliases for the event methods. Keep the old names to maintain backwards compatibility.
137
141
  if namespace?
138
142
  klass.send(:alias_method, "may_#{name}_#{namespace}?", "may_#{name}?")
@@ -248,5 +252,20 @@ module AASM
248
252
  end
249
253
  end
250
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
+
251
270
  end
252
271
  end
@@ -131,13 +131,13 @@ module AASM::Core
131
131
 
132
132
  if to_state == ::AASM::NO_VALUE
133
133
  to_state = nil
134
- elsif to_state.respond_to?(:to_sym) && transitions.map(&:to).flatten.include?(to_state.to_sym)
135
- # nop, to_state is a valid to-state
136
- else
134
+ elsif !(to_state.respond_to?(:to_sym) && transitions.map(&:to).flatten.include?(to_state.to_sym))
137
135
  # to_state is an argument
138
136
  args.unshift(to_state)
139
137
  to_state = nil
140
138
  end
139
+
140
+ # nop, to_state is a valid to-state
141
141
 
142
142
  transitions.each do |transition|
143
143
  next if to_state and !Array(transition.to).include?(to_state)
@@ -28,7 +28,7 @@ module AASM
28
28
  end
29
29
 
30
30
  def human_state
31
- AASM::Localizer.new.human_state_name(@instance.class, state_object_for_name(current_state))
31
+ state_object_for_name(current_state).display_name
32
32
  end
33
33
 
34
34
  def states(options={}, *args)
@@ -78,6 +78,17 @@ module AASM
78
78
  events
79
79
  end
80
80
 
81
+ def permitted_transitions
82
+ events(permitted: true).flat_map do |event|
83
+ available_transitions = event.transitions_from_state(current_state)
84
+ allowed_transitions = available_transitions.select { |t| t.allowed?(@instance) }
85
+
86
+ allowed_transitions.map do |transition|
87
+ { event: event.name, state: transition.to }
88
+ end
89
+ end
90
+ end
91
+
81
92
  def state_object_for_name(name)
82
93
  obj = @instance.class.aasm(@name).states.find {|s| s.name == name}
83
94
  raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
@@ -91,7 +102,7 @@ module AASM
91
102
  when Proc
92
103
  state.call(@instance)
93
104
  else
94
- raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
105
+ raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
95
106
  end
96
107
  end
97
108
 
@@ -104,11 +115,12 @@ module AASM
104
115
  end
105
116
 
106
117
  def fire(event_name, *args, &block)
107
- @instance.send(:aasm_fire_event, @name, event_name, {persist: false}, *args, &block)
118
+ @instance.send(event_name, *args, &block)
108
119
  end
109
120
 
110
121
  def fire!(event_name, *args, &block)
111
- @instance.send(:aasm_fire_event, @name, event_name, {persist: true}, *args, &block)
122
+ event_name = event_name.to_s.+("!").to_sym
123
+ @instance.send(event_name, *args, &block)
112
124
  end
113
125
 
114
126
  def set_current_state_with_persistence(state)
@@ -61,6 +61,24 @@ module AASM
61
61
 
62
62
  private
63
63
 
64
+ def aasm_execute_after_commit
65
+ begin
66
+ require 'after_commit_everywhere'
67
+ raise LoadError unless Gem::Version.new(::AfterCommitEverywhere::VERSION) >= Gem::Version.new('0.1.5')
68
+
69
+ self.extend ::AfterCommitEverywhere
70
+ after_commit do
71
+ yield
72
+ end
73
+ rescue LoadError
74
+ warn <<-MSG
75
+ [DEPRECATION] :after_commit AASM callback is not safe in terms of race conditions and redundant calls.
76
+ Please add `gem 'after_commit_everywhere', '~> 0.1', '>= 0.1.5'` to your Gemfile in order to fix that.
77
+ MSG
78
+ yield
79
+ end
80
+ end
81
+
64
82
  def aasm_raise_invalid_record
65
83
  raise ActiveRecord::RecordInvalid.new(self)
66
84
  end
@@ -81,6 +81,10 @@ module AASM
81
81
  true
82
82
  end
83
83
 
84
+ def aasm_execute_after_commit
85
+ yield
86
+ end
87
+
84
88
  def aasm_write_state_attribute(state, name=:default)
85
89
  aasm_write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name))
86
90
  end
@@ -116,32 +120,32 @@ module AASM
116
120
 
117
121
  # Returns true if event was fired successfully and transaction completed.
118
122
  def aasm_fire_event(state_machine_name, name, options, *args, &block)
119
- if aasm_supports_transactions? && options[:persist]
120
- event = self.class.aasm(state_machine_name).state_machine.events[name]
121
- event.fire_callbacks(:before_transaction, self, *args)
122
- event.fire_global_callbacks(:before_all_transactions, self, *args)
123
-
124
- begin
125
- success = if options[:persist] && use_transactions?(state_machine_name)
126
- aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
127
- super
128
- end
129
- else
123
+ return super unless aasm_supports_transactions? && options[:persist]
124
+
125
+ event = self.class.aasm(state_machine_name).state_machine.events[name]
126
+ event.fire_callbacks(:before_transaction, self, *args)
127
+ event.fire_global_callbacks(:before_all_transactions, self, *args)
128
+
129
+ begin
130
+ success = if options[:persist] && use_transactions?(state_machine_name)
131
+ aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
130
132
  super
131
133
  end
134
+ else
135
+ super
136
+ end
132
137
 
133
- if success
138
+ if success && !(event.options.keys & [:after_commit, :after_all_commits]).empty?
139
+ aasm_execute_after_commit do
134
140
  event.fire_callbacks(:after_commit, self, *args)
135
141
  event.fire_global_callbacks(:after_all_commits, self, *args)
136
142
  end
137
-
138
- success
139
- ensure
140
- event.fire_callbacks(:after_transaction, self, *args)
141
- event.fire_global_callbacks(:after_all_transactions, self, *args)
142
143
  end
143
- else
144
- super
144
+
145
+ success
146
+ ensure
147
+ event.fire_callbacks(:after_transaction, self, *args)
148
+ event.fire_global_callbacks(:after_all_transactions, self, *args)
145
149
  end
146
150
  end
147
151
 
@@ -2,7 +2,11 @@ RSpec::Matchers.define :transition_from do |from_state|
2
2
  match do |obj|
3
3
  @state_machine_name ||= :default
4
4
  obj.aasm(@state_machine_name).current_state = from_state.to_sym
5
- obj.send(@event, *@args) && obj.aasm(@state_machine_name).current_state == @to_state.to_sym
5
+ begin
6
+ obj.send(@event, *@args) && obj.aasm(@state_machine_name).current_state == @to_state.to_sym
7
+ rescue AASM::InvalidTransition
8
+ false
9
+ end
6
10
  end
7
11
 
8
12
  chain :on do |state_machine_name|
@@ -1,3 +1,3 @@
1
1
  module AASM
2
- VERSION = "5.0.5"
2
+ VERSION = "5.1.1"
3
3
  end
@@ -27,7 +27,7 @@ ActiveRecord::Migration.suppress_messages do
27
27
  t.string "status"
28
28
  end
29
29
 
30
- %w(validators multiple_validators workers invalid_persistors multiple_invalid_persistors silent_persistors multiple_silent_persistors).each do |table_name|
30
+ %w(validators multiple_validators workers invalid_persistors multiple_invalid_persistors silent_persistors multiple_silent_persistors active_record_callbacks).each do |table_name|
31
31
  ActiveRecord::Migration.create_table table_name, :force => true do |t|
32
32
  t.string "name"
33
33
  t.string "status"
@@ -51,4 +51,9 @@ ActiveRecord::Migration.suppress_messages do
51
51
  t.string "search"
52
52
  t.string "sync"
53
53
  end
54
+
55
+ ActiveRecord::Migration.create_table "instance_level_skip_validation_examples", :force => true do |t|
56
+ t.string "state"
57
+ t.string "some_string"
58
+ end
54
59
  end
@@ -4,9 +4,6 @@ en:
4
4
  localizer_test_model:
5
5
  close: "Let's close it!"
6
6
 
7
- attributes:
8
- localizer_test_model:
9
- aasm_state/opened: "It's open now!"
10
7
  errors:
11
8
  messages:
12
9
  record_invalid: "Invalid record"
@@ -1,9 +1,5 @@
1
1
  en:
2
2
  activerecord:
3
- events:
4
- localizer_test_model:
5
- close: "Let's close it!"
6
-
7
3
  attributes:
8
4
  localizer_test_model:
9
5
  aasm_state:
@@ -0,0 +1,5 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ localizer_test_model:
5
+ aasm_state/opened: "It's open now!"
@@ -0,0 +1,93 @@
1
+ class ActiveRecordCallback < ActiveRecord::Base
2
+ include AASM
3
+
4
+ def reset_data
5
+ @data = []
6
+ end
7
+
8
+ def data
9
+ @data.join(' ')
10
+ end
11
+
12
+ aasm column: :status do
13
+ before_all_events :before_all_events
14
+ after_all_events :after_all_events
15
+ ensure_on_all_events :ensure_on_all_events
16
+ after_all_transitions :after_all_transitions
17
+
18
+ state :open, :initial => true,
19
+ :before_enter => :before_enter_open,
20
+ :enter => :enter_open,
21
+ :after_enter => :after_enter_open,
22
+ :before_exit => :before_exit_open,
23
+ :exit => :exit_open,
24
+ :after_exit => :after_exit_open
25
+
26
+ state :closed,
27
+ :before_enter => :before_enter_closed,
28
+ :enter => :enter_closed,
29
+ :after_enter => :after_enter_closed,
30
+ :before_exit => :before_exit_closed,
31
+ :exit => :exit_closed,
32
+ :after_exit => :after_exit_closed
33
+
34
+ event :close,
35
+ :before => :before_event,
36
+ :after => :after_event,
37
+ :guard => :event_guard,
38
+ :before_success => :event_before_success,
39
+ :after_commit => :event_after_commit,
40
+ :ensure => :ensure_event do
41
+ transitions :to => :closed, :from => [:open],
42
+ :guard => :transition_guard,
43
+ :after => :after_transition,
44
+ :success => :success_transition
45
+ end
46
+
47
+ event :open, :before => :before_event, :after => :after_event do
48
+ transitions :to => :open, :from => :closed
49
+ end
50
+ end
51
+
52
+ def log(text)
53
+ @data ||= []
54
+ @data << text
55
+ #puts text
56
+ end
57
+
58
+ def aasm_write_state(*args); log('aasm_write_state'); true; end
59
+ def before_enter_open; log('before_enter_open'); end
60
+ def enter_open; log('enter_open'); end
61
+ def before_exit_open; log('before_exit_open'); end
62
+ def after_enter_open; log('after_enter_open'); end
63
+ def exit_open; log('exit_open'); end
64
+ def after_exit_open; log('after_exit_open'); end
65
+
66
+ def before_enter_closed; log('before_enter_closed'); end
67
+ def enter_closed; log('enter_closed'); end
68
+ def before_exit_closed; log('before_exit_closed'); end
69
+ def exit_closed; log('exit_closed'); end
70
+ def after_enter_closed; log('after_enter_closed'); end
71
+ def after_exit_closed; log('after_exit_closed'); end
72
+
73
+ def event_guard; log('event_guard'); !@fail_event_guard; end
74
+ def transition_guard; log('transition_guard'); !@fail_transition_guard; end
75
+
76
+ def event_before_success; log('event_before_success'); end
77
+
78
+ def after_transition; log('after_transition'); end
79
+ def after_all_transitions; log('after_all_transitions'); end
80
+
81
+ def before_all_events; log('before_all_events') end
82
+ def before_event; log('before_event'); end
83
+ def after_event; log('after_event'); end
84
+ def after_all_events; log('after_all_events'); end
85
+
86
+ def after_transition; log('after_transition'); end
87
+ def success_transition; log('transition_success'); end
88
+
89
+ def ensure_event; log('ensure'); end
90
+ def ensure_on_all_events; log('ensure'); end
91
+
92
+ def event_after_commit; log('after_commit'); end
93
+ end