neopoly/bound

View on GitHub
lib/bound.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require "bound/version"
require "bound/caller"
require "bound/errors"

class Bound
  def self.new(*args)
    new_bound_class.required(*args)
  end

  def self.optional(*args)
    new_bound_class.optional(*args)
  end

  def self.required(*args)
    new_bound_class.required(*args)
  end

  def self.validate?
    !@validation_disabled
  end

  def self.disable_validation
    @validation_disabled = true
    StaticBoundClass.define_initializer_without_validation
  end

  private

  def self.new_bound_class
    Class.new(StaticBoundClass) do
    end
  end

  class BoundValidator
    attr_accessor :attributes, :optional_attributes, :nested_array_attributes

    def initialize(bound, target, overwrite)
      @bound = bound
      @target = target || {}
      @overwrite = overwrite || {}
    end

    def validate!
      ensure_all_given_attributes_are_known!
      attributes.each do |attribute|
        ensure_present! attribute
      end
      nested_array_attributes.each do |nested_array_attribute|
        ensure_array! nested_array_attribute
      end
      ensure_all_attributes_are_callable!
    end

    private

    def ensure_all_given_attributes_are_known!
      a = overwritten_attrs + target_attrs
      b = attributes + optional_attributes
      a.each do |attr|
        unless b.include? attr
          message = "Unknown attribute: #{attr.inspect} in #{b}"
          raise UnknownAttributeError, message
        end
      end
    end

    def ensure_present!(attribute)
      if !overwritten?(attribute) && !target_has?(attribute)
        message = "Missing attribute: #{attribute}"
        raise MissingAttributeError, message
      end
    end

    def ensure_array!(attribute)
      message = "Expected %s to be an array"
      if overwritten?(attribute)
        unless val = overwritten(attribute).kind_of?(Array)
          raise(ArgumentError, message % val.inspect)
        end
      elsif target_has?(attribute)
        unless val = target(attribute).kind_of?(Array)
          raise(ArgumentError, message % val.inspect)
        end
      else
      end
    end

    def ensure_all_attributes_are_callable!
      attributes.each do |attr|
        @bound.send attr
      end
      optional_attributes.each do |attr|
        @bound.send attr if set? attr
      end
    end

    def overwritten_attrs
      @overwrite.keys
    end

    def target_attrs
      if @target.kind_of?(Hash)
        @target.keys
      else
        []
      end
    end

    def overwritten?(attr)
      @overwrite.key?(attr)
    end

    def overwritten(attr)
      @overwrite[attr]
    end

    def target_has?(attr)
      @target.kind_of?(Hash) ? @target.key?(attr) : @target.respond_to?(attr)
    end

    def target(attr)
      @target.kind_of?(Hash) ? @target[attr] : @target.send(attr)
    end

    def set?(attr)
      target_has?(attr) || overwritten?(attr)
    end
  end

  class StaticBoundClass
    def ==(other)
      false unless other
      true
    end

    def validate!
    end

    def self.define_initializer_without_validation
      define_initializer(nil)
    end

    def self.define_initializer(after_init = 'validate!')
      code = <<-EOR
        def initialize(target = nil, overwrite = nil)
          @t, @o = target, overwrite
          %s
        end
      EOR

      class_eval code % after_init
    end

    def self.define_attributes(*attributes)
      if attributes.last.kind_of? Hash
        nested_attributes = attributes.pop
      else
        nested_attributes = {}
      end

      if nested_attributes.keys.any? { |a| !a.kind_of? Symbol }
        message = "Invalid list of attributes: #{nested_attributes.inspect}"
        raise ArgumentError, message
      end

      if attributes.any? { |a| !a.kind_of? Symbol }
        message = "Invalid list of attributes: #{attributes.inspect}"
        raise ArgumentError, message
      end

      nested_attributes.each do |attribute, nested_class|
        define_nested_delegate attribute, nested_class
        define_equality attribute
      end

      attributes.each do |attribute|
        define_delegate attribute
        define_equality attribute
      end
    end

    def self.define_validator
      attributes = symbolize_attributes(@attributes ||= [])
      optional_attributes = symbolize_attributes(@optional_attributes ||= [])
      nested_array_attributes = symbolize_attributes(@nested_array_attributes ||= [])

      undef_method :validate!

      code = <<-EOR
        def validate!
          v = Bound::BoundValidator.new(self, @t, @o)
          v.attributes = [#{attributes}]
          v.optional_attributes = [#{optional_attributes}]
          v.nested_array_attributes = [#{nested_array_attributes}]
          v.validate!
        end
        private :validate!
      EOR
      class_eval code
    end

    def self.symbolize_attributes(attributes)
      attributes.map { |attr| ":#{attr}" }.join(", ")
    end

    def self.set_required_attributes(attributes, nested_array_attributes)
      @attributes ||= []
      @attributes.concat attributes
      @attributes.concat nested_array_attributes
      @nested_array_attributes ||= []
      @nested_array_attributes.concat nested_array_attributes
      define_validator
    end

    def self.set_optional_attributes(attributes, nested_array_attributes)
      @optional_attributes ||= []
      @optional_attributes.concat attributes
      @optional_attributes.concat nested_array_attributes
      @nested_array_attributes ||= []
      @nested_array_attributes.concat nested_array_attributes
      define_validator
    end

    def self.define_equality(attr)
      @equality ||= []
      @equality << attr
      undef_method :==
      code = <<-EOR
        def ==(other)
          return false unless other
          #{@equality.inspect}.all? do |attr|
            other.respond_to?(attr) &&
              other.send(attr) == send(attr)
          end
        end
      EOR
      class_eval code
    end

    def self.define_delegate(attr, prefix = '')
      code = <<-EOR
        def #{prefix}#{attr}
          if @o && @o.key?(:#{attr})
            @o[:#{attr}]
          elsif @t
            if @t.kind_of?(Hash)
              @t[:#{attr}]
            elsif @t.respond_to?(:#{attr})
              @t.#{attr}
            end
          end
        end
      EOR
      class_eval code
    end

    def self.define_nested_delegate(attr, nested_class)
      define_delegate attr, 'get_'
      code = <<-EOR
        class << self
          def get_#{attr}_class
            @#{attr}_class
          end
          def set_#{attr}_class(arg)
            @#{attr}_class = arg
          end
          private :set_#{attr}_class
        end
      EOR

      if nested_class.kind_of? Array
        nested_class = nested_class.first
        code += <<-EOR
          def #{attr}
            return @#{attr} if defined? @#{attr}
            return [] unless val = get_#{attr}
            @#{attr} ||= val.map{|t| self.class.get_#{attr}_class.new t}
          end
          private :get_#{attr}
        EOR
      else
        code += <<-EOR
          def #{attr}
            return @#{attr} if defined? @#{attr}
            return nil unless val = get_#{attr}
            @#{attr} ||= self.class.get_#{attr}_class.new(val)
          end
          private :get_#{attr}
        EOR
      end
      class_eval code
      self.send :"set_#{attr}_class", nested_class
    end

    def self.required(*attributes)
      set_attributes(:set_required_attributes, attributes)
    end

    def self.optional(*attributes)
      set_attributes(:set_optional_attributes, attributes)
    end

    def self.set_attributes(type, attributes)
      self.define_attributes(*attributes)

      array_attributes = []
      if attributes.last.kind_of? Hash
        attributes.pop.each do |attr, nested_class|
          array_attributes << attr if nested_class.kind_of? Array
          attributes << attr
        end
      end

      self.send(type, attributes, array_attributes)
      self
    end

    define_initializer
  end

end