elgalu/strongly_typed

View on GitHub
lib/strongly_typed/attributes.rb

Summary

Maintainability
A
35 mins
Test Coverage
module StronglyTyped
  module Attributes

    # Create attribute accesors for the included class
    #   Also validations and coercions for the type specified
    #
    # @param [Symbol] name the accessor name
    # @param [Class] type the class type to use for validations and coercions
    #
    # @example
    #   class Person
    #     include StronglyTyped::Model
    #
    #     attribute :id, Integer
    #     attribute :slug, String
    #   end
    #
    #   Person.new(id: 1, slug: 'elgalu')
    #   #=> #<Person:0x00c98 @id=1, @slug="elgalu">
    #   leo.id   #=> 1
    #   leo.slug #=> "elgalu"
    def attribute(name, type=Object)
      name = name.to_sym #normalize

      raise NameError, "attribute `#{name}` already created" if members.include?(name)
      raise TypeError, "second argument, type, must be a Class but got `#{type.inspect}` insted" unless type.is_a?(Class)
      raise TypeError, "directly converting to Bignum is not supported, use Integer instead" if type == Bignum

      new_attribute(name, type)
    end

    # Memoized hash storing keys for names & values for types pairs
    #
    # @return [Hash] attributes
    def attributes
      @attributes ||= {}
    end

    # Returns the attribute names created with attribute()
    #
    # @return [Array<Symbol>] all the attribute names
    def members
      attributes.keys
    end

    private

    # Add new attribute for the tiny object modeled
    #
    # @param [Symbol] name the attribute name
    # @param [Class] type the class type
    #
    # @private
    def new_attribute(name, type)
      attributes[name] = type
      define_attr_reader(name)
      define_attr_writer(name, type)
      name
    end

    # Define attr_reader method for the new attribute
    #
    # @param [Symbol] name the attribute name
    #
    # @private
    def define_attr_reader(name)
      define_method(name) do
        instance_variable_get("@#{name}")
      end
    end

    # Define attr_writer method for the new attribute
    #   with the added feature of validations and coercions.
    #
    # @param [Symbol] name the attribute name
    # @param [Class] type the class type
    #
    # @raise [TypeError] if unable to coerce the value
    #
    # @private
    def define_attr_writer(name, type)
      define_method("#{name}=") do |value|
        unless value.kind_of?(type)
          value = coerce(value, to: type)
          unless value.kind_of?(type)
            raise TypeError, "Attribute `#{name}` only accepts `#{type}` but got `#{value}`:`#{value.class}` instead"
          end
        end
        instance_variable_set("@#{name}", value)
      end
    end

  end
end