motion-state-machine 0.8.1

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