lutaml-model 0.8.105 → 0.8.107
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/lutaml/model/attribute.rb +9 -3
- data/lib/lutaml/model/configuration.rb +1 -1
- data/lib/lutaml/model/serialize/initialization.rb +13 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +129 -73
- data/lib/lutaml/xml/leptris.rb +20 -9
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +15 -6
- data/spec/lutaml/xml/plan_fast_path_spec.rb +4 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bcd55c9589b05b0eb9c4c9e0231a8e92b9063c3c34a5f9c76da5fc99e2c28f9
|
|
4
|
+
data.tar.gz: eb53b700df96a27985ff448b264fc9c4f022200d0efc8ebb313db635c6618f49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e865d6c056755e187437ebf83d2423b9027df62f0f91822ff85f52541b935219869fb76e3cdd5f98c7422aef828ecea618c454259e82f0c86fa9457ca4e43dc
|
|
7
|
+
data.tar.gz: 65b581acd3765958cbf7d0e2f1607dfc552d60b441ea42d85bbd247a5c04b4487957cd951853c1fc822b3bd79ad701e6c02820476b022af5f73398d7dc8191e4
|
data/Gemfile
CHANGED
|
@@ -49,7 +49,7 @@ gem "rexml"
|
|
|
49
49
|
# exact-URI attribute rows, nested-attribute capture, wildcard rows,
|
|
50
50
|
# walk-side ns retention and leniency, :unqualified rows, and the
|
|
51
51
|
# address-based text attach all ride it.
|
|
52
|
-
gem "leptris", ">= 1.9.
|
|
52
|
+
gem "leptris", ">= 1.9.351.1"
|
|
53
53
|
gem "teptris"
|
|
54
54
|
gem "yeptris" unless Gem.win_platform?
|
|
55
55
|
# TODO: revert rng branch to main when lutaml/rng#32 is merged
|
|
@@ -389,12 +389,18 @@ module Lutaml
|
|
|
389
389
|
|
|
390
390
|
def cast_value(value, register)
|
|
391
391
|
if list_like?(value)
|
|
392
|
-
|
|
392
|
+
items = cast_items(value, register)
|
|
393
393
|
elsif coerce_to_collection?(value)
|
|
394
|
-
|
|
394
|
+
items = cast_items([value], register)
|
|
395
395
|
else
|
|
396
|
-
cast_element(value, register)
|
|
396
|
+
return cast_element(value, register)
|
|
397
397
|
end
|
|
398
|
+
# cast_items already returns a fresh array, so the plain-Array
|
|
399
|
+
# collection needs no re-wrap — build_collection's
|
|
400
|
+
# collection_class.new would copy it again.
|
|
401
|
+
return items if !custom_collection? && collection_class == ::Array
|
|
402
|
+
|
|
403
|
+
build_collection(items, register: register)
|
|
398
404
|
end
|
|
399
405
|
|
|
400
406
|
# Plain Arrays count as list input. On singular attributes
|
|
@@ -223,6 +223,7 @@ module Lutaml
|
|
|
223
223
|
# Clear memoized attribute merge (see .attributes)
|
|
224
224
|
@merged_attributes_cache = nil
|
|
225
225
|
@instantiate_writers = nil
|
|
226
|
+
@instantiate_attr_pairs = nil
|
|
226
227
|
|
|
227
228
|
# Clear per-Attribute type caches (stale entries from GC'd TypeContext objects)
|
|
228
229
|
class_attributes.each_value(&:clear_type_cache)
|
|
@@ -402,7 +403,10 @@ module Lutaml
|
|
|
402
403
|
attrs_by = attributes(register_id)
|
|
403
404
|
writers = instantiate_writers(register_id)
|
|
404
405
|
given = 0
|
|
405
|
-
|
|
406
|
+
# Memoized pair array: Hash#each mints a [name, attr] pair
|
|
407
|
+
# per attribute per instance (~5 allocs/instance on
|
|
408
|
+
# hydration-heavy corpora); Array#each reuses its elements.
|
|
409
|
+
instantiate_attr_pairs(register_id).each do |name, attr|
|
|
406
410
|
next if attr.derived?
|
|
407
411
|
|
|
408
412
|
# Symbol keys hit directly (native-extension callers);
|
|
@@ -468,6 +472,14 @@ module Lutaml
|
|
|
468
472
|
end
|
|
469
473
|
end
|
|
470
474
|
|
|
475
|
+
# [name, attr] pairs in attributes order — instantiate's loop
|
|
476
|
+
# iterates this instead of the attributes Hash so the per-call
|
|
477
|
+
# pair array disappears (see the instantiate comment).
|
|
478
|
+
def instantiate_attr_pairs(register_id = nil)
|
|
479
|
+
(@instantiate_attr_pairs ||= {})[register_id] ||=
|
|
480
|
+
attributes(register_id).to_a.freeze
|
|
481
|
+
end
|
|
482
|
+
|
|
471
483
|
# Define register-specific attribute methods on the class itself.
|
|
472
484
|
#
|
|
473
485
|
# Called once per (class, register) combination. Replaces per-instance
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -15,6 +15,10 @@ module Lutaml
|
|
|
15
15
|
# (their values echo name and type_tag; native collection values
|
|
16
16
|
# echo neither — leptris-ruby#220).
|
|
17
17
|
module PlanHydrator
|
|
18
|
+
# Row kinds whose child type/plan resolve once per plan (see
|
|
19
|
+
# child_row_plans).
|
|
20
|
+
CHILD_ROW_KINDS = %i[nested ordered_deferred].freeze
|
|
21
|
+
|
|
18
22
|
class << self
|
|
19
23
|
# plan: the compiled plan entry for model_class
|
|
20
24
|
# value: the walk root PlanValue (element)
|
|
@@ -41,7 +45,7 @@ module Lutaml
|
|
|
41
45
|
element_buckets(node, plan[:contested_names])
|
|
42
46
|
end
|
|
43
47
|
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
44
|
-
child_kwargs, children, delegates =
|
|
48
|
+
child_kwargs, children, delegates, run_children =
|
|
45
49
|
children_kwargs(model_class, plan, value, buckets, register)
|
|
46
50
|
plan[:collection_defaults].each do |name|
|
|
47
51
|
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
@@ -51,8 +55,8 @@ module Lutaml
|
|
|
51
55
|
# TODO.max-perf/33: the bulk constructor — allocate +
|
|
52
56
|
# compiled writers, from_hash semantics — measured ~30x on
|
|
53
57
|
# hydration-heavy corpora versus the generic constructor.
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
attr_kwargs.update(child_kwargs)
|
|
59
|
+
instance = model_class.instantiate(attr_kwargs, register)
|
|
56
60
|
instance.lutaml_parent = parent if parent
|
|
57
61
|
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
58
62
|
# Root prefix fidelity (the interpretive transform records
|
|
@@ -84,7 +88,8 @@ module Lutaml
|
|
|
84
88
|
child.lutaml_parent = instance
|
|
85
89
|
child.lutaml_root ||= instance.lutaml_root || instance
|
|
86
90
|
end
|
|
87
|
-
interpret_deferred(model_class, plan, value, instance, node,
|
|
91
|
+
interpret_deferred(model_class, plan, value, instance, node,
|
|
92
|
+
buckets, run_children)
|
|
88
93
|
route_delegates(delegates, instance)
|
|
89
94
|
instance
|
|
90
95
|
end
|
|
@@ -144,8 +149,12 @@ module Lutaml
|
|
|
144
149
|
kwargs = {}
|
|
145
150
|
sole_claimants = sole_claimant_names(plan)
|
|
146
151
|
ns_attr_names = plan[:ns_attr_names]
|
|
147
|
-
plan_attr_rows(plan)
|
|
148
|
-
|
|
152
|
+
rows = plan_attr_rows(plan)
|
|
153
|
+
# #1670 batch face (leptris 1.9.351.1): one FFI crossing
|
|
154
|
+
# per node instead of one per (node x attribute name).
|
|
155
|
+
bound = value.attributes_snapshot
|
|
156
|
+
rows.each do |rule, attr, name|
|
|
157
|
+
v = bound[name]
|
|
149
158
|
if v.nil? && node && sole_claimants.include?(name)
|
|
150
159
|
# #754/#790 parity: a sole-claimant attribute rule binds
|
|
151
160
|
# any qualification (the interpretive matcher's lenient
|
|
@@ -235,7 +244,8 @@ module Lutaml
|
|
|
235
244
|
def children_kwargs(_model_class, plan, value, buckets = nil,
|
|
236
245
|
register = nil)
|
|
237
246
|
register ||= Lutaml::Model::Config.default_register
|
|
238
|
-
grouped, tagged = group_children(value,
|
|
247
|
+
grouped, tagged, run_children = group_children(value,
|
|
248
|
+
plan[:row_tags])
|
|
239
249
|
buckets = nil unless buckets && plan[:needs_nodes]
|
|
240
250
|
kwargs = {}
|
|
241
251
|
children = []
|
|
@@ -244,10 +254,21 @@ module Lutaml
|
|
|
244
254
|
# the block-backed Hash is a fresh object per hydrated row.
|
|
245
255
|
spellings = nil
|
|
246
256
|
row_tags = plan[:row_tags]
|
|
257
|
+
wire_names = row_wire_names(plan)
|
|
258
|
+
row_memo = child_row_plans(plan, register)
|
|
259
|
+
# Array#each with a top-of-loop counter: each_with_index
|
|
260
|
+
# mints an enumerator per hydrated node — 14.7K allocations
|
|
261
|
+
# per parse on the pnas guardrail — and next must keep its
|
|
262
|
+
# continue semantics.
|
|
263
|
+
rows = plan[:rows]
|
|
264
|
+
idx = -1
|
|
247
265
|
# rubocop:disable Metrics/BlockLength -- one row-kind
|
|
248
266
|
# dispatch table; splitting it would scatter the contract
|
|
249
|
-
|
|
267
|
+
rows.each do |row|
|
|
268
|
+
idx += 1
|
|
269
|
+
rule, attr, kind, spelling, delegate, _row_type = row
|
|
250
270
|
tag = row_tags&.[](idx)
|
|
271
|
+
name = wire_names[idx]
|
|
251
272
|
case kind
|
|
252
273
|
when :scalar
|
|
253
274
|
# Raw passthrough: the model constructor is the single
|
|
@@ -265,7 +286,7 @@ module Lutaml
|
|
|
265
286
|
first = if tag
|
|
266
287
|
tagged[tag]&.first
|
|
267
288
|
else
|
|
268
|
-
grouped.dig(
|
|
289
|
+
grouped.dig(name, 0)
|
|
269
290
|
end
|
|
270
291
|
v = first&.string_value
|
|
271
292
|
v = mapped_empty_value(rule) if v.nil? && first
|
|
@@ -277,7 +298,7 @@ module Lutaml
|
|
|
277
298
|
when :wildcard_any
|
|
278
299
|
# interpreted post-instance (interpret_deferred)
|
|
279
300
|
when :content
|
|
280
|
-
runs = content_runs(value)
|
|
301
|
+
runs = content_runs(value, run_children)
|
|
281
302
|
if runs.empty? && !attr.collection?
|
|
282
303
|
# An empty element's content binds through the rule's
|
|
283
304
|
# value map for scalar content attributes: "" for the
|
|
@@ -293,15 +314,13 @@ module Lutaml
|
|
|
293
314
|
# walk against its own node + element_order from the
|
|
294
315
|
# node's children. Without one (direct PlanHydrator
|
|
295
316
|
# use), the subtree fragment-parses interpretively.
|
|
296
|
-
if buckets
|
|
297
|
-
child_type
|
|
298
|
-
|
|
299
|
-
selected, = child_bucket(buckets, rule, child_plan,
|
|
300
|
-
contested_name?(plan, rule))
|
|
317
|
+
if buckets && row_memo[idx]
|
|
318
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
319
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
301
320
|
items = (selected || []).map do |n|
|
|
302
321
|
call(child_type, child_plan,
|
|
303
322
|
child_plan[:descriptor].walk(n), node: n,
|
|
304
|
-
register:
|
|
323
|
+
register: child_register)
|
|
305
324
|
end
|
|
306
325
|
unless items.empty?
|
|
307
326
|
children.concat(items)
|
|
@@ -314,10 +333,10 @@ module Lutaml
|
|
|
314
333
|
# source node casts through the attribute — the full
|
|
315
334
|
# interpretive machinery on the bridged island,
|
|
316
335
|
# identical semantics to the interpretive path.
|
|
317
|
-
contested_uri = if plan[:contested_names]&.include?(
|
|
336
|
+
contested_uri = if plan[:contested_names]&.include?(name)
|
|
318
337
|
child_type_uri(attr, register)
|
|
319
338
|
end
|
|
320
|
-
elements = deferred_elements(value,
|
|
339
|
+
elements = deferred_elements(value, name, buckets,
|
|
321
340
|
contested_uri)
|
|
322
341
|
next if elements.empty?
|
|
323
342
|
|
|
@@ -330,7 +349,7 @@ module Lutaml
|
|
|
330
349
|
attr.collection? ? results : results.first)
|
|
331
350
|
end
|
|
332
351
|
when :collection_cb
|
|
333
|
-
values = grouped[
|
|
352
|
+
values = grouped[name].to_a.map do |item|
|
|
334
353
|
item&.string_value
|
|
335
354
|
end
|
|
336
355
|
values = mapped_capture_values(values, rule)
|
|
@@ -344,7 +363,7 @@ module Lutaml
|
|
|
344
363
|
values = if tag
|
|
345
364
|
tagged[tag].to_a.map(&:string_value)
|
|
346
365
|
else
|
|
347
|
-
native_collection(value,
|
|
366
|
+
native_collection(value, name)
|
|
348
367
|
end
|
|
349
368
|
values = mapped_capture_values(values, rule) unless values.nil?
|
|
350
369
|
unless values.nil?
|
|
@@ -354,29 +373,33 @@ module Lutaml
|
|
|
354
373
|
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
355
374
|
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
356
375
|
when :nested
|
|
357
|
-
child_type
|
|
358
|
-
|
|
359
|
-
selected
|
|
360
|
-
contested_name?(plan, rule))
|
|
361
|
-
if selected && plan[:contested_names]&.include?(rule.name.to_s)
|
|
376
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
377
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
378
|
+
if selected && contested
|
|
362
379
|
# Contested name: the walk's grouped values cross-echo
|
|
363
380
|
# every same-name element into every row — dispatch
|
|
364
381
|
# per source node instead (the walk is row-scoped).
|
|
365
382
|
items = selected.map do |n|
|
|
366
383
|
call(child_type, child_plan,
|
|
367
384
|
child_plan[:descriptor].walk(n), node: n,
|
|
368
|
-
register:
|
|
385
|
+
register: child_register)
|
|
369
386
|
end
|
|
370
387
|
else
|
|
371
|
-
values = grouped.fetch(
|
|
388
|
+
values = grouped.fetch(name, [])
|
|
372
389
|
cursor = if child_plan[:needs_nodes] && buckets
|
|
373
390
|
selected
|
|
374
391
|
end
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
# Indexed loop: each_with_index.map mints an
|
|
393
|
+
# enumerator per node with nested rows.
|
|
394
|
+
count = values.size
|
|
395
|
+
items = Array.new(count)
|
|
396
|
+
pos = 0
|
|
397
|
+
while pos < count
|
|
398
|
+
items[pos] = call(child_type, child_plan, values[pos],
|
|
399
|
+
node: cursor && cursor[pos],
|
|
400
|
+
register: child_register)
|
|
401
|
+
pos += 1
|
|
402
|
+
end
|
|
380
403
|
end
|
|
381
404
|
next if items.empty?
|
|
382
405
|
|
|
@@ -399,7 +422,7 @@ module Lutaml
|
|
|
399
422
|
attr.collection? ? values : values.first)
|
|
400
423
|
end
|
|
401
424
|
end
|
|
402
|
-
[kwargs, children, delegates]
|
|
425
|
+
[kwargs, children, delegates, run_children]
|
|
403
426
|
end
|
|
404
427
|
|
|
405
428
|
# Deferred islands: raw subtrees become wrapper elements via a
|
|
@@ -410,11 +433,20 @@ module Lutaml
|
|
|
410
433
|
# node (no fragment parse — the reparse dominated deferred
|
|
411
434
|
# hydration). Ordered children only land here without a node.
|
|
412
435
|
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
413
|
-
buckets = nil)
|
|
414
|
-
|
|
436
|
+
buckets = nil, run_children = nil)
|
|
437
|
+
# Array#each + top counter: no per-node enumerator (see
|
|
438
|
+
# children_kwargs).
|
|
439
|
+
rows = plan[:rows]
|
|
440
|
+
idx = -1
|
|
441
|
+
rows.each do |row|
|
|
442
|
+
idx += 1
|
|
443
|
+
rule = row[0]
|
|
444
|
+
attr = row[1]
|
|
445
|
+
kind = row[2]
|
|
446
|
+
wire_name = row_wire_names(plan)[idx]
|
|
415
447
|
case kind
|
|
416
448
|
when :content_deferred
|
|
417
|
-
runs = content_runs(value)
|
|
449
|
+
runs = content_runs(value, run_children)
|
|
418
450
|
if runs.empty?
|
|
419
451
|
# The interpretive pipeline classifies the raw ""
|
|
420
452
|
# through the rule's value map: the :empty directive
|
|
@@ -437,13 +469,13 @@ module Lutaml
|
|
|
437
469
|
# concatenated).
|
|
438
470
|
instance.public_send(:"#{attr.name}=", runs.join)
|
|
439
471
|
when :raw
|
|
440
|
-
raws = raw_strings(value,
|
|
472
|
+
raws = raw_strings(value, wire_name)
|
|
441
473
|
next if raws.empty?
|
|
442
474
|
|
|
443
475
|
instance.public_send(:"#{attr.name}=",
|
|
444
476
|
attr.collection? ? raws : raws.first)
|
|
445
477
|
when :custom_method
|
|
446
|
-
elements = deferred_elements(value,
|
|
478
|
+
elements = deferred_elements(value, wire_name, buckets)
|
|
447
479
|
next if elements.empty?
|
|
448
480
|
|
|
449
481
|
args = attr.collection? ? elements : elements.first
|
|
@@ -453,7 +485,7 @@ module Lutaml
|
|
|
453
485
|
when :ordered_deferred
|
|
454
486
|
next if node # hydrated natively in children_kwargs
|
|
455
487
|
|
|
456
|
-
results = deferred_elements(value,
|
|
488
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
457
489
|
attr.cast(element, :xml, register,
|
|
458
490
|
lutaml_parent: instance,
|
|
459
491
|
lutaml_root: instance.lutaml_root || instance)
|
|
@@ -496,7 +528,7 @@ module Lutaml
|
|
|
496
528
|
attr.collection? ? results : results.first,
|
|
497
529
|
)
|
|
498
530
|
when :polymorphic
|
|
499
|
-
results = deferred_elements(value,
|
|
531
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
500
532
|
attr.cast(element, :xml, register,
|
|
501
533
|
polymorphic: rule.polymorphic,
|
|
502
534
|
lutaml_parent: instance,
|
|
@@ -515,16 +547,16 @@ module Lutaml
|
|
|
515
547
|
# construction only, no parse), falling back to a fragment
|
|
516
548
|
# parse of the captured subtree when no node is available.
|
|
517
549
|
# Both lists are document-ordered for the row's name.
|
|
518
|
-
def deferred_elements(value,
|
|
519
|
-
return raw_strings(value,
|
|
550
|
+
def deferred_elements(value, name, buckets, contested_uri = nil)
|
|
551
|
+
return raw_strings(value, name).map { |raw| fragment_element(raw) } unless buckets
|
|
520
552
|
|
|
521
|
-
nodes = buckets[
|
|
553
|
+
nodes = buckets[name]
|
|
522
554
|
return [] unless nodes
|
|
523
555
|
|
|
524
556
|
if contested_uri
|
|
525
557
|
# Contested name: bind only the (URI, local) matches —
|
|
526
558
|
# the type's own mapping namespace is the row identity.
|
|
527
|
-
nodes = buckets[[contested_uri,
|
|
559
|
+
nodes = buckets[[contested_uri, name]] || []
|
|
528
560
|
end
|
|
529
561
|
nodes.map { |n| bridge_element(n) }
|
|
530
562
|
end
|
|
@@ -580,15 +612,14 @@ module Lutaml
|
|
|
580
612
|
end
|
|
581
613
|
end
|
|
582
614
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
615
|
+
# Content runs ride the children snapshot: the nil-name
|
|
616
|
+
# entries ARE the :collection run groups (one crossing per
|
|
617
|
+
# node via group_children, none here). The per-group
|
|
618
|
+
# at/count crossings are the drain itself.
|
|
619
|
+
def content_runs(_value, run_children)
|
|
620
|
+
(run_children || []).flat_map do |group|
|
|
621
|
+
Array.new(group.count) { |j| group.at(j).string_value }
|
|
590
622
|
end
|
|
591
|
-
out
|
|
592
623
|
end
|
|
593
624
|
|
|
594
625
|
# Native collection rows echo their producing row's name
|
|
@@ -603,11 +634,11 @@ module Lutaml
|
|
|
603
634
|
nil
|
|
604
635
|
end
|
|
605
636
|
|
|
606
|
-
def raw_strings(value,
|
|
637
|
+
def raw_strings(value, name)
|
|
607
638
|
out = []
|
|
608
639
|
value.count.times do |idx|
|
|
609
640
|
child = value.at(idx)
|
|
610
|
-
if child.name ==
|
|
641
|
+
if child.name == name && child.kind == :raw
|
|
611
642
|
out << child.string_value
|
|
612
643
|
end
|
|
613
644
|
end
|
|
@@ -656,8 +687,14 @@ module Lutaml
|
|
|
656
687
|
grouped = {}
|
|
657
688
|
want_tags = !row_tags.nil?
|
|
658
689
|
tagged = want_tags ? {} : nil
|
|
690
|
+
run_children = nil
|
|
659
691
|
names.each_with_index do |name, i|
|
|
660
|
-
|
|
692
|
+
if name.nil?
|
|
693
|
+
# Content runs: same snapshot pass — at()/count() per
|
|
694
|
+
# child cost a crossing and a wrapper each.
|
|
695
|
+
(run_children ||= []) << children[i]
|
|
696
|
+
next
|
|
697
|
+
end
|
|
661
698
|
|
|
662
699
|
child = children[i]
|
|
663
700
|
(grouped[name] ||= []) << child
|
|
@@ -665,7 +702,7 @@ module Lutaml
|
|
|
665
702
|
(tagged[tags[i]] ||= []) << child
|
|
666
703
|
end
|
|
667
704
|
end
|
|
668
|
-
[grouped, tagged]
|
|
705
|
+
[grouped, tagged, run_children]
|
|
669
706
|
end
|
|
670
707
|
|
|
671
708
|
# Element children bucketed by local name, document order
|
|
@@ -693,27 +730,46 @@ module Lutaml
|
|
|
693
730
|
buckets
|
|
694
731
|
end
|
|
695
732
|
|
|
696
|
-
#
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
# the walk's grouped values for position filtering).
|
|
705
|
-
def child_bucket(buckets, rule, child_plan, contested)
|
|
706
|
-
name = rule.name.to_s
|
|
707
|
-
all = buckets ? buckets[name] : nil
|
|
708
|
-
return [all, all] unless buckets && all && contested
|
|
733
|
+
# Selected source nodes for a child row: contested names
|
|
734
|
+
# dispatch by (URI, local); every other row binds the whole
|
|
735
|
+
# name bucket leniently (any qualification). The pair form
|
|
736
|
+
# this replaces allocated a fresh array per row per node on
|
|
737
|
+
# the ordered walk (niso gate).
|
|
738
|
+
def child_selection(buckets, name, child_plan, contested)
|
|
739
|
+
all = buckets && buckets[name]
|
|
740
|
+
return all unless buckets && all && contested
|
|
709
741
|
|
|
710
742
|
tree_ns = child_plan[:tree][:ns]
|
|
711
743
|
exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
744
|
+
exact_uri ? (buckets[[exact_uri, name]] || []) : all
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Per-(plan, register) child-row tuples, computed once and
|
|
748
|
+
# reused across every node the walk hands this plan: the
|
|
749
|
+
# per-node type lookup, compile hit, child register
|
|
750
|
+
# resolution, and contested flag were each a measurable slice
|
|
751
|
+
# of the ordered-lane allocation profile (niso gate).
|
|
752
|
+
def child_row_plans(plan, register)
|
|
753
|
+
plan[:child_row_memo] ||= {}
|
|
754
|
+
plan[:child_row_memo][register] ||= plan[:rows].map do |rule, attr, kind, _spelling, _delegate, row_type|
|
|
755
|
+
next nil unless CHILD_ROW_KINDS.include?(kind)
|
|
756
|
+
|
|
757
|
+
child_type = row_type || attr.type(register)
|
|
758
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
759
|
+
child_register = Lutaml::Model::Register.resolve_for_child(
|
|
760
|
+
child_type, register
|
|
761
|
+
)
|
|
762
|
+
[child_type, child_plan, child_register,
|
|
763
|
+
contested_name?(plan, rule)]
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Plan-frozen wire names: rule.name.to_s minted a fresh string
|
|
768
|
+
# per row per node in the hot loop.
|
|
769
|
+
def row_wire_names(plan)
|
|
770
|
+
plan[:row_wire_names] ||= plan[:rows].map do |rule, _attr, _kind, _spelling, _delegate, _row_type|
|
|
771
|
+
rule.name.to_s
|
|
715
772
|
end
|
|
716
|
-
[all, all]
|
|
717
773
|
end
|
|
718
774
|
end
|
|
719
775
|
end
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -8,22 +8,33 @@ module Lutaml
|
|
|
8
8
|
# reference leptris types directly (the moxml adapters and the
|
|
9
9
|
# interpretive path never enter here).
|
|
10
10
|
module Leptris
|
|
11
|
-
# Single capability floor (leptris 1.9.
|
|
11
|
+
# Single capability floor (leptris 1.9.351.1): the child-handles
|
|
12
12
|
# snapshot (1.9.273), exact-URI attribute rows (#1486, 1.9.289),
|
|
13
13
|
# nested-attribute capture (#1563, 1.9.313), wildcard rows
|
|
14
14
|
# (#1552, 1.9.313), child-row ns_uri retention and qualified
|
|
15
15
|
# attribute leniency (#1585/#1586, 1.9.320), the :unqualified ns
|
|
16
|
-
# form (#1560, 1.9.323),
|
|
17
|
-
# text attach (#408, 1.9.334/1.9.336)
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
16
|
+
# form (#1560, 1.9.323), build_result with the address-based
|
|
17
|
+
# text attach (#408, 1.9.334/1.9.336), and the plan-value
|
|
18
|
+
# attributes batch face (PlanValue#attributes_snapshot,
|
|
19
|
+
# leptris-ruby#447). Bundles resolving an older gem keep the
|
|
20
|
+
# interpretive path — PlanCompiler.compile answers nil — the
|
|
21
|
+
# opt-out protocol.
|
|
22
|
+
MIN_LEPTRIS_VERSION = "1.9.351.1"
|
|
21
23
|
|
|
24
|
+
# Memoized: the compiler consults this on every cache hit (per
|
|
25
|
+
# child row per node), and the comparison minted a Gem::Version
|
|
26
|
+
# per call — visible in the niso ordered-lane allocation profile.
|
|
27
|
+
# The loaded gem cannot change mid-process.
|
|
22
28
|
def self.plan_path_compatible?
|
|
23
|
-
return
|
|
29
|
+
return @plan_path_compatible if defined?(@plan_path_compatible)
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
@plan_path_compatible =
|
|
32
|
+
if defined?(Gem)
|
|
33
|
+
spec = Gem.loaded_specs["leptris"]
|
|
34
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
35
|
+
else
|
|
36
|
+
false
|
|
37
|
+
end
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
autoload :NativeEmission, "lutaml/xml/leptris/native_emission"
|
|
@@ -10,15 +10,24 @@ require "lutaml/xml/adapter/nokogiri_adapter"
|
|
|
10
10
|
# must surface in the raised message — otherwise the error names the
|
|
11
11
|
# input's emptiness while the input is neither empty nor the problem
|
|
12
12
|
# (lutaml-model#956).
|
|
13
|
+
#
|
|
14
|
+
# moxml#369 changed the recoverable-malformation contract: the leptris
|
|
15
|
+
# adapter's non-strict parse now RECOVERS (the engine's recover mode,
|
|
16
|
+
# leptris#1660) and returns the best-effort tree instead of failing —
|
|
17
|
+
# a mismatched closer yields a document, not a raise. Only inputs with
|
|
18
|
+
# nothing to recover into (whitespace-only) still reach the guard,
|
|
19
|
+
# carrying the recovery diagnostic.
|
|
13
20
|
RSpec.describe "empty-document errors surface engine parse diagnostics" do
|
|
14
21
|
describe Lutaml::Xml::Adapter::LeptrisAdapter do
|
|
15
|
-
it "
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
expect(error.message).to include("XML parse error at byte 14")
|
|
22
|
+
it "returns a recovered tree for recoverable malformations (moxml#369)" do
|
|
23
|
+
version = Gem.loaded_specs.fetch("moxml").version
|
|
24
|
+
recovered = version >= Gem::Version.new("0.5.131")
|
|
25
|
+
unless recovered
|
|
26
|
+
skip "moxml predates the recover-mode wiring (0.5.131)"
|
|
21
27
|
end
|
|
28
|
+
|
|
29
|
+
doc = described_class.parse("<root><a>x</b></root>")
|
|
30
|
+
expect(doc.root.name).to eq("root")
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
it "reports the engine diagnostic for whitespace-only input" do
|
|
@@ -31,13 +31,14 @@ RSpec.describe "XML plan fast path" do
|
|
|
31
31
|
let(:root_class) { classes[1] }
|
|
32
32
|
|
|
33
33
|
around do |example|
|
|
34
|
-
|
|
34
|
+
old_fast = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
35
|
+
old_ordered = Lutaml::Model::Config.instance.xml_plan_ordered_path
|
|
35
36
|
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
36
37
|
Lutaml::Model::Config.instance.xml_plan_ordered_path = true
|
|
37
38
|
example.run
|
|
38
39
|
ensure
|
|
39
|
-
Lutaml::Model::Config.instance.xml_plan_fast_path =
|
|
40
|
-
Lutaml::Model::Config.instance.xml_plan_ordered_path =
|
|
40
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old_fast
|
|
41
|
+
Lutaml::Model::Config.instance.xml_plan_ordered_path = old_ordered
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Real-corpus crash (niso-jats): sec-in-sec nesting is a
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.107
|
|
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-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|