simple_state_machine 0.0.0

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,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