stator 0.2.0 → 0.3.3
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 +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
|