enum_ext 0.5.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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,