lutaml-model 0.8.39 → 0.8.40

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: 15f9489eb4db353b481f160703e848e35b730cef179d32874941fc537ed962d3
4
- data.tar.gz: f0067a5ffc4d66c8312d9cc478d9bd7d1771d9b639111c3158956b2d98e4468e
3
+ metadata.gz: 597b465123fe390703bdb760a4f6e18e4069b55bea8998633e2a4c0c7f5fcb9c
4
+ data.tar.gz: e848597e1ad2cef05670d8dc8de6f7480bf028fde627021c5a61f554ad99a2f0
5
5
  SHA512:
6
- metadata.gz: 9f1372cbf492e141015524382a3ac98cdc5f484441d70c593b7ad67b9021bfa959732700679534bb3690f86c064e3a1a82269833c5c73794fe87af24f21d239b
7
- data.tar.gz: '048d412becdba7a8a99070740663d0d0a33e88d02018708818ce816d1ba4669a1f26efbf08d0c67390818c5efede0e362ef8f1785536c4233a2fa6dadd07da61'
6
+ metadata.gz: 231f9601a9ce76ae516a6435b2c7185e15c2cc6680d0aefbad27ef328b25f67f0ed2635be8319eff6c3a64338f3bd2f9855ab1daa51058018345eb77cf3aa0d9
7
+ data.tar.gz: 88af6f65b2fadf91990f6f6f10a03a799007acda351d87a059f00325c85514291fbaccfd99580dd1daac3090bd61f4aa1ec26efffd058438c7e6abdd9a53cea4
@@ -129,6 +129,7 @@ module Lutaml
129
129
  end
130
130
  end
131
131
  elsif attr.unresolved_type == Lutaml::Model::Type::Reference
132
+ Lutaml::Model::Store.reference_types_in_use!
132
133
  define_reference_methods(name, register_id)
133
134
  else
134
135
  define_regular_attribute_methods(name, attr)
@@ -33,11 +33,30 @@ module Lutaml
33
33
  @instance ||= new
34
34
  end
35
35
 
36
+ # lutaml-model#808: Ruby 3.3's GC has a WeakMap-mark hazard under
37
+ # heavy insert churn (freed slot dereferenced at wmap mark time).
38
+ # Registration is only ever consumed by `ref:` resolution — when
39
+ # no Reference-typed attribute exists anywhere, nothing can
40
+ # resolve, so registration is skipped entirely and the WeakMap
41
+ # stays quiet. Set the first time an attribute with a Reference
42
+ # type is defined.
43
+ # Not memoized `||=` — the setter flips it to true for the
44
+ # process lifetime and reads must see that.
45
+ def reference_types_in_use?
46
+ @reference_types_in_use ? true : false
47
+ end
48
+
49
+ def reference_types_in_use!
50
+ @reference_types_in_use = true
51
+ end
52
+
36
53
  def reset!
37
54
  @instance = new
38
55
  end
39
56
 
40
57
  def register(object)
58
+ return unless reference_types_in_use?
59
+
41
60
  instance.register(object)
42
61
  end
43
62
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.39"
5
+ VERSION = "0.8.40"
6
6
  end
7
7
  end
@@ -299,13 +299,23 @@ module Lutaml
299
299
  # Create element
300
300
  xml.create_and_add_element(tag_name, attributes: attributes,
301
301
  prefix: prefix) do |inner_xml|
302
- # Handle raw content (map_all directive)
302
+ # Handle raw content (map_all directive). lutaml-model#223:
303
+ # with coexisting mappings the raw remainder is the TAIL —
304
+ # mapped children render first through the normal paths
305
+ # below, then the fragment is appended. Standalone map_all
306
+ # (raw is the whole content) keeps the historical
307
+ # raw-only rendering by fronting the fragment and skipping
308
+ # children only when there is nothing mapped.
303
309
  has_raw_content = false
304
310
  if element.is_a?(Lutaml::Xml::DataModel::XmlElement)
305
311
  raw_content = element.raw_content
306
312
  if raw_content && !raw_content.to_s.empty?
307
- inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
308
- has_raw_content = true
313
+ if element.children.empty?
314
+ inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
315
+ has_raw_content = true
316
+ else
317
+ (tail_raw_fragments ||= []) << raw_content.to_s
318
+ end
309
319
  end
310
320
  end
311
321
 
@@ -343,6 +353,9 @@ module Lutaml
343
353
  end
344
354
  end
345
355
  end
356
+ (tail_raw_fragments || []).each do |fragment|
357
+ inner_xml.add_xml_fragment(inner_xml, fragment)
358
+ end
346
359
  end
347
360
  end
348
361
 
@@ -268,11 +268,20 @@ module Lutaml
268
268
  attributes["xmlns"] = "" if needs_xmlns_blank
269
269
 
270
270
  xml.create_and_add_element(qualified_name, attributes: attributes) do
271
+ tail_raw = nil
271
272
  if xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement)
272
273
  raw_content = xml_element.raw_content
273
274
  if raw_content && !raw_content.to_s.empty?
274
- xml.add_xml_fragment(xml, raw_content.to_s)
275
- return
275
+ # lutaml-model#223: with coexisting map_all, the raw
276
+ # remainder is the tail — mapped children render first.
277
+ # Standalone map_all (no mapped children) keeps the
278
+ # historical raw-only rendering.
279
+ if xml_element.children.empty?
280
+ xml.add_xml_fragment(xml, raw_content.to_s)
281
+ return
282
+ end
283
+
284
+ tail_raw = raw_content.to_s
276
285
  end
277
286
  end
278
287
 
@@ -306,6 +315,8 @@ module Lutaml
306
315
  end
307
316
  end
308
317
 
318
+ xml.add_xml_fragment(xml, tail_raw) if tail_raw
319
+
309
320
  if xml_element.text_content
310
321
  if xml_element.cdata
311
322
  xml.cdata(xml_element.text_content.to_s)
@@ -109,6 +109,13 @@ module Lutaml
109
109
  children.map(&:to_xml).join
110
110
  end
111
111
 
112
+ # The element including its own tags — the building block for
113
+ # scoped raw capture (#223): an unclaimed child contributes its
114
+ # whole subtree verbatim.
115
+ def inner_xml_or_self
116
+ to_xml
117
+ end
118
+
112
119
  private
113
120
 
114
121
  def build_element_xml(builder)
@@ -979,9 +979,14 @@ module Lutaml
979
979
  "is allowed with #{TYPES[:all_content]}"
980
980
  end
981
981
 
982
- if !(elements.empty? && content_mapping.nil?) && type == TYPES[:all_content]
983
- raise StandardError,
984
- "#{TYPES[:all_content]} is not allowed with other mappings"
982
+ if type == TYPES[:all_content] &&
983
+ !(elements.empty? && content_mapping.nil?)
984
+ # lutaml-model#223: map_all may coexist with mapped siblings.
985
+ # The raw capture receives only the UNCLAIMED content — the
986
+ # verbatim subtree minus children matched by element rules.
987
+ # The historical exclusivity raised here; the coexistence
988
+ # contract is enforced by scoped capture on parse and
989
+ # remainder-after-mapped emission on serialize.
985
990
  end
986
991
  end
987
992
 
@@ -55,14 +55,17 @@ module Lutaml
55
55
  # ISO-13849 profile). Memoize one instance per (element, class)
56
56
  # for the whole parse; the memo travels in the propagated
57
57
  # deserialization options.
58
- memo = options[:__lutaml_child_memo] ||= {}
59
- memo_key = [data.object_id, model_class.object_id]
60
- if (existing = memo[memo_key])
58
+ # Nested identity maps keyed on the element object itself —
59
+ # flat object_id pairs could collide after GC id reuse on
60
+ # long parses (#808 audit).
61
+ memo = options[:__lutaml_child_memo] ||= {}.compare_by_identity
62
+ per_class = (memo[data] ||= {}.compare_by_identity)
63
+ if (existing = per_class[model_class])
61
64
  return existing
62
65
  end
63
66
 
64
67
  instance = model_class.allocate_for_deserialization(child_register)
65
- memo[memo_key] = instance
68
+ per_class[model_class] = instance
66
69
  else
67
70
  instance = model_class.new
68
71
  register_accessor_methods_for(instance, child_register)
@@ -345,7 +348,7 @@ module Lutaml
345
348
  end
346
349
 
347
350
  value = if rule.raw_mapping?
348
- doc.root.inner_xml
351
+ scoped_raw_inner_xml(doc, xml_mapping)
349
352
  elsif rule.content_mapping?
350
353
  rule.cdata ? doc.cdata : doc.text
351
354
  elsif (group = grouped_plain_rules[rule_to]) &&
@@ -713,6 +716,23 @@ _effective_register)
713
716
  # lutaml-model#154: decode HTML entities for mappings that opted in
714
717
  # (`html_entities` in the xml block). Values stay untouched
715
718
  # otherwise — XML keeps undefined entities literal by design.
719
+ # lutaml-model#223: with coexisting map_all, the raw capture is
720
+ # the verbatim subtree MINUS the children claimed by element
721
+ # rules (they hydrate their own attributes). Standalone map_all
722
+ # (no element rules) keeps the full inner_xml.
723
+ def scoped_raw_inner_xml(doc, xml_mapping)
724
+ claimed = []
725
+ xml_mapping&.elements&.each do |element_rule|
726
+ Array(element_rule.name).each { |n| claimed << n.to_s }
727
+ end
728
+ return doc.root.inner_xml if claimed.empty?
729
+
730
+ doc.root.element_children.reject do |child|
731
+ claimed.include?(child.unprefixed_name) ||
732
+ claimed.include?(child.name)
733
+ end.map(&:inner_xml_or_self).join
734
+ end
735
+
716
736
  def decode_html_entities_value(value, enabled)
717
737
  return value if value.nil? || !value.is_a?(String)
718
738
  return value unless enabled
@@ -185,28 +185,32 @@ module MapAllSpec
185
185
  end
186
186
  end
187
187
 
188
- describe "invalid mapping combinations" do
189
- it "raises error when combining map_all with other mappings" do
190
- expect do
191
- InvalidDocument.json do
188
+ describe "coexistence with other mappings (#223)" do
189
+ it "captures only unclaimed content alongside mapped elements" do
190
+ doc_class = Class.new(Lutaml::Model::Serializable) do
191
+ attribute :title, :string
192
+ attribute :rest, :string
193
+
194
+ xml do
195
+ element "eref"
192
196
  map_element "title", to: :title
197
+ map_all to: :rest
193
198
  end
194
- end.to raise_error(
195
- StandardError,
196
- "map_all is not allowed with other mappings",
197
- )
198
- end
199
+ end
199
200
 
200
- it "raises error when combining other mappings are used with map_all" do
201
- expect do
202
- InvalidDocument.yaml do
203
- map_element "title", to: :title
204
- map_all to: :content
205
- end
206
- end.to raise_error(
207
- StandardError,
208
- "map_all is not allowed with other mappings",
209
- )
201
+ source = %(<eref><title>T</title><p>para</p><note>N</note></eref>)
202
+ parsed = doc_class.from_xml(source)
203
+ expect(parsed.title).to eq("T")
204
+ expect(parsed.rest).to eq("<p>para</p><note>N</note>")
205
+
206
+ out = doc_class.to_xml(parsed)
207
+ expect(out).to include("<title>T</title>")
208
+ expect(out).to include("<p>para</p>")
209
+ expect(out).to include("<note>N</note>")
210
+
211
+ reparsed = doc_class.from_xml(out)
212
+ expect(reparsed.title).to eq("T")
213
+ expect(reparsed.rest).to eq("<p>para</p><note>N</note>")
210
214
  end
211
215
  end
212
216
  end
@@ -3,6 +3,13 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe Lutaml::Model::Store do
6
+ before do
7
+ # Registration only runs when a Reference-typed attribute exists
8
+ # somewhere (#808); the internal specs opt in explicitly.
9
+ described_class.reference_types_in_use!
10
+ described_class.clear
11
+ end
12
+
6
13
  let(:model_class) do
7
14
  Class.new(Lutaml::Model::Serializable) do
8
15
  attribute :id, :string
@@ -10,7 +17,6 @@ RSpec.describe Lutaml::Model::Store do
10
17
  end
11
18
  end
12
19
 
13
- before { described_class.clear }
14
20
  after { described_class.clear }
15
21
 
16
22
  describe "#register and #resolve" do
@@ -974,7 +974,6 @@ RSpec.describe Lutaml::Xml::Mapping do
974
974
  end
975
975
 
976
976
  context "when mixed content is true and child is content_mapping" do
977
- let(:map_all_error) { "map_all is not allowed with other mappings" }
978
977
  let(:invalid_element) { "must be defined without namespace" }
979
978
  let(:mfenced) do
980
979
  <<~XML
@@ -994,7 +993,7 @@ RSpec.describe Lutaml::Xml::Mapping do
994
993
  XML
995
994
  end
996
995
 
997
- it "raises error when map_all used in content_mapping without custom methods" do
996
+ it "allows map_all alongside element mappings (#223)" do
998
997
  original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
999
998
 
1000
999
  begin
@@ -1005,7 +1004,7 @@ RSpec.describe Lutaml::Xml::Mapping do
1005
1004
  XmlMappingSpec::MmlMath.xml do
1006
1005
  map_all to: :all_content
1007
1006
  end
1008
- end.to raise_error(StandardError, map_all_error)
1007
+ end.not_to raise_error
1009
1008
  ensure
1010
1009
  XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
1011
1010
  { xml: original_mapping })
@@ -1013,16 +1012,16 @@ RSpec.describe Lutaml::Xml::Mapping do
1013
1012
  end
1014
1013
  end
1015
1014
 
1016
- it "can be defined after any other mapping" do
1015
+ it "still validates its own arguments when combined (#223)" do
1017
1016
  original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
1018
1017
 
1019
1018
  begin
1020
1019
  expect do
1021
1020
  XmlMappingSpec::MmlMath.xml do
1022
- map_all to: :all_content
1023
- namespace XmiNewNamespace
1021
+ map_all to: :all_content, namespace: "http://explicit.ns"
1024
1022
  end
1025
- end.to raise_error(StandardError, map_all_error)
1023
+ end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError,
1024
+ /namespace is not allowed/)
1026
1025
  ensure
1027
1026
  XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
1028
1027
  { xml: original_mapping })
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.39
4
+ version: 0.8.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.