moxml 0.5.42 → 0.5.44
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/Gemfile +1 -1
- data/lib/moxml/adapter/leptris.rb +94 -16
- data/lib/moxml/adapter/rexml.rb +1 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/cache.rb +18 -25
- data/spec/integration/shared_examples/integration_workflows.rb +34 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd5c99b547004d2f6521f882b006da2b612ee74cabf92775a3b061d9f096bc20
|
|
4
|
+
data.tar.gz: 652bce88c043068ecd97ef7008044557f764661a2e792f1128d435d4e83d715a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba8297809db361e15cac3cbfbc132aa5545ceee6fe4e3d8b7df9a6250e7585ce930eaf7ba7d15de155975848f0d2deb205dcf8b0ebbdd3e00d96cbfc8d758633
|
|
7
|
+
data.tar.gz: 982406a0ff5f858aeed1569fd4ca1d4536bbb2b9841d791b4f560475b19b77641db0831fac7efd6760c3493216a9db8635de55d5e146f158cc0f16297fc97895
|
data/Gemfile
CHANGED
|
@@ -42,6 +42,17 @@ module Moxml
|
|
|
42
42
|
DIGEST_SUPPORTED =
|
|
43
43
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
|
|
44
44
|
|
|
45
|
+
# 1.9.163.7 added the XPath seams (TODO.perf/15+16):
|
|
46
|
+
# Searchable#at_xpath materializes nodeset entry 0 and frees
|
|
47
|
+
# the handle in one C dispatch — no NodeSet container, no
|
|
48
|
+
# AutoPointer (2.8x on repeat at_xpath) — and
|
|
49
|
+
# CompiledXPath#eval_ptrs runs the compiled handle against
|
|
50
|
+
# raw pointers, skipping the per-call EvaluationContext
|
|
51
|
+
# (~2us on every multi-result query).
|
|
52
|
+
NATIVE_XPATH_SEAMS =
|
|
53
|
+
defined?(::Leptris::XML::Native) &&
|
|
54
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.7")
|
|
55
|
+
|
|
45
56
|
# Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
|
|
46
57
|
# TypedData node wrappers with C-bound hot reads and bulk
|
|
47
58
|
# children construction. Minted from #root downward; the C
|
|
@@ -89,7 +100,17 @@ module Moxml
|
|
|
89
100
|
# sites must never depend on the layer having loaded
|
|
90
101
|
# (issue #217 — binding-only installs crashed on the
|
|
91
102
|
# unguarded bridges).
|
|
103
|
+
# Class-body level: a bare @ivar inside `class << self` lands
|
|
104
|
+
# on the singleton's singleton, invisible to the methods at
|
|
105
|
+
# call time (same shape as @native_doc_roots below).
|
|
106
|
+
@binding_of = ObjectSpace::WeakMap.new
|
|
107
|
+
|
|
92
108
|
class << self
|
|
109
|
+
# native -> bridged binding node. The bridge object is
|
|
110
|
+
# identity-stable for the native's lifetime (the binding's
|
|
111
|
+
# per-document wrap cache), so repeat bridges — the write
|
|
112
|
+
# loop on a factory-created element, re-accessed attribute
|
|
113
|
+
# lists — skip the document resolution and pointer wrap.
|
|
93
114
|
# Binding node for any native: identity for binding nodes
|
|
94
115
|
# (and on installs without the native layer — the constant
|
|
95
116
|
# check short-circuits), a Node.wrap over the shared C
|
|
@@ -98,11 +119,14 @@ module Moxml
|
|
|
98
119
|
return node unless NATIVE_READ_LAYER &&
|
|
99
120
|
node.is_a?(::Leptris::XML::NativeNode)
|
|
100
121
|
|
|
122
|
+
bridged = @binding_of[node]
|
|
123
|
+
return bridged if bridged
|
|
124
|
+
|
|
101
125
|
doc = doc_for(node)
|
|
102
126
|
ptr = ::FFI::Pointer.new(node.address)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
bridged = ::Leptris::XML::Node.wrap(ptr, doc)
|
|
128
|
+
@binding_of[node] = bridged unless bridged.nil?
|
|
129
|
+
bridged
|
|
106
130
|
end
|
|
107
131
|
end
|
|
108
132
|
|
|
@@ -281,7 +305,7 @@ module Moxml
|
|
|
281
305
|
end
|
|
282
306
|
|
|
283
307
|
def set_root(doc, element)
|
|
284
|
-
doc.root = element
|
|
308
|
+
to_binding(doc).root = to_binding(element)
|
|
285
309
|
end
|
|
286
310
|
|
|
287
311
|
def bare_set_qname_safe?
|
|
@@ -1290,6 +1314,9 @@ module Moxml
|
|
|
1290
1314
|
# cached AST three times per xpath call cost ~23% of repeated
|
|
1291
1315
|
# selective queries, so the boolean caches beside it.
|
|
1292
1316
|
NATIVE_GATE_CACHE = XPath::Cache.new(100)
|
|
1317
|
+
# Root-context queries alone take the parent-axis guard; the
|
|
1318
|
+
# AST walk is per-expression, so the verdict caches too.
|
|
1319
|
+
PARENT_AXIS_CACHE = XPath::Cache.new(100)
|
|
1293
1320
|
|
|
1294
1321
|
def xpath(node, expression, namespaces = {})
|
|
1295
1322
|
node = to_binding(node) if NATIVE_READ_LAYER
|
|
@@ -1310,6 +1337,14 @@ module Moxml
|
|
|
1310
1337
|
# @return [Array, Object, nil] native results, or nil when the
|
|
1311
1338
|
# query must run on the Ruby engine
|
|
1312
1339
|
def native_xpath(node, expression, namespaces, first_only: false)
|
|
1340
|
+
# Canonical natives (the #219 unification) are bare TypedData
|
|
1341
|
+
# wrappers — no Searchable methods, no document handle for
|
|
1342
|
+
# the gates below. The memoized bridge recovers the binding
|
|
1343
|
+
# identity; without it every element-context query fell to
|
|
1344
|
+
# the Ruby engine (a silent native-path loss since #219).
|
|
1345
|
+
if NATIVE_READ_LAYER && node.is_a?(::Leptris::XML::NativeNode)
|
|
1346
|
+
node = to_binding(node)
|
|
1347
|
+
end
|
|
1313
1348
|
return nil unless native_context_node?(node)
|
|
1314
1349
|
return nil unless native_expression?(expression)
|
|
1315
1350
|
# The binding reports the root element parentless while moxml
|
|
@@ -1317,27 +1352,70 @@ module Moxml
|
|
|
1317
1352
|
# root element keep the Ruby engine.
|
|
1318
1353
|
if node.is_a?(::Leptris::XML::Element) &&
|
|
1319
1354
|
node.document&.root.equal?(node) &&
|
|
1320
|
-
|
|
1355
|
+
PARENT_AXIS_CACHE.get_or_set(expression) do
|
|
1356
|
+
expression_uses_parent_axis?(expression)
|
|
1357
|
+
end
|
|
1321
1358
|
return nil
|
|
1322
1359
|
end
|
|
1323
1360
|
|
|
1324
1361
|
compiled = NATIVE_XPATH_CACHE.get_or_set(expression) do
|
|
1325
1362
|
::Leptris::XML::XPath.compile(expression)
|
|
1326
1363
|
end
|
|
1327
|
-
|
|
1328
|
-
|
|
1364
|
+
document = node.is_a?(::Leptris::XML::Document) ? node : node.document
|
|
1365
|
+
|
|
1366
|
+
if namespaces && !namespaces.empty?
|
|
1367
|
+
# Namespace-bound evaluation has no raw-pointer entry —
|
|
1368
|
+
# Searchable resolves the ns set for both result shapes.
|
|
1369
|
+
result = if first_only
|
|
1370
|
+
node.at_xpath(expression, namespaces)
|
|
1371
|
+
else
|
|
1372
|
+
compiled.eval(node, namespaces)
|
|
1373
|
+
end
|
|
1374
|
+
return case result
|
|
1375
|
+
when ::Leptris::XML::NodeSet
|
|
1376
|
+
first_only ? result.first : result.extend(Moxml::LazyNodeSet)
|
|
1329
1377
|
else
|
|
1330
|
-
|
|
1378
|
+
result
|
|
1379
|
+
end
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
unless NATIVE_XPATH_SEAMS
|
|
1383
|
+
result = compiled.eval(node)
|
|
1384
|
+
return case result
|
|
1385
|
+
when ::Leptris::XML::NodeSet
|
|
1386
|
+
first_only ? result.first : result.extend(Moxml::LazyNodeSet)
|
|
1387
|
+
else
|
|
1388
|
+
result
|
|
1331
1389
|
end
|
|
1332
|
-
case result
|
|
1333
|
-
when ::Leptris::XML::NodeSet
|
|
1334
|
-
# The binding's #to_a mints one Ruby wrapper per result
|
|
1335
|
-
# node; hand the native set through so .size/.first stay
|
|
1336
|
-
# native-side (LazyNodeSet holds it unmaterialized).
|
|
1337
|
-
first_only ? result.first : result.extend(Moxml::LazyNodeSet)
|
|
1338
|
-
else
|
|
1339
|
-
result
|
|
1340
1390
|
end
|
|
1391
|
+
|
|
1392
|
+
# eval_ptrs against raw pointers — the per-call
|
|
1393
|
+
# EvaluationContext that #eval builds costs ~2us of Ruby
|
|
1394
|
+
# on every query (leptris-ruby TODO.perf/15).
|
|
1395
|
+
doc_ptr = node.is_a?(::Leptris::XML::Document) ? node.c_ptr : document.c_ptr
|
|
1396
|
+
context_ptr = node.is_a?(::Leptris::XML::Document) ? nil : node.c_ptr
|
|
1397
|
+
result_ptr = compiled.eval_ptrs(doc_ptr, context_ptr)
|
|
1398
|
+
return nil if result_ptr.null?
|
|
1399
|
+
|
|
1400
|
+
if first_only
|
|
1401
|
+
# The single-result seam: nodeset entry 0 materializes
|
|
1402
|
+
# and frees in one C dispatch — no NodeSet container, no
|
|
1403
|
+
# AutoPointer (leptris-ruby TODO.perf/16). The module
|
|
1404
|
+
# object back means a scalar, whose wrap (and free)
|
|
1405
|
+
# stays with wrap_xpath_first_result.
|
|
1406
|
+
return ::Leptris::XML::Searchable.wrap_xpath_first_result(
|
|
1407
|
+
document, result_ptr
|
|
1408
|
+
)
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
# The binding's #to_a mints one Ruby wrapper per result
|
|
1412
|
+
# node; hand the native set through so .size/.first stay
|
|
1413
|
+
# native-side (LazyNodeSet holds it unmaterialized).
|
|
1414
|
+
# Scalars pass through unwrapped-and-unextended.
|
|
1415
|
+
result = ::Leptris::XML::Searchable.wrap_xpath_result(
|
|
1416
|
+
document, result_ptr
|
|
1417
|
+
)
|
|
1418
|
+
result.is_a?(::Leptris::XML::NodeSet) ? result.extend(Moxml::LazyNodeSet) : result
|
|
1341
1419
|
rescue ::Leptris::XML::XPathError
|
|
1342
1420
|
# Not supported by the native engine — the Ruby engine is a
|
|
1343
1421
|
# full XPath 1.0 implementation, including Moxml's syntax
|
data/lib/moxml/adapter/rexml.rb
CHANGED
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/cache.rb
CHANGED
|
@@ -11,8 +11,12 @@ module Moxml
|
|
|
11
11
|
# @param [Integer] max_size Maximum number of entries to cache
|
|
12
12
|
def initialize(max_size = DEFAULT_SIZE)
|
|
13
13
|
@max_size = max_size
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# One insertion-ordered hash: re-inserting a key on access
|
|
15
|
+
# makes the first entry the least-recently-used. An
|
|
16
|
+
# Array-backed order list cost ~600ns per hit in O(n)
|
|
17
|
+
# deletes — the adapter's xpath path hits three caches per
|
|
18
|
+
# call.
|
|
19
|
+
@entries = {}
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
# Gets a value from the cache or sets it using the provided block.
|
|
@@ -21,11 +25,9 @@ module Moxml
|
|
|
21
25
|
# @yield Block to execute if key is not in cache
|
|
22
26
|
# @return [Object] Cached or newly computed value
|
|
23
27
|
def get_or_set(key)
|
|
24
|
-
if @
|
|
25
|
-
|
|
26
|
-
@
|
|
27
|
-
@access_order.push(key)
|
|
28
|
-
@cache[key]
|
|
28
|
+
if @entries.key?(key)
|
|
29
|
+
value = @entries.delete(key)
|
|
30
|
+
@entries[key] = value
|
|
29
31
|
else
|
|
30
32
|
value = yield
|
|
31
33
|
set(key, value)
|
|
@@ -39,16 +41,9 @@ module Moxml
|
|
|
39
41
|
# @param [Object] value
|
|
40
42
|
# @return [Object] The value
|
|
41
43
|
def set(key, value)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# Remove least recently used
|
|
46
|
-
lru_key = @access_order.shift
|
|
47
|
-
@cache.delete(lru_key)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@cache[key] = value
|
|
51
|
-
@access_order.push(key)
|
|
44
|
+
@entries.delete(key)
|
|
45
|
+
@entries[key] = value
|
|
46
|
+
@entries.shift if @entries.size > @max_size
|
|
52
47
|
value
|
|
53
48
|
end
|
|
54
49
|
|
|
@@ -57,26 +52,24 @@ module Moxml
|
|
|
57
52
|
# @param [Object] key
|
|
58
53
|
# @return [Object, nil]
|
|
59
54
|
def get(key)
|
|
60
|
-
return unless @
|
|
55
|
+
return unless @entries.key?(key)
|
|
61
56
|
|
|
62
|
-
@
|
|
63
|
-
@
|
|
64
|
-
@cache[key]
|
|
57
|
+
value = @entries.delete(key)
|
|
58
|
+
@entries[key] = value
|
|
65
59
|
end
|
|
66
60
|
|
|
67
61
|
# Clears the cache.
|
|
68
62
|
#
|
|
69
63
|
# @return [void]
|
|
70
64
|
def clear
|
|
71
|
-
@
|
|
72
|
-
@access_order.clear
|
|
65
|
+
@entries.clear
|
|
73
66
|
end
|
|
74
67
|
|
|
75
68
|
# Returns the current size of the cache.
|
|
76
69
|
#
|
|
77
70
|
# @return [Integer]
|
|
78
71
|
def size
|
|
79
|
-
@
|
|
72
|
+
@entries.size
|
|
80
73
|
end
|
|
81
74
|
|
|
82
75
|
# Checks if a key exists in the cache.
|
|
@@ -84,7 +77,7 @@ module Moxml
|
|
|
84
77
|
# @param [Object] key
|
|
85
78
|
# @return [Boolean]
|
|
86
79
|
def key?(key)
|
|
87
|
-
@
|
|
80
|
+
@entries.key?(key)
|
|
88
81
|
end
|
|
89
82
|
end
|
|
90
83
|
end
|
|
@@ -143,4 +143,38 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
143
143
|
)
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
|
+
|
|
147
|
+
describe "document root assignment" do
|
|
148
|
+
it "attaches a factory-created element as the root and serializes" do
|
|
149
|
+
doc = context.create_document
|
|
150
|
+
root = doc.create_element("root")
|
|
151
|
+
root["id"] = "1"
|
|
152
|
+
child = doc.create_element("child")
|
|
153
|
+
child.text = "data"
|
|
154
|
+
root.add_child(child)
|
|
155
|
+
|
|
156
|
+
doc.root = root
|
|
157
|
+
|
|
158
|
+
expect(doc.root.name).to eq("root")
|
|
159
|
+
expect(doc.root["id"]).to eq("1")
|
|
160
|
+
expect(doc.root.parent).to eq(doc)
|
|
161
|
+
expect(doc.to_xml).to include("<root id=\"1\">", "<child>data</child>")
|
|
162
|
+
|
|
163
|
+
reparsed = context.parse(doc.to_xml)
|
|
164
|
+
expect(reparsed.root["id"]).to eq("1")
|
|
165
|
+
expect(reparsed.at_xpath("//child").text).to eq("data")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "replaces an existing root" do
|
|
169
|
+
doc = context.parse("<old><nested/></old>")
|
|
170
|
+
replacement = doc.create_element("new")
|
|
171
|
+
replacement.add_child("text")
|
|
172
|
+
|
|
173
|
+
doc.root = replacement
|
|
174
|
+
|
|
175
|
+
expect(doc.root.name).to eq("new")
|
|
176
|
+
expect(doc.to_xml).to include("<new>text</new>")
|
|
177
|
+
expect(doc.to_xml).not_to include("old")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
146
180
|
end
|