AlbertGazizov/attr_validator

View on GitHub
lib/attr_validator/validator.rb

Summary

Maintainability
C
1 day
Test Coverage
module AttrValidator::Validator
  extend AttrValidator::Concern

  included do
    class_attribute :validations, :associated_validations, :custom_validations
  end

  module ClassMethods
    def validates(*args)
      options = args.pop
      AttrValidator::ArgsValidator.is_hash!(options, "last argument")

      self.validations ||= {}
      args.each do |attr_name|
        add_validations(attr_name, options)
      end
    end

    def validate_associated(association_name, options)
      AttrValidator::ArgsValidator.not_nil!(options[:validator], :validator)
      AttrValidator::ArgsValidator.is_class_or_symbol!(options[:validator], :validator)
      AttrValidator::ArgsValidator.is_symbol_or_block!(options[:if], :if) if options[:if]
      AttrValidator::ArgsValidator.is_symbol_or_block!(options[:unless], :unless) if options[:unless]

      self.associated_validations ||= {}
      self.associated_validations[association_name] = options
    end

    def validate(method_name = nil, &block)
      self.custom_validations ||= []
      if block_given?
        self.custom_validations << block
      elsif method_name
        AttrValidator::ArgsValidator.is_symbol!(method_name, "validate method name")
        self.custom_validations << method_name
      else
        raise ArgumentError, "method name or block should be given for validate"
      end
    end

    private

    def add_validations(attr_name, options)
      self.validations[attr_name] ||= {}
      options.each do |validator_name, validation_options|
        validator = AttrValidator.validators[validator_name]
        unless validator
          raise AttrValidator::Errors::MissingValidatorError, "Validator with name '#{validator_name}' doesn't exist"
        end
        validator.validate_options(validation_options)
        self.validations[attr_name][validator] = validation_options
      end
    end
  end

  def validate(entity)
    errors = AttrValidator::ValidationErrors.new
    self.validations ||= {}
    self.custom_validations ||= []
    self.associated_validations ||= {}

    self.validations.each do |attr_name, validators|
      error_messages = validate_attr(attr_name, entity, validators)
      errors.add_all(attr_name, error_messages) unless error_messages.empty?
    end
    self.associated_validations.each do |association_name, options|
      next if skip_validation?(options)
      validator = options[:validator].is_a?(Class) ? options[:validator].new : self.send(options[:validator])
      children = entity.send(association_name)
      if children.is_a?(Array)
        validate_children(association_name, validator, children, errors)
      elsif children
        validate_child(association_name, validator, children, errors)
      end
    end
    self.custom_validations.each do |custom_validation|
      if custom_validation.is_a?(Symbol)
        self.send(custom_validation, entity, errors)
      else # it's Proc
        custom_validation.call(entity, errors)
      end
    end
    errors.to_hash
  end

  def validate!(entity)
    errors = validate(entity)
    unless errors.empty?
      raise AttrValidator::Errors::ValidationError.new("Validation Error", errors)
    end
  end

  private

  def validate_attr(attr_name, entity, validators)
    attr_value = entity.send(attr_name)
    error_messages = []
    validators.each do |validator, validation_rule|
      error_messages = validator.validate(attr_value, validation_rule)
      break unless error_messages.empty?
    end
    error_messages
  end

  def skip_validation?(options)
    if options[:if]
      if options[:if].is_a?(Symbol)
        true unless self.send(options[:if])
      elsif options[:if].is_a?(Proc)
        true unless self.instance_exec(&options[:if])
      else
        false
      end
    elsif options[:unless]
      if options[:unless].is_a?(Symbol)
        true if self.send(options[:unless])
      elsif options[:unless].is_a?(Proc)
        true if self.instance_exec(&options[:unless])
      else
        false
      end
    end
  end

  def validate_children(association_name, validator, children, errors)
    if validator.respond_to?(:validate_all)
      children_errors = validator.validate_all(children)
    elsif validator.respond_to?(:validate)
      children_errors = children.inject([]) do |errors, child|
        errors << validator.validate(child).to_hash
      end
    else
      raise NotImplementedError, "Validator should respond at least to :validate or :validate_all"
    end
    unless children_errors.all?(&:empty?)
      errors.messages["#{association_name}_errors".to_sym] ||= []
      errors.messages["#{association_name}_errors".to_sym] += children_errors
    end
  end

  def validate_child(association_name, validator, child, errors)
    child_errors = validator.validate(child).to_hash
    unless child_errors.empty?
      errors.messages["#{association_name}_errors".to_sym] = child_errors
    end
  end

end