statesman 7.4.0 → 12.1.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +7 -0
  3. data/.github/workflows/tests.yml +112 -0
  4. data/.gitignore +65 -15
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +14 -1
  7. data/.rubocop_todo.yml +37 -28
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +262 -41
  10. data/CONTRIBUTING.md +23 -4
  11. data/Gemfile +4 -6
  12. data/README.md +243 -43
  13. data/docs/COMPATIBILITY.md +2 -2
  14. data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
  15. data/lib/generators/statesman/generator_helpers.rb +12 -4
  16. data/lib/statesman/adapters/active_record.rb +84 -55
  17. data/lib/statesman/adapters/active_record_queries.rb +19 -7
  18. data/lib/statesman/adapters/active_record_transition.rb +5 -1
  19. data/lib/statesman/adapters/memory.rb +5 -1
  20. data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
  21. data/lib/statesman/callback.rb +2 -2
  22. data/lib/statesman/config.rb +3 -10
  23. data/lib/statesman/exceptions.rb +13 -7
  24. data/lib/statesman/guard.rb +1 -1
  25. data/lib/statesman/machine.rb +68 -0
  26. data/lib/statesman/version.rb +1 -1
  27. data/lib/statesman.rb +5 -5
  28. data/lib/tasks/statesman.rake +5 -5
  29. data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
  30. data/spec/generators/statesman/migration_generator_spec.rb +5 -1
  31. data/spec/spec_helper.rb +44 -7
  32. data/spec/statesman/adapters/active_record_queries_spec.rb +34 -12
  33. data/spec/statesman/adapters/active_record_spec.rb +176 -51
  34. data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
  35. data/spec/statesman/adapters/memory_spec.rb +0 -1
  36. data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
  37. data/spec/statesman/adapters/shared_examples.rb +3 -4
  38. data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +206 -0
  39. data/spec/statesman/callback_spec.rb +0 -2
  40. data/spec/statesman/config_spec.rb +0 -2
  41. data/spec/statesman/exceptions_spec.rb +17 -4
  42. data/spec/statesman/guard_spec.rb +0 -2
  43. data/spec/statesman/machine_spec.rb +252 -15
  44. data/spec/statesman/utils_spec.rb +0 -2
  45. data/spec/support/active_record.rb +156 -24
  46. data/spec/support/exactly_query_databases.rb +35 -0
  47. data/statesman.gemspec +9 -10
  48. metadata +32 -59
  49. data/.circleci/config.yml +0 -187
@@ -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
@@ -42,11 +38,17 @@ module Statesman
42
38
  def create(from, to, metadata = {})
43
39
  create_transition(from.to_s, to.to_s, metadata)
44
40
  rescue ::ActiveRecord::RecordNotUnique => e
45
- raise TransitionConflictError, e.message if transition_conflict_error? e
41
+ if transition_conflict_error? e
42
+ # The history has the invalid transition on the end of it, which means
43
+ # `current_state` would then be incorrect. We force a reload of the history to
44
+ # avoid this.
45
+ transitions_for_parent.reload
46
+ raise TransitionConflictError, e.message
47
+ end
46
48
 
47
49
  raise
48
50
  ensure
49
- @last_transition = nil
51
+ reset
50
52
  end
51
53
 
52
54
  def history(force_reload: false)
@@ -62,23 +64,30 @@ module Statesman
62
64
  def last(force_reload: false)
63
65
  if force_reload
64
66
  @last_transition = history(force_reload: true).last
67
+ elsif instance_variable_defined?(:@last_transition)
68
+ @last_transition
65
69
  else
66
- @last_transition ||= history.last
70
+ @last_transition = history.last
71
+ end
72
+ end
73
+
74
+ def reset
75
+ if instance_variable_defined?(:@last_transition)
76
+ remove_instance_variable(:@last_transition)
67
77
  end
68
78
  end
69
79
 
70
80
  private
71
81
 
72
- # rubocop:disable Metrics/MethodLength
73
82
  def create_transition(from, to, metadata)
74
83
  transition = transitions_for_parent.build(
75
84
  default_transition_attributes(to, metadata),
76
85
  )
77
86
 
78
- ::ActiveRecord::Base.transaction(requires_new: true) do
87
+ transition_class.transaction(requires_new: true) do
79
88
  @observer.execute(:before, from, to, transition)
80
89
 
81
- if mysql_gaplock_protection?
90
+ if mysql_gaplock_protection?(transition_class.connection)
82
91
  # We save the transition first with most_recent falsy, then mark most_recent
83
92
  # true after to avoid letting MySQL acquire a next-key lock which can cause
84
93
  # deadlocks.
@@ -106,7 +115,6 @@ module Statesman
106
115
 
107
116
  transition
108
117
  end
109
- # rubocop:enable Metrics/MethodLength
110
118
 
111
119
  def default_transition_attributes(to, metadata)
112
120
  {
@@ -118,8 +126,8 @@ module Statesman
118
126
  end
119
127
 
120
128
  def add_after_commit_callback(from, to, transition)
121
- ::ActiveRecord::Base.connection.add_transaction_record(
122
- ActiveRecordAfterCommitWrap.new do
129
+ transition_class.connection.add_transaction_record(
130
+ ActiveRecordAfterCommitWrap.new(transition_class.connection) do
123
131
  @observer.execute(:after_commit, from, to, transition)
124
132
  end,
125
133
  )
@@ -132,7 +140,7 @@ module Statesman
132
140
  # Sets the given transition most_recent = t while unsetting the most_recent of any
133
141
  # previous transitions.
134
142
  def update_most_recents(most_recent_id = nil)
135
- update = build_arel_manager(::Arel::UpdateManager)
143
+ update = build_arel_manager(::Arel::UpdateManager, transition_class)
136
144
  update.table(transition_table)
137
145
  update.where(most_recent_transitions(most_recent_id))
138
146
  update.set(build_most_recents_update_all_values(most_recent_id))
@@ -140,20 +148,33 @@ module Statesman
140
148
  # MySQL will validate index constraints across the intermediate result of an
141
149
  # update. This means we must order our update to deactivate the previous
142
150
  # most_recent before setting the new row to be true.
143
- 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
144
154
 
145
- ::ActiveRecord::Base.connection.update(update.to_sql)
155
+ transition_class.connection.update(update.to_sql(transition_class))
146
156
  end
147
157
 
148
158
  def most_recent_transitions(most_recent_id = nil)
149
159
  if most_recent_id
150
- transitions_of_parent.and(
160
+ concrete_transitions_of_parent.and(
151
161
  transition_table[:id].eq(most_recent_id).or(
152
162
  transition_table[:most_recent].eq(true),
153
163
  ),
154
164
  )
155
165
  else
156
- 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
157
178
  end
158
179
  end
159
180
 
@@ -200,7 +221,7 @@ module Statesman
200
221
  if most_recent_id
201
222
  Arel::Nodes::Case.new.
202
223
  when(transition_table[:id].eq(most_recent_id)).then(db_true).
203
- else(not_most_recent_value).to_sql
224
+ else(not_most_recent_value).to_sql(transition_class)
204
225
  else
205
226
  Arel::Nodes::SqlLiteral.new(not_most_recent_value)
206
227
  end
@@ -210,26 +231,21 @@ module Statesman
210
231
  # change in Arel as we move into Rails >6.0.
211
232
  #
212
233
  # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
213
- def build_arel_manager(manager)
234
+ def build_arel_manager(manager, engine)
214
235
  if manager.instance_method(:initialize).arity.zero?
215
236
  manager.new
216
237
  else
217
- manager.new(::ActiveRecord::Base)
238
+ manager.new(engine)
218
239
  end
219
240
  end
220
241
 
221
242
  def next_sort_key
222
- (last && last.sort_key + 10) || 10
243
+ (last && (last.sort_key + 10)) || 10
223
244
  end
224
245
 
225
246
  def serialized?(transition_class)
226
- if ::ActiveRecord.respond_to?(:gem_version) &&
227
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
228
- transition_class.type_for_attribute("metadata").
229
- is_a?(::ActiveRecord::Type::Serialized)
230
- else
231
- transition_class.serialized_attributes.include?("metadata")
232
- end
247
+ transition_class.type_for_attribute("metadata").
248
+ is_a?(::ActiveRecord::Type::Serialized)
233
249
  end
234
250
 
235
251
  def transition_conflict_error?(err)
@@ -240,7 +256,7 @@ module Statesman
240
256
  end
241
257
 
242
258
  def unique_indexes
243
- ::ActiveRecord::Base.connection.
259
+ transition_class.connection.
244
260
  indexes(transition_class.table_name).
245
261
  select do |index|
246
262
  next unless index.unique
@@ -252,13 +268,18 @@ module Statesman
252
268
  end
253
269
  end
254
270
 
255
- def parent_join_foreign_key
256
- association =
257
- parent_model.class.
258
- reflect_on_all_associations(:has_many).
259
- 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
260
274
 
261
- 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)
262
283
  end
263
284
 
264
285
  def association_join_primary_key(association)
@@ -292,34 +313,42 @@ module Statesman
292
313
  return nil if column.nil?
293
314
 
294
315
  [
295
- column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
316
+ column, default_timezone == :utc ? Time.now.utc : Time.now
296
317
  ]
297
318
  end
298
319
 
299
- def mysql_gaplock_protection?
300
- 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)
301
332
  end
302
333
 
303
334
  def db_true
304
- value = ::ActiveRecord::Base.connection.type_cast(
305
- true,
306
- transition_class.columns_hash["most_recent"],
307
- )
308
- ::ActiveRecord::Base.connection.quote(value)
335
+ transition_class.connection.quote(type_cast(true))
309
336
  end
310
337
 
311
338
  def db_false
312
- value = ::ActiveRecord::Base.connection.type_cast(
313
- false,
314
- transition_class.columns_hash["most_recent"],
315
- )
316
- ::ActiveRecord::Base.connection.quote(value)
339
+ transition_class.connection.quote(type_cast(false))
317
340
  end
318
341
 
319
342
  def db_null
320
343
  Arel::Nodes::SqlLiteral.new("NULL")
321
344
  end
322
345
 
346
+ # Type casting against a column is deprecated and will be removed in Rails 6.2.
347
+ # See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
348
+ def type_cast(value)
349
+ transition_class.connection.type_cast(value)
350
+ end
351
+
323
352
  # Check whether the `most_recent` column allows null values. If it doesn't, set old
324
353
  # records to `false`, otherwise, set them to `NULL`.
325
354
  #
@@ -337,9 +366,9 @@ module Statesman
337
366
  end
338
367
 
339
368
  class ActiveRecordAfterCommitWrap
340
- def initialize(&block)
369
+ def initialize(connection, &block)
341
370
  @callback = block
342
- @connection = ::ActiveRecord::Base.connection
371
+ @connection = connection
343
372
  end
344
373
 
345
374
  def self.trigger_transactional_callbacks?
@@ -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}.id = " \
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
@@ -127,6 +135,10 @@ module Statesman
127
135
  "and #{transition_class}."
128
136
  end
129
137
 
138
+ def model_primary_key
139
+ transition_reflection.active_record_primary_key
140
+ end
141
+
130
142
  def model_foreign_key
131
143
  transition_reflection.foreign_key
132
144
  end
@@ -141,7 +153,7 @@ module Statesman
141
153
  end
142
154
 
143
155
  def db_true
144
- ::ActiveRecord::Base.connection.quote(true)
156
+ model.connection.quote(true)
145
157
  end
146
158
  end
147
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
@@ -37,10 +37,14 @@ module Statesman
37
37
  @history
38
38
  end
39
39
 
40
+ def reset
41
+ @history = []
42
+ end
43
+
40
44
  private
41
45
 
42
46
  def next_sort_key
43
- (last && last.sort_key + 10) || 10
47
+ (last && (last.sort_key + 10)) || 10
44
48
  end
45
49
  end
46
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)
@@ -2,9 +2,13 @@
2
2
 
3
3
  module Statesman
4
4
  class InvalidStateError < StandardError; end
5
+
5
6
  class InvalidTransitionError < StandardError; end
7
+
6
8
  class InvalidCallbackError < StandardError; end
9
+
7
10
  class TransitionConflictError < StandardError; end
11
+
8
12
  class MissingTransitionAssociation < StandardError; end
9
13
 
10
14
  class TransitionFailedError < StandardError
@@ -24,13 +28,15 @@ module Statesman
24
28
  end
25
29
 
26
30
  class GuardFailedError < StandardError
27
- def initialize(from, to)
31
+ def initialize(from, to, callback)
28
32
  @from = from
29
33
  @to = to
34
+ @callback = callback
30
35
  super(_message)
36
+ set_backtrace(callback.source_location.join(":")) if callback&.source_location
31
37
  end
32
38
 
33
- attr_reader :from, :to
39
+ attr_reader :from, :to, :callback
34
40
 
35
41
  private
36
42
 
@@ -48,8 +54,8 @@ module Statesman
48
54
 
49
55
  def _message(transition_class_name)
50
56
  "#{transition_class_name}#metadata is not serialized. If you " \
51
- "are using a non-json column type, you should `include " \
52
- "Statesman::Adapters::ActiveRecordTransition`"
57
+ "are using a non-json column type, you should `include " \
58
+ "Statesman::Adapters::ActiveRecordTransition`"
53
59
  end
54
60
  end
55
61
 
@@ -62,9 +68,9 @@ module Statesman
62
68
 
63
69
  def _message(transition_class_name)
64
70
  "#{transition_class_name}#metadata column type cannot be json " \
65
- "and serialized simultaneously. If you are using a json " \
66
- "column type, it is not necessary to `include " \
67
- "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`"
68
74
  end
69
75
  end
70
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(*args)
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
 
@@ -209,6 +269,10 @@ module Statesman
209
269
  @storage_adapter.last(force_reload: force_reload)
210
270
  end
211
271
 
272
+ def last_transition_to(state)
273
+ history.reverse.find { |transition| transition.to_state.to_sym == state.to_sym }
274
+ end
275
+
212
276
  def can_transition_to?(new_state, metadata = {})
213
277
  validate_transition(from: current_state,
214
278
  to: new_state,
@@ -257,6 +321,10 @@ module Statesman
257
321
  false
258
322
  end
259
323
 
324
+ def reset
325
+ @storage_adapter.reset
326
+ end
327
+
260
328
  private
261
329
 
262
330
  def adapter_class(transition_class)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "7.4.0"
4
+ VERSION = "12.1.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