anthonyw-simple_state 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -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/anthonyw/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
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
@@ -0,0 +1,117 @@
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
+ instance_eval(&blk)
34
+ end
35
+
36
+ ##
37
+ # Defines a new state.
38
+ #
39
+ # @param [Symbol] name
40
+ # The name of the state.
41
+ # @param [Block] &blk
42
+ # An optional block for defining transitions for the state. If no block
43
+ # is given, the state will be an end-point.
44
+ #
45
+ def state(name, &blk)
46
+ @klass.states[name] = []
47
+ @klass.initial_state ||= name
48
+
49
+ @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
50
+ def #{name}? # def prepared?
51
+ self.state == :#{name} # self.state == :prepared
52
+ end # end
53
+ RUBY
54
+
55
+ # Define transitions for this state.
56
+ StateBuilder.new(@klass, name).build(&blk) if blk
57
+ end
58
+
59
+ ##
60
+ # Responsible for building events for a given state.
61
+ #
62
+ class StateBuilder
63
+ def initialize(klass, state)
64
+ @klass, @state = klass, state
65
+ end
66
+
67
+ ##
68
+ # Specialises a state by defining events.
69
+ #
70
+ # @param [Block] &blk
71
+ # An block for defining transitions for the state.
72
+ #
73
+ def build(&blk)
74
+ instance_eval(&blk)
75
+ end
76
+
77
+ ##
78
+ # Defines an event and transition.
79
+ #
80
+ # @param [Symbol] event_name A name for this event.
81
+ # @param [Hash] opts An options hash for customising the event.
82
+ #
83
+ def event(event, opts = {})
84
+ unless opts[:transitions_to].kind_of?(Symbol)
85
+ raise ArgumentError, 'You must declare a :transitions_to state ' \
86
+ 'when defining events'
87
+ end
88
+
89
+ # Keep track of valid transitions for this state.
90
+ @klass.states[@state].push([event, opts[:transitions_to]])
91
+
92
+ unless @klass.method_defined?(:"#{event}!")
93
+ # Example:
94
+ #
95
+ # def process!
96
+ # if self.class._valid_transition?(self.state, :process)
97
+ # self.state =
98
+ # self.class._determine_new_state(self.state, :process)
99
+ # else
100
+ # false
101
+ # end
102
+ # end
103
+ @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
104
+ def #{event}!
105
+ if self.class._valid_transition?(self.state, :#{event})
106
+ self.state =
107
+ self.class._determine_new_state(self.state, :#{event})
108
+ else
109
+ false
110
+ end
111
+ end
112
+ RUBY
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -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 _valid_transition?(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,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,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,199 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ # Event Validation ===========================================================
4
+
5
+ describe 'Generated event methods when the transition is valid' do
6
+ before(:each) do
7
+ @c = state_class do
8
+ state :begin do
9
+ event :go, :transitions_to => :state_one
10
+ end
11
+
12
+ state :state_one
13
+ end
14
+ end
15
+
16
+ it 'should return the new state' do
17
+ @c.go!.should == :state_one
18
+ end
19
+
20
+ it 'should transition the instance to the new state' do
21
+ @c.go!
22
+ @c.should be_state_one
23
+ end
24
+ end
25
+
26
+ describe 'Generated event methods when the transition is not valid' do
27
+ before(:each) do
28
+ @c = state_class do
29
+ state :begin do
30
+ event :go, :transitions_to => :state_one
31
+ end
32
+
33
+ state :state_one do
34
+ event :go_again, :transitions_to => :state_two
35
+ end
36
+
37
+ state :state_two
38
+ end
39
+ end
40
+
41
+ it 'should return false' do
42
+ @c.go_again!.should be_false
43
+ end
44
+
45
+ it "should not change the instance's state" do
46
+ @c.go_again!
47
+ @c.should be_begin
48
+ end
49
+ end
50
+
51
+
52
+ # Multiple Paths Definition ==================================================
53
+
54
+ describe 'Generated event methods when mulitple states share the same event' do
55
+ before(:each) do
56
+ @path = state_class do
57
+ state :begin do
58
+ event :s1, :transitions_to => :state_one
59
+ event :s2, :transitions_to => :state_two
60
+ end
61
+
62
+ state :state_one do
63
+ event :go, :transitions_to => :state_three
64
+ end
65
+
66
+ state :state_two do
67
+ event :go, :transitions_to => :state_four
68
+ end
69
+
70
+ state :state_three
71
+ state :state_four
72
+ end
73
+ end
74
+
75
+ it 'should transition to state_three if currently in state_one' do
76
+ @path.s1!
77
+ @path.go!
78
+ @path.should be_state_three
79
+ end
80
+
81
+ it 'should transition to state_four if current in state_two' do
82
+ @path.s2!
83
+ @path.go!
84
+ @path.should be_state_four
85
+ end
86
+ end
87
+
88
+
89
+ # Test full workflow =========================================================
90
+ # This tests all the possible transition permutations of a state machine.
91
+
92
+ describe 'Generated event methods (integration)' do
93
+ before(:each) do
94
+ @c = state_class do
95
+ state :prepared do
96
+ event :requires_decompress, :transitions_to => :requires_decompress
97
+ event :invalid_extension, :transitions_to => :halted
98
+ event :processed, :transitions_to => :processed
99
+ event :processing_failed, :transitions_to => :halted
100
+ end
101
+
102
+ state :processed do
103
+ event :stored, :transitions_to => :stored
104
+ event :store_failed, :transitions_to => :halted
105
+ end
106
+
107
+ state :requires_decompress do
108
+ event :decompressed, :transitions_to => :complete
109
+ event :decompress_failed, :transitions_to => :halted
110
+ end
111
+
112
+ state :stored do
113
+ event :cleaned, :transitions_to => :complete
114
+ end
115
+
116
+ state :halted do
117
+ event :cleaned, :transitions_to => :failed
118
+ end
119
+
120
+ state :failed
121
+ state :complete
122
+ end
123
+ end
124
+
125
+ it 'should successfully change the state to complete via the ' \
126
+ 'intermediate states' do
127
+
128
+ # begin -> processed -> stored -> complete
129
+
130
+ @c.should be_prepared
131
+
132
+ @c.processed!.should == :processed
133
+ @c.should be_processed
134
+
135
+ @c.stored!.should == :stored
136
+ @c.should be_stored
137
+
138
+ @c.cleaned!.should == :complete
139
+ @c.should be_complete
140
+ end
141
+
142
+ it 'should successfully change the state to complete via successful ' \
143
+ 'decompress' do
144
+
145
+ # begin -> requires_decompress -> complete
146
+
147
+ @c.requires_decompress!.should == :requires_decompress
148
+ @c.should be_requires_decompress
149
+
150
+ @c.decompressed!.should == :complete
151
+ @c.should be_complete
152
+ end
153
+
154
+ it 'should successfully change the state to failed via failed decompress' do
155
+ # begins -> requires_decompress -> halted -> failed
156
+
157
+ @c.requires_decompress!.should == :requires_decompress
158
+ @c.should be_requires_decompress
159
+
160
+ @c.decompress_failed!.should == :halted
161
+ @c.should be_halted
162
+
163
+ @c.cleaned!.should == :failed
164
+ @c.should be_failed
165
+ end
166
+
167
+ it 'should successfully change the state to failed via invalid extension' do
168
+ # begins -> halted -> failed
169
+
170
+ @c.invalid_extension!.should == :halted
171
+ @c.should be_halted
172
+
173
+ @c.cleaned!.should == :failed
174
+ @c.should be_failed
175
+ end
176
+
177
+ it 'should successfully change the state to failed via failed processing' do
178
+ # begins -> halted -> failed
179
+
180
+ @c.processing_failed!.should == :halted
181
+ @c.should be_halted
182
+
183
+ @c.cleaned!.should == :failed
184
+ @c.should be_failed
185
+ end
186
+
187
+ it 'should successfully change the state to failed via failed storage' do
188
+ # begins -> processed -> halted -> failed
189
+
190
+ @c.processed!.should == :processed
191
+ @c.should be_processed
192
+
193
+ @c.store_failed!.should == :halted
194
+ @c.should be_halted
195
+
196
+ @c.cleaned!.should == :failed
197
+ @c.should be_failed
198
+ end
199
+ 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,20 @@
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
+ 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anthonyw-simple_state
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Williams
@@ -23,8 +23,20 @@ extra_rdoc_files:
23
23
  - README.markdown
24
24
  - LICENSE
25
25
  files:
26
- - README.markdown
27
26
  - LICENSE
27
+ - README.markdown
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/simple_state
31
+ - lib/simple_state/builder.rb
32
+ - lib/simple_state/mixins.rb
33
+ - lib/simple_state.rb
34
+ - spec/builder_spec.rb
35
+ - spec/event_methods_spec.rb
36
+ - spec/mixins_spec.rb
37
+ - spec/predicate_methods_spec.rb
38
+ - spec/simple_state_spec.rb
39
+ - spec/spec_helper.rb
28
40
  has_rdoc: true
29
41
  homepage: http://github.com/anthonyw/simple_state
30
42
  post_install_message: