statesman 9.0.0 → 13.0.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +31 -0
  3. data/.devcontainer/docker-compose.yml +39 -0
  4. data/.github/workflows/tests.yml +130 -0
  5. data/.gitignore +65 -15
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +11 -1
  8. data/.rubocop_todo.yml +23 -38
  9. data/.ruby-version +1 -1
  10. data/CHANGELOG.md +229 -43
  11. data/CONTRIBUTING.md +14 -13
  12. data/Gemfile +18 -3
  13. data/README.md +203 -74
  14. data/docs/COMPATIBILITY.md +3 -3
  15. data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
  16. data/lib/generators/statesman/generator_helpers.rb +2 -2
  17. data/lib/statesman/adapters/active_record.rb +69 -52
  18. data/lib/statesman/adapters/active_record_queries.rb +15 -7
  19. data/lib/statesman/adapters/active_record_transition.rb +5 -1
  20. data/lib/statesman/adapters/memory.rb +1 -1
  21. data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
  22. data/lib/statesman/callback.rb +2 -2
  23. data/lib/statesman/config.rb +3 -10
  24. data/lib/statesman/exceptions.rb +9 -7
  25. data/lib/statesman/guard.rb +1 -1
  26. data/lib/statesman/machine.rb +60 -0
  27. data/lib/statesman/version.rb +1 -1
  28. data/lib/statesman.rb +5 -5
  29. data/lib/tasks/statesman.rake +5 -5
  30. data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
  31. data/spec/generators/statesman/migration_generator_spec.rb +5 -1
  32. data/spec/spec_helper.rb +44 -7
  33. data/spec/statesman/adapters/active_record_queries_spec.rb +8 -10
  34. data/spec/statesman/adapters/active_record_spec.rb +144 -55
  35. data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
  36. data/spec/statesman/adapters/memory_spec.rb +0 -1
  37. data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
  38. data/spec/statesman/adapters/shared_examples.rb +6 -7
  39. data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +206 -0
  40. data/spec/statesman/callback_spec.rb +0 -2
  41. data/spec/statesman/config_spec.rb +0 -2
  42. data/spec/statesman/exceptions_spec.rb +8 -4
  43. data/spec/statesman/guard_spec.rb +0 -2
  44. data/spec/statesman/machine_spec.rb +231 -19
  45. data/spec/statesman/utils_spec.rb +0 -2
  46. data/spec/support/active_record.rb +156 -29
  47. data/spec/support/exactly_query_databases.rb +35 -0
  48. data/statesman.gemspec +2 -17
  49. metadata +14 -238
  50. data/.circleci/config.yml +0 -127
@@ -7,19 +7,15 @@ module Statesman
7
7
  class ActiveRecord
8
8
  JSON_COLUMN_TYPES = %w[json jsonb].freeze
9
9
 
10
- def self.database_supports_partial_indexes?
10
+ def self.database_supports_partial_indexes?(model)
11
11
  # Rails 3 doesn't implement `supports_partial_index?`
12
- if ::ActiveRecord::Base.connection.respond_to?(:supports_partial_index?)
13
- ::ActiveRecord::Base.connection.supports_partial_index?
12
+ if model.connection.respond_to?(:supports_partial_index?)
13
+ model.connection.supports_partial_index?
14
14
  else
15
- ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
15
+ model.connection.adapter_name.casecmp("postgresql").zero?
16
16
  end
17
17
  end
18
18
 
19
- def self.adapter_name
20
- ::ActiveRecord::Base.connection.adapter_name.downcase
21
- end
22
-
23
19
  def initialize(transition_class, parent_model, observer, options = {})
24
20
  serialized = serialized?(transition_class)
25
21
  column_type = transition_class.columns_hash["metadata"].sql_type
@@ -52,7 +48,7 @@ module Statesman
52
48
 
53
49
  raise
54
50
  ensure
55
- @last_transition = nil
51
+ reset
56
52
  end
57
53
 
58
54
  def history(force_reload: false)
@@ -65,32 +61,33 @@ module Statesman
65
61
  end
66
62
  end
67
63
 
68
- # rubocop:disable Naming/MemoizedInstanceVariableName
69
64
  def last(force_reload: false)
70
65
  if force_reload
71
66
  @last_transition = history(force_reload: true).last
67
+ elsif instance_variable_defined?(:@last_transition)
68
+ @last_transition
72
69
  else
73
- @last_transition ||= history.last
70
+ @last_transition = history.last
74
71
  end
75
72
  end
76
- # rubocop:enable Naming/MemoizedInstanceVariableName
77
73
 
78
74
  def reset
79
- @last_transition = nil
75
+ if instance_variable_defined?(:@last_transition)
76
+ remove_instance_variable(:@last_transition)
77
+ end
80
78
  end
81
79
 
82
80
  private
83
81
 
84
- # rubocop:disable Metrics/MethodLength
85
82
  def create_transition(from, to, metadata)
86
83
  transition = transitions_for_parent.build(
87
84
  default_transition_attributes(to, metadata),
88
85
  )
89
86
 
90
- ::ActiveRecord::Base.transaction(requires_new: true) do
87
+ transition_class.transaction(requires_new: true) do
91
88
  @observer.execute(:before, from, to, transition)
92
89
 
93
- if mysql_gaplock_protection?
90
+ if mysql_gaplock_protection?(transition_class.connection)
94
91
  # We save the transition first with most_recent falsy, then mark most_recent
95
92
  # true after to avoid letting MySQL acquire a next-key lock which can cause
96
93
  # deadlocks.
@@ -118,7 +115,6 @@ module Statesman
118
115
 
119
116
  transition
120
117
  end
121
- # rubocop:enable Metrics/MethodLength
122
118
 
123
119
  def default_transition_attributes(to, metadata)
124
120
  {
@@ -130,8 +126,8 @@ module Statesman
130
126
  end
131
127
 
132
128
  def add_after_commit_callback(from, to, transition)
133
- ::ActiveRecord::Base.connection.add_transaction_record(
134
- ActiveRecordAfterCommitWrap.new do
129
+ transition_class.connection.add_transaction_record(
130
+ ActiveRecordAfterCommitWrap.new(transition_class.connection) do
135
131
  @observer.execute(:after_commit, from, to, transition)
136
132
  end,
137
133
  )
@@ -144,7 +140,7 @@ module Statesman
144
140
  # Sets the given transition most_recent = t while unsetting the most_recent of any
145
141
  # previous transitions.
146
142
  def update_most_recents(most_recent_id = nil)
147
- update = build_arel_manager(::Arel::UpdateManager)
143
+ update = build_arel_manager(::Arel::UpdateManager, transition_class)
148
144
  update.table(transition_table)
149
145
  update.where(most_recent_transitions(most_recent_id))
150
146
  update.set(build_most_recents_update_all_values(most_recent_id))
@@ -152,20 +148,33 @@ module Statesman
152
148
  # MySQL will validate index constraints across the intermediate result of an
153
149
  # update. This means we must order our update to deactivate the previous
154
150
  # most_recent before setting the new row to be true.
155
- update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
151
+ if mysql_gaplock_protection?(transition_class.connection)
152
+ update.order(transition_table[:most_recent].desc)
153
+ end
156
154
 
157
- ::ActiveRecord::Base.connection.update(update.to_sql)
155
+ transition_class.connection.update(update.to_sql(transition_class))
158
156
  end
159
157
 
160
158
  def most_recent_transitions(most_recent_id = nil)
161
159
  if most_recent_id
162
- transitions_of_parent.and(
160
+ concrete_transitions_of_parent.and(
163
161
  transition_table[:id].eq(most_recent_id).or(
164
162
  transition_table[:most_recent].eq(true),
165
163
  ),
166
164
  )
167
165
  else
168
- transitions_of_parent.and(transition_table[:most_recent].eq(true))
166
+ concrete_transitions_of_parent.and(transition_table[:most_recent].eq(true))
167
+ end
168
+ end
169
+
170
+ def concrete_transitions_of_parent
171
+ if transition_sti?
172
+ transitions_of_parent.and(
173
+ transition_table[transition_class.inheritance_column].
174
+ eq(transition_class.name),
175
+ )
176
+ else
177
+ transitions_of_parent
169
178
  end
170
179
  end
171
180
 
@@ -212,7 +221,7 @@ module Statesman
212
221
  if most_recent_id
213
222
  Arel::Nodes::Case.new.
214
223
  when(transition_table[:id].eq(most_recent_id)).then(db_true).
215
- else(not_most_recent_value).to_sql
224
+ else(not_most_recent_value).to_sql(transition_class)
216
225
  else
217
226
  Arel::Nodes::SqlLiteral.new(not_most_recent_value)
218
227
  end
@@ -222,26 +231,21 @@ module Statesman
222
231
  # change in Arel as we move into Rails >6.0.
223
232
  #
224
233
  # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
225
- def build_arel_manager(manager)
234
+ def build_arel_manager(manager, engine)
226
235
  if manager.instance_method(:initialize).arity.zero?
227
236
  manager.new
228
237
  else
229
- manager.new(::ActiveRecord::Base)
238
+ manager.new(engine)
230
239
  end
231
240
  end
232
241
 
233
242
  def next_sort_key
234
- (last && last.sort_key + 10) || 10
243
+ (last && (last.sort_key + 10)) || 10
235
244
  end
236
245
 
237
246
  def serialized?(transition_class)
238
- if ::ActiveRecord.respond_to?(:gem_version) &&
239
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
240
- transition_class.type_for_attribute("metadata").
241
- is_a?(::ActiveRecord::Type::Serialized)
242
- else
243
- transition_class.serialized_attributes.include?("metadata")
244
- end
247
+ transition_class.type_for_attribute("metadata").
248
+ is_a?(::ActiveRecord::Type::Serialized)
245
249
  end
246
250
 
247
251
  def transition_conflict_error?(err)
@@ -252,7 +256,7 @@ module Statesman
252
256
  end
253
257
 
254
258
  def unique_indexes
255
- ::ActiveRecord::Base.connection.
259
+ transition_class.connection.
256
260
  indexes(transition_class.table_name).
257
261
  select do |index|
258
262
  next unless index.unique
@@ -264,13 +268,18 @@ module Statesman
264
268
  end
265
269
  end
266
270
 
267
- def parent_join_foreign_key
268
- association =
269
- parent_model.class.
270
- reflect_on_all_associations(:has_many).
271
- find { |r| r.name.to_s == @association_name.to_s }
271
+ def transition_sti?
272
+ transition_class.column_names.include?(transition_class.inheritance_column)
273
+ end
272
274
 
273
- association_join_primary_key(association)
275
+ def parent_association
276
+ parent_model.class.
277
+ reflect_on_all_associations(:has_many).
278
+ find { |r| r.name.to_s == @association_name.to_s }
279
+ end
280
+
281
+ def parent_join_foreign_key
282
+ association_join_primary_key(parent_association)
274
283
  end
275
284
 
276
285
  def association_join_primary_key(association)
@@ -304,20 +313,30 @@ module Statesman
304
313
  return nil if column.nil?
305
314
 
306
315
  [
307
- column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
316
+ column, default_timezone == :utc ? Time.now.utc : Time.now
308
317
  ]
309
318
  end
310
319
 
311
- def mysql_gaplock_protection?
312
- Statesman.mysql_gaplock_protection?
320
+ def default_timezone
321
+ # Rails 7 deprecates ActiveRecord::Base.default_timezone
322
+ # in favour of ActiveRecord.default_timezone
323
+ if ::ActiveRecord.respond_to?(:default_timezone)
324
+ return ::ActiveRecord.default_timezone
325
+ end
326
+
327
+ ::ActiveRecord::Base.default_timezone
328
+ end
329
+
330
+ def mysql_gaplock_protection?(connection)
331
+ Statesman.mysql_gaplock_protection?(connection)
313
332
  end
314
333
 
315
334
  def db_true
316
- ::ActiveRecord::Base.connection.quote(type_cast(true))
335
+ transition_class.connection.quote(type_cast(true))
317
336
  end
318
337
 
319
338
  def db_false
320
- ::ActiveRecord::Base.connection.quote(type_cast(false))
339
+ transition_class.connection.quote(type_cast(false))
321
340
  end
322
341
 
323
342
  def db_null
@@ -327,7 +346,7 @@ module Statesman
327
346
  # Type casting against a column is deprecated and will be removed in Rails 6.2.
328
347
  # See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
329
348
  def type_cast(value)
330
- ::ActiveRecord::Base.connection.type_cast(value)
349
+ transition_class.connection.type_cast(value)
331
350
  end
332
351
 
333
352
  # Check whether the `most_recent` column allows null values. If it doesn't, set old
@@ -347,9 +366,9 @@ module Statesman
347
366
  end
348
367
 
349
368
  class ActiveRecordAfterCommitWrap
350
- def initialize(&block)
369
+ def initialize(connection, &block)
351
370
  @callback = block
352
- @connection = ::ActiveRecord::Base.connection
371
+ @connection = connection
353
372
  end
354
373
 
355
374
  def self.trigger_transactional_callbacks?
@@ -360,11 +379,9 @@ module Statesman
360
379
  true
361
380
  end
362
381
 
363
- # rubocop: disable Naming/PredicateName
364
382
  def has_transactional_callbacks?
365
383
  true
366
384
  end
367
- # rubocop: enable Naming/PredicateName
368
385
 
369
386
  def committed!(*)
370
387
  @callback.call
@@ -39,7 +39,7 @@ module Statesman
39
39
  end
40
40
 
41
41
  def included(base)
42
- ensure_inheritance(base)
42
+ ensure_inheritance(base) if base.respond_to?(:subclasses) && base.subclasses.any?
43
43
 
44
44
  query_builder = QueryBuilder.new(base, **@args)
45
45
 
@@ -49,6 +49,14 @@ module Statesman
49
49
 
50
50
  define_in_state(base, query_builder)
51
51
  define_not_in_state(base, query_builder)
52
+
53
+ define_method(:reload) do |*a|
54
+ instance = super(*a)
55
+ if instance.respond_to?(:state_machine, true)
56
+ instance.send(:state_machine).reset
57
+ end
58
+ instance
59
+ end
52
60
  end
53
61
 
54
62
  private
@@ -95,18 +103,18 @@ module Statesman
95
103
  def states_where(states)
96
104
  if initial_state.to_s.in?(states.map(&:to_s))
97
105
  "#{most_recent_transition_alias}.to_state IN (?) OR " \
98
- "#{most_recent_transition_alias}.to_state IS NULL"
106
+ "#{most_recent_transition_alias}.to_state IS NULL"
99
107
  else
100
108
  "#{most_recent_transition_alias}.to_state IN (?) AND " \
101
- "#{most_recent_transition_alias}.to_state IS NOT NULL"
109
+ "#{most_recent_transition_alias}.to_state IS NOT NULL"
102
110
  end
103
111
  end
104
112
 
105
113
  def most_recent_transition_join
106
114
  "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias} " \
107
- "ON #{model.table_name}.#{model_primary_key} = " \
108
- "#{most_recent_transition_alias}.#{model_foreign_key} " \
109
- "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
115
+ "ON #{model.table_name}.#{model_primary_key} = " \
116
+ "#{most_recent_transition_alias}.#{model_foreign_key} " \
117
+ "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
110
118
  end
111
119
 
112
120
  private
@@ -145,7 +153,7 @@ module Statesman
145
153
  end
146
154
 
147
155
  def db_true
148
- ::ActiveRecord::Base.connection.quote(true)
156
+ model.connection.quote(true)
149
157
  end
150
158
  end
151
159
  end
@@ -10,7 +10,11 @@ module Statesman
10
10
  extend ActiveSupport::Concern
11
11
 
12
12
  included do
13
- serialize :metadata, JSON
13
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.1")
14
+ serialize :metadata, coder: JSON
15
+ else
16
+ serialize :metadata, JSON
17
+ end
14
18
 
15
19
  class_attribute :updated_timestamp_column
16
20
  self.updated_timestamp_column = DEFAULT_UPDATED_TIMESTAMP_COLUMN
@@ -44,7 +44,7 @@ module Statesman
44
44
  private
45
45
 
46
46
  def next_sort_key
47
- (last && last.sort_key + 10) || 10
47
+ (last && (last.sort_key + 10)) || 10
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module Adapters
5
+ module TypeSafeActiveRecordQueries
6
+ def configure_state_machine(args = {})
7
+ transition_class = args.fetch(:transition_class)
8
+ initial_state = args.fetch(:initial_state)
9
+
10
+ include(
11
+ ActiveRecordQueries::ClassMethods.new(
12
+ transition_class: transition_class,
13
+ initial_state: initial_state,
14
+ most_recent_transition_alias: try(:most_recent_transition_alias),
15
+ transition_name: try(:transition_name),
16
+ ),
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -40,11 +40,11 @@ module Statesman
40
40
  end
41
41
 
42
42
  def matches_from_state(from, to)
43
- (from == self.from && (to.nil? || self.to.empty?))
43
+ from == self.from && (to.nil? || self.to.empty?)
44
44
  end
45
45
 
46
46
  def matches_to_state(from, to)
47
- ((from.nil? || self.from.nil?) && self.to.include?(to))
47
+ (from.nil? || self.from.nil?) && self.to.include?(to)
48
48
  end
49
49
 
50
50
  def matches_both_states(from, to)
@@ -15,17 +15,10 @@ module Statesman
15
15
  @adapter_class = adapter_class
16
16
  end
17
17
 
18
- def mysql_gaplock_protection?
19
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
20
-
18
+ def mysql_gaplock_protection?(connection)
21
19
  # If our adapter class suggests we're using mysql, enable gaplock protection by
22
20
  # default.
23
- enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
24
- @mysql_gaplock_protection
25
- end
26
-
27
- def enable_mysql_gaplock_protection
28
- @mysql_gaplock_protection = true
21
+ mysql_adapter?(connection)
29
22
  end
30
23
 
31
24
  private
@@ -34,7 +27,7 @@ module Statesman
34
27
  adapter_name = adapter_name(adapter_class)
35
28
  return false unless adapter_name
36
29
 
37
- adapter_name.start_with?("mysql")
30
+ adapter_name.downcase.start_with?("mysql", "trilogy")
38
31
  end
39
32
 
40
33
  def adapter_name(adapter_class)
@@ -28,13 +28,15 @@ module Statesman
28
28
  end
29
29
 
30
30
  class GuardFailedError < StandardError
31
- def initialize(from, to)
31
+ def initialize(from, to, callback)
32
32
  @from = from
33
33
  @to = to
34
+ @callback = callback
34
35
  super(_message)
36
+ set_backtrace(callback.source_location.join(":")) if callback&.source_location
35
37
  end
36
38
 
37
- attr_reader :from, :to
39
+ attr_reader :from, :to, :callback
38
40
 
39
41
  private
40
42
 
@@ -52,8 +54,8 @@ module Statesman
52
54
 
53
55
  def _message(transition_class_name)
54
56
  "#{transition_class_name}#metadata is not serialized. If you " \
55
- "are using a non-json column type, you should `include " \
56
- "Statesman::Adapters::ActiveRecordTransition`"
57
+ "are using a non-json column type, you should `include " \
58
+ "Statesman::Adapters::ActiveRecordTransition`"
57
59
  end
58
60
  end
59
61
 
@@ -66,9 +68,9 @@ module Statesman
66
68
 
67
69
  def _message(transition_class_name)
68
70
  "#{transition_class_name}#metadata column type cannot be json " \
69
- "and serialized simultaneously. If you are using a json " \
70
- "column type, it is not necessary to `include " \
71
- "Statesman::Adapters::ActiveRecordTransition`"
71
+ "and serialized simultaneously. If you are using a json " \
72
+ "column type, it is not necessary to `include " \
73
+ "Statesman::Adapters::ActiveRecordTransition`"
72
74
  end
73
75
  end
74
76
  end
@@ -6,7 +6,7 @@ require_relative "exceptions"
6
6
  module Statesman
7
7
  class Guard < Callback
8
8
  def call(*args)
9
- raise GuardFailedError.new(from, to) unless super(*args)
9
+ raise GuardFailedError.new(from, to, callback) unless super
10
10
  end
11
11
  end
12
12
  end
@@ -42,6 +42,17 @@ module Statesman
42
42
  states << name
43
43
  end
44
44
 
45
+ def remove_state(state_name)
46
+ state_name = state_name.to_s
47
+
48
+ remove_transitions(from: state_name)
49
+ remove_transitions(to: state_name)
50
+ remove_callbacks(from: state_name)
51
+ remove_callbacks(to: state_name)
52
+
53
+ @states.delete(state_name.to_s)
54
+ end
55
+
45
56
  def successors
46
57
  @successors ||= {}
47
58
  end
@@ -70,6 +81,20 @@ module Statesman
70
81
  successors[from] += to
71
82
  end
72
83
 
84
+ def remove_transitions(from: nil, to: nil)
85
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
86
+ return if successors.nil?
87
+
88
+ if from.present?
89
+ @successors[from.to_s].delete(to.to_s) if to.present?
90
+ @successors.delete(from.to_s) if to.nil? || successors[from.to_s].empty?
91
+ elsif to.present?
92
+ @successors.
93
+ transform_values! { |to_states| to_states - [to.to_s] }.
94
+ filter! { |_from_state, to_states| to_states.any? }
95
+ end
96
+ end
97
+
73
98
  def before_transition(options = {}, &block)
74
99
  add_callback(callback_type: :before, callback_class: Callback,
75
100
  from: options[:from], to: options[:to], &block)
@@ -151,6 +176,33 @@ module Statesman
151
176
  callback_class.new(from: from, to: to, callback: block)
152
177
  end
153
178
 
179
+ def remove_callbacks(from: nil, to: nil)
180
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
181
+ return if callbacks.nil?
182
+
183
+ @callbacks.transform_values! do |callbacks|
184
+ filter_callbacks(callbacks, from: from, to: to)
185
+ end
186
+ end
187
+
188
+ def filter_callbacks(callbacks, from: nil, to: nil)
189
+ callbacks.filter_map do |callback|
190
+ next if callback.from == from && to.nil?
191
+
192
+ if callback.to.include?(to) && (from.nil? || callback.from == from)
193
+ next if callback.to == [to]
194
+
195
+ callback = Statesman::Callback.new({
196
+ from: callback.from,
197
+ to: callback.to - [to],
198
+ callback: callback.callback,
199
+ })
200
+ end
201
+
202
+ callback
203
+ end
204
+ end
205
+
154
206
  def validate_callback_type_and_class(callback_type, callback_class)
155
207
  raise ArgumentError, "missing keyword: callback_type" if callback_type.nil?
156
208
  raise ArgumentError, "missing keyword: callback_class" if callback_class.nil?
@@ -181,12 +233,20 @@ module Statesman
181
233
  def initialize(object,
182
234
  options = {
183
235
  transition_class: Statesman::Adapters::MemoryTransition,
236
+ initial_transition: false,
184
237
  })
185
238
  @object = object
186
239
  @transition_class = options[:transition_class]
187
240
  @storage_adapter = adapter_class(@transition_class).new(
188
241
  @transition_class, object, self, options
189
242
  )
243
+
244
+ if options[:initial_transition]
245
+ if history.empty? && self.class.initial_state
246
+ @storage_adapter.create(nil, self.class.initial_state)
247
+ end
248
+ end
249
+
190
250
  send(:after_initialize) if respond_to? :after_initialize
191
251
  end
192
252
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "9.0.0"
4
+ VERSION = "13.0.0"
5
5
  end
data/lib/statesman.rb CHANGED
@@ -6,7 +6,7 @@ module Statesman
6
6
  autoload :Callback, "statesman/callback"
7
7
  autoload :Guard, "statesman/guard"
8
8
  autoload :Utils, "statesman/utils"
9
- autoload :Version, "statesman/version"
9
+ autoload :VERSION, "statesman/version"
10
10
  module Adapters
11
11
  autoload :Memory, "statesman/adapters/memory"
12
12
  autoload :ActiveRecord, "statesman/adapters/active_record"
@@ -14,6 +14,8 @@ module Statesman
14
14
  "statesman/adapters/active_record_transition"
15
15
  autoload :ActiveRecordQueries,
16
16
  "statesman/adapters/active_record_queries"
17
+ autoload :TypeSafeActiveRecordQueries,
18
+ "statesman/adapters/type_safe_active_record_queries"
17
19
  end
18
20
  require "statesman/railtie" if defined?(::Rails::Railtie)
19
21
 
@@ -32,10 +34,8 @@ module Statesman
32
34
  @storage_adapter || Adapters::Memory
33
35
  end
34
36
 
35
- def self.mysql_gaplock_protection?
36
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
37
-
38
- @mysql_gaplock_protection = config.mysql_gaplock_protection?
37
+ def self.mysql_gaplock_protection?(connection)
38
+ config.mysql_gaplock_protection?(connection)
39
39
  end
40
40
 
41
41
  def self.config
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :statesman do
4
- desc "Set most_recent to false for old transitions and to true for the "\
4
+ desc "Set most_recent to false for old transitions and to true for the " \
5
5
  "latest one. Safe to re-run"
6
6
  task :backfill_most_recent, [:parent_model_name] => :environment do |_, args|
7
7
  parent_model_name = args.parent_model_name
@@ -21,8 +21,8 @@ namespace :statesman do
21
21
  batch_size = 500
22
22
 
23
23
  parent_class.find_in_batches(batch_size: batch_size) do |models|
24
- ActiveRecord::Base.transaction(requires_new: true) do
25
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
24
+ transition_class.transaction(requires_new: true) do
25
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(transition_class)
26
26
  # Set all transitions' most_recent to FALSE
27
27
  transition_class.where(parent_fk => models.map(&:id)).
28
28
  update_all(most_recent: false, updated_at: updated_at)
@@ -56,8 +56,8 @@ namespace :statesman do
56
56
  end
57
57
 
58
58
  done_models += batch_size
59
- puts "Updated #{transition_class.name.pluralize} for "\
60
- "#{[done_models, total_models].min}/#{total_models} "\
59
+ puts "Updated #{transition_class.name.pluralize} for " \
60
+ "#{[done_models, total_models].min}/#{total_models} " \
61
61
  "#{parent_model_name.pluralize}"
62
62
  end
63
63
  end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/active_record_transition_generator"
6
5
 
7
6
  describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
7
+ before do
8
+ stub_const("Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("BaconTransition", Class.new(ActiveRecord::Base))
10
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
11
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
12
+ end
13
+
8
14
  it_behaves_like "a generator" do
9
15
  let(:migration_name) { "db/migrate/create_bacon_transitions.rb" }
10
16
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/migration_generator"
6
5
 
7
6
  describe Statesman::MigrationGenerator, type: :generator do
7
+ before do
8
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
10
+ end
11
+
8
12
  it_behaves_like "a generator" do
9
13
  let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" }
10
14
  end