Class: SuperStruct

Inherits:
Object show all
Defined in:
lib/mega/sstruct.rb

Overview

:title: 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, ss, ss, 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 Method Summary collapse

Class Method Details

.new(*args) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/mega/sstruct.rb', line 228

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

.open(*args) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/mega/sstruct.rb', line 359

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