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.
@@ -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
+
@@ -1,3 +1,3 @@
1
1
  module EnumExt
2
- VERSION = "0.5.3"
2
+ VERSION = "0.8.0"
3
3
  end