require 'json'

# A better struct.
class Structure

  # Mix in the Enumerable module.
  include Enumerable

  @@keys = []

  # Defines an attribute key.
  #
  # Takes a name and an optional hash of options. Available options are:
  #
  # * :type, which can be Integer, Float, String, and Array.
  #
  #    class Book
  #      key :title,   :type => String
  #      key :authors, :type => Array
  #    end
  #
  def self.key(name, options={})
    if method_defined?(name)
      raise NameError, "#{name} is already defined"
    end

    name = name.to_sym
    type = options[:type]
    @@keys << name

    module_eval do

      # Define a getter.
      define_method(name) { @attributes[name] }

      # Define a setter. The setter will optionally typecast.
      define_method("#{name}=") do |value|
        modifiable[name] =
          if type && value
            Kernel.send(type.to_s, value)
          else
            value
          end
      end
    end
  end

  # Creates a new structure.
  #
  # Optionally, populates the structure with a hash of attributes. Otherwise,
  # all values default to nil.
  def initialize(seed = {})
    @attributes =
      @@keys.inject({}) do |attributes, name|
        attributes[name] = nil
        attributes
      end

    seed.each { |key, value| self.send("#{key}=", value) }
  end

  # A hash of attributes.
  attr_reader :attributes

  def each(&block)
    @attributes.each { |value| block.call(value) }
  end

  # Returns an array populated with the attribute keys.
  def keys
    @attributes.keys
  end

  # Returns an array populated with the attribute values.
  def values
    @attributes.values
  end

  # Compares this object with another object for equality. A Structure is equal
  # to the other object when latter is also a Structure and the two objects'
  # attributes are equal.
  def ==(other)
    other.is_a?(Structure) && @attributes == other.attributes
  end

  private

  def modifiable
    begin
      @modifiable = true
    rescue
      raise TypeError, "can't modify frozen #{self.class}", caller(3)
    end
    @attributes
  end
end