tonchis/hatch

View on GitHub
lib/hatch.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Hatch
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  def errors
    []
  end

  def valid?
    true
  end

  module ClassMethods
    @@validations = {}

    def self.extended(klass)
      klass_symbol = klass.to_s.to_sym
      @@validations[klass_symbol] = {}

      invalid_class = Class.new do
        include InvalidInstanceMethods
      end

      klass.const_set("Invalid#{klass.name.split('::').last}", invalid_class)
    end

    def certify(attribute, error, &block)
      @@validations[self.to_s.to_sym][attribute] = Validation.new(error, &block)
    end

    def certifies(attribute, validation, error = nil)
      @@validations[self.to_s.to_sym][attribute] = Validation.send(validation, error)
    end

    def hatch(args = {})
      validated_attributes = []
      @@validations[self.to_s.to_sym].each_pair do |attribute, validation|
        validated_attributes << ValidatedAttribute.validate(attribute,
                                                            args[attribute],
                                                            validation.error,
                                                            &validation.block)
      end

      build(validated_attributes)
    end

    def build(validated_attributes)
      if validated_attributes.all? {|validated_attribute| validated_attribute.valid?}
        set_instance_variables(new, *validated_attributes)
      else
        const_get("Invalid#{self.to_s.split('::').last}").new(*validated_attributes)
      end
    end
    private :build

    def set_instance_variables(instance, *args)
      @@validations[instance.class.name.to_sym].keys.each_with_index do |attribute, index|
        instance.instance_variable_set("@#{attribute}", args[index].value)
      end
      instance
    end
    private :set_instance_variables

    module InvalidInstanceMethods
      attr :errors

      def initialize(*validated_attributes)
        @validated_attributes = validated_attributes
        @errors = Errors.build(@validated_attributes)
        respond_to_readable_attributes
      end

      def valid?
        false
      end

      def respond_to_readable_attributes
        readable_attributes.each do |readable_attribute|
          self.class.send(:define_method,
                          readable_attribute.attribute,
                          -> {readable_attribute.value})
        end
      end
      private :respond_to_readable_attributes

      def readable_attributes
        instance_methods = extended_class.instance_methods(false)

        @validated_attributes.select do |validated_attribute|
          instance_methods.include?(validated_attribute.attribute)
        end
      end
      private :readable_attributes

      def extended_class
        self.class.name.gsub(/(.*)(\:\:Invalid)(.*)/, '\1').split('::').inject(Object) do |mod, class_name|
          mod.const_get(class_name)
        end
      end
      private :extended_class
    end
  end

  class ValidatedAttribute
    attr :attribute, :value, :error

    def self.validate(attribute, value, error, &block)
      if yield(value)
        ValidAttribute.new(attribute, value)
      else
        InvalidAttribute.new(attribute, value, error)
      end
    end

    def initialize(attribute, value, error = [])
      @attribute, @value, @error = attribute, value, error
    end
  end

  class ValidAttribute < ValidatedAttribute
    def valid?
      true
    end

    def invalid?
      false
    end
  end

  class InvalidAttribute < ValidatedAttribute
    def valid?
      false
    end

    def invalid?
      true
    end
  end

  class Validation
    attr :error, :block

    def initialize(error, &block)
      @error, @block = error, block
    end

    def self.not_nil(error)
      new(error || 'must not be nil') {|value| !value.nil?}
    end

    def self.positive_number(error)
      new(error || 'must be a positive number') {|value| !value.nil? && value > 0}
    end

    def self.not_empty(error)
      new(error || 'must not be empty') {|value| !value.nil? && !value.empty?}
    end
  end

  class Errors < Hash
    def self.build(validated_attributes)
      self[attributes_and_errors(validated_attributes)]
    end

    def on(attr)
      self[attr]
    end

    def full_messages
      values.reject {|value| value.empty?}
    end

    def empty?
      full_messages.empty?
    end

    def self.attributes_and_errors(validated_attributes)
      validated_attributes.map do |validated_attribute|
        [validated_attribute.attribute, validated_attribute.error]
      end
    end
    private_class_method :attributes_and_errors
  end
end