statesman 12.1.0 → 13.1.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +31 -0
  3. data/.devcontainer/docker-compose.yml +39 -0
  4. data/.github/workflows/tests.yml +42 -24
  5. data/.gitignore +1 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +1 -4
  8. data/.rubocop_todo.yml +23 -38
  9. data/.ruby-version +1 -1
  10. data/CHANGELOG.md +31 -7
  11. data/Gemfile +15 -1
  12. data/README.md +47 -26
  13. data/docs/COMPATIBILITY.md +1 -1
  14. data/lib/generators/statesman/active_record_transition_generator.rb +4 -5
  15. data/lib/generators/statesman/generator_helpers.rb +28 -14
  16. data/lib/generators/statesman/migration_generator.rb +4 -8
  17. data/lib/generators/statesman/templates/active_record_transition_model.rb.erb +3 -3
  18. data/lib/generators/statesman/templates/create_migration.rb.erb +3 -2
  19. data/lib/generators/statesman/templates/update_migration.rb.erb +3 -2
  20. data/lib/statesman/adapters/active_record.rb +9 -5
  21. data/lib/statesman/adapters/active_record_transition.rb +6 -0
  22. data/lib/statesman/adapters/memory.rb +1 -1
  23. data/lib/statesman/adapters/memory_transition.rb +3 -1
  24. data/lib/statesman/exceptions.rb +2 -0
  25. data/lib/statesman/guard.rb +1 -1
  26. data/lib/statesman/machine.rb +16 -0
  27. data/lib/statesman/version.rb +1 -1
  28. data/spec/generators/statesman/active_record_transition_generator_spec.rb +18 -9
  29. data/spec/generators/statesman/migration_generator_spec.rb +20 -13
  30. data/spec/spec_helper.rb +1 -0
  31. data/spec/statesman/adapters/active_record_queries_spec.rb +2 -2
  32. data/spec/statesman/adapters/active_record_spec.rb +8 -6
  33. data/spec/statesman/adapters/memory_transition_spec.rb +4 -2
  34. data/spec/statesman/adapters/shared_examples.rb +4 -3
  35. data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +1 -1
  36. data/spec/statesman/exceptions_spec.rb +10 -0
  37. data/spec/statesman/machine_spec.rb +46 -12
  38. data/spec/support/active_record.rb +18 -2
  39. data/spec/support/generators_shared_examples.rb +2 -2
  40. data/statesman.gemspec +1 -15
  41. metadata +7 -202
@@ -10,36 +10,44 @@ module Statesman
10
10
  "app/models/#{klass.underscore}.rb"
11
11
  end
12
12
 
13
- def migration_class_name
14
- klass.gsub("::", "").pluralize
15
- end
16
-
17
- def next_migration_number
18
- Time.now.utc.strftime("%Y%m%d%H%M%S")
19
- end
20
-
21
13
  def parent_name
22
- parent.demodulize.underscore
14
+ parent.underscore.split("/").join("_")
23
15
  end
24
16
 
25
17
  def parent_table_name
26
- parent.demodulize.underscore.pluralize
18
+ parent.underscore.split("/").join("_").tableize
27
19
  end
28
20
 
29
21
  def parent_id
30
22
  parent_name + "_id"
31
23
  end
32
24
 
33
- def table_name
25
+ def association_name
34
26
  klass.demodulize.underscore.pluralize
35
27
  end
36
28
 
29
+ def table_name
30
+ klass.underscore.split("/").join("_").tableize
31
+ end
32
+
33
+ def metadata_column_type
34
+ if ActiveRecord::Base.connection.supports_json?
35
+ postgres? ? :jsonb : :json
36
+ else
37
+ :text
38
+ end
39
+ end
40
+
37
41
  def index_name(index_id)
38
42
  "index_#{table_name}_#{index_id}"
39
43
  end
40
44
 
45
+ def postgres?
46
+ configuration.adapter.try(:match, /postgres/)
47
+ end
48
+
41
49
  def mysql?
42
- configuration.try(:[], "adapter").try(:match, /mysql/)
50
+ configuration.adapter.try(:match, /mysql/)
43
51
  end
44
52
 
45
53
  # [] is deprecated and will be removed in 6.2
@@ -52,11 +60,17 @@ module Statesman
52
60
  end
53
61
 
54
62
  def database_supports_partial_indexes?
55
- Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(klass.constantize)
63
+ Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(parent.constantize)
56
64
  end
57
65
 
58
66
  def metadata_default_value
59
- Utils.rails_5_or_higher? ? "{}" : "{}".inspect
67
+ Utils.rails_5_or_higher? ? "{}" : "'{}'"
68
+ end
69
+
70
+ def metadata_column_config
71
+ return if mysql?
72
+
73
+ ", default: #{metadata_default_value}"
60
74
  end
61
75
  end
62
76
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "rails/generators/active_record"
4
5
  require "generators/statesman/generator_helpers"
5
6
 
6
7
  # Add statesman attributes to a pre-existing transition class
7
8
  module Statesman
8
9
  class MigrationGenerator < Rails::Generators::Base
9
10
  include Statesman::GeneratorHelpers
11
+ include ActiveRecord::Generators::Migration
10
12
 
11
13
  desc "Add the required Statesman attributes to your transition model"
12
14
 
@@ -15,14 +17,8 @@ module Statesman
15
17
 
16
18
  source_root File.expand_path("templates", __dir__)
17
19
 
18
- def create_model_file
19
- template("update_migration.rb.erb", file_name)
20
- end
21
-
22
- private
23
-
24
- def file_name
25
- "db/migrate/#{next_migration_number}_add_statesman_to_#{table_name}.rb"
20
+ def create_migration_file
21
+ migration_template("update_migration.rb.erb", File.join(db_migrate_path, "add_statesman_to_#{table_name}.rb"))
26
22
  end
27
23
  end
28
24
  end
@@ -9,17 +9,17 @@ class <%= klass %> < <%= Statesman::Utils.rails_5_or_higher? ? 'ApplicationRecor
9
9
  # self.updated_timestamp_column = nil
10
10
 
11
11
  <%- unless Statesman::Utils.rails_4_or_higher? -%>
12
- attr_accessible :to_state, :metadata, :sort_key
12
+ attr_accessible :from_state, :to_state, :metadata, :sort_key
13
13
 
14
14
  <%- end -%>
15
- belongs_to :<%= parent_name %><%= class_name_option %>, inverse_of: :<%= table_name %>
15
+ belongs_to :<%= parent_name %><%= class_name_option %>, inverse_of: :<%= association_name %>
16
16
 
17
17
  after_destroy :update_most_recent, if: :most_recent?
18
18
 
19
19
  private
20
20
 
21
21
  def update_most_recent
22
- last_transition = <%= parent_name %>.<%= table_name %>.order(:sort_key).last
22
+ last_transition = <%= parent_name %>.<%= association_name %>.order(:sort_key).last
23
23
  return unless last_transition.present?
24
24
  last_transition.update_column(:most_recent, true)
25
25
  end
@@ -1,8 +1,9 @@
1
- class Create<%= migration_class_name %> < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" if Statesman::Utils.rails_5_or_higher? %>
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" if Statesman::Utils.rails_5_or_higher? %>
2
2
  def change
3
3
  create_table :<%= table_name %> do |t|
4
+ t.string :from_state, null: false
4
5
  t.string :to_state, null: false
5
- t.text :metadata<%= ", default: #{metadata_default_value}" unless mysql? %>
6
+ t.<%= metadata_column_type %> :metadata<%= metadata_column_config %>
6
7
  t.integer :sort_key, null: false
7
8
  t.integer :<%= parent_id %>, null: false
8
9
  t.boolean :most_recent<%= ", null: false" if database_supports_partial_indexes? %>
@@ -1,7 +1,8 @@
1
- class AddStatesmanTo<%= migration_class_name %> < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" if Statesman::Utils.rails_5_or_higher? %>
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" if Statesman::Utils.rails_5_or_higher? %>
2
2
  def change
3
+ add_column :<%= table_name %>, :from_state, :string, null: false
3
4
  add_column :<%= table_name %>, :to_state, :string, null: false
4
- add_column :<%= table_name %>, :metadata, :text<%= ", default: #{metadata_default_value}" unless mysql? %>
5
+ add_column :<%= table_name %>, :metadata, :<%= metadata_column_type %><%= metadata_column_config%>
5
6
  add_column :<%= table_name %>, :sort_key, :integer, null: false
6
7
  add_column :<%= table_name %>, :<%= parent_id %>, :integer, null: false
7
8
  add_column :<%= table_name %>, :most_recent, null: false
@@ -81,7 +81,7 @@ module Statesman
81
81
 
82
82
  def create_transition(from, to, metadata)
83
83
  transition = transitions_for_parent.build(
84
- default_transition_attributes(to, metadata),
84
+ default_transition_attributes(from, to, metadata),
85
85
  )
86
86
 
87
87
  transition_class.transaction(requires_new: true) do
@@ -116,13 +116,19 @@ module Statesman
116
116
  transition
117
117
  end
118
118
 
119
- def default_transition_attributes(to, metadata)
120
- {
119
+ def default_transition_attributes(from, to, metadata)
120
+ attributes = {
121
121
  to_state: to,
122
122
  sort_key: next_sort_key,
123
123
  metadata: metadata,
124
124
  most_recent: not_most_recent_value(db_cast: false),
125
125
  }
126
+
127
+ if @transition_class.has_attribute?(:from_state)
128
+ attributes[:from_state] = from
129
+ end
130
+
131
+ attributes
126
132
  end
127
133
 
128
134
  def add_after_commit_callback(from, to, transition)
@@ -379,11 +385,9 @@ module Statesman
379
385
  true
380
386
  end
381
387
 
382
- # rubocop: disable Naming/PredicateName
383
388
  def has_transactional_callbacks?
384
389
  true
385
390
  end
386
- # rubocop: enable Naming/PredicateName
387
391
 
388
392
  def committed!(*)
389
393
  @callback.call
@@ -19,6 +19,12 @@ module Statesman
19
19
  class_attribute :updated_timestamp_column
20
20
  self.updated_timestamp_column = DEFAULT_UPDATED_TIMESTAMP_COLUMN
21
21
  end
22
+
23
+ def from_state
24
+ if has_attribute?(:from_state)
25
+ self[:from_state]
26
+ end
27
+ end
22
28
  end
23
29
  end
24
30
  end
@@ -20,7 +20,7 @@ module Statesman
20
20
  def create(from, to, metadata = {})
21
21
  from = from.to_s
22
22
  to = to.to_s
23
- transition = transition_class.new(to, next_sort_key, metadata)
23
+ transition = transition_class.new(from, to, next_sort_key, metadata)
24
24
 
25
25
  @observer.execute(:before, from, to, transition)
26
26
  @history << transition
@@ -5,13 +5,15 @@ module Statesman
5
5
  class MemoryTransition
6
6
  attr_accessor :created_at
7
7
  attr_accessor :updated_at
8
+ attr_accessor :from_state
8
9
  attr_accessor :to_state
9
10
  attr_accessor :sort_key
10
11
  attr_accessor :metadata
11
12
 
12
- def initialize(to, sort_key, metadata = {})
13
+ def initialize(from, to, sort_key, metadata = {})
13
14
  @created_at = Time.now
14
15
  @updated_at = Time.now
16
+ @from_state = from
15
17
  @to_state = to
16
18
  @sort_key = sort_key
17
19
  @metadata = metadata
@@ -11,6 +11,8 @@ module Statesman
11
11
 
12
12
  class MissingTransitionAssociation < StandardError; end
13
13
 
14
+ class StateConstantConflictError < StandardError; end
15
+
14
16
  class TransitionFailedError < StandardError
15
17
  def initialize(from, to)
16
18
  @from = from
@@ -6,7 +6,7 @@ require_relative "exceptions"
6
6
  module Statesman
7
7
  class Guard < Callback
8
8
  def call(*args)
9
- raise GuardFailedError.new(from, to, callback) unless super(*args)
9
+ raise GuardFailedError.new(from, to, callback) unless super
10
10
  end
11
11
  end
12
12
  end
@@ -39,6 +39,8 @@ module Statesman
39
39
  validate_initial_state(name)
40
40
  @initial_state = name
41
41
  end
42
+ define_state_constant(name)
43
+
42
44
  states << name
43
45
  end
44
46
 
@@ -163,6 +165,20 @@ module Statesman
163
165
 
164
166
  private
165
167
 
168
+ def define_state_constant(state_name)
169
+ constant_name = state_name.upcase.gsub(/[^A-Z0-9]/, "_")
170
+
171
+ if const_defined?(constant_name)
172
+ return if const_get(constant_name) == state_name
173
+
174
+ raise StateConstantConflictError, "Name conflict: '#{name}::#{constant_name}' is already " \
175
+ "defined as '#{const_get(constant_name)}' attempting to redefine " \
176
+ "as '#{state_name}'"
177
+ else
178
+ const_set(constant_name, state_name)
179
+ end
180
+ end
181
+
166
182
  def add_callback(callback_type: nil, callback_class: nil,
167
183
  from: nil, to: nil, &block)
168
184
  validate_callback_type_and_class(callback_type, callback_class)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "12.1.0"
4
+ VERSION = "13.1.0"
5
5
  end
@@ -11,23 +11,33 @@ describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
11
11
  stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
12
12
  end
13
13
 
14
+ around { |e| Timecop.freeze(Time.parse("2025-01-01 00:00:00"), &e) }
15
+
14
16
  it_behaves_like "a generator" do
15
- let(:migration_name) { "db/migrate/create_bacon_transitions.rb" }
17
+ let(:migration_name) { "db/migrate/create_yummy_bacon_transitions.rb" }
16
18
  end
17
19
 
18
20
  describe "creates a migration" do
19
- subject(:migration) { file("db/migrate/#{time}_create_bacon_transitions.rb") }
21
+ subject(:migration) { file("db/migrate/20250101000000_create_yummy_bacon_transitions.rb") }
20
22
 
21
23
  before do
22
- allow(Time).to receive(:now).and_return(mock_time)
23
24
  run_generator %w[Yummy::Bacon Yummy::BaconTransition]
24
25
  end
25
26
 
26
- let(:mock_time) { double("Time", utc: double("UTCTime", strftime: time)) }
27
- let(:time) { "5678309" }
28
-
29
27
  it "includes a foreign key" do
30
- expect(migration).to contain("add_foreign_key :bacon_transitions, :bacons")
28
+ expect(migration).to contain("add_foreign_key :yummy_bacon_transitions, :yummy_bacons")
29
+ end
30
+
31
+ it "uses the right column type for Postgres", if: postgres? do
32
+ expect(migration).to contain("t.jsonb :metadata, default: {}")
33
+ end
34
+
35
+ it "uses the right column type for MySQL", if: mysql? do
36
+ expect(migration).to contain("t.json :metadata")
37
+ end
38
+
39
+ it "uses the right column type for SQLite", if: sqlite? do
40
+ expect(migration).to contain("t.json :metadata, default: {}")
31
41
  end
32
42
  end
33
43
 
@@ -36,8 +46,7 @@ describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
36
46
 
37
47
  before { run_generator %w[Yummy::Bacon Yummy::BaconTransition] }
38
48
 
39
- it { is_expected.to contain(/:bacon_transition/) }
40
- it { is_expected.to_not contain(%r{:yummy/bacon}) }
49
+ it { is_expected.to contain(/:bacon_transitions/) }
41
50
  it { is_expected.to contain(/class_name: 'Yummy::Bacon'/) }
42
51
  end
43
52
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "support/generators_shared_examples"
4
4
  require "generators/statesman/migration_generator"
5
+ require "rails/generators/testing/behavior"
5
6
 
6
7
  describe Statesman::MigrationGenerator, type: :generator do
7
8
  before do
@@ -9,40 +10,46 @@ describe Statesman::MigrationGenerator, type: :generator do
9
10
  stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
10
11
  end
11
12
 
13
+ around { |e| Timecop.freeze(Time.parse("2025-01-01 00:00:00"), &e) }
14
+
12
15
  it_behaves_like "a generator" do
13
- let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" }
16
+ let(:migration_name) { "db/migrate/20250101000000_add_statesman_to_yummy_bacon_transitions.rb" }
14
17
  end
15
18
 
16
19
  describe "the model contains the correct words" do
17
20
  subject(:migration) do
18
21
  file(
19
- "db/migrate/#{migration_number}_add_statesman_to_bacon_transitions.rb",
22
+ "db/migrate/20250101000000_add_statesman_to_yummy_bacon_transitions.rb",
20
23
  )
21
24
  end
22
25
 
23
- let(:migration_number) { "5678309" }
24
-
25
- let(:mock_time) do
26
- double("Time", utc: double("UTCTime", strftime: migration_number))
27
- end
28
-
29
26
  before do
30
- allow(Time).to receive(:now).and_return(mock_time)
31
27
  run_generator %w[Yummy::Bacon Yummy::BaconTransition]
32
28
  end
33
29
 
34
- it { is_expected.to contain(/:bacon_transition/) }
35
- it { is_expected.to_not contain(%r{:yummy/bacon}) }
30
+ it { is_expected.to contain(/:yummy_bacon_transition/) }
36
31
  it { is_expected.to contain(/null: false/) }
37
32
 
38
33
  it "names the sorting index appropriately" do
39
34
  expect(migration).
40
- to contain("name: \"index_bacon_transitions_parent_sort\"")
35
+ to contain("name: \"index_yummy_bacon_transitions_parent_sort\"")
41
36
  end
42
37
 
43
38
  it "names the most_recent index appropriately" do
44
39
  expect(migration).
45
- to contain("name: \"index_bacon_transitions_parent_most_recent\"")
40
+ to contain("name: \"index_yummy_bacon_transitions_parent_most_recent\"")
41
+ end
42
+
43
+ it "uses the right column type for Postgres", if: postgres? do
44
+ expect(migration).to contain(":metadata, :jsonb")
45
+ end
46
+
47
+ it "uses the right column type for MySQL", if: mysql? do
48
+ expect(migration).to contain(":metadata, :json")
49
+ end
50
+
51
+ it "uses the right column type for SQLite", if: sqlite? do
52
+ expect(migration).to contain(":metadata, :json")
46
53
  end
47
54
  end
48
55
  end
data/spec/spec_helper.rb CHANGED
@@ -15,6 +15,7 @@ require "rspec/rails"
15
15
  require "support/exactly_query_databases"
16
16
  require "rspec/its"
17
17
  require "pry"
18
+ require "timecop"
18
19
 
19
20
  RSpec.configure do |config|
20
21
  config.raise_errors_for_deprecations!
@@ -214,13 +214,13 @@ describe Statesman::Adapters::ActiveRecordQueries, :active_record do
214
214
  context "using old configuration method" do
215
215
  let(:config_type) { :old }
216
216
 
217
- include_examples "testing methods"
217
+ it_behaves_like "testing methods"
218
218
  end
219
219
 
220
220
  context "using new configuration method" do
221
221
  let(:config_type) { :new }
222
222
 
223
- include_examples "testing methods"
223
+ it_behaves_like "testing methods"
224
224
  end
225
225
 
226
226
  context "with no association with the transition class" do
@@ -375,9 +375,10 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
375
375
  let(:adapter) { described_class.new(transition_class, model, observer) }
376
376
 
377
377
  context "with a previously looked up transition" do
378
- before { adapter.create(:x, :y) }
379
-
380
- before { adapter.last }
378
+ before do
379
+ adapter.create(:x, :y)
380
+ adapter.last
381
+ end
381
382
 
382
383
  it "caches the transition" do
383
384
  expect_any_instance_of(MyActiveRecordModel).
@@ -444,9 +445,10 @@ describe Statesman::Adapters::ActiveRecord, :active_record do
444
445
  end
445
446
 
446
447
  context "with a pre-fetched transition history" do
447
- before { adapter.create(:x, :y) }
448
-
449
- before { model.my_active_record_model_transitions.load_target }
448
+ before do
449
+ adapter.create(:x, :y)
450
+ model.my_active_record_model_transitions.load_target
451
+ end
450
452
 
451
453
  it "doesn't query the database" do
452
454
  expect(MyActiveRecordModelTransition).to_not receive(:connection)
@@ -4,10 +4,12 @@ require "statesman/adapters/memory_transition"
4
4
 
5
5
  describe Statesman::Adapters::MemoryTransition do
6
6
  describe "#initialize" do
7
+ let(:from) { :n }
7
8
  let(:to) { :y }
8
9
  let(:sort_key) { 0 }
9
- let(:create) { described_class.new(to, sort_key) }
10
+ let(:create) { described_class.new(from, to, sort_key) }
10
11
 
12
+ specify { expect(create.from_state).to equal(from) }
11
13
  specify { expect(create.to_state).to equal(to) }
12
14
  specify { expect(create.created_at).to be_a(Time) }
13
15
  specify { expect(create.updated_at).to be_a(Time) }
@@ -15,7 +17,7 @@ describe Statesman::Adapters::MemoryTransition do
15
17
 
16
18
  context "with metadata passed" do
17
19
  let(:metadata) { { some: :hash } }
18
- let(:create) { described_class.new(to, sort_key, metadata) }
20
+ let(:create) { described_class.new(from, to, sort_key, metadata) }
19
21
 
20
22
  specify { expect(create.metadata).to eq(metadata) }
21
23
  end
@@ -120,9 +120,10 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
120
120
  describe "#last" do
121
121
  subject { adapter.last }
122
122
 
123
- before { adapter.create(:x, :y) }
124
-
125
- before { adapter.create(:y, :z) }
123
+ before do
124
+ adapter.create(:x, :y)
125
+ adapter.create(:y, :z)
126
+ end
126
127
 
127
128
  it { is_expected.to be_a(transition_class) }
128
129
  specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
@@ -201,6 +201,6 @@ describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do
201
201
  end
202
202
 
203
203
  context "using configuration method" do
204
- include_examples "testing methods"
204
+ it_behaves_like "testing methods"
205
205
  end
206
206
  end
@@ -51,6 +51,16 @@ describe "Exceptions" do
51
51
  end
52
52
  end
53
53
 
54
+ describe "StateConstantConflictError" do
55
+ subject(:error) { Statesman::StateConstantConflictError.new }
56
+
57
+ its(:message) { is_expected.to eq("Statesman::StateConstantConflictError") }
58
+
59
+ its "string matches its message" do
60
+ expect(error.to_s).to eq(error.message)
61
+ end
62
+ end
63
+
54
64
  describe "TransitionFailedError" do
55
65
  subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
56
66
 
@@ -1,20 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe Statesman::Machine do
4
- let(:machine) { Class.new { include Statesman::Machine } }
4
+ let(:machine) do
5
+ Class.new do
6
+ include Statesman::Machine
7
+
8
+ def self.name
9
+ "MyStateMachine"
10
+ end
11
+ end
12
+ end
5
13
  let(:my_model) { Class.new { attr_accessor :current_state }.new }
6
14
 
7
15
  describe ".state" do
8
- before { machine.state(:x) }
9
-
10
- before { machine.state(:y) }
16
+ before do
17
+ machine.state(:x)
18
+ machine.state(:y)
19
+ end
11
20
 
12
21
  specify { expect(machine.states).to eq(%w[x y]) }
13
22
 
23
+ specify { expect(machine::X).to eq "x" }
24
+
25
+ specify { expect(machine::Y).to eq "y" }
26
+
14
27
  context "initial" do
15
- before { machine.state(:x, initial: true) }
28
+ before { machine.state(:z, initial: true) }
16
29
 
17
- specify { expect(machine.initial_state).to eq("x") }
30
+ specify { expect(machine.initial_state).to eq("z") }
18
31
 
19
32
  context "when an initial state is already defined" do
20
33
  it "raises an error" do
@@ -23,6 +36,27 @@ describe Statesman::Machine do
23
36
  end
24
37
  end
25
38
  end
39
+
40
+ context "when state name constant is already defined" do
41
+ context "with the same value" do
42
+ it "does not raise an error" do
43
+ machine.const_set(:SOME_CONST, "some_const")
44
+ expect { machine.state(:some_const) }.to_not raise_error
45
+ end
46
+ end
47
+
48
+ context "with a different value" do
49
+ it "raises an error about state constant conflict" do
50
+ machine.const_set(:SOME_CONST, "some_const_different")
51
+
52
+ expect { machine.state(:some_const) }.to raise_error(
53
+ Statesman::StateConstantConflictError, "Name conflict: 'MyStateMachine::SOME_CONST' is " \
54
+ "already defined as 'some_const_different' " \
55
+ "attempting to redefine as 'some_const'"
56
+ )
57
+ end
58
+ end
59
+ end
26
60
  end
27
61
 
28
62
  describe ".remove_state" do
@@ -610,9 +644,10 @@ describe Statesman::Machine do
610
644
  end
611
645
 
612
646
  context "with multiple transitions" do
613
- before { instance.transition_to!(:y) }
614
-
615
- before { instance.transition_to!(:z) }
647
+ before do
648
+ instance.transition_to!(:y)
649
+ instance.transition_to!(:z)
650
+ end
616
651
 
617
652
  it { is_expected.to eq("z") }
618
653
  end
@@ -627,12 +662,11 @@ describe Statesman::Machine do
627
662
  state :y
628
663
  transition from: :x, to: :y
629
664
  end
665
+ instance.transition_to!(:y)
630
666
  end
631
667
 
632
668
  let(:instance) { machine.new(my_model) }
633
669
 
634
- before { instance.transition_to!(:y) }
635
-
636
670
  context "when machine is in given state" do
637
671
  let(:state) { "y" }
638
672
 
@@ -995,7 +1029,7 @@ describe Statesman::Machine do
995
1029
  let(:instance) { machine.new(my_model) }
996
1030
  let(:metadata) { { some: :metadata } }
997
1031
 
998
- context "when it is succesful" do
1032
+ context "when it is successful" do
999
1033
  before do
1000
1034
  expect(instance).to receive(:transition_to!).once.
1001
1035
  with(:some_state, metadata).and_return(:some_state)