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.
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