Class: Moxml::Plan

Inherits:
Object
  • Object
show all
Defined in:
lib/moxml/plan.rb

Overview

Plan-compiled materialization: declare the document shape once, execute it against the adapter's bulk row stream — no wrapper tree, no NodeSet, no per-node contract dispatch. The compiled grammar instead of the interpreted walk.

plan = Moxml::Plan.new do
on("record") { |attrs, _text, fields| Record.new(attrs["id"], attrs["kind"], fields) }
on("field")  { |attrs, text, _kids| Field.new(attrs["name"], attrs["unit"], text) }
end
records = plan.parse(xml, ctx)

Handlers run bottom-up as each element completes; children is the array of handler values for matched child elements whose parent ALSO matched (values of unmatched parents are dropped — matching a parent is the consumer's decision to keep a subtree). attrs is a Hash of the element's attributes (local name => value); text is the first text child's content, or nil.

Name matching is global (flat table, not path-relative): element names unique per role — the dominant consumer shape — work as-is; distinct roles sharing a name need distinct documents or a rename upstream.

Adapters with a bulk stream answer plan_rows (leptris: the engine's one-pass C snapshot — pre-order, first-text on the row); the rest run the generic pre-order wrapper walk below, emitting the identical stream — spec-pinned equal.

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Plan

Returns a new instance of Plan.



32
33
34
35
# File 'lib/moxml/plan.rb', line 32

def initialize(&block)
  @handlers = {}
  instance_eval(&block) if block
end

Instance Method Details

#materialize(node) ⇒ Object

Materializes the plan against a document (or element) wrapper. Returns the values of top-level matched elements.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/moxml/plan.rb', line 53

def materialize(node)
  results = []
  frames = []   # per open depth: children values (nil = unmatched)
  meta = []     # per open depth: [name, attrs, text] (nil = unmatched)

  close_top = lambda do
    m = meta.pop
    children = frames.pop
    next unless m

    value = @handlers[m[0]].call(m[1], m[2], children)
    if frames.empty? || frames.last.nil?
      results << value
    else
      frames.last << value
    end
  end

  row = lambda do |name, attrs_pairs, text, depth|
    close_top.call while frames.size > depth
    handler = @handlers[name]
    if handler
      attrs = {}
      i = 0
      while i < attrs_pairs.length
        attrs[attrs_pairs[i]] = attrs_pairs[i + 1]
        i += 2
      end
      frames << []
      meta << [name, attrs, text]
    else
      frames << nil
      meta << nil
    end
  end

  adapter = node.context.config.adapter
  ran = adapter.plan_rows(node.native) do |name, pairs, text, depth|
    row.call(name, pairs, text, depth)
  end
  unless ran
    walk_wrappers(node) do |name, pairs, text, depth|
      row.call(name, pairs, text, depth)
    end
  end

  close_top.call while meta.any?
  results
end

#on(name, &handler) ⇒ Object

Registers a handler for elements named name (local name). Returns self so registrations chain.

Raises:



39
40
41
42
43
44
# File 'lib/moxml/plan.rb', line 39

def on(name, &handler)
  raise ArgumentError, "on(#{name.inspect}) requires a block" unless handler

  @handlers[name] = handler
  self
end

#parse(xml, context) ⇒ Object

Parses xml and materializes the plan against it.



47
48
49
# File 'lib/moxml/plan.rb', line 47

def parse(xml, context)
  materialize(context.parse(xml))
end