require 'ostruct'

class OpenStruct

  # Allows the initialization of an OpenStruct with a setter block:
  #
  #   person = OpenStruct.new do |o|
  #     o.name    = 'John Smith'
  #     o.gender  = :M
  #     o.age     = 71
  #   end
  #
  # You can still provide a hash for initialization purposes, and even combine
  # the two approaches if you wish.
  #
  #   person = OpenStruct.new(:name => 'John Smith', :age => 31) do |p|
  #     p.gender = :M
  #   end
  #
  # Alternatively you can provide a default block:
  #
  #   stuff = OpenStruct.new{ |o,k| o[k] = [] }
  #   stuff.place << :a
  #   stuff.place << :b
  #   stuff.place #=> [:a, :b]
  #
  # A setter block versus a defualt block is determined by the arity of
  # the block. You can not provide both at the same time.
  #
  # CREDIT: Noah Gibbs, Gavin Sinclair
  def initialize(hash=nil, &block)
    if block && block.arity==2
      @table = Hash.new(&block)
    else
      @table = {}
    end
    if hash
      for k,v in hash
        @table[k.to_sym] = v
        new_ostruct_member(k)
      end
    end
    if block && block.arity==1
      yield self
    end
  end

  #
  def each(&blk)
    @table.each(&blk)
  end

  #
  def to_h
    @table.dup
  end

  # Access a value in the OpenStruct by key, like a Hash.
  # This increases OpenStruct's "duckiness".
  #
  #   o = OpenStruct.new
  #   o.t = 4
  #   o['t']  #=> 4
  #
  def [](key)
    key = key.to_sym unless key.is_a?(Symbol)
    @table[key]
  end

  # Set a value in the OpenStruct by key, like a Hash.
  #
  #   o = OpenStruct.new
  #   o['t'] = 4
  #   o.t  #=> 4
  #
  def []=(key,val)
    raise TypeError, "can't modify frozen #{self.class}", caller(1) if self.frozen?
    key = key.to_sym unless key.is_a?(Symbol)
    @table[key]=val
  end

  # Provides access to an OpenStruct's inner table.
  #
  #   o = OpenStruct.new
  #   o.a = 1
  #   o.b = 2
  #   o.instance_delegate.map { |k, v| "#{k} #{v}" }
  #   #=> ["a 1", "b 2"]
  #
  def instance_delegate
    @table
  end

  #
  alias ostruct_delegate instance_delegate

  # Insert/update hash data on the fly.
  #
  #   o = OpenStruct.new
  #   o.ostruct_update(:a => 2)
  #   o.a  #=> 2
  #
  def ostruct_update(other)
    raise TypeError, "can't modify frozen #{self.class}", caller(1) if self.frozen?
    ##other = other.to_hash  #to_h ?
    for k,v in other
      @table[k.to_sym] = v
    end
    self
  end

  # Merge hash data creating a new OpenStruct object.
  #
  #   o = OpenStruct.new
  #   x = o.ostruct_merge(:a => 2)
  #   x.a  #=> 2
  #
  def ostruct_merge(other)
    o = dup
    o.ostruct_update(other)
    o
  end

  #--
  # TO BE DEPRECATED
  # Must consider that accessing instance_delegate instead can be dangerous.
  # Might we us a Functor to ensure the table keys are always symbols?
  #++

  # Insert/update hash data on the fly.
  #
  #   o = OpenStruct.new
  #   o.ostruct_update(:a => 2)
  #   o.a  #=> 2
  #
  def __update__(other)
    raise TypeError, "can't modify frozen #{self.class}", caller(1) if self.frozen?
    ##other = other.to_hash #to_h?
    for k,v in other
      @table[k.to_sym] = v
    end
    self
  end

  # Merge hash data creating a new OpenStruct object.
  #
  #   o = OpenStruct.new
  #   x = o.ostruct_merge(:a => 2)
  #   x.a  #=> 2
  #
  def __merge__(other)
    o = dup
    o.__update__(other)
    o
  end

  #
  def to_ostruct
    self
  end

end


class Hash

  # Turns a hash into a generic object using an OpenStruct.
  #
  #   o = {'a' => 1}.to_ostruct
  #   o.a  #=> 1
  #
  def to_ostruct
    OpenStruct.new(self)
  end

  # Like to_ostruct but recusively objectifies all hash elements as well.
  #
  #   o = {'a' => { 'b' => 1 }}.to_ostruct_recurse
  #   o.a.b  #=> 1
  #
  # The +exclude+ parameter is used internally to prevent infinite
  # recursion and is not intended to be utilized by the end-user.
  # But for more advance use, if there is a particular subhash you
  # would like to prevent from being converted to an OpoenStruct
  # then include it in the +exclude+ hash referencing itself. Eg.
  #
  #   h = { 'a' => { 'b' => 1 } }
  #   o = h.to_ostruct_recurse( { h['a'] => h['a'] } )
  #   o.a['b']  #=> 1
  #
  # CREDIT: Alison Rowland, Jamie Macey, Mat Schaffer
  def to_ostruct_recurse(exclude={})
    return exclude[self] if exclude.key?( self )
    o = exclude[self] = OpenStruct.new
    h = self.dup
    each_pair do |k,v|
      h[k] = v.to_ostruct_recurse( exclude ) if v.respond_to?(:to_ostruct_recurse)
    end
    o.__update__(h)
  end
end

class NilClass
  # Create an empty OpenStruct object.
  def to_ostruct
    OpenStruct.new
  end
end

# Copyright (c) 2005 Thomas Sawyer (Ruby License)
