state_gate 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1a3a28a8136472a91cd7c75616e189dd2c92a7b57a0bc18645395a46c55ff906
4
+ data.tar.gz: c909cd3d3a1101056ff1fe33ea6b94e38008f26a093476d7aaeb4ac17847b1ad
5
+ SHA512:
6
+ metadata.gz: 6b9cae6865505456fafa9da807e8472eb148f8af294c7631690b28ebba7b01c20e93f27b2bbe3ccf16c7ec27ea872e604726323bebb7639c7e7db726eb4bdd34
7
+ data.tar.gz: 7c104de800820cedee61c177a44504e2e1f052aaf529fd4990d3407ae337a4004c925f12162762f2db66b34a96ecda327e06e0c96c4dfc299feef63885b0e6f6
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Builder
5
+ ##
6
+ # = Description
7
+ #
8
+ # Multiple private methods providing error handling functionality for
9
+ # StateGate::Builder.
10
+ #
11
+ module ConflictDetectionMethods
12
+
13
+ # Private
14
+ # ======================================================================
15
+ private
16
+
17
+ # Check if a class method is already defined. Checks are made:
18
+ # 1 --> is it an ActiveRecord dangerous method?
19
+ # 2 --> is it an ActiveRecord defined class method?
20
+ # 3 --> is it a singleton method of the klass?
21
+ # 4 --> is it defined within any of the klass ancestors?
22
+ #
23
+ # If found, raise an error giving a guide to where the method has been defined
24
+ #
25
+ def detect_class_method_conflict!(method_name)
26
+ defining_klass = _active_record_protected_method?(method_name) ||
27
+ _klass_singleton_method?(method_name) ||
28
+ _klass_ancestor_singleton_method?(method_name)
29
+
30
+ return unless defining_klass
31
+
32
+ raise_conflict_error method_name, type: 'a class', source: defining_klass
33
+ end
34
+
35
+
36
+
37
+ # Check an instance method is already defined. Checks are made:
38
+ # 1 --> is it an ActiveRecord dangerous method?
39
+ # 2 --> is it an ActiveRecord defined instance method?
40
+ # 3 --> is it an instance method of the klass?
41
+ # 4 --> is it defined within any of the klass ancestors?
42
+ #
43
+ # If found, raise an error giving a guide to where the method has been defined
44
+ #
45
+ def detect_instance_method_conflict!(method_name)
46
+ defining_klass = _active_record_protected_method?(method_name) ||
47
+ _klass_instance_method?(method_name) ||
48
+ _klass_ancestor_instance_method?(method_name)
49
+
50
+ return unless defining_klass
51
+
52
+ raise_conflict_error method_name, source: defining_klass
53
+ end
54
+
55
+
56
+
57
+ # Raise a StateGate::ConflictError with a details message of the problem
58
+ #
59
+ # = Message
60
+ #
61
+ # StateGate for Klass#attribute will generate a class
62
+ # method 'statuses', which is already defined by ActiveRecord.
63
+ #
64
+ def raise_conflict_error(method_name, type: 'an instance', source: 'ActiveRecord')
65
+ fail StateGate::ConflictError, I18n.t('state_gate.builder.conflict_err',
66
+ klass: @klass,
67
+ attribute: @attribute,
68
+ type: type,
69
+ method_name: method_name,
70
+ source: source)
71
+ end
72
+
73
+
74
+
75
+ # Check if the method is an ActiveRecord dangerous method name
76
+ #
77
+ def _active_record_protected_method?(method_name) #:nodoc:
78
+ 'ActiveRecord' if _dangerous_method_names.include?(method_name)
79
+ end
80
+
81
+
82
+
83
+ # Check if the method is a singleton method of the klass
84
+ #
85
+ def _klass_singleton_method?(method_name) #:nodoc:
86
+ @klass.name if @klass.singleton_methods(false).include?(method_name.to_sym)
87
+ end
88
+
89
+
90
+
91
+ # Check if the method is an ancestral singleton method of the klass
92
+ #
93
+ def _klass_ancestor_singleton_method?(method_name) #:nodoc:
94
+ return nil unless @klass.respond_to?(method_name)
95
+
96
+ @klass.singleton_class
97
+ .ancestors
98
+ .select { |a| a.instance_methods(false).include?(method_name.to_sym) }
99
+ .first
100
+ end
101
+
102
+
103
+
104
+ # Check if the method an instance method of the klass
105
+ #
106
+ def _klass_instance_method?(method_name) #:nodoc:
107
+ @klass.instance_methods(false).include?(method_name.to_sym) ? @klass.name : nil
108
+ end
109
+
110
+
111
+
112
+ # Check if the method is an ancestral singleton method of the klass
113
+ #
114
+ def _klass_ancestor_instance_method?(method_name) #:nodoc:
115
+ return nil unless @klass.instance_methods.include?(method_name.to_sym)
116
+
117
+ @klass.ancestors
118
+ .select { |a| a.instance_methods(false).include?(method_name.to_sym) }
119
+ .first
120
+ end
121
+
122
+
123
+
124
+ # returns an array of dagerous methods names found in
125
+ # ActiveRecord::AttributeMethods::RESTRICTED_CLASS_METHODS (which is called
126
+ # BLACKLISTED_CLASS_METHODS in 5.0 and 5.1)
127
+ #
128
+ def _dangerous_method_names
129
+ %w[private public protected allocate new name parent superclass]
130
+ end
131
+
132
+ end # ConflictDetectionMethods
133
+ end # Builder
134
+ end # StateGate
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Builder
5
+ ##
6
+ # = Description
7
+ #
8
+ # Multiple private methods enabling StateGate::Builder to dynamically
9
+ # generate module, instance and class helper methods.
10
+ #
11
+ module DynamicModuleCreationMethods
12
+
13
+ # Private
14
+ # ======================================================================
15
+ private
16
+
17
+
18
+
19
+ # = Dynamic Module Creation
20
+ # ======================================================================
21
+
22
+ # Dynamically generated module to hold the StateGate helper methods. This
23
+ # keeps a clear distinction between the state machine helper methods and the klass'
24
+ # own methods.
25
+ #
26
+ # The module is named after the class and is created if needed, or reused if exisitng.
27
+ #
28
+ # Note:
29
+ # the module is named "<klass>::StateGate_HelperMethods"
30
+ #
31
+ def _helper_methods_module
32
+ @_helper_methods_module ||= begin
33
+ if @klass.const_defined?('StateGate_HelperMethods')
34
+ "#{@klass}::StateGate_HelperMethods".constantize
35
+ else
36
+ @klass.const_set('StateGate_HelperMethods', Module.new)
37
+ mod = "#{@klass}::StateGate_HelperMethods".constantize
38
+ @klass.include mod
39
+ mod
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ # Method Re-defined Detection
46
+ # ======================================================================
47
+
48
+
49
+ # Adds the hook method :method_added to the Klass, detecting any new method
50
+ # definitions for an attribute already defined as a StateGate.
51
+ #
52
+ # If a matching method is discoverd, it adds a warning to logger, if defined,
53
+ # otherwise it outputs the warning to STDOUT via `puts`
54
+ #
55
+ # method_name - the name of the newly defined method.
56
+ #
57
+ # Note
58
+ #
59
+ # This method is added last so it does not trigger when StateGate adds
60
+ # the attribute methods.
61
+ #
62
+ # meta
63
+ #
64
+ # * loop though each state machine attribute.
65
+ # * does the new defined method use 'attr' or 'attr='?
66
+ # * if so then record an error logger if denied, othewise use `puts`
67
+ #
68
+ def _generate_method_redefine_detection # rubocop:disable Metrics/MethodLength
69
+ @klass.instance_eval(%(
70
+ def method_added(method_name)
71
+ stateables.keys.each do |attr_name|
72
+ if method_name&.to_s == attr_name ||
73
+ method_name&.to_s == "\#{attr_name}="
74
+
75
+ msg = "WARNING! \#{self.name}#\#{attr_name} is a defined StateGate and"
76
+ msg += " redefining :\#{method_name} may cause conflict."
77
+
78
+ logger ? logger.warn(msg) : puts("\n\n\#{msg}\n\n")
79
+ end
80
+
81
+ super(method_name)
82
+ end
83
+ end
84
+ ), __FILE__, __LINE__ - 15)
85
+ end
86
+
87
+
88
+
89
+ # Method Creation
90
+ # ======================================================================
91
+
92
+ # Add an Class helper method to the _helper_methods_module
93
+ #
94
+ # method_name - a String name for the method, needed to check for conflicts
95
+ # file - a String file name for error reporting
96
+ # line - a String or Integer line number for error reporting
97
+ # method_body - a String to bhe evaluates in the module
98
+ #
99
+ def add__klass__helper_method(method_name, file, line, method_body)
100
+ detect_class_method_conflict!(method_name)
101
+ @klass.instance_eval(method_body, file, line)
102
+ end
103
+
104
+
105
+
106
+ # Add an instance helper method to the _helper_methods_module
107
+ #
108
+ # method_name - a String name for the method, needed to check for conflicts
109
+ # file - a String file name for error reporting
110
+ # line - a String or Integer line number for error reporting
111
+ # method_body - a String to bhe evaluates in the module
112
+ #
113
+ def add__instance__helper_method(method_name, file, line, method_body)
114
+ detect_instance_method_conflict!(method_name)
115
+ _helper_methods_module.module_eval(method_body, file, line)
116
+ end
117
+
118
+ end # DynamicModuleCreationMethods
119
+ end # Builder
120
+ end # StateGate
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Builder
5
+ ##
6
+ # = Description
7
+ #
8
+ # Multiple private methods enabling StateGate::Builder to generate
9
+ # scopes for each state.
10
+ #
11
+ # * fetch all records with the given state:
12
+ # Klass.active # => Klass.where(state: :active)
13
+ #
14
+ # * fetch all records without the given state:
15
+ # Klass.not_active # => Klass.where.not(state: :active)
16
+ #
17
+ # * fetch all records with the supplied states:
18
+ # Klass.with_statuses(:pending, :active) # => Klass.where(state: [:pending, :active])
19
+ #
20
+ module ScopeMethods
21
+
22
+ # = Private
23
+ # ======================================================================
24
+ private
25
+
26
+
27
+ # Add scopes to the klass for filtering by state
28
+ #
29
+ # Note:
30
+ # The scope name is a concatenation of <prefix><state name><suffix>
31
+ #
32
+ def generate_scope_methods
33
+ return unless @engine.include_scopes?
34
+
35
+ _add__klass__state_scopes
36
+ _add__klass__not_state_scopes
37
+ _add__klass__with_attrs_scope
38
+
39
+ _add__klass__with_attrs_scope(@alias) if @alias
40
+ end
41
+
42
+
43
+
44
+ # ======================================================================
45
+ # Klass methods
46
+ # ======================================================================
47
+
48
+ # Add a klass method that scopes records to the specified state.
49
+ # eg:
50
+ # Klass.active # => ActiveRecord::Relation
51
+ # Klass.active_status # => ActiveRecord::Relation
52
+ #
53
+ def _add__klass__state_scopes
54
+ attr_name = @attribute
55
+
56
+ @engine.states.each do |state|
57
+ scope_name = @engine.scope_name_for_state(state)
58
+ detect_class_method_conflict! scope_name
59
+ @klass.scope(scope_name, -> { where(attr_name => state) })
60
+ end # each state
61
+ end # _add__klass__state_scopes
62
+
63
+
64
+
65
+ # Add a klass method that scopes records to those without the specified state.
66
+ # eg:
67
+ # Klass.not_active # => ActiveRecord::Relation
68
+ # Klass.not_active_status # => ActiveRecord::Relation
69
+ #
70
+ def _add__klass__not_state_scopes
71
+ attr_name = @attribute
72
+
73
+ @engine.states.each do |state|
74
+ scope_name = @engine.scope_name_for_state(state)
75
+ detect_class_method_conflict! "not_#{scope_name}"
76
+ @klass.scope "not_#{scope_name}", -> { where.not(attr_name => state) }
77
+ end # each state
78
+ end # _add__klass__not_state_scopes
79
+
80
+
81
+
82
+ # Add a klass method that scopes records to the given states.
83
+ # eg:
84
+ # Klass.with_statuses(:active, :pending) # => ActiveRecord::Relation
85
+ #
86
+ def _add__klass__with_attrs_scope(method_name = @attribute)
87
+ attr_name = @attribute
88
+ method_name = "with_#{method_name.to_s.pluralize}"
89
+
90
+ detect_class_method_conflict! method_name
91
+ @klass.scope method_name, ->(states) { where(attr_name => Array(states)) }
92
+ end # _add__klass__with_attrs_scope
93
+
94
+ end # LockingMethods
95
+ end # Builder
96
+ end # StateGate
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Builder
5
+ ##
6
+ # = Description
7
+ #
8
+ # Multiple private methods enabling StateGate::Builder to generate
9
+ # state functionality.
10
+ #
11
+ # * query the class for all state:
12
+ # Klass.statuses # => [:pending, :active, :archived]
13
+ #
14
+ # * query the class for the human names of all state:
15
+ # Klass.human_statuses # => ['Pending Activation', 'Active', 'Archived']
16
+ #
17
+ # * query the class for an Array of human names/state names for use in a select form:
18
+ # Klass.statuses_for_select
19
+ # # => [['Pending Activation', 'pending'],["Active', 'active'], ['Archived','archived']]
20
+ #
21
+ # * list all attribute states:
22
+ # .status_states # => [:pending, :active, :archived]
23
+ #
24
+ # * list all human names for the attribute states:
25
+ # .status_human_names # => ['Pending Activation', 'Active', 'Archived']
26
+ #
27
+ # * list the human name for the attribute state:
28
+ # .human_status # => 'Pending Activation'
29
+ #
30
+ # * is a particular state set:
31
+ # .pending? # => false
32
+ # .active? # => true
33
+ # .archived? # => false
34
+ #
35
+ # * is a particular state not set:
36
+ # .not_pending? # => true
37
+ # .not_active? # => false
38
+ # .not_archived? # => true
39
+ #
40
+ # * list the allowed transitions for the current state.
41
+ # .status_transitions # => [:suspended, :archived]
42
+ #
43
+ module StateMethods
44
+
45
+ # Private
46
+ # ======================================================================
47
+ private
48
+
49
+ # Add Class and instance methods that allow querying states
50
+ #
51
+ def generate_state_methods
52
+ add_state_attribute_methods
53
+ add_state_alias_methods
54
+ end
55
+
56
+
57
+
58
+ # add attribute methods
59
+ #
60
+ def add_state_attribute_methods
61
+ _add__klass__attrs
62
+ _add__klass__human_attrs
63
+ _add__klass__attrs_for_select
64
+
65
+ _add__instance__attrs
66
+ _add__instance__human_attrs
67
+ _add__instance__human_attr
68
+ _add__instance__state?
69
+ _add__instance__not_state?
70
+ _add__instance__attrs_for_select
71
+ end
72
+
73
+
74
+
75
+ # add alias methods
76
+ #
77
+ def add_state_alias_methods
78
+ return unless @alias
79
+
80
+ _add__klass__attrs(@alias)
81
+ _add__klass__human_attrs(@alias)
82
+ _add__klass__attrs_for_select(@alias)
83
+
84
+ _add__instance__attrs(@alias)
85
+ _add__instance__human_attrs(@alias)
86
+ _add__instance__attrs_for_select(@alias)
87
+ end
88
+
89
+
90
+
91
+ # ======================================================================
92
+ # Class Merthods
93
+ # ======================================================================
94
+
95
+ # Adds a Class method to return an Array of the defined states for the attribute
96
+ # eg:
97
+ # Klass.statuses # => [:pending, :active, :suspended, :archived]
98
+ #
99
+ def _add__klass__attrs(method_name = @attribute)
100
+ method_name = method_name.to_s.pluralize
101
+
102
+ add__klass__helper_method(method_name, __FILE__, __LINE__ - 2, %(
103
+ def #{method_name}
104
+ stateables[:#{@attribute}].states
105
+ end
106
+ ))
107
+ end
108
+
109
+
110
+
111
+ # Adds a Class method to return an Array of the human names of the defined states
112
+ # for the attribute
113
+ # eg:
114
+ # Klass.human_statuses # => ['Pending Activation', 'Active',
115
+ # 'Suspended by Admin', 'Archived']
116
+ #
117
+ def _add__klass__human_attrs(method_name = @attribute)
118
+ method_name = "human_#{method_name.to_s.pluralize}"
119
+
120
+ add__klass__helper_method(method_name, __FILE__, __LINE__ - 2, %(
121
+ def #{method_name}
122
+ stateables[:#{@attribute}].human_states
123
+ end
124
+ ))
125
+ end
126
+
127
+
128
+
129
+ # Adds a Class method to return an Array of the human and state names for the
130
+ # attribute, suitable for using in a form select statement.
131
+ #
132
+ # sorted - if TRUE, the array is sorted in alphabetical order by human name
133
+ # otherwise it is in the order specified
134
+ #
135
+ # Klass.statuses_for_select # => [ ['Pending Activation', 'pending'],
136
+ # ['Active', 'active'],
137
+ # ['Suspended by Admin', 'suspended',
138
+ # ['Archived', 'archived'] ]
139
+ #
140
+ # Klass.statuses_for_select(true) # => [ ['Active', 'active'],
141
+ # ['Pending Activation', 'pending'],
142
+ # ['Suspended by Admin', 'suspended',
143
+ # ['Archived', 'archived'] ]
144
+ #
145
+ # Note:
146
+ # States should NEVER be set from direct user selection. This method is
147
+ # intended for use within search forms, where the user may filter by state.
148
+ #
149
+ def _add__klass__attrs_for_select(method_name = @attribute)
150
+ method_name = "#{method_name.to_s.pluralize}_for_select"
151
+
152
+ add__klass__helper_method(method_name, __FILE__, __LINE__ - 2, %(
153
+ def #{method_name}(sorted = false)
154
+ stateables[:#{@attribute}].states_for_select(sorted)
155
+ end
156
+ ))
157
+ end
158
+
159
+
160
+
161
+ # ======================================================================
162
+ # Instance Methods
163
+ # ======================================================================
164
+
165
+ # Adds an Instance method to return Array of the defined states for the attribute
166
+ # eg:
167
+ # .statuses # => [:pending, :active, :suspended, :archived]
168
+ #
169
+ def _add__instance__attrs(method_name = @attribute)
170
+ method_name = method_name.to_s.pluralize
171
+
172
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
173
+ def #{method_name}
174
+ stateables[:#{@attribute}].states
175
+ end
176
+ ))
177
+ end
178
+
179
+
180
+
181
+ # Adds an Instance method to return an Array of the human names for the attribute
182
+ # eg:
183
+ # .status_human_states # => ['Pending Activation', 'Active',
184
+ # 'Suspended by Admin', 'Archived']
185
+ #
186
+ def _add__instance__human_attrs(method_name = @attribute)
187
+ method_name = "human_#{method_name.to_s.pluralize}"
188
+
189
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
190
+ def #{method_name}
191
+ stateables[:#{@attribute}].human_states
192
+ end
193
+ ))
194
+ end
195
+
196
+
197
+
198
+ # Adds an Instance method to return the human name for the attribute's state
199
+ # eg:
200
+ # .human_status # => 'Suspended by Admin'
201
+ #
202
+ def _add__instance__human_attr(method_name = @attribute)
203
+ method_name = "human_#{method_name.to_s}"
204
+
205
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
206
+ def #{method_name}
207
+ stateables[:#{@attribute}].human_state_for(#{@attribute})
208
+ end
209
+ ))
210
+ end
211
+
212
+
213
+
214
+ # Adds an Instance method for each state, returning TRUE if the state is set
215
+ # eg:
216
+ # --> when :active
217
+ # .active? # => true
218
+ # .archived? # => false
219
+ #
220
+ def _add__instance__state?
221
+ @engine.states.each do |state|
222
+ method_name = "#{@engine.scope_name_for_state(state)}?"
223
+
224
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 3, %(
225
+ def #{method_name}
226
+ self[:#{@attribute}] == :#{state}.to_s
227
+ end
228
+ ))
229
+ end
230
+ end
231
+
232
+
233
+
234
+ # Adds an Instance method for each state, returning TRUE if the state is not set.
235
+ # eg:
236
+ # --> when :active
237
+ # .not_active? # => false
238
+ # .not_archived? # => true
239
+ #
240
+ # def _add__instance__not_state?
241
+ # attr_name = @attribute
242
+ #
243
+ def _add__instance__not_state?
244
+ @engine.states.each do |state|
245
+ method_name = "not_#{@engine.scope_name_for_state(state)}?"
246
+
247
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 3, %(
248
+ def #{method_name}
249
+ self[:#{@attribute}] != :#{state}.to_s
250
+ end
251
+ ))
252
+ end
253
+ end
254
+
255
+
256
+
257
+ # Adds a, Instance method to return an Array of the human and state names for the
258
+ # attribute, suitable for using in a form select statement.
259
+ #
260
+ # sorted - if TRUE, the array is sorted in alphabetical order by human name
261
+ # otherwise it is in the order specified
262
+ #
263
+ # .statuses_for_select # => [ ['Pending Activation', 'pending'],
264
+ # ['Active', 'active'],
265
+ # ['Suspended by Admin', 'suspended',
266
+ # ['Archived', 'archived'] ]
267
+ #
268
+ # .statuses_for_select(true) # => [ ['Active', 'active'],
269
+ # ['Pending Activation', 'pending'],
270
+ # ['Suspended by Admin', 'suspended',
271
+ # ['Archived', 'archived'] ]
272
+ #
273
+ # Note:
274
+ # States should NEVER be set from direct user selection. This method is
275
+ # intended for use within search forms, where the user may filter by state.
276
+ #
277
+ def _add__instance__attrs_for_select(method_name = @attribute)
278
+ method_name = "#{method_name.to_s.pluralize}_for_select"
279
+
280
+ add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
281
+ def #{method_name}(sorted = false)
282
+ stateables[:#{@attribute}].states_for_select(sorted)
283
+ end
284
+ ))
285
+ end
286
+
287
+ end # StateMethods
288
+ end # Builder
289
+ end # StateGate