Module: Enum::AttrSupport

Defined in:
lib/iron/enum/attr_support.rb

Overview

Provides helper methods to integrate enumerated constants (Enum) into your model layer. Given an enum defined like so:

module UserType
  enum :guest,  0
  enum :member, 1
  enum :admin,  2
end

To add an enumerated value to a Rails model, simply add a column of type :integer to your model, then declare it like so:

class User < ActiveRecord::Base
  # A symbol will be assumed to map to a valid enum class name
  enum_attr :user_type
  # If your attribute's name won't map automatically, you can pass a hash instead
  enum_attr :another_user_type => UserType
  # If you have multiple enum attributes, you can add them all in one call, just make
  # sure mapped attrs are at the end or you'll get an error
  enum_attr :user_type, :another_user_type => UserType
end

When using non-model classes, it’s the same syntax:

class User
  enum_attr :user_type
end

This will tell your class/model that the user_type attribute contains values from the UserType enum, and will add:

@user.user_type => integer value or nil
@user.user_type_admin? => true if object's user_type value == UserType::ADMIN
@user.user_type_admin! => set the object's user_type to be UserType::ADMIN (does not save model!)
@user.user_type_as_key => returns the key form of the current field value, eg :member
@user.user_type_as_name => returns text name of the current field's value, eg 'Guest'

In addition, you can set enum attributes via key, eg:

@user.user_type = :admin

and the key will be converted to a value on the fly.

ActiveRecord models get a few extras. To start, each enum attribute will add a smart scope:

User.with_user_type(UserType::MEMBER) => scope returning a relation selecting User instances where user_type's value == UserType::MEMBER

In addition, enum attributes will show up in #inspect output as e.g. UserType::GUEST instead of 0. Enum attributes will also generate an automatic inclusion validation to ensure that the attribute never ends up being an invalid value.

Instance Method Summary collapse

Instance Method Details

#enum_attr(*array_or_map) ⇒ Object

Call with enum_attr :field => Enum



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/iron/enum/attr_support.rb', line 55

def enum_attr(*array_or_map)
  # Convert to a full map
  field_to_enum_map = {}
  array_or_map.each do |info|
    if info.is_a?(Symbol)
      name = info.to_s.capitalize.gsub(/\_([a-z])/) { $1.capitalize }
      klass = Object.const_get(name) rescue nil
      raise "Unknown enum class '#{name}' for enum_attr :#{info}" unless klass
      field_to_enum_map[info] = klass
    elsif info.is_a?(Hash)
      field_to_enum_map.merge!(info)
    else
      raise "Invalid enum_attr key: #{info.inspect}"
    end
  end
  
  # Save off the attr map
  @enum_attrs ||= {}
  @enum_attrs.merge!(field_to_enum_map)

  # Run each newly added enum attribute
  field_to_enum_map.each_pair do |attr_field, enum|
    # Convert Enum to "Enum"
    enum_klass = enum.to_s

    # Set up general use sugar - allows calling:
    #   attr_as_key to get back eg :production or :online instead of 1 or 5
    #   attr_as_name to get back eg "Production" or "Online"
    class_eval <<-eos, __FILE__, __LINE__ + 1
      def #{attr_field}_as_key
        #{enum_klass}.key(self.#{attr_field})
      end

      def #{attr_field}_as_name
        #{enum_klass}.name(self.#{attr_field})
      end
    eos

    # Get all the possible values for this enum in :key format (ie as symbols)
    enum.keys.each do |key|
      # Get the value for this key (ie in integer format)
      val = enum.value(key)

      # Build sugar for testing and setting the attribute's enumerated value
      class_eval <<-eos, __FILE__, __LINE__ + 1
        def #{attr_field}_#{key}?
          self.#{attr_field} == #{val}
        end

        def #{attr_field}_#{key}!
          self.#{attr_field} = #{val}
        end
      eos
    end

    if defined?(ActiveRecord) && self < ActiveRecord::Base

      # Define a finder scope
      scope "with_#{attr_field}", lambda {|*vals|
        vals.flatten!
        if vals.empty?
          where("?", false)
        elsif vals.count == 1
          where(attr_field => enum.value(vals.first))
        else
          where(attr_field => enum.values(vals))
        end
      }
      
      # Define a validation
      validates attr_field, :inclusion => { 
        :in => enum.values,
        :message => "%{value} is not a valid #{enum_klass} value",
        :allow_nil => true
      }

      # Override default setter to allow setting an enum attribute via key
      class_eval <<-eos, __FILE__, __LINE__ + 1
        def #{attr_field}=(val)
          val = nil if val.is_a?(String) && val.empty?
          write_attribute(:#{attr_field}, #{enum_klass}.value(val))
        end
      eos

    else 

      # Create getter/setter to allow setting an enum attribute via key
      class_eval <<-eos, __FILE__, __LINE__ + 1
        def #{attr_field}
          @#{attr_field}
        end

        def #{attr_field}=(val)
          val = nil if val.is_a?(String) && val.empty?
          @#{attr_field} = #{enum_klass}.value(val)
        end
      eos

    end

  end
end

#enum_attr?(name) ⇒ Boolean

True if the given symbol maps to an enum-backed attribute

Returns:

  • (Boolean)


159
160
161
162
# File 'lib/iron/enum/attr_support.rb', line 159

def enum_attr?(name)
  return false unless @enum_attrs
  @enum_attrs.key?(name)
end

#enum_for_attr(name) ⇒ Object

Gets the enum class for a given attribute, or nil for none



165
166
167
168
# File 'lib/iron/enum/attr_support.rb', line 165

def enum_for_attr(name)
  return nil unless @enum_attrs
  @enum_attrs[name]
end