lutaml-model 0.8.100 → 0.8.114

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -5
  3. data/lib/compat/opal/lutaml_model_boot.rb +1 -0
  4. data/lib/lutaml/json/adapter/yeptris_adapter.rb +11 -0
  5. data/lib/lutaml/key_value/transform.rb +4 -0
  6. data/lib/lutaml/key_value/transformation.rb +10 -0
  7. data/lib/lutaml/model/attribute.rb +43 -3
  8. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  9. data/lib/lutaml/model/configuration.rb +13 -0
  10. data/lib/lutaml/model/serialize/builder.rb +22 -0
  11. data/lib/lutaml/model/serialize/format_conversion.rb +21 -3
  12. data/lib/lutaml/model/serialize/initialization.rb +14 -1
  13. data/lib/lutaml/model/type_resolver.rb +7 -5
  14. data/lib/lutaml/model/version.rb +1 -1
  15. data/lib/lutaml/xml/adapter/leptris_adapter.rb +1 -0
  16. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -45
  17. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  18. data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
  19. data/lib/lutaml/xml/leptris/plan_hydrator.rb +276 -58
  20. data/lib/lutaml/xml/leptris/plan_serializer.rb +298 -45
  21. data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
  22. data/lib/lutaml/xml/leptris.rb +25 -84
  23. data/lib/lutaml/xml/model_transform.rb +5 -0
  24. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +15 -6
  25. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +11 -0
  26. data/lutaml-model.gemspec +3 -3
  27. data/spec/lutaml/json/yeptris_adapter_spec.rb +3 -10
  28. data/spec/lutaml/model/adapter_resolver_configured_spec.rb +32 -8
  29. data/spec/lutaml/model/collection_strict_assignment_spec.rb +55 -0
  30. data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
  31. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  32. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  33. data/spec/lutaml/xml/plan_fast_path_spec.rb +12 -6
  34. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
  35. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  36. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
  37. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +3 -10
  38. metadata +16 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bd54a574eb02eec487d980543835abb2bec61c12104d6eea6b898a516fb301f
4
- data.tar.gz: ffc5d3ed5b0c6df7140839064baec52624a034b2d1c5cfdc764dc4ee78c199ce
3
+ metadata.gz: 381cba1519b14ff18965d44164e7d75e4564ac92c803815dad95277b7076ef70
4
+ data.tar.gz: acd3466cbc30ed27ce60ce62b181732ab957abde427da1e609003ced88c47a67
5
5
  SHA512:
6
- metadata.gz: 191c5ee654cc6863a6f8d865448f0fbb29c31f7d602e3ab2a356148fdca7117b254aa497bb1ebdd80da1c80f495cf46c5f1029897cf87e7423a7409747e4e97c
7
- data.tar.gz: 4b55a05f437df7e0c85c7545d2042990572c70a4d64b42d9ae3ee44950ca6875cd640c2089cd9620951264180556e2e3776e7b32028b1b1d9962cbdaad739ecb
6
+ metadata.gz: 8f48a3c4bc8a0242a714d945aae2aaca68ae6c0a8a87cedb038a1229363545bb41f1a3c24f833db86c89e3468cc216c77536af23385cce37e6ce20f8a2e3b142
7
+ data.tar.gz: 9ba5616b479da6952fd44284792ce708712ed6576ba90fa11507581aefd344f30ccd22e5887e3200c714feff15e7e8c95742149f49ac2a5618b2d5ea0f0b742a
data/Gemfile CHANGED
@@ -44,11 +44,12 @@ gem "rexml"
44
44
  # Native engines stay development-only: every adapter detects and
45
45
  # degrades gracefully at runtime (safe requires + prefer-if-available).
46
46
  # yeptris ships no Windows prebuilt platform gem.
47
- # leptris floor: the plan hydrator's one-crossing snapshot
48
- # (leptris_plan_value_children_snapshot) landed in 1.9.273.0; older
49
- # gems fall back to accessor enumeration at runtime, but CI and dev
50
- # resolve the snapshot-capable line.
51
- gem "leptris", ">= 1.9.273.0"
47
+ # leptris floor: the single capability line for the plan fast path
48
+ # (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
49
+ # exact-URI attribute rows, nested-attribute capture, wildcard rows,
50
+ # walk-side ns retention and leniency, :unqualified rows, and the
51
+ # address-based text attach all ride it.
52
+ gem "leptris", ">= 1.9.351.1"
52
53
  gem "teptris"
53
54
  gem "yeptris" unless Gem.win_platform?
54
55
  # TODO: revert rng branch to main when lutaml/rng#32 is merged
@@ -382,6 +382,7 @@ require "lutaml/xml/error/namespace_mismatch_error"
382
382
  require "lutaml/xml/error/schema_validation_error"
383
383
  require "lutaml/xml/error/xml_configuration_error"
384
384
  require "lutaml/xml/error/xml_error"
385
+ require "lutaml/xml/leptris/native_emission"
385
386
  require "lutaml/xml/leptris/plan_compiler"
386
387
  require "lutaml/xml/leptris/plan_hydrator"
387
388
  require "lutaml/xml/leptris/plan_order"
@@ -26,6 +26,17 @@ module Lutaml
26
26
  end
27
27
  private_class_method :require_engine
28
28
 
29
+ # True where the yeptris engine loads (libyeptris present for
30
+ # this platform). Idempotent; a missing or broken install
31
+ # reports false instead of raising, so optional-engine callers
32
+ # can gate on it.
33
+ def self.available?
34
+ require_engine
35
+ true
36
+ rescue LoadError
37
+ false
38
+ end
39
+
29
40
  # The parse-error contract is the standard adapter's: invalid
30
41
  # input raises JSON::ParserError regardless of the engine, so
31
42
  # consumers' rescue ladders (and this gem's own
@@ -687,6 +687,10 @@ partition = nil)
687
687
  # attribute is not a collection, so this guard defers exactly that case
688
688
  # to `.validate` while leaving declared ranges checked at parse. A mapped
689
689
  # PORO has no `.validate` to defer into, so it keeps the eager check.
690
+ # singular_tolerance: :first on the KV lane (#971): collapse an
691
+ # over-ingested collection before the writer stores it whole.
692
+ value = value.first if attr.tolerates_singular_overflow?(value)
693
+
690
694
  if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
691
695
  attr.valid_collection!(value, context)
692
696
  end
@@ -1031,6 +1031,16 @@ child_mappings, options)
1031
1031
  rule.attribute_type < Lutaml::Model::Serialize
1032
1032
  end
1033
1033
 
1034
+ if is_nested_model && !value.is_a?(Lutaml::Model::Serialize)
1035
+ # The KV mirror of the XML lane's #971 collapse: a tolerated
1036
+ # singular attribute holding an over-assigned collection
1037
+ # serializes its leading value instead of raising.
1038
+ attr = if model_class.respond_to?(:attributes)
1039
+ model_class.attributes[rule.attribute_name.to_sym]
1040
+ end
1041
+ value = value.first if attr&.tolerates_singular_overflow?(value)
1042
+ end
1043
+
1034
1044
  if is_nested_model
1035
1045
  # Validate that value matches the expected type
1036
1046
  # There are three valid cases:
@@ -47,6 +47,7 @@ module Lutaml
47
47
  min_length
48
48
  max_length
49
49
  desc
50
+ singular_tolerance
50
51
  ].freeze
51
52
 
52
53
  # Per-type-class memo of the custom from_xml/from_json probe used by #cast.
@@ -389,12 +390,34 @@ module Lutaml
389
390
 
390
391
  def cast_value(value, register)
391
392
  if list_like?(value)
392
- build_collection(cast_items(value, register), register: register)
393
+ items = cast_items(value, register)
393
394
  elsif coerce_to_collection?(value)
394
- build_collection(cast_items([value], register), register: register)
395
+ items = cast_items([value], register)
395
396
  else
396
- cast_element(value, register)
397
+ return cast_element(value, register)
397
398
  end
399
+ # cast_items already returns a fresh array, so the plain-Array
400
+ # collection needs no re-wrap — build_collection's
401
+ # collection_class.new would copy it again.
402
+ assert_flat_collection_items!(items)
403
+ return items if !custom_collection? && collection_class == ::Array
404
+
405
+ build_collection(items, register: register)
406
+ end
407
+
408
+ # Fail loudly on nested arrays. A collection's declared element
409
+ # type never admits an Array as an item; the legacy
410
+ # build_collection flatten silently absorbed mis-spliced builder
411
+ # output (expressir#506 surfaced it three frames away in a
412
+ # formatter). The error must land at the assignment boundary.
413
+ def assert_flat_collection_items!(items)
414
+ return unless items.any?(::Array)
415
+
416
+ raise TypeError,
417
+ "collection '#{name}': item " \
418
+ "#{items.index { |item| item.is_a?(::Array) }} is an Array " \
419
+ "where the declared element type is expected; nested arrays " \
420
+ "are not flattened — splice list results before assignment"
398
421
  end
399
422
 
400
423
  # Plain Arrays count as list input. On singular attributes
@@ -805,6 +828,17 @@ instance_object = nil)
805
828
  end
806
829
  end
807
830
 
831
+ # Singular-overflow tolerance (#971): ingest can hand a singular
832
+ # attribute a collection — relaton's upstream documents carry
833
+ # multiple <formattedref> elements per bibitem. With
834
+ # singular_tolerance: :first the leading value is kept instead of
835
+ # raising CollectionTrueMissingError; the default stays strict.
836
+ def tolerates_singular_overflow?(value)
837
+ !collection? && @options[:singular_tolerance] == :first &&
838
+ collection_instance?(value) &&
839
+ !value_policy.whole_value?(unresolved_type)
840
+ end
841
+
808
842
  def valid_collection!(value, caller)
809
843
  if collection_instance?(value) && !collection? &&
810
844
  !value_policy.whole_value?(unresolved_type)
@@ -1019,6 +1053,12 @@ instance_object = nil)
1019
1053
  "Use map_element \"name\", to: :#{name}, raw: :content instead."
1020
1054
  end
1021
1055
  @validations = @options[:validations]
1056
+ tolerance = @options[:singular_tolerance]
1057
+ unless tolerance.nil? || tolerance == :first
1058
+ raise ArgumentError,
1059
+ "Invalid singular_tolerance: #{tolerance.inspect} for " \
1060
+ "attribute '#{name}'. Supported: :first"
1061
+ end
1022
1062
  set_default_for_collection if collection?
1023
1063
  end
1024
1064
 
@@ -59,14 +59,14 @@ module Lutaml
59
59
  # updates so recursive type resolution can populate related keys.
60
60
  #
61
61
  # @param name [Symbol, String, Class] The type name or class to resolve
62
- # @param context [TypeContext] The resolution context
62
+ # @param context [TypeContext, nil] The resolution context
63
63
  # @return [Class] The resolved type class
64
64
  # @raise [UnknownTypeError] If type cannot be resolved
65
65
  def resolve(name, context, materialize: true)
66
66
  return @delegate.resolve(name, context, materialize: false) unless materialize
67
67
 
68
68
  cache_key = if name.is_a?(Class)
69
- [context.id, name]
69
+ [context_id_for(context), name]
70
70
  else
71
71
  build_cache_key(name, context)
72
72
  end
@@ -79,11 +79,11 @@ module Lutaml
79
79
  # Check if a type can be resolved, using cache if available.
80
80
  #
81
81
  # @param name [Symbol, String, Class] The type name or class to check
82
- # @param context [TypeContext] The resolution context
82
+ # @param context [TypeContext, nil] The resolution context
83
83
  # @return [Boolean] true if type can be resolved
84
84
  def resolvable?(name, context)
85
85
  cache_key = if name.is_a?(Class)
86
- [context.id, name]
86
+ [context_id_for(context), name]
87
87
  else
88
88
  build_cache_key(name, context)
89
89
  end
@@ -137,10 +137,17 @@ module Lutaml
137
137
  # Uses array for faster comparison than string concatenation.
138
138
  #
139
139
  # @param name [Symbol, String] The type name
140
- # @param context [TypeContext] The resolution context
140
+ # @param context [TypeContext, nil] The resolution context
141
141
  # @return [Array] The cache key (faster than string)
142
142
  def build_cache_key(name, context)
143
- [context.id, name.to_sym]
143
+ [context_id_for(context), name.to_sym]
144
+ end
145
+
146
+ # A nil context (e.g. a register ID that has no registered type
147
+ # context) is a legitimate "no namespace" resolution; the cache
148
+ # key just forgoes the namespace slot.
149
+ def context_id_for(context)
150
+ context&.id
144
151
  end
145
152
  end
146
153
  end
@@ -19,6 +19,12 @@ module Lutaml
19
19
  # compilable XML models (Leptris::XML::Descriptor walk). Opt-in
20
20
  # while the full semantics audit completes.
21
21
  attr_accessor :xml_plan_fast_path
22
+ # Ordered/mixed-content models on the plan path: opt-in. The
23
+ # per-node ordered hydration is FASTER than the interpretive
24
+ # lane on time but allocates above the downstream budget gates
25
+ # (niso pnas ratio 1.25); it ships dark until the allocation
26
+ # diet lands.
27
+ attr_accessor :xml_plan_ordered_path
22
28
  # #909: when false, XML deserialization skips the per-instance
23
29
  # element_order/attribute_order summaries — read-only consumers of
24
30
  # large documents never serialize back and should not pay for
@@ -31,6 +37,13 @@ module Lutaml
31
37
  @conversion_cache = nil
32
38
  @configured = false
33
39
  @xml_plan_fast_path = true
40
+ # On by default since the engine fix: leptris#1743 (the
41
+ # exponential flatten on diamond-shared plan graphs) shipped in
42
+ # 1.9.366.2's identity-memo flatten_plan — the ISO_13849 corpus
43
+ # that hung for 28+ CPU-min now parses in ~8s warm. The floor
44
+ # bump (MIN_LEPTRIS_VERSION = 1.9.366.2) keeps consumers on
45
+ # memo-less engines off this lane.
46
+ @xml_plan_ordered_path = true
34
47
  @element_order_tracking = true
35
48
  end
36
49
 
@@ -84,6 +84,26 @@ module Lutaml
84
84
  @__order_tracking__ ? true : false
85
85
  end
86
86
 
87
+ # Whether any writer setter ran on this instance. Parsed
88
+ # instances start pristine — every set element value is covered
89
+ # by the parse-built element_order, so ordered serialization
90
+ # can skip order reconciliation; the first writer-set marks the
91
+ # instance and reconciliation engages (writer-added values sit
92
+ # outside the parse order).
93
+ # @return [Boolean]
94
+ def mutated_after_parse?
95
+ @__mutated_after_parse__ ? true : false
96
+ end
97
+
98
+ # Construction-time reset: hydration routes every attribute
99
+ # through the generated writers (which mark mutations); a
100
+ # freshly built instance is definitionally not
101
+ # mutated-after-parse.
102
+ # @return [void]
103
+ def clear_mutated_after_parse!
104
+ @__mutated_after_parse__ = false
105
+ end
106
+
87
107
  # Record a singular attribute mutation in element_order.
88
108
  #
89
109
  # No-op unless order tracking is enabled (i.e. the instance was
@@ -102,6 +122,7 @@ module Lutaml
102
122
  # content for content-mapped attributes, ignored otherwise
103
123
  # @return [Object] the passed value
104
124
  def record_mutation(attribute_name, value = nil)
125
+ @__mutated_after_parse__ = true
105
126
  return value unless @__order_tracking__
106
127
 
107
128
  track_order(attribute_name, value, nil)
@@ -122,6 +143,7 @@ module Lutaml
122
143
  # @param value [Object, nil] The value assigned to the collection
123
144
  # @return [Object] the passed value
124
145
  def record_mutation_collection(attribute_name, value)
146
+ @__mutated_after_parse__ = true
125
147
  return value unless @__order_tracking__
126
148
  return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
127
149
 
@@ -138,19 +138,37 @@ module Lutaml
138
138
  register = Lutaml::Model::Config.default_register
139
139
  plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
140
140
  return nil unless plan
141
- return nil unless Lutaml::Xml::Leptris::PlanCompiler.engine_walk_safe?(plan)
142
141
 
143
- root = ::Leptris::XML.parse(data.to_s).root
142
+ # strict: the from_xml contract raises on malformed input —
143
+ # the FFI lane parses leniently by default and would answer a
144
+ # best-effort tree where every other adapter raises. The
145
+ # kwarg spellings vary by engine vintage (strict: landed
146
+ # after 1.9.353, recover: on 1.9.344+), so probe the resolved
147
+ # parse method and pass the strictest spelling it accepts.
148
+ root = ::Leptris::XML.parse(data.to_s, **leptris_strict_kwargs).root
144
149
  return nil if root.nil?
145
150
  return nil unless root.name == plan[:tree][:name]
146
151
 
147
152
  Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
148
153
  plan[:descriptor].walk(root),
149
- node: root)
154
+ node: root,
155
+ declaration_plan_mode:
156
+ options.fetch(
157
+ :import_declaration_plan, :eager
158
+ ))
150
159
  rescue ::Leptris::XML::ParseError => e
151
160
  raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
152
161
  end
153
162
 
163
+ def leptris_strict_kwargs
164
+ params = ::Leptris::XML.method(:parse).parameters
165
+ .map { |_, key| key }
166
+ kwargs = {}
167
+ kwargs[:recover] = false if params.include?(:recover)
168
+ kwargs[:strict] = true if params.include?(:strict)
169
+ kwargs
170
+ end
171
+
154
172
  # Get list of error types that can be raised during format parsing.
155
173
  # Core errors are always included; format-specific errors come from
156
174
  # FormatRegistry registrations.
@@ -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
- attrs_by.each do |name, attr|
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);
@@ -455,6 +459,7 @@ module Lutaml
455
459
  unknown = attrs.keys.find { |k| !known.include?(k.to_sym) }
456
460
  raise Error, "unknown attribute '#{unknown}' for #{self}"
457
461
  end
462
+ instance.clear_mutated_after_parse!
458
463
  instance
459
464
  end
460
465
 
@@ -467,6 +472,14 @@ module Lutaml
467
472
  end
468
473
  end
469
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
+
470
483
  # Define register-specific attribute methods on the class itself.
471
484
  #
472
485
  # Called once per (class, register) combination. Replaces per-instance
@@ -44,7 +44,9 @@ module Lutaml
44
44
  # 5. Raise UnknownTypeError if not found anywhere
45
45
  #
46
46
  # @param name [Symbol, String, Class] The type name or class to resolve
47
- # @param context [TypeContext] The resolution context
47
+ # @param context [TypeContext, nil] The resolution context (nil means
48
+ # no primary registry and no fallbacks — an id registered in the
49
+ # Register namespace but not as a type context)
48
50
  # @return [Class] The resolved type class
49
51
  # @raise [UnknownTypeError] If type cannot be resolved
50
52
  #
@@ -66,11 +68,11 @@ module Lutaml
66
68
  type_name = name.to_sym
67
69
 
68
70
  # 1. Check primary registry
69
- type = context.lookup_local(type_name)
71
+ type = context&.lookup_local(type_name)
70
72
  return apply_substitutions(type, context) if type
71
73
 
72
74
  # 2. Check fallback contexts in order
73
- if context.has_fallbacks?
75
+ if context&.has_fallbacks?
74
76
  context.fallback_contexts.each do |fallback_context|
75
77
  type = resolve_from_fallback(type_name, fallback_context)
76
78
  return apply_substitutions(type, context) if type
@@ -97,8 +99,8 @@ module Lutaml
97
99
  # 5. Type not found - raise error
98
100
  raise UnknownTypeError.new(
99
101
  type_name,
100
- context_id: context.id,
101
- available_types: available_type_names(context),
102
+ context_id: context&.id,
103
+ available_types: context ? available_type_names(context) : [],
102
104
  )
103
105
  end
104
106
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.100"
5
+ VERSION = "0.8.114"
6
6
  end
7
7
  end
@@ -9,6 +9,7 @@ module Lutaml
9
9
  MOXML_ADAPTER = Moxml::Adapter::Leptris
10
10
  BUILDER_CLASS = Builder::Leptris
11
11
  PARSED_ELEMENT_CLASS = Lutaml::Xml::LeptrisElement
12
+ PARSE_ERROR_CLASS = Moxml::ParseError
12
13
  end
13
14
  end
14
15
  end
@@ -20,10 +20,9 @@ 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
- raise_empty_document_error if root_element.nil?
25
+ raise_empty_document_error(parsed.parse_errors) if root_element.nil?
27
26
 
28
27
  root = self::PARSED_ELEMENT_CLASS.new(root_element)
29
28
  doc_pis = extract_document_processing_instructions(parsed)
@@ -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),
@@ -112,8 +69,11 @@ module Lutaml
112
69
  }
113
70
  end
114
71
 
115
- def raise_empty_document_error
72
+ def raise_empty_document_error(parse_errors = nil)
116
73
  message = self::EMPTY_DOCUMENT_ERROR_MESSAGE
74
+ if parse_errors && !parse_errors.empty?
75
+ message += " (the XML engine reported: #{parse_errors.join('; ')})"
76
+ end
117
77
 
118
78
  case self::EMPTY_DOCUMENT_ERROR_TYPE
119
79
  when :parse_exception