state_gate 1.2.3 → 2.0.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/state_gate/assertions.rb +246 -0
- data/lib/state_gate/builder/conflict_detection_methods.rb +59 -18
- data/lib/state_gate/builder/dynamic_module_creation_methods.rb +36 -22
- data/lib/state_gate/builder/scope_methods.rb +30 -20
- data/lib/state_gate/builder/state_methods.rb +108 -72
- data/lib/state_gate/builder/transition_methods.rb +54 -34
- data/lib/state_gate/builder/transition_validation_methods.rb +76 -66
- data/lib/state_gate/builder.rb +53 -43
- data/lib/state_gate/engine/configurator.rb +81 -63
- data/lib/state_gate/engine/errator.rb +42 -9
- data/lib/state_gate/engine/fixer.rb +21 -12
- data/lib/state_gate/engine/scoper.rb +12 -4
- data/lib/state_gate/engine/sequencer.rb +18 -5
- data/lib/state_gate/engine/stator.rb +98 -53
- data/lib/state_gate/engine/transitioner.rb +28 -14
- data/lib/state_gate/engine.rb +19 -6
- data/lib/state_gate/type.rb +22 -2
- data/lib/state_gate/version.rb +34 -0
- data/lib/state_gate.rb +88 -61
- metadata +9 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4ac3088aa966d3650145f5294d93731291dcd39397597dbb7d35eb69adb5a17
|
|
4
|
+
data.tar.gz: 4e8d593dd1facb0757146b580f3f05c6cfb335c2b163cfa0f09f12d5b072e55d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90471292a3c2be72e97274785d6934104ce2b3014d424bb4b91517de0d1ccade7f735c2ff6ca4ddfadcfae716d985708b3eb106466b9a0d8eb2c8ed59ab42a6b
|
|
7
|
+
data.tar.gz: 9afb2f7c2d284cf6456a61637403bb513c20bf2a93a37229059ae81c97e11731f0a2c5746b8ea4a2d797bbb7b8ad745b0d4c491b16746b64ec12d75df5c984a9
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateGate
|
|
4
|
+
##
|
|
5
|
+
# = Description
|
|
6
|
+
#
|
|
7
|
+
# Minitest assertions to verify StateGate defined states and transitions.
|
|
8
|
+
#
|
|
9
|
+
# The Minitest equivalent of the StateGate RSpec +have_states+ and
|
|
10
|
+
# +allow_transitions_on+ matchers.
|
|
11
|
+
#
|
|
12
|
+
# [:source_obj]
|
|
13
|
+
# The Class or Instance to be tested.
|
|
14
|
+
#
|
|
15
|
+
# [:attribute]
|
|
16
|
+
# The attribute being tested.
|
|
17
|
+
#
|
|
18
|
+
# [:states]
|
|
19
|
+
# The expected states as Symbols or Strings.
|
|
20
|
+
#
|
|
21
|
+
# [:from]
|
|
22
|
+
# The state being transitioned from.
|
|
23
|
+
#
|
|
24
|
+
# [:to]
|
|
25
|
+
# The states being transitioned to, as a Symbol, String or Array.
|
|
26
|
+
#
|
|
27
|
+
# assert_has_states Sysop, :status, :pending, :active
|
|
28
|
+
# refute_has_states Sysop, :status, :deleted
|
|
29
|
+
#
|
|
30
|
+
# assert_allows_transitions Sysop, :status, from: :active, to: [:locked, :archived]
|
|
31
|
+
# refute_allows_transitions Sysop, :status, from: :active, to: :pending
|
|
32
|
+
#
|
|
33
|
+
# +assert_has_states+ fails if an existing state is missing, or if given
|
|
34
|
+
# states are not defined for the attribute.
|
|
35
|
+
#
|
|
36
|
+
# +refute_has_states+ fails if any of the given states are defined for
|
|
37
|
+
# the attribute.
|
|
38
|
+
#
|
|
39
|
+
# +assert_allows_transitions+ fails if an allowed transition is missing, or
|
|
40
|
+
# if a given transition is not allowed.
|
|
41
|
+
#
|
|
42
|
+
# +refute_allows_transitions+ fails if any of the given transitions are
|
|
43
|
+
# allowed for the state.
|
|
44
|
+
#
|
|
45
|
+
module Assertions
|
|
46
|
+
|
|
47
|
+
# States
|
|
48
|
+
# ======================================================================
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Asserts the given states are exactly the states defined for the attribute.
|
|
52
|
+
#
|
|
53
|
+
# assert_has_states Sysop, :status, :pending, :active, :locked
|
|
54
|
+
# assert_has_states sysop, :status, %w[pending active locked]
|
|
55
|
+
#
|
|
56
|
+
def assert_has_states(source_obj, attribute, *states)
|
|
57
|
+
engine = state_gate_engine(source_obj, attribute)
|
|
58
|
+
key = StateGate.symbolize(attribute)
|
|
59
|
+
expected = state_gate_symbolize_states(states)
|
|
60
|
+
|
|
61
|
+
missing = engine.states - expected
|
|
62
|
+
extra = expected - engine.states
|
|
63
|
+
|
|
64
|
+
return pass if missing.empty? && extra.empty?
|
|
65
|
+
|
|
66
|
+
messages = []
|
|
67
|
+
messages << state_gate_also_valid_message(missing, key) if missing.any?
|
|
68
|
+
messages << state_gate_not_valid_message(extra, key) if extra.any?
|
|
69
|
+
|
|
70
|
+
flunk messages.join(' ')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Asserts none of the given states are defined for the attribute.
|
|
76
|
+
#
|
|
77
|
+
# refute_has_states Sysop, :status, :deleted, :banned
|
|
78
|
+
#
|
|
79
|
+
def refute_has_states(source_obj, attribute, *states)
|
|
80
|
+
engine = state_gate_engine(source_obj, attribute)
|
|
81
|
+
key = StateGate.symbolize(attribute)
|
|
82
|
+
expected = state_gate_symbolize_states(states)
|
|
83
|
+
|
|
84
|
+
found = expected.select { |s| engine.states.include?(s) }
|
|
85
|
+
|
|
86
|
+
return pass if found.empty?
|
|
87
|
+
|
|
88
|
+
if found.one?
|
|
89
|
+
flunk "#{state_gate_list(found)} is a valid state for ##{key}."
|
|
90
|
+
else
|
|
91
|
+
flunk "#{state_gate_list(found)} are valid states for ##{key}."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Transitions
|
|
97
|
+
# ======================================================================
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Asserts the given states are exactly the transitions allowed from the
|
|
101
|
+
# +:from+ state.
|
|
102
|
+
#
|
|
103
|
+
# assert_allows_transitions Sysop, :status, from: :active,
|
|
104
|
+
# to: [:locked, :suspended, :archived]
|
|
105
|
+
# assert_allows_transitions sysop, :status, from: :pending, to: :active
|
|
106
|
+
#
|
|
107
|
+
def assert_allows_transitions(source_obj, attribute, from: nil, to: nil)
|
|
108
|
+
key, engine, state, expected = state_gate_transition_setup(source_obj, attribute, from, to)
|
|
109
|
+
|
|
110
|
+
allowed = engine.transitions_for_state(state)
|
|
111
|
+
missing = allowed - expected
|
|
112
|
+
extra = expected - allowed
|
|
113
|
+
|
|
114
|
+
return pass if missing.empty? && extra.empty?
|
|
115
|
+
|
|
116
|
+
messages = []
|
|
117
|
+
if missing.any?
|
|
118
|
+
messages << "##{key} also transitions from :#{state} to #{state_gate_list(missing)}."
|
|
119
|
+
end
|
|
120
|
+
if extra.any?
|
|
121
|
+
messages << "##{key} does not transition from :#{state} to #{state_gate_list(extra)}."
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
flunk messages.join(' ')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Asserts none of the given states are transitions allowed from the
|
|
130
|
+
# +:from+ state.
|
|
131
|
+
#
|
|
132
|
+
# refute_allows_transitions Sysop, :status, from: :archived, to: [:locked, :suspended]
|
|
133
|
+
#
|
|
134
|
+
def refute_allows_transitions(source_obj, attribute, from: nil, to: nil)
|
|
135
|
+
_key, engine, state, expected = state_gate_transition_setup(source_obj, attribute, from, to)
|
|
136
|
+
|
|
137
|
+
found = expected & engine.transitions_for_state(state)
|
|
138
|
+
|
|
139
|
+
return pass if found.empty?
|
|
140
|
+
|
|
141
|
+
flunk ":#{state} is allowed to transition to #{state_gate_list(found)}."
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Helpers
|
|
146
|
+
# ======================================================================
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# Returns the StateGate engine for the attribute, flunking with the
|
|
152
|
+
# relevant message when the setup is incomplete or incorrect.
|
|
153
|
+
#
|
|
154
|
+
def state_gate_engine(source_obj, attribute) # :nodoc:
|
|
155
|
+
key = StateGate.symbolize(attribute)
|
|
156
|
+
|
|
157
|
+
flunk 'missing the <attribute> to be tested.' if key.blank?
|
|
158
|
+
|
|
159
|
+
unless source_obj.respond_to?(:stateables)
|
|
160
|
+
flunk "no state machines are defined for #{state_gate_source_name(source_obj)}."
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
engine = source_obj.stateables[key]
|
|
164
|
+
flunk "no state machine is defined for ##{key}." if engine.blank?
|
|
165
|
+
|
|
166
|
+
engine
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
##
|
|
171
|
+
# Validates the transition parameters, returning the attribute key, the
|
|
172
|
+
# engine, the +:from+ state and the expected +:to+ states.
|
|
173
|
+
#
|
|
174
|
+
def state_gate_transition_setup(source_obj, attribute, from, to) # :nodoc:
|
|
175
|
+
engine = state_gate_engine(source_obj, attribute)
|
|
176
|
+
key = StateGate.symbolize(attribute)
|
|
177
|
+
|
|
178
|
+
flunk 'missing the <from:> state.' if from.blank?
|
|
179
|
+
flunk 'missing the <to:> states.' if to.nil?
|
|
180
|
+
|
|
181
|
+
state = state_gate_symbolize_states([from]).first
|
|
182
|
+
unless engine.states.include?(state)
|
|
183
|
+
flunk ":#{state} is not a valid state for #{state_gate_source_name(source_obj)}##{key}."
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
expected = state_gate_symbolize_states(Array(to))
|
|
187
|
+
invalid = expected.reject { |s| engine.states.include?(s) }
|
|
188
|
+
|
|
189
|
+
if invalid.one?
|
|
190
|
+
flunk "#{state_gate_list(invalid)} is not a valid ##{key} state."
|
|
191
|
+
elsif invalid.any?
|
|
192
|
+
flunk "#{state_gate_list(invalid)} are not valid ##{key} states."
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
[key, engine, state, expected]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# The name of the Class or Instance being tested.
|
|
201
|
+
#
|
|
202
|
+
def state_gate_source_name(source_obj) # :nodoc:
|
|
203
|
+
source_obj.is_a?(Class) ? source_obj.name : source_obj.class.name
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# Flattens and symbolizes the given state names.
|
|
209
|
+
#
|
|
210
|
+
def state_gate_symbolize_states(states) # :nodoc:
|
|
211
|
+
states.flatten.map do |state|
|
|
212
|
+
StateGate.symbolize(state) || flunk("\"#{state}\" is not a valid state name.")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ":pending, :active, and :locked"
|
|
218
|
+
#
|
|
219
|
+
def state_gate_list(states) # :nodoc:
|
|
220
|
+
states.map { |s| ":#{s}" }.to_sentence
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# "...is also a valid state for #status."
|
|
225
|
+
#
|
|
226
|
+
def state_gate_also_valid_message(missing, key) # :nodoc:
|
|
227
|
+
if missing.one?
|
|
228
|
+
"#{state_gate_list(missing)} is also a valid state for ##{key}."
|
|
229
|
+
else
|
|
230
|
+
"#{state_gate_list(missing)} are also valid states for ##{key}."
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# "...is not a valid state for #status."
|
|
236
|
+
#
|
|
237
|
+
def state_gate_not_valid_message(extra, key) # :nodoc:
|
|
238
|
+
if extra.one?
|
|
239
|
+
"#{state_gate_list(extra)} is not a valid state for ##{key}."
|
|
240
|
+
else
|
|
241
|
+
"#{state_gate_list(extra)} are not valid states for ##{key}."
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
end # Assertions
|
|
246
|
+
end # StateGate
|
|
@@ -14,54 +14,73 @@ module StateGate
|
|
|
14
14
|
# ======================================================================
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
+
##
|
|
17
18
|
# Check if a class method is already defined. Checks are made:
|
|
18
19
|
# 1 --> is it an ActiveRecord dangerous method?
|
|
19
20
|
# 2 --> is it an ActiveRecord defined class method?
|
|
20
21
|
# 3 --> is it a singleton method of the klass?
|
|
21
22
|
# 4 --> is it defined within any of the klass ancestors?
|
|
22
23
|
#
|
|
23
|
-
#
|
|
24
|
+
# @param [Symbol] method_name
|
|
25
|
+
# the class method name to test for conclict
|
|
24
26
|
#
|
|
25
|
-
|
|
27
|
+
# @raise [ConflictError]
|
|
28
|
+
# if the method name has already been defined
|
|
29
|
+
#
|
|
30
|
+
def _detect_class_method_conflict!(method_name)
|
|
26
31
|
defining_klass = _active_record_protected_method?(method_name) ||
|
|
27
32
|
_klass_singleton_method?(method_name) ||
|
|
28
33
|
_klass_ancestor_singleton_method?(method_name)
|
|
29
34
|
|
|
30
35
|
return unless defining_klass
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
_raise_conflict_error method_name, type: 'a class', source: defining_klass
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
|
|
42
|
+
##
|
|
37
43
|
# Check an instance method is already defined. Checks are made:
|
|
38
44
|
# 1 --> is it an ActiveRecord dangerous method?
|
|
39
45
|
# 2 --> is it an ActiveRecord defined instance method?
|
|
40
46
|
# 3 --> is it an instance method of the klass?
|
|
41
47
|
# 4 --> is it defined within any of the klass ancestors?
|
|
42
48
|
#
|
|
43
|
-
#
|
|
49
|
+
# @param [Symbol] method_name
|
|
50
|
+
# the instance method name to test for conclict
|
|
51
|
+
#
|
|
52
|
+
# @raise [ConflictError]
|
|
53
|
+
# if the method name has already been defined
|
|
44
54
|
#
|
|
45
|
-
def
|
|
55
|
+
def _detect_instance_method_conflict!(method_name)
|
|
46
56
|
defining_klass = _active_record_protected_method?(method_name) ||
|
|
47
57
|
_klass_instance_method?(method_name) ||
|
|
48
58
|
_klass_ancestor_instance_method?(method_name)
|
|
49
59
|
|
|
50
60
|
return unless defining_klass
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
_raise_conflict_error method_name, source: defining_klass
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
|
|
56
66
|
|
|
57
67
|
# Raise a StateGate::ConflictError with a details message of the problem
|
|
58
68
|
#
|
|
59
|
-
#
|
|
69
|
+
# @param [Symbol] method_name
|
|
70
|
+
# the method name
|
|
71
|
+
#
|
|
72
|
+
# @param [String] type
|
|
73
|
+
# the optional definition of which type of errors this is: instance or class
|
|
60
74
|
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
75
|
+
# @param [String] source
|
|
76
|
+
# the optional class name
|
|
63
77
|
#
|
|
64
|
-
|
|
78
|
+
# @raise [ConflictError]
|
|
79
|
+
# with the message:
|
|
80
|
+
# StateGate for Klass#attribute will generate a class
|
|
81
|
+
# method 'statuses', which is already defined by ActiveRecord.
|
|
82
|
+
#
|
|
83
|
+
def _raise_conflict_error(method_name, type: 'an instance', source: 'ActiveRecord')
|
|
65
84
|
fail StateGate::ConflictError, I18n.t('state_gate.builder.conflict_err',
|
|
66
85
|
klass: @klass,
|
|
67
86
|
attribute: @attribute,
|
|
@@ -72,25 +91,37 @@ module StateGate
|
|
|
72
91
|
|
|
73
92
|
|
|
74
93
|
|
|
94
|
+
##
|
|
75
95
|
# Check if the method is an ActiveRecord dangerous method name
|
|
76
96
|
#
|
|
77
|
-
|
|
97
|
+
# @param [Symbol] method_name
|
|
98
|
+
# the method name
|
|
99
|
+
#
|
|
100
|
+
def _active_record_protected_method?(method_name)
|
|
78
101
|
'ActiveRecord' if _dangerous_method_names.include?(method_name)
|
|
79
102
|
end
|
|
80
103
|
|
|
81
104
|
|
|
82
105
|
|
|
106
|
+
##
|
|
83
107
|
# Check if the method is a singleton method of the klass
|
|
84
108
|
#
|
|
85
|
-
|
|
109
|
+
# @param [Symbol] method_name
|
|
110
|
+
# the method name
|
|
111
|
+
#
|
|
112
|
+
def _klass_singleton_method?(method_name)
|
|
86
113
|
@klass.name if @klass.singleton_methods(false).include?(method_name.to_sym)
|
|
87
114
|
end
|
|
88
115
|
|
|
89
116
|
|
|
90
117
|
|
|
118
|
+
##
|
|
91
119
|
# Check if the method is an ancestral singleton method of the klass
|
|
92
120
|
#
|
|
93
|
-
|
|
121
|
+
# @param [Symbol] method_name
|
|
122
|
+
# the method name
|
|
123
|
+
#
|
|
124
|
+
def _klass_ancestor_singleton_method?(method_name)
|
|
94
125
|
return nil unless @klass.respond_to?(method_name)
|
|
95
126
|
|
|
96
127
|
@klass.singleton_class
|
|
@@ -101,17 +132,25 @@ module StateGate
|
|
|
101
132
|
|
|
102
133
|
|
|
103
134
|
|
|
135
|
+
##
|
|
104
136
|
# Check if the method an instance method of the klass
|
|
105
137
|
#
|
|
106
|
-
|
|
138
|
+
# @param [Symbol] method_name
|
|
139
|
+
# the method name
|
|
140
|
+
#
|
|
141
|
+
def _klass_instance_method?(method_name)
|
|
107
142
|
@klass.instance_methods(false).include?(method_name.to_sym) ? @klass.name : nil
|
|
108
143
|
end
|
|
109
144
|
|
|
110
145
|
|
|
111
146
|
|
|
147
|
+
##
|
|
112
148
|
# Check if the method is an ancestral singleton method of the klass
|
|
113
149
|
#
|
|
114
|
-
|
|
150
|
+
# @param [Symbol] method_name
|
|
151
|
+
# the method name
|
|
152
|
+
#
|
|
153
|
+
def _klass_ancestor_instance_method?(method_name)
|
|
115
154
|
return nil unless @klass.instance_methods.include?(method_name.to_sym)
|
|
116
155
|
|
|
117
156
|
@klass.ancestors
|
|
@@ -121,9 +160,11 @@ module StateGate
|
|
|
121
160
|
|
|
122
161
|
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
#
|
|
126
|
-
#
|
|
163
|
+
##
|
|
164
|
+
# @return [Array[Symbol]]
|
|
165
|
+
# array of dagerous methods names found in
|
|
166
|
+
# ActiveRecord::AttributeMethods::RESTRICTED_CLASS_METHODS (which is called
|
|
167
|
+
# BLACKLISTED_CLASS_METHODS in 5.0 and 5.1)
|
|
127
168
|
#
|
|
128
169
|
def _dangerous_method_names
|
|
129
170
|
%w[private public protected allocate new name parent superclass]
|
|
@@ -19,14 +19,15 @@ module StateGate
|
|
|
19
19
|
# = Dynamic Module Creation
|
|
20
20
|
# ======================================================================
|
|
21
21
|
|
|
22
|
+
##
|
|
22
23
|
# Dynamically generated module to hold the StateGate helper methods. This
|
|
23
24
|
# keeps a clear distinction between the state machine helper methods and the klass'
|
|
24
25
|
# own methods.
|
|
25
26
|
#
|
|
26
27
|
# The module is named after the class and is created if needed, or reused if exisitng.
|
|
27
28
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
29
|
+
# @note
|
|
30
|
+
# the module is named "<klass>::StateGate_HelperMethods"
|
|
30
31
|
#
|
|
31
32
|
def _helper_methods_module
|
|
32
33
|
@_helper_methods_module ||= begin
|
|
@@ -46,6 +47,7 @@ module StateGate
|
|
|
46
47
|
# ======================================================================
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
##
|
|
49
51
|
# Adds the hook method :method_added to the Klass, detecting any new method
|
|
50
52
|
# definitions for an attribute already defined as a StateGate.
|
|
51
53
|
#
|
|
@@ -54,16 +56,15 @@ module StateGate
|
|
|
54
56
|
#
|
|
55
57
|
# method_name - the name of the newly defined method.
|
|
56
58
|
#
|
|
57
|
-
#
|
|
59
|
+
# @note
|
|
60
|
+
# This method is added last so it does not trigger when StateGate adds
|
|
61
|
+
# the attribute methods.
|
|
58
62
|
#
|
|
59
|
-
#
|
|
60
|
-
# the attribute methods.
|
|
63
|
+
# meta
|
|
61
64
|
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# * does the new defined method use 'attr' or 'attr='?
|
|
66
|
-
# * if so then record an error logger if denied, othewise use `puts`
|
|
65
|
+
# - loop though each state machine attribute.
|
|
66
|
+
# - does the new defined method use 'attr' or 'attr='?
|
|
67
|
+
# - if so then record an error logger if denied, othewise use `puts`
|
|
67
68
|
#
|
|
68
69
|
def _generate_method_redefine_detection # rubocop:disable Metrics/MethodLength
|
|
69
70
|
@klass.instance_eval(%(
|
|
@@ -89,29 +90,42 @@ module StateGate
|
|
|
89
90
|
# Method Creation
|
|
90
91
|
# ======================================================================
|
|
91
92
|
|
|
93
|
+
##
|
|
92
94
|
# Add an Class helper method to the _helper_methods_module
|
|
93
95
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
96
|
+
# @param [String] method_name
|
|
97
|
+
# name for the method to check for conflicts
|
|
98
|
+
#
|
|
99
|
+
# @param [String] file
|
|
100
|
+
# file name for error reporting
|
|
101
|
+
#
|
|
102
|
+
# @param [String,Integer] line
|
|
103
|
+
# line number for error reporting
|
|
104
|
+
#
|
|
105
|
+
# @param [String] method_body
|
|
106
|
+
# a String to be evaluated in the module
|
|
98
107
|
#
|
|
99
|
-
def
|
|
100
|
-
|
|
108
|
+
def _add__klass__helper_method(method_name, file, line, method_body)
|
|
109
|
+
_detect_class_method_conflict!(method_name)
|
|
101
110
|
@klass.instance_eval(method_body, file, line)
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
|
|
105
114
|
|
|
115
|
+
##
|
|
106
116
|
# Add an instance helper method to the _helper_methods_module
|
|
107
117
|
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
118
|
+
# @param [String] method_name
|
|
119
|
+
# name for the method to check for conflicts
|
|
120
|
+
# @param [String] file
|
|
121
|
+
# file name for error reporting
|
|
122
|
+
# @param [String] line
|
|
123
|
+
# line number for error reporting
|
|
124
|
+
# @param [String] method_body
|
|
125
|
+
# a String to bhe evaluates in the module
|
|
112
126
|
#
|
|
113
|
-
def
|
|
114
|
-
|
|
127
|
+
def _add__instance__helper_method(method_name, file, line, method_body)
|
|
128
|
+
_detect_instance_method_conflict!(method_name)
|
|
115
129
|
_helper_methods_module.module_eval(method_body, file, line)
|
|
116
130
|
end
|
|
117
131
|
|
|
@@ -8,14 +8,14 @@ module StateGate
|
|
|
8
8
|
# Multiple private methods enabling StateGate::Builder to generate
|
|
9
9
|
# scopes for each state.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# Klass.active
|
|
11
|
+
# - fetch all records with the given state:
|
|
12
|
+
# Klass.active #=> Klass.where(state: :active)
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
# Klass.not_active
|
|
14
|
+
# - fetch all records without the given state:
|
|
15
|
+
# Klass.not_active #=> Klass.where.not(state: :active)
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
# Klass.with_statuses(:pending, :active)
|
|
17
|
+
# - fetch all records with the supplied states:
|
|
18
|
+
# Klass.with_statuses(:pending, :active) #=> Klass.where(state: [:pending, :active])
|
|
19
19
|
#
|
|
20
20
|
module ScopeMethods
|
|
21
21
|
|
|
@@ -24,12 +24,13 @@ module StateGate
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
##
|
|
27
28
|
# Add scopes to the klass for filtering by state
|
|
28
29
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
30
|
+
# @note
|
|
31
|
+
# The scope name is a concatenation of <prefix><state name><suffix>
|
|
31
32
|
#
|
|
32
|
-
def
|
|
33
|
+
def _generate_scope_methods
|
|
33
34
|
return unless @engine.include_scopes?
|
|
34
35
|
|
|
35
36
|
_add__klass__state_scopes
|
|
@@ -45,49 +46,58 @@ module StateGate
|
|
|
45
46
|
# Klass methods
|
|
46
47
|
# ======================================================================
|
|
47
48
|
|
|
49
|
+
##
|
|
48
50
|
# Add a klass method that scopes records to the specified state.
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# Klass.active #=> ActiveRecord::Relation
|
|
54
|
+
# Klass.active_status #=> ActiveRecord::Relation
|
|
52
55
|
#
|
|
53
56
|
def _add__klass__state_scopes
|
|
54
57
|
attr_name = @attribute
|
|
55
58
|
|
|
56
59
|
@engine.states.each do |state|
|
|
57
60
|
scope_name = @engine.scope_name_for_state(state)
|
|
58
|
-
|
|
61
|
+
_detect_class_method_conflict! scope_name
|
|
59
62
|
@klass.scope(scope_name, -> { where(attr_name => state) })
|
|
60
63
|
end # each state
|
|
61
64
|
end # _add__klass__state_scopes
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
|
|
68
|
+
##
|
|
65
69
|
# Add a klass method that scopes records to those without the specified state.
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# Klass.not_active #=> ActiveRecord::Relation
|
|
73
|
+
# Klass.not_active_status #=> ActiveRecord::Relation
|
|
69
74
|
#
|
|
70
75
|
def _add__klass__not_state_scopes
|
|
71
76
|
attr_name = @attribute
|
|
72
77
|
|
|
73
78
|
@engine.states.each do |state|
|
|
74
79
|
scope_name = @engine.scope_name_for_state(state)
|
|
75
|
-
|
|
80
|
+
_detect_class_method_conflict! "not_#{scope_name}"
|
|
76
81
|
@klass.scope "not_#{scope_name}", -> { where.not(attr_name => state) }
|
|
77
82
|
end # each state
|
|
78
83
|
end # _add__klass__not_state_scopes
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
|
|
87
|
+
##
|
|
82
88
|
# Add a klass method that scopes records to the given states.
|
|
83
|
-
#
|
|
84
|
-
#
|
|
89
|
+
#
|
|
90
|
+
# @param [Symbol] method_name
|
|
91
|
+
# the method name for the new scope
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# Klass.with_statuses(:active, :pending) #=> ActiveRecord::Relation
|
|
85
95
|
#
|
|
86
96
|
def _add__klass__with_attrs_scope(method_name = @attribute)
|
|
87
97
|
attr_name = @attribute
|
|
88
98
|
method_name = "with_#{method_name.to_s.pluralize}"
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
_detect_class_method_conflict! method_name
|
|
91
101
|
@klass.scope method_name, ->(states) { where(attr_name => Array(states)) }
|
|
92
102
|
end # _add__klass__with_attrs_scope
|
|
93
103
|
|