statesman 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/.travis.yml +19 -4
  4. data/CHANGELOG.md +30 -0
  5. data/README.md +11 -35
  6. data/lib/generators/statesman/add_constraints_to_most_recent_generator.rb +28 -0
  7. data/lib/generators/statesman/add_most_recent_generator.rb +25 -0
  8. data/lib/generators/statesman/generator_helpers.rb +6 -2
  9. data/lib/generators/statesman/templates/add_constraints_to_most_recent_migration.rb.erb +13 -0
  10. data/lib/generators/statesman/templates/add_most_recent_migration.rb.erb +9 -0
  11. data/lib/generators/statesman/templates/create_migration.rb.erb +4 -3
  12. data/lib/generators/statesman/templates/update_migration.rb.erb +5 -4
  13. data/lib/statesman.rb +1 -0
  14. data/lib/statesman/adapters/active_record.rb +23 -8
  15. data/lib/statesman/adapters/active_record_queries.rb +62 -15
  16. data/lib/statesman/railtie.rb +9 -0
  17. data/lib/statesman/version.rb +1 -1
  18. data/lib/tasks/statesman.rake +49 -0
  19. data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions.rb +13 -0
  20. data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +9 -0
  21. data/spec/generators/statesman/active_record_transition_generator_spec.rb +0 -2
  22. data/spec/generators/statesman/add_constraints_to_most_recent_generator_spec.rb +38 -0
  23. data/spec/generators/statesman/add_most_recent_generator_spec.rb +35 -0
  24. data/spec/generators/statesman/migration_generator_spec.rb +10 -1
  25. data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -2
  26. data/spec/spec_helper.rb +22 -7
  27. data/spec/statesman/adapters/active_record_queries_spec.rb +110 -28
  28. data/spec/statesman/adapters/active_record_spec.rb +61 -31
  29. data/spec/statesman/adapters/mongoid_spec.rb +8 -17
  30. data/spec/statesman/adapters/shared_examples.rb +10 -17
  31. data/spec/statesman/callback_spec.rb +2 -6
  32. data/spec/statesman/config_spec.rb +2 -5
  33. data/spec/statesman/guard_spec.rb +3 -9
  34. data/spec/statesman/machine_spec.rb +91 -129
  35. data/spec/support/active_record.rb +35 -4
  36. data/spec/support/generators_shared_examples.rb +1 -4
  37. data/statesman.gemspec +5 -3
  38. metadata +52 -10
@@ -0,0 +1,9 @@
1
+ module Statesman
2
+ class Railtie < ::Rails::Railtie
3
+ railtie_name :statesman
4
+
5
+ rake_tasks do
6
+ load "tasks/statesman.rake"
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Statesman
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -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
@@ -0,0 +1,9 @@
1
+ class AddMostRecentToBaconTransitions < ActiveRecord::Migration
2
+ def up
3
+ add_column :bacon_transitions, :most_recent, :boolean, null: true
4
+ end
5
+
6
+ def down
7
+ remove_column :bacon_transitions, :most_recent
8
+ end
9
+ 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.before(:each) do
30
- # Connect to & cleanup test database
31
- ActiveRecord::Base.establish_connection(adapter: "sqlite3",
32
- database: DB.to_s)
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
- %w(my_models my_model_transitions).each do |table_name|
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
- config.after(:each) { DB.delete if DB.exist? }
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.my_active_record_model_transitions.create(to_state: :succeeded)
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.my_active_record_model_transitions.create(to_state: :failed)
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.my_active_record_model_transitions.create(to_state: :failed)
39
- model.my_active_record_model_transitions.create(to_state: :initial)
43
+ model.state_machine.transition_to(:failed)
44
+ model.state_machine.transition_to(:initial)
40
45
  model
41
46
  end
42
47
 
43
- describe ".in_state" do
44
- context "given a single state" do
45
- subject { MyActiveRecordModel.in_state(:succeeded) }
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
- it { is_expected.to include model }
48
- end
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
- context "given multiple states" do
51
- subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
65
+ context "given the initial state" do
66
+ subject { MyActiveRecordModel.in_state(:initial) }
52
67
 
53
- it { is_expected.to include model }
54
- it { is_expected.to include other_model }
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
- context "given the initial state" do
58
- subject { MyActiveRecordModel.in_state(:initial) }
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
- it { is_expected.to include initial_state_model }
61
- it { is_expected.to include returned_to_initial_model }
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
- describe ".not_in_state" do
66
- context "given a single state" do
67
- subject { MyActiveRecordModel.not_in_state(:failed) }
68
- it { is_expected.to include model }
69
- it { is_expected.not_to include other_model }
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
- context "given multiple states" do
73
- subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
74
- it do
75
- is_expected.to match_array([initial_state_model,
76
- returned_to_initial_model])
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