diff --git a/README.md b/README.md index 600e20a3..b709b6f8 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,24 @@ Machine.successors } ``` +## Class constants +Adding a state to a state machine will automatically create a constant for the value, for example: +```ruby +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 + +OrderStateMachine::PENDING #=> "pending" +OrderStateMachine::CHECKING_OUT # => "checking_out" +``` + ## Instance methods ### `Machine#current_state` 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 ab3053eb..26e60b5e 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) + raise StateConstantConflictError, "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/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 aefb516a..b8609c1e 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,16 @@ 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") + + expect { machine.state(:some_const) }.to raise_error( + Statesman::StateConstantConflictError, "Name conflict: 'Class::SOME_CONST' is already defined" + ) + end + end end describe ".remove_state" do