lutaml-model 0.8.63 → 0.8.65

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: e60ce4a1bedec13a3b778b0afea4c9be5c4779f721916bfafeb151e3bd8f8c1e
4
- data.tar.gz: b6f519fabe6c9dc9c0c5edeb871d6afcc23d6439eff433b15b74b532b1a633d7
3
+ metadata.gz: a8bbc0a4da8114a582ee834a333a8614ca1051747296a1e967eb36f47ef68b48
4
+ data.tar.gz: ec01b4058ae9ce9d400f229ae4f116bcdde25cdd015e3b96ff3e2b39af62ff10
5
5
  SHA512:
6
- metadata.gz: 646c4cfdfb4dc395f5f339332eb214b0d4cba7bf31710884357e5d8519db8825e0a8e937824f268bac8642ea5d23a7fee75686988b9d3a54923f508e50aaa3c0
7
- data.tar.gz: d738e9c618790d6575691acc7349991f1b4f28986947e75a3dc40a883978cbc3ce7f6ef28021b7c85f822037c6c63137ed22f1930fe6aba36dd0b266f84a451b
6
+ metadata.gz: 494b45969f4e8f67e5f79fe09eb6e1630e5b04109fe46b13210cc8dfe455a11e7b7d7955a91ae2132c037c7e53d21fb09c0462a55899cccaafc4d4e5b609bccd
7
+ data.tar.gz: 6694a345fda4b371988fab2af07b9311b7eed87f2e61a696eab7c2a247ba04950bbb93439b1019a27903dbfcf8c5815067f2d3449a41affe2cdb21a6a3deb583
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.63"
5
+ VERSION = "0.8.65"
6
6
  end
7
7
  end
@@ -34,16 +34,22 @@ module Lutaml
34
34
  # Recover-mode parsing must never masquerade input truncation as
35
35
  # success.
36
36
  #
37
- # libxml2 (the Nokogiri backend) caps its input buffer at 10 MB;
38
- # longer documents trip a fatal "Resource limit exceeded: Buffer
39
- # size limit exceeded, try XML_PARSE_HUGE" error mid-input. In
40
- # recover mode Nokogiri records that error on the document and
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
41
42
  # returns the partial tree, so every node past the limit silently
42
43
  # vanishes (lutaml-model#871). A resource-limit fatal means the
43
44
  # engine stopped early with input left — refuse to deserialize the
44
45
  # partial document instead of dropping trailing content without a
45
46
  # trace.
46
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
+ #
47
53
  # Other recover-mode fatals (e.g. an XML declaration after leading
48
54
  # whitespace) do not drop content and keep the long-standing
49
55
  # recover behavior; only the resource-limit family is truncating.
@@ -36,6 +36,17 @@ module Lutaml
36
36
 
37
37
  class << self
38
38
  def compile(model_class, register)
39
+ # A child model's declared lutaml_default_register takes
40
+ # precedence over the ambient (parent) register — the same
41
+ # contract the interpretive path applies through
42
+ # Register.resolve_for_child. Without this, plan compilation
43
+ # resolves the child's symbol attribute types in the parent
44
+ # context and raises UnknownTypeError for ids registered only
45
+ # in the child's own register (#876).
46
+ register = Lutaml::Model::Register.resolve_for_child(
47
+ model_class, register
48
+ )
49
+
39
50
  key = [model_class, register]
40
51
  return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
41
52
 
@@ -20,11 +20,21 @@ module Lutaml
20
20
  # node: the parsed source element (leptris); ordered/mixed
21
21
  # plans rebuild element_order from it and ordered children
22
22
  # hydrate natively against their own nodes
23
- def call(model_class, plan, value, parent: nil, node: nil)
23
+ def call(model_class, plan, value, parent: nil, node: nil,
24
+ register: nil)
25
+ # The child model's declared lutaml_default_register takes
26
+ # precedence over the ambient register — the same
27
+ # resolve_for_child contract the interpretive path applies.
28
+ # Without it, hydration resolves the child's symbol attribute
29
+ # types in the parent context and raises UnknownTypeError
30
+ # for ids registered only in the child's register (#876).
31
+ register = Lutaml::Model::Register.resolve_for_child(
32
+ model_class, register || Lutaml::Model::Config.default_register
33
+ )
24
34
  buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
25
35
  attr_kwargs = attributes_kwargs(plan, value, node)
26
36
  child_kwargs, children, delegates =
27
- children_kwargs(model_class, plan, value, buckets)
37
+ children_kwargs(model_class, plan, value, buckets, register)
28
38
  plan[:collection_defaults].each do |name|
29
39
  next if attr_kwargs.key?(name) || child_kwargs.key?(name)
30
40
 
@@ -80,7 +90,9 @@ module Lutaml
80
90
  # — child instances come back so the caller can decorate
81
91
  # parent/root links once the parent exists; delegate values
82
92
  # wait for the instance (their target object must exist).
83
- def children_kwargs(_model_class, plan, value, buckets = nil)
93
+ def children_kwargs(_model_class, plan, value, buckets = nil,
94
+ register = nil)
95
+ register ||= Lutaml::Model::Config.default_register
84
96
  grouped, tagged = group_children(value, plan[:row_tags])
85
97
  buckets = nil unless buckets && plan[:needs_nodes]
86
98
  kwargs = {}
@@ -126,7 +138,8 @@ module Lutaml
126
138
  child_plan = PlanCompiler.compile(child_type, register)
127
139
  items = buckets.fetch(rule.name.to_s, []).map do |n|
128
140
  call(child_type, child_plan,
129
- child_plan[:descriptor].walk(n), node: n)
141
+ child_plan[:descriptor].walk(n), node: n,
142
+ register: register)
130
143
  end
131
144
  unless items.empty?
132
145
  children.concat(items)
@@ -162,7 +175,7 @@ module Lutaml
162
175
  items = grouped.fetch(rule.name.to_s, []).each_with_index
163
176
  .map do |v, i|
164
177
  call(child_type, child_plan, v,
165
- node: cursor && cursor[i])
178
+ node: cursor && cursor[i], register: register)
166
179
  end
167
180
  next if items.empty?
168
181
 
@@ -25,16 +25,24 @@ module Lutaml
25
25
 
26
26
  class << self
27
27
  # plan: the compiler's entry for instance's class
28
- def call(instance, plan)
28
+ def call(instance, plan, register = nil)
29
+ # The child model's declared lutaml_default_register takes
30
+ # precedence over the ambient register — the resolve_for_child
31
+ # contract (#876); without it, child plans and attribute
32
+ # serialization resolve in the parent context.
33
+ register = Lutaml::Model::Register.resolve_for_child(
34
+ instance.class,
35
+ register || Lutaml::Model::Config.default_register,
36
+ )
29
37
  return nil if plan[:ordered] && instance.element_order.nil?
30
38
 
31
39
  doc = ::Leptris::XML::Document.create
32
40
  root = doc.create_element(plan[:tree][:name])
33
41
  doc.root = root
34
42
  if plan[:ordered]
35
- build_ordered(root, instance, plan, doc)
43
+ build_ordered(root, instance, plan, doc, register)
36
44
  else
37
- build(root, instance, plan, doc)
45
+ build(root, instance, plan, doc, register)
38
46
  end
39
47
  doc.to_xml(indent: 2, no_decl: true)
40
48
  end
@@ -54,8 +62,9 @@ module Lutaml
54
62
  Lutaml::Model::Config.default_register
55
63
  end
56
64
 
57
- def build(element, instance, plan, doc)
58
- write_attributes(element, instance, plan)
65
+ def build(element, instance, plan, doc, reg = nil)
66
+ reg ||= register
67
+ write_attributes(element, instance, plan, reg)
59
68
  plan[:rows].each do |rule, attr, kind, spelling, delegate|
60
69
  value = value_of(instance, attr, delegate)
61
70
  next unless rule.render?(value, instance)
@@ -67,24 +76,25 @@ module Lutaml
67
76
  # as the interpretive writer does.
68
77
  Array(value).each do |item|
69
78
  add_leaf(element, row_name(rule, spelling),
70
- attr.serialize(item, :xml, register), doc,
79
+ attr.serialize(item, :xml, reg), doc,
71
80
  attrs: rule.when_attribute)
72
81
  end
73
82
  when :collection_native, :collection_cb
74
83
  Array(value).each do |item|
75
84
  add_leaf(element, rule.name.to_s,
76
- attr.serialize(item, :xml, register), doc,
85
+ attr.serialize(item, :xml, reg), doc,
77
86
  attrs: rule.when_attribute)
78
87
  end
79
88
  when :nested, :ordered_deferred
80
89
  Array(value).each do |item|
81
- child_plan = PlanCompiler.compile(item.class, register)
90
+ child_plan = PlanCompiler.compile(item.class, reg)
82
91
  child = element.create_child(child_plan[:tree][:name])
83
92
  if child_plan[:ordered]
84
- return nil unless build_ordered(child, item, child_plan, doc)
93
+ return nil unless build_ordered(child, item, child_plan,
94
+ doc, reg)
85
95
 
86
96
  else
87
- build(child, item, child_plan, doc)
97
+ build(child, item, child_plan, doc, reg)
88
98
  end
89
99
  end
90
100
  when :raw
@@ -103,8 +113,9 @@ module Lutaml
103
113
  # the model are reflected; element_order text is the fallback.
104
114
  # PIs drop (interpretive parity). Returns nil when a nested
105
115
  # ordered child lacks element_order (caller falls back).
106
- def build_ordered(element, instance, plan, doc)
107
- write_attributes(element, instance, plan)
116
+ def build_ordered(element, instance, plan, doc, reg = nil)
117
+ reg ||= register
118
+ write_attributes(element, instance, plan, reg)
108
119
  rows_by_name = {}
109
120
  plan[:rows].each do |rule, attr, kind, spelling, delegate|
110
121
  next unless rule.name
@@ -162,7 +173,7 @@ module Lutaml
162
173
 
163
174
  element_indices[object.name] += 1
164
175
  add_leaf(element, object.name,
165
- attr.serialize(items[index], :xml, register), doc)
176
+ attr.serialize(items[index], :xml, reg), doc)
166
177
  when :nested, :ordered_deferred
167
178
  item = if attr.collection?
168
179
  index = element_indices[object.name]
@@ -173,18 +184,19 @@ module Lutaml
173
184
  end
174
185
  next unless item
175
186
 
176
- child_plan = PlanCompiler.compile(item.class, register)
187
+ child_plan = PlanCompiler.compile(item.class, reg)
177
188
  child = element.create_child(child_plan[:tree][:name])
178
189
  if child_plan[:ordered]
179
- return nil unless build_ordered(child, item, child_plan, doc)
190
+ return nil unless build_ordered(child, item, child_plan,
191
+ doc, reg)
180
192
  else
181
- build(child, item, child_plan, doc)
193
+ build(child, item, child_plan, doc, reg)
182
194
  end
183
195
  when :raw
184
196
  element.add_child(value.to_s) unless value.nil?
185
197
  when :scalar
186
198
  add_leaf(element, object.name,
187
- attr.serialize(value, :xml, register), doc)
199
+ attr.serialize(value, :xml, reg), doc)
188
200
  end
189
201
  end
190
202
  end
@@ -193,7 +205,7 @@ module Lutaml
193
205
 
194
206
  # Attributes in document order when the instance recorded it
195
207
  # (parsed models), else plan row order.
196
- def write_attributes(element, instance, plan)
208
+ def write_attributes(element, instance, plan, _reg = nil)
197
209
  recorded = instance.attribute_order
198
210
  if recorded && !recorded.empty?
199
211
  by_name = {}
@@ -213,7 +225,7 @@ module Lutaml
213
225
  end
214
226
  end
215
227
 
216
- def write_attribute(element, instance, name, entry)
228
+ def write_attribute(element, instance, name, entry, reg = nil)
217
229
  return unless entry
218
230
 
219
231
  _rule, attr, delegate = entry
@@ -221,7 +233,7 @@ module Lutaml
221
233
  return if value.nil?
222
234
 
223
235
  element.set_attribute(name,
224
- attr.serialize(value, :xml, register).to_s)
236
+ attr.serialize(value, :xml, reg || register).to_s)
225
237
  end
226
238
 
227
239
  # Partition rows (#88) re-emit their discriminator: the wire
data/lutaml-model.gemspec CHANGED
@@ -38,7 +38,10 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "canon"
39
39
  spec.add_dependency "concurrent-ruby"
40
40
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
41
- spec.add_dependency "moxml", "~> 0.5.56"
41
+ # 0.5.84 always passes XML_PARSE_HUGE to libxml2: very large documents
42
+ # parse completely instead of being silently truncated at the 10 MB
43
+ # input-buffer cap (lutaml-model#871).
44
+ spec.add_dependency "moxml", "~> 0.5.84"
42
45
  spec.add_dependency "ostruct"
43
46
  spec.add_dependency "rubyzip", "~> 3.4"
44
47
  spec.add_dependency "thor"
@@ -6,15 +6,16 @@ require "lutaml/model"
6
6
  require "lutaml/xml"
7
7
  require "lutaml/xml/adapter/nokogiri_adapter"
8
8
 
9
- # Regression tests for lutaml-model#871.
9
+ # Regression coverage for lutaml-model#871: very large documents must
10
+ # deserialize completely, with no silent truncation and no data loss.
10
11
  #
11
- # libxml2 (the Nokogiri backend) caps its input buffer at 10 MB. Parsing a
12
- # longer document in recover mode records a FATAL "Resource limit exceeded:
13
- # Buffer size limit exceeded, try XML_PARSE_HUGE" error on the document and
14
- # returns the partial tree, so every node past the cap silently vanishes.
15
- # XmlParser now refuses documents whose parse_errors carry a fatal error
16
- # instead of deserializing the truncated input.
17
- RSpec.describe "recovered fatal parse errors" do
12
+ # libxml2's default mode caps its input buffer at 10 MB and, in recover
13
+ # mode, silently drops every node past the cap. moxml 0.5.84 always
14
+ # passes XML_PARSE_HUGE, so the Nokogiri path now parses past the cap.
15
+ # XmlParser additionally refuses recovered parses that report the
16
+ # resource-limit fatal family, which still protects moxml releases that
17
+ # truncate.
18
+ RSpec.describe "parsing documents past libxml2's 10 MB buffer cap" do
18
19
  let(:annex_class) do
19
20
  Class.new(Lutaml::Model::Serializable) do
20
21
  attribute :id, :string
@@ -56,23 +57,28 @@ RSpec.describe "recovered fatal parse errors" do
56
57
  build_document(chunk_count: 6, chunk_size: 1_700_000, with_tail: true)
57
58
  end
58
59
 
59
- it "hits the libxml2 buffer cap in recover mode past 10 MB" do
60
+ it "confirms libxml2's default mode still trips the buffer cap past 10 MB" do
60
61
  errors = Nokogiri::XML(over_cap_document) { |config| config.recover.nonet }.errors
61
62
 
62
63
  expect(errors.map(&:message).join("\n")).to match(/\bFATAL:/)
63
64
  end
64
65
 
65
- it "raises instead of silently truncating on the nokogiri path" do
66
+ it "parses over-cap documents completely on the nokogiri path" do
66
67
  Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
67
- expect { root_class.from_xml(over_cap_document) }
68
- .to raise_error(Lutaml::Model::InvalidFormatError, /resource limit.*truncated/m)
68
+ model = root_class.from_xml(over_cap_document)
69
+
70
+ expect(model.annexes.map(&:id)).to eq(
71
+ ["annex0", "annex1", "annex2", "annex3", "annex4", "annex5",
72
+ "annex-tail-marker"],
73
+ )
69
74
  end
70
75
  end
71
76
 
72
- it "raises from the adapter layer directly" do
77
+ it "returns the complete tree from the adapter layer" do
73
78
  Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
74
- expect { Lutaml::Xml::Adapter::NokogiriAdapter.parse(over_cap_document) }
75
- .to raise_error(Lutaml::Model::InvalidFormatError, /XML_PARSE_HUGE/)
79
+ document = Lutaml::Xml::Adapter::NokogiriAdapter.parse(over_cap_document)
80
+
81
+ expect(document.root.children.count).to eq(7)
76
82
  end
77
83
  end
78
84
 
@@ -86,7 +92,7 @@ RSpec.describe "recovered fatal parse errors" do
86
92
  end
87
93
  end
88
94
 
89
- it "still parses smaller documents without any fatal-error handling" do
95
+ it "keeps parsing smaller documents unchanged" do
90
96
  Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
91
97
  model = root_class.from_xml(
92
98
  build_document(chunk_count: 2, chunk_size: 500_000, with_tail: true),
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Cross-register embedding on the plan paths (#876)" do
6
+ before do
7
+ stub_const("CrossRegister876", Module.new)
8
+ stub_const("CrossRegister876::Part", part_class)
9
+ stub_const("CrossRegister876::Widget", widget_class)
10
+ stub_const("Host876", host_class)
11
+
12
+ ctx = Lutaml::Model::GlobalContext.create_context(
13
+ id: :crossregister876, registry: Lutaml::Model::TypeRegistry.new,
14
+ )
15
+ ctx.registry.register(:part, CrossRegister876::Part)
16
+ ctx.registry.register(:widget, CrossRegister876::Widget)
17
+ end
18
+
19
+ let(:part_class) do
20
+ Class.new(Lutaml::Model::Serializable) do
21
+ def self.lutaml_default_register
22
+ :crossregister876
23
+ end
24
+
25
+ xml do
26
+ element "part"
27
+ end
28
+ end
29
+ end
30
+
31
+ let(:widget_class) do
32
+ klass = Class.new(Lutaml::Model::Serializable) do
33
+ def self.lutaml_default_register
34
+ :crossregister876
35
+ end
36
+
37
+ attribute :part_value, :part, collection: true
38
+
39
+ xml do
40
+ element "widget"
41
+ ordered
42
+ map_element "part", to: :part_value
43
+ end
44
+ end
45
+ klass
46
+ end
47
+
48
+ let(:host_class) do
49
+ Class.new(Lutaml::Model::Serializable) do
50
+ attribute :widget, CrossRegister876::Widget
51
+
52
+ xml do
53
+ element "host"
54
+ map_element "widget", to: :widget
55
+ end
56
+ end
57
+ end
58
+
59
+ let(:xml) { "<host><widget><part/></widget></host>" }
60
+
61
+ it "resolves the child's symbol types through the child register" do
62
+ model = Host876.from_xml(xml)
63
+ expect(model.widget.part_value.first).to be_a(CrossRegister876::Part)
64
+ end
65
+
66
+ it "round-trips through both the plan serializer and back" do
67
+ model = Host876.from_xml(xml)
68
+ xml_out = model.to_xml
69
+ expect(xml_out).to include("<widget>")
70
+ expect(xml_out).to include("<part/>")
71
+
72
+ reparsed = Host876.from_xml(xml_out)
73
+ expect(reparsed.widget.part_value.first).to be_a(CrossRegister876::Part)
74
+ end
75
+ end
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.63
4
+ version: 0.8.65
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-09-25 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -92,14 +92,14 @@ dependencies:
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: 0.5.56
95
+ version: 0.5.84
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: 0.5.56
102
+ version: 0.5.84
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: ostruct
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -1901,8 +1901,8 @@ files:
1901
1901
  - spec/lutaml/xml/adapter/nokogiri_adapter_spec.rb
1902
1902
  - spec/lutaml/xml/adapter/oga_adapter_spec.rb
1903
1903
  - spec/lutaml/xml/adapter/order_spec.rb
1904
+ - spec/lutaml/xml/adapter/over_cap_parse_spec.rb
1904
1905
  - spec/lutaml/xml/adapter/ox_adapter_spec.rb
1905
- - spec/lutaml/xml/adapter/recovered_fatal_parse_spec.rb
1906
1906
  - spec/lutaml/xml/adapter/rexml_adapter_spec.rb
1907
1907
  - spec/lutaml/xml/adapter/xml_namespace_spec.rb
1908
1908
  - spec/lutaml/xml/adapter_loader_spec.rb
@@ -1916,6 +1916,7 @@ files:
1916
1916
  - spec/lutaml/xml/conformance/xml_schema_instance_spec.rb
1917
1917
  - spec/lutaml/xml/content_model_validation_spec.rb
1918
1918
  - spec/lutaml/xml/cross_class_dispatch_spec.rb
1919
+ - spec/lutaml/xml/cross_register_plan_spec.rb
1919
1920
  - spec/lutaml/xml/data_model_spec.rb
1920
1921
  - spec/lutaml/xml/decisions/decision_engine_spec.rb
1921
1922
  - spec/lutaml/xml/decisions/decision_spec.rb