#-- # SuperStruct # Copyright (c) 2005 Hal Fulton # # Ruby License # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. #++ # = 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. # # == What It's Like # # It's like a Struct... # - you can pass in a list of symbols for accessors # - it will create a class for you # but... # - you don't have to pass in the class name # - it returns a "real" class # . instance variables have the expected names # . you can reopen and add methods # - it doesn't go into the Struct:: namespace # - it preserves the order of the fields # - you can use Strings instead of Symbols for the names # # It's like an Array... # - you can access the items by [number] and [number]= # but... # - you can also access the items by ["name"] and ["name"]= # - you can access the items by accessors # # It's like an OpenStruct... # - (if you use .open instead of .new) you can add fields # automatically with x.field or x.field=val # but... # - you can initialize it like a Struct # - it preserves the order of the fields # # It's like a Hash... # - data can be accessed by ["name"] # but... # - order (of entry or creation) is preserved # - arbitrary objects are not allowed (it does obj.to_str or obj.to_s) # - strings must be valid method names # # It's like Ara Howard's Named Array... # - we can access elements by ["name"] or ["name"]= # but... # - you can access the items by accessors # - strings must be valid method names # # It's like Florian Gross's Keyed List... # (to be done) # but... # - it preserves the order of the fields # # == Usage # # # Need not assign to existing fields (default to nil) # myStruct = SuperStruct.new(:alpha) # x = myStruct.new # x.alpha # nil # # # A value assigned at construction may be retrieved # myStruct = SuperStruct.new(:alpha) # x = myStruct.new(234) # x.alpha # 234 # # # Unassigned fields are nil # myStruct = SuperStruct.new(:alpha,:beta) # x = myStruct.new(234) # x.beta # nil # # # An open structure may not construct with nonexistent fields # myStruct = SuperStruct.open # x = myStruct.new(234) # error # # # An open structure may assign fields not previously existing # myStruct = SuperStruct.open # x = myStruct.new # x.foo = 123 # x.bar = 456 # # # The act of retrieving a nonexistent field from an open struct will # # create that field # myStruct = SuperStruct.open # x = myStruct.new # x.foo # nil # # # A field (in an open struct) that is unassigned will be nil # myStruct = SuperStruct.open # x = myStruct.new # y = x.foobar # # # A struct created with new rather than open cannot reference nonexistent # # fields # myStruct = SuperStruct.new # x = myStruct.new # x.foo # error # # # Adding a field to a struct will create a writer and reader for that field # # # An open struct will also create a writer and a reader together # # # A field has a real writer and reader corresponding to it # # # A string will work as well as a symbol # myStruct = SuperStruct.new("alpha") # # # to_a will return an array of values # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # assert(x.to_a == [7,8,9]) # # # Instance method 'members' will return a list of members (as strings) # myStruct = SuperStruct.new(:alpha,"beta") # x = myStruct.new # assert_equal(["alpha","beta"],x.members) # # # Class method 'members' will return a list of members (as strings) # myStruct = SuperStruct.new(:alpha,"beta") # assert_equal(["alpha","beta"],myStruct.members) # # # to_ary will allow a struct to be treated like an array in # # multiple assignment # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # a,b,c = x # assert(b == 8) # # # to_ary will allow a struct to be treated like an array in # # passed parameters # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # b = meth(*x) # # # to_hash will return a hash with fields as keys # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # h = x.to_hash # assert_equal({"alpha"=>7,"beta"=>8,"gamma"=>9},h) # # # A field name (String) may be used in a hash-like notation # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # y = x["beta"] # # # A field name (Symbol) may be used in a hash-like notation # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # y = x[:beta] # # # [offset,length] may be used as for arrays # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # y = x[0,2] # # # Ranges may be used as for arrays # myStruct = SuperStruct.new("alpha","beta","gamma") # x = myStruct.new(7,8,9) # y = x[1..2] # # # Adding a field to an open struct adds it to the instance # myStruct = SuperStruct.open(:alpha) # x = myStruct.new # x.beta = 5 # # # Adding a field to an open struct adds it to the class also # myStruct = SuperStruct.open(:alpha) # x = myStruct.new # x.beta = 5 # # # An array passed to SuperStruct.new need not be starred # myStruct = SuperStruct.new(%w[alpha beta gamma]) # x = myStruct.new # # # A hash passed to #set will set multiple values at once # myStruct = SuperStruct.new(%w[alpha beta gamma]) # x = myStruct.new # hash = {"alpha"=>234,"beta"=>345,"gamma"=>456} # x.set(hash) # # # ||= works properly # x = SuperStruct.open.new # x.foo ||= 333 # x.bar = x.bar || 444 # # # attr_tester will create a ?-method # myStruct = SuperStruct.new(:alive) # myStruct.attr_tester :alive # x = myStruct.new(true) # x.alive? # true # # # == Author(s) # # * Hal Fulton # 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 # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin testing # # SuperStruct test code # Hal Fulton # Version 1.0.1 (in sync with library code) # License: The Ruby License # require "test/unit" class Tester < Test::Unit::TestCase def test001 # Must pass in String or Symbol assert_raises(ArgumentError) { SuperStruct.new(0) } end def test002 # Must pass in valid name(s) assert_raises(ArgumentError) { SuperStruct.new("###") } end def test003 # Can't assign to nonexistent fields myStruct = SuperStruct.new assert_raises(ArgumentError) { myStruct.new(345) } end def test004 # Need not assign to existing fields (default to nil) myStruct = SuperStruct.new(:alpha) assert_nothing_raised(ArgumentError) { myStruct.new } end def test005 # A value assigned at construction may be retrieved myStruct = SuperStruct.new(:alpha) x = myStruct.new(234) assert(x.alpha == 234) end def test006 # Unassigned fields are nil myStruct = SuperStruct.new(:alpha,:beta) x = myStruct.new(234) assert(x.beta == nil) end def test007 # An open structure still may not construct with nonexistent fields myStruct = SuperStruct.open assert_raises(ArgumentError) { x = myStruct.new(234) } end def test008 # An open structure may assign fields not previously existing myStruct = SuperStruct.open x = myStruct.new assert_nothing_raised { x.foobar = 123 } end def test009 # A field assigned to an open struct after its construction may be retrieved myStruct = SuperStruct.open x = myStruct.new x.foobar = 123 assert(x.foobar == 123) end def test010 # The act of retrieving a nonexistent field from an open struct will # create that field myStruct = SuperStruct.open x = myStruct.new assert_nothing_raised { y = x.foobar } end def test011 # A field (in an open struct) that is unassigned will be nil myStruct = SuperStruct.open x = myStruct.new y = x.foobar assert(y == nil) end def test012 # A struct created with new rather than open cannot reference nonexistent # fields myStruct = SuperStruct.new x = myStruct.new assert_raises(NoMethodError) { y = x.foobar } end def test013 # Adding a field to a struct will create a writer and reader for that field myStruct = SuperStruct.new(:alpha) x = myStruct.new x.send(:alpha=,1) assert(x.alpha == 1) end def test014 # Only a single value may be passed to a writer (for code coverage) myStruct = SuperStruct.new(:alpha) x = myStruct.new assert_raises(ArgumentError) { x.send(:alpha=,1,2) } end def test015 # An open struct will also create a writer and a reader together myStruct = SuperStruct.open x = myStruct.new x.send(:alpha=,1) assert(x.alpha == 1) end def test016 # Only a single value may be passed to a writer (for code coverage) myStruct = SuperStruct.open x = myStruct.new assert_raises(ArgumentError) { x.send(:alpha=,1,2) } end def test017 # A field has a real writer and reader corresponding to it myStruct = SuperStruct.new(:alpha) x = myStruct.new assert(myStruct.instance_methods.include?("alpha")) assert(myStruct.instance_methods.include?("alpha=")) end def test018 # Creating a field by retrieval in an open struct will NOT create a writer # (This behavior has changed!) myStruct = SuperStruct.open x = myStruct.new y = x.alpha assert(myStruct.instance_methods.include?("alpha")) assert(!myStruct.instance_methods.include?("alpha=")) end def test019 # Creating a field by writing in an open struct will NOT create a reader # (This behavior has changed!) myStruct = SuperStruct.open x = myStruct.new x.alpha = 5 assert(myStruct.instance_methods.include?("alpha=")) assert(!myStruct.instance_methods.include?("alpha")) end def test020 # A string will work as well as a symbol myStruct = SuperStruct.new("alpha") x = myStruct.new assert(myStruct.instance_methods.include?("alpha")) assert(myStruct.instance_methods.include?("alpha=")) end def test021 # to_a will return an array of values myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) assert(x.to_a == [7,8,9]) end def test022 # Instance method 'members' will return a list of members (as strings) myStruct = SuperStruct.new(:alpha,"beta") x = myStruct.new assert_equal(["alpha","beta"],x.members) end def test023 # Class method 'members' will return a list of members (as strings) myStruct = SuperStruct.new(:alpha,"beta") assert_equal(["alpha","beta"],myStruct.members) end def test024 # to_ary will allow a struct to be treated like an array in # multiple assignment myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) a,b,c = x assert(b == 8) end def aux025(*arr) # Just used in test 25 arr[1] end def test025 # to_ary will allow a struct to be treated like an array in # passed parameters myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) b = aux025(*x) assert(b == 8) end def test026 # to_hash will return a hash with fields as keys myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) h = x.to_hash assert_equal({"alpha"=>7,"beta"=>8,"gamma"=>9},h) end def test027 # A field name (String) may be used in a hash-like notation myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) y = x["beta"] assert(8,y) end def test028 # A field name (Symbol) may be used in a hash-like notation myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) y = x[:beta] assert(8,y) end def test029 # [offset,length] may be used as for arrays myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) y = x[0,2] assert([7,8],y) end def test030 # Ranges may be used as for arrays myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) y = x[1..2] assert([8,9],y) end def test031 # Adding a field to an open struct adds it to the instance myStruct = SuperStruct.open(:alpha) x = myStruct.new x.beta = 5 assert_equal(["alpha","beta"],x.members) end def test032 # Adding a field to an open struct adds it to the class also myStruct = SuperStruct.open(:alpha) x = myStruct.new x.beta = 5 assert_equal(["alpha","beta"],myStruct.members) end def test033 # An array passed to SuperStruct.new need not be starred myStruct = SuperStruct.new(%w[alpha beta gamma]) x = myStruct.new assert_equal(%w[alpha beta gamma],x.members) end def xtest034 # A hash passed to SuperStruct.new will initialize the values # (but order will not be predictable!) assert false, "Not implemented yet." end def test035 # A hash passed to #set will set multiple values at once myStruct = SuperStruct.new(%w[alpha beta gamma]) x = myStruct.new hash = {"alpha"=>234,"beta"=>345,"gamma"=>456} x.set(hash) assert_equal([234,345,456], x.to_a) end def test036 # Make sure ||= works properly x = SuperStruct.open.new x.foo ||= 333 x.bar = x.bar || 444 assert_equal(333,x.foo) assert_equal(444,x.bar) end def test037 # A simple array index works ok myStruct = SuperStruct.new("alpha","beta","gamma") x = myStruct.new(7,8,9) assert_equal(7,x[0]) assert_equal(8,x[1]) assert_equal(9,x[2]) end def test038 # attr_tester will create a ?-method klass = SuperStruct.new(:alpha,:beta,:gamma) klass.attr_tester :alpha, :gamma x = klass.new(22,33,nil) assert(x.alpha?) assert_raises(NoMethodError) { x.beta? } assert(! x.gamma?) end def test039 # attr_tester works with open() (?-methods not created) klass = SuperStruct.open(:alpha,:beta,:gamma) klass.attr_tester :alpha, :gamma x = klass.new(22,33,nil) assert(x.alpha?) assert_raises(NoMethodError) { x.beta? } # ?-methods are not automatic assert(! x.gamma?) end end =end