golem_statemachine 0.9 → 0.9.5

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.
@@ -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.