module EnumX::DSL::ClassMethods

DSL methods

Public Instance Methods

enum(attribute, *args) click to toggle source

@!method enum(name, [enum], validation_options = {})

Defines an enum for an attribute. This works on ActiveRecord objects, but also on other objects. However, for non-ActiveRecord objects, make sure that the underlying attribute already exists.

@param [Symbol] attribute The attribute to add enum support to. @param [EnumX|Enumerable|Symbol]

The enum to use. Specify a symbol to look up the enum from the defined enums, or use
an array to create an ad-hoc enum for this attribute, with name "#{model}_#{attribute.pluralize}",
e.g. 'post_statuses'.

@param [Hash] validation_options

Options for the validation routine. The options listed below are extracted from this hash.

@option validation_options :validation

Set to false to disable validation altogether. TODO SPEC

@option validation_options :flags

Set to true to enable a flag enum attribute.

@option validation_options :mnemonics

Set to true to create enum mnemonics on the receiver class. These are question-mark style methods for
all the values on the enum. Note that these may override existing methods, so use them only for enums
that define a part of the 'identity' of the receiving class, such as a status or a kind.

@example The following creates an enum on an ActiveRecord object.

enum :kind                     # => Uses EnumX.kinds

@example The following creates an enum on an ActiveRecord object, but uses 'account_kinds' as the name.

enum :kind, :account_kinds     # => Uses EnumX.account_kinds

@example The following creates an enum on an ActiveRecord object, but uses a custom array.

class Account < ActiveRecord::Base
  enum :kind, %w[ normal special ]  # => Uses an on the fly enum with name 'account_kinds'.
end

@example The following creates an enum on an ActiveRecord object, but uses a custom array, on an anonymous class.

Class.new(ActiveRecord::Base) do
  enum :kind, %w[ normal special ]  # => Uses an on the fly enum with name '_kinds'.
end

@example The following creates an enum on a non-ActiveRecord class.

class Account
  attr_accessor :kind   # Required first!
  enum :kind
end

@example The following creates an enum and creates mnemonics.

class Account < ActiveRecord::Base
  enum :kind, :mnemonics => true
end

# Given that enum 'kinds' has options %[ normal special ], the following methods are now added:
Account.new(:kind => :normal).normal? # => true
Account.new(:kind => :special).normal? # => true
Account.new(:kind => :normal).special? # => false
Account.new(:kind => :special).special? # => false
Account.new(:kind => :special).something_else? # => raises NoMethodError
# File lib/enum_x/dsl.rb, line 89
        def enum(attribute, *args)

          validation_options = args.extract_options!
          enum = args.shift
          raise ArgumentError, "too many arguments (2..3 expected)" if args.length > 0

          flags = validation_options.delete(:flags)
          mnemonics = validation_options.delete(:mnemonics)

          # Determine the default name of the enum, and the name of the class-level enum reader.
          enum_reader_name = if flags
            # The attribute is already specified in the plural form (enum :statuses).
            attribute.to_s
          else
            # The attribute is specified in the singular form - pluralize it (enum :status).
            attribute.to_s.pluralize
          end

          enum = case enum_opt = enum
          when nil then EnumX[enum_reader_name]
          when EnumX then enum
          when Symbol, String then EnumX[enum]
          when Enumerable
            name = if self.name
              "#{self.name.demodulize.underscore}_#{enum_reader_name}"
            else
              # Anonymous class - use just the attribute name with an underscore in front of it.
              "_#{enum_reader_name}"
            end

            EnumX.new(name, enum)
          end
          raise ArgumentError, "cannot find enum #{(enum_opt || enum_reader_name).inspect}" unless enum

          # Define a shorthand enum accessor method.
          unless respond_to?(enum_reader_name)
            # Regular class. As the class may be inherited, make sure to try superclasses as well.
            class_eval <<-RUBY, __FILE__, __LINE__+1
              def self.#{enum_reader_name}
                @#{enum_reader_name} ||= if superclass.respond_to?(:#{enum_reader_name})
                  superclass.#{enum_reader_name}
                end
              end
            RUBY
          end

          # Store the enum on this class.
          instance_variable_set "@#{enum_reader_name}", enum

          if flags
            # Define a flags enum.

            # Validation
            if validation_options[:validation] != false

              validation_options.assert_valid_keys :allow_blank
              if validation_options[:allow_blank] != false
                class_eval <<-RUBY, __FILE__, __LINE__+1
                  validates_each :#{attribute} do |record, attribute, value|
                    if value.present?
                      value = [ value ] unless value.is_a?(Enumerable)
                      if not_included_value = value.find{ |v| !enum.values.include?(v) }
                        record.errors.add attribute, :inclusion, :value => not_included_value
                      end
                    end
                  end
                RUBY
              else
                class_eval <<-RUBY, __FILE__, __LINE__+1
                  validates_each :#{attribute} do |record, attribute, value|
                    value = [ value ] unless value.is_a?(Enumerable) || value.nil?
                    if value.blank?
                      record.errors.add attribute, :blank
                    elsif not_included_value = value.find{ |v| !enum.values.include?(v) }
                      record.errors.add attribute, :inclusion, :value => not_included_value
                    end
                  end
                RUBY
              end

            end

            # Serialize the value if this is an ActiveRecord class AND if the database actually contains
            # this column.
            if defined?(ActiveRecord) && self < ActiveRecord::Base && self.column_names.include?(attribute.to_s)
              serialize attribute, FlagsSerializer.new(enum)
            end

            # Provide a customized reader.
            DSL.define_multi_reader self, attribute
            DSL.define_multi_writer self, attribute

            # Provide two Squeel sifters.
            if respond_to?(:sifter)
              class_eval <<-RUBY, __FILE__, __LINE__+1
                sifter(:#{attribute}_include) { |value| instance_eval('#{attribute}') =~ "%|\#{value}|%" }
                sifter(:#{attribute}_exclude) { |value| instance_eval('#{attribute}') !~ "%|\#{value}|%" }
              RUBY
            end

          else
            # Define a single enum.

            # Validation
            if validation_options[:validation] != false
              # Provide validations.
              validation_options = validation_options.merge(:in => enum.values)
              validation_options[:allow_blank] = true unless validation_options.key?(:allow_blank)

              validates_inclusion_of attribute, validation_options
            end

            # Serialize the value if this is an ActiveRecord class AND if the database actually contains
            # this column.
            if defined?(ActiveRecord) && self < ActiveRecord::Base && self.column_names.include?(attribute.to_s)
              serialize attribute, SingleSerializer.new(enum)
            end

            # Provide a customized reader.
            DSL.define_single_reader self, attribute
            DSL.define_single_writer self, attribute

          end

          # Provide mnemonics if requested
          DSL.define_mnemonics self, attribute, enum if mnemonics

        end