antw-simple_state 0.2.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Anthony Williams
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,224 @@
1
+ # SimpleState
2
+
3
+ Yet another state machine library for Ruby.
4
+
5
+ ## Why _another_ state machine library?
6
+
7
+ There are several existing implementations of state machines in Ruby, notably
8
+ [pluginaweek/state_machine][pluginaweek], [rubyist/aasm][rubyist] and
9
+ [ryan-allen/workflow][ryanallen]. However, all felt rather heavy and
10
+ cumbersome, when all I really needed was a lightweight means for setting the
11
+ state of a class instance, and transitioning from one state to another.
12
+
13
+ There is no explicit support for adding your own code to customise
14
+ transitions, nor is there any support for callbacks or event guards (although
15
+ you can still do similar things fairly trivially). It's called **Simple**State
16
+ for a reason! The library adds some helper methods to your class, keeps track
17
+ of the valid states, makes sure that a transition is permitted, and that's
18
+ about it.
19
+
20
+ ## Why use SimpleState?
21
+
22
+ <ul style="margin-top: 1em">
23
+ <li>Lightweight.</li>
24
+ <li>method_missing isn't used. ;)</li>
25
+ <li>No dependencies.</li>
26
+ <li>No extensions to core classes.</li>
27
+ <li>Tested on Ruby 1.8.6 (p383), 1.8.7 (p174), 1.9.1 (p243), and JRuby
28
+ 1.3.1.</li>
29
+ <li>Uses an API similar to Workflow, which I find to be more logical than
30
+ that in the acts_as_state_machine family.</li>
31
+ </ul>
32
+
33
+ ## Why use something else?
34
+
35
+ <ul style="margin-top: 1em">
36
+ <li>The three libraries mentioned above make available, as part of their
37
+ DSL, a means of customising events/transitions with your own code.
38
+ SimpleState makes no such provision, however you can mimic the behaviour
39
+ quite easily as documented in example 3, below.</li>
40
+ <li>Similarly, some other libraries provide the ability to add guard
41
+ conditions -- a condition which must be satisfied before a transition
42
+ can take place. SimpleState also does explicitly support this, however it
43
+ is possible by adapting example 3.
44
+ <li>SimpleState forces you to use an attribute called `state` - other
45
+ libraries let you choose whatever name you want.</li>
46
+ <li>Uses a class variable to keep track of transitions - doesn't lend itself
47
+ all that well to subclassing your state machines.</li>
48
+ </ul>
49
+
50
+ If SimpleState's limitations are too much for you, then you are probably
51
+ better off choosing one of the other libraries instead.
52
+
53
+ ## Examples
54
+
55
+ ### Example 1: Basic usage
56
+
57
+ require 'rubygems'
58
+ require 'simple_state'
59
+
60
+ class SimpleStateMachine
61
+ extend SimpleState # Adds state_machine method to this class.
62
+
63
+ state_machine do
64
+ state :not_started do
65
+ event :start, :transitions_to => :started
66
+ end
67
+
68
+ state :started do
69
+ event :finish, :transitions_to => :finished
70
+ event :cancel, :transitions_to => :cancelled
71
+ end
72
+
73
+ state :finished
74
+ state :cancelled
75
+ end
76
+ end
77
+
78
+ SimpleState makes one assumption: that the first call to `state` in the
79
+ `state_machine` block is the default state; every instance of
80
+ SimpleStateMachine will begin with the state `:not_started`.
81
+
82
+ _Note: if you define `#initialize` in your class, you should ensure that you
83
+ call `super` or the default state won't get set._
84
+
85
+ The above example declares four states: `not_started`, `started`, `finished`
86
+ and `cancelled`. If your instance is in the `not_started` state it may
87
+ transition to the `started` state by calling `SimpleStateMachine#start!`. Once
88
+ `started` it can then transition to `finished` or `cancelled` using
89
+ `SimpleStateMachine#finish!` and `SimpleStateMachine#cancel!`.
90
+
91
+ Along with the bang methods for changing an instance's state, there are
92
+ predicate methods which will return true or false depending on the current
93
+ state of the instance.
94
+
95
+ instance = SimpleStateMachine.new # Initial state will be :not_started
96
+
97
+ instance.not_started? # => true
98
+ instance.started? # => false
99
+ instance.finished? # => false
100
+ instance.cancelled? # => false
101
+
102
+ instance.start!
103
+
104
+ instance.not_started? # => false
105
+ instance.started? # => true
106
+ instance.finished? # => false
107
+ instance.cancelled? # => false
108
+
109
+ # etc...
110
+
111
+ ### Example 2: Events in multiple states
112
+
113
+ It is possible for the same event to be used in multiple states:
114
+
115
+ state :not_started do
116
+ event :start, :transitions_to => :started
117
+ event :cancel, :transitions_to => :cancelled # <--
118
+ end
119
+
120
+ state :started do
121
+ event :finish, :transitions_to => :finished
122
+ event :cancel, :transitions_to => :cancelled # <--
123
+ end
124
+
125
+ ... or for the event to do something different depending on the object's
126
+ current state:
127
+
128
+ state :not_started do
129
+ event :start, :transitions_to => :started
130
+ event :cancel, :transitions_to => :cancelled_before_start # <--
131
+ end
132
+
133
+ state :started do
134
+ event :finish, :transitions_to => :finished
135
+ event :cancel, :transitions_to => :cancelled # <--
136
+ end
137
+
138
+ state :finished
139
+ state :cancelled
140
+ state :cancelled_before_start
141
+
142
+ ### Example 3: Customising event transitions
143
+
144
+ If the built in event methods aren't sufficient and you need to do extra stuff
145
+ to your class during a particular event, you can simply override the method;
146
+ the original method is available via `super`:
147
+
148
+ class OverriddenEvent
149
+ extend SimpleState
150
+
151
+ state_machine do
152
+ state :start do
153
+ event :start, :transitions_to => :started
154
+ end
155
+
156
+ state :started
157
+ end
158
+
159
+ def start!
160
+ puts "Before super() : state=#{self.state}"
161
+ ret = super
162
+ puts "After super() : state=#{self.state}"
163
+ ret
164
+ end
165
+ end
166
+
167
+ OverriddenEvent.new.start!
168
+ # => Before super() : state=start
169
+ # => After super() : state=finished
170
+ # => :started
171
+
172
+ If the event transition isn't valid, super will simply return false, otherwise
173
+ it will return the symbol representing the new state.
174
+
175
+ def start!
176
+ if new_state = super
177
+ puts "Started! The new state is #{self.state}"
178
+ else
179
+ puts "Could not start!"
180
+ end
181
+
182
+ new_state
183
+ end
184
+
185
+ machine = OverriddenEvent.new
186
+ machine.start!
187
+ => Started! The new state is finished
188
+ => :started
189
+
190
+ machine.start!
191
+ => Could not start!
192
+ => false
193
+
194
+ If you need to know whether a transition will be permitted before you call
195
+ super(), SimpleState provides `#event_permitted?`, expecting you to provide a
196
+ symbol representing the event.
197
+
198
+ machine.event_permitted?(:start)
199
+ # => true|false
200
+
201
+ This also provides an easy means for creating guard conditions:
202
+
203
+ def start!
204
+ if event_permitted?(:start) && SomeExternalService.can_start?(self)
205
+ super
206
+ end
207
+ end
208
+
209
+ ## ORM Integration
210
+
211
+ SimpleState should play nicely with your ORM of choice. When an object's state
212
+ is set, `YourObject#state=` is called with a symbol representing the state.
213
+ Simply add a string/enum property called `state` to your DataMapper class, or
214
+ a `state` field to your ActiveRecord database and things should be fine. I
215
+ confess to having no familiarity with Sequel, but I don't foresee any
216
+ difficulty there either.
217
+
218
+ ## License
219
+
220
+ SimpleState is released under the MIT License; see LICENSE for details.
221
+
222
+ [pluginaweek]: http://github.com/pluginaweek/state_machine (pluginaweek's state_machine library)
223
+ [rubyist]: http://github.com/rubyist/aasm (rubyist's aasm library)
224
+ [ryanallen]: http://github.com/ryan-allen/workflow (ryan-allen's Workflow library)
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "simple_state"
8
+ gem.platform = Gem::Platform::RUBY
9
+ gem.summary = 'A *very simple* state machine implementation.'
10
+ gem.description = gem.summary
11
+ gem.email = "anthony@ninecraft.com"
12
+ gem.homepage = "http://github.com/antw/simple_state"
13
+ gem.authors = ["Anthony Williams"]
14
+
15
+ gem.extra_rdoc_files = %w(README.markdown LICENSE)
16
+
17
+ gem.files = %w(LICENSE README.markdown Rakefile VERSION.yml) +
18
+ Dir.glob("{lib,spec}/**/*")
19
+ end
20
+ rescue LoadError
21
+ puts "Jeweler not available. Install it with: sudo gem install " \
22
+ "technicalpickles-jeweler -s http://gems.github.com"
23
+ end
24
+
25
+ # rDoc =======================================================================
26
+
27
+ require 'rake/rdoctask'
28
+ Rake::RDocTask.new do |rdoc|
29
+ rdoc.rdoc_dir = 'rdoc'
30
+ rdoc.title = 'simple_state'
31
+ rdoc.options << '--line-numbers' << '--inline-source'
32
+ rdoc.rdoc_files.include('README*')
33
+ rdoc.rdoc_files.include('lib/**/*.rb')
34
+ end
35
+
36
+ # rSpec & rcov ===============================================================
37
+
38
+ desc "Run all examples (or a specific spec with TASK=xxxx)"
39
+ Spec::Rake::SpecTask.new('spec') do |t|
40
+ t.spec_opts = ["-c -f s"]
41
+ t.spec_files = begin
42
+ if ENV["TASK"]
43
+ ENV["TASK"].split(',').map { |task| "spec/**/#{task}_spec.rb" }
44
+ else
45
+ FileList['spec/**/*_spec.rb']
46
+ end
47
+ end
48
+ end
49
+
50
+ desc "Run all examples with RCov"
51
+ Spec::Rake::SpecTask.new('spec:rcov') do |t|
52
+ t.spec_files = FileList['spec/**/*.rb']
53
+ t.spec_opts = ['-c -f s']
54
+ t.rcov = true
55
+ t.rcov_opts = ['--exclude', 'spec']
56
+ end
57
+
58
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :patch: 1
@@ -0,0 +1,14 @@
1
+ module SimpleState
2
+ class Error < StandardError; end
3
+ class ArgumentError < Error; end
4
+
5
+ ##
6
+ # Sets up a state machine on the current class.
7
+ #
8
+ def state_machine(&blk)
9
+ Builder.new(self).build(&blk)
10
+ end
11
+ end
12
+
13
+ require 'simple_state/builder'
14
+ require 'simple_state/mixins'
@@ -0,0 +1,150 @@
1
+ module SimpleState
2
+ ##
3
+ # Responsible for taking a state machine block and building the methods.
4
+ #
5
+ # The builder is run whenever you call +state_machine+ on a class and does
6
+ # a number of things.
7
+ #
8
+ # * Firstly, it adds a :state reader if one is not defined, and a
9
+ # _private_ :state writer.
10
+ #
11
+ # * It adds a +states+ method to the class, used for easily accessing
12
+ # the list of states for the class, and the events belonging to each
13
+ # state (and the state that the event transitions to).
14
+ #
15
+ # * Four internal methods +initial_state+, +initial_state=+,
16
+ # +_determine_new_state+ and +_valid_transition+ which are used
17
+ # internally by SimpleState for aiding the transition from one state to
18
+ # another.
19
+ #
20
+ class Builder
21
+ def initialize(klass)
22
+ @klass = klass
23
+ end
24
+
25
+ ##
26
+ # Trigger for building the state machine methods.
27
+ #
28
+ def build(&blk)
29
+ @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
30
+ include ::SimpleState::Mixins
31
+ RUBY
32
+
33
+ # Create an anonymous module which will be added to the state machine
34
+ # class's inheritance chain.
35
+ mod = @mod = Module.new
36
+ mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
37
+ def self.inspect
38
+ "SimpleState::#{@klass}AnonMixin"
39
+ end
40
+
41
+ # Handles the change of state.
42
+ # @api private
43
+ def _change_state_using_event!(event)
44
+ self.state = self.class._determine_new_state(self.state, event)
45
+ end
46
+
47
+ # Returns if the passed event is permitted with the instance in it's
48
+ # current state.
49
+ # @api public
50
+ def event_permitted?(event)
51
+ self.class._event_permitted?(self.state, event)
52
+ end
53
+
54
+ # Returns true if the given symbol matches the current state.
55
+ # @api public
56
+ def in_state?(state)
57
+ self.state == state
58
+ end
59
+ RUBY
60
+
61
+ # Declare the state machine rules.
62
+ instance_eval(&blk)
63
+
64
+ # Insert the anonymous module.
65
+ @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
66
+ include mod
67
+ RUBY
68
+ end
69
+
70
+ ##
71
+ # Defines a new state.
72
+ #
73
+ # @param [Symbol] name
74
+ # The name of the state.
75
+ # @param [Block] &blk
76
+ # An optional block for defining transitions for the state. If no block
77
+ # is given, the state will be an end-point.
78
+ #
79
+ def state(name, &blk)
80
+ @klass.states[name] = []
81
+ @klass.initial_state ||= name
82
+
83
+ @mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
84
+ def #{name}? # def prepared?
85
+ in_state?(:#{name}) # self.state == :prepared
86
+ end # end
87
+ RUBY
88
+
89
+ # Define transitions for this state.
90
+ StateBuilder.new(@klass, @mod, name).build(&blk) if blk
91
+ end
92
+
93
+ ##
94
+ # Responsible for building events for a given state.
95
+ #
96
+ class StateBuilder
97
+ def initialize(klass, mod, state)
98
+ @klass, @module, @state = klass, mod, state
99
+ end
100
+
101
+ ##
102
+ # Specialises a state by defining events.
103
+ #
104
+ # @param [Block] &blk
105
+ # An block for defining transitions for the state.
106
+ #
107
+ def build(&blk)
108
+ instance_eval(&blk)
109
+ end
110
+
111
+ ##
112
+ # Defines an event and transition.
113
+ #
114
+ # @param [Symbol] event_name A name for this event.
115
+ # @param [Hash] opts An options hash for customising the event.
116
+ #
117
+ def event(event, opts = {})
118
+ unless opts[:transitions_to].kind_of?(Symbol)
119
+ raise ArgumentError, 'You must declare a :transitions_to state ' \
120
+ 'when defining events'
121
+ end
122
+
123
+ # Keep track of valid transitions for this state.
124
+ @klass.states[@state].push([event, opts[:transitions_to]])
125
+
126
+ unless @module.method_defined?(:"#{event}!")
127
+ # Example:
128
+ #
129
+ # def process!
130
+ # if event_permitted?(:process)
131
+ # _change_state_using_event!(:process)
132
+ # else
133
+ # false
134
+ # end
135
+ # end
136
+ @module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
137
+ def #{event}!
138
+ if event_permitted?(:#{event})
139
+ _change_state_using_event!(:#{event})
140
+ else
141
+ false
142
+ end
143
+ end
144
+ RUBY
145
+ end
146
+ end # def event
147
+ end # StateBuilder
148
+
149
+ end # Builder
150
+ end # SimpleState
@@ -0,0 +1,65 @@
1
+ module SimpleState
2
+ module Mixins
3
+ def self.included(klass)
4
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
5
+ attr_reader :state unless method_defined?(:state)
6
+ @@states = {}
7
+ @@initial_state = nil
8
+
9
+ unless method_defined?(:state=)
10
+ attr_writer :state
11
+ private :state=
12
+ end
13
+
14
+ extend Singleton
15
+ include Instance
16
+ RUBY
17
+ end
18
+
19
+ ##
20
+ # Defines singleton methods which are mixed in to a class when
21
+ # state_machine is called.
22
+ #
23
+ module Singleton
24
+ # @api private
25
+ def states
26
+ class_variable_get(:@@states)
27
+ end
28
+
29
+ # @api public
30
+ def initial_state=(state)
31
+ class_variable_set(:@@initial_state, state)
32
+ end
33
+
34
+ # @api public
35
+ def initial_state
36
+ class_variable_get(:@@initial_state)
37
+ end
38
+
39
+ # @api private
40
+ def _determine_new_state(current, to)
41
+ states[current] && (t = states[current].assoc(to)) && t.last
42
+ end
43
+
44
+ # @api private
45
+ def _event_permitted?(current, to)
46
+ states[current] and not states[current].assoc(to).nil?
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Defines instance methods which are mixed in to a class when
52
+ # state_machine is called.
53
+ #
54
+ module Instance
55
+ ##
56
+ # Set the initial value for the state machine after calling the original
57
+ # initialize method.
58
+ #
59
+ def initialize(*args, &blk)
60
+ super
61
+ self.state = self.class.initial_state
62
+ end
63
+ end # Instance
64
+ end # Mixins
65
+ end # SimpleState
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ # Basic State Machine ========================================================
4
+
5
+ describe SimpleState::Builder do
6
+ before(:each) do
7
+ @c = state_class do
8
+ state :prepared
9
+ state :processed
10
+ end
11
+ end
12
+
13
+ it 'should add a private state writer' do
14
+ @c.private_methods.map { |m| m.to_sym }.should include(:state=)
15
+ end
16
+
17
+ it 'should add a state reader' do
18
+ @c.methods.map { |m| m.to_sym }.should include(:state)
19
+ end
20
+
21
+ describe 'when defining states with no events' do
22
+ it 'should create a predicate' do
23
+ methods = @c.methods.map { |m| m.to_sym }
24
+ methods.should include(:prepared?)
25
+ methods.should include(:processed?)
26
+ end
27
+
28
+ it 'should add the state to register' do
29
+ @c.class.states.keys.should include(:prepared)
30
+ @c.class.states.keys.should include(:processed)
31
+ end
32
+ end
33
+ end
34
+
35
+ # State Machine with Events ==================================================
36
+
37
+ describe 'when defining a state with an event' do
38
+ before(:each) do
39
+ @evented_state = state_class do
40
+ state :prepared do
41
+ event :process, :transitions_to => :processed
42
+ event :processing_failed, :transitions_to => :failed
43
+ end
44
+
45
+ state :processed
46
+ end
47
+ end
48
+
49
+ it 'should add a bang method for the transition' do
50
+ @evented_state.methods.map { |m| m.to_sym }.should \
51
+ include(:process!)
52
+ @evented_state.methods.map { |m| m.to_sym }.should \
53
+ include(:processing_failed!)
54
+ end
55
+
56
+ it 'should add the state to register' do
57
+ @evented_state.class.states.keys.should include(:prepared)
58
+ @evented_state.class.states.keys.should include(:processed)
59
+ end
60
+
61
+ it 'should raise an argument error if no :transitions_to is provided' do
62
+ lambda {
63
+ Class.new do
64
+ extend SimpleState
65
+ state_machine do
66
+ state(:prepared) { event :process }
67
+ end
68
+ end
69
+ }.should raise_error(SimpleState::ArgumentError)
70
+ end
71
+ end
@@ -0,0 +1,227 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe 'Generated event methods' do
4
+
5
+ it 'should permit the use of super when overriding them' do
6
+ @c = Class.new do
7
+ attr_reader :called
8
+
9
+ extend SimpleState
10
+
11
+ state_machine do
12
+ state :begin do
13
+ event :go, :transitions_to => :finished
14
+ end
15
+
16
+ state :finished
17
+ end
18
+
19
+ def go!
20
+ @called = true
21
+ super()
22
+ end
23
+ end.new
24
+
25
+ @c.should be_begin
26
+ lambda { @c.go! }.should_not raise_error(NoMethodError)
27
+ @c.should be_finished
28
+ @c.called.should be_true
29
+ end
30
+
31
+ # Event Validation =========================================================
32
+
33
+ describe 'when the transition is valid' do
34
+ before(:each) do
35
+ @c = state_class do
36
+ state :begin do
37
+ event :go, :transitions_to => :state_one
38
+ end
39
+
40
+ state :state_one
41
+ end
42
+ end
43
+
44
+ it 'should return the new state' do
45
+ @c.go!.should == :state_one
46
+ end
47
+
48
+ it 'should transition the instance to the new state' do
49
+ @c.go!
50
+ @c.should be_state_one
51
+ end
52
+ end
53
+
54
+ describe 'when the transition is not valid' do
55
+ before(:each) do
56
+ @c = state_class do
57
+ state :begin do
58
+ event :go, :transitions_to => :state_one
59
+ end
60
+
61
+ state :state_one do
62
+ event :go_again, :transitions_to => :state_two
63
+ end
64
+
65
+ state :state_two
66
+ end
67
+ end
68
+
69
+ it 'should return false' do
70
+ @c.go_again!.should be_false
71
+ end
72
+
73
+ it "should not change the instance's state" do
74
+ @c.go_again!
75
+ @c.should be_begin
76
+ end
77
+ end
78
+
79
+ # Multiple Paths Definition ================================================
80
+
81
+ describe 'when mulitple states share the same event' do
82
+ before(:each) do
83
+ @path = state_class do
84
+ state :begin do
85
+ event :s1, :transitions_to => :state_one
86
+ event :s2, :transitions_to => :state_two
87
+ end
88
+
89
+ state :state_one do
90
+ event :go, :transitions_to => :state_three
91
+ end
92
+
93
+ state :state_two do
94
+ event :go, :transitions_to => :state_four
95
+ end
96
+
97
+ state :state_three
98
+ state :state_four
99
+ end
100
+ end
101
+
102
+ it 'should transition to state_three if currently in state_one' do
103
+ @path.s1!
104
+ @path.go!
105
+ @path.should be_state_three
106
+ end
107
+
108
+ it 'should transition to state_four if current in state_two' do
109
+ @path.s2!
110
+ @path.go!
111
+ @path.should be_state_four
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ # Test full workflow =========================================================
118
+ # This tests all the possible transition permutations of a state machine.
119
+
120
+ describe 'Generated event methods (integration)' do
121
+ before(:each) do
122
+ @c = state_class do
123
+ state :prepared do
124
+ event :requires_decompress, :transitions_to => :requires_decompress
125
+ event :invalid_extension, :transitions_to => :halted
126
+ event :processed, :transitions_to => :processed
127
+ event :processing_failed, :transitions_to => :halted
128
+ end
129
+
130
+ state :processed do
131
+ event :stored, :transitions_to => :stored
132
+ event :store_failed, :transitions_to => :halted
133
+ end
134
+
135
+ state :requires_decompress do
136
+ event :decompressed, :transitions_to => :complete
137
+ event :decompress_failed, :transitions_to => :halted
138
+ end
139
+
140
+ state :stored do
141
+ event :cleaned, :transitions_to => :complete
142
+ end
143
+
144
+ state :halted do
145
+ event :cleaned, :transitions_to => :failed
146
+ end
147
+
148
+ state :failed
149
+ state :complete
150
+ end
151
+ end
152
+
153
+ it 'should successfully change the state to complete via the ' \
154
+ 'intermediate states' do
155
+
156
+ # begin -> processed -> stored -> complete
157
+
158
+ @c.should be_prepared
159
+
160
+ @c.processed!.should == :processed
161
+ @c.should be_processed
162
+
163
+ @c.stored!.should == :stored
164
+ @c.should be_stored
165
+
166
+ @c.cleaned!.should == :complete
167
+ @c.should be_complete
168
+ end
169
+
170
+ it 'should successfully change the state to complete via successful ' \
171
+ 'decompress' do
172
+
173
+ # begin -> requires_decompress -> complete
174
+
175
+ @c.requires_decompress!.should == :requires_decompress
176
+ @c.should be_requires_decompress
177
+
178
+ @c.decompressed!.should == :complete
179
+ @c.should be_complete
180
+ end
181
+
182
+ it 'should successfully change the state to failed via failed decompress' do
183
+ # begins -> requires_decompress -> halted -> failed
184
+
185
+ @c.requires_decompress!.should == :requires_decompress
186
+ @c.should be_requires_decompress
187
+
188
+ @c.decompress_failed!.should == :halted
189
+ @c.should be_halted
190
+
191
+ @c.cleaned!.should == :failed
192
+ @c.should be_failed
193
+ end
194
+
195
+ it 'should successfully change the state to failed via invalid extension' do
196
+ # begins -> halted -> failed
197
+
198
+ @c.invalid_extension!.should == :halted
199
+ @c.should be_halted
200
+
201
+ @c.cleaned!.should == :failed
202
+ @c.should be_failed
203
+ end
204
+
205
+ it 'should successfully change the state to failed via failed processing' do
206
+ # begins -> halted -> failed
207
+
208
+ @c.processing_failed!.should == :halted
209
+ @c.should be_halted
210
+
211
+ @c.cleaned!.should == :failed
212
+ @c.should be_failed
213
+ end
214
+
215
+ it 'should successfully change the state to failed via failed storage' do
216
+ # begins -> processed -> halted -> failed
217
+
218
+ @c.processed!.should == :processed
219
+ @c.should be_processed
220
+
221
+ @c.store_failed!.should == :halted
222
+ @c.should be_halted
223
+
224
+ @c.cleaned!.should == :failed
225
+ @c.should be_failed
226
+ end
227
+ end
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe SimpleState::Mixins::Instance do
4
+ describe '#initialize' do
5
+ it 'should set the initial state' do
6
+ c = state_class do
7
+ state :begin
8
+ state :finish
9
+ end
10
+
11
+ c.state.should == :begin
12
+ end
13
+
14
+ it 'should call the original #initialize' do
15
+ parent = Class.new do
16
+ attr_reader :called
17
+
18
+ def initialize
19
+ @called = true
20
+ end
21
+ end
22
+
23
+ child = Class.new(parent) do
24
+ extend SimpleState
25
+ state_machine do
26
+ state :begin
27
+ end
28
+ end
29
+
30
+ child.new.called.should be_true
31
+ child.new.state.should == :begin
32
+ end
33
+
34
+ it 'should have separate state machines for each class' do
35
+ class_one = state_class do
36
+ state :one
37
+ end
38
+
39
+ class_two = state_class do
40
+ state :two
41
+ end
42
+
43
+ class_one.class.states.keys.should == [:one]
44
+ class_two.class.states.keys.should == [:two]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe SimpleState, 'generated predicate methods' do
4
+ before(:each) do
5
+ @predicate_test = state_class do
6
+ state :state_one
7
+ state :state_two
8
+ state :state_three
9
+ end
10
+ end
11
+
12
+ it 'should return true if the current state matches the predicate' do
13
+ @predicate_test.should be_state_one
14
+ end
15
+
16
+ it 'should return false if the current state does not match the predicate' do
17
+ @predicate_test.should_not be_state_two
18
+ @predicate_test.should_not be_state_three
19
+ end
20
+
21
+ it 'should permit the use of super when overriding them' do
22
+ @c = Class.new do
23
+ attr_reader :called
24
+
25
+ extend SimpleState
26
+
27
+ state_machine do
28
+ state :begin
29
+ end
30
+
31
+ def begin?
32
+ @called = true
33
+ super()
34
+ end
35
+ end.new
36
+
37
+ lambda { @c.begin? }.should_not raise_error(NoMethodError)
38
+ @c.called.should be_true
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe "SimpleState" do
4
+ it "should add a state_machine method to the class" do
5
+ Class.new { extend SimpleState }.methods.map do |m|
6
+ # Ruby 1.9 compat.
7
+ m.to_sym
8
+ end.should include(:state_machine)
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ # $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'simple_state'
7
+
8
+ ##
9
+ # Creates an anonymous class which uses SimpleState.
10
+ #
11
+ def state_class(&blk)
12
+ Class.new do
13
+ extend SimpleState
14
+ state_machine(&blk)
15
+ end.new
16
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: antw-simple_state
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Anthony Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-21 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A *very simple* state machine implementation.
17
+ email: anthony@ninecraft.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.markdown
25
+ files:
26
+ - LICENSE
27
+ - README.markdown
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/simple_state.rb
31
+ - lib/simple_state/builder.rb
32
+ - lib/simple_state/mixins.rb
33
+ - spec/builder_spec.rb
34
+ - spec/event_methods_spec.rb
35
+ - spec/mixins_spec.rb
36
+ - spec/predicate_methods_spec.rb
37
+ - spec/simple_state_spec.rb
38
+ - spec/spec_helper.rb
39
+ has_rdoc: false
40
+ homepage: http://github.com/antw/simple_state
41
+ licenses:
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.5
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: A *very simple* state machine implementation.
66
+ test_files:
67
+ - spec/builder_spec.rb
68
+ - spec/event_methods_spec.rb
69
+ - spec/mixins_spec.rb
70
+ - spec/predicate_methods_spec.rb
71
+ - spec/simple_state_spec.rb
72
+ - spec/spec_helper.rb