stator 0.3.0 → 0.3.1
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.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/lib/stator/integration.rb +18 -15
- data/lib/stator/version.rb +6 -2
- data/spec/model_spec.rb +88 -67
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3ea9ce5a04f3d128aecd8f78788c7a06a49d2785dc3669a7c54a2c973fb01f2b
|
4
|
+
data.tar.gz: c638206a0fb9dedf3e53403d160425054be95b1f08223e6436239f500afb9eb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e32e3867650801979aefd648ebc2184e0a9cf9357c0fa08146a963308c35ec610b3c5ac4a9439149acfa086b98957dd1006484310b60ab0fb18fad56a26bfe51
|
7
|
+
data.tar.gz: b8fa99e1f0e0bdf6a3832395d9de0730ee3a990994509c2dc8b30aa6f5ee06ca1af3114b4f9c00899cb83b15515cca142841a521ee4f0532bc863ba4d03532fe
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.5.1
|
data/lib/stator/integration.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Stator
|
2
4
|
class Integration
|
3
5
|
|
4
|
-
delegate :states, :
|
5
|
-
delegate :transitions, :
|
6
|
-
delegate :namespace, :
|
6
|
+
delegate :states, to: :@machine
|
7
|
+
delegate :transitions, to: :@machine
|
8
|
+
delegate :namespace, to: :@machine
|
7
9
|
|
8
10
|
def initialize(machine, record)
|
9
11
|
@machine = machine
|
@@ -11,7 +13,7 @@ module Stator
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def state=(new_value)
|
14
|
-
@record.send("#{@machine.field}=",
|
16
|
+
@record.send("#{@machine.field}=", new_value)
|
15
17
|
end
|
16
18
|
|
17
19
|
def state
|
@@ -35,11 +37,11 @@ module Stator
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def validate_transition
|
38
|
-
return unless
|
40
|
+
return unless state_changed?
|
39
41
|
return if @machine.skip_validations
|
40
42
|
|
41
|
-
was =
|
42
|
-
is =
|
43
|
+
was = state_was
|
44
|
+
is = state
|
43
45
|
|
44
46
|
if @record.new_record?
|
45
47
|
invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
|
@@ -48,7 +50,7 @@ module Stator
|
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
#
|
53
|
+
# TODO: i18n
|
52
54
|
def invalid_state!
|
53
55
|
@record.errors.add(@machine.field, "is not a valid state")
|
54
56
|
end
|
@@ -60,8 +62,8 @@ module Stator
|
|
60
62
|
def track_transition
|
61
63
|
return if @machine.skip_transition_tracking
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
+
attempt_to_track_state(state)
|
66
|
+
attempt_to_track_state_changed_timestamp
|
65
67
|
|
66
68
|
true
|
67
69
|
end
|
@@ -82,7 +84,6 @@ module Stator
|
|
82
84
|
|
83
85
|
# grab all the states and their timestamps that occur on or after state_at and on or before the time in question
|
84
86
|
later_states = all_states.map do |s|
|
85
|
-
|
86
87
|
next if state == s
|
87
88
|
|
88
89
|
at = @record.send("#{s}_#{@machine.field}_at")
|
@@ -98,8 +99,8 @@ module Stator
|
|
98
99
|
return true if later_states.empty?
|
99
100
|
|
100
101
|
# grab the states that were present at the lowest timestamp
|
101
|
-
later_groups = later_states.group_by{|s| s[:at] }
|
102
|
-
later_group_key = later_groups.keys.
|
102
|
+
later_groups = later_states.group_by { |s| s[:at] }
|
103
|
+
later_group_key = later_groups.keys.min
|
103
104
|
later_states = later_groups[later_group_key]
|
104
105
|
|
105
106
|
# if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
|
@@ -111,13 +112,14 @@ module Stator
|
|
111
112
|
end
|
112
113
|
|
113
114
|
def likely_state_at(t)
|
114
|
-
@machine.states.reverse.detect{|s| in_state_at?(s,t) }
|
115
|
+
@machine.states.reverse.detect { |s| in_state_at?(s, t) }
|
115
116
|
end
|
116
117
|
|
117
118
|
protected
|
118
119
|
|
119
120
|
def attempt_to_track_state(state_to_track)
|
120
121
|
return unless state_to_track
|
122
|
+
|
121
123
|
_attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
|
122
124
|
end
|
123
125
|
|
@@ -128,7 +130,8 @@ module Stator
|
|
128
130
|
def _attempt_to_track_change(field_name)
|
129
131
|
return unless @record.respond_to?(field_name)
|
130
132
|
return unless @record.respond_to?("#{field_name}=")
|
131
|
-
return unless @record.send(
|
133
|
+
return unless @record.send(field_name.to_s).nil? || state_changed?
|
134
|
+
return if @record.send("#{field_name}_changed?")
|
132
135
|
|
133
136
|
@record.send("#{field_name}=", (Time.zone || Time).now)
|
134
137
|
end
|
data/lib/stator/version.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Stator
|
4
|
+
|
2
5
|
MAJOR = 0
|
3
6
|
MINOR = 3
|
4
|
-
PATCH =
|
7
|
+
PATCH = 1
|
5
8
|
PRERELEASE = nil
|
6
9
|
|
7
|
-
VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join(
|
10
|
+
VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join(".")
|
11
|
+
|
8
12
|
end
|
data/spec/model_spec.rb
CHANGED
@@ -1,103 +1,104 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "spec_helper"
|
4
4
|
|
5
|
-
|
5
|
+
describe Stator::Model do
|
6
|
+
it "should set the default state after initialization" do
|
6
7
|
u = User.new
|
7
|
-
u.state.should eql(
|
8
|
+
u.state.should eql("pending")
|
8
9
|
end
|
9
10
|
|
10
|
-
it
|
11
|
+
it "should see the initial setting of the state as a change with the initial state as the previous value" do
|
11
12
|
u = User.new
|
12
|
-
u.state =
|
13
|
-
u.state_was.should eql(
|
13
|
+
u.state = "activated"
|
14
|
+
u.state_was.should eql("pending")
|
14
15
|
end
|
15
16
|
|
16
|
-
it
|
17
|
+
it "should not obstruct normal validations" do
|
17
18
|
u = User.new
|
18
19
|
u.should_not be_valid
|
19
20
|
u.errors[:email].grep(/length/).should_not be_empty
|
20
21
|
end
|
21
22
|
|
22
|
-
it
|
23
|
+
it "should ensure a valid state transition when given a bogus state" do
|
23
24
|
u = User.new
|
24
|
-
u.state =
|
25
|
+
u.state = "anythingelse"
|
25
26
|
|
26
27
|
u.should_not be_valid
|
27
|
-
u.errors[:state].should eql([
|
28
|
+
u.errors[:state].should eql(["is not a valid state"])
|
28
29
|
end
|
29
30
|
|
30
|
-
it
|
31
|
-
u = User.new(:
|
32
|
-
u.state =
|
31
|
+
it "should allow creation at any state" do
|
32
|
+
u = User.new(email: "doug@example.com")
|
33
|
+
u.state = "hyperactivated"
|
33
34
|
|
34
35
|
u.should be_valid
|
35
36
|
end
|
36
37
|
|
37
|
-
it
|
38
|
+
it "should ensure a valid state transition when given an illegal state based on the current state" do
|
38
39
|
u = User.new
|
39
40
|
|
40
41
|
allow(u).to receive(:new_record?).and_return(false)
|
41
42
|
|
42
|
-
u.state =
|
43
|
+
u.state = "hyperactivated"
|
43
44
|
|
44
45
|
u.should_not be_valid
|
45
46
|
u.errors[:state].should_not be_empty
|
46
47
|
end
|
47
48
|
|
48
|
-
it
|
49
|
-
u = User.new(:
|
49
|
+
it "should not allow a transition that is currently in a `to` state" do
|
50
|
+
u = User.new(email: "fred@example.com")
|
50
51
|
u.activate!
|
51
52
|
u.hyperactivate!
|
52
53
|
|
53
|
-
lambda{
|
54
|
+
lambda {
|
54
55
|
u.hyperactivate!
|
55
56
|
}.should raise_error(/cannot transition to \"hyperactivated\" from \"hyperactivated\"/)
|
56
57
|
end
|
57
58
|
|
58
|
-
it
|
59
|
+
it "should run conditional validations" do
|
59
60
|
u = User.new
|
60
|
-
u.state =
|
61
|
+
u.state = "semiactivated"
|
61
62
|
u.should_not be_valid
|
62
63
|
|
63
64
|
u.errors[:state].should be_empty
|
64
65
|
u.errors[:email].grep(/format/).should_not be_empty
|
65
66
|
end
|
66
67
|
|
67
|
-
it
|
68
|
-
u = User.new(:
|
68
|
+
it "should invoke callbacks" do
|
69
|
+
u = User.new(activated: true, email: "doug@example.com", name: "doug")
|
69
70
|
u.activated.should == true
|
70
71
|
|
71
72
|
u.deactivate
|
72
73
|
|
73
74
|
u.activated.should == false
|
74
|
-
u.state.should eql(
|
75
|
+
u.state.should eql("deactivated")
|
75
76
|
u.activated_state_at.should be_nil
|
76
77
|
u.should be_persisted
|
77
78
|
end
|
78
79
|
|
79
|
-
it
|
80
|
-
u = User.new(:
|
81
|
-
lambda{
|
80
|
+
it "should blow up if the record is invalid and a bang method is used" do
|
81
|
+
u = User.new(email: "doug@other.com", name: "doug")
|
82
|
+
lambda {
|
82
83
|
u.activate!
|
83
84
|
}.should raise_error(ActiveRecord::RecordInvalid)
|
84
85
|
end
|
85
86
|
|
86
|
-
it
|
87
|
+
it "should allow for other fields to be used other than state" do
|
87
88
|
a = Animal.new
|
88
89
|
a.should be_valid
|
89
90
|
|
90
91
|
a.birth!
|
91
92
|
end
|
92
93
|
|
93
|
-
it
|
94
|
+
it "should create implicit transitions for state declarations" do
|
94
95
|
a = Animal.new
|
95
96
|
a.should_not be_grown_up
|
96
|
-
a.status =
|
97
|
+
a.status = "grown_up"
|
97
98
|
a.save
|
98
99
|
end
|
99
100
|
|
100
|
-
it
|
101
|
+
it "should allow multiple machines in the same model" do
|
101
102
|
f = Farm.new
|
102
103
|
f.should be_dirty
|
103
104
|
f.should be_house_dirty
|
@@ -112,30 +113,30 @@ describe Stator::Model do
|
|
112
113
|
f.should_not be_house_dirty
|
113
114
|
end
|
114
115
|
|
115
|
-
it
|
116
|
+
it "should allow saving to be skipped" do
|
116
117
|
f = Farm.new
|
117
118
|
f.cleanup(false)
|
118
119
|
|
119
120
|
f.should_not be_persisted
|
120
121
|
end
|
121
122
|
|
122
|
-
it
|
123
|
+
it "should allow no initial state" do
|
123
124
|
f = Factory.new
|
124
125
|
f.state.should be_nil
|
125
126
|
|
126
127
|
f.construct.should eql(true)
|
127
128
|
|
128
|
-
f.state.should eql(
|
129
|
+
f.state.should eql("constructed")
|
129
130
|
end
|
130
131
|
|
131
|
-
it
|
132
|
+
it "should allow any transition if validations are opted out of" do
|
132
133
|
u = User.new
|
133
|
-
u.email =
|
134
|
+
u.email = "doug@example.com"
|
134
135
|
|
135
136
|
u.can_hyperactivate?.should eql(false)
|
136
137
|
u.hyperactivate.should eql(false)
|
137
138
|
|
138
|
-
u.state.should eql(
|
139
|
+
u.state.should eql("pending")
|
139
140
|
|
140
141
|
u.without_state_transition_validations do
|
141
142
|
u.can_hyperactivate?.should eql(true)
|
@@ -143,13 +144,13 @@ describe Stator::Model do
|
|
143
144
|
end
|
144
145
|
end
|
145
146
|
|
146
|
-
it
|
147
|
+
it "should skip tracking timestamps if opted out of" do
|
147
148
|
u = User.new
|
148
|
-
u.email =
|
149
|
+
u.email = "doug@example.com"
|
149
150
|
|
150
151
|
u.without_state_transition_tracking do
|
151
152
|
u.semiactivate!
|
152
|
-
u.state.should eql(
|
153
|
+
u.state.should eql("semiactivated")
|
153
154
|
u.semiactivated_state_at.should be_nil
|
154
155
|
end
|
155
156
|
|
@@ -159,9 +160,8 @@ describe Stator::Model do
|
|
159
160
|
u.activated_state_at.should_not be_nil
|
160
161
|
end
|
161
162
|
|
162
|
-
describe
|
163
|
-
|
164
|
-
it 'should answer the question of whether the state is currently the one invoked' do
|
163
|
+
describe "helper methods" do
|
164
|
+
it "should answer the question of whether the state is currently the one invoked" do
|
165
165
|
a = Animal.new
|
166
166
|
a.should be_unborn
|
167
167
|
a.should_not be_born
|
@@ -172,7 +172,7 @@ describe Stator::Model do
|
|
172
172
|
a.should_not be_unborn
|
173
173
|
end
|
174
174
|
|
175
|
-
it
|
175
|
+
it "should determine if it can validly execute a transition" do
|
176
176
|
a = Animal.new
|
177
177
|
a.can_birth?.should eql(true)
|
178
178
|
|
@@ -180,22 +180,20 @@ describe Stator::Model do
|
|
180
180
|
|
181
181
|
a.can_birth?.should eql(false)
|
182
182
|
end
|
183
|
-
|
184
183
|
end
|
185
184
|
|
186
|
-
describe
|
187
|
-
|
185
|
+
describe "tracker methods" do
|
188
186
|
before do
|
189
|
-
Time.zone =
|
187
|
+
Time.zone = "Eastern Time (US & Canada)"
|
190
188
|
end
|
191
189
|
|
192
|
-
it
|
190
|
+
it "should store the initial state timestamp when the record is created" do
|
193
191
|
a = Animal.new
|
194
192
|
a.save
|
195
193
|
a.unborn_status_at.should be_within(1).of(Time.zone.now)
|
196
194
|
end
|
197
195
|
|
198
|
-
it
|
196
|
+
it "should store when a record changed state for the first time" do
|
199
197
|
a = Animal.new
|
200
198
|
a.unborn_status_at.should be_nil
|
201
199
|
a.born_status_at.should be_nil
|
@@ -204,7 +202,7 @@ describe Stator::Model do
|
|
204
202
|
a.born_status_at.should be_within(1).of(Time.zone.now)
|
205
203
|
end
|
206
204
|
|
207
|
-
it
|
205
|
+
it "should store when a record change states" do
|
208
206
|
a = Animal.new
|
209
207
|
a.status_changed_at.should be_nil
|
210
208
|
|
@@ -218,12 +216,11 @@ describe Stator::Model do
|
|
218
216
|
a.save
|
219
217
|
|
220
218
|
a.status_changed_at.should eql(previous_status_changed_at)
|
221
|
-
|
222
219
|
end
|
223
220
|
|
224
|
-
it
|
221
|
+
it "should prepend the setting of the timestamp so other callbacks can use it" do
|
225
222
|
u = User.new
|
226
|
-
u.email =
|
223
|
+
u.email = "doug@example.com"
|
227
224
|
|
228
225
|
u.tagged_at.should be_nil
|
229
226
|
u.semiactivate!
|
@@ -232,11 +229,36 @@ describe Stator::Model do
|
|
232
229
|
u.tagged_at.should_not be_nil
|
233
230
|
end
|
234
231
|
|
232
|
+
it "should respect the timestamp if explicitly provided" do
|
233
|
+
t = Time.at(Time.now.to_i - 3600)
|
234
|
+
|
235
|
+
u = User.new
|
236
|
+
u.email = "doug@example.com"
|
237
|
+
u.state = "semiactivated"
|
238
|
+
u.semiactivated_state_at = t
|
239
|
+
u.save!
|
240
|
+
|
241
|
+
u.state.should eql("semiactivated")
|
242
|
+
u.semiactivated_state_at.should eql(t)
|
243
|
+
end
|
244
|
+
|
245
|
+
it "should respect the timestamp if explicitly provided via create" do
|
246
|
+
t = Time.at(Time.now.to_i - 3600)
|
247
|
+
|
248
|
+
u = User.create!(
|
249
|
+
email: "doug@example.com",
|
250
|
+
state: "semiactivated",
|
251
|
+
semiactivated_state_at: t
|
252
|
+
)
|
253
|
+
|
254
|
+
u.state.should eql("semiactivated")
|
255
|
+
u.semiactivated_state_at.should eql(t)
|
256
|
+
end
|
235
257
|
end
|
236
258
|
|
237
|
-
describe
|
238
|
-
it
|
239
|
-
u = User.new(:
|
259
|
+
describe "aliasing" do
|
260
|
+
it "should allow aliasing within the dsl" do
|
261
|
+
u = User.new(email: "doug@example.com")
|
240
262
|
u.should respond_to(:active?)
|
241
263
|
u.should respond_to(:inactive?)
|
242
264
|
|
@@ -253,11 +275,11 @@ describe Stator::Model do
|
|
253
275
|
u.should be_active
|
254
276
|
u.should_not be_inactive
|
255
277
|
|
256
|
-
User::ACTIVE_STATES.should eql([
|
257
|
-
User::INACTIVE_STATES.should eql([
|
278
|
+
User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
|
279
|
+
User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
|
258
280
|
|
259
|
-
User.active.to_sql.gsub(
|
260
|
-
User.inactive.to_sql.gsub(
|
281
|
+
User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
|
282
|
+
User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
|
261
283
|
end
|
262
284
|
|
263
285
|
it "should evaluate inverses correctly" do
|
@@ -272,7 +294,7 @@ describe Stator::Model do
|
|
272
294
|
f.should be_house_cleaned
|
273
295
|
end
|
274
296
|
|
275
|
-
it
|
297
|
+
it "should namespace aliases just like everything else" do
|
276
298
|
f = Farm.new
|
277
299
|
f.should respond_to(:house_cleaned?)
|
278
300
|
|
@@ -282,24 +304,23 @@ describe Stator::Model do
|
|
282
304
|
f.should be_house_cleaned
|
283
305
|
end
|
284
306
|
|
285
|
-
it
|
307
|
+
it "should allow for explicit constant and scope names to be provided" do
|
286
308
|
User.should respond_to(:luke_warmers)
|
287
309
|
(!!defined?(User::LUKE_WARMERS)).should eql(true)
|
288
310
|
u = User.new
|
289
311
|
u.should respond_to(:luke_warm?)
|
290
312
|
end
|
291
313
|
|
292
|
-
it
|
314
|
+
it "should not create constants or scopes by default" do
|
293
315
|
u = User.new
|
294
316
|
u.should respond_to(:iced_tea?)
|
295
317
|
(!!defined?(User::ICED_TEA_STATES)).should eql(false)
|
296
318
|
User.should_not respond_to(:iced_tea)
|
297
319
|
end
|
298
320
|
|
299
|
-
it
|
321
|
+
it "should determine the full list of states correctly" do
|
300
322
|
states = User._stator("").states
|
301
|
-
states.should eql([
|
323
|
+
states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
|
302
324
|
end
|
303
325
|
end
|
304
|
-
|
305
326
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
78
|
rubyforge_project:
|
79
|
-
rubygems_version: 2.
|
79
|
+
rubygems_version: 2.7.7
|
80
80
|
signing_key:
|
81
81
|
specification_version: 4
|
82
82
|
summary: The simplest of ActiveRecord state machines
|