finite_machine 0.0.1 → 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.
data/Rakefile CHANGED
@@ -1 +1,41 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Run all specs'
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
9
+ end
10
+
11
+ namespace :spec do
12
+ desc 'Run unit specs'
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
15
+ end
16
+
17
+ desc 'Run integration specs'
18
+ RSpec::Core::RakeTask.new(:integration) do |task|
19
+ task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
20
+ end
21
+ end
22
+
23
+ rescue LoadError
24
+ %w[spec spec:unit spec:integration].each do |name|
25
+ task name do
26
+ $stderr.puts "In order to run #{name}, do `gem install rspec`"
27
+ end
28
+ end
29
+ end
30
+
31
+ desc 'Run all specs'
32
+ task ci: %w[ spec ]
33
+
34
+ desc 'Load gem inside irb console'
35
+ task :console do
36
+ require 'irb'
37
+ require 'irb/completion'
38
+ require File.join(__FILE__, '../lib/finite_machine')
39
+ ARGV.clear
40
+ IRB.start
41
+ end
@@ -1,9 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ require "thread"
4
+ require "sync"
5
+
1
6
  require "finite_machine/version"
7
+ require "finite_machine/threadable"
8
+ require "finite_machine/callable"
9
+ require "finite_machine/event"
10
+ require "finite_machine/transition"
11
+ require "finite_machine/dsl"
12
+ require "finite_machine/state_machine"
13
+ require "finite_machine/subscribers"
14
+ require "finite_machine/observer"
2
15
 
3
16
  module FiniteMachine
4
17
 
5
- def self.create(&block)
6
- StateMachine.new(&block)
18
+ DEFAULT_STATE = :none
19
+
20
+ DEFAULT_EVENT_NAME = :init
21
+
22
+ ANY_STATE = :any
23
+
24
+ ANY_EVENT = :any
25
+
26
+ # Returned when transition has successfully performed
27
+ SUCCEEDED = 1
28
+
29
+ # Returned when transition is cancelled in callback
30
+ CANCELLED = 2
31
+
32
+ # Returned when transition has not changed the state
33
+ NOTRANSITION = 3
34
+
35
+ # When transition between states is invalid
36
+ TransitionError = Class.new(::StandardError)
37
+
38
+ InvalidEventError = Class.new(::NoMethodError)
39
+
40
+ # Raised when a callback is defined with invalid name
41
+ InvalidCallbackNameError = Class.new(::StandardError)
42
+
43
+ Environment = Struct.new(:target)
44
+
45
+ # TODO: this should instantiate system not the state machine
46
+ # and then delegate calls to StateMachine instance etc...
47
+ def self.define(*args, &block)
48
+ StateMachine.new(*args, &block)
7
49
  end
8
50
 
9
51
  end # FiniteMachine
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # A generic interface for executing strings, symbol methods or procs.
6
+ class Callable
7
+
8
+ attr_reader :object
9
+
10
+ # Initialize a Callable
11
+ #
12
+ # @param [Symbol, String, Proc] object
13
+ # the callable object
14
+ #
15
+ # @api public
16
+ def initialize(object)
17
+ @object = object
18
+ end
19
+
20
+ # Invert callable
21
+ #
22
+ # @api public
23
+ def invert
24
+ lambda { |*args, &block| !self.call(*args, &block) }
25
+ end
26
+
27
+ # Execute action
28
+ #
29
+ # @param [Object] target
30
+ #
31
+ # @api public
32
+ def call(env, *args, &block)
33
+ target = env.target
34
+ case object
35
+ when Symbol
36
+ target.__send__(@object.to_sym)
37
+ when String
38
+ value = eval "lambda { #{@object} }"
39
+ target.instance_exec(&value)
40
+ when ::Proc
41
+ if object.arity >= 1
42
+ object.call(target, *args)
43
+ else
44
+ object.call
45
+ end
46
+ else
47
+ raise ArgumentError, "Unknown callable #{@object}"
48
+ end
49
+ end
50
+ end # Callable
51
+ end # FiniteMachine
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ class GenericDSL
6
+ class << self
7
+ # @api private
8
+ attr_accessor :top_level
9
+ end
10
+
11
+ def initialize(machine)
12
+ @machine = machine
13
+ end
14
+
15
+ def method_missing(method_name, *args, &block)
16
+ if @machine.respond_to?(method_name)
17
+ @machine.send(method_name, *args, &block)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def call(&block)
24
+ instance_eval(&block)
25
+ # top_level.instance_eval(&block)
26
+ end
27
+ end # GenericDSL
28
+
29
+ class DSL < GenericDSL
30
+
31
+ attr_reader :machine
32
+
33
+ attr_reader :defer
34
+
35
+ attr_reader :initial_event
36
+
37
+ def initialize(machine)
38
+ super(machine)
39
+ machine.state = FiniteMachine::DEFAULT_STATE
40
+ @defer = true
41
+ end
42
+
43
+ # Define initial state
44
+ #
45
+ # @params [String, Hash] value
46
+ #
47
+ # @api public
48
+ def initial(value)
49
+ if value.is_a?(String) || value.is_a?(Symbol)
50
+ state, name = value, FiniteMachine::DEFAULT_EVENT_NAME
51
+ @defer = false
52
+ else
53
+ state = value[:state]
54
+ name = value.has_key?(:event) ? value[:event] : FiniteMachine::DEFAULT_EVENT_NAME
55
+ @defer = value[:defer] || true
56
+ end
57
+ @initial_event = name
58
+ event = proc { event name, from: FiniteMachine::DEFAULT_STATE, to: state }
59
+ machine.events.call(&event)
60
+ end
61
+
62
+ def target(value)
63
+ machine.env.target = value
64
+ end
65
+
66
+ # Define terminal state
67
+ #
68
+ # @api public
69
+ def terminal(value)
70
+ machine.final_state = value
71
+ end
72
+
73
+ # Define state machine events
74
+ #
75
+ # @api public
76
+ def events(&block)
77
+ machine.events.call(&block)
78
+ end
79
+
80
+ # Define state machine callbacks
81
+ #
82
+ # @api public
83
+ def callbacks(&block)
84
+ machine.observer.call(&block)
85
+ end
86
+
87
+ # Error handler that throws exception when machine is in illegal state
88
+ #
89
+ # @api public
90
+ def error
91
+ end
92
+ end # DSL
93
+
94
+ class EventsDSL < GenericDSL
95
+
96
+ attr_reader :machine
97
+
98
+ def initialize(machine)
99
+ super(machine)
100
+ end
101
+
102
+ # Create event and associate transition
103
+ #
104
+ # @api public
105
+ def event(name, attrs = {}, &block)
106
+ _transition = Transition.new(machine, attrs.merge!(name: name))
107
+ add_transition(_transition)
108
+ define_event(_transition)
109
+ end
110
+
111
+ # Add transition
112
+ #
113
+ # @param [Transition] _transition
114
+ #
115
+ # @api private
116
+ def add_transition(_transition)
117
+ _transition.from.each do |from|
118
+ machine.transitions[_transition.name][from] = _transition.to || from
119
+ end
120
+ end
121
+
122
+ # Define event
123
+ #
124
+ # @param [String] name
125
+ # @param [Transition] _transition
126
+ #
127
+ # @api private
128
+ def define_event(_transition)
129
+ machine.class.__send__ :define_method, _transition.name do |*args, &block|
130
+ transition(_transition, *args, &block)
131
+ end
132
+ end
133
+ end # EventsDSL
134
+ end # FiniteMachine
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # A class responsible for event notification
6
+ class Event
7
+ include Threadable
8
+
9
+ MESSAGE = :trigger
10
+
11
+ # Event state
12
+ attr_threadsafe :state
13
+
14
+ # Event type
15
+ attr_threadsafe :type
16
+
17
+ # Data associated with the event
18
+ attr_threadsafe :data
19
+
20
+ # Transition associated with the event
21
+ attr_threadsafe :transition
22
+
23
+ def initialize(state, transition, *data, &block)
24
+ @state = state
25
+ @transition = transition
26
+ @data = *data
27
+ @type = self.class.event_name
28
+ end
29
+
30
+ def notify(subscriber, *args, &block)
31
+ if subscriber.respond_to? MESSAGE
32
+ subscriber.__send__(MESSAGE, self, *args, &block)
33
+ end
34
+ end
35
+
36
+ class Anystate < Event; end
37
+
38
+ class Enterstate < Anystate; end
39
+
40
+ class Transitionstate < Anystate; end
41
+
42
+ class Exitstate < Anystate; end
43
+
44
+ class Anyaction < Event; end
45
+
46
+ class Enteraction < Anyaction; end
47
+
48
+ class Transitionaction < Anyaction; end
49
+
50
+ class Exitaction < Anyaction; end
51
+
52
+ EVENTS = Anystate, Enterstate, Transitionstate, Exitstate,
53
+ Anyaction, Enteraction, Transitionaction, Exitaction
54
+
55
+ def self.event_name
56
+ name.split('::').last.downcase.to_sym
57
+ end
58
+
59
+ EVENTS.each do |event|
60
+ (class << self; self; end).class_eval do
61
+ define_method(event.event_name) { event.event_name }
62
+ end
63
+ end
64
+
65
+ end # Event
66
+ end # FiniteMachine
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # A class responsible for observing state changes
6
+ class Observer
7
+ include Threadable
8
+
9
+ # The current state machine
10
+ attr_threadsafe :machine
11
+
12
+ # The hooks to trigger around the transition lifecycle.
13
+ attr_threadsafe :hooks
14
+
15
+ # Initialize an Observer
16
+ #
17
+ # @api public
18
+ def initialize(machine)
19
+ @machine = machine
20
+ @machine.subscribe(self)
21
+
22
+ @hooks = Hash.new { |events_hash, event_type|
23
+ events_hash[event_type] = Hash.new { |state_hash, name|
24
+ state_hash[name] = []
25
+ }
26
+ }
27
+ end
28
+
29
+ # Evaluate in current context
30
+ #
31
+ # @api private
32
+ def call(&block)
33
+ instance_eval(&block)
34
+ end
35
+
36
+ # Register callback for a given event.
37
+ #
38
+ # @param [Symbol] event_type
39
+ # @param [Symbol] name
40
+ # @param [Proc] callback
41
+ #
42
+ # @api public
43
+ def on(event_type = ANY_EVENT, name = ANY_STATE, &callback)
44
+ ensure_valid_callback_name!(name)
45
+ hooks[event_type][name] << callback
46
+ end
47
+
48
+ def on_enter(*args, &callback)
49
+ if machine.states.any? { |state| state == args.first }
50
+ on :enterstate, *args, &callback
51
+ elsif machine.event_names.any? { |name| name == args.first }
52
+ on :enteraction, *args, &callback
53
+ else
54
+ on :enterstate, *args, &callback
55
+ on :enteraction, *args, &callback
56
+ end
57
+ end
58
+
59
+ def on_transition(*args, &callback)
60
+ if machine.states.any? { |state| state == args.first }
61
+ on :transitionstate, *args, &callback
62
+ elsif machine.event_names.any? { |name| name == args.first }
63
+ on :transitionaction, *args, &callback
64
+ else
65
+ on :transitionstate, *args, &callback
66
+ on :transitionaction, *args, &callback
67
+ end
68
+ end
69
+
70
+ def on_exit(*args, &callback)
71
+ if machine.states.any? { |state| state == args.first }
72
+ on :exitstate, *args, &callback
73
+ elsif machine.event_names.any? { |name| name == args.first }
74
+ on :exitaction, *args, &callback
75
+ else
76
+ on :exitstate, *args, &callback
77
+ on :exitaction, *args, &callback
78
+ end
79
+ end
80
+
81
+ def method_missing(method_name, *args, &block)
82
+ _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/)
83
+ if callback_names.include?(callback_name.to_sym)
84
+ send(event_name, callback_name.to_sym, *args, &block)
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def respond_to_missing?(method_name, include_private = false)
91
+ _, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/)
92
+ callback_names.include?(callback_name.to_sym)
93
+ end
94
+
95
+ TransitionEvent = Struct.new(:from, :to, :name) do
96
+ def build(_transition)
97
+ self.from = _transition.from.first
98
+ self.to = _transition.to
99
+ self.name = _transition.name
100
+ end
101
+ end
102
+
103
+ def run_callback(hook, event)
104
+ trans_event = TransitionEvent.new
105
+ trans_event.build(event.transition)
106
+ hook.call(trans_event, *event.data)
107
+ end
108
+
109
+ def trigger(event)
110
+ [event.type, ANY_EVENT].each do |event_type|
111
+ [event.state, ANY_STATE].each do |event_state|
112
+ hooks[event_type][event_state].each do |hook|
113
+ run_callback hook, event
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def callback_names
122
+ @callback_names = Set.new
123
+ @callback_names.merge machine.event_names
124
+ @callback_names.merge machine.states
125
+ @callback_names.merge [ANY_STATE, ANY_EVENT]
126
+ end
127
+
128
+ def ensure_valid_callback_name!(name)
129
+ unless callback_names.include?(name)
130
+ raise InvalidCallbackNameError, "#{name} is not a valid callback name." +
131
+ " Valid callback names are #{callback_names.to_a.inspect}"
132
+ end
133
+ end
134
+
135
+ end # Observer
136
+ end # FiniteMachine