simple_state_machine 0.0.0 → 0.1.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/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