moxml 0.5.59 → 0.5.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/moxml/adapter/base.rb +9 -0
- data/lib/moxml/adapter/leptris.rb +44 -0
- data/lib/moxml/plan.rb +133 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/plan_spec.rb +102 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70fbb27b6a314c772f06699958b27c017a5033c33a62f84ff98d15c3c8a866a0
|
|
4
|
+
data.tar.gz: 4222b8eb5f128d74bc55a3284596f11bc975684eae28ff3f0cb8fe56bb2a4899
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08307faaef4bfdfbda347f2f860f71f4aedabfc1b30bf81bcdfe876296ee68df6138e42efcf92c04cb94f3696a8ff33b038ce40bf826ad87491a1ee07364e2c3'
|
|
7
|
+
data.tar.gz: 1f084671c8983ed53172652e6322670b09550758b76378b30905bf1c1c30e7cc531d9fdd46edc3ecd7d9bf2ffa6cbd23e79a989a9d0237eae020191265c29641
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -358,6 +358,15 @@ namespace_validation_mode: :strict)
|
|
|
358
358
|
nil
|
|
359
359
|
end
|
|
360
360
|
|
|
361
|
+
# Plan row stream (Moxml::Plan): yields |name, attrs_pairs
|
|
362
|
+
# (flat [k, v, ...]), first-text, depth| per element in
|
|
363
|
+
# document order (pre-order). Returns true when the adapter
|
|
364
|
+
# provided the stream; nil/false lets the plan run its
|
|
365
|
+
# generic wrapper walk.
|
|
366
|
+
def plan_rows(_native)
|
|
367
|
+
nil
|
|
368
|
+
end
|
|
369
|
+
|
|
361
370
|
# Deterministic native-memory release for adapters backed by
|
|
362
371
|
# C trees (issue #134). GC-managed engines no-op; released
|
|
363
372
|
# documents raise the engine's use-after-free error on
|
|
@@ -493,6 +493,50 @@ module Moxml
|
|
|
493
493
|
extend Markers
|
|
494
494
|
extend Materialize
|
|
495
495
|
|
|
496
|
+
# Plan row stream (Moxml::Plan): the engine's one-pass C
|
|
497
|
+
# snapshot is pre-order with the first text child on the row —
|
|
498
|
+
# exactly the plan's shape, with no wrapper minting at all.
|
|
499
|
+
# Marker-bearing documents stay on the generic path (the bulk
|
|
500
|
+
# stream has no marker split).
|
|
501
|
+
NATIVE_PLAN_ROWS =
|
|
502
|
+
NATIVE_READ_LAYER &&
|
|
503
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.4")
|
|
504
|
+
|
|
505
|
+
def self.plan_rows(native)
|
|
506
|
+
return nil unless NATIVE_READ_LAYER
|
|
507
|
+
|
|
508
|
+
doc = if native.is_a?(::Leptris::XML::Document)
|
|
509
|
+
native
|
|
510
|
+
else
|
|
511
|
+
doc_for(native)
|
|
512
|
+
end
|
|
513
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
514
|
+
|
|
515
|
+
root_binding = if native.is_a?(::Leptris::XML::Document)
|
|
516
|
+
doc.root
|
|
517
|
+
else
|
|
518
|
+
to_binding(native)
|
|
519
|
+
end
|
|
520
|
+
return nil unless root_binding
|
|
521
|
+
|
|
522
|
+
rows = if NATIVE_PLAN_ROWS
|
|
523
|
+
::Leptris::XML::Native.snapshot_rows(doc,
|
|
524
|
+
root_binding.c_address)
|
|
525
|
+
else
|
|
526
|
+
doc.snapshot(root_binding)
|
|
527
|
+
end
|
|
528
|
+
rows.each do |row|
|
|
529
|
+
if row.is_a?(::Array)
|
|
530
|
+
yield(row[0], row[1], row[2], row[3])
|
|
531
|
+
else
|
|
532
|
+
next unless row[:kind] == "element"
|
|
533
|
+
|
|
534
|
+
yield(row[:name], row[:attrs].flatten, row[:text], row[:depth])
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
true
|
|
538
|
+
end
|
|
539
|
+
|
|
496
540
|
class << self
|
|
497
541
|
def attachments
|
|
498
542
|
@attachments ||= Moxml::NativeAttachment.new
|
data/lib/moxml/plan.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Plan-compiled materialization: declare the document shape once,
|
|
5
|
+
# execute it against the adapter's bulk row stream — no wrapper
|
|
6
|
+
# tree, no NodeSet, no per-node contract dispatch. The compiled
|
|
7
|
+
# grammar instead of the interpreted walk.
|
|
8
|
+
#
|
|
9
|
+
# plan = Moxml::Plan.new do
|
|
10
|
+
# on("record") { |attrs, _text, fields| Record.new(attrs["id"], attrs["kind"], fields) }
|
|
11
|
+
# on("field") { |attrs, text, _kids| Field.new(attrs["name"], attrs["unit"], text) }
|
|
12
|
+
# end
|
|
13
|
+
# records = plan.parse(xml, ctx)
|
|
14
|
+
#
|
|
15
|
+
# Handlers run bottom-up as each element completes; +children+ is
|
|
16
|
+
# the array of handler values for matched child elements whose
|
|
17
|
+
# parent ALSO matched (values of unmatched parents are dropped —
|
|
18
|
+
# matching a parent is the consumer's decision to keep a subtree).
|
|
19
|
+
# +attrs+ is a Hash of the element's attributes (local name =>
|
|
20
|
+
# value); +text+ is the first text child's content, or nil.
|
|
21
|
+
#
|
|
22
|
+
# Name matching is global (flat table, not path-relative): element
|
|
23
|
+
# names unique per role — the dominant consumer shape — work as-is;
|
|
24
|
+
# distinct roles sharing a name need distinct documents or a
|
|
25
|
+
# rename upstream.
|
|
26
|
+
#
|
|
27
|
+
# Adapters with a bulk stream answer plan_rows (leptris: the
|
|
28
|
+
# engine's one-pass C snapshot — pre-order, first-text on the
|
|
29
|
+
# row); the rest run the generic pre-order wrapper walk below,
|
|
30
|
+
# emitting the identical stream — spec-pinned equal.
|
|
31
|
+
class Plan
|
|
32
|
+
def initialize(&block)
|
|
33
|
+
@handlers = {}
|
|
34
|
+
instance_eval(&block) if block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Registers a handler for elements named +name+ (local name).
|
|
38
|
+
# Returns self so registrations chain.
|
|
39
|
+
def on(name, &handler)
|
|
40
|
+
raise ArgumentError, "on(#{name.inspect}) requires a block" unless handler
|
|
41
|
+
|
|
42
|
+
@handlers[name] = handler
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Parses +xml+ and materializes the plan against it.
|
|
47
|
+
def parse(xml, context)
|
|
48
|
+
materialize(context.parse(xml))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Materializes the plan against a document (or element) wrapper.
|
|
52
|
+
# Returns the values of top-level matched elements.
|
|
53
|
+
def materialize(node)
|
|
54
|
+
results = []
|
|
55
|
+
frames = [] # per open depth: children values (nil = unmatched)
|
|
56
|
+
meta = [] # per open depth: [name, attrs, text] (nil = unmatched)
|
|
57
|
+
|
|
58
|
+
close_top = lambda do
|
|
59
|
+
m = meta.pop
|
|
60
|
+
children = frames.pop
|
|
61
|
+
next unless m
|
|
62
|
+
|
|
63
|
+
value = @handlers[m[0]].call(m[1], m[2], children)
|
|
64
|
+
if frames.empty? || frames.last.nil?
|
|
65
|
+
results << value
|
|
66
|
+
else
|
|
67
|
+
frames.last << value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
row = lambda do |name, attrs_pairs, text, depth|
|
|
72
|
+
close_top.call while frames.size > depth
|
|
73
|
+
handler = @handlers[name]
|
|
74
|
+
if handler
|
|
75
|
+
attrs = {}
|
|
76
|
+
i = 0
|
|
77
|
+
while i < attrs_pairs.length
|
|
78
|
+
attrs[attrs_pairs[i]] = attrs_pairs[i + 1]
|
|
79
|
+
i += 2
|
|
80
|
+
end
|
|
81
|
+
frames << []
|
|
82
|
+
meta << [name, attrs, text]
|
|
83
|
+
else
|
|
84
|
+
frames << nil
|
|
85
|
+
meta << nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
adapter = node.context.config.adapter
|
|
90
|
+
ran = adapter.plan_rows(node.native) do |name, pairs, text, depth|
|
|
91
|
+
row.call(name, pairs, text, depth)
|
|
92
|
+
end
|
|
93
|
+
unless ran
|
|
94
|
+
walk_wrappers(node) do |name, pairs, text, depth|
|
|
95
|
+
row.call(name, pairs, text, depth)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
close_top.call while meta.any?
|
|
100
|
+
results
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Generic pre-order walk over the wrapper tree — the fallback
|
|
106
|
+
# stream for adapters without a bulk path. Same protocol as
|
|
107
|
+
# plan_rows: |name, attrs_pairs (flat [k, v, ...]), first-text,
|
|
108
|
+
# depth|.
|
|
109
|
+
def walk_wrappers(node, depth = 0)
|
|
110
|
+
case node
|
|
111
|
+
when Document
|
|
112
|
+
(root = node.root) && walk_wrappers(root, depth) { |*a| yield(*a) }
|
|
113
|
+
when Element
|
|
114
|
+
first_text = nil
|
|
115
|
+
node.children.each do |child|
|
|
116
|
+
first_text ||= child.content if child.is_a?(Text)
|
|
117
|
+
end
|
|
118
|
+
yield(node.name, attribute_pairs(node), first_text, depth)
|
|
119
|
+
node.children.each do |child|
|
|
120
|
+
walk_wrappers(child, depth + 1) { |*a| yield(*a) } if child.is_a?(Element)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def attribute_pairs(element)
|
|
126
|
+
pairs = []
|
|
127
|
+
element.attributes.each do |attr|
|
|
128
|
+
pairs << attr.name << attr.value
|
|
129
|
+
end
|
|
130
|
+
pairs
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml.rb
CHANGED
|
@@ -93,6 +93,7 @@ module Moxml
|
|
|
93
93
|
autoload :XmlUtils, "moxml/xml_utils"
|
|
94
94
|
autoload :XmlEmitter, "moxml/xml_emitter"
|
|
95
95
|
autoload :Materializer, "moxml/materializer"
|
|
96
|
+
autoload :Plan, "moxml/plan"
|
|
96
97
|
autoload :Adapter, "moxml/adapter"
|
|
97
98
|
autoload :XPath, "moxml/xpath"
|
|
98
99
|
autoload :SAX, "moxml/sax"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Moxml::Plan: shape-compiled materialization over the adapter's
|
|
6
|
+
# bulk row stream (leptris C snapshot) and the generic wrapper walk.
|
|
7
|
+
# Both paths must produce identical values.
|
|
8
|
+
RSpec.describe Moxml::Plan do
|
|
9
|
+
PlanRow = Struct.new(:id, :kind, :fields) # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
10
|
+
PlanCell = Struct.new(:name, :unit, :value) # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
11
|
+
|
|
12
|
+
let(:xml) do
|
|
13
|
+
<<~XML
|
|
14
|
+
<?xml version="1.0"?>
|
|
15
|
+
<catalog>
|
|
16
|
+
<record id="r0" kind="k0"><field name="f0" unit="u0">v 0.0</field><field name="f1" unit="u1">v 0.1</field></record>
|
|
17
|
+
<record id="r1" kind="k1"><field name="f0" unit="u0">v 1.0</field></record>
|
|
18
|
+
</catalog>
|
|
19
|
+
XML
|
|
20
|
+
end
|
|
21
|
+
let(:plan) do
|
|
22
|
+
described_class.new do
|
|
23
|
+
on("record") do |attrs, _text, fields|
|
|
24
|
+
PlanRow.new(attrs["id"], attrs["kind"], fields)
|
|
25
|
+
end
|
|
26
|
+
on("field") do |attrs, text, _kids|
|
|
27
|
+
PlanCell.new(attrs["name"], attrs["unit"], text)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
shared_examples "plan materialization" do |adapter_name|
|
|
33
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
34
|
+
|
|
35
|
+
it "builds the typed shape" do
|
|
36
|
+
rows = plan.parse(xml, ctx)
|
|
37
|
+
|
|
38
|
+
expect(rows.size).to eq(2)
|
|
39
|
+
expect(rows[0].id).to eq("r0")
|
|
40
|
+
expect(rows[0].kind).to eq("k0")
|
|
41
|
+
expect(rows[0].fields.size).to eq(2)
|
|
42
|
+
expect(rows[0].fields[0]).to eq(PlanCell.new("f0", "u0", "v 0.0"))
|
|
43
|
+
expect(rows[0].fields[1]).to eq(PlanCell.new("f1", "u1", "v 0.1"))
|
|
44
|
+
expect(rows[1].fields[0].value).to eq("v 1.0")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "drops values of unmatched parents" do
|
|
48
|
+
orphans = described_class.new do
|
|
49
|
+
on("field") { |attrs, text, _| PlanCell.new(attrs["name"], attrs["unit"], text) }
|
|
50
|
+
end.parse(xml, ctx)
|
|
51
|
+
|
|
52
|
+
# fields match everywhere; no record handler keeps them
|
|
53
|
+
expect(orphans.size).to eq(3)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "returns top-level values for a matched root" do
|
|
57
|
+
roots = described_class.new do
|
|
58
|
+
on("catalog") { |_a, _t, kids| [:catalog, kids] }
|
|
59
|
+
on("record") { |attrs, _t, fields| [attrs["id"], fields] }
|
|
60
|
+
on("field") { |attrs, text, _| [attrs["name"], text] }
|
|
61
|
+
end.parse(xml, ctx)
|
|
62
|
+
|
|
63
|
+
expect(roots.size).to eq(1)
|
|
64
|
+
expect(roots[0][0]).to eq(:catalog)
|
|
65
|
+
expect(roots[0][1].size).to eq(2)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "handles depth-3 nesting" do
|
|
69
|
+
deep = described_class.new do
|
|
70
|
+
on("catalog") { |_a, _t, records| records }
|
|
71
|
+
on("record") { |attrs, _t, fields| [attrs["id"], fields] }
|
|
72
|
+
on("field") { |attrs, text, _| [attrs["name"], text] }
|
|
73
|
+
end.parse(xml, ctx)
|
|
74
|
+
|
|
75
|
+
expect(deep.size).to eq(1)
|
|
76
|
+
expect(deep[0]).to eq(
|
|
77
|
+
[["r0", [["f0", "v 0.0"], ["f1", "v 0.1"]]],
|
|
78
|
+
["r1", [["f0", "v 1.0"]]]],
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "is reusable across documents" do
|
|
83
|
+
first = plan.parse(xml, ctx)
|
|
84
|
+
second = plan.parse(xml, ctx)
|
|
85
|
+
|
|
86
|
+
expect(first).to eq(second)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "bulk path (leptris)" do
|
|
91
|
+
it_behaves_like "plan materialization", :leptris
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "generic path" do
|
|
95
|
+
it_behaves_like "plan materialization", :nokogiri
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "requires a handler block" do
|
|
99
|
+
expect { described_class.new.on("x") }
|
|
100
|
+
.to raise_error(ArgumentError, /requires a block/)
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.60
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- lib/moxml/native_attachment/opal.rb
|
|
215
215
|
- lib/moxml/node.rb
|
|
216
216
|
- lib/moxml/node_set.rb
|
|
217
|
+
- lib/moxml/plan.rb
|
|
217
218
|
- lib/moxml/processing_instruction.rb
|
|
218
219
|
- lib/moxml/sax.rb
|
|
219
220
|
- lib/moxml/sax/block_handler.rb
|
|
@@ -487,6 +488,7 @@ files:
|
|
|
487
488
|
- spec/moxml/opal_oga_smoke_spec.rb
|
|
488
489
|
- spec/moxml/opal_rexml_adapter_spec.rb
|
|
489
490
|
- spec/moxml/opal_smoke_spec.rb
|
|
491
|
+
- spec/moxml/plan_spec.rb
|
|
490
492
|
- spec/moxml/processing_instruction_spec.rb
|
|
491
493
|
- spec/moxml/readonly_parse_spec.rb
|
|
492
494
|
- spec/moxml/sax/namespace_splitter_spec.rb
|