statesman 3.5.0 → 7.4.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 +5 -5
- data/.circleci/config.yml +49 -250
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +26 -6
- data/CHANGELOG.md +106 -0
- data/Gemfile +10 -4
- data/Guardfile +2 -0
- data/README.md +78 -48
- data/Rakefile +2 -4
- data/lib/generators/statesman/active_record_transition_generator.rb +2 -0
- data/lib/generators/statesman/generator_helpers.rb +2 -0
- data/lib/generators/statesman/migration_generator.rb +2 -0
- data/lib/statesman.rb +14 -4
- data/lib/statesman/adapters/active_record.rb +259 -37
- data/lib/statesman/adapters/active_record_queries.rb +100 -36
- data/lib/statesman/adapters/active_record_transition.rb +2 -0
- data/lib/statesman/adapters/memory.rb +2 -0
- data/lib/statesman/adapters/memory_transition.rb +2 -0
- data/lib/statesman/callback.rb +2 -0
- data/lib/statesman/config.rb +28 -0
- data/lib/statesman/exceptions.rb +34 -2
- data/lib/statesman/guard.rb +3 -4
- data/lib/statesman/machine.rb +29 -7
- data/lib/statesman/railtie.rb +2 -0
- data/lib/statesman/utils.rb +2 -0
- data/lib/statesman/version.rb +3 -1
- data/lib/tasks/statesman.rake +3 -1
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb +2 -0
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb +2 -0
- data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +2 -0
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +2 -0
- data/spec/generators/statesman/migration_generator_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -30
- data/spec/statesman/adapters/active_record_queries_spec.rb +167 -91
- data/spec/statesman/adapters/active_record_spec.rb +15 -1
- data/spec/statesman/adapters/active_record_transition_spec.rb +2 -0
- data/spec/statesman/adapters/memory_spec.rb +2 -0
- data/spec/statesman/adapters/memory_transition_spec.rb +2 -0
- data/spec/statesman/adapters/shared_examples.rb +2 -0
- data/spec/statesman/callback_spec.rb +2 -0
- data/spec/statesman/config_spec.rb +2 -0
- data/spec/statesman/exceptions_spec.rb +88 -0
- data/spec/statesman/guard_spec.rb +2 -0
- data/spec/statesman/machine_spec.rb +79 -4
- data/spec/statesman/utils_spec.rb +2 -0
- data/spec/support/active_record.rb +9 -12
- data/spec/support/generators_shared_examples.rb +2 -0
- data/statesman.gemspec +19 -7
- metadata +40 -32
- data/lib/generators/statesman/mongoid_transition_generator.rb +0 -25
- data/lib/generators/statesman/templates/mongoid_transition_model.rb.erb +0 -14
- data/lib/statesman/adapters/mongoid.rb +0 -66
- data/lib/statesman/adapters/mongoid_transition.rb +0 -10
- data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -23
- data/spec/statesman/adapters/mongoid_spec.rb +0 -86
- data/spec/support/mongoid.rb +0 -28
@@ -1,51 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Statesman
|
2
4
|
module Adapters
|
3
5
|
module ActiveRecordQueries
|
6
|
+
def self.check_missing_methods!(base)
|
7
|
+
missing_methods = %i[transition_class initial_state].
|
8
|
+
reject { |m| base.respond_to?(m) }
|
9
|
+
return if missing_methods.none?
|
10
|
+
|
11
|
+
raise NotImplementedError,
|
12
|
+
"#{missing_methods.join(', ')} method(s) should be defined on " \
|
13
|
+
"the model. Alternatively, use the new form of `include " \
|
14
|
+
"Statesman::Adapters::ActiveRecordQueries[" \
|
15
|
+
"transition_class: MyTransition, " \
|
16
|
+
"initial_state: :some_state]`"
|
17
|
+
end
|
18
|
+
|
4
19
|
def self.included(base)
|
5
|
-
base
|
20
|
+
check_missing_methods!(base)
|
21
|
+
|
22
|
+
base.include(
|
23
|
+
ClassMethods.new(
|
24
|
+
transition_class: base.transition_class,
|
25
|
+
initial_state: base.initial_state,
|
26
|
+
most_recent_transition_alias: base.try(:most_recent_transition_alias),
|
27
|
+
transition_name: base.try(:transition_name),
|
28
|
+
),
|
29
|
+
)
|
6
30
|
end
|
7
31
|
|
8
|
-
|
9
|
-
|
10
|
-
|
32
|
+
def self.[](**args)
|
33
|
+
ClassMethods.new(**args)
|
34
|
+
end
|
11
35
|
|
12
|
-
|
13
|
-
|
36
|
+
class ClassMethods < Module
|
37
|
+
def initialize(**args)
|
38
|
+
@args = args
|
14
39
|
end
|
15
40
|
|
16
|
-
def
|
17
|
-
|
41
|
+
def included(base)
|
42
|
+
ensure_inheritance(base)
|
18
43
|
|
19
|
-
|
20
|
-
where("NOT (#{states_where(most_recent_transition_alias, states)})",
|
21
|
-
states)
|
22
|
-
end
|
44
|
+
query_builder = QueryBuilder.new(base, **@args)
|
23
45
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
46
|
+
base.define_singleton_method(:most_recent_transition_join) do
|
47
|
+
query_builder.most_recent_transition_join
|
48
|
+
end
|
49
|
+
|
50
|
+
define_in_state(base, query_builder)
|
51
|
+
define_not_in_state(base, query_builder)
|
29
52
|
end
|
30
53
|
|
31
54
|
private
|
32
55
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
56
|
+
def ensure_inheritance(base)
|
57
|
+
klass = self
|
58
|
+
existing_inherited = base.method(:inherited)
|
59
|
+
base.define_singleton_method(:inherited) do |subclass|
|
60
|
+
existing_inherited.call(subclass)
|
61
|
+
subclass.send(:include, klass)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def define_in_state(base, query_builder)
|
66
|
+
base.define_singleton_method(:in_state) do |*states|
|
67
|
+
states = states.flatten
|
68
|
+
|
69
|
+
joins(most_recent_transition_join).
|
70
|
+
where(query_builder.states_where(states), states)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_not_in_state(base, query_builder)
|
75
|
+
base.define_singleton_method(:not_in_state) do |*states|
|
76
|
+
states = states.flatten
|
77
|
+
|
78
|
+
joins(most_recent_transition_join).
|
79
|
+
where("NOT (#{query_builder.states_where(states)})", states)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class QueryBuilder
|
85
|
+
def initialize(model, transition_class:, initial_state:,
|
86
|
+
most_recent_transition_alias: nil,
|
87
|
+
transition_name: nil)
|
88
|
+
@model = model
|
89
|
+
@transition_class = transition_class
|
90
|
+
@initial_state = initial_state
|
91
|
+
@most_recent_transition_alias = most_recent_transition_alias
|
92
|
+
@transition_name = transition_name
|
36
93
|
end
|
37
94
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
95
|
+
def states_where(states)
|
96
|
+
if initial_state.to_s.in?(states.map(&:to_s))
|
97
|
+
"#{most_recent_transition_alias}.to_state IN (?) OR " \
|
98
|
+
"#{most_recent_transition_alias}.to_state IS NULL"
|
99
|
+
else
|
100
|
+
"#{most_recent_transition_alias}.to_state IN (?) AND " \
|
101
|
+
"#{most_recent_transition_alias}.to_state IS NOT NULL"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def most_recent_transition_join
|
106
|
+
"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}"
|
41
110
|
end
|
42
111
|
|
112
|
+
private
|
113
|
+
|
114
|
+
attr_reader :model, :transition_class, :initial_state
|
115
|
+
|
43
116
|
def transition_name
|
44
|
-
transition_class.table_name.to_sym
|
117
|
+
@transition_name || transition_class.table_name.to_sym
|
45
118
|
end
|
46
119
|
|
47
120
|
def transition_reflection
|
48
|
-
reflect_on_all_associations(:has_many).each do |value|
|
121
|
+
model.reflect_on_all_associations(:has_many).each do |value|
|
49
122
|
return value if value.klass == transition_class
|
50
123
|
end
|
51
124
|
|
@@ -62,18 +135,9 @@ module Statesman
|
|
62
135
|
transition_reflection.table_name
|
63
136
|
end
|
64
137
|
|
65
|
-
def states_where(temporary_table_name, states)
|
66
|
-
if initial_state.to_s.in?(states.map(&:to_s))
|
67
|
-
"#{temporary_table_name}.to_state IN (?) OR " \
|
68
|
-
"#{temporary_table_name}.to_state IS NULL"
|
69
|
-
else
|
70
|
-
"#{temporary_table_name}.to_state IN (?) AND " \
|
71
|
-
"#{temporary_table_name}.to_state IS NOT NULL"
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
138
|
def most_recent_transition_alias
|
76
|
-
|
139
|
+
@most_recent_transition_alias ||
|
140
|
+
"most_recent_#{transition_name.to_s.singularize}"
|
77
141
|
end
|
78
142
|
|
79
143
|
def db_true
|
data/lib/statesman/callback.rb
CHANGED
data/lib/statesman/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "json"
|
2
4
|
require_relative "exceptions"
|
3
5
|
|
@@ -12,5 +14,31 @@ module Statesman
|
|
12
14
|
def storage_adapter(adapter_class)
|
13
15
|
@adapter_class = adapter_class
|
14
16
|
end
|
17
|
+
|
18
|
+
def mysql_gaplock_protection?
|
19
|
+
return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
|
20
|
+
|
21
|
+
# If our adapter class suggests we're using mysql, enable gaplock protection by
|
22
|
+
# 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
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def mysql_adapter?(adapter_class)
|
34
|
+
adapter_name = adapter_name(adapter_class)
|
35
|
+
return false unless adapter_name
|
36
|
+
|
37
|
+
adapter_name.start_with?("mysql")
|
38
|
+
end
|
39
|
+
|
40
|
+
def adapter_name(adapter_class)
|
41
|
+
adapter_class.respond_to?(:adapter_name) && adapter_class&.adapter_name
|
42
|
+
end
|
15
43
|
end
|
16
44
|
end
|
data/lib/statesman/exceptions.rb
CHANGED
@@ -1,12 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Statesman
|
2
4
|
class InvalidStateError < StandardError; end
|
3
5
|
class InvalidTransitionError < StandardError; end
|
4
6
|
class InvalidCallbackError < StandardError; end
|
5
|
-
class GuardFailedError < StandardError; end
|
6
|
-
class TransitionFailedError < StandardError; end
|
7
7
|
class TransitionConflictError < StandardError; end
|
8
8
|
class MissingTransitionAssociation < StandardError; end
|
9
9
|
|
10
|
+
class TransitionFailedError < StandardError
|
11
|
+
def initialize(from, to)
|
12
|
+
@from = from
|
13
|
+
@to = to
|
14
|
+
super(_message)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :from, :to
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def _message
|
22
|
+
"Cannot transition from '#{from}' to '#{to}'"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class GuardFailedError < StandardError
|
27
|
+
def initialize(from, to)
|
28
|
+
@from = from
|
29
|
+
@to = to
|
30
|
+
super(_message)
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :from, :to
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def _message
|
38
|
+
"Guard on transition from: '#{from}' to '#{to}' returned false"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
10
42
|
class UnserializedMetadataError < StandardError
|
11
43
|
def initialize(transition_class_name)
|
12
44
|
super(_message(transition_class_name))
|
data/lib/statesman/guard.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "callback"
|
2
4
|
require_relative "exceptions"
|
3
5
|
|
4
6
|
module Statesman
|
5
7
|
class Guard < Callback
|
6
8
|
def call(*args)
|
7
|
-
unless super(*args)
|
8
|
-
raise GuardFailedError,
|
9
|
-
"Guard on transition from: '#{from}' to '#{to}' returned false"
|
10
|
-
end
|
9
|
+
raise GuardFailedError.new(from, to) unless super(*args)
|
11
10
|
end
|
12
11
|
end
|
13
12
|
end
|
data/lib/statesman/machine.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "version"
|
2
4
|
require_relative "exceptions"
|
3
5
|
require_relative "guard"
|
@@ -46,10 +48,12 @@ module Statesman
|
|
46
48
|
|
47
49
|
def callbacks
|
48
50
|
@callbacks ||= {
|
49
|
-
before:
|
50
|
-
after:
|
51
|
+
before: [],
|
52
|
+
after: [],
|
53
|
+
after_transition_failure: [],
|
54
|
+
after_guard_failure: [],
|
51
55
|
after_commit: [],
|
52
|
-
guards:
|
56
|
+
guards: [],
|
53
57
|
}
|
54
58
|
end
|
55
59
|
|
@@ -83,6 +87,16 @@ module Statesman
|
|
83
87
|
from: options[:from], to: options[:to], &block)
|
84
88
|
end
|
85
89
|
|
90
|
+
def after_transition_failure(options = {}, &block)
|
91
|
+
add_callback(callback_type: :after_transition_failure, callback_class: Callback,
|
92
|
+
from: options[:from], to: options[:to], &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def after_guard_failure(options = {}, &block)
|
96
|
+
add_callback(callback_type: :after_guard_failure, callback_class: Callback,
|
97
|
+
from: options[:from], to: options[:to], &block)
|
98
|
+
end
|
99
|
+
|
86
100
|
def validate_callback_condition(options = { from: nil, to: nil })
|
87
101
|
from = to_s_or_nil(options[:from])
|
88
102
|
to = array_to_s_or_nil(options[:to])
|
@@ -219,6 +233,17 @@ module Statesman
|
|
219
233
|
@storage_adapter.create(initial_state, new_state, metadata)
|
220
234
|
|
221
235
|
true
|
236
|
+
rescue TransitionFailedError => e
|
237
|
+
execute_on_failure(:after_transition_failure, initial_state, new_state, e)
|
238
|
+
raise
|
239
|
+
rescue GuardFailedError => e
|
240
|
+
execute_on_failure(:after_guard_failure, initial_state, new_state, e)
|
241
|
+
raise
|
242
|
+
end
|
243
|
+
|
244
|
+
def execute_on_failure(phase, initial_state, new_state, exception)
|
245
|
+
callbacks = callbacks_for(phase, from: initial_state, to: new_state)
|
246
|
+
callbacks.each { |cb| cb.call(@object, exception) }
|
222
247
|
end
|
223
248
|
|
224
249
|
def execute(phase, initial_state, new_state, transition)
|
@@ -265,10 +290,7 @@ module Statesman
|
|
265
290
|
to = to_s_or_nil(options[:to])
|
266
291
|
|
267
292
|
successors = self.class.successors[from] || []
|
268
|
-
unless successors.include?(to)
|
269
|
-
raise TransitionFailedError,
|
270
|
-
"Cannot transition from '#{from}' to '#{to}'"
|
271
|
-
end
|
293
|
+
raise TransitionFailedError.new(from, to) unless successors.include?(to)
|
272
294
|
|
273
295
|
# Call all guards, they raise exceptions if they fail
|
274
296
|
guards_for(from: from, to: to).each do |guard|
|
data/lib/statesman/railtie.rb
CHANGED
data/lib/statesman/utils.rb
CHANGED
data/lib/statesman/version.rb
CHANGED
data/lib/tasks/statesman.rake
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :statesman do
|
2
4
|
desc "Set most_recent to false for old transitions and to true for the "\
|
3
5
|
"latest one. Safe to re-run"
|
@@ -19,7 +21,7 @@ namespace :statesman do
|
|
19
21
|
batch_size = 500
|
20
22
|
|
21
23
|
parent_class.find_in_batches(batch_size: batch_size) do |models|
|
22
|
-
ActiveRecord::Base.transaction do
|
24
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
23
25
|
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
|
24
26
|
# Set all transitions' most_recent to FALSE
|
25
27
|
transition_class.where(parent_fk => models.map(&:id)).
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "statesman"
|
2
4
|
require "sqlite3"
|
3
5
|
require "mysql2"
|
@@ -20,36 +22,7 @@ RSpec.configure do |config|
|
|
20
22
|
config.order = "random"
|
21
23
|
|
22
24
|
def connection_failure
|
23
|
-
if defined?(Moped)
|
24
|
-
Moped::Errors::ConnectionFailure
|
25
|
-
else
|
26
|
-
Mongo::Error::NoServerAvailable
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
if config.exclusion_filter[:mongo]
|
31
|
-
puts "Skipping Mongo tests"
|
32
|
-
else
|
33
|
-
require "mongoid"
|
34
|
-
|
35
|
-
# Try a mongo connection at the start of the suite and raise if it fails
|
36
|
-
begin
|
37
|
-
Mongoid.configure do |mongo_config|
|
38
|
-
if defined?(Moped)
|
39
|
-
mongo_config.connect_to("statesman_test")
|
40
|
-
mongo_config.sessions["default"]["options"]["max_retries"] = 2
|
41
|
-
else
|
42
|
-
mongo_config.connect_to("statesman_test", server_selection_timeout: 2)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
# Attempting a mongo operation will trigger 2 retries then throw an
|
46
|
-
# exception if mongo is not running.
|
47
|
-
Mongoid.purge!
|
48
|
-
rescue connection_failure => error
|
49
|
-
puts "The spec suite requires MongoDB to be installed and running locally"
|
50
|
-
puts "Mongo dependent specs can be filtered with rspec --tag '~mongo'"
|
51
|
-
raise(error)
|
52
|
-
end
|
25
|
+
Moped::Errors::ConnectionFailure if defined?(Moped)
|
53
26
|
end
|
54
27
|
|
55
28
|
if config.exclusion_filter[:active_record]
|