generic-state-machine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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