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.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer.json +31 -0
- data/.devcontainer/docker-compose.yml +39 -0
- data/.github/workflows/tests.yml +130 -0
- data/.gitignore +65 -15
- data/.rspec +2 -0
- data/.rubocop.yml +11 -1
- data/.rubocop_todo.yml +23 -38
- data/.ruby-version +1 -1
- data/CHANGELOG.md +229 -43
- data/CONTRIBUTING.md +14 -13
- data/Gemfile +18 -3
- data/README.md +203 -74
- data/docs/COMPATIBILITY.md +3 -3
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +2 -2
- data/lib/statesman/adapters/active_record.rb +69 -52
- data/lib/statesman/adapters/active_record_queries.rb +15 -7
- data/lib/statesman/adapters/active_record_transition.rb +5 -1
- data/lib/statesman/adapters/memory.rb +1 -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 +9 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +60 -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 +8 -10
- data/spec/statesman/adapters/active_record_spec.rb +144 -55
- 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 +6 -7
- 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 +8 -4
- data/spec/statesman/guard_spec.rb +0 -2
- data/spec/statesman/machine_spec.rb +231 -19
- data/spec/statesman/utils_spec.rb +0 -2
- data/spec/support/active_record.rb +156 -29
- data/spec/support/exactly_query_databases.rb +35 -0
- data/statesman.gemspec +2 -17
- metadata +14 -238
- 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
|
|
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
|
|
@@ -52,7 +48,7 @@ module Statesman
|
|
|
52
48
|
|
|
53
49
|
raise
|
|
54
50
|
ensure
|
|
55
|
-
|
|
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
|
|
70
|
+
@last_transition = history.last
|
|
74
71
|
end
|
|
75
72
|
end
|
|
76
|
-
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
77
73
|
|
|
78
74
|
def reset
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
if mysql_gaplock_protection?(transition_class.connection)
|
|
152
|
+
update.order(transition_table[:most_recent].desc)
|
|
153
|
+
end
|
|
156
154
|
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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,
|
|
316
|
+
column, default_timezone == :utc ? Time.now.utc : Time.now
|
|
308
317
|
]
|
|
309
318
|
end
|
|
310
319
|
|
|
311
|
-
def
|
|
312
|
-
|
|
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
|
-
|
|
335
|
+
transition_class.connection.quote(type_cast(true))
|
|
317
336
|
end
|
|
318
337
|
|
|
319
338
|
def db_false
|
|
320
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
@@ -145,7 +153,7 @@ module Statesman
|
|
|
145
153
|
end
|
|
146
154
|
|
|
147
155
|
def db_true
|
|
148
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
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
|
|
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
|
data/lib/tasks/statesman.rake
CHANGED
|
@@ -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
|
-
|
|
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
|