module EnumId

Defines an enumerated field (stored as an integral id). The field is defined by its name (which must be the column name without the _id suffix), and a hash which maps the id valid values of the field to symbolic constants. The hash can also contain options with symbolic keys; currently the only valid option is :required which checks for non-nil values at validation; if a value other than true is assigned to it, it should be a hash with options that will be relayed to validates_presence_of

Public Instance Methods

enum_id(name, options_and_enum_values) click to toggle source
# File lib/enum_id.rb, line 11
def enum_id(name, options_and_enum_values)
  options_and_enum_values = options_and_enum_values.group_by{|k,v| k.kind_of?(Integer) ? :values : :options}
  options = (options_and_enum_values[:options]||[]).to_h
  symbols_map = (options_and_enum_values[:values]||[]).to_h
  ids_map = symbols_map.invert

  required = options[:required]

  name_id = :"#{name}_id"
  symbols_const = :"ENUM_ID_#{name}_SYMBOLS"
  ids_const = :"ENUM_ID_#{name}_IDS"

  const_set symbols_const, symbols_map
  const_set ids_const, ids_map

  model_class = self

  # Instance methods

  # Access the enumerated value as a symbol.
  define_method name do
    # #{model_class.name}.#{name}(#{name}_id)
    model_class.send name, self.send(name_id)
  end

  # Assigns the enumerated value as a symbol or id (integer)
  define_method :"#{name}=" do |st|
    # self.#{name}_id = #{model_class.name}.#{name}_id(st)
    self.send :"#{name_id}=", model_class.send(name_id, st)
  end

  # Access the human-name of the (symbolic or integral id) value (translated)
  define_method :"#{name}_name" do
    # #{model_class.name}.#{name}_name(#{name}_id)
    model_class.send :"#{name}_name", self.send(name_id)
  end

  symbols_map.values.each do |stat|
    define_method :"#{stat}?" do
      send(name) == stat
    end
  end

  # Class methods

  model_metaclass = class << model_class; self; end
  model_metaclass.instance_eval do
    define_method name do |id|
      # id && (#{symbols_const}[id.to_i] || raise("Invalid status id: #{id}"))
      id && (symbols_map[id.to_i] || raise("Invalid #{name} id: #{id}"))
    end

    define_method name_id do |st|
      st && if st.kind_of?(Integer)
        raise "Invalid #{name} id: #{st}" unless send(:"#{name}_ids").include?(st)
        st
      elsif st.kind_of?(Symbol)
        ids_map[st.to_sym] || raise("Invalid #{name}: #{st.inspect}")
      else
        raise TypeError,"Integer or Symbol argument expected (got a #{st.class.name})."
      end
    end

    define_method :"#{name}_symbol" do |st|
      st && (st.kind_of?(Integer) ? send(name, st) : st.to_sym)
    end

    define_method :"#{name}_name" do |st|
      st = send(:"#{name}_symbol", st)
      st && I18n.t("enum_id.#{model_class.name.underscore}.#{name}.#{st}")
    end

    define_method :"#{name}_ids" do
      symbols_map.keys.sort
    end

    define_method :"#{name}_symbols" do
      send(:"#{name}_ids").map{|id| send(:"#{name}_symbol", id)}
    end
  end

  # Define validations
  if required == true
    validates_inclusion_of name_id, :in=>model_class.send(:"#{name}_ids")
  else
    validates_inclusion_of name_id, :in=>model_class.send(:"#{name}_ids")+[nil]
    validates_presence_of name_id, required if required
  end
end