joshwlewis/validates_by_schema

View on GitHub
lib/validates_by_schema/validation_option.rb

Summary

Maintainability
A
2 hrs
Test Coverage
class ValidatesBySchema::ValidationOption
  # column here must be an ActiveRecord column
  # i.e. MyARModel.columns.first
  attr_accessor :klass, :column, :unique_indexes

  def initialize(klass, column, unique_indexes = [])
    @klass = klass
    @column = column
    @unique_indexes = unique_indexes.select { |index| index.columns.first == column.name }
  end

  def define!
    if association
      # Only presence and uniqueness are handled for associations.
      # presence on the association name, uniqueness on the column name.
      define_belongs_to_presence_validation
    else
      define_validations(to_hash)
    end
    define_uniqueness_validations
  end

  private

  def define_belongs_to_presence_validation
    if !ActiveRecord::Base.belongs_to_required_by_default && presence
      klass.validates association.name, presence: true
    end
  end

  def define_uniqueness_validations
    uniqueness.each do |options|
      define_validations(uniqueness: options)
    end
  end

  def define_validations(options)
    klass.validates column.name, options if options.present?
  end

  def presence?
    presence && column.type != :boolean
  end

  def presence
    !column.null
  end

  def enum?
    klass.respond_to?(:defined_enums) && klass.defined_enums.has_key?(column.name)
  end

  def numericality?
    [:integer, :decimal, :float].include?(column.type) && !enum?
  end

  def numericality
    numericality = {}
    if column.type == :integer
      numericality[:only_integer] = true
      if integer_max
        numericality[:less_than] = integer_max
        numericality[:greater_than] = -integer_max
      end
    elsif column.type == :decimal && decimal_max
      numericality[:less_than_or_equal_to] = decimal_max
      numericality[:greater_than_or_equal_to] = -decimal_max
    end
    numericality[:allow_nil] = true
    numericality
  end

  def uniqueness
    unique_indexes.map do |index|
      {
        scope: index.columns.reject { |col| col == column.name },
        conditions: -> { where(index.where) },
        allow_nil: column.null,
        case_sensitive: case_sensitive?,
        if: ->(model) { index.columns.any? { |c| model.send("#{c}_changed?") } }
      }
    end
  end

  def case_sensitive?
    !klass.connection.respond_to?(:collation) ||
      !klass.connection.collation.end_with?('_ci')
  end

  def array?
    column.respond_to?(:array) && column.array
  end

  def length?
    [:string, :text].include?(column.type) && column.limit && !array?
  end

  def length
    { maximum: column.limit, allow_nil: true }
  end

  def inclusion?
    column.type == :boolean
  end

  def inclusion
    { in: [true, false], allow_nil: column.null }
  end

  def integer_max
    (2**(8 * column.limit)) / 2 if column.limit
  end

  def decimal_max
    10.0**(column.precision - column.scale) - 10.0**(-column.scale) if column.precision && column.scale
  end

  def association
    @association ||= klass.reflect_on_all_associations(:belongs_to).find do |a|
      a.foreign_key.to_s == column.name
    end
  end

  def to_hash
    [:presence, :numericality, :length, :inclusion].inject({}) do |h, k|
      send(:"#{k}?") ? h.merge(k => send(k)) : h
    end
  end

  def to_s
    to_hash.inspect
  end

end