statesman 13.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b15bf5cd57e2049db1ae4a0e93f41b16528c8a73c5741cf6337ec280f6ed1041
4
- data.tar.gz: 28fba6b8a3304b97fb97a56d145243b5da73f79057fc05449dd3c48a6ea76f72
3
+ metadata.gz: f93bd968475f6cfd090311f9998a2350993df852aa3796fab5d3b52f962d6147
4
+ data.tar.gz: 3789c10d55df44ee6e921f9d2297d7d0e0767f42521309ac14aed8e49a0986a8
5
5
  SHA512:
6
- metadata.gz: 5df7c20cbbc737fc8527d959ad8d894cc0e5fece227099287dc1a96d72cbc62000d5d87b854f4a8483b2ee58c726154d9d50ca7568be27fc4126afe5a94818e5
7
- data.tar.gz: 29c4efb1b93e7057ba343b8c4894978e3d9660a53043755f8d26e6378fa804085cd83e8d81203ab7f81a2cbcdd89bab5126042a113d9bb0ca376cb5967b65ba3
6
+ metadata.gz: 13b9e129ec5bb984066a05c408ee99c9965861775269bab232e6aac86ee75b5c5485a14ccc8476b3b96d0bc383cc68a1f67d32cf9d3eea575478e25ed0245d56
7
+ data.tar.gz: d88507f263405847ea99f8a7cc88ec4e63c3a82addcef3a20e9c0e1c83372df38868278459702c0855e535d7ebca031e18ddaee32031b0beb162932963156325
data/.gitignore CHANGED
@@ -66,3 +66,4 @@ Gemfile.lock
66
66
 
67
67
  # JetBrains
68
68
  .idea
69
+ spec/support/generator-tmp/
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v13.1.0 9th October 2025
9
+
10
+ ### Added
11
+
12
+ - State constants for defining available states [#515](https://github.com/gocardless/statesman/pull/515)
13
+ - from_state method to transition history for easier access to previous states [#493](https://github.com/gocardless/statesman/pull/493)
14
+
15
+ ### Fixed
16
+
17
+ - Fixed transition model generator for namespaced models [#555](https://github.com/gocardless/statesman/pull/555)
18
+
19
+ ### Changed
20
+
21
+ - Updated README and clarified wording [#444](https://github.com/gocardless/statesman/pull/444)
22
+
8
23
  ## v13.0.0 29th August 2025
9
24
 
10
25
  ### Changes
data/README.md CHANGED
@@ -33,7 +33,7 @@ protection.
33
33
  To get started, just add Statesman to your `Gemfile`, and then run `bundle`:
34
34
 
35
35
  ```ruby
36
- gem 'statesman', '~> 12.0.0'
36
+ gem 'statesman', '~> 13.0.0'
37
37
  ```
38
38
 
39
39
  ## Usage
@@ -233,12 +233,14 @@ or 5. To do that
233
233
  t.json :metadata, default: {}
234
234
  ```
235
235
 
236
- - Remove the `include Statesman::Adapters::ActiveRecordTransition` statement from
237
- your transition model. (If you want to customise your transition class's "updated
238
- timestamp column", as described above, you should define a
239
- `.updated_timestamp_column` method on your class and return the name of the column
240
- as a symbol, or `nil` if you don't want to record an updated timestamp on
241
- transitions.)
236
+ * Remove the `include Statesman::Adapters::ActiveRecordTransition` statement from
237
+ your transition model, which would've instructed ActiveRecord to serialize the
238
+ metadata.
239
+ * The module that you just removed enables customizing the updatated timestamp column
240
+ as described above. Having removed it, if you want to customise your transition class's
241
+ "updated timestamp column", you should define a `.updated_timestamp_column` method on
242
+ your class and return the name of the column as a symbol, or `nil` if you don't want
243
+ to record an updated timestamp on transitions.
242
244
 
243
245
  ## Configuration
244
246
 
@@ -407,6 +409,24 @@ Machine.successors
407
409
  }
408
410
  ```
409
411
 
412
+ ## Class constants
413
+ Adding a state to a state machine will automatically create a constant for the value, for example:
414
+ ```ruby
415
+ class OrderStateMachine
416
+ include Statesman::Machine
417
+
418
+ state :pending, initial: true
419
+ state :checking_out
420
+ state :cancelled
421
+
422
+ # Constants created as a side effect of adding state
423
+ transition from: PENDING, to: [CHECKING_OUT, CANCELLED]
424
+ end
425
+
426
+ OrderStateMachine::PENDING #=> "pending"
427
+ OrderStateMachine::CHECKING_OUT # => "checking_out"
428
+ ```
429
+
410
430
  ## Instance methods
411
431
 
412
432
  ### `Machine#current_state`
@@ -541,8 +561,8 @@ model and passing in `transition_class` and `initial_state` as options.
541
561
 
542
562
  In 4.1.2 and below, these two options had to be defined as methods on the model,
543
563
  but 5.0.0 and above allow this style of configuration as well.
544
- The old method pollutes the model with extra class methods, and is deprecated,
545
- to be removed in 6.0.0.
564
+ The old way pollutes the model with extra class methods, and is deprecated,
565
+ to be removed in the future.
546
566
 
547
567
  ```ruby
548
568
  class Order < ActiveRecord::Base
@@ -1,11 +1,13 @@
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
  module Statesman
7
8
  class ActiveRecordTransitionGenerator < Rails::Generators::Base
8
9
  include Statesman::GeneratorHelpers
10
+ include ActiveRecord::Generators::Migration
9
11
 
10
12
  desc "Create an ActiveRecord-based transition model" \
11
13
  "with the required attributes"
@@ -16,14 +18,11 @@ module Statesman
16
18
  source_root File.expand_path("templates", __dir__)
17
19
 
18
20
  def create_model_file
19
- template("create_migration.rb.erb", migration_file_name)
20
21
  template("active_record_transition_model.rb.erb", model_file_name)
21
22
  end
22
23
 
23
- private
24
-
25
- def migration_file_name
26
- "db/migrate/#{next_migration_number}_create_#{table_name}.rb"
24
+ def create_migration_file
25
+ migration_template("create_migration.rb.erb", File.join(db_migrate_path, "create_#{table_name}.rb"))
27
26
  end
28
27
  end
29
28
  end
@@ -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)
@@ -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
@@ -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 = "13.0.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!
@@ -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
@@ -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,7 +1,15 @@
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
@@ -12,10 +20,14 @@ describe Statesman::Machine do
12
20
 
13
21
  specify { expect(machine.states).to eq(%w[x y]) }
14
22
 
23
+ specify { expect(machine::X).to eq "x" }
24
+
25
+ specify { expect(machine::Y).to eq "y" }
26
+
15
27
  context "initial" do
16
- before { machine.state(:x, initial: true) }
28
+ before { machine.state(:z, initial: true) }
17
29
 
18
- specify { expect(machine.initial_state).to eq("x") }
30
+ specify { expect(machine.initial_state).to eq("z") }
19
31
 
20
32
  context "when an initial state is already defined" do
21
33
  it "raises an error" do
@@ -24,6 +36,27 @@ describe Statesman::Machine do
24
36
  end
25
37
  end
26
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
27
60
  end
28
61
 
29
62
  describe ".remove_state" do
@@ -70,6 +70,7 @@ end
70
70
  class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
71
71
  def change
72
72
  create_table :my_active_record_model_transitions do |t|
73
+ t.string :from_state
73
74
  t.string :to_state
74
75
  t.integer :my_active_record_model_id
75
76
  t.integer :sort_key
@@ -189,6 +190,7 @@ end
189
190
  class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
190
191
  def change
191
192
  create_table :other_active_record_model_transitions do |t|
193
+ t.string :from_state
192
194
  t.string :to_state
193
195
  t.integer :other_active_record_model_id
194
196
  t.integer :sort_key
@@ -284,6 +286,7 @@ end
284
286
  class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
285
287
  def change
286
288
  create_table :my_namespace_my_active_record_model_transitions do |t|
289
+ t.string :from_state
287
290
  t.string :to_state
288
291
  t.integer :my_active_record_model_id
289
292
  t.integer :sort_key
@@ -411,3 +414,16 @@ class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
411
414
  end
412
415
  end
413
416
  end
417
+
418
+ def postgres?
419
+ ActiveRecord::Base.connection.adapter_name.match?(/postgres/i)
420
+ end
421
+
422
+ def mysql?
423
+ ActiveRecord::Base.connection.adapter_name.match?(/mysql/i) ||
424
+ ActiveRecord::Base.connection.adapter_name.match?(/trilogy/i)
425
+ end
426
+
427
+ def sqlite?
428
+ ActiveRecord::Base.connection.adapter_name.match?(/sqlite/i)
429
+ end
@@ -12,8 +12,8 @@ shared_examples "a generator" do
12
12
 
13
13
  let(:gen) { generator %w[Yummy::Bacon Yummy::BaconTransition] }
14
14
 
15
- it "invokes create_model_file method" do
16
- expect(gen).to receive(:create_model_file)
15
+ it "invokes create_migration_file method" do
16
+ expect(gen).to receive(:create_migration_file)
17
17
  gen.invoke_all
18
18
  end
19
19
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statesman
3
3
  version: !ruby/object:Gem::Version
4
- version: 13.0.0
4
+ version: 13.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless