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