finite_machine 0.0.1 → 0.1.0

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