stator 0.9.0.beta → 0.9.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 +4 -4
- data/lib/stator/alias.rb +30 -51
- data/lib/stator/integration.rb +49 -42
- data/lib/stator/machine.rb +58 -59
- data/lib/stator/model.rb +73 -78
- data/lib/stator/transition.rb +60 -61
- data/lib/stator/version.rb +4 -2
- data/lib/stator.rb +0 -13
- data/stator.gemspec +16 -5
- metadata +71 -45
- data/.github/workflows/build.yml +0 -28
- data/.gitignore +0 -18
- data/.rspec +0 -2
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/Appraisals +0 -23
- data/Gemfile +0 -11
- data/LICENSE.txt +0 -22
- data/README.md +0 -214
- data/Rakefile +0 -9
- data/gemfiles/activerecord_5.1.gemfile +0 -12
- data/gemfiles/activerecord_5.1.gemfile.lock +0 -74
- data/gemfiles/activerecord_5.2.gemfile +0 -12
- data/gemfiles/activerecord_5.2.gemfile.lock +0 -74
- data/gemfiles/activerecord_5.gemfile +0 -12
- data/gemfiles/activerecord_5.gemfile.lock +0 -74
- data/gemfiles/activerecord_6.0.gemfile +0 -12
- data/gemfiles/activerecord_6.0.gemfile.lock +0 -74
- data/gemfiles/activerecord_6.1.gemfile +0 -12
- data/gemfiles/activerecord_6.1.gemfile.lock +0 -73
- data/gemfiles/activerecord_7.0.gemfile +0 -12
- data/gemfiles/activerecord_7.0.gemfile.lock +0 -71
- data/spec/model_spec.rb +0 -418
- data/spec/spec_helper.rb +0 -38
- data/spec/support/models.rb +0 -207
- data/spec/support/schema.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69cde7f0870cc5691869d9e3dbc94c886a74472ec0c858ded58d7f4fde0b5ac1
|
4
|
+
data.tar.gz: 9d72a8142353d239883d4c0d244ca1ef9d46e9dde2bf602e19a7c28e53951e3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29bcc2f18487e5d081e8270646b6a5b612f04aae32c7d0341dda09bf1d90440e6f0788215553ea0d3f86528d8a5593df61d9cb62790fe586af49202bc34db7f1
|
7
|
+
data.tar.gz: 354327de6f695f02c8dffd2b9ed1a6a717c49cb88628a1b1edd7120e152348c6c2aa39f010018d05cb9f0638b3917afd811c3dce49a840996d1881a003ec985f
|
data/lib/stator/alias.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Stator
|
4
2
|
class Alias
|
5
|
-
attr_reader :machine, :name, :namespace, :attr_name, :states, :not, :opposite_args, :constant, :scope
|
6
3
|
|
7
4
|
def initialize(machine, name, options = {})
|
8
5
|
@machine = machine
|
9
6
|
@name = name
|
10
|
-
@namespace = machine.namespace
|
7
|
+
@namespace = @machine.namespace
|
8
|
+
@full_name = [@namespace, @name].compact.join('_')
|
11
9
|
@states = []
|
12
10
|
@not = false
|
13
11
|
@opposite = nil
|
@@ -15,12 +13,8 @@ module Stator
|
|
15
13
|
@scope = options[:scope]
|
16
14
|
end
|
17
15
|
|
18
|
-
def attr_name
|
19
|
-
@attr_name ||= generate_attr_name
|
20
|
-
end
|
21
|
-
|
22
16
|
def is(*args)
|
23
|
-
@states |= args.map(&:
|
17
|
+
@states |= args.map(&:to_s)
|
24
18
|
end
|
25
19
|
|
26
20
|
def is_not(*args)
|
@@ -28,75 +22,60 @@ module Stator
|
|
28
22
|
is(*args)
|
29
23
|
end
|
30
24
|
|
31
|
-
alias not? not
|
32
|
-
|
33
25
|
def opposite(*args)
|
34
|
-
|
35
|
-
@opposite_args = args
|
26
|
+
@opposite = args
|
36
27
|
end
|
37
28
|
|
38
29
|
def evaluate
|
39
30
|
generate_methods
|
40
31
|
|
41
|
-
|
32
|
+
if @opposite
|
33
|
+
op = @machine.state_alias(*@opposite)
|
42
34
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
op.is(*states) if not?
|
47
|
-
op.is_not(*states) unless not?
|
35
|
+
op.is(*@states) if @not
|
36
|
+
op.is_not(*@states) if !@not
|
37
|
+
end
|
48
38
|
end
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
def inverse_states
|
53
|
-
(machine.states - states).map(&:to_sym)
|
54
|
-
end
|
40
|
+
protected
|
55
41
|
|
56
42
|
def inferred_constant_name
|
57
|
-
[
|
58
|
-
end
|
59
|
-
|
60
|
-
def generate_attr_name
|
61
|
-
if namespace == Stator.default_namespace
|
62
|
-
name
|
63
|
-
else
|
64
|
-
[namespace, name].compact.join('_').to_sym
|
65
|
-
end
|
43
|
+
[@full_name.upcase, @machine.field.to_s.pluralize.upcase].join('_')
|
66
44
|
end
|
67
45
|
|
68
46
|
def generate_methods
|
69
|
-
expected_states = (not? ? inverse_states : states)
|
70
47
|
|
71
|
-
|
72
|
-
name = (scope == true ? attr_name : scope)
|
48
|
+
not_states = (@machine.states - @states)
|
73
49
|
|
74
|
-
|
75
|
-
|
50
|
+
if @scope
|
51
|
+
name = @scope == true ? @full_name : @scope
|
52
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
53
|
+
scope #{name.inspect}, lambda {
|
54
|
+
where(_stator(#{@namespace.inspect}).field => #{(@not ? not_states : @states).inspect})
|
55
|
+
}
|
76
56
|
EV
|
77
57
|
end
|
78
58
|
|
79
|
-
if constant
|
80
|
-
name =
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
#{name} = #{inverse_states}.freeze
|
59
|
+
if @constant
|
60
|
+
name = @constant == true ? inferred_constant_name : @constant.to_s.upcase
|
61
|
+
if @not
|
62
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
63
|
+
#{name} = #{not_states.inspect}.freeze
|
85
64
|
EV
|
86
65
|
else
|
87
|
-
machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
88
|
-
#{name} = #{states}.freeze
|
66
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
67
|
+
#{name} = #{@states.inspect}.freeze
|
89
68
|
EV
|
90
69
|
end
|
91
70
|
end
|
92
71
|
|
93
|
-
machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
94
|
-
def #{
|
95
|
-
integration =
|
96
|
-
|
97
|
-
#{expected_states}.include?(integration.state.to_sym)
|
72
|
+
@machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
73
|
+
def #{@full_name}?
|
74
|
+
integration = _stator(#{@namespace.inspect}).integration(self)
|
75
|
+
#{(@not ? not_states : @states).inspect}.include?(integration.state)
|
98
76
|
end
|
99
77
|
EV
|
100
78
|
end
|
79
|
+
|
101
80
|
end
|
102
81
|
end
|
data/lib/stator/integration.rb
CHANGED
@@ -2,48 +2,48 @@
|
|
2
2
|
|
3
3
|
module Stator
|
4
4
|
class Integration
|
5
|
-
delegate :states, to: :machine
|
6
|
-
delegate :transitions, to: :machine
|
7
|
-
delegate :namespace, to: :machine
|
8
5
|
|
9
|
-
|
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
|
10
12
|
|
11
13
|
def initialize(machine, record)
|
12
14
|
@machine = machine
|
13
15
|
@record = record
|
14
|
-
@skip_transition_tracking = false
|
15
16
|
end
|
16
17
|
|
17
18
|
def state=(new_value)
|
18
|
-
record.send("#{machine.field}=", new_value)
|
19
|
+
@record.send("#{@machine.field}=", new_value)
|
19
20
|
end
|
20
21
|
|
21
22
|
def state
|
22
|
-
record.send(machine.field)
|
23
|
+
@record.send(@machine.field)
|
23
24
|
end
|
24
25
|
|
25
26
|
def state_was(use_previous = false)
|
26
27
|
if use_previous
|
27
|
-
record.
|
28
|
+
@record.attribute_before_last_save(@machine.field)
|
28
29
|
else
|
29
|
-
record.
|
30
|
+
@record.attribute_in_database(@machine.field)
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
33
34
|
def state_by?(state, time)
|
34
|
-
field_name = "#{state}_#{machine.field}_at"
|
35
|
-
return false unless record.respond_to?(field_name)
|
36
|
-
return false if record.send(field_name).nil?
|
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?
|
37
38
|
return true if time.nil?
|
38
|
-
|
39
|
-
record.send(field_name) <= time
|
39
|
+
@record.send(field_name) <= time
|
40
40
|
end
|
41
41
|
|
42
42
|
def state_changed?(use_previous = false)
|
43
43
|
if use_previous
|
44
|
-
|
44
|
+
@record.saved_change_to_attribute?(@machine.field)
|
45
45
|
else
|
46
|
-
record.
|
46
|
+
@record.will_save_change_to_attribute?(@machine.field)
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
@@ -51,20 +51,23 @@ module Stator
|
|
51
51
|
return unless state_changed?
|
52
52
|
return if skip_validations
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
was = state_was
|
55
|
+
is = state
|
56
|
+
|
57
|
+
if @record.new_record?
|
58
|
+
invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
|
56
59
|
else
|
57
|
-
invalid_transition!(
|
60
|
+
invalid_transition!(was, is) unless @machine.matching_transition(was, is)
|
58
61
|
end
|
59
62
|
end
|
60
63
|
|
61
64
|
# TODO: i18n
|
62
65
|
def invalid_state!
|
63
|
-
record.errors.add(machine.field,
|
66
|
+
@record.errors.add(@machine.field, "is not a valid state")
|
64
67
|
end
|
65
68
|
|
66
69
|
def invalid_transition!(was, is)
|
67
|
-
record.errors.add(machine.field, "cannot transition to #{is} from #{was}")
|
70
|
+
@record.errors.add(@machine.field, "cannot transition to #{is.inspect} from #{was.inspect}")
|
68
71
|
end
|
69
72
|
|
70
73
|
def track_transition
|
@@ -80,7 +83,7 @@ module Stator
|
|
80
83
|
state = state.to_s
|
81
84
|
t = t.to_time
|
82
85
|
|
83
|
-
state_at = record.send("#{state}_#{machine.field}_at")
|
86
|
+
state_at = @record.send("#{state}_#{@machine.field}_at")
|
84
87
|
|
85
88
|
# if we've never been in the state, the answer is no
|
86
89
|
return false if state_at.nil?
|
@@ -88,18 +91,20 @@ module Stator
|
|
88
91
|
# if we came into this state later in life, the answer is no
|
89
92
|
return false if state_at > t
|
90
93
|
|
91
|
-
all_states = machine.states.reverse
|
94
|
+
all_states = @machine.states.reverse
|
92
95
|
|
93
96
|
# grab all the states and their timestamps that occur on or after state_at and on or before the time in question
|
94
|
-
later_states = all_states.
|
97
|
+
later_states = all_states.map do |s|
|
95
98
|
next if state == s
|
96
99
|
|
97
|
-
at = record.send("#{s}_#{machine.field}_at")
|
100
|
+
at = @record.send("#{s}_#{@machine.field}_at")
|
98
101
|
|
99
|
-
next if at.nil?
|
102
|
+
next if at.nil?
|
103
|
+
next if at < state_at
|
104
|
+
next if at > t
|
100
105
|
|
101
106
|
{ state: s, at: at }
|
102
|
-
end
|
107
|
+
end.compact
|
103
108
|
|
104
109
|
# if there were no states on or after the state_at, the answer is yes
|
105
110
|
return true if later_states.empty?
|
@@ -110,51 +115,53 @@ module Stator
|
|
110
115
|
later_states = later_groups[later_group_key]
|
111
116
|
|
112
117
|
# if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
|
113
|
-
|
118
|
+
if later_states[0][:at] == state_at
|
119
|
+
return all_states.index(state) < all_states.index(later_states[0][:state])
|
120
|
+
end
|
114
121
|
|
115
122
|
false
|
116
123
|
end
|
117
124
|
|
118
125
|
def likely_state_at(t)
|
119
|
-
machine.states.reverse.detect { |s| in_state_at?(s, t) }
|
126
|
+
@machine.states.reverse.detect { |s| in_state_at?(s, t) }
|
120
127
|
end
|
121
128
|
|
122
129
|
def without_validation
|
123
|
-
was = skip_validations
|
130
|
+
was = @skip_validations
|
124
131
|
@skip_validations = true
|
125
|
-
yield record
|
132
|
+
yield @record
|
126
133
|
ensure
|
127
134
|
@skip_validations = was
|
128
135
|
end
|
129
136
|
|
130
137
|
def without_transition_tracking
|
131
|
-
was = skip_transition_tracking
|
138
|
+
was = @skip_transition_tracking
|
132
139
|
@skip_transition_tracking = true
|
133
|
-
yield record
|
140
|
+
yield @record
|
134
141
|
ensure
|
135
142
|
@skip_transition_tracking = was
|
136
143
|
end
|
137
144
|
|
138
|
-
|
145
|
+
protected
|
139
146
|
|
140
147
|
def attempt_to_track_state(state_to_track)
|
141
148
|
return unless state_to_track
|
142
149
|
|
143
|
-
_attempt_to_track_change("#{state_to_track}_#{machine.field}_at")
|
150
|
+
_attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
|
144
151
|
end
|
145
152
|
|
146
153
|
def attempt_to_track_state_changed_timestamp
|
147
|
-
_attempt_to_track_change("#{machine.field}_changed_at")
|
154
|
+
_attempt_to_track_change("#{@machine.field}_changed_at")
|
148
155
|
end
|
149
156
|
|
150
157
|
def _attempt_to_track_change(field_name)
|
151
|
-
return unless record.respond_to?(field_name)
|
152
|
-
return unless record.respond_to?("#{field_name}=")
|
153
|
-
return unless record.send(field_name.to_s).nil? || state_changed?
|
154
|
-
|
155
|
-
return if record.send("#{field_name}_changed?")
|
158
|
+
return unless @record.respond_to?(field_name)
|
159
|
+
return unless @record.respond_to?("#{field_name}=")
|
160
|
+
return unless @record.send(field_name.to_s).nil? || state_changed?
|
161
|
+
return if @record.will_save_change_to_attribute?(field_name) && !@record.send(field_name.to_s).nil?
|
156
162
|
|
157
|
-
record.send("#{field_name}=", (Time.zone || Time).now)
|
163
|
+
@record.send("#{field_name}=", (Time.zone || Time).now)
|
158
164
|
end
|
165
|
+
|
159
166
|
end
|
160
167
|
end
|
data/lib/stator/machine.rb
CHANGED
@@ -1,65 +1,57 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Stator
|
4
2
|
class Machine
|
5
|
-
attr_reader :initial_state, :field, :transitions, :states, :namespace,
|
6
|
-
:class_name, :name, :aliases, :options, :tracking_enabled, :klass
|
7
|
-
|
8
|
-
def self.find_or_create(klass, *kwargs)
|
9
|
-
kwargs = kwargs.first
|
10
|
-
|
11
|
-
klass._stators[kwargs[:namespace]] ||= new(klass, **kwargs)
|
12
|
-
end
|
13
|
-
|
14
|
-
def klass
|
15
|
-
@klass ||= class_name.constantize
|
16
|
-
end
|
17
3
|
|
18
|
-
|
19
|
-
|
4
|
+
attr_reader :initial_state
|
5
|
+
attr_reader :field
|
6
|
+
attr_reader :transition_names
|
7
|
+
attr_reader :transitions
|
8
|
+
attr_reader :states
|
9
|
+
attr_reader :namespace
|
20
10
|
|
11
|
+
def initialize(klass, options = {})
|
21
12
|
@class_name = klass.name
|
22
13
|
@field = options[:field] || :state
|
23
|
-
@namespace =
|
14
|
+
@namespace = options[:namespace]
|
24
15
|
|
25
|
-
@initial_state = options[:initial]
|
26
|
-
@states = [initial_state].compact
|
16
|
+
@initial_state = options[:initial] && options[:initial].to_s
|
27
17
|
@tracking_enabled = options[:track] || false
|
28
18
|
|
29
19
|
@transitions = []
|
30
20
|
@aliases = []
|
31
21
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
alias tracking_enabled? tracking_enabled
|
22
|
+
# pushed out into their own variables for performance reasons (AR integration can use method missing - see the HelperMethods module)
|
23
|
+
@transition_names = []
|
24
|
+
@states = [@initial_state].compact
|
36
25
|
|
37
|
-
|
38
|
-
instance_eval(&block)
|
39
|
-
evaluate
|
26
|
+
@options = options
|
40
27
|
end
|
41
28
|
|
42
29
|
def integration(record)
|
43
|
-
Stator::Integration.new(self, record)
|
30
|
+
::Stator::Integration.new(self, record)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_transition(name)
|
34
|
+
@transitions.detect{|t| t.name.to_s == name.to_s}
|
44
35
|
end
|
45
36
|
|
46
37
|
def transition(name, &block)
|
47
|
-
Stator::Transition.new(class_name, name, namespace)
|
48
|
-
|
38
|
+
t = ::Stator::Transition.new(@class_name, name, @namespace)
|
39
|
+
t.instance_eval(&block) if block_given?
|
49
40
|
|
50
|
-
|
41
|
+
verify_transition_validity(t)
|
51
42
|
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
@transitions << t
|
44
|
+
@transition_names |= [t.full_name] unless t.full_name.blank?
|
45
|
+
@states |= [t.to_state] unless t.to_state.nil?
|
46
|
+
|
47
|
+
t
|
55
48
|
end
|
56
49
|
|
57
50
|
def state_alias(name, options = {}, &block)
|
58
|
-
Stator::Alias.new(self, name, options)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
51
|
+
a = ::Stator::Alias.new(self, name, options)
|
52
|
+
a.instance_eval(&block) if block_given?
|
53
|
+
@aliases << a
|
54
|
+
a
|
63
55
|
end
|
64
56
|
|
65
57
|
def state(name, &block)
|
@@ -70,32 +62,34 @@ module Stator
|
|
70
62
|
end
|
71
63
|
end
|
72
64
|
|
65
|
+
def tracking_enabled?
|
66
|
+
@tracking_enabled
|
67
|
+
end
|
68
|
+
|
73
69
|
def conditional(*states, &block)
|
74
|
-
|
70
|
+
_namespace = @namespace
|
75
71
|
|
76
|
-
klass.instance_exec(
|
72
|
+
klass.instance_exec(proc { states.map(&:to_s).include?(self._stator(_namespace).integration(self).state) }, &block)
|
77
73
|
end
|
78
74
|
|
79
75
|
def matching_transition(from, to)
|
80
|
-
transitions.detect
|
76
|
+
@transitions.detect do |transition|
|
77
|
+
transition.valid?(from, to)
|
78
|
+
end
|
81
79
|
end
|
82
80
|
|
83
81
|
def evaluate
|
84
|
-
transitions.each(&:evaluate)
|
85
|
-
aliases.each(&:evaluate)
|
82
|
+
@transitions.each(&:evaluate)
|
83
|
+
@aliases.each(&:evaluate)
|
86
84
|
generate_methods
|
87
85
|
end
|
88
86
|
|
89
|
-
|
90
|
-
|
91
|
-
def attr_name(name)
|
92
|
-
if namespace == Stator.default_namespace
|
93
|
-
name.to_sym
|
94
|
-
else
|
95
|
-
[namespace, name].compact.join('_').to_sym
|
96
|
-
end
|
87
|
+
def klass
|
88
|
+
@class_name.constantize
|
97
89
|
end
|
98
90
|
|
91
|
+
protected
|
92
|
+
|
99
93
|
def verify_transition_validity(transition)
|
100
94
|
verify_state_singularity_of_transition(transition)
|
101
95
|
verify_name_singularity_of_transition(transition)
|
@@ -104,29 +98,34 @@ module Stator
|
|
104
98
|
def verify_state_singularity_of_transition(transition)
|
105
99
|
transition.from_states.each do |from|
|
106
100
|
if matching_transition(from, transition.to_state)
|
107
|
-
raise "[Stator] another transition already exists which moves #{class_name} from #{from} to #{transition}"
|
101
|
+
raise "[Stator] another transition already exists which moves #{@class_name} from #{from.inspect} to #{transition.to_state.inspect}"
|
108
102
|
end
|
109
103
|
end
|
110
104
|
end
|
111
105
|
|
112
106
|
def verify_name_singularity_of_transition(transition)
|
113
|
-
if transitions.detect
|
114
|
-
raise "[Stator] another transition already exists with the name of #{transition.name} in the #{class_name} class"
|
107
|
+
if @transitions.detect{|other| transition.name && transition.name == other.name }
|
108
|
+
raise "[Stator] another transition already exists with the name of #{transition.name.inspect} in the #{@class_name} class"
|
115
109
|
end
|
116
110
|
end
|
117
111
|
|
118
112
|
def generate_methods
|
119
|
-
states.each do |state|
|
113
|
+
self.states.each do |state|
|
114
|
+
method_name = [@namespace, state].compact.join('_')
|
120
115
|
klass.class_eval <<-EV, __FILE__, __LINE__ + 1
|
121
|
-
def #{
|
122
|
-
|
116
|
+
def #{method_name}?
|
117
|
+
integration = self._stator(#{@namespace.inspect}).integration(self)
|
118
|
+
integration.state == #{state.to_s.inspect}
|
123
119
|
end
|
124
120
|
|
125
|
-
def #{
|
126
|
-
|
121
|
+
def #{method_name}_state_by?(time)
|
122
|
+
integration = self._stator(#{@namespace.inspect}).integration(self)
|
123
|
+
integration.state_by?(#{state.to_s.inspect}, time)
|
127
124
|
end
|
128
125
|
EV
|
129
126
|
end
|
130
127
|
end
|
128
|
+
|
129
|
+
|
131
130
|
end
|
132
131
|
end
|