statesman 1.1.0 → 1.2.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/.rubocop.yml +7 -1
- data/.travis.yml +19 -4
- data/CHANGELOG.md +30 -0
- data/README.md +11 -35
- data/lib/generators/statesman/add_constraints_to_most_recent_generator.rb +28 -0
- data/lib/generators/statesman/add_most_recent_generator.rb +25 -0
- data/lib/generators/statesman/generator_helpers.rb +6 -2
- data/lib/generators/statesman/templates/add_constraints_to_most_recent_migration.rb.erb +13 -0
- data/lib/generators/statesman/templates/add_most_recent_migration.rb.erb +9 -0
- data/lib/generators/statesman/templates/create_migration.rb.erb +4 -3
- data/lib/generators/statesman/templates/update_migration.rb.erb +5 -4
- data/lib/statesman.rb +1 -0
- data/lib/statesman/adapters/active_record.rb +23 -8
- data/lib/statesman/adapters/active_record_queries.rb +62 -15
- data/lib/statesman/railtie.rb +9 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/tasks/statesman.rake +49 -0
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions.rb +13 -0
- data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +9 -0
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +0 -2
- data/spec/generators/statesman/add_constraints_to_most_recent_generator_spec.rb +38 -0
- data/spec/generators/statesman/add_most_recent_generator_spec.rb +35 -0
- data/spec/generators/statesman/migration_generator_spec.rb +10 -1
- data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -2
- data/spec/spec_helper.rb +22 -7
- data/spec/statesman/adapters/active_record_queries_spec.rb +110 -28
- data/spec/statesman/adapters/active_record_spec.rb +61 -31
- data/spec/statesman/adapters/mongoid_spec.rb +8 -17
- data/spec/statesman/adapters/shared_examples.rb +10 -17
- data/spec/statesman/callback_spec.rb +2 -6
- data/spec/statesman/config_spec.rb +2 -5
- data/spec/statesman/guard_spec.rb +3 -9
- data/spec/statesman/machine_spec.rb +91 -129
- data/spec/support/active_record.rb +35 -4
- data/spec/support/generators_shared_examples.rb +1 -4
- data/statesman.gemspec +5 -3
- metadata +52 -10
data/lib/statesman/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
namespace :statesman do
|
2
|
+
desc "Set most_recent to false for old transitions and to true for the "\
|
3
|
+
"latest one. Safe to re-run"
|
4
|
+
task :backfill_most_recent, [:parent_model_name] => :environment do |_, args|
|
5
|
+
parent_model_name = args.parent_model_name
|
6
|
+
abort("Parent model name must be specified") unless parent_model_name
|
7
|
+
|
8
|
+
parent_class = parent_model_name.constantize
|
9
|
+
transition_class = parent_class.transition_class
|
10
|
+
parent_fk = "#{parent_model_name.demodulize.underscore}_id"
|
11
|
+
|
12
|
+
total_models = parent_class.count
|
13
|
+
done_models = 0
|
14
|
+
batch_size = 500
|
15
|
+
|
16
|
+
parent_class.find_in_batches(batch_size: batch_size) do |models|
|
17
|
+
ActiveRecord::Base.transaction do
|
18
|
+
# Set all transitions' most_recent to FALSE
|
19
|
+
transition_class.where(parent_fk => models.map(&:id)).
|
20
|
+
update_all(most_recent: false)
|
21
|
+
|
22
|
+
# Set current transition's most_recent to TRUE
|
23
|
+
ActiveRecord::Base.connection.execute %{
|
24
|
+
UPDATE #{transition_class.table_name}
|
25
|
+
SET most_recent = true
|
26
|
+
FROM
|
27
|
+
(
|
28
|
+
SELECT initial_t.id, subsequent_t.created_at
|
29
|
+
FROM #{transition_class.table_name} initial_t
|
30
|
+
LEFT JOIN #{transition_class.table_name} subsequent_t ON
|
31
|
+
(
|
32
|
+
initial_t.#{parent_fk} = subsequent_t.#{parent_fk}
|
33
|
+
AND initial_t.sort_key < subsequent_t.sort_key
|
34
|
+
)
|
35
|
+
WHERE initial_t.#{parent_fk}
|
36
|
+
IN (#{models.map { |p| "'#{p.id}'" }.join(',')})
|
37
|
+
AND subsequent_t.id IS NULL
|
38
|
+
) x
|
39
|
+
WHERE #{transition_class.table_name}.id = x.id
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
done_models += batch_size
|
44
|
+
puts "Updated #{transition_class.name.pluralize} for "\
|
45
|
+
"#{[done_models, total_models].min}/#{total_models} "\
|
46
|
+
"#{parent_model_name.pluralize}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddConstraintsToMostRecentForBaconTransitions < ActiveRecord::Migration
|
2
|
+
disable_ddl_transaction!
|
3
|
+
|
4
|
+
def up
|
5
|
+
add_index :bacon_transitions, [:bacon_id, :most_recent], unique: true, where: "most_recent", name: "index_bacon_transitions_parent_most_recent", algorithm: :concurrently
|
6
|
+
change_column_null :bacon_transitions, :most_recent, false
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
remove_index :bacon_transitions, name: "index_bacon_transitions_parent_most_recent"
|
11
|
+
change_column_null :bacon_transitions, :most_recent, true
|
12
|
+
end
|
13
|
+
end
|
@@ -3,7 +3,6 @@ require "support/generators_shared_examples"
|
|
3
3
|
require "generators/statesman/active_record_transition_generator"
|
4
4
|
|
5
5
|
describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
|
6
|
-
|
7
6
|
it_behaves_like "a generator" do
|
8
7
|
let(:migration_name) { 'db/migrate/create_bacon_transitions.rb' }
|
9
8
|
end
|
@@ -24,5 +23,4 @@ describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
|
|
24
23
|
it { is_expected.not_to contain(/class_name:/) }
|
25
24
|
it { is_expected.to contain(/class BaconTransition/) }
|
26
25
|
end
|
27
|
-
|
28
26
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "support/generators_shared_examples"
|
3
|
+
require "generators/statesman/add_constraints_to_most_recent_generator"
|
4
|
+
|
5
|
+
describe Statesman::AddConstraintsToMostRecentGenerator, type: :generator do
|
6
|
+
it_behaves_like "a generator" do
|
7
|
+
let(:migration_name) do
|
8
|
+
'db/migrate/add_constraints_to_most_recent_for_bacon_transitions.rb'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "the migration contains the correct words" do
|
13
|
+
let(:migration_number) { '5678309' }
|
14
|
+
|
15
|
+
let(:mock_time) do
|
16
|
+
double('Time', utc: double('UTCTime', strftime: migration_number))
|
17
|
+
end
|
18
|
+
|
19
|
+
subject(:migration_file) do
|
20
|
+
file("db/migrate/#{migration_number}_"\
|
21
|
+
"add_constraints_to_most_recent_for_bacon_transitions.rb")
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:fixture_file) do
|
25
|
+
File.read("spec/fixtures/add_constraints_to_most_recent_for_"\
|
26
|
+
"bacon_transitions.rb")
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
allow(Time).to receive(:now).and_return(mock_time)
|
31
|
+
run_generator %w(Bacon BaconTransition)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "matches the fixture" do
|
35
|
+
expect(migration_file).to contain(fixture_file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "support/generators_shared_examples"
|
3
|
+
require "generators/statesman/add_most_recent_generator"
|
4
|
+
|
5
|
+
describe Statesman::AddMostRecentGenerator, type: :generator do
|
6
|
+
it_behaves_like "a generator" do
|
7
|
+
let(:migration_name) do
|
8
|
+
'db/migrate/add_most_recent_to_bacon_transitions.rb'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "the migration" do
|
13
|
+
let(:migration_number) { '5678309' }
|
14
|
+
|
15
|
+
let(:mock_time) do
|
16
|
+
double('Time', utc: double('UTCTime', strftime: migration_number))
|
17
|
+
end
|
18
|
+
|
19
|
+
subject(:migration_file) do
|
20
|
+
file("db/migrate/#{migration_number}_"\
|
21
|
+
"add_most_recent_to_bacon_transitions.rb")
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:fixture_file) do
|
25
|
+
File.read("spec/fixtures/add_most_recent_to_bacon_transitions.rb")
|
26
|
+
end
|
27
|
+
|
28
|
+
before { allow(Time).to receive(:now).and_return(mock_time) }
|
29
|
+
before { run_generator %w(Bacon BaconTransition) }
|
30
|
+
|
31
|
+
it "matches the fixture" do
|
32
|
+
expect(migration_file).to contain(fixture_file)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -3,7 +3,6 @@ require "support/generators_shared_examples"
|
|
3
3
|
require "generators/statesman/migration_generator"
|
4
4
|
|
5
5
|
describe Statesman::MigrationGenerator, type: :generator do
|
6
|
-
|
7
6
|
it_behaves_like "a generator" do
|
8
7
|
let(:migration_name) { 'db/migrate/add_statesman_to_bacon_transitions.rb' }
|
9
8
|
end
|
@@ -29,5 +28,15 @@ describe Statesman::MigrationGenerator, type: :generator do
|
|
29
28
|
it { is_expected.to contain(/:bacon_transition/) }
|
30
29
|
it { is_expected.not_to contain(/:yummy\/bacon/) }
|
31
30
|
it { is_expected.to contain(/null: false/) }
|
31
|
+
|
32
|
+
it "names the sorting index appropriately" do
|
33
|
+
expect(subject).
|
34
|
+
to contain("name: \"index_bacon_transitions_parent_sort\"")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "names the most_recent index appropriately" do
|
38
|
+
expect(subject).
|
39
|
+
to contain("name: \"index_bacon_transitions_parent_most_recent\"")
|
40
|
+
end
|
32
41
|
end
|
33
42
|
end
|
@@ -3,7 +3,6 @@ require "support/generators_shared_examples"
|
|
3
3
|
require "generators/statesman/mongoid_transition_generator"
|
4
4
|
|
5
5
|
describe Statesman::MongoidTransitionGenerator, type: :generator do
|
6
|
-
|
7
6
|
describe 'the model contains the correct words' do
|
8
7
|
before { run_generator %w(Yummy::Bacon Yummy::BaconTransition) }
|
9
8
|
subject { file('app/models/yummy/bacon_transition.rb') }
|
@@ -19,5 +18,4 @@ describe Statesman::MongoidTransitionGenerator, type: :generator do
|
|
19
18
|
it { is_expected.not_to contain(/class_name:/) }
|
20
19
|
it { is_expected.not_to contain(/CreateYummy::Bacon/) }
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -26,12 +26,21 @@ RSpec.configure do |config|
|
|
26
26
|
raise(error)
|
27
27
|
end
|
28
28
|
|
29
|
-
config.
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
if config.exclusion_filter[:active_record]
|
30
|
+
puts "Skipping ActiveRecord tests"
|
31
|
+
else
|
32
|
+
# Connect to the database for activerecord tests
|
33
|
+
db_conn_spec = ENV["DATABASE_URL"]
|
34
|
+
db_conn_spec ||= { adapter: "sqlite3", database: ":memory:" }
|
35
|
+
ActiveRecord::Base.establish_connection(db_conn_spec)
|
33
36
|
|
34
|
-
|
37
|
+
db_adapter = ActiveRecord::Base.connection.adapter_name
|
38
|
+
puts "Running with database adapter '#{db_adapter}'"
|
39
|
+
end
|
40
|
+
|
41
|
+
config.before(:each, active_record: true) do
|
42
|
+
tables = %w(my_active_record_models my_active_record_model_transitions)
|
43
|
+
tables.each do |table_name|
|
35
44
|
sql = "DROP TABLE IF EXISTS #{table_name};"
|
36
45
|
ActiveRecord::Base.connection.execute(sql)
|
37
46
|
end
|
@@ -45,9 +54,15 @@ RSpec.configure do |config|
|
|
45
54
|
def prepare_transitions_table
|
46
55
|
silence_stream(STDOUT) do
|
47
56
|
CreateMyActiveRecordModelTransitionMigration.migrate(:up)
|
57
|
+
MyActiveRecordModelTransition.reset_column_information
|
48
58
|
end
|
49
59
|
end
|
50
|
-
end
|
51
60
|
|
52
|
-
|
61
|
+
def drop_most_recent_column
|
62
|
+
silence_stream(STDOUT) do
|
63
|
+
DropMostRecentColumn.migrate(:up)
|
64
|
+
MyActiveRecordModelTransition.reset_column_information
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
53
68
|
end
|
@@ -1,11 +1,16 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
-
describe Statesman::Adapters::ActiveRecordQueries do
|
3
|
+
describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
4
4
|
before do
|
5
5
|
prepare_model_table
|
6
6
|
prepare_transitions_table
|
7
7
|
end
|
8
8
|
|
9
|
+
before do
|
10
|
+
Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
|
11
|
+
end
|
12
|
+
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
13
|
+
|
9
14
|
before do
|
10
15
|
MyActiveRecordModel.send(:include, Statesman::Adapters::ActiveRecordQueries)
|
11
16
|
MyActiveRecordModel.class_eval do
|
@@ -21,13 +26,13 @@ describe Statesman::Adapters::ActiveRecordQueries do
|
|
21
26
|
|
22
27
|
let!(:model) do
|
23
28
|
model = MyActiveRecordModel.create
|
24
|
-
model.
|
29
|
+
model.state_machine.transition_to(:succeeded)
|
25
30
|
model
|
26
31
|
end
|
27
32
|
|
28
33
|
let!(:other_model) do
|
29
34
|
model = MyActiveRecordModel.create
|
30
|
-
model.
|
35
|
+
model.state_machine.transition_to(:failed)
|
31
36
|
model
|
32
37
|
end
|
33
38
|
|
@@ -35,45 +40,122 @@ describe Statesman::Adapters::ActiveRecordQueries do
|
|
35
40
|
|
36
41
|
let!(:returned_to_initial_model) do
|
37
42
|
model = MyActiveRecordModel.create
|
38
|
-
model.
|
39
|
-
model.
|
43
|
+
model.state_machine.transition_to(:failed)
|
44
|
+
model.state_machine.transition_to(:initial)
|
40
45
|
model
|
41
46
|
end
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
context "with a most_recent column" do
|
49
|
+
describe ".in_state" do
|
50
|
+
context "given a single state" do
|
51
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
46
52
|
|
47
|
-
|
48
|
-
|
53
|
+
it { is_expected.to include model }
|
54
|
+
it { is_expected.not_to include other_model }
|
55
|
+
its(:to_sql) { is_expected.to include('most_recent') }
|
56
|
+
end
|
57
|
+
|
58
|
+
context "given multiple states" do
|
59
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
60
|
+
|
61
|
+
it { is_expected.to include model }
|
62
|
+
it { is_expected.to include other_model }
|
63
|
+
end
|
49
64
|
|
50
|
-
|
51
|
-
|
65
|
+
context "given the initial state" do
|
66
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
52
67
|
|
53
|
-
|
54
|
-
|
68
|
+
it { is_expected.to include initial_state_model }
|
69
|
+
it { is_expected.to include returned_to_initial_model }
|
70
|
+
end
|
71
|
+
|
72
|
+
context "given an array of states" do
|
73
|
+
subject { MyActiveRecordModel.in_state([:succeeded, :failed]) }
|
74
|
+
|
75
|
+
it { is_expected.to include model }
|
76
|
+
it { is_expected.to include other_model }
|
77
|
+
end
|
55
78
|
end
|
56
79
|
|
57
|
-
|
58
|
-
|
80
|
+
describe ".not_in_state" do
|
81
|
+
context "given a single state" do
|
82
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
83
|
+
it { is_expected.to include model }
|
84
|
+
it { is_expected.not_to include other_model }
|
85
|
+
its(:to_sql) { is_expected.to include('most_recent') }
|
86
|
+
end
|
87
|
+
|
88
|
+
context "given multiple states" do
|
89
|
+
subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
90
|
+
it do
|
91
|
+
is_expected.to match_array([initial_state_model,
|
92
|
+
returned_to_initial_model])
|
93
|
+
end
|
94
|
+
end
|
59
95
|
|
60
|
-
|
61
|
-
|
96
|
+
context "given an array of states" do
|
97
|
+
subject { MyActiveRecordModel.not_in_state([:succeeded, :failed]) }
|
98
|
+
it do
|
99
|
+
is_expected.to match_array([initial_state_model,
|
100
|
+
returned_to_initial_model])
|
101
|
+
end
|
102
|
+
end
|
62
103
|
end
|
63
104
|
end
|
64
105
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
106
|
+
context "without a most_recent column" do
|
107
|
+
before { drop_most_recent_column }
|
108
|
+
|
109
|
+
describe ".in_state" do
|
110
|
+
context "given a single state" do
|
111
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
112
|
+
|
113
|
+
it { is_expected.to include model }
|
114
|
+
end
|
115
|
+
|
116
|
+
context "given multiple states" do
|
117
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
118
|
+
|
119
|
+
it { is_expected.to include model }
|
120
|
+
it { is_expected.to include other_model }
|
121
|
+
end
|
122
|
+
|
123
|
+
context "given the initial state" do
|
124
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
125
|
+
|
126
|
+
it { is_expected.to include initial_state_model }
|
127
|
+
it { is_expected.to include returned_to_initial_model }
|
128
|
+
end
|
129
|
+
|
130
|
+
context "given an array of states" do
|
131
|
+
subject { MyActiveRecordModel.in_state([:succeeded, :failed]) }
|
132
|
+
|
133
|
+
it { is_expected.to include model }
|
134
|
+
it { is_expected.to include other_model }
|
135
|
+
end
|
70
136
|
end
|
71
137
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
is_expected.to
|
76
|
-
|
138
|
+
describe ".not_in_state" do
|
139
|
+
context "given a single state" do
|
140
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
141
|
+
it { is_expected.to include model }
|
142
|
+
it { is_expected.not_to include other_model }
|
143
|
+
end
|
144
|
+
|
145
|
+
context "given multiple states" do
|
146
|
+
subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
147
|
+
it do
|
148
|
+
is_expected.to match_array([initial_state_model,
|
149
|
+
returned_to_initial_model])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "given an array of states" do
|
154
|
+
subject { MyActiveRecordModel.not_in_state([:succeeded, :failed]) }
|
155
|
+
it do
|
156
|
+
is_expected.to match_array([initial_state_model,
|
157
|
+
returned_to_initial_model])
|
158
|
+
end
|
77
159
|
end
|
78
160
|
end
|
79
161
|
end
|