anthonyw-simple_state 0.1.1 → 0.1.2

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/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: