latest design (12/2004)

At the lowest level, the “Accessors” sub-module contains reader and creator functions that correspond to the various types of path elements (elt_name, @attr_name, elt_name etc.) that xml-xxpath supports. A reader function gets an array of nodes and the search parameters corresponding to its path element type (e.g. elt_name, attr_name, attr_value) and returns an array with all matching direct sub-nodes of any of the supplied nodes. A creator function gets one node and the search parameters and returns the created sub-node.

An XPath expression <things1>/<things2>/.../<thingsx> is compiled into a bunch of nested closures, each of which is responsible for a specific path element and calls the corresponding accessor function:

The all function is then trivially implemented on top of this:

def all(node,options={})
  raise "options not a hash" unless Hash===options
  if options[:create_new]
    return [ @creator_procs[-1].call(node,true) ]
  else
    last_nodes,rest_creator = catch(:not_found) do
      return @reader_proc.call([node])
    end
    if options[:ensure_created]
      [ rest_creator.call(last_nodes[0],false) ]
    else
      []
    end
  end
end

…and first, create_new etc. are even more trivial frontends to that.

The implementations of the @creator_procs look like this:

@creator_procs[0] =
  proc{|node,create_new| node}

@creator_procs[1] =
  proc {|node,create_new|
    @creator_procs[0].call(Accessors.create_subnode_by_<thingsx>(node,create_new,<thingsx>),
                           create_new)
  }

@creator_procs[2] =
  proc {|node,create_new|
    @creator_procs[1].call(Accessors.create_subnode_by_<thingsx-1>(node,create_new,<thingsx-1>),
                           create_new)
  }

...

@creator_procs[n] =
  proc {|node,create_new|
    @creator_procs[n-1].call(Accessors.create_subnode_by_<things[x+1-n]>(node,create_new,<things[x+1-n]>),
                             create_new)
  }

...
@creator_procs[x] =
  proc {|node,create_new|
    @creator_procs[x-1].call(Accessors.create_subnode_by_<things1>(node,create_new,<things1>),
                             create_new)
  }

..and the implementation of @reader_proc looks like this:

@reader_proc = rpx where

rp0 = proc {|nodes| nodes}

rp1 = proc {|nodes|
              next_nodes = Accessors.subnodes_by_<thingsx>(nodes,<thingsx>)
              if (next_nodes == [])
                throw :not_found, [nodes,@creator_procs[1]]
              else
                rp0.call(next_nodes)
              end
            }

rp2 = proc {|nodes|
              next_nodes = Accessors.subnodes_by_<thingsx-1>(nodes,<thingsx-1>)
              if (next_nodes == [])
                throw :not_found, [nodes,@creator_procs[2]]
              else
                rp1.call(next_nodes)
              end
            }
...

rpx = proc {|nodes|
              next_nodes = Accessors.subnodes_by_<things1>(nodes,<things1>)
              if (next_nodes == [])
                throw :not_found, [nodes,@creator_procs[x]]
              else
                rpx-1.call(next_nodes)
              end
            }