lutaml-model 0.8.104 → 0.8.106
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc1bb0f95876011fac44764d7ea55c334d24ea2a20911e46f4200448cf50cc1e
|
|
4
|
+
data.tar.gz: 23f02dbcf3dac25a19a50f4a8bf75b81d642b64514c0aeb9a820d727be63406f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d649b8eef84ef0cfa60ec5fec9e70b0fa45cdc44508325f12c7a17d66e3ca437535c3bf56bc725217f73d305046c9102d7dba067148f4efdec8996d2f2f8ec2
|
|
7
|
+
data.tar.gz: d81707bac3fb9bf9ebc3091c421253e3b9b8abd085891e03debcd19674af75ae5e6dc5f1b724fbb05e160847694dcc8752e9eb1fb17425a394c1babc67777273
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -20,7 +20,6 @@ module Lutaml
|
|
|
20
20
|
raw_xml = xml
|
|
21
21
|
xml = normalize_xml_for_parse(xml)
|
|
22
22
|
parsed = parse_with_moxml(xml, parse_encoding)
|
|
23
|
-
assert_no_truncated_recovered_parse!(parsed)
|
|
24
23
|
root_element = parsed.root
|
|
25
24
|
|
|
26
25
|
raise_empty_document_error(parsed.parse_errors) if root_element.nil?
|
|
@@ -31,31 +30,6 @@ module Lutaml
|
|
|
31
30
|
new(root, parse_encoding, **parse_document_options(raw_xml))
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
# Recover-mode parsing must never masquerade input truncation as
|
|
35
|
-
# success.
|
|
36
|
-
#
|
|
37
|
-
# moxml releases before 0.5.84 parsed Nokogiri documents without
|
|
38
|
-
# XML_PARSE_HUGE, so libxml2 capped its input buffer at 10 MB;
|
|
39
|
-
# longer documents tripped a fatal "Resource limit exceeded:
|
|
40
|
-
# Buffer size limit exceeded, try XML_PARSE_HUGE" error mid-input.
|
|
41
|
-
# In recover mode Nokogiri records that error on the document and
|
|
42
|
-
# returns the partial tree, so every node past the limit silently
|
|
43
|
-
# vanishes (lutaml-model#871). A resource-limit fatal means the
|
|
44
|
-
# engine stopped early with input left — refuse to deserialize the
|
|
45
|
-
# partial document instead of dropping trailing content without a
|
|
46
|
-
# trace.
|
|
47
|
-
#
|
|
48
|
-
# moxml 0.5.84 and later always pass XML_PARSE_HUGE, so the
|
|
49
|
-
# Nokogiri path no longer truncates and this guard stays silent;
|
|
50
|
-
# it remains for older moxml releases and for any other adapter
|
|
51
|
-
# that reports the resource-limit family.
|
|
52
|
-
#
|
|
53
|
-
# Other recover-mode fatals (e.g. an XML declaration after leading
|
|
54
|
-
# whitespace) do not drop content and keep the long-standing
|
|
55
|
-
# recover behavior; only the resource-limit family is truncating.
|
|
56
|
-
TRUNCATION_FATAL_MARKER =
|
|
57
|
-
/Resource limit exceeded|Buffer size limit exceeded/
|
|
58
|
-
|
|
59
33
|
private
|
|
60
34
|
|
|
61
35
|
def normalize_xml_for_parse(xml)
|
|
@@ -88,23 +62,6 @@ module Lutaml
|
|
|
88
62
|
end
|
|
89
63
|
end
|
|
90
64
|
|
|
91
|
-
def assert_no_truncated_recovered_parse!(parsed)
|
|
92
|
-
errors = parsed.parse_errors
|
|
93
|
-
return if errors.nil? || errors.empty?
|
|
94
|
-
|
|
95
|
-
fatal = errors.find do |message|
|
|
96
|
-
message.match?(TRUNCATION_FATAL_MARKER)
|
|
97
|
-
end
|
|
98
|
-
return if fatal.nil?
|
|
99
|
-
|
|
100
|
-
raise Lutaml::Model::InvalidFormatError.new(
|
|
101
|
-
:xml,
|
|
102
|
-
"the XML engine reported a fatal resource limit and recovered " \
|
|
103
|
-
"with a truncated document; refusing to deserialize " \
|
|
104
|
-
"partial input (#{fatal})",
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
65
|
def parse_document_options(xml)
|
|
109
66
|
{
|
|
110
67
|
doctype: extract_doctype_from_xml(xml),
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -244,10 +248,13 @@ module Lutaml
|
|
|
244
248
|
# the block-backed Hash is a fresh object per hydrated row.
|
|
245
249
|
spellings = nil
|
|
246
250
|
row_tags = plan[:row_tags]
|
|
251
|
+
wire_names = row_wire_names(plan)
|
|
252
|
+
row_memo = child_row_plans(plan, register)
|
|
247
253
|
# rubocop:disable Metrics/BlockLength -- one row-kind
|
|
248
254
|
# dispatch table; splitting it would scatter the contract
|
|
249
|
-
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate,
|
|
255
|
+
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, _row_type), idx|
|
|
250
256
|
tag = row_tags&.[](idx)
|
|
257
|
+
name = wire_names[idx]
|
|
251
258
|
case kind
|
|
252
259
|
when :scalar
|
|
253
260
|
# Raw passthrough: the model constructor is the single
|
|
@@ -265,7 +272,7 @@ module Lutaml
|
|
|
265
272
|
first = if tag
|
|
266
273
|
tagged[tag]&.first
|
|
267
274
|
else
|
|
268
|
-
grouped.dig(
|
|
275
|
+
grouped.dig(name, 0)
|
|
269
276
|
end
|
|
270
277
|
v = first&.string_value
|
|
271
278
|
v = mapped_empty_value(rule) if v.nil? && first
|
|
@@ -293,15 +300,13 @@ module Lutaml
|
|
|
293
300
|
# walk against its own node + element_order from the
|
|
294
301
|
# node's children. Without one (direct PlanHydrator
|
|
295
302
|
# 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))
|
|
303
|
+
if buckets && row_memo[idx]
|
|
304
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
305
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
301
306
|
items = (selected || []).map do |n|
|
|
302
307
|
call(child_type, child_plan,
|
|
303
308
|
child_plan[:descriptor].walk(n), node: n,
|
|
304
|
-
register:
|
|
309
|
+
register: child_register)
|
|
305
310
|
end
|
|
306
311
|
unless items.empty?
|
|
307
312
|
children.concat(items)
|
|
@@ -314,10 +319,10 @@ module Lutaml
|
|
|
314
319
|
# source node casts through the attribute — the full
|
|
315
320
|
# interpretive machinery on the bridged island,
|
|
316
321
|
# identical semantics to the interpretive path.
|
|
317
|
-
contested_uri = if plan[:contested_names]&.include?(
|
|
322
|
+
contested_uri = if plan[:contested_names]&.include?(name)
|
|
318
323
|
child_type_uri(attr, register)
|
|
319
324
|
end
|
|
320
|
-
elements = deferred_elements(value,
|
|
325
|
+
elements = deferred_elements(value, name, buckets,
|
|
321
326
|
contested_uri)
|
|
322
327
|
next if elements.empty?
|
|
323
328
|
|
|
@@ -330,7 +335,7 @@ module Lutaml
|
|
|
330
335
|
attr.collection? ? results : results.first)
|
|
331
336
|
end
|
|
332
337
|
when :collection_cb
|
|
333
|
-
values = grouped[
|
|
338
|
+
values = grouped[name].to_a.map do |item|
|
|
334
339
|
item&.string_value
|
|
335
340
|
end
|
|
336
341
|
values = mapped_capture_values(values, rule)
|
|
@@ -344,7 +349,7 @@ module Lutaml
|
|
|
344
349
|
values = if tag
|
|
345
350
|
tagged[tag].to_a.map(&:string_value)
|
|
346
351
|
else
|
|
347
|
-
native_collection(value,
|
|
352
|
+
native_collection(value, name)
|
|
348
353
|
end
|
|
349
354
|
values = mapped_capture_values(values, rule) unless values.nil?
|
|
350
355
|
unless values.nil?
|
|
@@ -354,28 +359,26 @@ module Lutaml
|
|
|
354
359
|
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
355
360
|
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
356
361
|
when :nested
|
|
357
|
-
child_type
|
|
358
|
-
|
|
359
|
-
selected
|
|
360
|
-
contested_name?(plan, rule))
|
|
361
|
-
if selected && plan[:contested_names]&.include?(rule.name.to_s)
|
|
362
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
363
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
364
|
+
if selected && contested
|
|
362
365
|
# Contested name: the walk's grouped values cross-echo
|
|
363
366
|
# every same-name element into every row — dispatch
|
|
364
367
|
# per source node instead (the walk is row-scoped).
|
|
365
368
|
items = selected.map do |n|
|
|
366
369
|
call(child_type, child_plan,
|
|
367
370
|
child_plan[:descriptor].walk(n), node: n,
|
|
368
|
-
register:
|
|
371
|
+
register: child_register)
|
|
369
372
|
end
|
|
370
373
|
else
|
|
371
|
-
values = grouped.fetch(
|
|
374
|
+
values = grouped.fetch(name, [])
|
|
372
375
|
cursor = if child_plan[:needs_nodes] && buckets
|
|
373
376
|
selected
|
|
374
377
|
end
|
|
375
378
|
items = values.each_with_index
|
|
376
379
|
.map do |v, i|
|
|
377
380
|
call(child_type, child_plan, v,
|
|
378
|
-
node: cursor && cursor[i], register:
|
|
381
|
+
node: cursor && cursor[i], register: child_register)
|
|
379
382
|
end
|
|
380
383
|
end
|
|
381
384
|
next if items.empty?
|
|
@@ -411,7 +414,11 @@ module Lutaml
|
|
|
411
414
|
# hydration). Ordered children only land here without a node.
|
|
412
415
|
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
413
416
|
buckets = nil)
|
|
414
|
-
plan[:rows].
|
|
417
|
+
plan[:rows].each_with_index do |row, idx|
|
|
418
|
+
rule = row[0]
|
|
419
|
+
attr = row[1]
|
|
420
|
+
kind = row[2]
|
|
421
|
+
wire_name = row_wire_names(plan)[idx]
|
|
415
422
|
case kind
|
|
416
423
|
when :content_deferred
|
|
417
424
|
runs = content_runs(value)
|
|
@@ -437,13 +444,13 @@ module Lutaml
|
|
|
437
444
|
# concatenated).
|
|
438
445
|
instance.public_send(:"#{attr.name}=", runs.join)
|
|
439
446
|
when :raw
|
|
440
|
-
raws = raw_strings(value,
|
|
447
|
+
raws = raw_strings(value, wire_name)
|
|
441
448
|
next if raws.empty?
|
|
442
449
|
|
|
443
450
|
instance.public_send(:"#{attr.name}=",
|
|
444
451
|
attr.collection? ? raws : raws.first)
|
|
445
452
|
when :custom_method
|
|
446
|
-
elements = deferred_elements(value,
|
|
453
|
+
elements = deferred_elements(value, wire_name, buckets)
|
|
447
454
|
next if elements.empty?
|
|
448
455
|
|
|
449
456
|
args = attr.collection? ? elements : elements.first
|
|
@@ -453,7 +460,7 @@ module Lutaml
|
|
|
453
460
|
when :ordered_deferred
|
|
454
461
|
next if node # hydrated natively in children_kwargs
|
|
455
462
|
|
|
456
|
-
results = deferred_elements(value,
|
|
463
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
457
464
|
attr.cast(element, :xml, register,
|
|
458
465
|
lutaml_parent: instance,
|
|
459
466
|
lutaml_root: instance.lutaml_root || instance)
|
|
@@ -496,7 +503,7 @@ module Lutaml
|
|
|
496
503
|
attr.collection? ? results : results.first,
|
|
497
504
|
)
|
|
498
505
|
when :polymorphic
|
|
499
|
-
results = deferred_elements(value,
|
|
506
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
500
507
|
attr.cast(element, :xml, register,
|
|
501
508
|
polymorphic: rule.polymorphic,
|
|
502
509
|
lutaml_parent: instance,
|
|
@@ -515,16 +522,16 @@ module Lutaml
|
|
|
515
522
|
# construction only, no parse), falling back to a fragment
|
|
516
523
|
# parse of the captured subtree when no node is available.
|
|
517
524
|
# Both lists are document-ordered for the row's name.
|
|
518
|
-
def deferred_elements(value,
|
|
519
|
-
return raw_strings(value,
|
|
525
|
+
def deferred_elements(value, name, buckets, contested_uri = nil)
|
|
526
|
+
return raw_strings(value, name).map { |raw| fragment_element(raw) } unless buckets
|
|
520
527
|
|
|
521
|
-
nodes = buckets[
|
|
528
|
+
nodes = buckets[name]
|
|
522
529
|
return [] unless nodes
|
|
523
530
|
|
|
524
531
|
if contested_uri
|
|
525
532
|
# Contested name: bind only the (URI, local) matches —
|
|
526
533
|
# the type's own mapping namespace is the row identity.
|
|
527
|
-
nodes = buckets[[contested_uri,
|
|
534
|
+
nodes = buckets[[contested_uri, name]] || []
|
|
528
535
|
end
|
|
529
536
|
nodes.map { |n| bridge_element(n) }
|
|
530
537
|
end
|
|
@@ -603,11 +610,11 @@ module Lutaml
|
|
|
603
610
|
nil
|
|
604
611
|
end
|
|
605
612
|
|
|
606
|
-
def raw_strings(value,
|
|
613
|
+
def raw_strings(value, name)
|
|
607
614
|
out = []
|
|
608
615
|
value.count.times do |idx|
|
|
609
616
|
child = value.at(idx)
|
|
610
|
-
if child.name ==
|
|
617
|
+
if child.name == name && child.kind == :raw
|
|
611
618
|
out << child.string_value
|
|
612
619
|
end
|
|
613
620
|
end
|
|
@@ -693,27 +700,46 @@ module Lutaml
|
|
|
693
700
|
buckets
|
|
694
701
|
end
|
|
695
702
|
|
|
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
|
|
703
|
+
# Selected source nodes for a child row: contested names
|
|
704
|
+
# dispatch by (URI, local); every other row binds the whole
|
|
705
|
+
# name bucket leniently (any qualification). The pair form
|
|
706
|
+
# this replaces allocated a fresh array per row per node on
|
|
707
|
+
# the ordered walk (niso gate).
|
|
708
|
+
def child_selection(buckets, name, child_plan, contested)
|
|
709
|
+
all = buckets && buckets[name]
|
|
710
|
+
return all unless buckets && all && contested
|
|
709
711
|
|
|
710
712
|
tree_ns = child_plan[:tree][:ns]
|
|
711
713
|
exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
714
|
+
exact_uri ? (buckets[[exact_uri, name]] || []) : all
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Per-(plan, register) child-row tuples, computed once and
|
|
718
|
+
# reused across every node the walk hands this plan: the
|
|
719
|
+
# per-node type lookup, compile hit, child register
|
|
720
|
+
# resolution, and contested flag were each a measurable slice
|
|
721
|
+
# of the ordered-lane allocation profile (niso gate).
|
|
722
|
+
def child_row_plans(plan, register)
|
|
723
|
+
plan[:child_row_memo] ||= {}
|
|
724
|
+
plan[:child_row_memo][register] ||= plan[:rows].map do |rule, attr, kind, _spelling, _delegate, row_type|
|
|
725
|
+
next nil unless CHILD_ROW_KINDS.include?(kind)
|
|
726
|
+
|
|
727
|
+
child_type = row_type || attr.type(register)
|
|
728
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
729
|
+
child_register = Lutaml::Model::Register.resolve_for_child(
|
|
730
|
+
child_type, register
|
|
731
|
+
)
|
|
732
|
+
[child_type, child_plan, child_register,
|
|
733
|
+
contested_name?(plan, rule)]
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Plan-frozen wire names: rule.name.to_s minted a fresh string
|
|
738
|
+
# per row per node in the hot loop.
|
|
739
|
+
def row_wire_names(plan)
|
|
740
|
+
plan[:row_wire_names] ||= plan[:rows].map do |rule, _attr, _kind, _spelling, _delegate, _row_type|
|
|
741
|
+
rule.name.to_s
|
|
715
742
|
end
|
|
716
|
-
[all, all]
|
|
717
743
|
end
|
|
718
744
|
end
|
|
719
745
|
end
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -19,11 +19,20 @@ module Lutaml
|
|
|
19
19
|
# answers nil — the opt-out protocol.
|
|
20
20
|
MIN_LEPTRIS_VERSION = "1.9.336.0"
|
|
21
21
|
|
|
22
|
+
# Memoized: the compiler consults this on every cache hit (per
|
|
23
|
+
# child row per node), and the comparison minted a Gem::Version
|
|
24
|
+
# per call — visible in the niso ordered-lane allocation profile.
|
|
25
|
+
# The loaded gem cannot change mid-process.
|
|
22
26
|
def self.plan_path_compatible?
|
|
23
|
-
return
|
|
27
|
+
return @plan_path_compatible if defined?(@plan_path_compatible)
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
@plan_path_compatible =
|
|
30
|
+
if defined?(Gem)
|
|
31
|
+
spec = Gem.loaded_specs["leptris"]
|
|
32
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
33
|
+
else
|
|
34
|
+
false
|
|
35
|
+
end
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
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
|