composable_state_machine 1.0.2
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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.simplecov +4 -0
- data/.travis.yml +8 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +352 -0
- data/Rakefile +19 -0
- data/assets/class-diagram.yuml +24 -0
- data/assets/uml-class-diagram.png +0 -0
- data/composable_state_machine.gemspec +35 -0
- data/lib/composable_state_machine.rb +45 -0
- data/lib/composable_state_machine/behaviors.rb +48 -0
- data/lib/composable_state_machine/callback_runner.rb +19 -0
- data/lib/composable_state_machine/callbacks.rb +56 -0
- data/lib/composable_state_machine/default_callback_runner.rb +16 -0
- data/lib/composable_state_machine/invalid_event.rb +7 -0
- data/lib/composable_state_machine/invalid_transition.rb +7 -0
- data/lib/composable_state_machine/invalid_trigger.rb +7 -0
- data/lib/composable_state_machine/machine.rb +21 -0
- data/lib/composable_state_machine/machine_with_external_state.rb +41 -0
- data/lib/composable_state_machine/model.rb +55 -0
- data/lib/composable_state_machine/transitions.rb +73 -0
- data/lib/composable_state_machine/version.rb +3 -0
- data/spec/integration/auto_update_state_spec.rb +38 -0
- data/spec/integration/instance_callbacks_spec.rb +47 -0
- data/spec/integration/leave_callbacks_spec.rb +60 -0
- data/spec/integration/leave_callbacks_with_composition_spec.rb +68 -0
- data/spec/lib/composable_state_machine/behaviors_spec.rb +83 -0
- data/spec/lib/composable_state_machine/callback_runner_spec.rb +54 -0
- data/spec/lib/composable_state_machine/callbacks_spec.rb +106 -0
- data/spec/lib/composable_state_machine/machine_spec.rb +25 -0
- data/spec/lib/composable_state_machine/machine_with_external_state_spec.rb +97 -0
- data/spec/lib/composable_state_machine/model_spec.rb +76 -0
- data/spec/lib/composable_state_machine/transitions_spec.rb +77 -0
- data/spec/lib/composable_state_machine_spec.rb +53 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/delegation.rb +196 -0
- metadata +218 -0
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
desc 'Default: run the specs'
|
6
|
+
task :default do
|
7
|
+
system('bundle exec rspec')
|
8
|
+
end
|
9
|
+
|
10
|
+
desc 'Run the specs'
|
11
|
+
task :spec => :default
|
12
|
+
|
13
|
+
desc 'Open an irb session preloaded with this library'
|
14
|
+
task :console do
|
15
|
+
exec 'irb -rubygems -I lib -r composable_state_machine.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
YARD::Rake::YardocTask.new do |_|
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
// composable_state_machine class diagram
|
2
|
+
[≺≺interface≻≻;ICallbackRunner||run_state_machine_callback(callback){bg:green}]
|
3
|
+
[≺≺interface≻≻;ICallable||call(*)]
|
4
|
+
[≺≺interface≻≻;IMachine||trigger(event);==(other_state){bg:orange}]
|
5
|
+
[Machine|state|{bg:orange}]
|
6
|
+
[Model|initial_state|transition(event...);run_callbacks();run_callbacks_for(){bg:red}]
|
7
|
+
[Transitions||on(event...);transition(event...){bg:blue}]
|
8
|
+
[Behaviors||on(behavior...){bg:green}]
|
9
|
+
[Callbacks||on(trigger...){bg:green}]
|
10
|
+
[≺≺interface≻≻;ICallable]^-.-[Behaviors]
|
11
|
+
[≺≺interface≻≻;ICallable]^-.-[Callbacks]
|
12
|
+
[≺≺interface≻≻;ICallbackRunner]-.-> called[≺≺interface≻≻;ICallable]
|
13
|
+
[≺≺interface≻≻;ICallbackRunner]^-.-[≺≺mixin≻≻;CallbackRunner{bg:green}]
|
14
|
+
[≺≺interface≻≻;ICallbackRunner]^-.-[DefaultCallbackRunner{bg:green}]
|
15
|
+
[≺≺interface≻≻;IMachine]^-.-[MachineWithExternalState]
|
16
|
+
[Model]-1> default runner[≺≺interface≻≻;ICallbackRunner]
|
17
|
+
[Model]-1>[Transitions]
|
18
|
+
[Model]-1>[Behaviors]
|
19
|
+
[Behaviors]-*> by behavior[Callbacks]
|
20
|
+
[Callbacks]-*> by trigger[≺≺interface≻≻;ICallable]
|
21
|
+
[MachineWithExternalState{bg:orange}]^[Machine]
|
22
|
+
[MachineWithExternalState]-1>[Model]
|
23
|
+
[MachineWithExternalState]-1>[≺≺interface≻≻;ICallbackRunner]
|
24
|
+
[MachineWithExternalState]-2> state reader/writer[≺≺interface≻≻;ICallable]
|
Binary file
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'composable_state_machine/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'composable_state_machine'
|
8
|
+
spec.version = ComposableStateMachine::VERSION
|
9
|
+
spec.authors = ['Simeon Simeonov']
|
10
|
+
spec.email = ['sim@swoop.com']
|
11
|
+
spec.description = %q{Small, fast and flexible state machines using composition.}
|
12
|
+
spec.summary = %q{The composition patterns in this implementation make it easy to circumvent the limitations of other state machine gems. A single state machine model can be shared across thousands of machine instances without the usual overhead. An object can have more than one state machine. States and events can be any objects, not just strings or symbols. Events can take optional parameters. Different state machine models can fire different types of callbacks. Adding new types of callbacks takes a couple of lines of code. Explicit callback runners enable easy decoration for logging, caching or other purposes. No external dependencies and 100% code coverage.}
|
13
|
+
spec.homepage = 'https://github.com/swoop-inc/composable_state_machine'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = %w(lib)
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'simplecov'
|
25
|
+
spec.add_development_dependency 'coveralls'
|
26
|
+
spec.add_development_dependency 'awesome_print'
|
27
|
+
spec.add_development_dependency 'yard'
|
28
|
+
spec.add_development_dependency 'redcarpet'
|
29
|
+
|
30
|
+
if RUBY_PLATFORM =~ /darwin/ && RUBY_VERSION =~ /^2/
|
31
|
+
spec.add_development_dependency 'guard'
|
32
|
+
spec.add_development_dependency 'guard-rspec'
|
33
|
+
spec.add_development_dependency 'growl'
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Dir['lib/composable_state_machine/**/*.rb'].each { |f| require File.expand_path(f) }
|
2
|
+
|
3
|
+
# @author {https://github.com/ssimeonov Simeon Simeonov}, {http://swoop.com Swoop, Inc.}
|
4
|
+
#
|
5
|
+
# For examples, see the {file:README.html README}.
|
6
|
+
module ComposableStateMachine
|
7
|
+
|
8
|
+
# Creates a state machine model.
|
9
|
+
#
|
10
|
+
# State machine models are immutable and can be shared across many state machine instances.
|
11
|
+
#
|
12
|
+
# @param [Hash] options the options to create a model with.
|
13
|
+
# @option options [Hash, Transitions] :transitions State machine transitions. A {Transitions} object will be created if a Hash is provided.
|
14
|
+
# @option options [Hash, Behaviors] :behaviors State machine behaviors. A {Behaviors} object will be created if a Hash is provided. If omitted, a high-performance behaviors stub will be used.
|
15
|
+
# @option options [Object] :callback_runner (DefaultCallbackRunner) Object whose #run_state_machine_callback method will be used to execute behavior callbacks. {DefaultCallbackRunner} simply calls a Proc's #call method.
|
16
|
+
# @option options [Object] :initial_state (nil) Default initial state for the machine. This can be overriden by a machine instance.
|
17
|
+
# @option options [Object] :model_factory (Model) The object whose #new method will be called to create a model.
|
18
|
+
def self.model(options)
|
19
|
+
options = options.dup
|
20
|
+
unless options[:transitions].respond_to?(:transition)
|
21
|
+
options[:transitions] = Transitions.new(options[:transitions] || {})
|
22
|
+
end
|
23
|
+
unless options[:behaviors].respond_to?(:call)
|
24
|
+
options[:behaviors] = Behaviors.new(options[:behaviors] || {})
|
25
|
+
end
|
26
|
+
(options[:model_factory] || Model).new(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a state machine from a model.
|
30
|
+
#
|
31
|
+
# The variable number of arguments is driven by the difference in initialization APIs between different machines.
|
32
|
+
#
|
33
|
+
# The last argument must be an options Hash. It may have at least the following options:
|
34
|
+
#
|
35
|
+
# - :callback_runner [Object] (DefaultCallbackRunner) Object whose #run_state_machine_callback method will be used to execute behavior callbacks. {DefaultCallbackRunner} simply calls a Proc's #call method. When you want to execute callbacks in the context of an object, include the {CallbackRunner} mixin in the object and then pass the object as the callback runner here.
|
36
|
+
# - :state [Object] (model#initial_state) State the machine is in.
|
37
|
+
# - :model_factory [Object] ({Machine}) The object whose #new method will be called to create the machine.
|
38
|
+
#
|
39
|
+
# @param [Model, ...] model the state machine model.
|
40
|
+
# @param [any] args additional arguments passed to the model.
|
41
|
+
def self.machine(model, *args)
|
42
|
+
options = args.last
|
43
|
+
(options[:machine_factory] || Machine).new(model, *args)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# Defines the behaviors of a state machine.
|
4
|
+
class Behaviors
|
5
|
+
# Creates a {Behaviors} object.
|
6
|
+
#
|
7
|
+
# @param [Hash<behavior, Hash<trigger, callback>>] behaviors triggers and callbacks per behavior. The triggers and callbacks can be provided as an object the responds to #call or they can be provided as a Hash<trigger, callback>. In the latter case, the callbacks factory will be used to create an object that would manage the callbacks for the behavior.
|
8
|
+
# @param [Object] callbacks_factory object whose #new method will be called with a Hash<trigger, callback>.
|
9
|
+
def initialize(behaviors = {}, callbacks_factory = Callbacks)
|
10
|
+
@callbacks_factory = callbacks_factory
|
11
|
+
@behaviors = behaviors.reduce({}) do |memo, (behavior, value)|
|
12
|
+
handler = value.respond_to?(:call) ? value : @callbacks_factory.new(value)
|
13
|
+
memo[behavior] = handler
|
14
|
+
memo
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds callbacks for a behavior.
|
19
|
+
#
|
20
|
+
# Selects the callback manager for the behavior and forwards to its #on method.
|
21
|
+
#
|
22
|
+
# @param [Object] behavior the behavior
|
23
|
+
# @param [Array<Object>] args parameters to pass to the callback manager's #on method.
|
24
|
+
#
|
25
|
+
# @return [self] for chaining
|
26
|
+
def on(behavior, *args, &block)
|
27
|
+
(@behaviors[behavior] ||= @callbacks_factory.new).on(*args, &block)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Runs callbacks for a behavior with a runner.
|
32
|
+
#
|
33
|
+
# Selects the callback manager for the behavior and forwards to its #call method.
|
34
|
+
#
|
35
|
+
# @param [Object] runner the runner
|
36
|
+
# @param [Object] behavior the behavior
|
37
|
+
# @param [Array<Object>] args parameters to pass to the callback manager's #call method.
|
38
|
+
#
|
39
|
+
# @return [self] for chaining
|
40
|
+
def call(runner, behavior, *args)
|
41
|
+
@behaviors[behavior].tap do |handler|
|
42
|
+
handler.call(runner, *args) if handler
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# Mixin module that runs callbacks with self pointing to the object the module is included in.
|
4
|
+
module CallbackRunner
|
5
|
+
# Runs a callback with self pointing to the object the module is included in.
|
6
|
+
#
|
7
|
+
# @param [Proc, Method, UnboundMethod, ...] callback the callback. Unbound methods will be bound to the object the mixin is included in.
|
8
|
+
# @param [Array<Object>] args parameters to pass to the callback.
|
9
|
+
#
|
10
|
+
# @return [Object] the result of the callback
|
11
|
+
def run_state_machine_callback(callback, *args)
|
12
|
+
if callback.respond_to?(:bind)
|
13
|
+
callback = callback.bind(self)
|
14
|
+
end
|
15
|
+
instance_exec(*args, &callback)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# Manages callbacks for a behavior.
|
4
|
+
class Callbacks
|
5
|
+
# Creates a {Callbacks} object.
|
6
|
+
#
|
7
|
+
# @param [Hash<trigger, callback(s)>] callbacks maps triggers to zero or more callbacks.
|
8
|
+
def initialize(callbacks = {})
|
9
|
+
@callbacks = Hash.new { |hash, key| hash[key] = [] }
|
10
|
+
callbacks.each_pair do |trigger, proc|
|
11
|
+
if proc.respond_to?(:each)
|
12
|
+
proc.each do |callback|
|
13
|
+
on(trigger, callback)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
on(trigger, proc)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds a callback for a trigger.
|
22
|
+
#
|
23
|
+
# @param [Object] trigger the callback trigger
|
24
|
+
# @param [Proc, Method, ...] proc an object responding to #call. If non-nil, it will be given precedence to #block
|
25
|
+
# @param [block] block an optional block implementing the callback
|
26
|
+
#
|
27
|
+
# @return [self] for chaining
|
28
|
+
def on(trigger, proc = nil, &block)
|
29
|
+
@callbacks[trigger] << (proc || block)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Runs the callbacks for a trigger with a runner.
|
34
|
+
#
|
35
|
+
# Runs the callbacks for the :any trigger for every trigger.
|
36
|
+
#
|
37
|
+
# @param [Object] runner the runner
|
38
|
+
# @param [Object] trigger the callback trigger
|
39
|
+
# @param [Array<Object>] args parameters to pass to the callbacks' #call methods.
|
40
|
+
#
|
41
|
+
# @return [self] for chaining
|
42
|
+
def call(runner, trigger, *args)
|
43
|
+
if trigger == :any
|
44
|
+
raise InvalidTrigger.new(':any is not a valid trigger')
|
45
|
+
end
|
46
|
+
@callbacks[trigger].each do |callback|
|
47
|
+
runner.run_state_machine_callback(callback, *args)
|
48
|
+
end
|
49
|
+
@callbacks[:any].each do |callback|
|
50
|
+
runner.run_state_machine_callback(callback, *args)
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# Default callback runner that executes callbacks in their current binding.
|
4
|
+
class DefaultCallbackRunner
|
5
|
+
# Runs a callback in its current binding.
|
6
|
+
#
|
7
|
+
# @param [Proc, Method, ...] callback the callback, which must respond to #call.
|
8
|
+
# @param [Array<Object>] args parameters to pass to the callback.
|
9
|
+
#
|
10
|
+
# @return [Object] the result of the callback
|
11
|
+
def self.run_state_machine_callback(callback, *args)
|
12
|
+
callback.call(*args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'machine_with_external_state'
|
2
|
+
|
3
|
+
module ComposableStateMachine
|
4
|
+
|
5
|
+
# Machine with its own state.
|
6
|
+
class Machine < MachineWithExternalState
|
7
|
+
attr_reader :state
|
8
|
+
|
9
|
+
# Creates a machine. Delegates to {MachineWithExternalState#initialize} passing method(:state) & method(:state=) as the state reader and writer.
|
10
|
+
def initialize(model, options = {})
|
11
|
+
super(model, method(:state), method(:state=), options)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def state=(new_state)
|
17
|
+
@state = new_state
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# State machine instance that manages its state via a reader and writer objects.
|
4
|
+
class MachineWithExternalState
|
5
|
+
# Creates an instance.
|
6
|
+
#
|
7
|
+
# @param [Model, ...] model the model for the machine
|
8
|
+
# @param [Proc, Method, ...] state_reader object whose #call method will return the current state
|
9
|
+
# @param [Proc, Method, ...] state_writer object whose #call method will be called with the new state after a transition
|
10
|
+
# @param [Hash] options the options to create the machine with.
|
11
|
+
# @option options [Object] :state (model#initial_state) State of the machine.
|
12
|
+
# @option options [Object] :callback_runner (model#callback_runner) Object whose #run_state_machine_callback method will be used to execute behavior callbacks.
|
13
|
+
def initialize(model, state_reader, state_writer, options = {})
|
14
|
+
@model = model
|
15
|
+
@state_reader = state_reader
|
16
|
+
@state_writer = state_writer
|
17
|
+
@callback_runner = options[:callback_runner] || model.callback_runner
|
18
|
+
initial_state = options[:state] || model.initial_state
|
19
|
+
@state_writer.call(initial_state)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Executes the transition and behaviors associated with an event.
|
23
|
+
#
|
24
|
+
# @param [Object] event the event
|
25
|
+
# @param [Array<Object>] args event arguments
|
26
|
+
#
|
27
|
+
# @return [Object, nil] the result of model#transition
|
28
|
+
def trigger(event, *args)
|
29
|
+
current_state = @state_reader.call
|
30
|
+
@model.transition(current_state, event, args, @callback_runner, &@state_writer)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Checks whether the state of the machine is equal to another state
|
34
|
+
#
|
35
|
+
# @return [TrueClass, FalseClass]
|
36
|
+
def ==(other_state)
|
37
|
+
@state_reader.call == other_state
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# An immutable state machine model that can be shared across many instances.
|
4
|
+
class Model
|
5
|
+
attr_reader :initial_state
|
6
|
+
attr_reader :callback_runner
|
7
|
+
|
8
|
+
# Creates a state machine model.
|
9
|
+
#
|
10
|
+
# @param [Hash] options the options to create a model with.
|
11
|
+
# @option options [Hash, Transitions] :transitions State machine transitions. A {Transitions} object will be created if a Hash is provided.
|
12
|
+
# @option options [Hash, Behaviors] :behaviors State machine behaviors. A {Behaviors} object will be created if a Hash is provided. If omitted, a high-performance behaviors stub will be used.
|
13
|
+
# @option options [Object] :initial_state (nil) Default initial state for the machine. This can be overriden by a machine instance.
|
14
|
+
# @option options [Object] :callback_runner (DefaultCallbackRunner) Object whose #run_state_machine_callback method will be used to execute behavior callbacks. {DefaultCallbackRunner} simply calls a Proc's #call method.
|
15
|
+
def initialize(options = {})
|
16
|
+
@initial_state = options[:initial_state]
|
17
|
+
@transitions = options[:transitions]
|
18
|
+
@behaviors = options[:behaviors] || proc {}
|
19
|
+
@callback_runner = options[:callback_runner] || DefaultCallbackRunner
|
20
|
+
end
|
21
|
+
|
22
|
+
# Performs a transition of the machine, executing behaviors as needed.
|
23
|
+
#
|
24
|
+
# @param current_state [Object] the current state of the machine
|
25
|
+
# @param event [Object] the event the machine has received
|
26
|
+
# @param arguments [Enumerable] ([]) any arguments related to the event.
|
27
|
+
# @param callback_runner [Object] ({Model#callback_runner}) the runner with which to execute callbacks
|
28
|
+
#
|
29
|
+
# @yield [Object] the new state of the machine, if a transition happened
|
30
|
+
#
|
31
|
+
# @return [Object] the new state of the machine, if a transition happened
|
32
|
+
# @return [nil] if no transition happened
|
33
|
+
def transition(current_state, event, arguments = [], callback_runner = nil)
|
34
|
+
@transitions.transition(current_state, event).tap do |new_state|
|
35
|
+
if new_state && new_state != current_state
|
36
|
+
callback_runner ||= @callback_runner
|
37
|
+
run_callbacks(callback_runner, current_state, event, new_state, arguments)
|
38
|
+
yield new_state if block_given?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Runs the callbacks for all behaviors for a state transition
|
44
|
+
def run_callbacks(callback_runner, current_state, event, new_state, arguments, &block)
|
45
|
+
run_callbacks_for(callback_runner, :enter, new_state,
|
46
|
+
current_state, event, new_state, *arguments)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Runs the callbacks for one behavior for a start transition
|
50
|
+
def run_callbacks_for(callback_runner, behavior, *args)
|
51
|
+
@behaviors.call(callback_runner, behavior, *args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module ComposableStateMachine
|
2
|
+
|
3
|
+
# Defines the transitions a state machine can make.
|
4
|
+
class Transitions
|
5
|
+
# Creates a {Transitions} object.
|
6
|
+
#
|
7
|
+
# @note While nil is a valid state to transition from, it is not a valid state to transition to.
|
8
|
+
#
|
9
|
+
# @param (Hash<event, Hash<from_state, to_state>>) transitions transitions Hash mapping events to a Hash from -> to state transitions
|
10
|
+
def initialize(transitions = {})
|
11
|
+
@transitions_for = transitions
|
12
|
+
validate_transitions
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds to the transitions for an event.
|
16
|
+
#
|
17
|
+
# @param [Object] event event causing the transition
|
18
|
+
# @param [Hash<from_state, to_state>] transitions transitions to the added to the transitions for the event.
|
19
|
+
#
|
20
|
+
# @return [self] for chaining
|
21
|
+
def on(event, transitions)
|
22
|
+
(@transitions_for[event] ||= {}).tap do |transitions_for_event|
|
23
|
+
transitions_for_event.merge!(transitions)
|
24
|
+
validate_transitions_for_event(event, transitions_for_event)
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks the transition map for a valid transition from a state given an event
|
30
|
+
#
|
31
|
+
# @param [Object] state the state the machine is in
|
32
|
+
# @param [Object] event event causing the transition
|
33
|
+
#
|
34
|
+
# @return [Object] new state for the machine, if a transition can occur
|
35
|
+
# @return [nil] if a transition cannot happen with this event
|
36
|
+
#
|
37
|
+
# @raise [InvalidEvent] if an unknown event is provided
|
38
|
+
def transition(state, event)
|
39
|
+
transitions_for_event = @transitions_for[event]
|
40
|
+
unless transitions_for_event
|
41
|
+
raise InvalidEvent.new("invalid event", event, state)
|
42
|
+
end
|
43
|
+
transitions_for_event[state]
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array<Object>] events for the machine
|
47
|
+
def events
|
48
|
+
@transitions_for.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Array<Object>] the states of the machine
|
52
|
+
def states
|
53
|
+
events.map { |e| @transitions_for[e].to_a }.flatten.uniq
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def validate_transitions
|
59
|
+
@transitions_for.each_pair do |event, transitions|
|
60
|
+
validate_transitions_for_event(event, transitions)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_transitions_for_event(event, transitions)
|
65
|
+
transitions.each_pair do |from, to|
|
66
|
+
if to.nil?
|
67
|
+
raise InvalidTransition.new("transition to nil from #{from.inspect} for #{event.inspect} event")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|