stator 0.2.2 → 0.4.0
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/.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 +47 -17
- data/lib/stator/machine.rb +17 -26
- data/lib/stator/model.rb +28 -14
- data/lib/stator/transition.rb +23 -18
- data/lib/stator/version.rb +7 -3
- data/spec/model_spec.rb +183 -67
- data/spec/spec_helper.rb +1 -0
- data/spec/support/models.rb +27 -0
- data/spec/support/schema.rb +10 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd6df35f26ca3ad5d575a7ca144abf6bd17ab7eb24b5ada82fb46e499998ce99
|
4
|
+
data.tar.gz: 5e67fc30ac3a46248afee90f68781afb1628a8145ec6da935573a1ee4e29d573
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1d45f5f4797c75bc8b124041cab91b0e85bec9e936ec76fdfc15b53a5f3c88db2cd733546f20f50fdf2ec3edb7f5b3cffeb93996053afc0a2784de6fd9d17e5
|
7
|
+
data.tar.gz: 779f3a8c8b04b04acb5abdf84722a7418a0c5a8c35f859d02d8c35419ac6398bd5b64b9a80cadf329f4ae9fde4f666664da2075d500187ea1ba144557c240f47
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.5.1
|
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,9 +1,14 @@
|
|
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
|
@@ -11,7 +16,7 @@ module Stator
|
|
11
16
|
end
|
12
17
|
|
13
18
|
def state=(new_value)
|
14
|
-
@record.send("#{@machine.field}=",
|
19
|
+
@record.send("#{@machine.field}=", new_value)
|
15
20
|
end
|
16
21
|
|
17
22
|
def state
|
@@ -26,6 +31,14 @@ module Stator
|
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
34
|
+
def state_by?(state, time)
|
35
|
+
field_name = "#{state}_#{@machine.field}_at"
|
36
|
+
return false unless @record.respond_to?(field_name)
|
37
|
+
return false if @record.send(field_name).nil?
|
38
|
+
return true if time.nil?
|
39
|
+
@record.send(field_name) <= time
|
40
|
+
end
|
41
|
+
|
29
42
|
def state_changed?(use_previous = false)
|
30
43
|
if use_previous
|
31
44
|
!!@record.previous_changes[@machine.field.to_s]
|
@@ -35,11 +48,11 @@ module Stator
|
|
35
48
|
end
|
36
49
|
|
37
50
|
def validate_transition
|
38
|
-
return unless
|
39
|
-
return if
|
51
|
+
return unless state_changed?
|
52
|
+
return if skip_validations
|
40
53
|
|
41
|
-
was =
|
42
|
-
is =
|
54
|
+
was = state_was
|
55
|
+
is = state
|
43
56
|
|
44
57
|
if @record.new_record?
|
45
58
|
invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
|
@@ -48,7 +61,7 @@ module Stator
|
|
48
61
|
end
|
49
62
|
end
|
50
63
|
|
51
|
-
#
|
64
|
+
# TODO: i18n
|
52
65
|
def invalid_state!
|
53
66
|
@record.errors.add(@machine.field, "is not a valid state")
|
54
67
|
end
|
@@ -58,10 +71,10 @@ module Stator
|
|
58
71
|
end
|
59
72
|
|
60
73
|
def track_transition
|
61
|
-
return if
|
74
|
+
return if skip_transition_tracking
|
62
75
|
|
63
|
-
|
64
|
-
|
76
|
+
attempt_to_track_state(state)
|
77
|
+
attempt_to_track_state_changed_timestamp
|
65
78
|
|
66
79
|
true
|
67
80
|
end
|
@@ -82,7 +95,6 @@ module Stator
|
|
82
95
|
|
83
96
|
# grab all the states and their timestamps that occur on or after state_at and on or before the time in question
|
84
97
|
later_states = all_states.map do |s|
|
85
|
-
|
86
98
|
next if state == s
|
87
99
|
|
88
100
|
at = @record.send("#{s}_#{@machine.field}_at")
|
@@ -98,8 +110,8 @@ module Stator
|
|
98
110
|
return true if later_states.empty?
|
99
111
|
|
100
112
|
# 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.
|
113
|
+
later_groups = later_states.group_by { |s| s[:at] }
|
114
|
+
later_group_key = later_groups.keys.min
|
103
115
|
later_states = later_groups[later_group_key]
|
104
116
|
|
105
117
|
# if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
|
@@ -111,13 +123,30 @@ module Stator
|
|
111
123
|
end
|
112
124
|
|
113
125
|
def likely_state_at(t)
|
114
|
-
@machine.states.reverse.detect{|s| in_state_at?(s,t) }
|
126
|
+
@machine.states.reverse.detect { |s| in_state_at?(s, t) }
|
127
|
+
end
|
128
|
+
|
129
|
+
def without_validation
|
130
|
+
was = @skip_validations
|
131
|
+
@skip_validations = true
|
132
|
+
yield @record
|
133
|
+
ensure
|
134
|
+
@skip_validations = was
|
135
|
+
end
|
136
|
+
|
137
|
+
def without_transition_tracking
|
138
|
+
was = @skip_transition_tracking
|
139
|
+
@skip_transition_tracking = true
|
140
|
+
yield @record
|
141
|
+
ensure
|
142
|
+
@skip_transition_tracking = was
|
115
143
|
end
|
116
144
|
|
117
145
|
protected
|
118
146
|
|
119
147
|
def attempt_to_track_state(state_to_track)
|
120
148
|
return unless state_to_track
|
149
|
+
|
121
150
|
_attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
|
122
151
|
end
|
123
152
|
|
@@ -128,7 +157,8 @@ module Stator
|
|
128
157
|
def _attempt_to_track_change(field_name)
|
129
158
|
return unless @record.respond_to?(field_name)
|
130
159
|
return unless @record.respond_to?("#{field_name}=")
|
131
|
-
return unless @record.send(
|
160
|
+
return unless @record.send(field_name.to_s).nil? || state_changed?
|
161
|
+
return if @record.send("#{field_name}_changed?")
|
132
162
|
|
133
163
|
@record.send("#{field_name}=", (Time.zone || Time).now)
|
134
164
|
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)
|
@@ -131,6 +117,11 @@ module Stator
|
|
131
117
|
integration = self._stator(#{@namespace.inspect}).integration(self)
|
132
118
|
integration.state == #{state.to_s.inspect}
|
133
119
|
end
|
120
|
+
|
121
|
+
def #{method_name}_state_by?(time)
|
122
|
+
integration = self._stator(#{@namespace.inspect}).integration(self)
|
123
|
+
integration.state_by?(#{state.to_s.inspect}, time)
|
124
|
+
end
|
134
125
|
EV
|
135
126
|
end
|
136
127
|
end
|
data/lib/stator/model.rb
CHANGED
@@ -35,26 +35,29 @@ module Stator
|
|
35
35
|
|
36
36
|
def self.included(base)
|
37
37
|
base.class_eval do
|
38
|
-
before_save :
|
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
|
+
def state_by?(state, t, namespace = '')
|
51
|
+
_integration(namespace).state_by?(state, t)
|
52
|
+
end
|
53
53
|
|
54
|
+
protected
|
54
55
|
|
55
|
-
def
|
56
|
+
def _stator_maybe_track_transition
|
56
57
|
self._stators.each do |namespace, machine|
|
57
|
-
machine.
|
58
|
+
next unless machine.tracking_enabled?
|
59
|
+
|
60
|
+
_integration(namespace).track_transition
|
58
61
|
end
|
59
62
|
|
60
63
|
true
|
@@ -70,23 +73,28 @@ module Stator
|
|
70
73
|
end
|
71
74
|
end
|
72
75
|
|
76
|
+
def initialize_dup(other)
|
77
|
+
@_integrations = {}
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
73
81
|
def without_state_transition_validations(namespace = '')
|
74
|
-
|
75
|
-
yield
|
82
|
+
_integration(namespace).without_validation do
|
83
|
+
yield self
|
76
84
|
end
|
77
85
|
end
|
78
86
|
|
79
87
|
def without_state_transition_tracking(namespace = '')
|
80
|
-
|
81
|
-
yield
|
88
|
+
_integration(namespace).without_transition_tracking do
|
89
|
+
yield self
|
82
90
|
end
|
83
91
|
end
|
84
92
|
|
85
93
|
protected
|
86
94
|
|
87
95
|
def _stator_validate_transition
|
88
|
-
self._stators.
|
89
|
-
|
96
|
+
self._stators.each_key do |namespace|
|
97
|
+
_integration(namespace).validate_transition
|
90
98
|
end
|
91
99
|
end
|
92
100
|
|
@@ -94,6 +102,12 @@ module Stator
|
|
94
102
|
self.class._stator(namespace)
|
95
103
|
end
|
96
104
|
|
105
|
+
def _integration(namespace = '')
|
106
|
+
@_integrations ||= {}
|
107
|
+
@_integrations[namespace] ||= _stator(namespace).integration(self)
|
108
|
+
@_integrations[namespace]
|
109
|
+
end
|
110
|
+
|
97
111
|
end
|
98
112
|
end
|
99
113
|
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 = 4
|
7
|
+
PATCH = 0
|
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")
|
161
167
|
|
162
|
-
|
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)
|
194
|
+
|
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,12 +252,11 @@ 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
|
224
|
-
u.email =
|
259
|
+
u.email = "doug@example.com"
|
225
260
|
|
226
261
|
u.tagged_at.should be_nil
|
227
262
|
u.semiactivate!
|
@@ -230,11 +265,93 @@ describe Stator::Model do
|
|
230
265
|
u.tagged_at.should_not be_nil
|
231
266
|
end
|
232
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
|
308
|
+
|
309
|
+
describe "#state_by?" do
|
310
|
+
it "should be true when the transition is earlier" do
|
311
|
+
t = Time.now
|
312
|
+
u = User.create!( email: "doug@example.com", activated_state_at: t)
|
313
|
+
u.state_by?(:activated, Time.at(t.to_i + 1)).should be true
|
314
|
+
u.activated_state_by?(Time.at(t.to_i + 1)).should be true
|
315
|
+
end
|
316
|
+
|
317
|
+
it "should be true when the transition is at the same time" do
|
318
|
+
t = Time.now
|
319
|
+
u = User.create!( email: "doug@example.com", activated_state_at: t)
|
320
|
+
u.state_by?(:activated, t).should be true
|
321
|
+
u.activated_state_by?(t).should be true
|
322
|
+
end
|
323
|
+
|
324
|
+
it "should be false when the transition is later" do
|
325
|
+
t = Time.now
|
326
|
+
u = User.create!( email: "doug@example.com", activated_state_at: t)
|
327
|
+
u.state_by?(:activated, Time.at(t.to_i - 1)).should be false
|
328
|
+
u.activated_state_by?(Time.at(t.to_i - 1)).should be false
|
329
|
+
end
|
330
|
+
|
331
|
+
it "should be false when the transition is nil" do
|
332
|
+
t = Time.now
|
333
|
+
u = User.create!( email: "doug@example.com", activated_state_at: nil)
|
334
|
+
u.state_by?(:activated, t).should be false
|
335
|
+
u.activated_state_by?(t).should be false
|
336
|
+
end
|
337
|
+
|
338
|
+
it "should be true when the transition is not nil and the time is nil" do
|
339
|
+
u = User.create!( email: "doug@example.com", activated_state_at: Time.now)
|
340
|
+
u.state_by?(:activated, nil).should be true
|
341
|
+
u.activated_state_by?(nil).should be true
|
342
|
+
end
|
343
|
+
|
344
|
+
it "should be false when both are nil" do
|
345
|
+
u = User.create!(email: "doug@example.com", activated_state_at: nil)
|
346
|
+
u.state_by?(:activated, nil).should be false
|
347
|
+
u.activated_state_by?(nil).should be false
|
348
|
+
end
|
349
|
+
end
|
233
350
|
end
|
234
351
|
|
235
|
-
describe
|
236
|
-
it
|
237
|
-
u = User.new(:
|
352
|
+
describe "aliasing" do
|
353
|
+
it "should allow aliasing within the dsl" do
|
354
|
+
u = User.new(email: "doug@example.com")
|
238
355
|
u.should respond_to(:active?)
|
239
356
|
u.should respond_to(:inactive?)
|
240
357
|
|
@@ -251,11 +368,11 @@ describe Stator::Model do
|
|
251
368
|
u.should be_active
|
252
369
|
u.should_not be_inactive
|
253
370
|
|
254
|
-
User::ACTIVE_STATES.should eql([
|
255
|
-
User::INACTIVE_STATES.should eql([
|
371
|
+
User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
|
372
|
+
User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
|
256
373
|
|
257
|
-
User.active.to_sql.gsub(
|
258
|
-
User.inactive.to_sql.gsub(
|
374
|
+
User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
|
375
|
+
User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
|
259
376
|
end
|
260
377
|
|
261
378
|
it "should evaluate inverses correctly" do
|
@@ -270,7 +387,7 @@ describe Stator::Model do
|
|
270
387
|
f.should be_house_cleaned
|
271
388
|
end
|
272
389
|
|
273
|
-
it
|
390
|
+
it "should namespace aliases just like everything else" do
|
274
391
|
f = Farm.new
|
275
392
|
f.should respond_to(:house_cleaned?)
|
276
393
|
|
@@ -280,24 +397,23 @@ describe Stator::Model do
|
|
280
397
|
f.should be_house_cleaned
|
281
398
|
end
|
282
399
|
|
283
|
-
it
|
400
|
+
it "should allow for explicit constant and scope names to be provided" do
|
284
401
|
User.should respond_to(:luke_warmers)
|
285
402
|
(!!defined?(User::LUKE_WARMERS)).should eql(true)
|
286
403
|
u = User.new
|
287
404
|
u.should respond_to(:luke_warm?)
|
288
405
|
end
|
289
406
|
|
290
|
-
it
|
407
|
+
it "should not create constants or scopes by default" do
|
291
408
|
u = User.new
|
292
409
|
u.should respond_to(:iced_tea?)
|
293
410
|
(!!defined?(User::ICED_TEA_STATES)).should eql(false)
|
294
411
|
User.should_not respond_to(:iced_tea)
|
295
412
|
end
|
296
413
|
|
297
|
-
it
|
414
|
+
it "should determine the full list of states correctly" do
|
298
415
|
states = User._stator("").states
|
299
|
-
states.should eql([
|
416
|
+
states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
|
300
417
|
end
|
301
418
|
end
|
302
|
-
|
303
419
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/models.rb
CHANGED
@@ -134,6 +134,33 @@ class Zoo < ActiveRecord::Base
|
|
134
134
|
end
|
135
135
|
end
|
136
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
|
137
164
|
|
138
165
|
class Farm < ActiveRecord::Base
|
139
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.4.0
|
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-07-07 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.4.5.1
|
78
|
+
rubygems_version: 3.0.6
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: The simplest of ActiveRecord state machines
|