makandra/assignable_values

View on GitHub
lib/assignable_values/active_record/restriction/base.rb

Summary

Maintainability
C
1 day
Test Coverage
module AssignableValues
  module ActiveRecord
    module Restriction
      class Base

        attr_reader :model, :property, :options, :values, :default, :secondary_default

        SUPPORTED_OPTIONS = [
          :allow_blank,
          :decorate,
          :default,
          :include_old_value,
          :message,
          :multiple,
          :secondary_default,
          :through,
        ].freeze
        private_constant :SUPPORTED_OPTIONS

        def initialize(model, property, options, &values)
          @model = model
          @property = property
          @options = options
          @values = values
          validate_supported_options!
          ensure_values_given
          setup_default
          define_assignable_values_method
          setup_validation
        end

        def validate_record(record)
          value = current_value(record)
          unless allow_blank?(record) && value.blank?
            begin
              unless assignable_value?(record, value)
                record.errors.add(error_property, not_included_error_message)
              end
            rescue DelegateUnavailable
              # if the delegate is unavailable, the validation is skipped
            end
          end
        end

        def set_default(record)
          if record.new_record? && record.send(property).nil?
            default_value = evaluate_default(record, default)
            begin
              if secondary_default? && !assignable_value?(record, default_value)
                secondary_default_value = evaluate_default(record, secondary_default)
                if assignable_value?(record, secondary_default_value)
                  default_value = secondary_default_value
                end
              end
            rescue AssignableValues::DelegateUnavailable
              # skip secondary defaults if querying assignable values from a nil delegate
            end
            record.send("#{property}=", default_value)
          end
          true
        end

        def assignable_values(record, options = {})
          additional_assignable_values = []
          current_values = assignable_values_from_record_or_delegate(record)

          if options.fetch(:include_old_value, true) && has_previously_saved_value?(record)
            old_value = previously_saved_value(record)
            if @options[:multiple]
              if old_value.is_a?(Array)
                additional_assignable_values = old_value
              end
            elsif !old_value.blank? && !current_values.include?(old_value)
              additional_assignable_values << old_value
            end
          end

          if options[:decorate]
            current_values = decorate_values(current_values, record.class)
            additional_assignable_values = decorate_values(additional_assignable_values, record.class)
          end

          if additional_assignable_values.present?
            # will not keep current_values scoped
            additional_assignable_values | current_values
          else
            current_values
          end
        end

        private

        def error_property
          property
        end

        def not_included_error_message
          if @options[:message]
            @options[:message]
          else
            I18n.t('errors.messages.inclusion', :default => 'is not included in the list')
          end
        end

        def assignable_value?(record, value)
          if @options[:multiple]
            assignable_multi_value?(record, value)
          else
            assignable_single_value?(record, value)
          end
        end

        def assignable_single_value?(record, value)
          (has_previously_saved_value?(record) && value == previously_saved_value(record)) ||
            (value.blank? && allow_blank?(record)) ||
            included_in_assignable_values?(record, value)
        end

        def included_in_assignable_values?(record, value)
          values_or_scope = assignable_values(record, :include_old_value => false)

          if is_scope?(values_or_scope)
            values_or_scope.exists?(value.id) unless value.nil?
          else
            values_or_scope.include?(value)
          end
        end

        def is_scope?(object)
          object.respond_to?(:scoped) || object.respond_to?(:all)
        end

        def assignable_multi_value?(record, value)
          (has_previously_saved_value?(record) && value == previously_saved_value(record)) ||
            (value.blank? ? allow_blank?(record) : subset?(value, assignable_values(record)))
        end

        def subset?(array1, array2)
          array1.is_a?(Array) && array2.is_a?(Array) && (array1 - array2).empty?
        end

        def evaluate_default(record, value_or_proc)
          if value_or_proc.is_a?(Proc)
            record.instance_exec(&value_or_proc)
          else
            value_or_proc
          end
        end

        def current_value(record)
          record.send(property)
        end

        def has_previously_saved_value?(record)
          raise NotImplementedError
        end

        def previously_saved_value(record)
          raise NotImplementedError
        end

        def decorate_values(values, _klass)
          values
        end

        def delegate?
          @options.has_key?(:through)
        end

        def default?
          @options.has_key?(:default)
        end

        def secondary_default?
          @options.has_key?(:secondary_default)
        end

        def allow_blank?(record)
          evaluate_option(record, @options[:allow_blank])
        end

        def delegate_definition
          options[:through]
        end

        def enhance_model_singleton(&block)
          @model.singleton_class.class_eval(&block)
        end

        def enhance_model(&block)
          @model.class_eval(&block)
        end

        def setup_default
          if default?
            @default = options[:default] # for attr_reader
            @secondary_default = options[:secondary_default] # for attr_reader
            ensure_after_initialize_callback_enabled
            restriction = self
            enhance_model do
              set_default_method = :"set_default_#{restriction.property}"
              define_method set_default_method do
                restriction.set_default(self)
              end
              after_initialize set_default_method
            end
          elsif secondary_default?
            raise AssignableValues::NoDefault, "cannot use the :secondary_default option without a :default option"
          end
        end

        def ensure_after_initialize_callback_enabled
          if active_record_2?
            enhance_model do
              # Old ActiveRecord version only call after_initialize callbacks only if this method is defined in a class.
              unless method_defined?(:after_initialize)
                define_method(:after_initialize) {}
              end
            end
          end
        end

        def active_record_2?
          ::ActiveRecord::VERSION::MAJOR < 3
        end

        def setup_validation
          restriction = self
          enhance_model do
            validate_method = :"validate_#{restriction.property}_assignable"
            define_method validate_method do
              restriction.validate_record(self)
            end
            validate validate_method.to_sym
          end
        end

        def define_assignable_values_method
          restriction = self
          enhance_model do
            assignable_values_method = :"assignable_#{restriction.property.to_s.pluralize}"
            define_method assignable_values_method do |*args|
              # Ruby 1.8.7 does not support optional block arguments :(
              options = args.first || {}
              options.merge!({:decorate => true})
              restriction.assignable_values(self, options)
            end
          end
        end

        def assignable_values_from_record_or_delegate(record)
          assignable_values = if delegate?
            assignable_values_from_delegate(record)
          else
            record.instance_exec(&@values)
          end

          if is_scope?(assignable_values)
            assignable_values
          else
            Array(assignable_values)
          end
        end

        def delegate(record)
          evaluate_option(record, delegate_definition)
        end

        def evaluate_option(record, option)
          case option
          when NilClass, TrueClass, FalseClass then option
          when Symbol then record.send(option)
          when Proc then record.instance_exec(&option)
          else raise "Illegal option type: #{option.inspect}"
          end
        end

        def assignable_values_from_delegate(record)
          delegate = delegate(record)
          delegate.present? or raise DelegateUnavailable, "Cannot query a nil delegate for assignable values"
          delegate_query_method = :"assignable_#{model.name.underscore.gsub('/', '_')}_#{property.to_s.pluralize}"
          args = delegate.method(delegate_query_method).arity == 0 ? [] : [record]
          delegate.send(delegate_query_method, *args)
        end

        def ensure_values_given
          @values or @options[:through] or raise NoValuesGiven, 'You must supply the list of assignable values by either a block or :through option'
        end

        def validate_supported_options!
          unsupported_options = @options.keys - SUPPORTED_OPTIONS
          if unsupported_options.any?
            raise UnsupportedOption,
              "The following options are not supported: #{unsupported_options.map { |o| ":#{o}" }.join(', ')}"
          end
        end
      end
    end
  end
end