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.
- 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