aasm 5.0.2 → 5.0.7

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.travis.yml +42 -11
  5. data/Appraisals +17 -2
  6. data/CHANGELOG.md +33 -0
  7. data/Dockerfile +1 -6
  8. data/README.md +118 -99
  9. data/aasm.gemspec +3 -0
  10. data/gemfiles/norails.gemfile +10 -0
  11. data/gemfiles/rails_5.0.gemfile +1 -1
  12. data/gemfiles/rails_5.1.gemfile +1 -1
  13. data/gemfiles/rails_5.2.gemfile +13 -0
  14. data/lib/aasm/aasm.rb +29 -27
  15. data/lib/aasm/base.rb +25 -7
  16. data/lib/aasm/core/event.rb +3 -3
  17. data/lib/aasm/instance_base.rb +15 -3
  18. data/lib/aasm/persistence/active_record_persistence.rb +10 -1
  19. data/lib/aasm/persistence/base.rb +1 -1
  20. data/lib/aasm/persistence/core_data_query_persistence.rb +2 -1
  21. data/lib/aasm/persistence/dynamoid_persistence.rb +1 -1
  22. data/lib/aasm/persistence/mongoid_persistence.rb +1 -1
  23. data/lib/aasm/persistence/no_brainer_persistence.rb +1 -1
  24. data/lib/aasm/persistence/orm.rb +23 -19
  25. data/lib/aasm/persistence/redis_persistence.rb +1 -1
  26. data/lib/aasm/rspec/transition_from.rb +5 -1
  27. data/lib/aasm/version.rb +1 -1
  28. data/spec/database.rb +8 -0
  29. data/spec/models/active_record/instance_level_skip_validation_example.rb +19 -0
  30. data/spec/models/active_record/person.rb +23 -0
  31. data/spec/models/callbacks/with_state_arg.rb +5 -1
  32. data/spec/models/callbacks/with_state_arg_multiple.rb +4 -1
  33. data/spec/models/simple_example.rb +6 -0
  34. data/spec/spec_helper.rb +10 -0
  35. data/spec/unit/abstract_class_spec.rb +27 -0
  36. data/spec/unit/callback_multiple_spec.rb +4 -0
  37. data/spec/unit/callbacks_spec.rb +4 -0
  38. data/spec/unit/complex_example_spec.rb +0 -1
  39. data/spec/unit/event_spec.rb +13 -0
  40. data/spec/unit/persistence/active_record_persistence_multiple_spec.rb +4 -4
  41. data/spec/unit/persistence/active_record_persistence_spec.rb +58 -4
  42. data/spec/unit/rspec_matcher_spec.rb +3 -0
  43. data/spec/unit/simple_example_spec.rb +15 -0
  44. metadata +54 -2
@@ -17,12 +17,15 @@ Gem::Specification.new do |s|
17
17
  s.required_ruby_version = '>= 1.9.3'
18
18
 
19
19
  s.add_dependency 'concurrent-ruby', '~> 1.0'
20
+ s.add_dependency 'after_commit_action', '~> 1.0'
20
21
 
21
22
  s.add_development_dependency 'rake'
22
23
  s.add_development_dependency 'sdoc'
23
24
  s.add_development_dependency 'rspec', ">= 3"
24
25
  s.add_development_dependency 'generator_spec'
25
26
  s.add_development_dependency 'appraisal'
27
+ s.add_development_dependency "simplecov"
28
+ s.add_development_dependency "codecov", ">= 0.1.10"
26
29
 
27
30
  # debugging
28
31
  # s.add_development_dependency 'debugger'
@@ -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: "../"
@@ -6,7 +6,7 @@ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
6
  gem "rails", "5.0.0"
7
7
  gem "mongoid", "~> 6.0"
8
8
  gem "sequel"
9
- gem "dynamoid", "~> 1", platforms: :ruby
9
+ gem "dynamoid", "~> 1.3", platforms: :ruby
10
10
  gem "aws-sdk", "~> 2", platforms: :ruby
11
11
  gem "redis-objects"
12
12
 
@@ -6,7 +6,7 @@ gem "sqlite3", "~> 1.3.5", platforms: :ruby
6
6
  gem "rails", "5.1"
7
7
  gem "mongoid", "~>6.0"
8
8
  gem "sequel"
9
- gem "dynamoid", "~> 1", platforms: :ruby
9
+ gem "dynamoid", "~> 1.3", platforms: :ruby
10
10
  gem "aws-sdk", "~>2", platforms: :ruby
11
11
  gem "redis-objects"
12
12
 
@@ -0,0 +1,13 @@
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
+
13
+ gemspec path: "../"
@@ -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,
@@ -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
@@ -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)
@@ -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)
@@ -1,3 +1,4 @@
1
+ require 'after_commit_action'
1
2
  require 'aasm/persistence/orm'
2
3
  module AASM
3
4
  module Persistence
@@ -28,6 +29,7 @@ module AASM
28
29
  # end
29
30
  #
30
31
  def self.included(base)
32
+ base.send(:include, ::AfterCommitAction) unless base.include?(::AfterCommitAction)
31
33
  base.send(:include, AASM::Persistence::Base)
32
34
  base.send(:include, AASM::Persistence::ORM)
33
35
  base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
@@ -88,6 +90,12 @@ module AASM
88
90
  end
89
91
  end
90
92
 
93
+ def aasm_execute_after_commit
94
+ execute_after_commit do
95
+ yield
96
+ end
97
+ end
98
+
91
99
  def aasm_enum(name=:default)
92
100
  case AASM::StateMachineStore.fetch(self.class, true).machine(name).config.enum
93
101
  when false then nil
@@ -141,7 +149,8 @@ module AASM
141
149
 
142
150
  def aasm_column_is_blank?(state_machine_name)
143
151
  attribute_name = self.class.aasm(state_machine_name).attribute_name
144
- attribute_names.include?(attribute_name.to_s) && send(attribute_name).blank?
152
+ attribute_names.include?(attribute_name.to_s) &&
153
+ (send(attribute_name).respond_to?(:empty?) ? !!send(attribute_name).empty? : !send(attribute_name))
145
154
  end
146
155
 
147
156
  def aasm_validate_states
@@ -34,7 +34,7 @@ module AASM
34
34
  # This allows for nil aasm states - be sure to add validation to your model
35
35
  def aasm_read_state(name=:default)
36
36
  state = send(self.class.aasm(name).attribute_name)
37
- if state.blank?
37
+ if !state || state.empty?
38
38
  aasm_new_record? ? aasm(name).determine_state_name(self.class.aasm(name).initial_state) : nil
39
39
  else
40
40
  state.to_sym
@@ -77,7 +77,8 @@ module AASM
77
77
  #
78
78
  def aasm_ensure_initial_state
79
79
  AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |state_machine_name|
80
- send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank?
80
+ next if !send(self.class.aasm(state_machine_name).attribute_name) || send(self.class.aasm(state_machine_name).attribute_name).empty?
81
+ send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s)
81
82
  end
82
83
  end
83
84
  end # InstanceMethods
@@ -83,7 +83,7 @@ module AASM
83
83
  #
84
84
  def aasm_ensure_initial_state
85
85
  AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |state_machine_name|
86
- aasm(state_machine_name).enter_initial_state if send(self.class.aasm(state_machine_name).attribute_name).blank?
86
+ aasm(state_machine_name).enter_initial_state if !send(self.class.aasm(state_machine_name).attribute_name) || send(self.class.aasm(state_machine_name).attribute_name).empty?
87
87
  end
88
88
  end
89
89
  end # InstanceMethods
@@ -106,7 +106,7 @@ module AASM
106
106
  # mongoid has_many relationship does not load child object attributes when
107
107
  # only ids are loaded, for example parent.child_ids will not load child object attributes.
108
108
  # This feature is introduced in mongoid > 4.
109
- if attribute_names.include?(attribute_name) && attributes[attribute_name].blank?
109
+ if attribute_names.include?(attribute_name) && !attributes[attribute_name] || attributes[attribute_name].empty?
110
110
  # attribute_missing? is defined in mongoid > 4
111
111
  return if Mongoid::VERSION.to_f >= 4 && attribute_missing?(attribute_name)
112
112
  send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s)
@@ -96,7 +96,7 @@ module AASM
96
96
  def aasm_ensure_initial_state
97
97
  AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |name|
98
98
  aasm_column = self.class.aasm(name).attribute_name
99
- aasm(name).enter_initial_state if read_attribute(aasm_column).blank?
99
+ aasm(name).enter_initial_state if !read_attribute(aasm_column) || read_attribute(aasm_column).empty?
100
100
  end
101
101
  end
102
102
  end # InstanceMethods
@@ -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
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
 
@@ -69,7 +69,7 @@ module AASM
69
69
  def aasm_ensure_initial_state
70
70
  AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |name|
71
71
  aasm_column = self.class.aasm(name).attribute_name
72
- aasm(name).enter_initial_state if send(aasm_column).value.blank?
72
+ aasm(name).enter_initial_state if !send(aasm_column).value || send(aasm_column).value.empty?
73
73
  end
74
74
  end
75
75
 
@@ -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.2"
2
+ VERSION = "5.0.7"
3
3
  end
@@ -14,6 +14,9 @@ ActiveRecord::Migration.suppress_messages do
14
14
  ActiveRecord::Migration.create_table "implemented_abstract_class_dsls", :force => true do |t|
15
15
  t.string "status"
16
16
  end
17
+ ActiveRecord::Migration.create_table "users", :force => true do |t|
18
+ t.string "status"
19
+ end
17
20
 
18
21
  ActiveRecord::Migration.create_table "complex_active_record_examples", :force => true do |t|
19
22
  t.string "left"
@@ -48,4 +51,9 @@ ActiveRecord::Migration.suppress_messages do
48
51
  t.string "search"
49
52
  t.string "sync"
50
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
51
59
  end