generic-state-machine 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 35027276d3d5fcc5399387fa3bef4d380ab472138b322c1a53a02cd3ef925a13
4
+ data.tar.gz: c3490ced6cf5e6eb9bab74a2f1597fd687c6bad3c71e2e4ffe3b145d32e466f5
5
+ SHA512:
6
+ metadata.gz: 0ee0898b46875cff5bec19184cf1708f31da6a491050450611d7f5c09d9e37f351aa83820cb588b5f033ca3d8228c813fad9082844d17dc06b90147a6765f9fb
7
+ data.tar.gz: f2277892803a2e2b80e90b7832bc97fe4eecd7fd1b7db6a469ce999dbe9e3019fc8786b95458bdd82da063b6d7da595460599e201b6721fffe14ed5c0aa6fdaf
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module GenericStateMachine
5
+ ##
6
+ module DSL
7
+ ##
8
+ # Method is used to describe the properties the GSM should have
9
+ # @raise [GenericStateMachine::Errors::DSLError]
10
+ #
11
+ def describe(&block)
12
+ raise GenericStateMachine::Errors::DSLError, '#describe needs a block' unless block_given?
13
+
14
+ dsl = StateMachineDSL.create &block
15
+
16
+ _create_state_machine dsl
17
+ end
18
+
19
+ ##
20
+ # Struct describing a transition
21
+ #
22
+ Transition = Struct.new(:from, :to, :condition)
23
+ ##
24
+ # Struct describing a hook
25
+ #
26
+ Hook = Struct.new(:name, :handler, :condition)
27
+
28
+ private
29
+
30
+ ##
31
+ # Actually create an instance of StateMachine using a DSL object
32
+ # @param [StateMachineDSL]
33
+ # @raise [GenericStateMachine::Errors::DSLError]
34
+ #
35
+ def _create_state_machine(dsl)
36
+ transitions = dsl.transitions.collect do |t|
37
+ _transition_from_struct t
38
+ end
39
+
40
+ GenericStateMachine::StateMachineFactory.create start: dsl.starting,
41
+ transitions: transitions, hooks: dsl.hooks
42
+ end
43
+
44
+ ##
45
+ # Helper creating a Transition instance
46
+ # @param [Transition] transition
47
+ #
48
+ def _transition_from_struct(transition)
49
+ GenericStateMachine::Transition.new transition.from, transition.to, transition.condition
50
+ end
51
+ end
52
+ end
53
+
54
+ # Make DSL available directly under main module
55
+ GenericStateMachine.extend GenericStateMachine::DSL
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module GenericStateMachine
5
+ ##
6
+ module DSL
7
+ ##
8
+ # StateMachineDSL provides convenient methods for creating a state machine
9
+ #
10
+ class StateMachineDSL
11
+ attr_reader :transitions, :hooks, :starting
12
+
13
+ ##
14
+ # Add transition
15
+ # @param [Symbol] from The current state
16
+ # @param [Symbol] to The state to transit to
17
+ # @param [Object] condition An optional condition determining whether the transition should be executed
18
+ # @raise [GenericStateMachine::Errors::DSLError] on any error
19
+ #
20
+ def transition(from:, to:, condition: true)
21
+ @transitions ||= []
22
+ @transitions << Transition.new(from, to, condition)
23
+ rescue StandardError => e
24
+ raise GenericStateMachine::Errors::DSLError, e
25
+ end
26
+
27
+ ##
28
+ # Add hook
29
+ # @param [Symbol] hook The name of the hook
30
+ # @param [Symbol] handler Name of the handler to be executed
31
+ # @param [Object] condition Optional condition determining whether the hook should be executed
32
+ # @raise [GenericStateMachine::Errors::DSLError] on any error
33
+ #
34
+ def register(hook:, handler:, condition: nil)
35
+ raise GenericStateMachine::Errors::HookError, "Hook '#{hook}' isn't a valid hook" unless
36
+ GenericStateMachine::AVAILABLE_HOOKS.include?(hook)
37
+ raise GenericStateMachine::Errors::HookError, "Can't use value '#{hook}' as hook" if
38
+ hook.nil? || !hook.is_a?(Symbol)
39
+
40
+ @hooks ||= []
41
+ @hooks << Hook.new(hook, handler, condition)
42
+ rescue StandardError => e
43
+ raise GenericStateMachine::Errors::DSLError, e
44
+ end
45
+
46
+ ##
47
+ # Set the starting state
48
+ # @param [Symbol] from
49
+ # @raise GenericStateMachine::Errors::DSLError on any error
50
+ #
51
+ def start(from:)
52
+ raise GenericStateMachine::Errors::StateError, "Can't use value '#{from}' as state" if
53
+ from.nil? || !from.is_a?(Symbol)
54
+
55
+ @starting = from
56
+ rescue StandardError => e
57
+ raise GenericStateMachine::Errors::DSLError, e
58
+ end
59
+ class << self
60
+ ##
61
+ # Factory method creating the DSL helper object
62
+ #
63
+ def create(&block)
64
+ obj = new
65
+ obj.instance_eval(&block)
66
+
67
+ obj
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ describe 'GenericStateMachine::DSL#describe' do
6
+ context 'GSM factory #describe' do
7
+ it 'responds to #describe' do
8
+ expect(GenericStateMachine.respond_to?(:describe)).to be_truthy
9
+ end
10
+
11
+ it 'throws DSLError when used #describe w/o block' do
12
+ expect { GenericStateMachine.describe }.to raise_error GenericStateMachine::Errors::DSLError,
13
+ '#describe needs a block'
14
+ end
15
+ end
16
+
17
+ context 'Transitions' do
18
+ it 'has #transitions' do
19
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
20
+
21
+ expect(dsl.respond_to?(:transitions)).to be_truthy
22
+ end
23
+
24
+ it 'accepts required parameters in #transition' do
25
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
26
+ expect {
27
+ dsl.transition from: :state, to: :state
28
+ }.to_not raise_error
29
+ end
30
+
31
+ it 'accepts optional parameters in #transition' do
32
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
33
+ expect {
34
+ dsl.transition from: :state, to: :state, condition: true
35
+ }.to_not raise_error
36
+ end
37
+
38
+ it 'has one valid transition after adding one' do
39
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
40
+ dsl.transition from: :state, to: :state
41
+
42
+ expect(dsl.transitions.count).to eq 1
43
+ expect(dsl.transitions.first).to be_a GenericStateMachine::DSL::Transition
44
+ end
45
+
46
+ it 'added transition has given values' do
47
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
48
+ dsl.transition from: :from_state, to: :to_state, condition: :some_condition
49
+ transition = dsl.transitions.first
50
+
51
+ expect(transition.from).to eq :from_state
52
+ expect(transition.to).to eq :to_state
53
+ expect(transition.condition).to eq :some_condition
54
+ end
55
+ end
56
+
57
+ context 'Hooks' do
58
+ it 'has #hooks' do
59
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
60
+
61
+ expect(dsl.respond_to?(:hooks)).to be_truthy
62
+ end
63
+
64
+ it 'accepts required parameters in #register' do
65
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
66
+ expect {
67
+ dsl.register hook: :before_transition, handler: :some_func
68
+ }.to_not raise_error
69
+ end
70
+
71
+ it 'accepts optional parameters in #register' do
72
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
73
+ expect {
74
+ dsl.register hook: :before_transition, handler: :some_func, condition: :foo_bar
75
+ }.to_not raise_error
76
+ end
77
+
78
+ it 'has one valid hook after adding one' do
79
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
80
+ dsl.register hook: :after_transition, handler: :some_func
81
+
82
+ expect(dsl.hooks.count).to eq 1
83
+ expect(dsl.hooks.first).to be_a GenericStateMachine::DSL::Hook
84
+ end
85
+
86
+ it 'raises error if invalid hook is used' do
87
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
88
+
89
+ expect {
90
+ dsl.register hook: :some_hook, handler: :some_func
91
+ }.to raise_error GenericStateMachine::Errors::DSLError
92
+ end
93
+ end
94
+
95
+ context 'Starting state' do
96
+ it 'has a starting state after setting it' do
97
+ dsl = GenericStateMachine::DSL::StateMachineDSL.new
98
+ dsl.start from: :some_state
99
+
100
+ expect(dsl.starting).to eq :some_state
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,105 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ require 'simplecov'
17
+ SimpleCov.start
18
+
19
+ require_relative '../generic_state_machine'
20
+
21
+ RSpec.configure do |config|
22
+ # rspec-expectations config goes here. You can use an alternate
23
+ # assertion/expectation library such as wrong or the stdlib/minitest
24
+ # assertions if you prefer.
25
+ config.expect_with :rspec do |expectations|
26
+ # This option will default to `true` in RSpec 4. It makes the `description`
27
+ # and `failure_message` of custom matchers include text for helper methods
28
+ # defined using `chain`, e.g.:
29
+ # be_bigger_than(2).and_smaller_than(4).description
30
+ # # => "be bigger than 2 and smaller than 4"
31
+ # ...rather than:
32
+ # # => "be bigger than 2"
33
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
34
+ end
35
+
36
+ # rspec-mocks config goes here. You can use an alternate test double
37
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
38
+ config.mock_with :rspec do |mocks|
39
+ # Prevents you from mocking or stubbing a method that does not exist on
40
+ # a real object. This is generally recommended, and will default to
41
+ # `true` in RSpec 4.
42
+ mocks.verify_partial_doubles = true
43
+ end
44
+
45
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
46
+ # have no way to turn it off -- the option exists only for backwards
47
+ # compatibility in RSpec 3). It causes shared context metadata to be
48
+ # inherited by the metadata hash of host groups and examples, rather than
49
+ # triggering implicit auto-inclusion in groups with matching metadata.
50
+ config.shared_context_metadata_behavior = :apply_to_host_groups
51
+
52
+ # The settings below are suggested to provide a good initial experience
53
+ # with RSpec, but feel free to customize to your heart's content.
54
+ =begin
55
+ # This allows you to limit a spec run to individual examples or groups
56
+ # you care about by tagging them with `:focus` metadata. When nothing
57
+ # is tagged with `:focus`, all examples get run. RSpec also provides
58
+ # aliases for `it`, `describe`, and `context` that include `:focus`
59
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
60
+ config.filter_run_when_matching :focus
61
+
62
+ # Allows RSpec to persist some state between runs in order to support
63
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
64
+ # you configure your source control system to ignore this file.
65
+ config.example_status_persistence_file_path = "spec/examples.txt"
66
+
67
+ # Limits the available syntax to the non-monkey patched syntax that is
68
+ # recommended. For more details, see:
69
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
70
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
71
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
72
+ config.disable_monkey_patching!
73
+
74
+ # This setting enables warnings. It's recommended, but in some cases may
75
+ # be too noisy due to issues in dependencies.
76
+ config.warnings = true
77
+
78
+ # Many RSpec users commonly either run the entire suite or an individual
79
+ # file, and it's useful to allow more verbose output when running an
80
+ # individual spec file.
81
+ if config.files_to_run.one?
82
+ # Use the documentation formatter for detailed output,
83
+ # unless a formatter has already been configured
84
+ # (e.g. via a command-line flag).
85
+ config.default_formatter = "doc"
86
+ end
87
+
88
+ # Print the 10 slowest examples and example groups at the
89
+ # end of the spec run, to help surface which specs are running
90
+ # particularly slow.
91
+ config.profile_examples = 10
92
+
93
+ # Run specs in random order to surface order dependencies. If you find an
94
+ # order dependency and want to debug it, you can fix the order by providing
95
+ # the seed, which is printed after each run.
96
+ # --seed 1234
97
+ config.order = :random
98
+
99
+ # Seed global randomization in this process using the `--seed` CLI option.
100
+ # Setting this allows you to use `--seed` to deterministically reproduce
101
+ # test failures related to randomization by passing the same `--seed` value
102
+ # as the one that triggered the failure.
103
+ Kernel.srand config.seed
104
+ =end
105
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ describe GenericStateMachine::StateMachine do
6
+ context 'API' do
7
+ let(:gsm) do
8
+ GenericStateMachine::StateMachine.new :start_state
9
+ end
10
+
11
+ it 'has property #current' do
12
+ expect(gsm.respond_to?(:current)).to be_truthy
13
+ end
14
+
15
+ it 'has method #next!' do
16
+ expect(gsm.respond_to?(:next!)).to be_truthy
17
+ end
18
+
19
+ it 'has method #register' do
20
+ expect(gsm.respond_to?(:register)).to be_truthy
21
+ end
22
+
23
+ it 'has method #add' do
24
+ expect(gsm.respond_to?(:add)).to be_truthy
25
+ end
26
+ end
27
+
28
+ context 'Transitions' do
29
+ let(:gsm) do
30
+ machine = GenericStateMachine::StateMachine.new :start_state
31
+ machine.add transition: GenericStateMachine::Transition.new(:start_state, :end_state)
32
+
33
+ machine
34
+ end
35
+
36
+ it 'can add transition' do
37
+ expect { gsm }.to_not raise_error
38
+ end
39
+
40
+ it "can't add invalid transition" do
41
+ expect { gsm.add transition: 'some value' }.to raise_error GenericStateMachine::Errors::StateMachineError
42
+ end
43
+
44
+ it 'next switches current state to ":end_state"' do
45
+ gsm.next!
46
+ expect(gsm.current).to eq :end_state
47
+ end
48
+ end
49
+
50
+ context 'Hooks' do
51
+ let(:gsm) do
52
+ GenericStateMachine::StateMachine.new :start_state
53
+ end
54
+
55
+ it 'can register a proc as handler' do
56
+ expect {
57
+ gsm.register hook: :before_transition, handler: proc { puts 'BEFORE_TRANSITION called' }
58
+ }.to_not raise_error
59
+ end
60
+
61
+ it 'really executes the hook handler' do
62
+ $global_value = 'change me'
63
+ gsm.register hook: :before_transition, handler: proc { $global_value = 'foo bar' }
64
+ # Call private method for testing purposes
65
+ gsm.send :emit!, :before_transition
66
+
67
+ expect($global_value).to eq 'foo bar'
68
+ end
69
+
70
+ it 'can register an instance method as handler' do
71
+ class Tester
72
+ attr_reader :value
73
+ def initialize; @value = 'change me'; end
74
+ def change_value; @value = 'foo bar'; end
75
+ end
76
+
77
+ t = Tester.new
78
+ gsm.register hook: :before_transition, handler: t.method(:change_value)
79
+ # Call private method for testing purposes
80
+ gsm.send :emit!, :before_transition
81
+
82
+ expect(t.value).to eq 'foo bar'
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ describe 'GenericStateMachine::History' do
6
+ context 'Object creation' do
7
+ end
8
+
9
+ context 'Entry items' do
10
+ pending 'Item creation'
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ describe GenericStateMachine::StateMachineFactory do
6
+ context 'Object creation' do
7
+ it 'Creates a factory' do
8
+ expect { GenericStateMachine::StateMachineFactory.new }.to_not raise_error
9
+ end
10
+ end
11
+
12
+ context 'State machine creation' do
13
+ let(:factory) do
14
+ GenericStateMachine::StateMachineFactory
15
+ end
16
+
17
+ it "can't create a GSM w/o starting state" do
18
+ expect {
19
+ factory.create start: nil, transitions: [], hooks: []
20
+ }.to raise_error GenericStateMachine::Errors::GenericStateMachineError,
21
+ "Can't create state machine w/o starting state"
22
+ end
23
+
24
+ it "can't create a GSM w/o transitions" do
25
+ expect {
26
+ factory.create start: :some_start, transitions: [], hooks: []
27
+ }.to raise_error GenericStateMachine::Errors::GenericStateMachineError,
28
+ "Can't create state machine w/o transitions"
29
+ end
30
+
31
+ it 'can create a valid GSM' do
32
+ t = GenericStateMachine::Transition.new :from_state, :to_state
33
+
34
+ expect { factory.create start: :from_state, transitions: [t] }.to_not raise_error
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ describe GenericStateMachine::Transition do
6
+ context 'Object creation' do
7
+ it 'Creates a transition' do
8
+ expect { GenericStateMachine::Transition.new(:from_state, :to_state) }.to_not raise_error
9
+ end
10
+ end
11
+
12
+ context 'Properties' do
13
+ let(:transition) do
14
+ GenericStateMachine::Transition.new(:from_state, :to_state)
15
+ end
16
+
17
+ it 'has #from property' do
18
+ expect(transition.respond_to?(:from)).to be_truthy
19
+ end
20
+
21
+ it 'has #to property' do
22
+ expect(transition.respond_to?(:to)).to be_truthy
23
+ end
24
+
25
+ it 'has #condition property' do
26
+ expect(transition.respond_to?(:condition)).to be_truthy
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GenericStateMachine
4
+ module Errors
5
+ # Basic error class
6
+ class GenericStateMachineError < StandardError; end
7
+ # DSLError indicates problems when using the DSL
8
+ class DSLError < GenericStateMachineError; end
9
+ # StateError indicates problems assigning a state
10
+ class StateError < GenericStateMachineError; end
11
+ # HookError indicates problems using a hook
12
+ class HookError < GenericStateMachineError; end
13
+ # StateMachineError is raised when errors happen inside the state machine
14
+ class StateMachineError < GenericStateMachineError; end
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module GenericStateMachine
5
+ # List available hooks
6
+ AVAILABLE_HOOKS = %i[before_transition after_transition end_reached].freeze
7
+
8
+ ##
9
+ # StateMachine class
10
+ #
11
+ class StateMachine
12
+ attr_reader :current
13
+
14
+ ##
15
+ # Create a new state machine
16
+ # @param [Symbol] start_state The starting state
17
+ # @raise GenericStateMachine::Errors::StateMachineError
18
+ #
19
+ def initialize(start_state)
20
+ raise GenericStateMachine::Errors::StateMachineError, "Can't create state machine w/o start state" if
21
+ start_state.nil? || !start_state.is_a?(Symbol)
22
+
23
+ initialize_hooks
24
+ @transitions = {}
25
+ @current = start_state
26
+ end
27
+
28
+ ##
29
+ # Add transition
30
+ # @param [GenericStateMachine::Transition] transition
31
+ # @raise GenericStateMachine::Errors::StateMachineError
32
+ #
33
+ def add(transition:)
34
+ raise GenericStateMachine::Errors::StateMachineError, "Invalid transition '#{transition}'" if
35
+ transition.nil? || !transition.is_a?(GenericStateMachine::Transition)
36
+
37
+ @transitions[transition.from] = transition
38
+ end
39
+
40
+ ##
41
+ # Switch current state
42
+ # @raise GenericStateMachine::Errors::StateMachineError
43
+ #
44
+ def next!
45
+ raise GenericStateMachine::Errors::StateMachineError, "No transition found for '#{@current}'" unless
46
+ @transitions.key?(@current)
47
+
48
+ # emit :before_transition
49
+ emit! :before_transition
50
+ @current = @transitions[@current].to
51
+ # emit :after_transition
52
+ emit! :after_transition
53
+ end
54
+
55
+ ##
56
+ # Register a new hook
57
+ # @param [Symbol] hook
58
+ # @param [Object] handler The handler called when the hook is reached
59
+ # @raise GenericStateMachine::Errors::StateMachineError
60
+ #
61
+ def register(hook:, handler:)
62
+ raise GenericStateMachine::Errors::StateMachineError, "Invalid hook '#{hook}'" unless
63
+ AVAILABLE_HOOKS.include?(hook) || hook.nil? || !hook.is_a?(Symbol)
64
+ raise GenericStateMachine::Errors::StateMachineError, 'Invalid handler' if
65
+ handler.nil?
66
+
67
+ @hooks[hook] << handler
68
+ end
69
+
70
+ private
71
+
72
+ ##
73
+ # Emit hook
74
+ # @param [Symbol] hook The hook to be emitted
75
+ #
76
+ def emit!(hook)
77
+ @hooks[hook].each(&:call)
78
+ end
79
+
80
+ ##
81
+ # Initialize hooks hash
82
+ #
83
+ def initialize_hooks
84
+ @hooks = {}
85
+
86
+ GenericStateMachine::AVAILABLE_HOOKS.each do |hook|
87
+ @hooks[hook] = []
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module GenericStateMachine
5
+ ##
6
+ # Factory class creating StateMachine instances
7
+ #
8
+ class StateMachineFactory
9
+ class << self
10
+ ##
11
+ # Create a StateMachine instance
12
+ # @param [Symbol] start The starting state
13
+ # @param [Array] transitions All available transitions
14
+ # @param [Array] hooks Optional collection of hooks
15
+ # @raise GenericStateMachine::Errors::GenericStateMachineError
16
+ #
17
+ def create(start:, transitions:, hooks: [])
18
+ raise GenericStateMachine::Errors::GenericStateMachineError, "Can't create state machine w/o starting state" if
19
+ start.nil? || !start.is_a?(Symbol)
20
+ raise GenericStateMachine::Errors::GenericStateMachineError, "Can't create state machine w/o transitions" unless
21
+ transitions.is_a?(Array)
22
+ raise GenericStateMachine::Errors::GenericStateMachineError, "Can't create state machine w/o transitions" if
23
+ transitions.empty?
24
+
25
+ _validate_transitions transitions
26
+ _validate_start_state start, transitions
27
+ _validate_hooks(hooks) unless hooks.empty?
28
+
29
+ GenericStateMachine::StateMachine.new start
30
+ end
31
+
32
+ private
33
+
34
+ ##
35
+ # Helper method validating if given start state has a transition
36
+ # raise GenericStateMachine::Errors::GenericStateMachineError if no transition is found
37
+ #
38
+ def _validate_start_state(start, transitions)
39
+ valid = false
40
+
41
+ transitions.each do |t|
42
+ if t.from == start
43
+ valid = true
44
+ break
45
+ end
46
+ end
47
+
48
+ raise GenericStateMachine::Errors::GenericStateMachineError, "State '#{start}' isn't a valid start state" unless
49
+ valid
50
+ end
51
+
52
+ ##
53
+ # Helper method validating if all elements are transitions
54
+ # raise GenericStateMachine::Errors::GenericStateMachineError if one of the elements is no Transition instance
55
+ #
56
+ def _validate_transitions(transitions)
57
+ transitions.each do |t|
58
+ raise GenericStateMachine::Errors::GenericStateMachineError, "Element '#{t}' isn't a transition" unless
59
+ t.is_a?(GenericStateMachine::Transition)
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Helper method validating if all elements are hooks
65
+ # raise GenericStateMachine::Errors::GenericStateMachineError if one of the elements is no Hook instance
66
+ #
67
+ def _validate_hooks(hooks)
68
+ hooks.each do |t|
69
+ raise GenericStateMachine::Errors::GenericStateMachineError, "Element '#{t}' isn't a hook" unless
70
+ t.is_a?(GenericStateMachine::Hook)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module GenericStateMachine
5
+ ##
6
+ # Transition represents one transition of the state
7
+ #
8
+ class Transition
9
+ attr_reader :from, :to, :condition
10
+
11
+ def initialize(from, to, condition = nil)
12
+ @from = from
13
+ @to = to
14
+ @condition = condition
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: generic-state-machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Stätter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Provides the possibility to create simple state machines
14
+ email: thomas.staetter@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - dsl/dsl.rb
20
+ - dsl/state_machine_dsl.rb
21
+ - spec/dsl/describe_spec.rb
22
+ - spec/spec_helper.rb
23
+ - spec/unit/generic_state_machine_spec.rb
24
+ - spec/unit/history_spec.rb
25
+ - spec/unit/state_machine_factory_spec.rb
26
+ - spec/unit/transition_spec.rb
27
+ - state_machine/errors.rb
28
+ - state_machine/state_machine.rb
29
+ - state_machine/state_machine_factory.rb
30
+ - state_machine/transition.rb
31
+ homepage: https://github.com/tstaetter/generic-state-machine
32
+ licenses: []
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.7.1
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.1.2
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Generic state machine
53
+ test_files:
54
+ - spec/spec_helper.rb
55
+ - spec/unit/state_machine_factory_spec.rb
56
+ - spec/unit/history_spec.rb
57
+ - spec/unit/transition_spec.rb
58
+ - spec/unit/generic_state_machine_spec.rb
59
+ - spec/dsl/describe_spec.rb