simple_state_machine 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -22,10 +22,10 @@ state transitions.
22
22
 
23
23
 
24
24
  This has a couple of advantages:
25
+ - Encapsulate state transitions (no need for :guards),
25
26
  - Arguments can be passed to 'events',
26
27
  - 'events' can return a value, remember, it's just a method,
27
28
  - Validation errors can be set on the model if you are using ActiveRecord / ActiveModel,
28
- - Encapsulate state transitions (no need for :guards).
29
29
 
30
30
  To use the code, you need to do 3 things:
31
31
  - extend SimpleStateMachine,
@@ -34,7 +34,7 @@ To use the code, you need to do 3 things:
34
34
 
35
35
  ==== Example usage
36
36
 
37
- class Lamp
37
+ class LampWithHotelSwitch
38
38
 
39
39
  extend SimpleStateMachine
40
40
 
@@ -42,17 +42,17 @@ To use the code, you need to do 3 things:
42
42
  self.state = :off
43
43
  end
44
44
 
45
- def push_button1
46
- puts "click1: #{state}"
45
+ def push_switch_1
46
+ puts 'pushed switch 1 #{state}'
47
47
  end
48
- event :push_button1, :off => :on,
49
- :on => :off
48
+ event :push_switch_1, :off => :on,
49
+ :on => :off
50
50
 
51
- def push_button2
52
- puts "click1: #{state}"
51
+ def push_switch_2
52
+ puts 'pushed switch 2 #{state}'
53
53
  end
54
- event :push_button2, :off => :on,
55
- :on => :off
54
+ event :push_switch_2, :off => :on,
55
+ :on => :off
56
56
 
57
57
  end
58
58
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/examples/user.rb CHANGED
@@ -3,7 +3,7 @@ class User < ActiveRecord::Base
3
3
 
4
4
  validates_presence_of :name
5
5
 
6
- extend SimpleStateMachine
6
+ extend SimpleStateMachine::ActiveRecord
7
7
 
8
8
  def after_initialize
9
9
  self.state ||= 'new'
@@ -16,8 +16,8 @@ class User < ActiveRecord::Base
16
16
  event :invite, :new => :invited
17
17
 
18
18
  def confirm_invitation activation_code
19
- if activation_code != self.activation_code
20
- errors.add(:activation_code, 'Invalid')
19
+ if self.activation_code != activation_code
20
+ errors.add 'activation_code', 'is invalid'
21
21
  end
22
22
  end
23
23
  event :confirm_invitation, :invited => :active
@@ -34,4 +34,4 @@ class User < ActiveRecord::Base
34
34
  true
35
35
  end
36
36
 
37
- end
37
+ end
@@ -0,0 +1,51 @@
1
+ module SimpleStateMachine::ActiveRecord
2
+
3
+ include SimpleStateMachine::EventMixin
4
+
5
+ def state_machine_decorator subject
6
+ Decorator.new subject
7
+ end
8
+
9
+ class Decorator < SimpleStateMachine::Decorator
10
+
11
+ def decorate event_name, from, to
12
+ super event_name, from, to
13
+ unless @subject.method_defined?("#{event_name}_and_save")
14
+ @subject.send(:define_method, "#{event_name}_and_save") do |*args|
15
+ old_state = state
16
+ send "#{event_name}", *args
17
+ if save
18
+ return true
19
+ else
20
+ self.state = old_state
21
+ return false
22
+ end
23
+ end
24
+ end
25
+ unless @subject.method_defined?("#{event_name}_and_save!")
26
+ @subject.send(:define_method, "#{event_name}_and_save!") do |*args|
27
+ old_state = state
28
+ send "#{event_name}", *args
29
+ if !self.errors.entries.empty?
30
+ self.state = old_state
31
+ raise ActiveRecord::RecordInvalid.new(self)
32
+ end
33
+ begin
34
+ save!
35
+ rescue ActiveRecord::RecordInvalid
36
+ self.state = old_state
37
+ raise
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def define_state_setter_method; end
46
+
47
+ def define_state_getter_method; end
48
+
49
+ end
50
+
51
+ end
@@ -1,209 +1,149 @@
1
1
  module SimpleStateMachine
2
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
3
+ module EventMixin
15
4
 
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] ||= {}
5
+ def event event_name, state_transitions
27
6
  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)
7
+ state_machine_definition.add_transition(event_name, from, to)
8
+ state_machine_decorator(self).decorate( event_name, from, to)
30
9
  end
31
10
  end
32
11
 
33
- def inherits_from_active_record_base?(subject)
34
- subject.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
12
+ def state_machine_definition
13
+ @state_machine_definition ||= StateMachineDefinition.new
14
+ end
15
+
16
+ def state_machine_decorator subject
17
+ Decorator.new subject
35
18
  end
36
19
 
37
20
  end
21
+
22
+ include EventMixin
38
23
 
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
24
+ class StateMachineDefinition
61
25
 
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
26
+ def transitions
27
+ @transitions ||= {}
28
+ end
66
29
 
30
+ def add_transition event_name, from, to
31
+ transitions[event_name.to_s] ||= {}
32
+ transitions[event_name.to_s][from.to_s] = to.to_s
67
33
  end
68
34
 
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
35
+ end
36
+
37
+ class StateMachine
77
38
 
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
39
+ def initialize(subject)
40
+ @subject = subject
41
+ end
42
+
43
+ def next_state(event_name)
44
+ transitions[event_name.to_s][@subject.state]
45
+ end
46
+
47
+ def transition(event_name)
48
+ if to = next_state(event_name)
49
+ result = yield
50
+ # TODO refactor out to AR module
51
+ if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
52
+ if @subject.errors.entries.empty?
87
53
  @subject.state = to
88
- if event_name =~ /\!$/
89
- @subject.save! #TODO maybe save_without_validation!
90
- else
91
- @subject.save
92
- end
54
+ return true
55
+ else
56
+ return false
93
57
  end
94
58
  else
95
- illegal_event_callback event_name
59
+ @subject.state = to
60
+ return result
96
61
  end
62
+ else
63
+ illegal_event_callback event_name
97
64
  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
65
  end
66
+
67
+ private
68
+
69
+ def transitions
70
+ @subject.class.state_machine_definition.transitions
71
+ end
108
72
 
73
+ def illegal_event_callback event_name
74
+ # override with your own implementation, like setting errors in your model
75
+ raise "You cannot '#{event_name}' when state is '#{@subject.state}'"
76
+ end
77
+
109
78
  end
110
79
 
111
- module Decorator
112
- class Base
80
+ class Decorator
113
81
 
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
82
+ def initialize(subject)
83
+ @subject = subject
84
+ define_state_machine_method
85
+ define_state_getter_method
86
+ define_state_setter_method
87
+ end
127
88
 
128
- private
89
+ def decorate event_name, from, to
90
+ define_state_helper_method(from)
91
+ define_state_helper_method(to)
92
+ define_event_method(event_name)
93
+ decorate_event_method(event_name)
94
+ end
129
95
 
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
96
+ private
135
97
 
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
98
+ def define_state_machine_method
99
+ @subject.send(:define_method, "state_machine") do
100
+ @state_machine ||= StateMachine.new(self)
142
101
  end
102
+ end
143
103
 
144
- def define_event_method event_name
145
- unless @subject.method_defined?("#{event_name}")
146
- @subject.send(:define_method, "#{event_name}") {}
104
+ def define_state_helper_method state
105
+ unless @subject.method_defined?("#{state.to_s}?")
106
+ @subject.send(:define_method, "#{state.to_s}?") do
107
+ self.state == state.to_s
147
108
  end
148
109
  end
110
+ end
149
111
 
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
112
+ def define_event_method event_name
113
+ unless @subject.method_defined?("#{event_name}")
114
+ @subject.send(:define_method, "#{event_name}") {}
161
115
  end
116
+ end
162
117
 
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
118
+ def decorate_event_method event_name
119
+ # TODO put in transaction for activeRecord?
120
+ unless @subject.method_defined?("with_managed_state_#{event_name}")
121
+ @subject.send(:define_method, "with_managed_state_#{event_name}") do |*args|
122
+ return state_machine.transition(event_name) do
123
+ send("without_managed_state_#{event_name}", *args)
167
124
  end
168
125
  end
126
+ @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
127
+ @subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
169
128
  end
129
+ end
170
130
 
171
- def define_state_getter_method
172
- unless @subject.method_defined?('state')
173
- @subject.send(:define_method, 'state') do
174
- @state
175
- end
131
+ def define_state_setter_method
132
+ unless @subject.method_defined?('state=')
133
+ @subject.send(:define_method, 'state=') do |new_state|
134
+ @state = new_state.to_s
176
135
  end
177
136
  end
137
+ end
178
138
 
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
139
+ def define_state_getter_method
140
+ unless @subject.method_defined?('state')
141
+ @subject.send(:define_method, 'state') do
142
+ @state
188
143
  end
189
144
  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
145
  end
200
146
 
201
- def define_state_setter_method; end
202
-
203
- def define_state_getter_method; end
204
-
205
- end
206
-
207
147
  end
208
148
 
209
- end
149
+ end
@@ -1 +1,2 @@
1
- require 'simple_state_machine/simple_state_machine'
1
+ require 'simple_state_machine/simple_state_machine'
2
+ require 'simple_state_machine/active_record'
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{simple_state_machine}
8
- s.version = "0.0.0"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Marek de Heus", "Petrik de Heus"]
12
- s.date = %q{2010-06-28}
12
+ s.date = %q{2010-08-12}
13
13
  s.description = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
14
14
  s.email = ["FIX@example.com"]
15
15
  s.extra_rdoc_files = [
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "examples/traffic_light.rb",
28
28
  "examples/user.rb",
29
29
  "lib/simple_state_machine.rb",
30
+ "lib/simple_state_machine/active_record.rb",
30
31
  "lib/simple_state_machine/simple_state_machine.rb",
31
32
  "simple_state_machine.gemspec",
32
33
  "spec/active_record_spec.rb",
@@ -1,7 +1,7 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  require 'rubygems'
4
- gem 'activerecord', '>= 1.15.4.7794'
4
+ gem 'activerecord', '~> 2.3.5'
5
5
  require 'active_record'
6
6
  require 'examples/user'
7
7
 
@@ -40,44 +40,46 @@ describe User do
40
40
  User.new.should be_new
41
41
  end
42
42
 
43
+ # TODO needs nesting/grouping, seems to have some duplication
44
+
43
45
  describe "events" do
44
46
  it "persists transitions" do
45
47
  user = User.create!(:name => 'name')
46
- user.invite.should == true
48
+ user.invite_and_save.should == true
47
49
  User.find(user.id).should be_invited
48
50
  User.find(user.id).activation_code.should_not be_nil
49
51
  end
50
52
 
51
53
  it "persists transitions with !" do
52
54
  user = User.create!(:name => 'name')
53
- user.invite!.should == true
55
+ user.invite_and_save!.should == true
54
56
  User.find(user.id).should be_invited
55
57
  User.find(user.id).activation_code.should_not be_nil
56
58
  end
57
59
 
58
60
  it "raises an error if an invalid state_transition is called" do
59
61
  user = User.create!(:name => 'name')
60
- l = lambda { user.confirm_invitation 'abc' }
62
+ l = lambda { user.confirm_invitation_and_save 'abc' }
61
63
  l.should raise_error(RuntimeError, "You cannot 'confirm_invitation' when state is 'new'")
62
64
  end
63
65
 
64
66
  it "returns false if event called and record is invalid" do
65
67
  user = User.new
66
68
  user.should_not be_valid
67
- user.invite.should == false
69
+ user.invite_and_save.should == false
68
70
  end
69
71
 
70
72
  it "keeps state if event called and record is invalid" do
71
73
  user = User.new
72
74
  user.should_not be_valid
73
- user.invite.should == false
75
+ user.invite_and_save.should == false
74
76
  user.should be_new
75
77
  end
76
78
 
77
79
  it "raises a RecordInvalid event if called with ! and record is invalid" do
78
80
  user = User.new
79
81
  user.should_not be_valid
80
- l = lambda { user.invite! }
82
+ l = lambda { user.invite_and_save! }
81
83
  l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
82
84
  end
83
85
 
@@ -85,44 +87,43 @@ describe User do
85
87
  user = User.new
86
88
  user.should_not be_valid
87
89
  begin
88
- user.invite!
90
+ user.invite_and_save!
89
91
  rescue ActiveRecord::RecordInvalid;end
90
92
  user.should be_new
91
93
  end
92
94
 
93
- it "returns falls if record is valid but event adds errors" do
95
+ it "will inspect errors after event and reset state" do
94
96
  user = User.create!(:name => 'name')
95
- user.invite!
96
- user.should be_valid
97
- r = user.confirm_invitation('x').should == false
97
+ user.invite_and_save!
98
+ user.should be_invited
99
+ user.confirm_invitation('x')
100
+ user.errors.entries.should == [['activation_code', 'is invalid']]
101
+ user.should be_invited
98
102
  end
99
103
 
100
- it "keeps state if record is valid but event adds errors" do
104
+ it "returns false if event adds errors" do
101
105
  user = User.create!(:name => 'name')
102
- user.invite!
103
- user.should be_valid
104
- user.confirm_invitation('x')
105
- user.should be_invited
106
+ user.invite_and_save!
107
+ user.confirm_invitation('x').should == false
106
108
  end
107
109
 
108
- it "raises a RecordInvalid if record is valid but event adds errors" do
110
+ it "raises a RecordInvalid if 'event'_and_save! is called and event adds errors" do
109
111
  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")
112
+ user.invite_and_save!
113
+ l = lambda { user.confirm_invitation_and_save!('x') }
114
+ l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid")
114
115
  end
115
116
 
116
117
  it "keeps state if record is valid but event adds errors" do
117
118
  user = User.create!(:name => 'name')
118
- user.invite!
119
- user.should be_valid
119
+ user.invite_and_save!
120
+ user.should be_invited
120
121
  begin
121
- user.confirm_invitation!('x')
122
+ user.confirm_invitation_and_save!('x')
122
123
  rescue ActiveRecord::RecordInvalid;end
123
124
  user.should be_invited
124
125
  end
125
126
 
126
127
  end
127
128
 
128
- end
129
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 0
10
- version: 0.0.0
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Marek de Heus
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-06-28 00:00:00 +02:00
19
+ date: 2010-08-12 00:00:00 +02:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
@@ -56,6 +56,7 @@ files:
56
56
  - examples/traffic_light.rb
57
57
  - examples/user.rb
58
58
  - lib/simple_state_machine.rb
59
+ - lib/simple_state_machine/active_record.rb
59
60
  - lib/simple_state_machine/simple_state_machine.rb
60
61
  - simple_state_machine.gemspec
61
62
  - spec/active_record_spec.rb