motion-state-machine 0.8.1

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/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rvmrc
7
+ build
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in motion-state-machine.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Sebastian Burkhart
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # motion-state-machine
2
+
3
+ Hey, this is `motion-state-machine` — a state machine gem designed for
4
+ [RubyMotion](http://rubymotion.com) for iOS.
5
+
6
+ ## Motivation
7
+
8
+ Undefined states and visual glitches in applications with complex UIs can
9
+ be a hassle, especially when the UI is animated and the app has to handle
10
+ asynchronous data retrieved in the background.
11
+
12
+ Well-defined UI state machines avoid these problems while ensuring that
13
+ asynchronous event handling does not lead to undefined results (a.k.a. bugs).
14
+
15
+ MacRuby and Cocoa don't provide a simple library to address this —
16
+ motion-state-machine should fill the gap for RubyMotion developers.
17
+
18
+ motion-state-machine comes with a simple and nice-looking syntax to define
19
+ states and transitions:
20
+
21
+ fsm = StateMachine::Base.new start_state: :awake
22
+
23
+ fsm.when :awake do |state|
24
+ state.on_entry { puts "I'm awake, started and alive!" }
25
+ state.transition_to :sleeping, on: :finished_hard_work,
26
+ state.die on: :too_hard_work
27
+ end
28
+
29
+ It is [Grand Central Dispatch](https://developer.apple.com/library/mac/#documentation/Performance/Reference/GCD_libdispatch_Ref/Reference/reference.html)-aware
30
+ and uses GCD queues for synchronization.
31
+
32
+ ## Installation
33
+
34
+ 1. If not done yet, add `bundler` gem management to your RubyMotion app.
35
+ See <http://thunderboltlabs.com/posts/using-bundler-with-rubymotion> for
36
+ an explanation how.
37
+
38
+ 2. Add this line to your application's Gemfile:
39
+
40
+ gem 'motion-state-machine',
41
+ git: "git://github.com/opyh/motion-state-machine.git"
42
+
43
+ 3. Execute:
44
+
45
+ $ bundle
46
+
47
+ ## Usage
48
+
49
+ The following example shows how to initialize and define a state machine:
50
+
51
+ fsm = StateMachine::Base.new start_state: :working, verbose: true
52
+
53
+ This initializes a state machine. Calling `fsm.start!` would start the
54
+ machine in the defined start state `:working`. Using `:verbose` activates
55
+ debug output on the console.
56
+
57
+ ### Defining states and transitions
58
+
59
+ After initialization, you can define states and transitions:
60
+
61
+ fsm.when :working do |state|
62
+
63
+ state.on_entry { puts "I'm awake, started and alive!" }
64
+ state.on_exit { puts "Phew. That was enough work." }
65
+
66
+ state.transition_to :sleeping,
67
+ on: :finished_hard_work,
68
+ if: proc { really_worked_enough_for_now? },
69
+ action: proc { puts "Will go to sleep now." }
70
+
71
+ state.die on: :too_hard_work
72
+
73
+ end
74
+
75
+ This defines…
76
+
77
+ 1. An entry and an exit action block, called when entering/exiting the state
78
+ :working.
79
+
80
+ 2. a transition from state `:working` to `:sleeping`, happening when calling
81
+ `fsm.event(:finished_hard_work)`.
82
+
83
+ Before the transition is executed, the state machine asks the `:if` guard
84
+ block if the transition is allowed. Returning `false` in this block would
85
+ prevent the transition from happening.
86
+
87
+ If the transition is executed, the machine calls the given `:action` block.
88
+
89
+ 3. another transition that terminates the state machine when calling
90
+ `fsm.event(:too_hard_work)`. When terminated, the state machine stops
91
+ responding to events.
92
+
93
+ Note that a transition from a state to itself can be _internal_: Entry/exit
94
+ actions are not called on execution in this case.
95
+
96
+ ### Handling events, timeouts and NSNotifications
97
+
98
+ Transitions can be triggered…
99
+
100
+ - by calling the state machine's `#event` method (see above).
101
+
102
+ - automatically after a given timeout:
103
+
104
+ fsm.when :sleeping do |state|
105
+ state.transition_to :working, after: 20
106
+ end
107
+
108
+ (goes back to `:working` after 20 seconds in state `:sleeping`)
109
+
110
+ - when a `NSNotification` is posted:
111
+
112
+ fsm.when :awake do |state|
113
+ state.transition_to :in_background,
114
+ on_notification: UIApplicationDidEnterBackgroundNotification
115
+ end
116
+
117
+ ### How fast is it?
118
+
119
+ The implementation is designed for general non-performance-intensive purposes
120
+ like managing UI state behavior. It may be too slow for parsing XML, realtime
121
+ signal processing with high sample rates and similar tasks.
122
+
123
+ Anyways, it should be able to handle several thousand events per second on
124
+ an iOS device.
125
+
126
+ ## Contributing
127
+
128
+ Feel free to fork the project and send me a pull request if you would
129
+ like me to integrate your bugfix, enhancement, or feature.
130
+
131
+ You can easily add new triggering mechanisms — they can be
132
+ implemented in few lines by subclassing the `Transition` class (see
133
+ the implementation of `NotificationTransition` for an example).
134
+
135
+ I'm also open for suggestions regarding the interface design.
136
+
137
+ To contribute,
138
+
139
+ 1. Fork it
140
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
141
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
142
+ 4. Push to the branch (`git push origin my-new-feature`)
143
+ 5. Create new Pull Request
144
+
145
+ If the feature has specs, I will probably merge it :)
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env rake
2
+
3
+ $:.unshift("/Library/RubyMotion/lib")
4
+
5
+ require 'rubygems'
6
+ require 'rake'
7
+ require 'motion/project'
8
+ require "bundler/gem_tasks"
9
+
10
+ Bundler.setup
11
+ Bundler.require
12
+
13
+ Motion::Project::App.setup do |app|
14
+ app.name = 'testSuite'
15
+ app.identifier = 'com.screenfashion.motion-state-machine.spec-app'
16
+ app.specs_dir = './spec/motion-state-machine'
17
+ app.development do
18
+ # TODO: How to use module namespacing here?
19
+ app.delegate_class = 'MotionStateMachineSpecAppDelegate'
20
+ end
21
+ end
22
+
23
+ namespace :spec do
24
+ task :lib do
25
+ sh "bacon #{Dir.glob("spec/lib/**/*_spec.rb").join(' ')}"
26
+ end
27
+
28
+ task :motion => 'spec'
29
+
30
+ task :all => [:lib, :motion]
31
+ end
@@ -0,0 +1,9 @@
1
+ unless defined?(Motion::Project::Config)
2
+ raise "This file must be required within a RubyMotion project Rakefile."
3
+ end
4
+
5
+ Motion::Project::App.setup do |app|
6
+ Dir.glob(File.join(File.dirname(__FILE__), 'motion-state-machine/*.rb')).each do |file|
7
+ app.files.unshift(file)
8
+ end
9
+ end
@@ -0,0 +1,223 @@
1
+ # Hey, this is +motion-state-machine+, a state machine designed for
2
+ # RubyMotion.
3
+ #
4
+ # It comes with a simple syntax to define states and transitions (see
5
+ # {Base#when}). It is aware of Grand Central Dispatch queues and uses
6
+ # them for synchronization.
7
+ #
8
+ # Its home is {https://github.com/opyh/motion-state-machine}.
9
+ #
10
+ # See the {file:README.md} for an overview and introduction.
11
+ #
12
+ # You might also want to look at {Base#when} and {State::TransitionDefinitionDSL}.
13
+
14
+ module StateMachine
15
+
16
+ # Base class of a finite state machine (FSM). See {StateMachine} for
17
+ # an overview.
18
+
19
+ class Base
20
+
21
+ # @return [Dispatch::Queue] the GCD queue where the state
22
+ # machine was started (or +nil+ if the state machine has
23
+ # not been started yet)
24
+ attr_reader :initial_queue
25
+
26
+ # @return [String] Name of the state machine.
27
+ # Only used in debug output.
28
+ attr_reader :name
29
+
30
+ # @return [Boolean] Indicates if the machine logs debug output.
31
+ attr_reader :verbose
32
+
33
+ # @return [State] Current {State} (or +nil+ if not in any
34
+ # state, e.g. after exiting and before entering a new state)
35
+ attr_accessor :current_state
36
+
37
+
38
+ # Initializes a new StateMachine.
39
+ #
40
+ # @param options [Hash]
41
+ # Configuration options for the FSM.
42
+ #
43
+ # @option options [Symbol] :start_state
44
+ # First state after start
45
+ #
46
+ # @option options [String] :name ("State machine")
47
+ # Name used in debugging output (optional)
48
+ #
49
+ # @option options [Boolean] :verbose (false)
50
+ # Indicate if the machine should output log texts to console.
51
+ #
52
+ # @example
53
+ # fsm = StateMachine::Base.new start_state: :awake
54
+ #
55
+ # @return [StateMachine::Base] a new StateMachine object
56
+
57
+ def initialize(options)
58
+ super
59
+ @name = options[:name] || "State machine"
60
+ @verbose = !!options[:verbose]
61
+ @state_symbols_to_states = {}
62
+
63
+ waiting_for_start_state = state :waiting_for_start,
64
+ "waiting for start (internal state)"
65
+ start_state = options[:start_state].to_sym
66
+ if start_state.nil?
67
+ raise ArgumentError, "You have to supply a :start_state option."
68
+ end
69
+ state start_state, options[:start_state_name]
70
+ self.when :waiting_for_start, do |state|
71
+ state.transition_to start_state, on: :start
72
+ end
73
+
74
+ @current_state = waiting_for_start_state
75
+ @current_state.send :enter!
76
+ end
77
+
78
+
79
+ # Adds defined transitions to the state machine. States that
80
+ # you refer to with symbols are created automatically, on-the-fly,
81
+ # so you do not have to define them with an extra statement.
82
+ #
83
+ # @param source_state_symbol [Symbol]
84
+ # Identifier of the state from which the transitions begins.
85
+ #
86
+ # @example Define transitions from a state +:awake+ to other states:
87
+ # fsm.when :awake do |state|
88
+ # state.transition_to ...
89
+ # state.die :on => ...
90
+ # state.on_entry { ... }
91
+ # state.on_exit { ... }
92
+ # end
93
+ #
94
+ # @yieldparam [TransitionDefinitionDSL] Call configuration methods
95
+ # on this object to define transitions. See
96
+ # {TransitionDefinitionDSL} for a list of possible methods.
97
+ #
98
+ # @see State::TransitionDefinitionDSL
99
+
100
+ def when(source_state_symbol, &block)
101
+ raise_outside_initial_queue
102
+ source_state = state source_state_symbol
103
+ source_state.send :add_transition_map_defined_in, &block
104
+ end
105
+
106
+
107
+ # @return an array of registered {StateMachine::State} objects.
108
+
109
+ def states
110
+ @state_symbols_to_states.values
111
+ end
112
+
113
+
114
+ # Starts the finite state machine. The machine will be in its
115
+ # start state afterwards. For synchronization, it will remember
116
+ # from which queue/thread it was started.
117
+
118
+ def start!
119
+ @initial_queue = Dispatch::Queue.current
120
+ event :start
121
+ end
122
+
123
+
124
+ # Sends an event to the state machine. If a matching
125
+ # transition was defined, the transition will be executed. If
126
+ # no transition matches, the event will just be ignored.
127
+ #
128
+ # @note You should call this method from the same queue / thread
129
+ # where the state machine was started.
130
+ #
131
+ # @param event_symbol [Symbol] The event to trigger on the
132
+ # state machine.
133
+ #
134
+ # @example
135
+ # my_state_machine.event :some_event
136
+
137
+ def event(event_symbol)
138
+ transition = @events[event_symbol]
139
+ transition.send(:handle_in_source_state) unless transition.nil?
140
+ end
141
+
142
+
143
+ # @returns [Boolean] +true+ if the machine has been terminated,
144
+ # +false+ otherwise.
145
+
146
+ def terminated?
147
+ current_state.terminating?
148
+ end
149
+
150
+ # Should stop the machine and clean up memory.
151
+ # Should call exit actions on the current state, if defined.
152
+ #
153
+ # Not yet tested / really implemented yet, so use with care and
154
+ # make a pull request if you should implement it ;)
155
+
156
+ def stop_and_cleanup
157
+ raise_outside_initial_queue
158
+ @state_machine.log "Stopping #{self}..." if @verbose
159
+ @current_state.send :exit!
160
+ @current_state = nil
161
+ @state_symbols_to_states.values.each(&:cleanup)
162
+ end
163
+
164
+
165
+ def inspect
166
+ # Overridden to avoid debug output overhead
167
+ # (default output would include all attributes)
168
+
169
+ "#<#{self.class}:#{object_id.to_s(16)}>"
170
+ end
171
+
172
+
173
+ # @api private
174
+ # Returns a State object identified by the given symbol.
175
+
176
+ def state(symbol, name = nil)
177
+ unless symbol.is_a?(Symbol)
178
+ raise ArgumentError,
179
+ "You have to supply a symbol to #state. "\
180
+ "Maybe you wanted to call #current_state?"
181
+ end
182
+ raise_outside_initial_queue
183
+ name ||= symbol.to_s
184
+ @state_symbols_to_states[symbol] ||= State.new(self,
185
+ symbol: symbol,
186
+ name: name)
187
+ end
188
+
189
+
190
+ # @api private
191
+ #
192
+ # Registers a block that should be called when {#event} is called
193
+ # with the given symbol as parameter.
194
+ #
195
+ # @param event_symbol [Symbol]
196
+ # symbol that identifies the block
197
+ #
198
+ # @param transition [Transition]
199
+ # transition that should be executed when calling {#event} with
200
+ # +event_symbol+ as parameter
201
+
202
+ def register_event_handler(event_symbol, transition)
203
+ (@events ||= {})[event_symbol] = transition
204
+ end
205
+
206
+
207
+ # @api private
208
+
209
+ def raise_outside_initial_queue
210
+ outside = Dispatch::Queue.current.to_s != @initial_queue.to_s
211
+ if @initial_queue && outside
212
+ raise RuntimeError,
213
+ "Can't call this from outside #{@initial_queue} "\
214
+ "(called from #{Dispatch::Queue.current})."
215
+ end
216
+ end
217
+
218
+ def log(text)
219
+ puts text if @verbose
220
+ end
221
+
222
+ end
223
+ end