enum_ext 0.5.3 → 0.8.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.
data/lib/enum_ext.rb CHANGED
@@ -1,4 +1,9 @@
1
1
  require "enum_ext/version"
2
+ require "enum_ext/annotated"
3
+ require "enum_ext/enum_wrapper"
4
+ require "enum_ext/humanize_helpers"
5
+ require "enum_ext/basic_helpers"
6
+ require "enum_ext/superset_helpers"
2
7
 
3
8
  # Let's assume we have model Request with enum status, and we have model Order with requests like this:
4
9
  # class Request
@@ -19,380 +24,60 @@ puts <<~DEPRECATION
19
24
 
20
25
  Ex for enum named kinds it could look like this:
21
26
 
22
- Class.ext_sets_to_kinds --> Class.kinds.set_to_basic
23
- Class.ext_kinds --> Class.kinds.sets
27
+ Class.ext_sets_to_kinds --> Class.kinds.superset_to_basic
28
+ Class.ext_kinds --> Class.kinds.supersets
24
29
  Class.all_kinds_defs --> Class.kinds.all
30
+
31
+ Class.t_kinds --> Class.kinds.t
25
32
  Class.t_kinds_options --> Class.kinds.t_options
26
33
  Class.t_named_set_kinds_options --> Class.kinds.t_named_set_options
27
-
28
- Enum extensions preferable way will be using param to original enum call or single exetension method:
29
- Ex:
30
- #Instead of three method calls:
31
- enum kind: {}
32
- enum_i :kind
33
- enum_mass_assign :kind
34
34
 
35
- #You should go with ext option instead:
36
- enum kinds: {}, ext: [:enum_i, :enum_mass_assign]
37
-
38
- # OR in case of standalone enum definition:
39
- enum kinds: {}
40
- enum_ext [:enum_i, :enum_mass_assign, supersets: {} ]
41
35
  DEPRECATION
42
36
 
43
37
  module EnumExt
44
-
45
- class << self
46
- def define_set_to_enum_method( extended_class, enum_plural)
47
- # ext_sets_to_kinds( :ready_for_shipment, :delivery_set ) --> [:ready_for_shipment, :on_delivery, :delivered]
48
- extended_class.define_singleton_method("ext_sets_to_#{enum_plural}") do |*enum_or_sets|
49
- return [] if enum_or_sets.blank?
50
- enum_or_sets_strs = enum_or_sets.map(&:to_s)
51
-
52
- next_level_deeper = try("ext_#{enum_plural}").slice( *enum_or_sets_strs ).values.flatten
53
- (enum_or_sets_strs & send(enum_plural).keys | send("ext_sets_to_#{enum_plural}", *next_level_deeper)).uniq
54
- end
55
- end
56
-
57
- def define_summary_methods(extended_class, enum_plural)
58
- extended_class.define_singleton_method("ext_#{enum_plural}") do
59
- @enum_ext_summary ||= ActiveSupport::HashWithIndifferentAccess.new
60
- end unless respond_to?("ext_#{enum_plural}")
61
-
62
- extended_class.define_singleton_method("all_#{enum_plural}") do
63
- {
64
- **send(enum_plural),
65
- "ext_#{enum_plural}": {
66
- **send("ext_#{enum_plural}")
67
- }
68
- } unless respond_to?("all_#{enum_plural}")
69
- end
70
- end
71
- end
38
+ include HumanizeHelpers # translate and humanize
39
+ include SupersetHelpers # enum_supersets
40
+ include BasicHelpers # enum_i, mass_assign, multi_enum_scopes
72
41
 
73
42
  # extending enum with inplace settings
74
- # enum status: {}, ext: [:enum_i, :mass_assign_enum, :enum_multi_scopes]
75
- # enum_i and mass_assign_enum ara
76
- def enum(definitions)
77
- extensions = definitions.delete(:ext)
78
-
79
- super(definitions).tap do
80
- definitions.each do |name,|
81
- [*extensions].each{|ext_method| send(ext_method, name) }
82
- end
83
- end
84
- end
85
-
86
- # Defines instance method a shortcut for getting integer value of an enum.
87
- # for enum named 'status' will generate:
88
- #
89
- # instance.status_i
90
- def enum_i( enum_name )
91
- define_method "#{enum_name}_i" do
92
- self.class.send("#{enum_name.to_s.pluralize}")[send(enum_name)].to_i
93
- end
94
- end
95
-
96
- # Defines two scopes for one for an inclusion: `WHERE enum IN( enum1, enum2 )`,
97
- # and the second for an exclusion: `WHERE enum NOT IN( enum1, enum2 )`
98
- #
99
- # Ex:
100
- # Request.with_statuses( :payed, :delivery_set ) # >> :payed and [:ready_for_shipment, :on_delivery, :delivered] requests
101
- # Request.without_statuses( :payed ) # >> scope for all requests with statuses not eq to :payed
102
- # Request.without_statuses( :payed, :in_warehouse ) # >> scope all requests with statuses not eq to :payed or :ready_for_shipment
103
- def multi_enum_scopes(enum_name)
104
- enum_plural = enum_name.to_s.pluralize
105
-
106
- self.instance_eval do
107
- # with_enums scope
108
- scope "with_#{enum_plural}", -> (*enum_list) {
109
- enum_list.blank? ? nil : where( enum_name => send("ext_sets_to_#{enum_plural}", *enum_list) )
110
- } if !respond_to?("with_#{enum_plural}") && respond_to?(:scope)
111
-
112
- # without_enums scope
113
- scope "without_#{enum_plural}", -> (*enum_list) {
114
- enum_list.blank? ? nil : where.not( enum_name => send("ext_sets_to_#{enum_plural}", *enum_list) )
115
- } if !respond_to?("without_#{enum_plural}") && respond_to?(:scope)
116
-
117
- EnumExt.define_set_to_enum_method(self, enum_plural)
118
- EnumExt.define_summary_methods(self, enum_plural)
119
- end
120
- end
121
-
122
- # ext_enum_sets
123
- # This method intend for creating and using some sets of enum values
124
- #
125
- # it creates: scopes for subsets,
126
- # instance method with ?,
127
- # and some class methods helpers
128
- #
129
- # For this call:
130
- # ext_enum_sets :status, {
131
- # delivery_set: [:ready_for_shipment, :on_delivery, :delivered] # for shipping department for example
132
- # in_warehouse: [:ready_for_shipment] # this scope is just for superposition example below
133
- # }
134
- #
135
- # it will generate:
136
- # instance:
137
- # methods: delivery_set?, in_warehouse?
138
- # class:
139
- # named scopes: delivery_set, in_warehouse
140
- # parametrized scopes: with_statuses, without_statuses
141
- # class helpers:
142
- # - delivery_set_statuses (=[:ready_for_shipment, :on_delivery, :delivered] ), in_warehouse_statuses
143
- # - delivery_set_statuses_i (= [3,4,5]), in_warehouse_statuses_i (=[3])
144
- # class translation helpers ( started with t_... )
145
- # for select inputs purposes:
146
- # - t_delivery_set_statuses_options (= [['translation or humanization', :ready_for_shipment] ...])
147
- # same as above but with integer as value ( for example to use in Active admin filters )
148
- # - t_delivery_set_statuses_options_i (= [['translation or humanization', 3] ...])
149
-
150
- # Console:
151
- # request.on_delivery!
152
- # request.delivery_set? # >> true
153
-
154
- # Request.delivery_set.exists?(request) # >> true
155
- # Request.in_warehouse.exists?(request) # >> false
156
- #
157
- # Request.delivery_set_statuses # >> [:ready_for_shipment, :on_delivery, :delivered]
158
-
159
- #Rem:
160
- # ext_enum_sets can be called twice defining a superposition of already defined sets ( considering previous example ):
161
- # ext_enum_sets :status, {
162
- # outside_warehouse: ( delivery_set_statuses - in_warehouse_statuses )... any other array operations like &, + and so can be used
163
- # }
164
- def ext_enum_sets( enum_name, options = {} )
165
- enum_plural = enum_name.to_s.pluralize
166
-
167
- self.instance_eval do
168
- EnumExt.define_set_to_enum_method(self, enum_plural)
169
- EnumExt.define_summary_methods(self, enum_plural)
170
-
171
- puts(<<~DEPRECATION) unless respond_to?("with_#{enum_plural}")
172
- ----------------DEPRECATION WARNING----------------
173
- - with/without_#{enum_plural} are served now via multi_enum_scopes method,
174
- and will be removed from the ext_enum_sets in the next version!
175
- DEPRECATION
176
- multi_enum_scopes(enum_name)
177
-
178
- send("ext_#{enum_plural}").merge!( options.transform_values{ _1.map(&:to_s) } )
179
-
180
- options.each do |set_name, enum_vals|
181
- # set_name scope
182
- scope set_name, -> { where( enum_name => send("#{set_name}_#{enum_plural}") ) } if respond_to?(:scope)
183
-
184
- # class.enum_set_values
185
- define_singleton_method( "#{set_name}_#{enum_plural}" ) do
186
- send("ext_sets_to_#{enum_plural}", *enum_vals)
187
- end
188
-
189
- # instance.set_name?
190
- define_method "#{set_name}?" do
191
- send(enum_name) && self.class.send( "#{set_name}_#{enum_plural}" ).include?( send(enum_name) )
192
- end
193
-
194
- # t_... - are translation dependent methods
195
- # This one is a narrow case helpers just a quick subset of t_ enums options for a set
196
- # class.t_enums_options
197
- define_singleton_method( "t_#{set_name}_#{enum_plural}_options" ) do
198
- return [["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2] unless respond_to?( "t_#{enum_plural}_options_raw" )
199
-
200
- send("t_#{enum_plural}_options_raw", send("t_#{set_name}_#{enum_plural}") )
201
- end
202
-
203
- # class.t_enums_options_i
204
- define_singleton_method( "t_#{set_name}_#{enum_plural}_options_i" ) do
205
- return [["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2] unless respond_to?( "t_#{enum_plural}_options_raw_i" )
206
-
207
- send("t_#{enum_plural}_options_raw_i", send("t_#{set_name}_#{enum_plural}") )
208
- end
209
-
210
- # protected?
211
- # class.t_set_name_enums ( translations or humanizations subset for a given set )
212
- define_singleton_method( "t_#{set_name}_#{enum_plural}" ) do
213
- return [(["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2)].to_h unless respond_to?( "t_#{enum_plural}" )
214
-
215
- send( "t_#{enum_plural}" ).slice( *send("#{set_name}_#{enum_plural}") )
216
- end
217
- end
218
- end
219
- end
220
-
221
- # Ex mass_assign_enum
222
- #
223
- # Used for mass assigning for collection without callbacks it creates bang methods for collections using update_all.
224
- # it's often case when you need bulk update without callbacks, so it's gets frustrating to repeat:
225
- # some_scope.update_all(status: Request.statuses[:new_status], update_at: Time.now)
226
- #
227
- # If you need callbacks you can do like this: some_scope.each(&:new_stat!) but if you don't need callbacks
228
- # and you have lots of records to change at once you need update_all
229
- #
230
- # mass_assign_enum( :status )
231
- #
232
- # class methods:
233
- # in_cart! paid! in_warehouse! and so
234
- #
235
- # Console:
236
- # request1.in_cart!
237
- # request2.waiting_for_payment!
238
- # Request.with_statuses( :in_cart, :waiting_for_payment ).payed!
239
- # request1.paid? # >> true
240
- # request2.paid? # >> true
241
- # request1.updated_at # >> Time.now
242
- #
243
- # order.requests.paid.all?(&:paid?) # >> true
244
- # order.requests.paid.delivered!
245
- # order.requests.map(&:status).uniq #>> [:delivered]
246
-
247
- def mass_assign_enum( *enums_names )
248
- enums_names.each do |enum_name|
249
- enum_vals = self.send( enum_name.to_s.pluralize )
250
-
251
- enum_vals.keys.each do |enum_el|
252
- define_singleton_method( "#{enum_el}!" ) do
253
- self.update_all( {enum_name => enum_vals[enum_el]}.merge( self.column_names.include?('updated_at') ? {updated_at: Time.now} : {} ))
254
- end
43
+ # enum status: {}, ext: [:enum_i, :mass_assign_enum, :enum_multi_scopes, enum_supersets: { }]
44
+ # and wrapping and replacing original enum with a wrapper object
45
+ #
46
+ # I'm using signature of a ActiveRecord 7 here: enum(name = nil, values = nil, **options)
47
+ # in earlier versions of ActiveRecord signature looks different: enum(definitions),
48
+ # so calling super should be different based on ActiveRecord major version
49
+ def enum(name = nil, values = nil, **options)
50
+ single_enum_definition = name.present?
51
+ extensions = options.delete(:ext)
52
+
53
+ (ActiveRecord::VERSION::MAJOR >= 7 ? super : super(options)).tap do |multiple_enum_definitions|
54
+ if single_enum_definition
55
+ enum_ext(name, extensions)
56
+ else
57
+ multiple_enum_definitions.each { |enum_name,| enum_ext(enum_name, extensions) }
255
58
  end
256
59
  end
257
60
  end
258
- alias_method :enum_mass_assign, :mass_assign_enum
259
-
260
- # if app doesn't need internationalization, it may use humanize_enum to make enum user friendly
261
- #
262
- # class Request
263
- # humanize_enum :status, {
264
- # #locale dependent example with pluralization and lambda:
265
- # payed: -> (t_self) { I18n.t("request.status.payed", count: t_self.sum ) }
266
- #
267
- # #locale dependent example with pluralization and proc:
268
- # payed: Proc.new{ I18n.t("request.status.payed", count: self.sum ) }
269
- #
270
- # #locale independent:
271
- # ready_for_shipment: "Ready to go!"
272
- # }
273
- # end
274
- #
275
- # Could be called multiple times, all humanization definitions will be merged under the hood:
276
- # humanize_enum :status, {
277
- # payed: I18n.t("scope.#{status}")
278
- # }
279
- # humanize_enum :status, {
280
- # billed: I18n.t("scope.#{status}")
281
- # }
282
- #
283
- #
284
- # Example with block:
285
- #
286
- # humanize_enum :status do
287
- # I18n.t("scope.#{status}")
288
- # end
289
- #
290
- # in views select:
291
- # f.select :status, Request.t_statuses_options
292
- #
293
- # in select in Active Admin filter
294
- # collection: Request.t_statuses_options_i
295
- #
296
- # Rem: select options breaks when using lambda() with params
297
- #
298
- # Console:
299
- # request.sum = 3
300
- # request.payed!
301
- # request.status # >> payed
302
- # request.t_status # >> "Payed 3 dollars"
303
- # Request.t_statuses # >> { in_cart: -> { I18n.t("request.status.in_cart") }, .... }
304
- def humanize_enum( *args, &block )
305
- enum_name = args.shift
306
- localizations = args.pop
307
- enum_plural = enum_name.to_s.pluralize
308
-
309
- self.instance_eval do
310
-
311
- #t_enum
312
- define_method "t_#{enum_name}" do
313
- t = block || @@localizations.try(:with_indifferent_access)[send(enum_name)]
314
- if t.try(:lambda?)
315
- t.try(:arity) == 1 && t.call( self ) || t.try(:call)
316
- elsif t.is_a?(Proc)
317
- instance_eval(&t)
318
- else
319
- t
320
- end.to_s
321
- end
322
-
323
- @@localizations ||= {}.with_indifferent_access
324
- # if localization is abscent than block must be given
325
- @@localizations.merge!(
326
- localizations.try(:with_indifferent_access) ||
327
- localizations ||
328
- send(enum_plural).keys.map{|en| [en, Proc.new{ self.new({ enum_name => en }).send("t_#{enum_name}") }] }.to_h.with_indifferent_access
329
- )
330
- #t_enums
331
- define_singleton_method( "t_#{enum_plural}" ) do
332
- @@localizations
333
- end
334
-
335
- #t_enums_options
336
- define_singleton_method( "t_#{enum_plural}_options" ) do
337
- send("t_#{enum_plural}_options_raw", send("t_#{enum_plural}") )
338
- end
339
-
340
- #t_enums_options_i
341
- define_singleton_method( "t_#{enum_plural}_options_i" ) do
342
- send("t_#{enum_plural}_options_raw_i", send("t_#{enum_plural}") )
343
- end
344
-
345
- define_method "t_#{enum_name}=" do |new_val|
346
- send("#{enum_name}=", new_val)
347
- end
348
61
 
349
- #protected?
350
- define_singleton_method( "t_#{enum_plural}_options_raw_i" ) do |t_enum_set|
351
- send("t_#{enum_plural}_options_raw", t_enum_set ).map do | key_val |
352
- key_val[1] = send(enum_plural)[key_val[1]]
353
- key_val
354
- end
355
- end
356
-
357
- define_singleton_method( "t_#{enum_plural}_options_raw" ) do |t_enum_set|
358
- t_enum_set.invert.to_a.map do | key_val |
359
- # since all procs in t_enum are evaluated in context of a record than it's not always possible to create select options
360
- if key_val[0].respond_to?(:call)
361
- if key_val[0].try(:arity) < 1
362
- key_val[0] = key_val[0].try(:call) rescue "Cannot create option for #{key_val[1]} ( proc fails to evaluate )"
363
- else
364
- key_val[0] = "Cannot create option for #{key_val[1]} because of a lambda"
365
- end
366
- end
367
- key_val
368
- end
369
- end
370
- end
62
+ # its an extension helper, on the opposite to basic enum method could be called multiple times
63
+ def enum_ext(enum_name, extensions)
64
+ replace_enum_with_wrapper(enum_name)
65
+ # [:enum_i, :enum_multi_scopes, enum_supersets: { valid: [:fresh, :cool], invalid: [:stale] }]
66
+ # --> [:enum_i, :enum_multi_scopes, [:enum_supersets, { valid: [:fresh, :cool], invalid: [:stale] }]
67
+ [*extensions].map { _1.try(:to_a)&.flatten || _1 }
68
+ .each { |(ext_method, params)| send(*[ext_method, enum_name, params].compact) }
371
69
  end
372
- alias localize_enum humanize_enum
373
70
 
374
- # Simple way to translate enum.
375
- # It use either given scope as second argument, or generated activerecord.attributes.model_name_underscore.enum_name
376
- # If block is given than no scopes are taken in consider
377
- def translate_enum( *args, &block )
378
- enum_name = args.shift
379
- enum_plural = enum_name.to_s.pluralize
380
- t_scope = args.pop || "activerecord.attributes.#{self.name.underscore}.#{enum_plural}"
71
+ private
72
+ def replace_enum_with_wrapper(enum_name)
73
+ enum_name_plural = enum_name.to_s.pluralize
74
+ return if send(enum_name_plural).is_a?(EnumWrapper)
381
75
 
382
- if block_given?
383
- humanize_enum( enum_name, &block )
384
- else
385
- humanize_enum( enum_name, send(enum_plural).keys.map{|en| [ en, Proc.new{ I18n.t("#{t_scope}.#{en}") }] }.to_h )
386
- end
76
+ # enum will freeze values so there is no other way to move extended functionality,
77
+ # than to use wrapper and delegate everything to enum_values
78
+ enum_wrapper = EnumWrapper.new(send(enum_name_plural), self, enum_name)
79
+ # "self" here is a base enum class, so we are replacing original enum definition, with a wrapper
80
+ define_singleton_method(enum_name_plural) { enum_wrapper }
387
81
  end
388
82
 
389
- # human_attribute_name is redefined for automation like this:
390
- # p #{object.class.human_attribute_name( attr_name )}:
391
- # p object.send(attr_name)
392
- def human_attribute_name( name, options = {} )
393
- # if name starts from t_ and there is a column with the last part then ...
394
- name[0..1] == 't_' && column_names.include?(name[2..-1]) ? super( name[2..-1], options ) : super( name, options )
395
- end
396
-
397
-
398
83
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enum_ext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alekseyl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-04 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -161,15 +161,29 @@ files:
161
161
  - ".gitignore"
162
162
  - ".travis.yml"
163
163
  - CHANGELOG.md
164
- - Gemfile
165
- - Gemfile.lock
164
+ - Dockerfile
165
+ - Dockerfile_rails_7
166
+ - Gemfile_rails_6
167
+ - Gemfile_rails_6.lock
168
+ - Gemfile_rails_7
169
+ - Gemfile_rails_7.lock
166
170
  - LICENSE.txt
167
171
  - README.md
168
172
  - Rakefile
169
173
  - bin/console
170
174
  - bin/setup
175
+ - docker-compose.yml
171
176
  - enum_ext.gemspec
177
+ - img.png
178
+ - img_1.png
179
+ - img_2.png
180
+ - img_3.png
172
181
  - lib/enum_ext.rb
182
+ - lib/enum_ext/annotated.rb
183
+ - lib/enum_ext/basic_helpers.rb
184
+ - lib/enum_ext/enum_wrapper.rb
185
+ - lib/enum_ext/humanize_helpers.rb
186
+ - lib/enum_ext/superset_helpers.rb
173
187
  - lib/enum_ext/version.rb
174
188
  homepage: https://github.com/alekseyl/enum_ext
175
189
  licenses:
@@ -190,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
204
  - !ruby/object:Gem::Version
191
205
  version: '0'
192
206
  requirements: []
193
- rubygems_version: 3.1.2
207
+ rubygems_version: 3.2.15
194
208
  signing_key:
195
209
  specification_version: 4
196
210
  summary: 'Enum extension introduces: enum supersets, enum mass-assign, easy localization,