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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Dockerfile +22 -0
- data/Dockerfile_rails_7 +25 -0
- data/{Gemfile → Gemfile_rails_6} +1 -0
- data/{Gemfile.lock → Gemfile_rails_6.lock} +2 -2
- data/Gemfile_rails_7 +6 -0
- data/Gemfile_rails_7.lock +102 -0
- data/README.md +106 -25
- data/docker-compose.yml +24 -0
- data/img.png +0 -0
- data/img_1.png +0 -0
- data/img_2.png +0 -0
- data/img_3.png +0 -0
- data/lib/enum_ext/annotated.rb +172 -0
- data/lib/enum_ext/basic_helpers.rb +79 -0
- data/lib/enum_ext/enum_wrapper.rb +78 -0
- data/lib/enum_ext/humanize_helpers.rb +161 -0
- data/lib/enum_ext/superset_helpers.rb +77 -0
- data/lib/enum_ext/version.rb +1 -1
- data/lib/enum_ext.rb +43 -358
- metadata +19 -5
@@ -0,0 +1,79 @@
|
|
1
|
+
module EnumExt::BasicHelpers
|
2
|
+
|
3
|
+
# Defines instance method a shortcut for getting integer value of an enum.
|
4
|
+
# for enum named 'status' will generate:
|
5
|
+
#
|
6
|
+
# instance.status_i
|
7
|
+
private
|
8
|
+
def enum_i( enum_name )
|
9
|
+
define_method "#{enum_name}_i" do
|
10
|
+
self.class.send("#{enum_name.to_s.pluralize}")[send(enum_name)].to_i
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Defines two scopes for one for an inclusion: `WHERE enum IN( enum1, enum2 )`,
|
15
|
+
# and the second for an exclusion: `WHERE enum NOT IN( enum1, enum2 )`
|
16
|
+
#
|
17
|
+
# Ex:
|
18
|
+
# Request.with_statuses( :payed, :delivery_set ) # >> :payed and [:ready_for_shipment, :on_delivery, :delivered] requests
|
19
|
+
# Request.without_statuses( :payed ) # >> scope for all requests with statuses not eq to :payed
|
20
|
+
# Request.without_statuses( :payed, :in_warehouse ) # >> scope all requests with statuses not eq to :payed or :ready_for_shipment
|
21
|
+
def multi_enum_scopes(enum_name)
|
22
|
+
enum_plural = enum_name.to_s.pluralize
|
23
|
+
enum_obj = send(enum_plural)
|
24
|
+
|
25
|
+
self.instance_eval do
|
26
|
+
# EnumExt.define_superset_to_enum_method(self, enum_plural)
|
27
|
+
# EnumExt.define_summary_methods(self, enum_plural)
|
28
|
+
|
29
|
+
# with_enums scope
|
30
|
+
scope "with_#{enum_plural}", -> (*enum_list) {
|
31
|
+
enum_list.blank? ? nil : where( enum_name => enum_obj.superset_to_enum(*enum_list) )
|
32
|
+
} if !respond_to?("with_#{enum_plural}") && respond_to?(:scope)
|
33
|
+
|
34
|
+
# without_enums scope
|
35
|
+
scope "without_#{enum_plural}", -> (*enum_list) {
|
36
|
+
enum_list.blank? ? nil : where.not( enum_name => enum_obj.superset_to_enum(*enum_list) )
|
37
|
+
} if !respond_to?("without_#{enum_plural}") && respond_to?(:scope)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ex mass_assign_enum
|
42
|
+
#
|
43
|
+
# Used for mass assigning for collection without callbacks it creates bang methods for collections using update_all.
|
44
|
+
# it's often case when you need bulk update without callbacks, so it's gets frustrating to repeat:
|
45
|
+
# some_scope.update_all(status: Request.statuses[:new_status], update_at: Time.now)
|
46
|
+
#
|
47
|
+
# If you need callbacks you can do like this: some_scope.each(&:new_stat!) but if you don't need callbacks
|
48
|
+
# and you have lots of records to change at once you need update_all
|
49
|
+
#
|
50
|
+
# mass_assign_enum( :status )
|
51
|
+
#
|
52
|
+
# class methods:
|
53
|
+
# in_cart! paid! in_warehouse! and so
|
54
|
+
#
|
55
|
+
# Console:
|
56
|
+
# request1.in_cart!
|
57
|
+
# request2.waiting_for_payment!
|
58
|
+
# Request.with_statuses( :in_cart, :waiting_for_payment ).payed!
|
59
|
+
# request1.paid? # >> true
|
60
|
+
# request2.paid? # >> true
|
61
|
+
# request1.updated_at # >> Time.now
|
62
|
+
#
|
63
|
+
# order.requests.paid.all?(&:paid?) # >> true
|
64
|
+
# order.requests.paid.delivered!
|
65
|
+
# order.requests.map(&:status).uniq #>> [:delivered]
|
66
|
+
|
67
|
+
def mass_assign_enum( *enums_names )
|
68
|
+
enums_names.each do |enum_name|
|
69
|
+
enum_vals = self.send( enum_name.to_s.pluralize )
|
70
|
+
|
71
|
+
enum_vals.keys.each do |enum_el|
|
72
|
+
define_singleton_method( "#{enum_el}!" ) do
|
73
|
+
self.update_all( {enum_name => enum_vals[enum_el]}.merge( self.column_names.include?('updated_at') ? {updated_at: Time.now} : {} ))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
alias_method :enum_mass_assign, :mass_assign_enum
|
79
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# This is an wrapper class for a basic enum.
|
2
|
+
# Since enum values will be freezed right after the definition, we can't enrich enum directly functionality
|
3
|
+
# We can only wrap it with our own object and delegate enum base base functionality internally
|
4
|
+
class EnumExt::EnumWrapper
|
5
|
+
include EnumExt::Annotated
|
6
|
+
|
7
|
+
# supersets is storing exact definitions, if you need a raw mapping use class.statuses.superset_statuses
|
8
|
+
attr_reader :enum_values, :supersets, :supersets_raw, :t_options_raw, :localizations, :base_class, :enum_name
|
9
|
+
|
10
|
+
delegate_missing_to :enum_values
|
11
|
+
delegate :inspect, to: :enum_values
|
12
|
+
|
13
|
+
def initialize(enum_values, base_class, enum_name)
|
14
|
+
@enum_values = enum_values
|
15
|
+
@supersets = ActiveSupport::HashWithIndifferentAccess.new
|
16
|
+
@supersets_raw = ActiveSupport::HashWithIndifferentAccess.new
|
17
|
+
|
18
|
+
@t_options_raw = ActiveSupport::HashWithIndifferentAccess.new
|
19
|
+
@localizations = ActiveSupport::HashWithIndifferentAccess.new
|
20
|
+
|
21
|
+
@base_class = base_class
|
22
|
+
@enum_name = enum_name
|
23
|
+
end
|
24
|
+
|
25
|
+
# ext_sets_to_kinds( :ready_for_shipment, :delivery_set ) -->
|
26
|
+
# [:ready_for_shipment, :on_delivery, :delivered]
|
27
|
+
def superset_to_enum( *enum_or_sets )
|
28
|
+
return [] if enum_or_sets.blank?
|
29
|
+
enum_or_sets_strs = enum_or_sets.map(&:to_s)
|
30
|
+
|
31
|
+
next_level_deeper = supersets.slice( *enum_or_sets_strs ).values.flatten
|
32
|
+
(enum_or_sets_strs & enum_values.keys | send(:superset_to_enum, *next_level_deeper)).uniq
|
33
|
+
end
|
34
|
+
|
35
|
+
def all
|
36
|
+
{
|
37
|
+
**enum_values,
|
38
|
+
supersets: {
|
39
|
+
**send(:supersets_raw)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def t_options_i
|
45
|
+
evaluate_localizations_to_i(localizations)
|
46
|
+
end
|
47
|
+
|
48
|
+
def t_options
|
49
|
+
evaluate_localizations(localizations)
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :t, :localizations
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def evaluate_localizations(t_enum_set)
|
57
|
+
# { kind => kind_translator, kind2 => kind2_translator } --> [[kind_translator, kind], [kind2_translator, kind2]]
|
58
|
+
t_enum_set.invert.to_a.map do | translator, enum_key |
|
59
|
+
# since all procs in t_enum are evaluated in context of a record than it's not always possible to create select options automatically
|
60
|
+
translation = if translator.respond_to?(:call)
|
61
|
+
if translator.arity < 1
|
62
|
+
translator.call rescue "Cannot create option for #{enum_key} ( proc fails to evaluate )"
|
63
|
+
else
|
64
|
+
"Cannot create option for #{enum_key} because of a lambda"
|
65
|
+
end
|
66
|
+
end || translator
|
67
|
+
[translation, enum_key]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def evaluate_localizations_to_i(t_enum_set)
|
72
|
+
# { kind => kind_translation, kind2 => kind2_translation } --> [[kind_translation, kind_i], [kind2_translation, kind2_i]]
|
73
|
+
evaluate_localizations(t_enum_set).map do | translation, name |
|
74
|
+
[ translation, self[name] ]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module EnumExt::HumanizeHelpers
|
2
|
+
|
3
|
+
# if app doesn't need internationalization, it may use humanize_enum to make enum user friendly
|
4
|
+
#
|
5
|
+
# class Request
|
6
|
+
# humanize_enum :status, {
|
7
|
+
# #locale dependent example with pluralization and lambda:
|
8
|
+
# payed: -> (t_self) { I18n.t("request.status.payed", count: t_self.sum ) }
|
9
|
+
#
|
10
|
+
# #locale dependent example with pluralization and proc:
|
11
|
+
# payed: Proc.new{ I18n.t("request.status.payed", count: self.sum ) }
|
12
|
+
#
|
13
|
+
# #locale independent:
|
14
|
+
# ready_for_shipment: "Ready to go!"
|
15
|
+
# }
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Could be called multiple times, all humanization definitions will be merged under the hood:
|
19
|
+
# humanize_enum :status, {
|
20
|
+
# payed: I18n.t("scope.#{status}")
|
21
|
+
# }
|
22
|
+
# humanize_enum :status, {
|
23
|
+
# billed: I18n.t("scope.#{status}")
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# Example with block:
|
28
|
+
#
|
29
|
+
# humanize_enum :status do
|
30
|
+
# I18n.t("scope.#{status}")
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# in views select:
|
34
|
+
# f.select :status, Request.t_statuses_options
|
35
|
+
#
|
36
|
+
# in select in Active Admin filter
|
37
|
+
# collection: Request.t_statuses_options_i
|
38
|
+
#
|
39
|
+
# Rem: select options breaks when using lambda() with params
|
40
|
+
#
|
41
|
+
# Console:
|
42
|
+
# request.sum = 3
|
43
|
+
# request.payed!
|
44
|
+
# request.status # >> payed
|
45
|
+
# request.t_status # >> "Payed 3 dollars"
|
46
|
+
# Request.t_statuses # >> { in_cart: -> { I18n.t("request.status.in_cart") }, .... }
|
47
|
+
def humanize_enum( *args, &block )
|
48
|
+
enum_name = args.shift
|
49
|
+
localization_definitions = args.pop
|
50
|
+
enum_plural = enum_name.to_s.pluralize
|
51
|
+
enum_object = send( enum_plural )
|
52
|
+
|
53
|
+
self.instance_eval do
|
54
|
+
# instance.t_enum
|
55
|
+
define_method "t_#{enum_name}" do
|
56
|
+
t = block || enum_object.localizations[send(enum_name)]
|
57
|
+
if t.try(:lambda?)
|
58
|
+
t.try(:arity) == 1 && t.call( self ) || t.try(:call)
|
59
|
+
elsif t.is_a?(Proc)
|
60
|
+
instance_eval(&t)
|
61
|
+
else
|
62
|
+
t
|
63
|
+
end.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
# if localization is absent than block must be given
|
67
|
+
enum_object.localizations.merge!(
|
68
|
+
localization_definitions.try(:with_indifferent_access) ||
|
69
|
+
send(enum_plural).map do |k, _v|
|
70
|
+
# little bit hackerish: instantiate object just with enum setup and then call its t_.. method which
|
71
|
+
[k, Proc.new{ self.new({ enum_name => k }).send("t_#{enum_name}") }]
|
72
|
+
end.to_h.with_indifferent_access
|
73
|
+
)
|
74
|
+
|
75
|
+
# hm.. lost myself here, why did I implement this method
|
76
|
+
define_method "t_#{enum_name}=" do |new_val|
|
77
|
+
send("#{enum_name}=", new_val)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
alias localize_enum humanize_enum
|
83
|
+
|
84
|
+
# Simple way to translate enum.
|
85
|
+
# It use either given scope as second argument, or generated activerecord.attributes.model_name_underscore.enum_name
|
86
|
+
# If block is given than no scopes are taken in consider
|
87
|
+
def translate_enum( *args, &block )
|
88
|
+
enum_name = args.shift
|
89
|
+
enum_plural = enum_name.to_s.pluralize
|
90
|
+
t_scope = args.pop || "activerecord.attributes.#{self.name.underscore}.#{enum_plural}"
|
91
|
+
|
92
|
+
if block_given?
|
93
|
+
humanize_enum( enum_name, &block )
|
94
|
+
else
|
95
|
+
humanize_enum( enum_name, send(enum_plural).keys.map{|en| [ en, Proc.new{ I18n.t("#{t_scope}.#{en}") }] }.to_h )
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# human_attribute_name is redefined for automation like this:
|
100
|
+
# p #{object.class.human_attribute_name( attr_name )}:
|
101
|
+
# p object.send(attr_name)
|
102
|
+
def human_attribute_name( name, options = {} )
|
103
|
+
# if name starts from t_ and there is a column with the last part then ...
|
104
|
+
name[0..1] == 't_' && column_names.include?(name[2..-1]) ? super( name[2..-1], options ) : super( name, options )
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# t_... methods for supersets will just slice
|
109
|
+
# original enum t_.. methods output and return only superset related values from it
|
110
|
+
#
|
111
|
+
def self.define_superset_humanization_helpers(base_class, superset_name, enum_name)
|
112
|
+
enum_plural = enum_name.to_s.pluralize
|
113
|
+
enum_object = base_class.send(enum_plural)
|
114
|
+
|
115
|
+
enum_object.define_singleton_method( "t_#{superset_name}_options" ) do
|
116
|
+
result = evaluate_localizations(send("t_#{superset_name}"))
|
117
|
+
return result unless result.blank?
|
118
|
+
|
119
|
+
[["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2]
|
120
|
+
end
|
121
|
+
|
122
|
+
# enums.t_options_i
|
123
|
+
enum_object.define_singleton_method( "t_#{superset_name}_options_i" ) do
|
124
|
+
result = evaluate_localizations_to_i( send("t_#{superset_name}") )
|
125
|
+
return result unless result.to_h.values.all?(&:blank?)
|
126
|
+
|
127
|
+
[["Enum translations are missing. Did you forget to translate #{enum_name}"]*2]
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# enums.t_superset ( translations or humanizations subset for a given set )
|
132
|
+
enum_object.define_singleton_method( "t_#{superset_name}" ) do
|
133
|
+
return [(["Enum translations are missing. Did you forget to translate #{enum_name}"]*2)].to_h if localizations.blank?
|
134
|
+
|
135
|
+
enum_object.localizations.slice( *enum_object.send(superset_name) )
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
# # t_... - are translation dependent methods
|
142
|
+
# # This one is a narrow case helpers just a quick subset of t_ enums options for a set
|
143
|
+
# # class.t_enums_options
|
144
|
+
# enum_obj.define_singleton_method( "t_#{superset_name}_options" ) do
|
145
|
+
# return [["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2] unless respond_to?( "t_#{enum_plural}_options_raw" )
|
146
|
+
#
|
147
|
+
# send("t_#{enum_plural}_options_raw", send("t_#{superset_name}_#{enum_plural}") )
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# # class.t_enums_options_i
|
151
|
+
# enum_obj.define_singleton_method( "t_#{superset_name}_options_i" ) do
|
152
|
+
# return [["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2] unless respond_to?( "t_#{enum_plural}_options_raw_i" )
|
153
|
+
#
|
154
|
+
# send("t_#{enum_plural}_options_raw_i", send("t_#{superset_name}_#{enum_plural}") )
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# enum_obj.define_singleton_method( "t_#{superset_name}_options" ) do
|
158
|
+
# return [["Enum translations call missed. Did you forget to call translate #{enum_name}"]*2] if t_options_raw.blank?
|
159
|
+
#
|
160
|
+
# t_options_raw["t_#{superset_name}"]
|
161
|
+
# end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module EnumExt::SupersetHelpers
|
2
|
+
# enum_supersets
|
3
|
+
# This method intend for creating and using some sets of enum values,
|
4
|
+
# you should
|
5
|
+
#
|
6
|
+
# it creates: scopes for subsets,
|
7
|
+
# instance method with ?
|
8
|
+
#
|
9
|
+
# For this call:
|
10
|
+
# enum status: [:in_cart, :waiting_for_payment, :paid, :packing, :ready_for_shipment, :on_delivery, :delivered],
|
11
|
+
# ext:[ , supersets: {
|
12
|
+
# delivery_set: [:ready_for_shipment, :on_delivery] # for shipping department for example
|
13
|
+
# in_warehouse: [:packing, :ready_for_shipment] # this scope is just for superposition example below
|
14
|
+
# sold: [:payd, :delivery_set, :in_warehouse, :delivered]
|
15
|
+
# } ]
|
16
|
+
#Rem:
|
17
|
+
# enum_supersets can be called twice defining a superposition of already defined supersets
|
18
|
+
# based on array operations, with already defined array methods ( considering previous example ):
|
19
|
+
# enum_supersets :status, {
|
20
|
+
# outside_warehouse: ( delivery_set_statuses - in_warehouse_statuses )... any other array operations like &, + and so can be used
|
21
|
+
# }
|
22
|
+
#
|
23
|
+
# so the enum_supersets will generate:
|
24
|
+
# instance:
|
25
|
+
# methods: delivery_set?, in_warehouse?
|
26
|
+
# class:
|
27
|
+
# named scopes: delivery_set, in_warehouse
|
28
|
+
# class helpers:
|
29
|
+
# - delivery_set_statuses (=[:ready_for_shipment, :on_delivery, :delivered] ), in_warehouse_statuses
|
30
|
+
# - delivery_set_statuses_i (= [3,4,5]), in_warehouse_statuses_i (=[3])
|
31
|
+
# class translation helpers ( started with t_... )
|
32
|
+
# for select inputs purposes:
|
33
|
+
# - t_delivery_set_statuses_options (= [['translation or humanization', :ready_for_shipment] ...])
|
34
|
+
# same as above but with integer as value ( for example to use in Active admin filters )
|
35
|
+
# - t_delivery_set_statuses_options_i (= [['translation or humanization', 3] ...])
|
36
|
+
|
37
|
+
# Console:
|
38
|
+
# request.on_delivery!
|
39
|
+
# request.delivery_set? # >> true
|
40
|
+
|
41
|
+
# Request.delivery_set.exists?(request) # >> true
|
42
|
+
# Request.in_warehouse.exists?(request) # >> false
|
43
|
+
#
|
44
|
+
# Request.statuses.supersets[:delivery_set] # >> [:ready_for_shipment, :on_delivery, :delivered]
|
45
|
+
private
|
46
|
+
def enum_supersets( enum_name, options = {} )
|
47
|
+
enum_plural = enum_name.to_s.pluralize
|
48
|
+
|
49
|
+
self.instance_eval do
|
50
|
+
enum_obj = send(enum_plural)
|
51
|
+
enum_obj.supersets.merge!( options.transform_values{ _1.try(:map, &:to_s) || _1.to_s } )
|
52
|
+
|
53
|
+
options.each do |superset_name, enum_vals|
|
54
|
+
raise "Can't define superset with name: #{superset_name}, #{enum_plural} already has such method!" if enum_obj.respond_to?(superset_name)
|
55
|
+
|
56
|
+
enum_obj.supersets_raw[superset_name] = enum_obj.superset_to_enum(*enum_vals)
|
57
|
+
|
58
|
+
# class.statuses.superset_statuses
|
59
|
+
enum_obj.define_singleton_method(superset_name) { enum_obj.superset_to_enum(*enum_vals) }
|
60
|
+
|
61
|
+
# superset_name scope
|
62
|
+
scope superset_name, -> { where( enum_name => enum_obj.send(superset_name) ) } if respond_to?(:scope)
|
63
|
+
|
64
|
+
# instance.superset_name?
|
65
|
+
define_method "#{superset_name}?" do
|
66
|
+
send(enum_name) && enum_obj.send(superset_name).include?( send(enum_name) )
|
67
|
+
end
|
68
|
+
|
69
|
+
EnumExt::HumanizeHelpers.define_superset_humanization_helpers( self, superset_name, enum_name )
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias_method :ext_enum_sets, :enum_supersets
|
75
|
+
end
|
76
|
+
|
77
|
+
|
data/lib/enum_ext/version.rb
CHANGED