enum_ext 0.5.2 → 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
@@ -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,