moxml 0.5.7 → 0.5.8
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 +86 -1
- data/lib/moxml/context.rb +7 -0
- data/lib/moxml/document.rb +7 -0
- data/lib/moxml/materializer.rb +90 -0
- data/lib/moxml/node.rb +7 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +1 -0
- data/spec/moxml/materializer_spec.rb +82 -0
- data/spec/moxml/readonly_parse_spec.rb +27 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77a959afbe83b0cc46a158571f792d07888f7bf29f3a65c1ea88be867892ffcc
|
|
4
|
+
data.tar.gz: 1a5a9515ba125977cd26e33b878e7ecaaed1a788e450e79f356c54a117c9e6c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff6e651e53a55b5ed4aed1e68eff9fa40e9a58aed81a3709ce101fb315bddbef928dcd1a8b31317fcf7dd42e65b92158191647365bc7c43fba46677551bd82d6
|
|
7
|
+
data.tar.gz: 3c3ad8fac372d42a2dd9d3e4660bc30987015975031fed6b80c1bfeea864f47ed36676d125132384b72d7ba51a3a1ef743877efd5611fd0ecc2cda96efae29e7
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -189,6 +189,15 @@ namespace_validation_mode: :strict)
|
|
|
189
189
|
true
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
+
# Whether the engine offers a bulk materialization path for
|
|
193
|
+
# Materializer (issue #132). When true, materialize_records
|
|
194
|
+
# yields flattened records for the subtree; returning nil
|
|
195
|
+
# (e.g. for document shapes the bulk path cannot express)
|
|
196
|
+
# falls back to the generic wrapper walk.
|
|
197
|
+
def bulk_materialize?
|
|
198
|
+
false
|
|
199
|
+
end
|
|
200
|
+
|
|
192
201
|
# Check if the native document has an XML declaration
|
|
193
202
|
# @param native_doc the native document object
|
|
194
203
|
# @param wrapper [Moxml::Document] the wrapper with has_xml_declaration flag
|
|
@@ -32,8 +32,11 @@ module Moxml
|
|
|
32
32
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
33
33
|
processed = preprocess_entities(xml_string)
|
|
34
34
|
|
|
35
|
+
# readonly: true (issue #133): the binding memoizes reads and
|
|
36
|
+
# refuses mutations — the parse-and-read lifecycle for
|
|
37
|
+
# multi-pass consumers (comparison, diff, signature).
|
|
35
38
|
native_doc = begin
|
|
36
|
-
::Leptris::XML::Document.parse(processed)
|
|
39
|
+
::Leptris::XML::Document.parse(processed, readonly: options[:readonly] == true)
|
|
37
40
|
rescue ::Leptris::XML::ParseError => e
|
|
38
41
|
# libleptris has no recovery mode that survives unclosed
|
|
39
42
|
# tags; non-strict callers get an empty document, matching
|
|
@@ -224,6 +227,88 @@ module Moxml
|
|
|
224
227
|
end
|
|
225
228
|
end
|
|
226
229
|
|
|
230
|
+
# Bulk materialization (issue #132): leptris_node_traverse
|
|
231
|
+
# walks the subtree with one FFI call; the only per-node cost
|
|
232
|
+
# is the C->Ruby callback and the property reads below. No
|
|
233
|
+
# Moxml::Node or Attribute wrapper is allocated.
|
|
234
|
+
def bulk_materialize?
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def materialize_records(native, &block)
|
|
239
|
+
doc = native.is_a?(::Leptris::XML::Document) ? native : native.document
|
|
240
|
+
# Marker-bearing text needs the split pipeline (children-level
|
|
241
|
+
# ER expansion); the bulk path has no marker handling.
|
|
242
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
243
|
+
|
|
244
|
+
root = native.is_a?(::Leptris::XML::Document) ? native.root : native
|
|
245
|
+
return nil if root.nil?
|
|
246
|
+
|
|
247
|
+
depth_memo = {}.compare_by_identity
|
|
248
|
+
root.traverse do |node|
|
|
249
|
+
record = case node
|
|
250
|
+
when ::Leptris::XML::Element
|
|
251
|
+
element_material_record(node, depth_memo)
|
|
252
|
+
when ::Leptris::XML::CDATA
|
|
253
|
+
# CDATA < Text in the binding: this arm must come
|
|
254
|
+
# first or CDATA content reports as text.
|
|
255
|
+
text_material_record(:cdata, node.content, node, depth_memo)
|
|
256
|
+
when ::Leptris::XML::Text
|
|
257
|
+
text_material_record(:text, node.content, node, depth_memo)
|
|
258
|
+
when ::Leptris::XML::Comment
|
|
259
|
+
text_material_record(:comment, node.content, node, depth_memo)
|
|
260
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
261
|
+
text_material_record(:processing_instruction, node.content, node, depth_memo)
|
|
262
|
+
.merge(qname: node.target)
|
|
263
|
+
end
|
|
264
|
+
yield(record) if record
|
|
265
|
+
end
|
|
266
|
+
true
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def element_material_record(node, depth_memo)
|
|
270
|
+
attributes = node.each_attribute.map do |attr|
|
|
271
|
+
# Local name + separate prefix, matching the generic
|
|
272
|
+
# path's resolver semantics (moxml's canonical shape).
|
|
273
|
+
name = attr.name
|
|
274
|
+
prefix = attr.prefix
|
|
275
|
+
name = name.split(":", 2)[1] || name if prefix
|
|
276
|
+
[name, attr.value, attr.namespace_uri, prefix]
|
|
277
|
+
end
|
|
278
|
+
ns = node.namespace
|
|
279
|
+
{
|
|
280
|
+
kind: :element,
|
|
281
|
+
qname: node.name,
|
|
282
|
+
prefix: node.prefix,
|
|
283
|
+
namespace_uri: ns&.href,
|
|
284
|
+
attributes: attributes,
|
|
285
|
+
text: nil,
|
|
286
|
+
depth: material_depth(node, depth_memo),
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def text_material_record(kind, text, node, depth_memo)
|
|
291
|
+
{
|
|
292
|
+
kind: kind,
|
|
293
|
+
qname: nil,
|
|
294
|
+
prefix: nil,
|
|
295
|
+
namespace_uri: nil,
|
|
296
|
+
attributes: Materializer::EMPTY_ATTRIBUTES,
|
|
297
|
+
text: text,
|
|
298
|
+
depth: material_depth(node, depth_memo),
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Binding wrappers are address-stable and #parent is memoized,
|
|
303
|
+
# so each edge is fetched once; depths resolve through the
|
|
304
|
+
# identity-keyed memo.
|
|
305
|
+
def material_depth(node, memo)
|
|
306
|
+
memo[node] ||= begin
|
|
307
|
+
parent = node.parent
|
|
308
|
+
parent.nil? || parent.is_a?(::Leptris::XML::Document) ? 0 : material_depth(parent, memo) + 1
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
227
312
|
def node_type(node)
|
|
228
313
|
case node
|
|
229
314
|
when ::Leptris::XML::Document then :document
|
data/lib/moxml/context.rb
CHANGED
|
@@ -74,6 +74,13 @@ module Moxml
|
|
|
74
74
|
doc
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Parse then flatten in one call — see Moxml::Materializer
|
|
78
|
+
# (issue #132). Yields records; returns an Enumerator when no
|
|
79
|
+
# block is given.
|
|
80
|
+
def materialize(xml, options = {}, &block)
|
|
81
|
+
parse(xml, options).materialize(&block)
|
|
82
|
+
end
|
|
83
|
+
|
|
77
84
|
# Parse XML using SAX (event-driven) parsing
|
|
78
85
|
#
|
|
79
86
|
# SAX parsing is memory-efficient and suitable for large XML files.
|
data/lib/moxml/document.rb
CHANGED
|
@@ -24,6 +24,13 @@ module Moxml
|
|
|
24
24
|
root_element ? Moxml::Node.wrap(root_element, context) : nil
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Materialize the root subtree — see Moxml::Materializer.
|
|
28
|
+
def materialize(&block)
|
|
29
|
+
return to_enum(:materialize) unless block
|
|
30
|
+
|
|
31
|
+
root&.materialize(&block)
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def create_element(name)
|
|
28
35
|
Element.new(adapter.create_element(name, owner_doc: @native), context)
|
|
29
36
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Flattened subtree records for conversion-style consumers
|
|
5
|
+
# (issue #132): one record per node, emitted in post-order (children
|
|
6
|
+
# before parents — build with a stack), carrying everything a
|
|
7
|
+
# consumer tree needs without allocating Moxml::Node or
|
|
8
|
+
# Moxml::Attribute wrappers.
|
|
9
|
+
#
|
|
10
|
+
# context.materialize(xml) { |r| ... }
|
|
11
|
+
# document.materialize.each { |r| ... } # Enumerator
|
|
12
|
+
#
|
|
13
|
+
# Record shape:
|
|
14
|
+
# { kind: :element|:text|:cdata|:comment|:processing_instruction,
|
|
15
|
+
# qname: "tag"|"pi-target"|nil, prefix: "p"|nil,
|
|
16
|
+
# namespace_uri: "urn:x"|nil,
|
|
17
|
+
# attributes: [[name, value, namespace_uri, prefix], ...],
|
|
18
|
+
# text: String|nil, depth: Integer }
|
|
19
|
+
#
|
|
20
|
+
# Adapters with a bulk path (leptris: one leptris_node_traverse FFI
|
|
21
|
+
# call for the whole subtree) answer bulk_materialize?; the others
|
|
22
|
+
# walk the wrapper tree generically.
|
|
23
|
+
module Materializer
|
|
24
|
+
# Shared frozen empty attribute list for non-element records;
|
|
25
|
+
# adapters' bulk paths reference it too.
|
|
26
|
+
EMPTY_ATTRIBUTES = [].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def materialize(node, &block)
|
|
31
|
+
adapter = node.context.config.adapter
|
|
32
|
+
return enum_for(:materialize, node) unless block
|
|
33
|
+
|
|
34
|
+
if adapter.bulk_materialize? && adapter.materialize_records(node.native, &block)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
walk(node, 0, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Generic post-order walk over the wrapper tree. Works on every
|
|
42
|
+
# adapter; the fast bulk path exists where the engine offers one.
|
|
43
|
+
def walk(node, depth, &block)
|
|
44
|
+
case node
|
|
45
|
+
when Element
|
|
46
|
+
node.children.each { |child| walk(child, depth + 1, &block) }
|
|
47
|
+
yield(element_record(node, depth))
|
|
48
|
+
when Text
|
|
49
|
+
yield(text_record(:text, node.content, depth))
|
|
50
|
+
when Cdata
|
|
51
|
+
yield(text_record(:cdata, node.content, depth))
|
|
52
|
+
when Comment
|
|
53
|
+
yield(text_record(:comment, node.content, depth))
|
|
54
|
+
when ProcessingInstruction
|
|
55
|
+
yield(text_record(:processing_instruction, node.content, depth).merge(qname: node.target))
|
|
56
|
+
when EntityReference
|
|
57
|
+
yield(text_record(:entity_reference, "&#{node.name};", depth).merge(qname: node.name))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def element_record(element, depth)
|
|
62
|
+
attributes = element.attributes.map do |attr|
|
|
63
|
+
ns = attr.namespace
|
|
64
|
+
[attr.name, attr.value, ns&.uri, ns&.prefix]
|
|
65
|
+
end
|
|
66
|
+
ns = element.namespace
|
|
67
|
+
{
|
|
68
|
+
kind: :element,
|
|
69
|
+
qname: element.name,
|
|
70
|
+
prefix: element.namespace_prefix,
|
|
71
|
+
namespace_uri: ns&.uri,
|
|
72
|
+
attributes: attributes,
|
|
73
|
+
text: nil,
|
|
74
|
+
depth: depth,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def text_record(kind, text, depth)
|
|
79
|
+
{
|
|
80
|
+
kind: kind,
|
|
81
|
+
qname: nil,
|
|
82
|
+
prefix: nil,
|
|
83
|
+
namespace_uri: nil,
|
|
84
|
+
attributes: EMPTY_ATTRIBUTES,
|
|
85
|
+
text: text,
|
|
86
|
+
depth: depth,
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -124,6 +124,13 @@ module Moxml
|
|
|
124
124
|
result.is_a?(Array) ? NodeSet.new(result, context) : result
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Flattened post-order records for this subtree without
|
|
128
|
+
# allocating wrappers — see Moxml::Materializer (issue #132).
|
|
129
|
+
# Returns an Enumerator when no block is given.
|
|
130
|
+
def materialize(&block)
|
|
131
|
+
Materializer.materialize(self, &block)
|
|
132
|
+
end
|
|
133
|
+
|
|
127
134
|
def at_xpath(expression, namespaces = {})
|
|
128
135
|
Moxml::Node.wrap(adapter.at_xpath(@native, expression, namespaces),
|
|
129
136
|
context)
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml.rb
CHANGED
|
@@ -87,6 +87,7 @@ module Moxml
|
|
|
87
87
|
autoload :NativeAttachment, "moxml/native_attachment"
|
|
88
88
|
autoload :XmlUtils, "moxml/xml_utils"
|
|
89
89
|
autoload :XmlEmitter, "moxml/xml_emitter"
|
|
90
|
+
autoload :Materializer, "moxml/materializer"
|
|
90
91
|
autoload :Adapter, "moxml/adapter"
|
|
91
92
|
autoload :XPath, "moxml/xpath"
|
|
92
93
|
autoload :SAX, "moxml/sax"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Materializer do
|
|
6
|
+
let(:xml) do
|
|
7
|
+
<<~XML
|
|
8
|
+
<catalog xmlns="urn:c" xmlns:p="urn:p">
|
|
9
|
+
<book id="b1" p:lang="en"><title>T</title><!--n--><?pi x?><![CDATA[raw]]></book>
|
|
10
|
+
</catalog>
|
|
11
|
+
XML
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def records_for(adapter)
|
|
15
|
+
Moxml.new(adapter).materialize(xml).to_a
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "emits one record per node in post-order with depth, names, and attributes" do
|
|
19
|
+
records = records_for(:leptris)
|
|
20
|
+
|
|
21
|
+
expect(records.map { |r| [r[:kind], r[:depth]] }).to eq(
|
|
22
|
+
[
|
|
23
|
+
# post-order: whitespace before <book>, then the book subtree,
|
|
24
|
+
# then the trailing whitespace, then the catalog itself
|
|
25
|
+
[:text, 1],
|
|
26
|
+
[:text, 3], [:element, 2], [:comment, 2],
|
|
27
|
+
[:processing_instruction, 2], [:cdata, 2],
|
|
28
|
+
[:element, 1],
|
|
29
|
+
[:text, 1],
|
|
30
|
+
[:element, 0]
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
book = records.find { |r| r[:qname] == "book" }
|
|
35
|
+
expect(book[:attributes]).to include(["id", "b1", nil, nil], ["lang", "en", "urn:p", "p"])
|
|
36
|
+
expect(book[:namespace_uri]).to eq("urn:c")
|
|
37
|
+
expect(book[:text]).to be_nil
|
|
38
|
+
|
|
39
|
+
title_text = records.find { |r| r[:kind] == :text && r[:text] == "T" }
|
|
40
|
+
expect(title_text[:attributes]).to eq([])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "uses the same record stream on the bulk path and the generic walk" do
|
|
44
|
+
skip "leptris not installed" unless Moxml::Adapter.available?(:leptris)
|
|
45
|
+
|
|
46
|
+
expect(records_for(:leptris)).to eq(records_for(:nokogiri))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "falls back to the generic walk for marker-bearing documents" do
|
|
50
|
+
entity_xml = "<r><a>pre post</a></r>"
|
|
51
|
+
records = Moxml.new(:leptris).materialize(entity_xml).to_a
|
|
52
|
+
|
|
53
|
+
expect(records.map { |r| [r[:kind], r[:text]] }).to eq(
|
|
54
|
+
[[:text, "pre"], [:entity_reference, " "], [:text, "post"],
|
|
55
|
+
[:element, nil], [:element, nil]],
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "materializes from any element subtree" do
|
|
60
|
+
doc = Moxml.new(:nokogiri).parse(xml)
|
|
61
|
+
title = doc.at_xpath("//*[local-name()='title']")
|
|
62
|
+
records = title.materialize.to_a
|
|
63
|
+
|
|
64
|
+
expect(records.map { |r| [r[:kind], r[:depth]] }).to eq([[:text, 1], [:element, 0]])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "allocates no wrappers on the bulk path" do
|
|
68
|
+
skip "leptris not installed" unless Moxml::Adapter.available?(:leptris)
|
|
69
|
+
|
|
70
|
+
ctx = Moxml.new(:leptris)
|
|
71
|
+
ctx.materialize(xml).to_a # warm
|
|
72
|
+
GC.start
|
|
73
|
+
before = GC.stat(:total_allocated_objects)
|
|
74
|
+
ctx.materialize(xml).to_a
|
|
75
|
+
allocated = GC.stat(:total_allocated_objects) - before
|
|
76
|
+
node_count = ctx.parse(xml).materialize.count
|
|
77
|
+
|
|
78
|
+
# Records themselves allocate (hash + FFI strings); the wrapper
|
|
79
|
+
# tree does not: far fewer allocations than 2 wrappers per node.
|
|
80
|
+
expect(allocated).to be < node_count * 20
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "readonly parsing" do
|
|
6
|
+
let(:xml) { '<r><a x="1">t</a></r>' }
|
|
7
|
+
|
|
8
|
+
it "parses and reads normally" do
|
|
9
|
+
doc = Moxml.new(:leptris).parse(xml, readonly: true)
|
|
10
|
+
|
|
11
|
+
expect(doc.root.name).to eq("r")
|
|
12
|
+
expect(doc.at_xpath("//a")["x"]).to eq("1")
|
|
13
|
+
expect(doc.at_xpath("//a").text).to eq("t")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "refuses mutations" do
|
|
17
|
+
doc = Moxml.new(:leptris).parse(xml, readonly: true)
|
|
18
|
+
|
|
19
|
+
expect { doc.root["y"] = "2" }.to raise_error(/ReadOnly/i)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "is a no-op concept on other adapters" do
|
|
23
|
+
doc = Moxml.new(:nokogiri).parse(xml, readonly: true)
|
|
24
|
+
|
|
25
|
+
expect(doc.root.name).to eq("r")
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -196,6 +196,7 @@ files:
|
|
|
196
196
|
- lib/moxml/entity_registry.rb
|
|
197
197
|
- lib/moxml/entity_registry_opal_data.rb
|
|
198
198
|
- lib/moxml/error.rb
|
|
199
|
+
- lib/moxml/materializer.rb
|
|
199
200
|
- lib/moxml/namespace.rb
|
|
200
201
|
- lib/moxml/native_attachment.rb
|
|
201
202
|
- lib/moxml/native_attachment/native.rb
|
|
@@ -454,6 +455,7 @@ files:
|
|
|
454
455
|
- spec/moxml/entity_spec.rb
|
|
455
456
|
- spec/moxml/error_spec.rb
|
|
456
457
|
- spec/moxml/lazy_parse_spec.rb
|
|
458
|
+
- spec/moxml/materializer_spec.rb
|
|
457
459
|
- spec/moxml/moxml_spec.rb
|
|
458
460
|
- spec/moxml/namespace_spec.rb
|
|
459
461
|
- spec/moxml/namespace_uri_validation_spec.rb
|
|
@@ -472,6 +474,7 @@ files:
|
|
|
472
474
|
- spec/moxml/opal_rexml_adapter_spec.rb
|
|
473
475
|
- spec/moxml/opal_smoke_spec.rb
|
|
474
476
|
- spec/moxml/processing_instruction_spec.rb
|
|
477
|
+
- spec/moxml/readonly_parse_spec.rb
|
|
475
478
|
- spec/moxml/sax/namespace_splitter_spec.rb
|
|
476
479
|
- spec/moxml/sax_entity_parity_spec.rb
|
|
477
480
|
- spec/moxml/sax_spec.rb
|