=begin rdoc

= SuperStruct

This is an easy way to create Struct-like classes; it converts easily
between hashes and arrays, and it allows OpenStruct-like dynamic naming
of members.

Unlike Struct, it creates a "real" class, and it has real instance variables 
with predictable names.

A basic limitation is that the hash keys must be legal method names (unless
used with send()).

Basically, ss["alpha"], ss[:alpha], ss[0], and ss.alpha all mean the same.

== Usage


== Author(s)

* Hal Fulton

== Legal

Ruby License (see incuded file)

=end


class SuperStruct

  def SuperStruct.new(*args)
    @table = []
    @setsyms = []        # Setter symbols
    klass = Class.new
    if (args.size == 1) && (args[0].is_a? Array)
      args = args[0]
    end
    strs = args.map {|x| x.to_s }
    args.each_with_index do |k,i|
      case
	when (! [String,Symbol].include? k.class)
	  raise ArgumentError, "Need a String or Symbol"
        when (strs[i] !~ /[_a-zA-Z][_a-zA-Z0-9]*/)
          raise ArgumentError, "Illegal character"
      end
      k = k.intern if k.is_a? String
      @table << k
      @setsyms << (k.to_s + "=").intern
      klass.instance_eval { attr_accessor k }
    end

    setsyms = @setsyms
    table   = @table
    vals    = @vals
    klass.class_eval do
      attr_reader :singleton
      define_method(:initialize) do |*vals|
        n = vals.size
	m = table.size
	case 
	  when n < m
	    # raise ArgumentError, "Too few arguments (#{n} for #{m})"
	    # Never mind... extra variables will just be nil
	  when n > m
	    raise ArgumentError, "Too many arguments (#{n} for #{m})"
	end
        setsyms.each_with_index do |var,i|
          self.send(var,vals[i])
        end
      end
      define_method(:pretty_print) do |q|  # pp.rb support
        q.object_group(self) do
          q.seplist(self.members, proc { q.text "," }) do |member|
#         self.members.each do |member|
#           q.text ","  # unless q.first?
            q.breakable
            q.text member.to_s
            q.text '='
            q.group(1) do
              q.breakable ''
              q.pp self[member]
            end
          end
        end
      end
      define_method(:inspect) do
        str = "#<#{self.class}:"
        table.each {|item| str << " #{item}=#{self.send(item)}" }
        str + ">"
      end
      define_method(:[]) do |*index|
        case index.map {|x| x.class }
	  when [Fixnum]
            self.send(table[*index])
          when [Fixnum,Fixnum], [Range]
	    table[*index].map {|x| self.send(x)}
	  when [String]
	    self.send(index[0].intern)
	  when [Symbol]
	    self.send(index[0])
        else
          raise ArgumentError,"Illegal index"
	end
      end
      define_method(:[]=) do |*index|
        value = index[-1]
        index = index[0..-2]
        case index.map {|x| x.class }
	  when [Fixnum]
            self.send(table[*index])
          when [Fixnum,Fixnum], [Range]
	    setsyms[*index].map {|x| self.send(x,value) }
	  when [String]
	    self.send(index[0].intern,value)
	  when [Symbol]
	    self.send(index[0],value)
        else
          raise ArgumentError,"Illegal index"
	end
      end
      define_method(:to_a)    { table.map {|x| eval("@"+x.to_s) } }
      define_method(:to_ary)  { to_a }
      define_method(:members) { table.map {|x| x.to_s } }
      define_method(:to_struct) do
        mems = table
        Struct.new("TEMP",*mems)
        # Struct::TEMP.new(*vals) # Why doesn't this work??
        data = mems.map {|x| self.send(x) }
        Struct::TEMP.new(*data)
      end
      define_method(:to_hash) do
        hash = {}
	table.each do |mem|
          mem = mem.to_s
          hash.update(mem => self.send(mem))
        end
	hash
      end
      define_method(:set) {|h| h.each_pair {|k,v| send(k.to_s+"=",v) } }

      # Class methods...

      @singleton = class << self
        self
      end

      @singleton.instance_eval do
        define_method(:members) do 
	  table.map {|x| x.to_s }
	end
        me = self
        define_method(:attr_tester) do |*syms| 
          syms.each {|sym| alias_method(sym.to_s+"?",sym) }
        end
      end

    end
    klass
  end


  def SuperStruct.open(*args)
    klass = SuperStruct.new(*args)
    table = @table
    setsyms = @setsyms
    table = @table
    klass.class_eval do
      define_method(:method_missing) do |meth, *args|
        mname = meth.id2name
        if mname =~ /=$/
          getter = mname.chop
          setter = mname
        elsif mname =~ /\?$/
          raise NoMethodError  # ?-methods are not created automatically
        else
          getter = mname
          setter = mname + "="
        end
        gsym = getter.intern
        ssym = setter.intern
        ivar = "@" + getter
        setsyms << setter
        table << getter
        len = args.length
        if mname == getter
          klass.class_eval do                 # getter
            define_method(getter) do
              instance_variable_get(ivar)
            end
          end
        else
          klass.class_eval do                 # setter
            define_method(setter) do |*args|
              if len != 1
                raise ArgumentError, "Wrong # of arguments (#{len} for 1)", 
                      caller(1)
              end
              instance_variable_set(ivar,args[0])
              instance_variable_get(ivar)
            end
          end
        end
        if mname == setter
          self.send(setter,*args)
        else
          if len == 0
            self.send(getter)
          else
            raise NoMethodError, "Undefined method '#{mname}' for #{self}", 
                  caller(1)
          end
        end
      end
    end
    klass
  end

end


require "test-sstruct" if $0 == __FILE__