lib/axiom/attribute.rb

Summary

Maintainability
A
35 mins
Test Coverage
# encoding: utf-8

module Axiom

  # Abstract base class representing a type of data in a relation tuple
  class Attribute
    extend Aliasable, DescendantsTracker
    include AbstractType, ::Comparable, Visitable
    include Equalizer.new(:name, :type, :required?)

    abstract_singleton_method :type

    # The attribute name
    #
    # @return [Symbol]
    #
    # @api private
    attr_reader :name

    # The attribute type
    #
    # @return [Class<Types::Object>]
    #
    # @api private
    attr_reader :type

    # Coerce an object into an Attribute
    #
    # @param [Attribute, #to_ary, #to_sym] object
    #   the object to coerce
    #
    # @return [Attribute]
    #
    # @api private
    def self.coerce(object)
      if object.kind_of?(Attribute)
        object
      elsif equal?(Attribute)
        Object.coerce(object)
      else
        name, type, options = object
        klass = type ? const_get(type.name) : self
        klass.new(name, options || EMPTY_HASH)
      end
    end

    # Extract the attribute name from the object
    #
    # @param [#name, #to_ary, #to_a] object
    #   the object to extract a name from
    #
    # @return [Symbol]
    #
    # @api private
    def self.name_from(object)
      if object.respond_to?(:name)
        object.name
      else
        Array(object).first.to_sym
      end
    end

    # Infer the Attribute type from the operand
    #
    # @param [Object] operand
    #
    # @return [Class<Attribute>]
    #
    # @api private
    def self.infer_type(operand)
      if operand.respond_to?(:type)
        operand.type
      else
        Types::Type.descendants.detect { |type| type.include?(operand) }
      end
    end

    # Initialize an Attribute
    #
    # @param [#to_sym] name
    #   the attribute name
    # @param [Hash] options
    #   the options for the attribute
    # @option options [Boolean] :required (true)
    #   if true, then the value cannot be nil
    #
    # @return [undefined]
    #
    # @api private
    def initialize(name, options = EMPTY_HASH)
      @name     = name.to_sym
      @required = options.fetch(:required, true)
      @type     = self.class.type
    end

    # Extract the value corresponding to this attribute from a tuple
    #
    # @example
    #   value = attribute.call(tuple)
    #
    # @param [Tuple] tuple
    #   the tuple to extract the value from
    #
    # @return [Object]
    #
    # @api public
    def call(tuple)
      tuple[self]
    end

    # Rename an attribute
    #
    # @example
    #   new_attribute = attribute.rename(new_name)
    #
    # @param [Symbol] new_name
    #   the new name to rename the attribute to
    #
    # @return [Attribute]
    #
    # @todo Make this have the same API as functions
    #
    # @api public
    def rename(new_name)
      if name.equal?(new_name)
        self
      else
        self.class.new(new_name, required: required?)
      end
    end

    # Test if the attribute is required
    #
    # @example
    #   attribute.required?  # => true or false
    #
    # @return [Boolean]
    #
    # @api public
    def required?
      @required
    end

    # Test if the attribute is optional
    #
    # @example
    #   attribute.optional?  # => true or false
    #
    # @return [Boolean]
    #
    # @api public
    def optional?
      !required?
    end

    # Test if the value matches the attribute constraints
    #
    # @example
    #   attribute.include?(value)  # => true or false
    #
    # @param [Object] value
    #   the value to test
    #
    # @return [Boolean]
    #
    # @api public
    def include?(value)
      valid_or_optional?(value, &type.method(:include?))
    end

  private

    # Test that the value is either valid or optional
    #
    # @param [Object] value
    #   the value to test
    #
    # @yield [] block representing attribute constraint
    #
    # @return [Boolean]
    #
    # @api private
    def valid_or_optional?(value)
      value.nil? ? optional? : yield(value)
    end

    # Coerce the object into an Attribute
    #
    # @param [Array] args
    #
    # @return [Attribute]
    #
    # @api private
    def coerce(*args)
      self.class.coerce(*args)
    end

  end # class Attribute
end # module Axiom