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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d447f74ea4b1c546e83fbb94819c6043a1fa48e0f9932ac7202009cecff63e1e
4
- data.tar.gz: 4c33c0af41d9c628ecc993d7015d4e827d1d451b266dd7ad4145f00b322670a3
3
+ metadata.gz: cd5c99b547004d2f6521f882b006da2b612ee74cabf92775a3b061d9f096bc20
4
+ data.tar.gz: 652bce88c043068ecd97ef7008044557f764661a2e792f1128d435d4e83d715a
5
5
  SHA512:
6
- metadata.gz: ebdc376975c3893facd06d793a04eebdfa9387814b131b9f0c5aecbd047fb57dcfadba80c40eee8ff60e52b4a7cbf1e886e67909b5f9b46623ca91b75cdb04b5
7
- data.tar.gz: 9cf48a7154d6336a24504df0ec1cd62d689859f1b8f9df85e313bf69f1db97a17191e813b7f2e10bdd8565644dae88f6b323b1cd109e64995e26ea8d537bb67a
6
+ metadata.gz: ba8297809db361e15cac3cbfbc132aa5545ceee6fe4e3d8b7df9a6250e7585ce930eaf7ba7d15de155975848f0d2deb205dcf8b0ebbdd3e00d96cbfc8d758633
7
+ data.tar.gz: 982406a0ff5f858aeed1569fd4ca1d4536bbb2b9841d791b4f560475b19b77641db0831fac7efd6760c3493216a9db8635de55d5e146f158cc0f16297fc97895
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.32"
12
+ gem "leptris", "~> 1.9.174"
13
13
  gem "libxml-ruby", "~> 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
@@ -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
- return ::Leptris::XML::Node.wrap(ptr, doc) if doc
104
-
105
- ::Leptris::XML::Node.wrap(ptr, nil)
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
- expression_uses_parent_axis?(expression)
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
- result = if namespaces && !namespaces.empty?
1328
- compiled.eval(node, namespaces)
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
- compiled.eval(node)
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
@@ -140,6 +140,7 @@ module Moxml
140
140
  end
141
141
 
142
142
  def set_root(doc, element)
143
+ doc.delete_element(doc.root) if doc.root
143
144
  doc.add_element(element)
144
145
  end
145
146
 
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.42"
4
+ VERSION = "0.5.44"
5
5
  end
@@ -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
- @cache = {}
15
- @access_order = []
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 @cache.key?(key)
25
- # Move to end (most recently used)
26
- @access_order.delete(key)
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
- if @cache.key?(key)
43
- @access_order.delete(key)
44
- elsif @cache.size >= @max_size
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 @cache.key?(key)
55
+ return unless @entries.key?(key)
61
56
 
62
- @access_order.delete(key)
63
- @access_order.push(key)
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
- @cache.clear
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
- @cache.size
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
- @cache.key?(key)
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
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.42
4
+ version: 0.5.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.