# 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] # # @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] # # @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