golem_statemachine 0.9 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,11 +24,24 @@ easy.
24
24
 
25
25
  == 1. Installation
26
26
 
27
- Install as a Rails plugin:
27
+ Install as a Gem:
28
+
29
+ gem install golem_statemachine
30
+
31
+ Then, if you're using Rails 2.3.x, in your environment.rb:
32
+
33
+ config.gem 'golem_statemachine', :lib => 'golem'
34
+
35
+ And if you're using Rails 3.x, add it to your Gemfile:
36
+
37
+ gem 'golem_statemachine', :require => 'golem'
38
+
39
+ Or, install as a Rails plugin:
28
40
 
29
41
  script/plugin install git://github.com/zuk/golem_statemachine.git
30
42
 
31
- If using Golem in an ActiveRecord model:
43
+
44
+ If you're using Golem in an ActiveRecord model:
32
45
 
33
46
  class Example < ActiveRecord::Base
34
47
 
@@ -59,7 +72,7 @@ A light switch is initially in an "off" state. When you flip the switch, it tran
59
72
 
60
73
  Here's the UML state machine diagram of an on/off switch:
61
74
 
62
- http://cloud.github.com/downloads/zuk/golem_statemachine/on_off_switch_UML.png
75
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/on_off_switch_UML.png
63
76
 
64
77
  And here's what this looks like in Ruby code using Golem:
65
78
 
@@ -107,7 +120,7 @@ inside the <tt>define_statemachine</tt> block:
107
120
 
108
121
  Now to create some states:
109
122
 
110
- http://cloud.github.com/downloads/zuk/golem_statemachine/monster_1_UML.png
123
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/monster_1_UML.png
111
124
 
112
125
  class Monster
113
126
  include Golem
@@ -120,7 +133,7 @@ http://cloud.github.com/downloads/zuk/golem_statemachine/monster_1_UML.png
120
133
 
121
134
  And an event:
122
135
 
123
- http://cloud.github.com/downloads/zuk/golem_statemachine/monster_2_UML.png
136
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/monster_2_UML.png
124
137
 
125
138
  class Monster
126
139
  include Golem
@@ -139,7 +152,7 @@ The block for each state describes what will happen when a given event occurs. I
139
152
 
140
153
  Now to make things a bit more interesting:
141
154
 
142
- http://cloud.github.com/downloads/zuk/golem_statemachine/monster_3_UML.png
155
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/monster_3_UML.png
143
156
 
144
157
  class Monster
145
158
  include Golem
@@ -195,7 +208,7 @@ Finally, every state can have an <tt>enter</tt> and <tt>exit</tt> action that wi
195
208
  is entered or exited. This can be a block, a callback method (as a Symbol), or a Proc/lambda. Also, in the interest
196
209
  of leaner code, we rewrite things using more compact syntax:
197
210
 
198
- http://cloud.github.com/downloads/zuk/golem_statemachine/monster_4_UML.png
211
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/monster_4_UML.png
199
212
 
200
213
  class Monster
201
214
  include Golem
@@ -261,7 +274,7 @@ from {Scott W. Ambler's primer on UML2 State Machine Diagrams}[http://www.agilem
261
274
 
262
275
  The UML state machine diagram:
263
276
 
264
- http://cloud.github.com/downloads/zuk/golem_statemachine/seminar_enrollment_UML.png
277
+ http://github.com/zuk/golem_statemachine/raw/master/examples/UML/seminar_enrollment_UML.png
265
278
 
266
279
  The Ruby implementation (see blow for discussion):
267
280
 
@@ -0,0 +1,25 @@
1
+
2
+ $gemspec = Gem::Specification.new do |s|
3
+ s.name = 'golem_statemachine'
4
+ s.version = '0.9.5'
5
+ s.authors = ["Matt Zukowski"]
6
+ s.email = ["matt@roughest.net"]
7
+ s.homepage = 'http://github.com/zuk/golem_statemachine'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.summary = %q{Adds finite state machine behaviour to Ruby classes.}
10
+ s.description = %q{Adds finite state machine behaviour to Ruby classes. Meant as an alternative to acts_as_state_machine/AASM.}
11
+
12
+ s.files = `git ls-files`.split("\n")
13
+ s.test_files = `git ls-files -- spec`.split("\n")
14
+
15
+ s.require_path = "lib"
16
+
17
+ s.extra_rdoc_files = ["README.rdoc", "MIT-LICENSE"]
18
+
19
+ s.add_dependency("activesupport")
20
+
21
+ s.rdoc_options = [
22
+ '--quiet', '--title', 'Golem Statmeachine Docs', '--opname',
23
+ 'index.html', '--line-numbers', '--main', 'README.rdoc', '--inline-source'
24
+ ]
25
+ end
@@ -1,27 +1,45 @@
1
- require 'activesupport'
1
+ require 'active_support/all'
2
2
 
3
3
  require 'golem/dsl/state_machine_def'
4
-
4
+ require 'ruby-debug'
5
5
  module Golem
6
+
6
7
  def self.included(mod)
7
8
  mod.extend Golem::ClassMethods
8
9
 
9
10
  # Override the initialize method in the object we're imbuing with statemachine
10
11
  # functionality so that we can do statemachine initialization when the object
11
12
  # is instantiated.
13
+ #
14
+ # For ActiveRecord, we use the after_initialize callback instead.
15
+ # FIXME: should use an ActiveRecord::Observer here since this will conflict with
16
+ # any user-set after_initialize callback.
12
17
  mod.class_eval do
13
- alias_method :_initialize, :initialize
14
- def initialize(*args)
15
- # call the original initialize
16
- _initialize(*args)
18
+ if Object.const_defined?('ActiveRecord') && mod < ActiveRecord::Base
19
+
20
+ after_initialize do
21
+ if respond_to?(:statemachines)
22
+ self.statemachines.each{|name, sm| sm.init(self)}
23
+ end
24
+ end
17
25
 
18
- if respond_to?(:statemachines)
19
- self.statemachines.each{|name, sm| sm.init(self, *args)}
26
+ else
27
+
28
+ alias_method :_initialize, :initialize
29
+
30
+ def initialize(*args)
31
+ # call the original initialize
32
+ _initialize(*args)
33
+
34
+ if respond_to?(:statemachines)
35
+ self.statemachines.each{|name, sm| sm.init(self)}
36
+ end
20
37
  end
38
+
21
39
  end
22
40
  end
23
41
  end
24
-
42
+
25
43
  module ClassMethods
26
44
  def define_statemachine(statemachine_name = nil, options = {}, &block)
27
45
  default_statemachine_name = :statemachine
@@ -64,9 +82,14 @@ module Golem
64
82
 
65
83
  # state reader
66
84
  define_method("#{state_attribute}".to_sym) do
85
+ # TODO: the second two cases here should be collapsed into the first
67
86
  case
68
- when state_attribute.respond_to?(:call)
69
- state = state_attribute.call(self)
87
+ when statemachine.state_attribute_reader
88
+ if statemachine.state_attribute_reader.respond_to?(:call)
89
+ state = statemachine.state_attribute_reader.call(self)
90
+ else
91
+ state = self.send(statemachine.state_attribute_reader)
92
+ end
70
93
  when Object.const_defined?('ActiveRecord') && self.kind_of?(ActiveRecord::Base)
71
94
  state = self[state_attribute.to_s] && self[state_attribute.to_s].to_sym
72
95
  else
@@ -89,14 +112,31 @@ module Golem
89
112
  new_state = new_state.to_sym
90
113
  raise ArgumentError, "#{new_state.inspect} is not a valid state for #{statemachine}!" unless statemachine.states[new_state]
91
114
 
115
+ # transition takes care of calling on_exit, so don't do it if we're in the middle of a transition
116
+ unless statemachine.is_transitioning?
117
+ from_state_obj = statemachine.states[self.send("#{state_attribute}")]
118
+ from_state_obj.callbacks[:on_exit].call(self) if from_state_obj.callbacks[:on_exit]
119
+ end
120
+
121
+ # TODO: the second two cases here whould be collapsed into the first
92
122
  case
93
- when state_attribute.respond_to?(:call)
94
- state_attribute.call(self, new_state)
123
+ when statemachine.state_attribute_writer
124
+ if statemachine.state_attribute_writer.respond_to?(:call)
125
+ statemachine.state_attribute_writer.call(self, new_state)
126
+ else
127
+ self.send(statemachine.state_attribute_writer, new_state)
128
+ end
95
129
  when Object.const_defined?('ActiveRecord') && self.kind_of?(ActiveRecord::Base)
96
130
  self[state_attribute.to_s] = new_state.to_s # store as String rather than Symbol to prevent serialization weirdness
97
131
  else
98
132
  self.instance_variable_set("@#{state_attribute}", new_state)
99
133
  end
134
+
135
+ # transition takes care of calling on_entry, so don't do it if we're in the middle of a transition
136
+ unless statemachine.is_transitioning?
137
+ new_state_obj = statemachine.states[new_state]
138
+ new_state_obj.callbacks[:on_enter].call(self) if new_state_obj.callbacks[:on_enter]
139
+ end
100
140
  end
101
141
 
102
142
  validate :check_for_transition_errors if respond_to? :validate
@@ -13,6 +13,15 @@ module Golem
13
13
  @machine = machine
14
14
  @state = @machine.get_or_define_state(state_name)
15
15
  @options = options
16
+
17
+ if options[:enter]
18
+ @state.callbacks[:on_enter] = Golem::Model::Callback.new(options[:enter])
19
+ end
20
+
21
+ if options[:exit]
22
+ @state.callbacks[:on_exit] = Golem::Model::Callback.new(options[:exit])
23
+ end
24
+
16
25
  instance_eval(&block) if block
17
26
  end
18
27
 
@@ -21,11 +30,13 @@ module Golem
21
30
  end
22
31
 
23
32
  def enter(callback = nil, &block)
33
+ raise Golem::DefinitionSyntaxError, "Enter action already set." if @state.callbacks[:on_enter]
24
34
  raise Golem::DefinitionSyntaxError, "Provide either a callback method or a block, not both." if callback && block
25
35
  @state.callbacks[:on_enter] = Golem::Model::Callback.new(block || callback)
26
36
  end
27
37
 
28
38
  def exit(callback = nil, &block)
39
+ raise Golem::DefinitionSyntaxError, "Exit action already set." if @state.callbacks[:on_exit]
29
40
  raise Golem::DefinitionSyntaxError, "Provide either a callback method or a block, not both." if callback && block
30
41
  @state.callbacks[:on_exit] = Golem::Model::Callback.new(block || callback)
31
42
  end
@@ -42,7 +42,17 @@ module Golem
42
42
 
43
43
  # Sets the state_attribute name.
44
44
  def state_attribute=(attribute)
45
- @state_attribute = attribute
45
+ @machine.state_attribute = attribute
46
+ end
47
+
48
+ # Sets the state_attribute reader.
49
+ def state_attribute_reader(reader = nil)
50
+ @machine.state_attribute_reader = reader
51
+ end
52
+
53
+ # Sets the state_attribute writer.
54
+ def state_attribute_writer(writer = nil)
55
+ @machine.state_attribute_writer = writer
46
56
  end
47
57
 
48
58
  def on_all_transitions(callback = nil, &block)
@@ -10,6 +10,8 @@ module Golem
10
10
  class StateMachine
11
11
  attr_accessor :name
12
12
  attr_accessor :state_attribute
13
+ attr_accessor :state_attribute_reader
14
+ attr_accessor :state_attribute_writer
13
15
  attr_reader :states
14
16
  attr_reader :events
15
17
  attr_accessor :transition_errors
@@ -26,6 +28,7 @@ module Golem
26
28
  @events = Golem::Util::ElementCollection.new(Golem::Model::Event)
27
29
  @transition_errors = []
28
30
  @throw_exceptions = false
31
+ @is_transitioning = false
29
32
  end
30
33
 
31
34
  def initial_state
@@ -44,6 +47,11 @@ module Golem
44
47
  def all_events
45
48
  @events
46
49
  end
50
+
51
+ # true if this statemachine is currently in the middle of a transition
52
+ def is_transitioning?
53
+ @is_transitioning
54
+ end
47
55
 
48
56
  def get_current_state_of(obj)
49
57
  obj.send(state_attribute)
@@ -53,13 +61,9 @@ module Golem
53
61
  obj.send("#{state_attribute}=".to_sym, state)
54
62
  end
55
63
 
56
- def init(obj, *args)
64
+ def init(obj)
57
65
  # set the initial state
58
- set_current_state_of(obj, initial_state)
59
-
60
- # call the on_entry callback for the initial state (if defined)
61
- init_state = states[get_current_state_of(obj)]
62
- init_state.callbacks[:on_enter].call(obj, *args) if init_state && init_state.callbacks[:on_enter]
66
+ set_current_state_of(obj, get_current_state_of(obj) || initial_state)
63
67
  end
64
68
 
65
69
  def fire_event_with_exceptions(obj, event, *args)
@@ -79,9 +83,11 @@ module Golem
79
83
  on_all_events.call(obj, event, args) if on_all_events
80
84
 
81
85
  if transition
86
+ @is_transitioning = true
87
+
82
88
  before_state = states[get_current_state_of(obj)]
83
89
  before_state.callbacks[:on_exit].call(obj, *args) if before_state.callbacks[:on_exit]
84
-
90
+
85
91
  set_current_state_of(obj, transition.to.name)
86
92
  transition.callbacks[:on_transition].call(obj, *args) if transition.callbacks[:on_transition]
87
93
  on_all_transitions.call(obj, event, transition, *args) if on_all_transitions
@@ -89,12 +95,23 @@ module Golem
89
95
  after_state = states[get_current_state_of(obj)]
90
96
  after_state.callbacks[:on_enter].call(obj, *args) if after_state.callbacks[:on_enter]
91
97
 
98
+ @is_transitioning = false
99
+
92
100
  save_result = true
93
- if @throw_exceptions
94
- save_result = obj.save! if obj.respond_to?(:save!)
95
- else
96
- save_result = obj.save if obj.respond_to?(:save)
101
+ if obj.respond_to?(:save!)
102
+ if @throw_exceptions
103
+ save_result = obj.save!
104
+ else
105
+ (save_result = obj.save!) rescue return false
106
+ end
107
+ elsif obj.respond_to?(:save)
108
+ if @throw_exceptions
109
+ save_result = obj.save
110
+ else
111
+ (save_result = obj.save) rescue return false
112
+ end
97
113
  end
114
+
98
115
  return save_result
99
116
  else
100
117
  return false
@@ -10,9 +10,7 @@ rescue
10
10
  require 'sqlite3'
11
11
  end
12
12
 
13
- require 'activerecord'
14
-
15
- require 'ruby-debug'
13
+ require 'active_record'
16
14
 
17
15
  class ActiveRecordTest < Test::Unit::TestCase
18
16
 
@@ -45,7 +43,7 @@ class ActiveRecordTest < Test::Unit::TestCase
45
43
  def teardown
46
44
  self.class.send(:remove_const, :Foo)
47
45
  end
48
-
46
+
49
47
  def test_restore_state
50
48
  foo = Foo.create(
51
49
  :state => 'b',
@@ -178,6 +176,55 @@ class ActiveRecordTest < Test::Unit::TestCase
178
176
  foo = Foo.create
179
177
  end
180
178
  end
179
+
180
+
181
+ def test_fire_entry_action_on_restore_state
182
+ foo = Foo.create(
183
+ :state => 'b',
184
+ :alpha_state => 'c',
185
+ :status => 'd'
186
+ )
187
+
188
+ Foo.instance_eval do
189
+ define_statemachine do
190
+ initial_state :a
191
+ state :a
192
+ state :b, :enter => proc{|foo| foo.instance_variable_set(:@woot, "yup")}
193
+ state :c
194
+ state :d
195
+ end
196
+
197
+ define_statemachine(:alpha) do
198
+ initial_state :a
199
+ state :a
200
+ state :b, :enter => proc{|foo| foo.instance_variable_set(:@woot, "nope")}
201
+ state :c
202
+ state :d
203
+ end
204
+
205
+ define_statemachine(:beta) do
206
+ state_attribute(:status)
207
+ initial_state :a
208
+ state :a, :enter => proc{|foo| foo.instance_variable_set(:@woot, "poop")}
209
+ state :b
210
+ state :c
211
+ state :d
212
+ end
213
+ end
214
+
215
+ foo = Foo.find(foo.id)
216
+
217
+ assert_equal "yup", foo.instance_variable_get(:@woot)
218
+
219
+ # check that initial state works too
220
+ foo = Foo.create
221
+
222
+ assert_equal "poop", foo.instance_variable_get(:@woot)
223
+
224
+ foo = Foo.find(foo.id)
225
+
226
+ assert_equal "poop", foo.instance_variable_get(:@woot)
227
+ end
181
228
 
182
229
 
183
230
  def test_transaction_around_fire_event
@@ -111,6 +111,102 @@ class MonsterTest < Test::Unit::TestCase
111
111
  assert_equal :two, obj.alpha_state
112
112
  assert_equal :three, obj.foo
113
113
  end
114
+
115
+ def test_define_state_attribute_reader_symbol
116
+ @klass.instance_eval do
117
+ define_statemachine do
118
+ initial_state :one
119
+
120
+ state_attribute_reader :foo
121
+
122
+ state :worked
123
+ end
124
+
125
+ define_method(:foo) do
126
+ return :worked
127
+ end
128
+ end
129
+
130
+ obj = @klass.new
131
+ assert_equal :worked, obj.state
132
+ end
133
+
134
+ def test_define_state_attribute_reader_proc
135
+ @klass.instance_eval do
136
+ define_statemachine do
137
+ initial_state :one
138
+
139
+ state_attribute_reader(Proc.new do |obj|
140
+ obj.foo
141
+ end)
142
+
143
+ state :worked
144
+ end
145
+
146
+ define_method(:foo) do
147
+ return :worked
148
+ end
149
+ end
150
+
151
+ obj = @klass.new
152
+ assert_equal :worked, obj.state
153
+ end
154
+
155
+ def test_define_state_attribute_writer_symbol
156
+ @klass.instance_eval do
157
+ define_statemachine do
158
+ initial_state :one
159
+
160
+ state_attribute_writer :foo=
161
+ state_attribute_reader :foo
162
+
163
+ state :worked
164
+ end
165
+
166
+ define_method(:foo=) do |new_state|
167
+ @foo = new_state
168
+ end
169
+ define_method(:foo) do
170
+ @foo
171
+ end
172
+ end
173
+
174
+ obj = @klass.new
175
+ assert_equal :one, obj.state
176
+
177
+ obj.state = :worked
178
+ assert_equal :worked, obj.state
179
+ assert_equal :worked, obj.instance_variable_get(:@foo)
180
+ end
181
+
182
+ def test_define_state_attribute_writer_proc
183
+ @klass.instance_eval do
184
+ define_statemachine do
185
+ initial_state :one
186
+
187
+ state_attribute_writer(Proc.new do |obj, new_state|
188
+ obj.foo = new_state
189
+ end)
190
+ state_attribute_reader :foo
191
+
192
+ state :worked
193
+ end
194
+
195
+ define_method(:foo=) do |new_state|
196
+ @foo = new_state
197
+ end
198
+ define_method(:foo) do
199
+ @foo
200
+ end
201
+ end
202
+
203
+ obj = @klass.new
204
+ assert_equal :one, obj.state
205
+
206
+ obj.state = :worked
207
+ assert_equal :worked, obj.state
208
+ assert_equal :worked, obj.instance_variable_get(:@foo)
209
+ end
114
210
 
115
211
 
116
212
  def test_define_states
@@ -2,4 +2,5 @@ require 'test/unit'
2
2
 
3
3
  $: << File.dirname(__FILE__)+'/../lib'
4
4
 
5
+ require 'rubygems'
5
6
  require 'golem'
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: golem_statemachine
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
5
- prerelease:
4
+ prerelease: false
6
5
  segments:
7
6
  - 0
8
7
  - 9
9
- version: "0.9"
8
+ - 5
9
+ version: 0.9.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Matt Zukowski
@@ -14,17 +14,16 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-04-28 00:00:00 Z
17
+ date: 2011-12-07 00:00:00 -05:00
18
+ default_executable:
18
19
  dependencies:
19
20
  - !ruby/object:Gem::Dependency
20
21
  name: activesupport
21
22
  prerelease: false
22
23
  requirement: &id001 !ruby/object:Gem::Requirement
23
- none: false
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- hash: 3
28
27
  segments:
29
28
  - 0
30
29
  version: "0"
@@ -45,10 +44,17 @@ files:
45
44
  - MIT-LICENSE
46
45
  - README.rdoc
47
46
  - Rakefile
47
+ - examples/UML/monster_1_UML.png
48
+ - examples/UML/monster_2_UML.png
49
+ - examples/UML/monster_3_UML.png
50
+ - examples/UML/monster_4_UML.png
51
+ - examples/UML/on_off_switch_UML.png
52
+ - examples/UML/seminar_enrollment_UML.png
48
53
  - examples/document.rb
49
54
  - examples/monster.rb
50
55
  - examples/seminar.rb
51
56
  - examples/seminar_enrollment.rb
57
+ - golem_statemachine.gemspec
52
58
  - init.rb
53
59
  - install.rb
54
60
  - lib/golem.rb
@@ -71,9 +77,9 @@ files:
71
77
  - test/problematic_test.rb
72
78
  - test/seminar_test.rb
73
79
  - test/statemachine_assertions.rb
74
- - test/test.db
75
80
  - test/test_helper.rb
76
81
  - uninstall.rb
82
+ has_rdoc: true
77
83
  homepage: http://github.com/zuk/golem_statemachine
78
84
  licenses: []
79
85
 
@@ -91,27 +97,23 @@ rdoc_options:
91
97
  require_paths:
92
98
  - lib
93
99
  required_ruby_version: !ruby/object:Gem::Requirement
94
- none: false
95
100
  requirements:
96
101
  - - ">="
97
102
  - !ruby/object:Gem::Version
98
- hash: 3
99
103
  segments:
100
104
  - 0
101
105
  version: "0"
102
106
  required_rubygems_version: !ruby/object:Gem::Requirement
103
- none: false
104
107
  requirements:
105
108
  - - ">="
106
109
  - !ruby/object:Gem::Version
107
- hash: 3
108
110
  segments:
109
111
  - 0
110
112
  version: "0"
111
113
  requirements: []
112
114
 
113
115
  rubyforge_project:
114
- rubygems_version: 1.7.2
116
+ rubygems_version: 1.3.6
115
117
  signing_key:
116
118
  specification_version: 3
117
119
  summary: Adds finite state machine behaviour to Ruby classes.