power_enum 0.8.6 → 0.9.1

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.
@@ -1,68 +1,152 @@
1
1
  # Copyright (c) 2005 Trevor Squires
2
+ # Copyright (c) 2012 Arthur Shagall
2
3
  # Released under the MIT License. See the LICENSE file for more details.
3
4
 
4
- module ActiveRecord
5
+ module ActiveRecord # :nodoc:
6
+
7
+ # Implements a mechanism to synthesize enum classes for simple enums. This is for situations where you wish to avoid
8
+ # cluttering the models directory with your enums.
9
+ #
10
+ # Create a custom Rails initializer: Rails.root/config/initializers/virtual_enumerations.rb
11
+ #
12
+ # ActiveRecord::VirtualEnumerations.define do |config|
13
+ # config.define 'ClassName',
14
+ # :table_name => 'table',
15
+ # :extends => 'SuperclassName',
16
+ # :conditions => ['something = ?', "value"],
17
+ # :order => 'column ASC',
18
+ # :on_lookup_failure => :enforce_strict,
19
+ # :name_column => 'name_column',
20
+ # :alias_name => false {
21
+ # # class_evaled_functions
22
+ # }
23
+ # end
24
+ #
25
+ # Only the 'ClassName' argument is required. :table_name is used to define a custom table name while the :extends
26
+ # option is used to set a custom superclass. Class names can be either camel-cased like ClassName or with
27
+ # underscores, like class_name. Strings and symbols are both fine.
28
+ #
29
+ # If you need to fine-tune the definition of the enum class, you can optionally pass in a block, which will be
30
+ # evaluated in the context of the enum class.
31
+ #
32
+ # Example:
33
+ #
34
+ # config.define :color, :on_lookup_failure => :enforce_strict, do
35
+ # def to_argb(alpha)
36
+ # case self.to_sym
37
+ # when :white
38
+ # [alpha, 255, 255, 255]
39
+ # when :red
40
+ # [alpha, 255, 0, 0]
41
+ # when :blue
42
+ # [alpha, 0, 0, 255]
43
+ # when :yellow
44
+ # [alpha, 255, 255, 0]
45
+ # when :black
46
+ # [alpha, 0, 0, 0]
47
+ # end
48
+ # end
49
+ # end
50
+ #
51
+ # As a convenience, if multiple enums share the same configuration, you can pass all of them to config.define.
52
+ #
53
+ # Example:
54
+ #
55
+ # config.define :booking_status, :connector_type, :color, :order => :name
56
+ #
57
+ # STI is also supported:
58
+ #
59
+ # Example:
60
+ #
61
+ # config.define :base_enum, :name_column => ;foo
62
+ # config.define :booking_status, :connector_type, :color, :extends => :base_enum
5
63
  module VirtualEnumerations # :nodoc:
6
- class << self
7
- def define
8
- raise ArgumentError, "#{self.name}: must pass a block to define()" unless block_given?
9
- config = ActiveRecord::VirtualEnumerations::Config.new
10
- yield config
11
- @config = config # we only overwrite config if no exceptions were thrown
64
+
65
+ # Defines enumeration classes. Passes a config object to the given block
66
+ # which is used to define the virtual enumerations. Call config.define for
67
+ # each enum or enums with a given set of options.
68
+ def self.define
69
+ raise ArgumentError, "#{self.name}: must pass a block to define()" unless block_given?
70
+ config = ActiveRecord::VirtualEnumerations::Config.new
71
+ yield config
72
+ @config = config # we only overwrite config if no exceptions were thrown
73
+ end
74
+
75
+ # Creates a constant for a virtual enum if a config is defined for it.
76
+ def self.synthesize_if_defined(const)
77
+ if @config && options = @config[const]
78
+ class_declaration = "class #{const} < #{options[:extends]}; end"
79
+
80
+ eval( class_declaration, TOPLEVEL_BINDING, __FILE__, __LINE__ )
81
+
82
+ virtual_enum_class = const_get( const )
83
+
84
+ inject_class_options( virtual_enum_class, options )
85
+
86
+ virtual_enum_class
87
+ else
88
+ nil
12
89
  end
13
-
14
- def synthesize_if_defined(const)
15
- options = @config[const]
16
- return nil unless options
17
- class_def = <<-end_eval
18
- class #{const} < #{options[:extends]}
19
- acts_as_enumerated :conditions => #{options[:conditions].inspect},
20
- :order => #{options[:order].inspect},
21
- :on_lookup_failure => #{options[:on_lookup_failure].inspect}
22
- set_table_name(#{options[:table_name].inspect}) unless #{options[:table_name].nil?}
23
- end
24
- end_eval
25
- eval(class_def, TOPLEVEL_BINDING)
26
- rval = const_get(const)
27
- if options[:post_synth_block]
28
- rval.class_eval(&options[:post_synth_block])
90
+ end
91
+
92
+ def self.inject_class_options( virtual_enum_class, options ) # :nodoc:
93
+ # Declare it acts_as_enumerated
94
+ virtual_enum_class.class_eval do
95
+ acts_as_enumerated :conditions => options[:conditions],
96
+ :order => options[:order],
97
+ :on_lookup_failure => options[:on_lookup_failure],
98
+ :name_column => options[:name_column],
99
+ :alias_name => options[:table_name]
100
+ end
101
+
102
+ # If necessary, set the table name
103
+ unless (table_name = options[:table_name]).blank?
104
+ virtual_enum_class.class_eval do
105
+ self.table_name = table_name
29
106
  end
30
- return rval
31
- end
107
+ end
108
+
109
+ if block = options[:customizations_block]
110
+ virtual_enum_class.class_eval(&block)
111
+ end
32
112
  end
33
-
113
+ private_class_method :inject_class_options
114
+
115
+ # Config class for VirtualEnumerations
34
116
  class Config
35
- def initialize
117
+ def initialize # :nodoc:
36
118
  @enumeration_defs = {}
37
119
  end
38
-
39
- def define(arg, options = {}, &synth_block)
40
- (arg.is_a?(Array) ? arg : [arg]).each do |class_name|
41
- camel_name = class_name.to_s.camelize
42
- raise ArgumentError, "ActiveRecord::VirtualEnumerations.define - invalid class_name argument (#{class_name.inspect})" if camel_name.blank?
43
- raise ArgumentError, "ActiveRecord::VirtualEnumerations.define - class_name already defined (#{camel_name})" if @enumeration_defs[camel_name.to_sym]
44
- options.assert_valid_keys(:table_name, :extends, :conditions, :order, :on_lookup_failure)
120
+
121
+ # Creates definition(s) for one or more enums.
122
+ def define(*args, &block)
123
+ options = args.extract_options!
124
+ args.compact!
125
+ args.flatten!
126
+ args.each do |class_name|
127
+ camel_name = class_name.to_s.camelize
128
+ if camel_name.blank?
129
+ raise ArgumentError, "ActiveRecord::VirtualEnumerations.define - invalid class_name argument (#{class_name.inspect})"
130
+ end
131
+ if @enumeration_defs[camel_name.to_sym]
132
+ raise ArgumentError, "ActiveRecord::VirtualEnumerations.define - class_name already defined (#{camel_name})"
133
+ end
134
+ options.assert_valid_keys(:table_name, :extends, :conditions, :order, :on_lookup_failure, :name_column, :alias_name)
45
135
  enum_def = options.clone
46
- enum_def[:extends] ||= "ActiveRecord::Base"
47
- enum_def[:post_synth_block] = synth_block
136
+ enum_def[:extends] = if superclass = enum_def[:extends]
137
+ superclass.to_s.camelize
138
+ else
139
+ "ActiveRecord::Base"
140
+ end
141
+ enum_def[:customizations_block] = block
48
142
  @enumeration_defs[camel_name.to_sym] = enum_def
49
143
  end
50
144
  end
51
-
145
+
146
+ # Proxies lookups to @enumeration_defs
52
147
  def [](arg)
53
148
  @enumeration_defs[arg]
54
149
  end
55
150
  end #class Config
56
151
  end #module VirtualEnumerations
57
152
  end #module ActiveRecord
58
-
59
- class Module # :nodoc:
60
- alias_method :enumerations_original_const_missing, :const_missing
61
- def const_missing(const_id)
62
- # let rails have a go at loading it
63
- enumerations_original_const_missing(const_id)
64
- rescue NameError
65
- # now it's our turn
66
- ActiveRecord::VirtualEnumerations.synthesize_if_defined(const_id) or raise
67
- end
68
- end
@@ -0,0 +1,405 @@
1
+ # Copyright (c) 2005 Trevor Squires
2
+ # Copyright (c) 2012 Arthur Shagall
3
+ # Released under the MIT License. See the LICENSE file for more details.
4
+
5
+ module PowerEnum::Enumerated
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # Returns false for ActiveRecord models that do not act as enumerated.
11
+ def acts_as_enumerated?
12
+ false
13
+ end
14
+
15
+ # Declares the model as enumerated. See the README for detailed usage instructions.
16
+ #
17
+ # === Supported options
18
+ # [:conditions]
19
+ # SQL search conditions
20
+ # [:order]
21
+ # SQL load order clause
22
+ # [:on_lookup_failure]
23
+ # Specifies the name of a class method to invoke when the +[]+ method is unable to locate a BookingStatus
24
+ # record for arg. The default is the built-in :enforce_none which returns nil. There are also built-ins for
25
+ # :enforce_strict (raise and exception regardless of the type for arg), :enforce_strict_literals (raises an
26
+ # exception if the arg is a Fixnum or Symbol), :enforce_strict_ids (raises and exception if the arg is a
27
+ # Fixnum) and :enforce_strict_symbols (raises an exception if the arg is a Symbol). The purpose of the
28
+ # :on_lookup_failure option is that a) under some circumstances a lookup failure is a Bad Thing and action
29
+ # should be taken, therefore b) a fallback action should be easily configurable. You can also give it a
30
+ # lambda that takes in a single argument (The arg that was passed to +[]+).
31
+ # [:name_column]
32
+ # Override for the 'name' column. By default, assumed to be 'name'.
33
+ # [:alias_name]
34
+ # By default, if a name column is not 'name', will create an alias of 'name' to the name_column attribute. Set
35
+ # this to +false+ if you don't want this behavior.
36
+ #
37
+ # === Examples
38
+ #
39
+ # ====Example 1
40
+ # class BookingStatus < ActiveRecord::Base
41
+ # acts_as_enumerated
42
+ # end
43
+ #
44
+ # ====Example 2
45
+ # class BookingStatus < ActiveRecord::Base
46
+ # acts_as_enumerated :on_lookup_failure => :enforce_strict
47
+ # end
48
+ #
49
+ # ====Example 3
50
+ # class BookingStatus < ActiveRecord::Base
51
+ # acts_as_enumerated :conditions => [:exclude => false],
52
+ # :order => 'created_at DESC',
53
+ # :on_lookup_failure => :lookup_failed,
54
+ # :name_column => :status_code
55
+ #
56
+ # def self.lookup_failed(arg)
57
+ # logger.error("Invalid status code lookup #{arg.inspect}")
58
+ # nil
59
+ # end
60
+ # end
61
+ #
62
+ # ====Example 4
63
+ # class BookingStatus < ActiveRecord::Base
64
+ # acts_as_enumerated :conditions => [:exclude => false],
65
+ # :order => 'created_at DESC',
66
+ # :on_lookup_failure => lambda { |arg| raise CustomError, "BookingStatus lookup failed; #{arg}" },
67
+ # :name_column => :status_code
68
+ # end
69
+ def acts_as_enumerated(options = {})
70
+ valid_keys = [:conditions, :order, :on_lookup_failure, :name_column, :alias_name]
71
+ options.assert_valid_keys(*valid_keys)
72
+
73
+ valid_keys.each do |key|
74
+ class_attribute "acts_enumerated_#{key.to_s}"
75
+ if options.has_key?( key )
76
+ self.send "acts_enumerated_#{key.to_s}=", options[key]
77
+ end
78
+ end
79
+
80
+ name_column = if options.has_key?(:name_column) && !options[:name_column].blank? then
81
+ options[:name_column].to_s.to_sym
82
+ else
83
+ :name
84
+ end
85
+
86
+ alias_name = if options.has_key?(:alias_name) then
87
+ options[:alias_name]
88
+ else
89
+ true
90
+ end
91
+
92
+ class_attribute :acts_enumerated_name_column
93
+ self.acts_enumerated_name_column = name_column
94
+
95
+ unless self.is_a? PowerEnum::Enumerated::EnumClassMethods
96
+ extend PowerEnum::Enumerated::EnumClassMethods
97
+
98
+ class_eval do
99
+ include PowerEnum::Enumerated::EnumInstanceMethods
100
+
101
+ before_save :enumeration_model_update
102
+ before_destroy :enumeration_model_update
103
+ validates name_column, :presence => true, :uniqueness => true
104
+
105
+ define_method :__enum_name__ do
106
+ read_attribute( name_column ).to_s
107
+ end
108
+
109
+ if alias_name && name_column != :name
110
+ alias_method :name, :__enum_name__
111
+ end
112
+ end # class_eval
113
+ end
114
+ end
115
+ end
116
+
117
+ module EnumClassMethods
118
+ attr_accessor :enumeration_model_updates_permitted
119
+
120
+ # Returns true for ActiveRecord models that act as enumerated.
121
+ def acts_as_enumerated?
122
+ true
123
+ end
124
+
125
+ # Returns all the enum values. Caches results after the first time this method is run.
126
+ def all
127
+ return @all if @all
128
+ conditions = self.acts_enumerated_conditions
129
+ order = self.acts_enumerated_order
130
+ @all = where(conditions).order(order).collect{|val| val.freeze}.freeze
131
+ end
132
+
133
+ # Returns all the active enum values. See the 'active?' instance method.
134
+ def active
135
+ return @all_active if @all_active
136
+ @all_active = all.select{ |enum| enum.active? }.freeze
137
+ end
138
+
139
+ # Returns all the inactive enum values. See the 'inactive?' instance method.
140
+ def inactive
141
+ return @all_inactive if @all_inactive
142
+ @all_inactive = all.select{ |enum| !enum.active? }.freeze
143
+ end
144
+
145
+ # Returns the names of all the enum values as an array of symbols.
146
+ def names
147
+ all.map { |item| item.name_sym }
148
+ end
149
+
150
+ # Enum lookup by Symbol, String, or id. Returns <tt>arg<tt> if arg is
151
+ # an enum instance. Passing in a list of arguments returns a list of enums.
152
+ def [](*args)
153
+ case args.size
154
+ when 0
155
+ nil
156
+ when 1
157
+ arg = args.first
158
+ case arg
159
+ when Symbol
160
+ return_val = lookup_name(arg.id2name) and return return_val
161
+ when String
162
+ return_val = lookup_name(arg) and return return_val
163
+ when Fixnum
164
+ return_val = lookup_id(arg) and return return_val
165
+ when self
166
+ return arg
167
+ when nil
168
+ nil
169
+ else
170
+ raise TypeError, "#{self.name}[]: argument should be a String, Symbol or Fixnum but got a: #{arg.class.name}"
171
+ end
172
+
173
+ handle_lookup_failure(arg)
174
+ else
175
+ args.map{ |item| self[item] }.uniq
176
+ end
177
+ end
178
+
179
+ # Deals with a lookup failure for the given argument.
180
+ def handle_lookup_failure(arg)
181
+ if (lookup_failure_handler = self.acts_enumerated_on_lookup_failure)
182
+ case lookup_failure_handler
183
+ when Proc
184
+ lookup_failure_handler.call(arg)
185
+ else
186
+ self.send(lookup_failure_handler, arg)
187
+ end
188
+ else
189
+ self.send(:enforce_none, arg)
190
+ end
191
+ end
192
+ private :handle_lookup_failure
193
+
194
+ # Enum lookup by id
195
+ def lookup_id(arg)
196
+ all_by_id[arg]
197
+ end
198
+
199
+ # Enum lookup by String
200
+ def lookup_name(arg)
201
+ all_by_name[arg]
202
+ end
203
+
204
+ # Returns true if the enum lookup by the given Symbol, String or id would have returned a value, false otherwise.
205
+ def include?(arg)
206
+ case arg
207
+ when Symbol
208
+ !lookup_name(arg.id2name).nil?
209
+ when String
210
+ !lookup_name(arg).nil?
211
+ when Fixnum
212
+ !lookup_id(arg).nil?
213
+ when self
214
+ possible_match = lookup_id(arg.id)
215
+ !possible_match.nil? && possible_match == arg
216
+ else
217
+ false
218
+ end
219
+ end
220
+
221
+ # NOTE: purging the cache is sort of pointless because
222
+ # of the per-process rails model.
223
+ # By default this blows up noisily just in case you try to be more
224
+ # clever than rails allows.
225
+ # For those times (like in Migrations) when you really do want to
226
+ # alter the records you can silence the carping by setting
227
+ # enumeration_model_updates_permitted to true.
228
+ def purge_enumerations_cache
229
+ unless self.enumeration_model_updates_permitted
230
+ raise "#{self.name}: cache purging disabled for your protection"
231
+ end
232
+ @all = @all_by_name = @all_by_id = @all_active = nil
233
+ end
234
+
235
+ # The preferred method to update an enumerations model. The same
236
+ # warnings as 'purge_enumerations_cache' and
237
+ # 'enumerations_model_update_permitted' apply. Pass a block to this
238
+ # method (no args) where you perform your updates. Cache will be
239
+ # flushed automatically.
240
+ def update_enumerations_model
241
+ if block_given?
242
+ begin
243
+ self.enumeration_model_updates_permitted = true
244
+ yield
245
+ ensure
246
+ purge_enumerations_cache
247
+ self.enumeration_model_updates_permitted = false
248
+ end
249
+ end
250
+ end
251
+
252
+ # Returns the name of the column this enum uses as the basic underlying value.
253
+ def name_column
254
+ @name_column ||= self.acts_enumerated_name_column
255
+ end
256
+
257
+ # ---Private methods---
258
+
259
+ # Returns a hash of all enumeration members keyed by their ids.
260
+ def all_by_id
261
+ @all_by_id ||= all_by_attribute( :id )
262
+ end
263
+ private :all_by_id
264
+
265
+ # Returns a hash of all the enumeration members keyed by their names.
266
+ def all_by_name
267
+ begin
268
+ @all_by_name ||= all_by_attribute( :__enum_name__ )
269
+ rescue NoMethodError => err
270
+ if err.name == name_column
271
+ raise TypeError, "#{self.name}: you need to define a '#{name_column}' column in the table '#{table_name}'"
272
+ end
273
+ raise
274
+ end
275
+ end
276
+ private :all_by_name
277
+
278
+ def all_by_attribute(attr)
279
+ all.inject({}) { |memo, item|
280
+ memo[item.send(attr)] = item
281
+ memo
282
+ }.freeze
283
+ end
284
+ private :all_by_attribute
285
+
286
+ def enforce_none(arg)
287
+ nil
288
+ end
289
+ private :enforce_none
290
+
291
+ def enforce_strict(arg)
292
+ raise_record_not_found(arg)
293
+ end
294
+ private :enforce_strict
295
+
296
+ def enforce_strict_literals(arg)
297
+ raise_record_not_found(arg) if (Fixnum === arg) || (Symbol === arg)
298
+ nil
299
+ end
300
+ private :enforce_strict_literals
301
+
302
+ def enforce_strict_ids(arg)
303
+ raise_record_not_found(arg) if Fixnum === arg
304
+ nil
305
+ end
306
+ private :enforce_strict_ids
307
+
308
+ def enforce_strict_symbols(arg)
309
+ raise_record_not_found(arg) if Symbol === arg
310
+ nil
311
+ end
312
+ private :enforce_strict_symbols
313
+
314
+ def raise_record_not_found(arg)
315
+ raise ActiveRecord::RecordNotFound, "Couldn't find a #{self.name} identified by (#{arg.inspect})"
316
+ end
317
+ private :raise_record_not_found
318
+
319
+ end
320
+
321
+ module EnumInstanceMethods
322
+ # Behavior depends on the type of +arg+.
323
+ #
324
+ # * If +arg+ is +nil+, returns +false+.
325
+ # * If +arg+ is an instance of +Symbol+, +Fixnum+ or +String+, returns the result of +BookingStatus[:foo] == BookingStatus[arg]+.
326
+ # * If +arg+ is an +Array+, returns +true+ if any member of the array returns +true+ for +===(arg)+, +false+ otherwise.
327
+ # * In all other cases, delegates to +===(arg)+ of the superclass.
328
+ #
329
+ # Examples:
330
+ #
331
+ # BookingStatus[:foo] === :foo #Returns true
332
+ # BookingStatus[:foo] === 'foo' #Returns true
333
+ # BookingStatus[:foo] === :bar #Returns false
334
+ # BookingStatus[:foo] === [:foo, :bar, :baz] #Returns true
335
+ # BookingStatus[:foo] === nil #Returns false
336
+ #
337
+ # You should note that defining an +:on_lookup_failure+ method that raises an exception will cause +===+ to
338
+ # also raise an exception for any lookup failure of +BookingStatus[arg]+.
339
+ def ===(arg)
340
+ case arg
341
+ when nil
342
+ false
343
+ when Symbol, String, Fixnum
344
+ return self == self.class[arg]
345
+ when Array
346
+ return self.in?(*arg)
347
+ else
348
+ super
349
+ end
350
+ end
351
+
352
+ alias_method :like?, :===
353
+
354
+ # Returns true if any element in the list returns true for ===(arg), false otherwise.
355
+ def in?(*list)
356
+ for item in list
357
+ self === item and return true
358
+ end
359
+ false
360
+ end
361
+
362
+ # Returns the symbol representation of the name of the enum. BookingStatus[:foo].name_sym returns :foo.
363
+ def name_sym
364
+ self.__enum_name__.to_sym
365
+ end
366
+
367
+ alias_method :to_sym, :name_sym
368
+
369
+ # By default enumeration #to_s should return stringified name of the enum. BookingStatus[:foo].to_s returns "foo"
370
+ def to_s
371
+ self.__enum_name__
372
+ end
373
+
374
+ # Returns true if the instance is active, false otherwise. If it has an attribute 'active',
375
+ # returns the attribute cast to a boolean, otherwise returns true. This method is used by the 'active'
376
+ # class method to select active enums.
377
+ def active?
378
+ @_active_status ||= ( attributes.include?('active') ? !!self.active : true )
379
+ end
380
+
381
+ # Returns true if the instance is inactive, false otherwise. Default implementations returns !active?
382
+ # This method is used by the 'inactive' class method to select inactive enums.
383
+ def inactive?
384
+ !active?
385
+ end
386
+
387
+ # NOTE: updating the models that back an acts_as_enumerated is
388
+ # rather dangerous because of rails' per-process model.
389
+ # The cached values could get out of synch between processes
390
+ # and rather than completely disallow changes I make you jump
391
+ # through an extra hoop just in case you're defining your enumeration
392
+ # values in Migrations. I.e. set enumeration_model_updates_permitted = true
393
+ def enumeration_model_update
394
+ if self.class.enumeration_model_updates_permitted
395
+ self.class.purge_enumerations_cache
396
+ true
397
+ else
398
+ # Ugh. This just seems hack-ish. I wonder if there's a better way.
399
+ self.errors.add(self.class.name_column, "changes to acts_as_enumeration model instances are not permitted")
400
+ false
401
+ end
402
+ end
403
+ private :enumeration_model_update
404
+ end # module EnumInstanceMethods
405
+ end # module PowerEnum::Enumerated