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 +1 -1
- data/lib/state_manager/adapters/active_record.rb +51 -8
- data/lib/state_manager/base.rb +25 -13
- data/lib/state_manager/state.rb +3 -2
- data/state_manager.gemspec +2 -2
- data/test/adapters/active_record_test.rb +34 -1
- data/test/helper.rb +18 -0
- data/test/plugins/delayed_job_test.rb +14 -22
- metadata +3 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
data/lib/state_manager/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
data/lib/state_manager/state.rb
CHANGED
@@ -87,11 +87,12 @@ module StateManager
|
|
87
87
|
!!self.class.specification.events[name]
|
88
88
|
end
|
89
89
|
|
90
|
-
def
|
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
|
data/state_manager.gemspec
CHANGED
@@ -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
|
+
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-
|
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.
|
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(
|
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.
|
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-
|
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:
|
274
|
+
hash: 1941568341528515178
|
275
275
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
276
276
|
none: false
|
277
277
|
requirements:
|