state_gate 1.2.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 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