stator 0.2.0 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +26 -0
- data/Gemfile +2 -2
- data/gemfiles/ar42.gemfile +1 -1
- data/gemfiles/ar52.gemfile +10 -0
- data/lib/stator/integration.rb +38 -24
- data/lib/stator/machine.rb +12 -26
- data/lib/stator/model.rb +25 -16
- data/lib/stator/transition.rb +23 -18
- data/lib/stator/version.rb +7 -3
- data/spec/model_spec.rb +146 -65
- data/spec/spec_helper.rb +1 -0
- data/spec/support/models.rb +29 -3
- data/spec/support/schema.rb +10 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4db54f443ba466ae3fc6f79857816b8719e0cbacf0615f8c014ae0c71c32e81
|
4
|
+
data.tar.gz: 53d72d27aafa1a340c02d2d5459235488bf5e12df9198312ffe1d5121fc24912
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2e76eb8e8bbe0e1dab6d8fbdbb6516f12cff56c26c3d10d974d4aadce08fbcabdcd49dbba710d0f904a1a04ea4c489125b18475a119de5e625a9cbb6e9406a4
|
7
|
+
data.tar.gz: 0fef985f07d95f3d8801f4e3534f478feb516fea223532e1e562cc85bebdc54c09222ee0087517d4c3f396f4d094ad80a9404f7e0cb69829ffa6c5ddc40bb297
|
data/.travis.yml
CHANGED
@@ -4,12 +4,38 @@ rvm:
|
|
4
4
|
- 2.0.0
|
5
5
|
- 2.1.6
|
6
6
|
- 2.2.3
|
7
|
+
- 2.4.5
|
8
|
+
- 2.5.3
|
7
9
|
|
8
10
|
gemfile:
|
9
11
|
- gemfiles/ar40.gemfile
|
10
12
|
- gemfiles/ar41.gemfile
|
11
13
|
- gemfiles/ar42.gemfile
|
14
|
+
- gemfiles/ar52.gemfile
|
12
15
|
|
13
16
|
matrix:
|
14
17
|
allow_failures:
|
15
18
|
- gemfile: gemfiles/ar42.gemfile
|
19
|
+
- gemfile: gemfiles/ar52.gemfile
|
20
|
+
|
21
|
+
exclude:
|
22
|
+
- rvm: 2.0.0
|
23
|
+
gemfile: gemfiles/ar52.gemfile
|
24
|
+
|
25
|
+
- rvm: 2.1.6
|
26
|
+
gemfile: gemfiles/ar52.gemfile
|
27
|
+
|
28
|
+
- rvm: 2.2.3
|
29
|
+
gemfile: gemfiles/ar52.gemfile
|
30
|
+
|
31
|
+
- rvm: 2.4.5
|
32
|
+
gemfile: gemfiles/ar40.gemfile
|
33
|
+
|
34
|
+
- rvm: 2.4.5
|
35
|
+
gemfile: gemfiles/ar41.gemfile
|
36
|
+
|
37
|
+
- rvm: 2.5.3
|
38
|
+
gemfile: gemfiles/ar40.gemfile
|
39
|
+
|
40
|
+
- rvm: 2.5.3
|
41
|
+
gemfile: gemfiles/ar41.gemfile
|
data/Gemfile
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in stator.gemspec
|
4
|
-
gem 'activerecord', '
|
4
|
+
gem 'activerecord', '5.2.3'
|
5
5
|
|
6
6
|
gemspec
|
7
7
|
|
8
|
+
gem 'activerecord-nulldb-adapter', '~> 0.4.0', require: false
|
8
9
|
gem 'rake'
|
9
|
-
gem 'activerecord-nulldb-adapter', :require => false, :git => 'git@github.com:nulldb/nulldb.git', :ref => 'ffc7dae4697c6b9fb15bed9edca3acb1f00eb5f0'
|
10
10
|
gem 'rspec'
|
data/gemfiles/ar42.gemfile
CHANGED
@@ -0,0 +1,10 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in stator.gemspec
|
4
|
+
gem 'activerecord', '~> 5.2.0'
|
5
|
+
|
6
|
+
gemspec :path => '../'
|
7
|
+
|
8
|
+
gem 'rake'
|
9
|
+
gem 'activerecord-nulldb-adapter', '~> 0.4.0', :require => false, :git => 'git@github.com:nulldb/nulldb.git'
|
10
|
+
gem 'rspec'
|
data/lib/stator/integration.rb
CHANGED
@@ -1,27 +1,28 @@
|
|
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
|
9
|
+
|
10
|
+
attr_reader :skip_validations
|
11
|
+
attr_reader :skip_transition_tracking
|
7
12
|
|
8
13
|
def initialize(machine, record)
|
9
14
|
@machine = machine
|
10
15
|
@record = record
|
11
16
|
end
|
12
17
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
18
|
def state=(new_value)
|
17
|
-
@record.send("#{@machine.field}=",
|
19
|
+
@record.send("#{@machine.field}=", new_value)
|
18
20
|
end
|
19
21
|
|
20
22
|
def state
|
21
23
|
@record.send(@machine.field)
|
22
24
|
end
|
23
25
|
|
24
|
-
|
25
26
|
def state_was(use_previous = false)
|
26
27
|
if use_previous
|
27
28
|
@record.previous_changes[@machine.field.to_s].try(:[], 0)
|
@@ -30,7 +31,6 @@ module Stator
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
33
|
-
|
34
34
|
def state_changed?(use_previous = false)
|
35
35
|
if use_previous
|
36
36
|
!!@record.previous_changes[@machine.field.to_s]
|
@@ -39,14 +39,12 @@ module Stator
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
42
|
def validate_transition
|
45
|
-
return unless
|
46
|
-
return if
|
43
|
+
return unless state_changed?
|
44
|
+
return if skip_validations
|
47
45
|
|
48
|
-
was =
|
49
|
-
is =
|
46
|
+
was = state_was
|
47
|
+
is = state
|
50
48
|
|
51
49
|
if @record.new_record?
|
52
50
|
invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
|
@@ -55,7 +53,7 @@ module Stator
|
|
55
53
|
end
|
56
54
|
end
|
57
55
|
|
58
|
-
#
|
56
|
+
# TODO: i18n
|
59
57
|
def invalid_state!
|
60
58
|
@record.errors.add(@machine.field, "is not a valid state")
|
61
59
|
end
|
@@ -65,10 +63,10 @@ module Stator
|
|
65
63
|
end
|
66
64
|
|
67
65
|
def track_transition
|
68
|
-
return if
|
66
|
+
return if skip_transition_tracking
|
69
67
|
|
70
|
-
|
71
|
-
|
68
|
+
attempt_to_track_state(state)
|
69
|
+
attempt_to_track_state_changed_timestamp
|
72
70
|
|
73
71
|
true
|
74
72
|
end
|
@@ -89,7 +87,6 @@ module Stator
|
|
89
87
|
|
90
88
|
# grab all the states and their timestamps that occur on or after state_at and on or before the time in question
|
91
89
|
later_states = all_states.map do |s|
|
92
|
-
|
93
90
|
next if state == s
|
94
91
|
|
95
92
|
at = @record.send("#{s}_#{@machine.field}_at")
|
@@ -105,8 +102,8 @@ module Stator
|
|
105
102
|
return true if later_states.empty?
|
106
103
|
|
107
104
|
# grab the states that were present at the lowest timestamp
|
108
|
-
later_groups = later_states.group_by{|s| s[:at] }
|
109
|
-
later_group_key = later_groups.keys.
|
105
|
+
later_groups = later_states.group_by { |s| s[:at] }
|
106
|
+
later_group_key = later_groups.keys.min
|
110
107
|
later_states = later_groups[later_group_key]
|
111
108
|
|
112
109
|
# if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
|
@@ -118,14 +115,30 @@ module Stator
|
|
118
115
|
end
|
119
116
|
|
120
117
|
def likely_state_at(t)
|
121
|
-
@machine.states.reverse.detect{|s| in_state_at?(s,t) }
|
118
|
+
@machine.states.reverse.detect { |s| in_state_at?(s, t) }
|
122
119
|
end
|
123
120
|
|
121
|
+
def without_validation
|
122
|
+
was = @skip_validations
|
123
|
+
@skip_validations = true
|
124
|
+
yield @record
|
125
|
+
ensure
|
126
|
+
@skip_validations = was
|
127
|
+
end
|
128
|
+
|
129
|
+
def without_transition_tracking
|
130
|
+
was = @skip_transition_tracking
|
131
|
+
@skip_transition_tracking = true
|
132
|
+
yield @record
|
133
|
+
ensure
|
134
|
+
@skip_transition_tracking = was
|
135
|
+
end
|
124
136
|
|
125
137
|
protected
|
126
138
|
|
127
139
|
def attempt_to_track_state(state_to_track)
|
128
140
|
return unless state_to_track
|
141
|
+
|
129
142
|
_attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
|
130
143
|
end
|
131
144
|
|
@@ -136,7 +149,8 @@ module Stator
|
|
136
149
|
def _attempt_to_track_change(field_name)
|
137
150
|
return unless @record.respond_to?(field_name)
|
138
151
|
return unless @record.respond_to?("#{field_name}=")
|
139
|
-
return unless @record.send(
|
152
|
+
return unless @record.send(field_name.to_s).nil? || state_changed?
|
153
|
+
return if @record.send("#{field_name}_changed?")
|
140
154
|
|
141
155
|
@record.send("#{field_name}=", (Time.zone || Time).now)
|
142
156
|
end
|
data/lib/stator/machine.rb
CHANGED
@@ -7,16 +7,14 @@ module Stator
|
|
7
7
|
attr_reader :transitions
|
8
8
|
attr_reader :states
|
9
9
|
attr_reader :namespace
|
10
|
-
attr_reader :skip_validations
|
11
|
-
attr_reader :skip_transition_tracking
|
12
|
-
|
13
10
|
|
14
11
|
def initialize(klass, options = {})
|
15
|
-
@class_name
|
16
|
-
@field
|
17
|
-
@namespace
|
12
|
+
@class_name = klass.name
|
13
|
+
@field = options[:field] || :state
|
14
|
+
@namespace = options[:namespace]
|
18
15
|
|
19
|
-
@initial_state
|
16
|
+
@initial_state = options[:initial] && options[:initial].to_s
|
17
|
+
@tracking_enabled = options[:track] || false
|
20
18
|
|
21
19
|
@transitions = []
|
22
20
|
@aliases = []
|
@@ -26,7 +24,6 @@ module Stator
|
|
26
24
|
@states = [@initial_state].compact
|
27
25
|
|
28
26
|
@options = options
|
29
|
-
|
30
27
|
end
|
31
28
|
|
32
29
|
def integration(record)
|
@@ -38,7 +35,6 @@ module Stator
|
|
38
35
|
end
|
39
36
|
|
40
37
|
def transition(name, &block)
|
41
|
-
|
42
38
|
t = ::Stator::Transition.new(@class_name, name, @namespace)
|
43
39
|
t.instance_eval(&block) if block_given?
|
44
40
|
|
@@ -66,8 +62,14 @@ module Stator
|
|
66
62
|
end
|
67
63
|
end
|
68
64
|
|
65
|
+
def tracking_enabled?
|
66
|
+
@tracking_enabled
|
67
|
+
end
|
68
|
+
|
69
69
|
def conditional(*states, &block)
|
70
|
-
|
70
|
+
_namespace = @namespace
|
71
|
+
|
72
|
+
klass.instance_exec(proc { states.map(&:to_s).include?(self._stator(_namespace).integration(self).state) }, &block)
|
71
73
|
end
|
72
74
|
|
73
75
|
def matching_transition(from, to)
|
@@ -86,22 +88,6 @@ module Stator
|
|
86
88
|
@class_name.constantize
|
87
89
|
end
|
88
90
|
|
89
|
-
def without_validation
|
90
|
-
was = @skip_validations
|
91
|
-
@skip_validations = true
|
92
|
-
yield
|
93
|
-
ensure
|
94
|
-
@skip_validations = was
|
95
|
-
end
|
96
|
-
|
97
|
-
def without_transition_tracking
|
98
|
-
was = @skip_transition_tracking
|
99
|
-
@skip_transition_tracking = true
|
100
|
-
yield
|
101
|
-
ensure
|
102
|
-
@skip_transition_tracking = was
|
103
|
-
end
|
104
|
-
|
105
91
|
protected
|
106
92
|
|
107
93
|
def verify_transition_validity(transition)
|
data/lib/stator/model.rb
CHANGED
@@ -14,7 +14,7 @@ module Stator
|
|
14
14
|
f = options[:field] || :state
|
15
15
|
# rescue nil since the table may not exist yet.
|
16
16
|
initial = self.columns_hash[f.to_s].default rescue nil
|
17
|
-
options = options.
|
17
|
+
options = options.merge(initial: initial) if initial
|
18
18
|
end
|
19
19
|
|
20
20
|
machine = (self._stators[options[:namespace].to_s] ||= ::Stator::Machine.new(self, options))
|
@@ -35,27 +35,25 @@ module Stator
|
|
35
35
|
|
36
36
|
def self.included(base)
|
37
37
|
base.class_eval do
|
38
|
-
|
38
|
+
before_save :_stator_maybe_track_transition, prepend: true
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
def in_state_at?(state, t, namespace = '')
|
43
|
-
|
44
|
-
machine.integration(self).in_state_at?(state, t)
|
43
|
+
_integration(namespace).in_state_at?(state, t)
|
45
44
|
end
|
46
45
|
|
47
46
|
def likely_state_at(t, namespace = '')
|
48
|
-
|
49
|
-
machine.integration(self).likely_state_at(t)
|
47
|
+
_integration(namespace).likely_state_at(t)
|
50
48
|
end
|
51
49
|
|
52
50
|
protected
|
53
51
|
|
54
|
-
|
55
|
-
def _stator_track_transition
|
56
|
-
|
52
|
+
def _stator_maybe_track_transition
|
57
53
|
self._stators.each do |namespace, machine|
|
58
|
-
machine.
|
54
|
+
next unless machine.tracking_enabled?
|
55
|
+
|
56
|
+
_integration(namespace).track_transition
|
59
57
|
end
|
60
58
|
|
61
59
|
true
|
@@ -71,23 +69,28 @@ module Stator
|
|
71
69
|
end
|
72
70
|
end
|
73
71
|
|
72
|
+
def initialize_dup(other)
|
73
|
+
@_integrations = {}
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
74
77
|
def without_state_transition_validations(namespace = '')
|
75
|
-
|
76
|
-
yield
|
78
|
+
_integration(namespace).without_validation do
|
79
|
+
yield self
|
77
80
|
end
|
78
81
|
end
|
79
82
|
|
80
83
|
def without_state_transition_tracking(namespace = '')
|
81
|
-
|
82
|
-
yield
|
84
|
+
_integration(namespace).without_transition_tracking do
|
85
|
+
yield self
|
83
86
|
end
|
84
87
|
end
|
85
88
|
|
86
89
|
protected
|
87
90
|
|
88
91
|
def _stator_validate_transition
|
89
|
-
self._stators.
|
90
|
-
|
92
|
+
self._stators.each_key do |namespace|
|
93
|
+
_integration(namespace).validate_transition
|
91
94
|
end
|
92
95
|
end
|
93
96
|
|
@@ -95,6 +98,12 @@ module Stator
|
|
95
98
|
self.class._stator(namespace)
|
96
99
|
end
|
97
100
|
|
101
|
+
def _integration(namespace = '')
|
102
|
+
@_integrations ||= {}
|
103
|
+
@_integrations[namespace] ||= _stator(namespace).integration(self)
|
104
|
+
@_integrations[namespace]
|
105
|
+
end
|
106
|
+
|
98
107
|
end
|
99
108
|
end
|
100
109
|
end
|
data/lib/stator/transition.rb
CHANGED
@@ -42,7 +42,7 @@ module Stator
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def conditional(options = {}, &block)
|
45
|
-
klass.instance_exec(
|
45
|
+
klass.instance_exec(conditional_block(options), &block)
|
46
46
|
end
|
47
47
|
|
48
48
|
def any
|
@@ -63,25 +63,30 @@ module Stator
|
|
63
63
|
@callbacks[kind] || []
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
66
|
+
def conditional_block(options = {})
|
67
67
|
options[:use_previous] ||= false
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
)
|
78
|
-
|
68
|
+
|
69
|
+
_namespace = @namespace
|
70
|
+
_froms = @froms
|
71
|
+
_to = @to
|
72
|
+
|
73
|
+
Proc.new do
|
74
|
+
(
|
75
|
+
self._stator(_namespace).integration(self).state_changed?(options[:use_previous])
|
76
|
+
) && (
|
77
|
+
_froms.include?(self._stator(_namespace).integration(self).state_was(options[:use_previous])) ||
|
78
|
+
_froms.include?(::Stator::Transition::ANY)
|
79
|
+
) && (
|
80
|
+
self._stator(_namespace).integration(self).state == _to ||
|
81
|
+
_to == ::Stator::Transition::ANY
|
82
|
+
)
|
83
|
+
end
|
79
84
|
end
|
80
85
|
|
81
86
|
def generate_methods
|
82
87
|
klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
83
88
|
def #{@full_name}(should_save = true)
|
84
|
-
integration =
|
89
|
+
integration = _integration(#{@namespace.to_s.inspect})
|
85
90
|
|
86
91
|
unless can_#{@full_name}?
|
87
92
|
integration.invalid_transition!(integration.state, #{@to.inspect}) if should_save
|
@@ -93,7 +98,7 @@ module Stator
|
|
93
98
|
end
|
94
99
|
|
95
100
|
def #{@full_name}!
|
96
|
-
integration =
|
101
|
+
integration = _integration(#{@namespace.to_s.inspect})
|
97
102
|
|
98
103
|
unless can_#{@full_name}?
|
99
104
|
integration.invalid_transition!(integration.state, #{@to.inspect})
|
@@ -105,10 +110,10 @@ module Stator
|
|
105
110
|
end
|
106
111
|
|
107
112
|
def can_#{@full_name}?
|
108
|
-
|
109
|
-
return true if
|
113
|
+
integration = _integration(#{@namespace.to_s.inspect})
|
114
|
+
return true if integration.skip_validations
|
110
115
|
|
111
|
-
|
116
|
+
machine = self._stator(#{@namespace.to_s.inspect})
|
112
117
|
transition = machine.transitions.detect{|t| t.full_name.to_s == #{@full_name.inspect}.to_s }
|
113
118
|
transition.can?(integration.state)
|
114
119
|
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
|
-
MINOR =
|
4
|
-
PATCH =
|
6
|
+
MINOR = 3
|
7
|
+
PATCH = 3
|
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,101 +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
|
-
u.
|
40
|
+
|
41
|
+
allow(u).to receive(:new_record?).and_return(false)
|
42
|
+
|
43
|
+
u.state = "hyperactivated"
|
41
44
|
|
42
45
|
u.should_not be_valid
|
43
46
|
u.errors[:state].should_not be_empty
|
44
47
|
end
|
45
48
|
|
46
|
-
it
|
47
|
-
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")
|
48
51
|
u.activate!
|
49
52
|
u.hyperactivate!
|
50
53
|
|
51
|
-
lambda{
|
54
|
+
lambda {
|
52
55
|
u.hyperactivate!
|
53
56
|
}.should raise_error(/cannot transition to \"hyperactivated\" from \"hyperactivated\"/)
|
54
57
|
end
|
55
58
|
|
56
|
-
it
|
59
|
+
it "should run conditional validations" do
|
57
60
|
u = User.new
|
58
|
-
u.state =
|
61
|
+
u.state = "semiactivated"
|
59
62
|
u.should_not be_valid
|
60
63
|
|
61
64
|
u.errors[:state].should be_empty
|
62
65
|
u.errors[:email].grep(/format/).should_not be_empty
|
63
66
|
end
|
64
67
|
|
65
|
-
it
|
66
|
-
u = User.new(:
|
68
|
+
it "should invoke callbacks" do
|
69
|
+
u = User.new(activated: true, email: "doug@example.com", name: "doug")
|
67
70
|
u.activated.should == true
|
68
71
|
|
69
72
|
u.deactivate
|
70
73
|
|
71
74
|
u.activated.should == false
|
72
|
-
u.state.should eql(
|
75
|
+
u.state.should eql("deactivated")
|
73
76
|
u.activated_state_at.should be_nil
|
74
77
|
u.should be_persisted
|
75
78
|
end
|
76
79
|
|
77
|
-
it
|
78
|
-
u = User.new(:
|
79
|
-
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 {
|
80
83
|
u.activate!
|
81
84
|
}.should raise_error(ActiveRecord::RecordInvalid)
|
82
85
|
end
|
83
86
|
|
84
|
-
it
|
87
|
+
it "should allow for other fields to be used other than state" do
|
85
88
|
a = Animal.new
|
86
89
|
a.should be_valid
|
87
90
|
|
88
91
|
a.birth!
|
89
92
|
end
|
90
93
|
|
91
|
-
it
|
94
|
+
it "should create implicit transitions for state declarations" do
|
92
95
|
a = Animal.new
|
93
96
|
a.should_not be_grown_up
|
94
|
-
a.status =
|
97
|
+
a.status = "grown_up"
|
95
98
|
a.save
|
96
99
|
end
|
97
100
|
|
98
|
-
it
|
101
|
+
it "should allow multiple machines in the same model" do
|
99
102
|
f = Farm.new
|
100
103
|
f.should be_dirty
|
101
104
|
f.should be_house_dirty
|
@@ -110,30 +113,30 @@ describe Stator::Model do
|
|
110
113
|
f.should_not be_house_dirty
|
111
114
|
end
|
112
115
|
|
113
|
-
it
|
116
|
+
it "should allow saving to be skipped" do
|
114
117
|
f = Farm.new
|
115
118
|
f.cleanup(false)
|
116
119
|
|
117
120
|
f.should_not be_persisted
|
118
121
|
end
|
119
122
|
|
120
|
-
it
|
123
|
+
it "should allow no initial state" do
|
121
124
|
f = Factory.new
|
122
125
|
f.state.should be_nil
|
123
126
|
|
124
127
|
f.construct.should eql(true)
|
125
128
|
|
126
|
-
f.state.should eql(
|
129
|
+
f.state.should eql("constructed")
|
127
130
|
end
|
128
131
|
|
129
|
-
it
|
132
|
+
it "should allow any transition if validations are opted out of" do
|
130
133
|
u = User.new
|
131
|
-
u.email =
|
134
|
+
u.email = "doug@example.com"
|
132
135
|
|
133
136
|
u.can_hyperactivate?.should eql(false)
|
134
137
|
u.hyperactivate.should eql(false)
|
135
138
|
|
136
|
-
u.state.should eql(
|
139
|
+
u.state.should eql("pending")
|
137
140
|
|
138
141
|
u.without_state_transition_validations do
|
139
142
|
u.can_hyperactivate?.should eql(true)
|
@@ -141,13 +144,13 @@ describe Stator::Model do
|
|
141
144
|
end
|
142
145
|
end
|
143
146
|
|
144
|
-
it
|
147
|
+
it "should skip tracking timestamps if opted out of" do
|
145
148
|
u = User.new
|
146
|
-
u.email =
|
149
|
+
u.email = "doug@example.com"
|
147
150
|
|
148
151
|
u.without_state_transition_tracking do
|
149
152
|
u.semiactivate!
|
150
|
-
u.state.should eql(
|
153
|
+
u.state.should eql("semiactivated")
|
151
154
|
u.semiactivated_state_at.should be_nil
|
152
155
|
end
|
153
156
|
|
@@ -157,9 +160,44 @@ describe Stator::Model do
|
|
157
160
|
u.activated_state_at.should_not be_nil
|
158
161
|
end
|
159
162
|
|
160
|
-
|
163
|
+
it "should skip tracking timestamps if opted out of with thread safety" do
|
164
|
+
threads = []
|
165
|
+
skip = User.new(email: "skip@example.com")
|
166
|
+
nope = User.new(email: "nope@example.com")
|
167
|
+
|
168
|
+
threads << Thread.new do
|
169
|
+
sleep 0.5
|
170
|
+
nope.semiactivate!
|
171
|
+
end
|
172
|
+
threads << Thread.new do
|
173
|
+
skip.without_state_transition_tracking do
|
174
|
+
sleep 1
|
175
|
+
skip.semiactivate!
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
threads.each(&:join)
|
180
|
+
|
181
|
+
nope.semiactivated_state_at.should_not be_nil
|
182
|
+
skip.semiactivated_state_at.should be_nil
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should not inherit _integration cache on dup" do
|
186
|
+
u = User.new(email: "user@example.com")
|
187
|
+
u.save!
|
188
|
+
|
189
|
+
u_duped = u.dup
|
190
|
+
|
191
|
+
u.semiactivate!
|
192
|
+
|
193
|
+
u_duped_integration = u_duped.send(:_integration)
|
161
194
|
|
162
|
-
|
195
|
+
u_duped_integration.state.should_not eql(u.state)
|
196
|
+
u_duped_integration.instance_values["record"].should eq(u_duped)
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "helper methods" do
|
200
|
+
it "should answer the question of whether the state is currently the one invoked" do
|
163
201
|
a = Animal.new
|
164
202
|
a.should be_unborn
|
165
203
|
a.should_not be_born
|
@@ -170,7 +208,7 @@ describe Stator::Model do
|
|
170
208
|
a.should_not be_unborn
|
171
209
|
end
|
172
210
|
|
173
|
-
it
|
211
|
+
it "should determine if it can validly execute a transition" do
|
174
212
|
a = Animal.new
|
175
213
|
a.can_birth?.should eql(true)
|
176
214
|
|
@@ -178,22 +216,20 @@ describe Stator::Model do
|
|
178
216
|
|
179
217
|
a.can_birth?.should eql(false)
|
180
218
|
end
|
181
|
-
|
182
219
|
end
|
183
220
|
|
184
|
-
describe
|
185
|
-
|
221
|
+
describe "tracker methods" do
|
186
222
|
before do
|
187
|
-
Time.zone =
|
223
|
+
Time.zone = "Eastern Time (US & Canada)"
|
188
224
|
end
|
189
225
|
|
190
|
-
it
|
226
|
+
it "should store the initial state timestamp when the record is created" do
|
191
227
|
a = Animal.new
|
192
228
|
a.save
|
193
229
|
a.unborn_status_at.should be_within(1).of(Time.zone.now)
|
194
230
|
end
|
195
231
|
|
196
|
-
it
|
232
|
+
it "should store when a record changed state for the first time" do
|
197
233
|
a = Animal.new
|
198
234
|
a.unborn_status_at.should be_nil
|
199
235
|
a.born_status_at.should be_nil
|
@@ -202,7 +238,7 @@ describe Stator::Model do
|
|
202
238
|
a.born_status_at.should be_within(1).of(Time.zone.now)
|
203
239
|
end
|
204
240
|
|
205
|
-
it
|
241
|
+
it "should store when a record change states" do
|
206
242
|
a = Animal.new
|
207
243
|
a.status_changed_at.should be_nil
|
208
244
|
|
@@ -216,23 +252,64 @@ describe Stator::Model do
|
|
216
252
|
a.save
|
217
253
|
|
218
254
|
a.status_changed_at.should eql(previous_status_changed_at)
|
219
|
-
|
220
255
|
end
|
221
256
|
|
222
|
-
it
|
257
|
+
it "should prepend the setting of the timestamp so other callbacks can use it" do
|
223
258
|
u = User.new
|
259
|
+
u.email = "doug@example.com"
|
260
|
+
|
224
261
|
u.tagged_at.should be_nil
|
225
|
-
u.semiactivate
|
262
|
+
u.semiactivate!
|
226
263
|
|
227
264
|
u.semiactivated_state_at.should_not be_nil
|
228
265
|
u.tagged_at.should_not be_nil
|
229
266
|
end
|
230
267
|
|
268
|
+
it "should respect the timestamp if explicitly provided" do
|
269
|
+
t = Time.at(Time.now.to_i - 3600)
|
270
|
+
|
271
|
+
u = User.new
|
272
|
+
u.email = "doug@example.com"
|
273
|
+
u.state = "semiactivated"
|
274
|
+
u.semiactivated_state_at = t
|
275
|
+
u.save!
|
276
|
+
|
277
|
+
u.state.should eql("semiactivated")
|
278
|
+
u.semiactivated_state_at.should eql(t)
|
279
|
+
end
|
280
|
+
|
281
|
+
it "should respect the timestamp if explicitly provided via create" do
|
282
|
+
t = Time.at(Time.now.to_i - 3600)
|
283
|
+
|
284
|
+
u = User.create!(
|
285
|
+
email: "doug@example.com",
|
286
|
+
state: "semiactivated",
|
287
|
+
semiactivated_state_at: t
|
288
|
+
)
|
289
|
+
|
290
|
+
u.state.should eql("semiactivated")
|
291
|
+
u.semiactivated_state_at.should eql(t)
|
292
|
+
end
|
293
|
+
|
294
|
+
it "should allow opting into track by namespace" do
|
295
|
+
z = ZooKeeper.new(name: "Doug")
|
296
|
+
z.employment_state.should eql("hired")
|
297
|
+
z.employment_fire!
|
298
|
+
z.fired_employment_state_at.should_not be_nil
|
299
|
+
|
300
|
+
z.employment_hire!
|
301
|
+
z.hired_employment_state_at.should_not be_nil
|
302
|
+
|
303
|
+
z.working_start!
|
304
|
+
z.started_working_state_at.should be_nil
|
305
|
+
z.working_end!
|
306
|
+
z.ended_working_state_at.should be_nil
|
307
|
+
end
|
231
308
|
end
|
232
309
|
|
233
|
-
describe
|
234
|
-
it
|
235
|
-
u = User.new(:
|
310
|
+
describe "aliasing" do
|
311
|
+
it "should allow aliasing within the dsl" do
|
312
|
+
u = User.new(email: "doug@example.com")
|
236
313
|
u.should respond_to(:active?)
|
237
314
|
u.should respond_to(:inactive?)
|
238
315
|
|
@@ -249,11 +326,11 @@ describe Stator::Model do
|
|
249
326
|
u.should be_active
|
250
327
|
u.should_not be_inactive
|
251
328
|
|
252
|
-
User::ACTIVE_STATES.should eql([
|
253
|
-
User::INACTIVE_STATES.should eql([
|
329
|
+
User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
|
330
|
+
User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
|
254
331
|
|
255
|
-
User.active.to_sql.gsub(
|
256
|
-
User.inactive.to_sql.gsub(
|
332
|
+
User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
|
333
|
+
User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
|
257
334
|
end
|
258
335
|
|
259
336
|
it "should evaluate inverses correctly" do
|
@@ -268,7 +345,7 @@ describe Stator::Model do
|
|
268
345
|
f.should be_house_cleaned
|
269
346
|
end
|
270
347
|
|
271
|
-
it
|
348
|
+
it "should namespace aliases just like everything else" do
|
272
349
|
f = Farm.new
|
273
350
|
f.should respond_to(:house_cleaned?)
|
274
351
|
|
@@ -278,19 +355,23 @@ describe Stator::Model do
|
|
278
355
|
f.should be_house_cleaned
|
279
356
|
end
|
280
357
|
|
281
|
-
it
|
358
|
+
it "should allow for explicit constant and scope names to be provided" do
|
282
359
|
User.should respond_to(:luke_warmers)
|
283
360
|
(!!defined?(User::LUKE_WARMERS)).should eql(true)
|
284
361
|
u = User.new
|
285
362
|
u.should respond_to(:luke_warm?)
|
286
363
|
end
|
287
364
|
|
288
|
-
it
|
365
|
+
it "should not create constants or scopes by default" do
|
289
366
|
u = User.new
|
290
367
|
u.should respond_to(:iced_tea?)
|
291
368
|
(!!defined?(User::ICED_TEA_STATES)).should eql(false)
|
292
369
|
User.should_not respond_to(:iced_tea)
|
293
370
|
end
|
294
|
-
end
|
295
371
|
|
372
|
+
it "should determine the full list of states correctly" do
|
373
|
+
states = User._stator("").states
|
374
|
+
states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
|
375
|
+
end
|
376
|
+
end
|
296
377
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/models.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
class User < ActiveRecord::Base
|
2
2
|
extend Stator::Model
|
3
3
|
|
4
|
-
|
4
|
+
before_save :set_tagged_at
|
5
5
|
|
6
|
-
|
7
|
-
stator track: true do
|
6
|
+
stator track: true, initial: :pending do
|
8
7
|
|
9
8
|
transition :activate do
|
10
9
|
from :pending, :semiactivated
|
@@ -135,6 +134,33 @@ class Zoo < ActiveRecord::Base
|
|
135
134
|
end
|
136
135
|
end
|
137
136
|
|
137
|
+
class ZooKeeper < ActiveRecord::Base
|
138
|
+
extend Stator::Model
|
139
|
+
|
140
|
+
stator namespace: 'employment', field: 'employment_state', track: true do
|
141
|
+
transition :hire do
|
142
|
+
from nil, :fired
|
143
|
+
to :hired
|
144
|
+
end
|
145
|
+
|
146
|
+
transition :fire do
|
147
|
+
from :hired
|
148
|
+
to :fired
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
stator namespace: 'working', field: 'working_state', track: false do
|
153
|
+
transition :start do
|
154
|
+
from nil, :ended
|
155
|
+
to :started
|
156
|
+
end
|
157
|
+
|
158
|
+
transition :end do
|
159
|
+
from :started
|
160
|
+
to :ended
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
138
164
|
|
139
165
|
class Farm < ActiveRecord::Base
|
140
166
|
extend Stator::Model
|
data/spec/support/schema.rb
CHANGED
@@ -22,6 +22,16 @@ ActiveRecord::Schema.define(:version => 20130628161227) do
|
|
22
22
|
t.datetime "born_status_at"
|
23
23
|
end
|
24
24
|
|
25
|
+
create_table "zoo_keepers", :force => true do |t|
|
26
|
+
t.string "name"
|
27
|
+
t.string "employment_state", :default => 'hired'
|
28
|
+
t.datetime "hired_employment_state_at"
|
29
|
+
t.datetime "fired_employment_state_at"
|
30
|
+
t.string "working_state"
|
31
|
+
t.datetime "started_working_state_at"
|
32
|
+
t.datetime "ended_working_state_at"
|
33
|
+
end
|
34
|
+
|
25
35
|
create_table "zoos", :force => true do |t|
|
26
36
|
t.string "name"
|
27
37
|
t.string "state", :default => 'closed'
|
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.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -44,6 +44,7 @@ files:
|
|
44
44
|
- gemfiles/ar40.gemfile
|
45
45
|
- gemfiles/ar41.gemfile
|
46
46
|
- gemfiles/ar42.gemfile
|
47
|
+
- gemfiles/ar52.gemfile
|
47
48
|
- lib/stator.rb
|
48
49
|
- lib/stator/alias.rb
|
49
50
|
- lib/stator/integration.rb
|
@@ -74,8 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
75
|
- !ruby/object:Gem::Version
|
75
76
|
version: '0'
|
76
77
|
requirements: []
|
77
|
-
|
78
|
-
rubygems_version: 2.7.7
|
78
|
+
rubygems_version: 3.0.6
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: The simplest of ActiveRecord state machines
|