state_manager 0.2.8 → 0.2.9

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.8
1
+ 0.2.9
@@ -1,6 +1,9 @@
1
1
  module StateManager
2
2
  module Adapters
3
3
  module ActiveRecord
4
+
5
+ class DirtyTransition < StandardError; end;
6
+
4
7
  include Base
5
8
 
6
9
  def self.matching_ancestors
@@ -11,7 +14,15 @@ module StateManager
11
14
 
12
15
  def self.included(base)
13
16
  # Make sure that the model is in a valid state before it is saved
14
- base.before_validation :_validate_states
17
+ base.before_validation do
18
+ validate_states!
19
+ end
20
+ base.before_save do
21
+ state_managers.values.map(&:before_save)
22
+ end
23
+ base.after_save do
24
+ state_managers.values.map(&:after_save)
25
+ end
15
26
 
16
27
  base.extend(ClassMethods)
17
28
  end
@@ -42,14 +53,46 @@ module StateManager
42
53
 
43
54
  module ManagerMethods
44
55
 
45
- def write_state(value)
46
- # Since new objects will have a nil state value, this method will be called
47
- # during instantiation. We want to hold off on writing to the database.
48
- if resource.new_record?
49
- resource.send :write_attribute, self.class._state_property, value.path
50
- else
51
- resource.send :update_attribute, self.class._state_property, value.path
56
+ attr_accessor :pending_transition
57
+
58
+ def self.included(base)
59
+ base.class_eval do
60
+ alias_method :_run_before_callbacks, :run_before_callbacks
61
+ alias_method :_run_after_callbacks, :run_after_callbacks
62
+
63
+ # In the AR use case, we don't want to run any callbacks
64
+ # until the model has been saved
65
+ def run_before_callbacks(*args)
66
+ self.pending_transition = args
67
+ end
68
+
69
+ def run_after_callbacks(*args)
70
+ end
52
71
  end
72
+ end
73
+
74
+ def transition_to(path)
75
+ raise(DirtyTransition, "Only one state transition may be performed before saving a record. This error could be caused by the record being initialized to a default state.") if pending_transition
76
+ super(path)
77
+ end
78
+
79
+ def before_save
80
+ return unless pending_transition
81
+ _run_before_callbacks(*pending_transition)
82
+ end
83
+
84
+ def after_save
85
+ return unless pending_transition
86
+ _run_after_callbacks(*pending_transition)
87
+ self.pending_transition = nil
88
+ end
89
+
90
+ def write_state(value)
91
+ resource.send :write_attribute, self.class._state_property, value.path
92
+ end
93
+
94
+ def persist_state
95
+ resource.save
53
96
  end
54
97
 
55
98
  end
@@ -55,18 +55,12 @@ module StateManager
55
55
  # a transition to the current state?
56
56
  to_state = enter_states.last || from_state
57
57
 
58
- # Before Callbacks
59
- will_transition(from_state, to_state, current_event)
60
- exit_states.each{ |s| s.exit }
61
- enter_states.each{ |s| s.enter }
58
+ run_before_callbacks(from_state, to_state, current_event, enter_states, exit_states)
62
59
 
63
60
  # Set the state on the underlying resource
64
61
  self.current_state = to_state
65
62
 
66
- # After Callbacks
67
- exit_states.each{ |s| s.exited }
68
- enter_states.each{ |s| s.entered }
69
- did_transition(from_state, to_state, current_event)
63
+ run_after_callbacks(from_state, to_state, current_event, enter_states, exit_states)
70
64
  end
71
65
 
72
66
  def current_state
@@ -78,16 +72,19 @@ module StateManager
78
72
  write_state(value)
79
73
  end
80
74
 
81
- # Send an event to the current state.
82
- #
83
- # Unlike the regular send_event method, this method recursively walks the
84
- # path of states starting at the current state.
85
75
  def send_event!(name, *args)
76
+ result = send_event(name, *args)
77
+ persist_state
78
+ result
79
+ end
80
+
81
+ def send_event(name, *args)
86
82
  self.current_event = name
87
83
  state = find_state_for_event(name)
88
84
  raise(InvalidEvent, name) unless state
89
- state.send_event name, *args
85
+ result = state.perform_event name, *args
90
86
  self.current_event = nil
87
+ result
91
88
  end
92
89
 
93
90
  def respond_to_event?(name)
@@ -138,6 +135,9 @@ module StateManager
138
135
  resource.send self.class._state_property
139
136
  end
140
137
 
138
+ def persist_state
139
+ end
140
+
141
141
  def will_transition(from, to, event)
142
142
  end
143
143
 
@@ -175,6 +175,18 @@ module StateManager
175
175
 
176
176
  attr_accessor :current_event
177
177
 
178
+ def run_before_callbacks(from_state, to_state, current_event, enter_states, exit_states)
179
+ will_transition(from_state, to_state, current_event)
180
+ exit_states.each{ |s| s.exit }
181
+ enter_states.each{ |s| s.enter }
182
+ end
183
+
184
+ def run_after_callbacks(from_state, to_state, current_event, enter_states, exit_states)
185
+ exit_states.each{ |s| s.exited }
186
+ enter_states.each{ |s| s.entered }
187
+ did_transition(from_state, to_state, current_event)
188
+ end
189
+
178
190
  end
179
191
 
180
192
  end
@@ -87,11 +87,12 @@ module StateManager
87
87
  !!self.class.specification.events[name]
88
88
  end
89
89
 
90
- def send_event(name, *args)
90
+ def perform_event(name, *args)
91
91
  name = name.to_sym
92
92
  event = self.class.specification.events[name]
93
- send(name, *args) if respond_to?(name)
93
+ result = send(name, *args) if respond_to?(name)
94
94
  transition_to(event[:transitions_to]) if event[:transitions_to]
95
+ result
95
96
  end
96
97
 
97
98
  # Find all the states along the path
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "state_manager"
8
- s.version = "0.2.8"
8
+ s.version = "0.2.9"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Gordon Hempton"]
12
- s.date = "2012-07-30"
12
+ s.date = "2012-08-29"
13
13
  s.description = "Finite state machine implementation that keeps logic separate from model classes and supports sub-states."
14
14
  s.email = "ghempton@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -3,6 +3,9 @@ require 'helper'
3
3
  class ActiveRecordTest < Test::Unit::TestCase
4
4
 
5
5
  class PostStates < StateManager::Base
6
+ attr_accessor :before_callbacks_called
7
+ attr_accessor :after_callbacks_called
8
+
6
9
  state :unsubmitted do
7
10
  event :submit, :transitions_to => 'submitted.awaiting_review'
8
11
  end
@@ -20,6 +23,14 @@ class ActiveRecordTest < Test::Unit::TestCase
20
23
  end
21
24
  state :active
22
25
  state :rejected
26
+
27
+ def will_transition(*args)
28
+ self.before_callbacks_called = true
29
+ end
30
+
31
+ def did_transition(*args)
32
+ self.after_callbacks_called = true
33
+ end
23
34
  end
24
35
 
25
36
  class Post < ActiveRecord::Base
@@ -67,7 +78,12 @@ class ActiveRecordTest < Test::Unit::TestCase
67
78
  def test_persist_initial_state
68
79
  @resource = Post.find(1)
69
80
  assert_state 'unsubmitted'
70
- assert !@resource.changed?, "state should have been persisted"
81
+ assert !@resource.state_manager.before_callbacks_called
82
+ assert !@resource.state_manager.after_callbacks_called
83
+ assert @resource.changed?, "state should not have been persisted"
84
+ @resource.save
85
+ assert @resource.state_manager.before_callbacks_called
86
+ assert @resource.state_manager.after_callbacks_called
71
87
  end
72
88
 
73
89
  def test_initial_state_value
@@ -119,4 +135,21 @@ class ActiveRecordTest < Test::Unit::TestCase
119
135
  assert_equal 4, Post.active.count
120
136
  assert_equal 0, Post.rejected.count
121
137
  end
138
+
139
+ def test_multiple_transitions
140
+ @resource = Post.find(2)
141
+ @resource.submit!
142
+ assert_state 'submitted.awaiting_review'
143
+ @resource.review!
144
+ assert_state 'submitted.reviewing'
145
+ end
146
+
147
+ def test_dirty_transition
148
+ @resource = Post.find(2)
149
+ @resource.state_manager.send_event :submit
150
+ assert_state 'submitted.awaiting_review'
151
+ assert_raise(StateManager::Adapters::ActiveRecord::DirtyTransition) do
152
+ @resource.state_manager.send_event :review
153
+ end
154
+ end
122
155
  end
data/test/helper.rb CHANGED
@@ -14,6 +14,7 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
14
  require 'pry'
15
15
  require 'delayed_job_active_record'
16
16
  require 'state_manager'
17
+ require 'timecop'
17
18
 
18
19
  class Test::Unit::TestCase
19
20
 
@@ -26,4 +27,21 @@ class Test::Unit::TestCase
26
27
  assert_equal path, state_manager.current_state.path
27
28
  end
28
29
 
30
+ def teardown
31
+ ActiveRecord::Base.connection.disconnect!
32
+ Timecop.return
33
+ end
34
+
35
+ # Convince delayed job that the duration has passed and perform any jobs that
36
+ # need doing
37
+ def time_warp(duration)
38
+ Timecop.travel(duration.from_now)
39
+ Delayed::Worker.new.work_off
40
+
41
+ # Check for any errors inside the delayed job
42
+ jobs = Delayed::Job.where('last_error IS NOT NULL')
43
+ error = jobs.last && jobs.last.last_error
44
+ raise "Delayed job error: #{error}" if error
45
+ end
46
+
29
47
  end
@@ -63,28 +63,11 @@ class DelayedJobTest < Test::Unit::TestCase
63
63
  state_manager
64
64
  end
65
65
 
66
- def teardown
67
- ActiveRecord::Base.connection.disconnect!
68
- Timecop.return
69
- end
70
-
71
-
72
- # Convince delayed job that the duration has passed and perform any jobs that
73
- # need doing
74
- def time_warp(duration)
75
- Timecop.travel(duration.from_now)
76
- Delayed::Worker.new.work_off
77
-
78
- # Check for any errors inside the delayed job
79
- jobs = Delayed::Job.where('last_error IS NOT NULL')
80
- error = jobs.last && jobs.last.last_error
81
- raise "Delayed job error: #{error}" if error
82
- end
83
-
84
66
  def test_delayed_event
85
67
  @resource = Project.find(1)
86
-
87
68
  assert_state 'unsubmitted.initial'
69
+ assert_equal 0, Delayed::Job.count
70
+ @resource.save
88
71
  assert_equal 1, Delayed::Job.count
89
72
 
90
73
  time_warp(4.hours)
@@ -104,22 +87,23 @@ class DelayedJobTest < Test::Unit::TestCase
104
87
 
105
88
  def test_expired_event
106
89
  @resource = Project.find(1)
107
-
108
90
  assert_state 'unsubmitted.initial'
91
+ @resource.save
109
92
  assert_equal 1, Delayed::Job.count
110
93
 
111
94
  @resource.submit!
112
95
 
113
96
  assert_state 'submitted'
114
97
 
115
- time_warp(4.hours)
98
+ time_warp(1.month)
116
99
 
100
+ assert_equal 0, Delayed::Job.count
117
101
  assert_state 'submitted', 'should not have transitioned'
118
102
  end
119
103
 
120
104
  def test_event_name_clashes
121
105
  @resource = Project.find(1)
122
-
106
+ @resource.save
123
107
  assert_state 'unsubmitted.initial'
124
108
  assert_equal 1, Delayed::Job.count
125
109
 
@@ -133,4 +117,12 @@ class DelayedJobTest < Test::Unit::TestCase
133
117
  assert_state 'accepted', 'remind event should not have been triggered'
134
118
  end
135
119
 
120
+ def test_new_record
121
+ @resource = Project.create
122
+ assert_state 'unsubmitted.initial'
123
+ time_warp(1.day)
124
+ @resource.reload
125
+ assert_state 'unsubmitted.reminded'
126
+ end
127
+
136
128
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.2.9
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-30 00:00:00.000000000 Z
12
+ date: 2012-08-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -271,7 +271,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
271
271
  version: '0'
272
272
  segments:
273
273
  - 0
274
- hash: 443953156936998846
274
+ hash: 1941568341528515178
275
275
  required_rubygems_version: !ruby/object:Gem::Requirement
276
276
  none: false
277
277
  requirements: