From 9df21799e0bfd3a94a223bad6d6ec87a85640a9a Mon Sep 17 00:00:00 2001 From: Stephann Vasconcelos <3025661+stephannv@users.noreply.github.com> Date: Wed, 2 Aug 2023 11:28:15 -0300 Subject: [PATCH 1/3] Define state constants --- README.md | 10 ++++++++++ lib/statesman/machine.rb | 12 ++++++++++++ spec/statesman/machine_spec.rb | 17 +++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 600e20a3..f0ae7ebd 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,16 @@ Machine.successors } ``` +## Class constants +Each machine's state will turn into a constant: +```ruby +Machine.state(:some_state, initial: true) +Machine.state(:another_state) + +Machine::SOME_STATE #=> "some_state" +Machine::ANOTHER_STATE # => "another_state" +``` + ## Instance methods ### `Machine#current_state` diff --git a/lib/statesman/machine.rb b/lib/statesman/machine.rb index ab3053eb..bde86f22 100644 --- a/lib/statesman/machine.rb +++ b/lib/statesman/machine.rb @@ -39,6 +39,8 @@ def state(name, options = { initial: false }) validate_initial_state(name) @initial_state = name end + define_state_constant(name) + states << name end @@ -163,6 +165,16 @@ def validate_from_and_to_state(from, to) private + def define_state_constant(state_name) + constant_name = state_name.upcase + + if const_defined?(constant_name) + warn "Name conflict: '#{self.class.name}::#{constant_name}' is already defined" + else + const_set(constant_name, state_name) + end + end + def add_callback(callback_type: nil, callback_class: nil, from: nil, to: nil, &block) validate_callback_type_and_class(callback_type, callback_class) diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index aefb516a..6a3da4e7 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -11,10 +11,14 @@ specify { expect(machine.states).to eq(%w[x y]) } + specify { expect(machine::X).to eq "x" } + + specify { expect(machine::Y).to eq "y" } + context "initial" do - before { machine.state(:x, initial: true) } + before { machine.state(:z, initial: true) } - specify { expect(machine.initial_state).to eq("x") } + specify { expect(machine.initial_state).to eq("z") } context "when an initial state is already defined" do it "raises an error" do @@ -23,6 +27,15 @@ end end end + + context "when state name constant is already defined" do + it "warns about name conflict" do + machine.const_set(:SOME_CONST, "some const") + warning_message = "Name conflict: 'Class::SOME_CONST' is already defined\n" + + expect { machine.state(:some_const) }.to output(warning_message).to_stderr + end + end end describe ".remove_state" do From a7745b0c171e6d3dba194fdfefb772ce42d1233b Mon Sep 17 00:00:00 2001 From: stephann <3025661+stephannv@users.noreply.github.com> Date: Thu, 11 Jan 2024 08:30:30 -0300 Subject: [PATCH 2/3] feat: Raise error on state constant name conflict --- lib/statesman/exceptions.rb | 2 ++ lib/statesman/machine.rb | 2 +- spec/statesman/exceptions_spec.rb | 10 ++++++++++ spec/statesman/machine_spec.rb | 5 +++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/statesman/exceptions.rb b/lib/statesman/exceptions.rb index 96adcc51..a870ac2a 100644 --- a/lib/statesman/exceptions.rb +++ b/lib/statesman/exceptions.rb @@ -11,6 +11,8 @@ class TransitionConflictError < StandardError; end class MissingTransitionAssociation < StandardError; end + class StateConstantConflictError < StandardError; end + class TransitionFailedError < StandardError def initialize(from, to) @from = from diff --git a/lib/statesman/machine.rb b/lib/statesman/machine.rb index bde86f22..26e60b5e 100644 --- a/lib/statesman/machine.rb +++ b/lib/statesman/machine.rb @@ -169,7 +169,7 @@ def define_state_constant(state_name) constant_name = state_name.upcase if const_defined?(constant_name) - warn "Name conflict: '#{self.class.name}::#{constant_name}' is already defined" + raise StateConstantConflictError, "Name conflict: '#{self.class.name}::#{constant_name}' is already defined" else const_set(constant_name, state_name) end diff --git a/spec/statesman/exceptions_spec.rb b/spec/statesman/exceptions_spec.rb index 36e0b7d9..bd7ccc4e 100644 --- a/spec/statesman/exceptions_spec.rb +++ b/spec/statesman/exceptions_spec.rb @@ -51,6 +51,16 @@ end end + describe "StateConstantConflictError" do + subject(:error) { Statesman::StateConstantConflictError.new } + + its(:message) { is_expected.to eq("Statesman::StateConstantConflictError") } + + its "string matches its message" do + expect(error.to_s).to eq(error.message) + end + end + describe "TransitionFailedError" do subject(:error) { Statesman::TransitionFailedError.new("from", "to") } diff --git a/spec/statesman/machine_spec.rb b/spec/statesman/machine_spec.rb index 6a3da4e7..b8609c1e 100644 --- a/spec/statesman/machine_spec.rb +++ b/spec/statesman/machine_spec.rb @@ -31,9 +31,10 @@ context "when state name constant is already defined" do it "warns about name conflict" do machine.const_set(:SOME_CONST, "some const") - warning_message = "Name conflict: 'Class::SOME_CONST' is already defined\n" - expect { machine.state(:some_const) }.to output(warning_message).to_stderr + expect { machine.state(:some_const) }.to raise_error( + Statesman::StateConstantConflictError, "Name conflict: 'Class::SOME_CONST' is already defined" + ) end end end From 66862fff106d7c703add48c9fa3b133e10fecd9f Mon Sep 17 00:00:00 2001 From: stephann <3025661+stephannv@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:30:10 -0300 Subject: [PATCH 3/3] Improve docs --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f0ae7ebd..b709b6f8 100644 --- a/README.md +++ b/README.md @@ -391,13 +391,21 @@ Machine.successors ``` ## Class constants -Each machine's state will turn into a constant: +Adding a state to a state machine will automatically create a constant for the value, for example: ```ruby -Machine.state(:some_state, initial: true) -Machine.state(:another_state) +class OrderStateMachine + include Statesman::Machine + + state :pending, initial: true + state :checking_out + state :cancelled + + # Constants created as a side effect of adding state + transition from: PENDING, to: [CHECKING_OUT, CANCELLED] +end -Machine::SOME_STATE #=> "some_state" -Machine::ANOTHER_STATE # => "another_state" +OrderStateMachine::PENDING #=> "pending" +OrderStateMachine::CHECKING_OUT # => "checking_out" ``` ## Instance methods