simple_state_machine 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Marek de Heus & Petrik de Heus
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,73 @@
1
+ = SimpleStateMachine
2
+
3
+ A Ruby Statemachine that focuses on events instead of states.
4
+
5
+ A state machine should help you:
6
+ - Encapsulate state,
7
+ - Guard state transitions,
8
+ - Make it trivial to add state transitions to any method.
9
+
10
+ Instead of using a DSL to define events, we use a different approach.
11
+ We have a very simple DSL to decorate existing methods with logic that guards
12
+ state transitions.
13
+
14
+ ==== example
15
+
16
+ def activate_account(activation_code)
17
+ if activation_code_invalid?(activation_code)
18
+ errors.add(:activation_code, 'Invalid')
19
+ end
20
+ end
21
+ event :activate_account, :pending => :active
22
+
23
+
24
+ This has a couple of advantages:
25
+ - Arguments can be passed to 'events',
26
+ - 'events' can return a value, remember, it's just a method,
27
+ - Validation errors can be set on the model if you are using ActiveRecord / ActiveModel,
28
+ - Encapsulate state transitions (no need for :guards).
29
+
30
+ To use the code, you need to do 3 things:
31
+ - extend SimpleStateMachine,
32
+ - set the initial state,
33
+ - decorate a method with the event DSL
34
+
35
+ ==== Example usage
36
+
37
+ class Lamp
38
+
39
+ extend SimpleStateMachine
40
+
41
+ def initialize
42
+ self.state = :off
43
+ end
44
+
45
+ def push_button1
46
+ puts "click1: #{state}"
47
+ end
48
+ event :push_button1, :off => :on,
49
+ :on => :off
50
+
51
+ def push_button2
52
+ puts "click1: #{state}"
53
+ end
54
+ event :push_button2, :off => :on,
55
+ :on => :off
56
+
57
+ end
58
+
59
+ This code was just released, we do not claim it to be stable.
60
+
61
+ == Note on Patches/Pull Requests
62
+
63
+ * Fork the project.
64
+ * Make your feature addition or bug fix.
65
+ * Add tests for it. This is important so I don't break it in a
66
+ future version unintentionally.
67
+ * Commit, do not mess with rakefile, version, or history.
68
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
69
+ * Send me a pull request. Bonus points for topic branches.
70
+
71
+ == Copyright
72
+
73
+ Copyright (c) 2010 Marek & Petrik. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "simple_state_machine"
8
+ gem.summary = %Q{A statemachine that focuses on events instead of states}
9
+ gem.description = %Q{A simple DSL to decorate existing methods with logic that guards state transitions.}
10
+ gem.email = ["FIX@example.com"]
11
+ gem.homepage = "http://github.com/p8/simple_state_machine"
12
+ gem.authors = ["Marek de Heus", "Petrik de Heus"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "simple_state_machine #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,34 @@
1
+ # Implementation of the AASM Conversation
2
+ #
3
+ # class Conversation
4
+ # include AASM
5
+ #
6
+ # aasm_column :current_state # defaults to aasm_state
7
+ #
8
+ # aasm_initial_state :unread
9
+ #
10
+ # aasm_state :unread
11
+ # aasm_state :read
12
+ # aasm_state :closed
13
+ #
14
+ # aasm_event :view do
15
+ # transitions :to => :read, :from => [:unread]
16
+ # end
17
+ #
18
+ # aasm_event :close do
19
+ # transitions :to => :closed, :from => [:read, :unread]
20
+ # end
21
+ # end
22
+
23
+ class Conversation
24
+ extend SimpleStateMachine
25
+
26
+ def initialize
27
+ self.state = 'unread'
28
+ end
29
+
30
+ event :view, :unread => :read
31
+ event :close, :unread => :closed,
32
+ :read => :closed
33
+
34
+ end
data/examples/lamp.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Lamp
2
+
3
+ extend SimpleStateMachine
4
+
5
+ def initialize
6
+ self.state = 'off'
7
+ end
8
+
9
+ def push_button1
10
+ puts "click1: #{state}"
11
+ end
12
+ event :push_button1, :off => :on,
13
+ :on => :off
14
+
15
+ def push_button2
16
+ puts "click2: #{state}"
17
+ end
18
+ event :push_button2, :off => :on,
19
+ :on => :off
20
+
21
+ end
@@ -0,0 +1,87 @@
1
+ # Implementation of the AASM Conversation
2
+ #
3
+ # class Relationship
4
+ # include AASM
5
+ #
6
+ # aasm_column :status
7
+ #
8
+ # aasm_initial_state Proc.new { |relationship| relationship.strictly_for_fun? ? :intimate : :dating }
9
+ #
10
+ # aasm_state :dating, :enter => :make_happy, :exit => :make_depressed
11
+ # aasm_state :intimate, :enter => :make_very_happy, :exit => :never_speak_again
12
+ # aasm_state :married, :enter => :give_up_intimacy, :exit => :buy_exotic_car_and_wear_a_combover
13
+ #
14
+ # aasm_event :get_intimate do
15
+ # transitions :to => :intimate, :from => [:dating], :guard => :drunk?
16
+ # end
17
+ #
18
+ # aasm_event :get_married do
19
+ # transitions :to => :married, :from => [:dating, :intimate], :guard => :willing_to_give_up_manhood?
20
+ # end
21
+ #
22
+ # def strictly_for_fun?; end
23
+ # def drunk?; end
24
+ # def willing_to_give_up_manhood?; end
25
+ # def make_happy; end
26
+ # def make_depressed; end
27
+ # def make_very_happy; end
28
+ # def never_speak_again; end
29
+ # def give_up_intimacy; end
30
+ # def buy_exotic_car_and_wear_a_combover; end
31
+ # end
32
+
33
+ class Relationship
34
+ extend SimpleStateMachine
35
+
36
+ def initialize
37
+ self.state = relationship.strictly_for_fun? ? get_intimate : start_dating
38
+ end
39
+
40
+ def start_dating
41
+ make_happy
42
+ # make_depressed not called on exit
43
+ end
44
+ event :start_dating, nil => :dating
45
+
46
+ def get_intimate
47
+ # exit if drunk?
48
+ make_very_happy
49
+ # never_speak_again not called on exit
50
+ end
51
+ event :get_intimate, :dating => :intimate
52
+
53
+ def get_married
54
+ give_up_intimacy
55
+ #exit unless willing_to_give_up_manhood?
56
+ # buy_exotic_car_and_wear_a_combover not called on exit
57
+ end
58
+ event :get_married, :dating => :married,
59
+ :intimate => :married
60
+
61
+ # extra event added
62
+ def stop_dating
63
+ make_depressed
64
+ end
65
+ event :stop_dating, :dating => nil
66
+
67
+ def dump
68
+ never_speak_again
69
+ end
70
+ event :dump, :intimate => :dump
71
+
72
+ def divorce
73
+ buy_exotic_car_and_wear_a_combover
74
+ end
75
+ event :divorce, :married => :divorced
76
+
77
+
78
+ def strictly_for_fun?; end
79
+ def drunk?; end
80
+ def willing_to_give_up_manhood?; end
81
+ def make_happy; end
82
+ def make_depressed; end
83
+ def make_very_happy; end
84
+ def never_speak_again; end
85
+ def give_up_intimacy; end
86
+ def buy_exotic_car_and_wear_a_combover; end
87
+ end
@@ -0,0 +1,17 @@
1
+ class TrafficLight
2
+
3
+ extend SimpleStateMachine
4
+
5
+ def initialize
6
+ self.state = 'green'
7
+ end
8
+
9
+ # state machine events
10
+ def change_state
11
+ puts "#{state} => #{@next_state}"
12
+ end
13
+
14
+ event :change_state, :green => :orange,
15
+ :orange => :red,
16
+ :red => :green
17
+ end
data/examples/user.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'digest/sha1'
2
+ class User < ActiveRecord::Base
3
+
4
+ validates_presence_of :name
5
+
6
+ extend SimpleStateMachine
7
+
8
+ def after_initialize
9
+ self.state ||= 'new'
10
+ end
11
+
12
+ def invite
13
+ self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
14
+ send_activation_email(self.activation_code)
15
+ end
16
+ event :invite, :new => :invited
17
+
18
+ def confirm_invitation activation_code
19
+ if activation_code != self.activation_code
20
+ errors.add(:activation_code, 'Invalid')
21
+ end
22
+ end
23
+ event :confirm_invitation, :invited => :active
24
+
25
+ #event :log_send_activation_code_failed, :new => :send_activation_code_failed
26
+ #
27
+ # def reset_password(new_password)
28
+ # self.password = new_password
29
+ # end
30
+ # # do not change state, but ensure that we are in proper state
31
+ # event :reset_password, :active => :active
32
+
33
+ def send_activation_email(code)
34
+ true
35
+ end
36
+
37
+ end
@@ -0,0 +1 @@
1
+ require 'simple_state_machine/simple_state_machine'
@@ -0,0 +1,209 @@
1
+ module SimpleStateMachine
2
+
3
+ def event event_name, state_transitions
4
+ @state_machine_definition ||= StateMachineDefinition.new self
5
+ @state_machine_definition.define_event event_name, state_transitions
6
+ end
7
+
8
+ def state_machine_definition
9
+ @state_machine_definition
10
+ end
11
+
12
+ class StateMachineDefinition
13
+
14
+ attr_reader :events
15
+
16
+ def initialize subject
17
+ @events = {}
18
+ @decorator = if inherits_from_active_record_base?(subject)
19
+ Decorator::ActiveRecord.new(subject)
20
+ else
21
+ Decorator::Base.new(subject)
22
+ end
23
+ end
24
+
25
+ def define_event event_name, state_transitions
26
+ @events[event_name.to_s] ||= {}
27
+ state_transitions.each do |from, to|
28
+ @events[event_name.to_s][from.to_s] = to.to_s
29
+ @decorator.decorate(event_name, from, to)
30
+ end
31
+ end
32
+
33
+ def inherits_from_active_record_base?(subject)
34
+ subject.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
35
+ end
36
+
37
+ end
38
+
39
+ module StateMachine
40
+
41
+ class Base
42
+ def initialize(subject)
43
+ @subject = subject
44
+ end
45
+
46
+ def next_state(event_name)
47
+ @subject.class.state_machine_definition.events[event_name.to_s][@subject.state]
48
+ end
49
+
50
+ def transition(event_name)
51
+ if to = next_state(event_name)
52
+ result = yield
53
+ @subject.state = to
54
+ return result
55
+ else
56
+ illegal_event_callback event_name
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def illegal_event_callback event_name
63
+ # override with your own implementation, like setting errors in your model
64
+ raise "You cannot '#{event_name}' when state is '#{@subject.state}'"
65
+ end
66
+
67
+ end
68
+
69
+ class ActiveRecord < Base
70
+
71
+ def next_state(event_name)
72
+ if event_name =~ /\!$/
73
+ event_name = event_name.chop
74
+ end
75
+ super event_name
76
+ end
77
+
78
+ def transition(event_name)
79
+ if to = next_state(event_name)
80
+ if with_error_counting { yield } > 0 || @subject.invalid?
81
+ if event_name =~ /\!$/
82
+ raise ::ActiveRecord::RecordInvalid.new(@subject)
83
+ else
84
+ return false
85
+ end
86
+ else
87
+ @subject.state = to
88
+ if event_name =~ /\!$/
89
+ @subject.save! #TODO maybe save_without_validation!
90
+ else
91
+ @subject.save
92
+ end
93
+ end
94
+ else
95
+ illegal_event_callback event_name
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def with_error_counting
102
+ original_errors_size = @subject.errors.size
103
+ yield
104
+ @subject.errors.size - original_errors_size
105
+ end
106
+
107
+ end
108
+
109
+ end
110
+
111
+ module Decorator
112
+ class Base
113
+
114
+ def initialize(subject)
115
+ @subject = subject
116
+ define_state_machine_method
117
+ define_state_getter_method
118
+ define_state_setter_method
119
+ end
120
+
121
+ def decorate event_name, from, to
122
+ define_state_helper_method(from)
123
+ define_state_helper_method(to)
124
+ define_event_method(event_name)
125
+ decorate_event_method(event_name)
126
+ end
127
+
128
+ private
129
+
130
+ def define_state_machine_method
131
+ @subject.send(:define_method, "state_machine") do
132
+ @state_machine ||= StateMachine::Base.new(self)
133
+ end
134
+ end
135
+
136
+ def define_state_helper_method state
137
+ unless @subject.method_defined?("#{state.to_s}?")
138
+ @subject.send(:define_method, "#{state.to_s}?") do
139
+ self.state == state.to_s
140
+ end
141
+ end
142
+ end
143
+
144
+ def define_event_method event_name
145
+ unless @subject.method_defined?("#{event_name}")
146
+ @subject.send(:define_method, "#{event_name}") {}
147
+ end
148
+ end
149
+
150
+ def decorate_event_method event_name
151
+ # TODO put in transaction for activeRecord?
152
+ unless @subject.method_defined?("with_managed_state_#{event_name}")
153
+ @subject.send(:define_method, "with_managed_state_#{event_name}") do |*args|
154
+ return state_machine.transition(event_name) do
155
+ send("without_managed_state_#{event_name}", *args)
156
+ end
157
+ end
158
+ @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
159
+ @subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
160
+ end
161
+ end
162
+
163
+ def define_state_setter_method
164
+ unless @subject.method_defined?('state=')
165
+ @subject.send(:define_method, 'state=') do |new_state|
166
+ @state = new_state.to_s
167
+ end
168
+ end
169
+ end
170
+
171
+ def define_state_getter_method
172
+ unless @subject.method_defined?('state')
173
+ @subject.send(:define_method, 'state') do
174
+ @state
175
+ end
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ class ActiveRecord < Base
182
+
183
+ def decorate event_name, from, to
184
+ super event_name, from, to
185
+ unless @subject.method_defined?("#{event_name}!")
186
+ @subject.send(:define_method, "#{event_name}!") do |*args|
187
+ send "#{event_name}", *args
188
+ end
189
+ end
190
+ decorate_event_method("#{event_name}!")
191
+ end
192
+
193
+ private
194
+
195
+ def define_state_machine_method
196
+ @subject.send(:define_method, "state_machine") do
197
+ @state_machine ||= StateMachine::ActiveRecord.new(self)
198
+ end
199
+ end
200
+
201
+ def define_state_setter_method; end
202
+
203
+ def define_state_getter_method; end
204
+
205
+ end
206
+
207
+ end
208
+
209
+ end
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{simple_state_machine}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Marek de Heus", "Petrik de Heus"]
12
+ s.date = %q{2010-06-28}
13
+ s.description = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
14
+ s.email = ["FIX@example.com"]
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "examples/conversation.rb",
25
+ "examples/lamp.rb",
26
+ "examples/relationship.rb",
27
+ "examples/traffic_light.rb",
28
+ "examples/user.rb",
29
+ "lib/simple_state_machine.rb",
30
+ "lib/simple_state_machine/simple_state_machine.rb",
31
+ "simple_state_machine.gemspec",
32
+ "spec/active_record_spec.rb",
33
+ "spec/decorator_spec.rb",
34
+ "spec/examples_spec.rb",
35
+ "spec/simple_state_machine_spec.rb",
36
+ "spec/spec.opts",
37
+ "spec/spec_helper.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/p8/simple_state_machine}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.7}
43
+ s.summary = %q{A statemachine that focuses on events instead of states}
44
+ s.test_files = [
45
+ "spec/active_record_spec.rb",
46
+ "spec/decorator_spec.rb",
47
+ "spec/examples_spec.rb",
48
+ "spec/simple_state_machine_spec.rb",
49
+ "spec/spec_helper.rb",
50
+ "examples/conversation.rb",
51
+ "examples/lamp.rb",
52
+ "examples/relationship.rb",
53
+ "examples/traffic_light.rb",
54
+ "examples/user.rb"
55
+ ]
56
+
57
+ if s.respond_to? :specification_version then
58
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
59
+ s.specification_version = 3
60
+
61
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
62
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
63
+ else
64
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
65
+ end
66
+ else
67
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
68
+ end
69
+ end
70
+
@@ -0,0 +1,128 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'rubygems'
4
+ gem 'activerecord', '>= 1.15.4.7794'
5
+ require 'active_record'
6
+ require 'examples/user'
7
+
8
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
9
+
10
+ def setup_db
11
+ ActiveRecord::Schema.define(:version => 1) do
12
+ create_table :users do |t|
13
+ t.column :id, :integer
14
+ t.column :name, :string
15
+ t.column :state, :string
16
+ t.column :activation_code, :string
17
+ t.column :created_at, :datetime
18
+ t.column :updated_at, :datetime
19
+ end
20
+ end
21
+ end
22
+
23
+ def teardown_db
24
+ ActiveRecord::Base.connection.tables.each do |table|
25
+ ActiveRecord::Base.connection.drop_table(table)
26
+ end
27
+ end
28
+
29
+ describe User do
30
+
31
+ before do
32
+ setup_db
33
+ end
34
+
35
+ after do
36
+ teardown_db
37
+ end
38
+
39
+ it "has a default state" do
40
+ User.new.should be_new
41
+ end
42
+
43
+ describe "events" do
44
+ it "persists transitions" do
45
+ user = User.create!(:name => 'name')
46
+ user.invite.should == true
47
+ User.find(user.id).should be_invited
48
+ User.find(user.id).activation_code.should_not be_nil
49
+ end
50
+
51
+ it "persists transitions with !" do
52
+ user = User.create!(:name => 'name')
53
+ user.invite!.should == true
54
+ User.find(user.id).should be_invited
55
+ User.find(user.id).activation_code.should_not be_nil
56
+ end
57
+
58
+ it "raises an error if an invalid state_transition is called" do
59
+ user = User.create!(:name => 'name')
60
+ l = lambda { user.confirm_invitation 'abc' }
61
+ l.should raise_error(RuntimeError, "You cannot 'confirm_invitation' when state is 'new'")
62
+ end
63
+
64
+ it "returns false if event called and record is invalid" do
65
+ user = User.new
66
+ user.should_not be_valid
67
+ user.invite.should == false
68
+ end
69
+
70
+ it "keeps state if event called and record is invalid" do
71
+ user = User.new
72
+ user.should_not be_valid
73
+ user.invite.should == false
74
+ user.should be_new
75
+ end
76
+
77
+ it "raises a RecordInvalid event if called with ! and record is invalid" do
78
+ user = User.new
79
+ user.should_not be_valid
80
+ l = lambda { user.invite! }
81
+ l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
82
+ end
83
+
84
+ it "keeps state if event! called and record is invalid" do
85
+ user = User.new
86
+ user.should_not be_valid
87
+ begin
88
+ user.invite!
89
+ rescue ActiveRecord::RecordInvalid;end
90
+ user.should be_new
91
+ end
92
+
93
+ it "returns falls if record is valid but event adds errors" do
94
+ user = User.create!(:name => 'name')
95
+ user.invite!
96
+ user.should be_valid
97
+ r = user.confirm_invitation('x').should == false
98
+ end
99
+
100
+ it "keeps state if record is valid but event adds errors" do
101
+ user = User.create!(:name => 'name')
102
+ user.invite!
103
+ user.should be_valid
104
+ user.confirm_invitation('x')
105
+ user.should be_invited
106
+ end
107
+
108
+ it "raises a RecordInvalid if record is valid but event adds errors" do
109
+ user = User.create!(:name => 'name')
110
+ user.invite!
111
+ user.should be_valid
112
+ l = lambda { user.confirm_invitation!('x') }
113
+ l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Activation code Invalid")
114
+ end
115
+
116
+ it "keeps state if record is valid but event adds errors" do
117
+ user = User.create!(:name => 'name')
118
+ user.invite!
119
+ user.should be_valid
120
+ begin
121
+ user.confirm_invitation!('x')
122
+ rescue ActiveRecord::RecordInvalid;end
123
+ user.should be_invited
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class WithoutEventMethods
4
+ extend SimpleStateMachine
5
+
6
+ def initialize
7
+ self.state = 'unread'
8
+ end
9
+ event :view, :unread => :read
10
+
11
+ end
12
+
13
+ class WithPredefinedStateHelperMethods
14
+ extend SimpleStateMachine
15
+
16
+ def initialize
17
+ self.state = 'unread'
18
+ end
19
+ event :view, :unread => :read
20
+
21
+ def unread?
22
+ raise "blah"
23
+ end
24
+ end
25
+
26
+
27
+ describe SimpleStateMachine::Decorator do
28
+
29
+ it "defines state_helper_methods for all states" do
30
+ TrafficLight.new.green?.should == true
31
+ TrafficLight.new.orange?.should == false
32
+ TrafficLight.new.red?.should == false
33
+ end
34
+
35
+ it "does not define an state_helper_method if it already exists" do
36
+ l = lambda { WithPredefinedStateHelperMethods.new.unread? }
37
+ l.should raise_error(RuntimeError, 'blah')
38
+ end
39
+
40
+ it "defines an event method if it doesn't exist" do
41
+ WithoutEventMethods.new.view
42
+ end
43
+
44
+ end
@@ -0,0 +1,59 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Examples" do
4
+ describe "TrafficLight" do
5
+ it "changes to the next state" do
6
+ tl = TrafficLight.new
7
+ tl.should be_green
8
+ tl.change_state
9
+ tl.should be_orange
10
+ tl.change_state
11
+ tl.should be_red
12
+ tl.change_state
13
+ tl.should be_green
14
+ end
15
+ end
16
+
17
+ describe "Lamp" do
18
+ it "changes between :on and :off" do
19
+ lamp = Lamp.new
20
+ lamp.should be_off
21
+ lamp.push_button1
22
+ lamp.should be_on
23
+ lamp.push_button2
24
+ lamp.should be_off
25
+ lamp.push_button2
26
+ lamp.should be_on
27
+ lamp.push_button1
28
+ lamp.should be_off
29
+ end
30
+ end
31
+
32
+ describe "Conversation" do
33
+ it "is :unread by default" do
34
+ conversation = Conversation.new
35
+ conversation.should be_unread
36
+ end
37
+
38
+ it "changes to read on view" do
39
+ conversation = Conversation.new
40
+ conversation.view
41
+ conversation.should be_read
42
+ end
43
+
44
+ it "changes to closed on close" do
45
+ conversation = Conversation.new
46
+ conversation.close
47
+ conversation.should be_closed
48
+ end
49
+
50
+ it "changes to closed on close if :read" do
51
+ conversation = Conversation.new
52
+ conversation.view
53
+ conversation.close
54
+ conversation.should be_closed
55
+ end
56
+ end
57
+
58
+ end
59
+
@@ -0,0 +1,52 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class SimpleExample
4
+ extend SimpleStateMachine
5
+ attr_reader :event2_called
6
+ def initialize
7
+ self.state = 'state1'
8
+ end
9
+ event :event1, :state1 => :state2
10
+ def event2
11
+ @event2_called = true
12
+ 'event2'
13
+ end
14
+ event :event2, :state2 => :state3
15
+ end
16
+
17
+ describe SimpleStateMachine do
18
+
19
+ it "has a default state" do
20
+ SimpleExample.new.state.should == 'state1'
21
+ end
22
+
23
+ describe "events" do
24
+ it "raise an error if an invalid state_transition is called" do
25
+ example = SimpleExample.new
26
+ lambda { example.event2 }.should raise_error(RuntimeError, "You cannot 'event2' when state is 'state1'")
27
+ end
28
+
29
+ it "return nil" do
30
+ example = SimpleExample.new
31
+ example.event1.should == nil
32
+ example.event2.should == 'event2'
33
+ end
34
+
35
+ it "calls existing methods" do
36
+ example = SimpleExample.new
37
+ example.event1
38
+ example.event2
39
+ example.event2_called.should == true
40
+ end
41
+
42
+ end
43
+
44
+ describe "state_machine" do
45
+ it "has a next_state method" do
46
+ example = SimpleExample.new
47
+ example.state_machine.next_state('event1').should == 'state2'
48
+ example.state_machine.next_state('event2').should be_nil
49
+ end
50
+ end
51
+
52
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --debugger
3
+ --backtrace
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'simple_state_machine'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ require 'examples/conversation'
8
+ require 'examples/lamp'
9
+ require 'examples/traffic_light'
10
+
11
+ Spec::Runner.configure do |config|
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_state_machine
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 0
10
+ version: 0.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Marek de Heus
14
+ - Petrik de Heus
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-06-28 00:00:00 +02:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rspec
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 13
31
+ segments:
32
+ - 1
33
+ - 2
34
+ - 9
35
+ version: 1.2.9
36
+ type: :development
37
+ version_requirements: *id001
38
+ description: A simple DSL to decorate existing methods with logic that guards state transitions.
39
+ email:
40
+ - FIX@example.com
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files:
46
+ - LICENSE
47
+ - README.rdoc
48
+ files:
49
+ - LICENSE
50
+ - README.rdoc
51
+ - Rakefile
52
+ - VERSION
53
+ - examples/conversation.rb
54
+ - examples/lamp.rb
55
+ - examples/relationship.rb
56
+ - examples/traffic_light.rb
57
+ - examples/user.rb
58
+ - lib/simple_state_machine.rb
59
+ - lib/simple_state_machine/simple_state_machine.rb
60
+ - simple_state_machine.gemspec
61
+ - spec/active_record_spec.rb
62
+ - spec/decorator_spec.rb
63
+ - spec/examples_spec.rb
64
+ - spec/simple_state_machine_spec.rb
65
+ - spec/spec.opts
66
+ - spec/spec_helper.rb
67
+ has_rdoc: true
68
+ homepage: http://github.com/p8/simple_state_machine
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options:
73
+ - --charset=UTF-8
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ requirements: []
95
+
96
+ rubyforge_project:
97
+ rubygems_version: 1.3.7
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: A statemachine that focuses on events instead of states
101
+ test_files:
102
+ - spec/active_record_spec.rb
103
+ - spec/decorator_spec.rb
104
+ - spec/examples_spec.rb
105
+ - spec/simple_state_machine_spec.rb
106
+ - spec/spec_helper.rb
107
+ - examples/conversation.rb
108
+ - examples/lamp.rb
109
+ - examples/relationship.rb
110
+ - examples/traffic_light.rb
111
+ - examples/user.rb