require 'active_support/concern' require 'active_support/core_ext/hash/keys' module Symbolize module ActiveRecord extend ActiveSupport::Concern included do # Returns an array of all the attributes that have been specified for symbolization class_attribute :symbolized_attributes, :instance_reader => false self.symbolized_attributes = [] end # Symbolize ActiveRecord attributes. Add # symbolize :attr_name # to your model class, to make an attribute return symbols instead of # string values. Setting such an attribute will accept symbols as well # as strings. In the database, the symbolized attribute should have # the column-type :string. # # Example: # class User < ActiveRecord::Base # symbolize :gender, :in => [:female, :male] # symbolize :so, :in => { # :linux => "Linux", # :mac => "Mac OS X" # } # symbolize :gui, , :in => [:gnome, :kde, :xfce], :allow_blank => true # symbolize :browser, :in => [:firefox, :opera], :i18n => false # end # # It will automattically lookup for i18n: # # activerecord: # symbolizes: # user: # gender: # female: Girl # male: Boy # # You can skip i18n lookup with :i18n => false # symbolize :gender, :in => [:female, :male], :i18n => false # # Its possible to use boolean fields also. # symbolize :switch, :in => [true, false] # # ... # switch: # "true": On # "false": Off # "nil": Unknown # module ClassMethods # Specifies that values of the given attributes should be returned # as symbols. The table column should be created of type string. def symbolize(*attr_names) configuration = attr_names.extract_options! configuration.assert_valid_keys(:in, :within, :i18n, :scopes, :methods, :capitalize, :validate, :default, :allow_blank, :allow_nil) enum = configuration[:in] || configuration[:within] i18n = configuration[:i18n] i18n = enum && !enum.is_a?(Hash) if i18n.nil? scopes = configuration[:scopes] methods = configuration[:methods] capitalize = configuration[:capitalize] validation = configuration[:validate] != false default_option = configuration[:default] attr_names.each do |attr_name| attr_name_str = attr_name.to_s if enum enum_hash = \ if enum.is_a?(Hash) enum else enum.each_with_object({}) do |e, a| a.store(e.respond_to?(:to_sym) ? e.to_sym : e, capitalize ? e.to_s.capitalize : e.to_s) end end values_name = attr_name_str + '_values' values_const_name = values_name.upcase # Get the values of :in const_set values_const_name, enum_hash unless const_defined? values_const_name [ 'get_' + values_name, attr_name_str + '_enum', ].each do |enum_method_name| define_singleton_method(enum_method_name) do if i18n enum_hash.each_key.map do |symbol| [i18n_translation_for(attr_name_str, symbol), symbol] end else enum_hash.map(&:reverse) end end end if methods enum_hash.each_key do |key| # It's a good idea to test for name collisions here and raise exceptions. # However, the existing software with this kind of errors will start crashing, # so I'd postpone this improvement until the next major version # this way it will not affect those people who use ~> in their Gemfiles # raise ArgumentError, "re-defined #{key}? method of #{self.name} class due to 'symbolize'" if method_defined?("#{key}?") define_method("#{key}?") do send(attr_name_str) == key.to_sym end end end if scopes if scopes == :shallow enum_hash.each_key do |name| next unless name.respond_to?(:to_sym) scope name, -> { where(attr_name_str => name) } # Figure out if this as another option, or default... # scope "not_#{name}", -> { where.not(attr_name_str => name) end else scope attr_name_str, ->(val) { where(attr_name_str => val) } end end if validation validates(*attr_names, configuration.slice(:allow_nil, :allow_blank).merge(:inclusion => { :in => enum_hash.keys })) end end define_method(attr_name_str) { read_and_symbolize_attribute(attr_name_str) || default_option } define_method(attr_name_str + '=') { |value| write_symbolized_attribute(attr_name_str, value) } if default_option before_save { self[attr_name_str] ||= default_option } else define_method(attr_name_str) { read_and_symbolize_attribute(attr_name_str) } end define_method(attr_name_str + '_text') do if i18n read_i18n_attribute(attr_name_str) else attr_value = send(attr_name_str) if enum enum_hash[attr_value] else attr_value.to_s end end end end # merge new symbolized attribute and create a new array to ensure that each class in inheritance hierarchy # has its own array of symbolized attributes self.symbolized_attributes += attr_names.map(&:to_s) end # Hook used by Rails to do extra stuff to attributes when they are initialized. def initialize_attributes(*args) super.tap do |attributes| # Make sure any default values read from the database are symbolized symbolized_attributes.each do |attr_name| attributes[attr_name] = symbolize_attribute(attributes[attr_name]) end end end # String becomes symbol, booleans string and nil nil. def symbolize_attribute(value) case value when String value.presence.try(:to_sym) when Symbol, TrueClass, FalseClass, Numeric value else nil end end def i18n_translation_for(attr_name, attr_value) I18n.translate("activerecord.symbolizes.#{model_name.to_s.underscore}.#{attr_name}.#{attr_value}") end end # String becomes symbol, booleans string and nil nil. def symbolize_attribute(value) self.class.symbolize_attribute(value) end # Return an attribute's value as a symbol or nil def read_and_symbolize_attribute(attr_name) symbolize_attribute(read_attribute(attr_name)) end # Return an attribute's i18n def read_i18n_attribute(attr_name) unless (t = self.class.i18n_translation_for(attr_name, read_attribute(attr_name))).is_a?(Hash) t end end # Write a symbolized value. Watch out for booleans. def write_symbolized_attribute(attr_name, value) write_attribute(attr_name, symbolize_attribute(value)) end end end