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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/tests.yml +112 -0
- data/.gitignore +65 -15
- data/.rspec +1 -0
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +37 -28
- data/.ruby-version +1 -0
- data/CHANGELOG.md +262 -41
- data/CONTRIBUTING.md +23 -4
- data/Gemfile +4 -6
- data/README.md +243 -43
- data/docs/COMPATIBILITY.md +2 -2
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +12 -4
- data/lib/statesman/adapters/active_record.rb +84 -55
- data/lib/statesman/adapters/active_record_queries.rb +19 -7
- data/lib/statesman/adapters/active_record_transition.rb +5 -1
- data/lib/statesman/adapters/memory.rb +5 -1
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
- data/lib/statesman/callback.rb +2 -2
- data/lib/statesman/config.rb +3 -10
- data/lib/statesman/exceptions.rb +13 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +68 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +5 -5
- data/lib/tasks/statesman.rake +5 -5
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
- data/spec/generators/statesman/migration_generator_spec.rb +5 -1
- data/spec/spec_helper.rb +44 -7
- data/spec/statesman/adapters/active_record_queries_spec.rb +34 -12
- data/spec/statesman/adapters/active_record_spec.rb +176 -51
- data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
- data/spec/statesman/adapters/memory_spec.rb +0 -1
- data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
- data/spec/statesman/adapters/shared_examples.rb +3 -4
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +206 -0
- data/spec/statesman/callback_spec.rb +0 -2
- data/spec/statesman/config_spec.rb +0 -2
- data/spec/statesman/exceptions_spec.rb +17 -4
- data/spec/statesman/guard_spec.rb +0 -2
- data/spec/statesman/machine_spec.rb +252 -15
- data/spec/statesman/utils_spec.rb +0 -2
- data/spec/support/active_record.rb +156 -24
- data/spec/support/exactly_query_databases.rb +35 -0
- data/statesman.gemspec +9 -10
- metadata +32 -59
- 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
|
13
|
-
|
12
|
+
if model.connection.respond_to?(:supports_partial_index?)
|
13
|
+
model.connection.supports_partial_index?
|
14
14
|
else
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
151
|
+
if mysql_gaplock_protection?(transition_class.connection)
|
152
|
+
update.order(transition_table[:most_recent].desc)
|
153
|
+
end
|
144
154
|
|
145
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
227
|
-
|
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
|
-
|
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
|
256
|
-
|
257
|
-
|
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
|
-
|
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,
|
316
|
+
column, default_timezone == :utc ? Time.now.utc : Time.now
|
296
317
|
]
|
297
318
|
end
|
298
319
|
|
299
|
-
def
|
300
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
106
|
+
"#{most_recent_transition_alias}.to_state IS NULL"
|
99
107
|
else
|
100
108
|
"#{most_recent_transition_alias}.to_state IN (?) AND " \
|
101
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
data/lib/statesman/callback.rb
CHANGED
@@ -40,11 +40,11 @@ module Statesman
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def matches_from_state(from, to)
|
43
|
-
|
43
|
+
from == self.from && (to.nil? || self.to.empty?)
|
44
44
|
end
|
45
45
|
|
46
46
|
def matches_to_state(from, to)
|
47
|
-
(
|
47
|
+
(from.nil? || self.from.nil?) && self.to.include?(to)
|
48
48
|
end
|
49
49
|
|
50
50
|
def matches_both_states(from, to)
|
data/lib/statesman/config.rb
CHANGED
@@ -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
|
-
|
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)
|
data/lib/statesman/exceptions.rb
CHANGED
@@ -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
|
-
|
52
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
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
|
data/lib/statesman/guard.rb
CHANGED
data/lib/statesman/machine.rb
CHANGED
@@ -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)
|
data/lib/statesman/version.rb
CHANGED
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 :
|
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
|
-
|
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
|