power_enum 0.8.6 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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