diff --git a/lib/salama-object-file.rb b/lib/salama-object-file.rb index d805ad9..1234380 100644 --- a/lib/salama-object-file.rb +++ b/lib/salama-object-file.rb @@ -1,3 +1,7 @@ +# Most of the external functionality is in the writer + +# if you want some attributes not written also check volotile + require_relative "sof/util" require_relative "sof/node" require_relative "sof/simple_node" diff --git a/lib/sof/array.rb b/lib/sof/array.rb index b9ef85b..935cd36 100644 --- a/lib/sof/array.rb +++ b/lib/sof/array.rb @@ -1,13 +1,39 @@ +# If a class defines to_sof_node it tells the write that it will generate Nodes itself +# this delegates to array_to_sof_node +Array.class_eval do + def to_sof_node(writer , level , ref ) + Sof.array_to_sof_node(self , writer , level , ref ) + end +end + module Sof + # Creates a ArrayNode (see there) for the Array. + # This mainly involves creating nodes for the children + def self.array_to_sof_node(array , writer , level , ref ) + node = Sof::ArrayNode.new(ref) + array.each do |object| + node.add writer.to_sof_node( object , level + 1) + end + node + end + + # A ArrayNode is a Node for a Array. See Node for when and how nodes are used in Sof. + # A ArrayNode has a list of children that hold the value node representations for + # the arrays values. + # class ArrayNode < Node def initialize ref super(ref) @children = [] end attr_reader :children + def add c @children << c end + + # The output of a Array can be a long or a short format + # The short is used for 7 or less SimpleNodes def out io , level = 0 super short = true @@ -22,6 +48,8 @@ module Sof end private + # This defines the short output which is basically what you would write in ruby + # ie [ value1 , value2 , ...] def short_out(io,level) io.write("[") @children.each_with_index do |child , i| @@ -30,6 +58,9 @@ module Sof end io.write("]") end + + # Arrays start with the minus on each line "-" + # and each line has the value def long_out io , level indent = " " * level @children.each_with_index do |child , i| @@ -39,18 +70,4 @@ module Sof end end end - - def self.array_to_sof_node(array , writer , level , ref ) - node = Sof::ArrayNode.new(ref) - array.each do |object| - node.add writer.to_sof_node( object , level + 1) - end - node - end - -end -Array.class_eval do - def to_sof_node(writer , level , ref ) - Sof.array_to_sof_node(self , writer , level , ref ) - end end diff --git a/lib/sof/hash.rb b/lib/sof/hash.rb index 6b1e725..a52fe05 100644 --- a/lib/sof/hash.rb +++ b/lib/sof/hash.rb @@ -1,13 +1,41 @@ +Hash.class_eval do + # If a class defines to_sof_node it tells the write that it will generate Nodes itself + # this delegates to hash_to_sof_node + def to_sof_node(writer , level , ref) + Sof.hash_to_sof_node( self , writer , level , ref) + end +end + module Sof + # Creates a HashNode (see there) for the Hash. + # This mainly involves creating nodes for key value pairs + def self.hash_to_sof_node(hash,writer , level , ref) + node = Sof::HashNode.new(ref) + hash.each do |key , object| + k = writer.to_sof_node( key ,level + 1) + v = writer.to_sof_node( object ,level + 1) + node.add(k , v) + end + node + end + + # A HashNode is a Node for a Hash. See Node for when and how nodes are used in Sof. + # A HashNode has a list of children that hold the key/value node representations for + # the hashes keys and values. + class HashNode < Node def initialize ref super(ref) @children = [] end attr_reader :children + def add key , val @children << [key,val] end + + # The output of a Hash can be a long or a short format + # The short is used for 7 or less SimpleNodes def out io , level = 0 super short = true @@ -21,6 +49,10 @@ module Sof long_out(io , level) end end + + private + # This defines the short output which is basically what you would write in ruby + # ie { key1 => value1 , ... } def short_out(io,level) io.write("{") children.each_with_index do |child , i| @@ -32,6 +64,10 @@ module Sof end io.write("}") end + + # The long output is like an array of associations. + # Arrays start with the minus on each line "-" + # and each line has the association key => value, same as used for the {} syntax def long_out io , level indent = " " * level children.each_with_index do |child , i| @@ -44,20 +80,4 @@ module Sof end end end - - def self.hash_to_sof_node(hash,writer , level , ref) - node = Sof::HashNode.new(ref) - hash.each do |key , object| - k = writer.to_sof_node( key ,level + 1) - v = writer.to_sof_node( object ,level + 1) - node.add(k , v) - end - node - end -end - -Hash.class_eval do - def to_sof_node(writer , level , ref) - Sof.hash_to_sof_node( self , writer , level , ref) - end end diff --git a/lib/sof/members.rb b/lib/sof/members.rb index 13e5c30..16a4fb4 100644 --- a/lib/sof/members.rb +++ b/lib/sof/members.rb @@ -1,24 +1,40 @@ module Sof - + + # Members are members of the graph to be written + # The class collects all reachable objects into a hash for further transformation + class Members include Util - + + # initialize with the "root" object + # any object really that then becomes the root. + # the result is easier to read if it really is a root + # All reachable objects are collected into "objects" hash + # The class keeps a counter for references encountered and creates unique + # occurences for each object based on object_id (not == or ===) def initialize root @root = root @counter = 1 @objects = {} - @referenced = false add(root , 0) end - attr_reader :objects , :root , :referenced - + attr_reader :objects , :root + + private + # recursively add reachable objects from this object + # this is called from the initialize and is private def add object , level + # not storing simple (value) objects return if is_value?(object) + + # see if we we came accross this before if( occurence = @objects[object.object_id] ) #puts "reset level #{level} at #{occurence.level}" if occurence.level > level - occurence.level = level + #always store the most shallow level + occurence.level = level end + # and only one Occurence for each object, create a reference for the second occurence unless occurence.referenced #puts "referencing #{@counter} , at level #{level}/#{occurence.level} " occurence.set_reference(@counter) @@ -26,18 +42,24 @@ module Sof end return end + + # if first time see, create and store Occurence o = Occurence.new( object , level ) @objects[object.object_id] = o + + # and recursively add attributes attributes = attributes_for(object) attributes.each do |a| val = get_value( object , a) add(val , level + 1) end + # and array values if( object.is_a? Array ) object.each do |a| add(a , level + 1) end end + # and hash keys/values if( object.is_a? Hash ) object.each do |a,b| add(a , level + 1) @@ -46,4 +68,6 @@ module Sof end end end + # TODO, since this class only has one function, and one instance + # it could be merged as class functions to Occurence end diff --git a/lib/sof/node.rb b/lib/sof/node.rb index c706be0..0f745cf 100644 --- a/lib/sof/node.rb +++ b/lib/sof/node.rb @@ -1,22 +1,37 @@ # We transform objects into a tree of nodes module Sof - # abstract base class for nodes in the tree - # may be referenced (should be a simple name or number) + + # Before writing the objects are transformed into a tree of nodes. + # as the Members (all objects) are a graph (not tree) this is achieved by referencing + # + # There are only two subclasses, SimpleNode and ObejctNode, for simple or not + # The base class only holds the referenced flag + # Also nodes must implement the out function + class Node include Util + # only has one attribute, the referenced flag + # This could be anything, but we use a simple number counter which is handled in the Members + # class during construction of the occurrence hash def initialize ref + #puts "node has ref #{self.class}:#{ref}" if ref @referenced = ref end + attr_reader :referenced + # must be able to output to a stream + # This function must be called as super as it handles possible reference marker "& num" def out io ,level io.write "&#{@referenced} " if @referenced end + + # helper function to return the output as a string + # ie creates stringio, calls out and returns the string def as_string(level) io = StringIO.new out(io,level) io.string end - attr_reader :referenced end end diff --git a/lib/sof/object_node.rb b/lib/sof/object_node.rb index 0b5c2da..f1c7e9a 100644 --- a/lib/sof/object_node.rb +++ b/lib/sof/object_node.rb @@ -2,18 +2,27 @@ module Sof # ObjectNode means node with structure - # ie arrays and hashes get transformed into these too + # ie arrays and hashes get transformed into these too, as well as objects with attributes class ObjectNode < Node + + # init with a string, much like a simple node + # structure is added after construction and kept in a children array def initialize data , ref super(ref) @data = data @children = [] end attr_reader :children , :data + + # children array hold key value pairs def add k , v @children << [k,v] end + + # write out at the given level + # level determines the indentation (level * space) + # write out the data and then the children (always key value on one line) def out io , level = 0 super io.write(@data) diff --git a/lib/sof/occurence.rb b/lib/sof/occurence.rb index 225cde9..c208e36 100644 --- a/lib/sof/occurence.rb +++ b/lib/sof/occurence.rb @@ -1,5 +1,9 @@ module Sof + # simple struct like class to wrap an object and hold additionally + # - the shallowest level at which it was seen + # - A possible reference + # - the fact if it has been written (for referenced objects) class Occurence def initialize object , level @object = object diff --git a/lib/sof/simple_node.rb b/lib/sof/simple_node.rb index acc43bc..b915503 100644 --- a/lib/sof/simple_node.rb +++ b/lib/sof/simple_node.rb @@ -1,19 +1,25 @@ -# We transform objects into a tree of nodes - module Sof # What makes a node simple is that it has no further structure # + # This may mean number/string/symbol, but also tiny arrays or objects with + # very little attributes. In other words simple/object is not the same distinction + # as value/not value + class SimpleNode < Node + + # data is a string that is written out in "out" function def initialize data , ref = nil super(ref) @data = data end attr_reader :data + + # just write the data given in construcor. simple. hence the name. def out io , level super(io,level) io.write(data) end end - + end diff --git a/lib/sof/util.rb b/lib/sof/util.rb index abd9367..4cd5dfe 100644 --- a/lib/sof/util.rb +++ b/lib/sof/util.rb @@ -1,5 +1,10 @@ module Sof + + # module for a couple of helpers that are needed in Members and Writer + module Util + # "value" is a property meaning simple/ not further structure + # hence int/bool/string etc are values def is_value? o return true if [true , false , nil].include?(o) return true if [Fixnum, Symbol, String].include?(o.class) @@ -9,13 +14,22 @@ module Sof return false end + # extract an attribute by the given name from the object + # done with instance_variable_get def get_value(object,name) object.instance_variable_get "@#{name}".to_sym end + # return a list of attributes for a given object + # attributes may be supressed with Volotile + # TODO should be able to specify order too def attributes_for object Sof::Util.attributes(object) end + + # return a list of attributes for a given object + # attributes may be supressed with Volotile + # TODO should be able to specify order too def self.attributes( object ) atts = object.instance_variables.collect{|i| i.to_s[1..-1].to_sym } # chop of @ atts - Volotile.attributes(object.class) diff --git a/lib/sof/volotile.rb b/lib/sof/volotile.rb index 704a9f8..7226f20 100644 --- a/lib/sof/volotile.rb +++ b/lib/sof/volotile.rb @@ -1,11 +1,27 @@ module Sof - class Volotile + + # Volotile module keeps track of attributes that are not menat to be written + # The idea being similar to private methods. So not every little detail is relevant + # for the object. Some attribuets may be calculated, cached etc, + # + # There is only one useful call for the user, "add" attributes for a given class + # + module Volotile @@mapping = { } - def self.attributes clazz - @@mapping[clazz] || [] - end + + # Add attributes that are then ommited from the sof writing process + # The clazz is the real class object (eg String), and thus the + # adding must happen after the class definition, often at the end of the file + # attributes are an array of Symbols def self.add clazz , attributes @@mapping[clazz] = attributes end + + private + # return the volotile attributes as an array (or empty array) + def self.attributes clazz + @@mapping[clazz] || [] + end + end end diff --git a/lib/sof/writer.rb b/lib/sof/writer.rb index beb82ff..484f2ca 100644 --- a/lib/sof/writer.rb +++ b/lib/sof/writer.rb @@ -1,10 +1,29 @@ module Sof + + # this function writes the object (and all reachable objects) out as sof + # and returns a string + # For trees or graphs this works best by handing roots + # Internally this is done in three steps: + # - All reachable objects are collected, these are called Occurences and the Members class does + # the collecting. Members holds a hash of occurences + # - A tree of nodes is created from the occurences. Different node classes for different classes + # - The nodes are witten to a steam + def self.write object + writer = Writer.new(Members.new(object) ) + writer.write + end + + # The writer does the coordinating work of the stages (see write function) class Writer include Util + + # Initialized with the Members (hash of occurences, see there) def initialize members @members = members end + # main function, creates nodes from the occurences and writes the nodes to a string + # returns the sof formatted string for all objects def write node = to_sof_node(@members.root , 0) io = StringIO.new @@ -12,6 +31,12 @@ module Sof io.string end + # create a Node (subclass) for an object at a given level. + # Level is mainly needed for the indenting + # from the object we get the Occurence and decide wether a reference node is needed + # simple objects (with more inner structure) become SimpleNodes + # Any structured object bocomes a ObjectNode + # Hash and Array create their own nodes via to_sof_node functions on the classes def to_sof_node(object , level) if is_value?(object) return SimpleNode.new(object.to_sof()) @@ -35,6 +60,10 @@ module Sof end end + # create an object node from the object + # simple nodes are returned for small objects + # small means only simple attributes and only 30 chars of them + # object nodes are basically arrays (see there) def object_sof_node( object , level , ref) if( object.is_a? Class ) return SimpleNode.new( object.name , ref ) @@ -55,11 +84,5 @@ module Sof end node end - - def self.write object - writer = Writer.new(Members.new(object) ) - writer.write - end - end end diff --git a/test/helper.rb b/test/helper.rb index 359dfa0..83bb5ea 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -17,7 +17,7 @@ require 'salama-object-file' module Checker def check should - out = Sof::Writer.write(@out) + out = Sof.write(@out) same = (should == out) puts "Shouldda\n#{out}" unless same assert_equal should , out