generic-state-machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/dsl/dsl.rb +55 -0
- data/dsl/state_machine_dsl.rb +72 -0
- data/spec/dsl/describe_spec.rb +103 -0
- data/spec/spec_helper.rb +105 -0
- data/spec/unit/generic_state_machine_spec.rb +85 -0
- data/spec/unit/history_spec.rb +12 -0
- data/spec/unit/state_machine_factory_spec.rb +37 -0
- data/spec/unit/transition_spec.rb +29 -0
- data/state_machine/errors.rb +16 -0
- data/state_machine/state_machine.rb +91 -0
- data/state_machine/state_machine_factory.rb +75 -0
- data/state_machine/transition.rb +17 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -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
|
data/dsl/dsl.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|