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