lutaml-model 0.8.39 → 0.8.41

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: 6fc47a2bee1b20995c17426890ae3fdc3f9db24317a8cb0c83ea3e81f13372be
4
+ data.tar.gz: 72b52030485d681b901b077b75195a40d7779c0ec12df982e8bc7f49b3ef9dad
5
5
  SHA512:
6
- metadata.gz: 9f1372cbf492e141015524382a3ac98cdc5f484441d70c593b7ad67b9021bfa959732700679534bb3690f86c064e3a1a82269833c5c73794fe87af24f21d239b
7
- data.tar.gz: '048d412becdba7a8a99070740663d0d0a33e88d02018708818ce816d1ba4669a1f26efbf08d0c67390818c5efede0e362ef8f1785536c4233a2fa6dadd07da61'
6
+ metadata.gz: dc281b89a9c032cff1f2a38de58b465dccc5385421d9daed7a8244adec62a282a79163b392224a2ead89d93281bbf1d6f5772f879f088a930332ae8821c7784e
7
+ data.tar.gz: 1d0c2b049cc9c45e7dfe0dd0a6ec6326c98011ab09c6a9f32eff39b36a67cfce1dd97f20341d301258acfc9c9eb282d43718d3a7ab0e0401ae29e6d1a936d724
@@ -32,6 +32,32 @@ module Lutaml
32
32
 
33
33
  alias == eql?
34
34
 
35
+ # Order-insensitive equality (lutaml-model#17): collection
36
+ # attributes compare as multisets — document order within a
37
+ # collection does not affect the result. Non-collection
38
+ # attributes and nested models keep strict equality.
39
+ #
40
+ # @param other [Object] the object to compare with
41
+ # @param ignore_element_order [Boolean] compare collections
42
+ # order-insensitively
43
+ # @return [Boolean]
44
+ def same_as?(other, ignore_element_order: true)
45
+ return true if equal?(other)
46
+ return false unless instance_of?(other.class)
47
+
48
+ unordered_eql?(self, other, {}, ignore_element_order)
49
+ end
50
+
51
+ # Structured diff (lutaml-model#18): returns one entry per
52
+ # differing attribute path, recursively through nested models
53
+ # and collections. Equal objects diff to [].
54
+ #
55
+ # @param other [Object] the object to diff against
56
+ # @return [Array<Hash>] [{ path:, left:, right: }] entries
57
+ def diff(other)
58
+ Lutaml::Model::ComparableModel::StructuralDiff.diff(self, other)
59
+ end
60
+
35
61
  def same_class?(other)
36
62
  other.instance_of?(self.class)
37
63
  end
@@ -145,6 +171,72 @@ module Lutaml
145
171
  end
146
172
  end
147
173
 
174
+ # Structured, path-addressed diff between two model trees
175
+ # (lutaml-model#18). Used by ComparableModel#diff.
176
+ module StructuralDiff
177
+ Entry = ::Struct.new(:path, :left, :right)
178
+
179
+ module_function
180
+
181
+ def diff(left, right, path = [])
182
+ return [] if left.equal?(right)
183
+ return [Entry.new(path_string(path), left, right)] unless comparable_pair?(left, right)
184
+
185
+ entries = []
186
+ left.class.attributes.each_key do |attr|
187
+ lval = left.public_send(attr)
188
+ rval = right.public_send(attr)
189
+ child_path = path + [attr]
190
+
191
+ if comparable_pair?(lval, rval)
192
+ entries.concat(diff(lval, rval, child_path))
193
+ elsif lval.is_a?(::Array) && rval.is_a?(::Array)
194
+ entries.concat(diff_collections(lval, rval, child_path))
195
+ elsif differing?(lval, rval)
196
+ entries << Entry.new(path_string(child_path), lval, rval)
197
+ end
198
+ end
199
+ entries
200
+ end
201
+
202
+ def diff_collections(lval, rval, path)
203
+ entries = []
204
+ max = [lval.size, rval.size].max
205
+ max.times do |i|
206
+ li = lval[i]
207
+ ri = rval[i]
208
+ child_path = path + [i]
209
+
210
+ if comparable_pair?(li, ri)
211
+ entries.concat(diff(li, ri, child_path))
212
+ elsif differing?(li, ri)
213
+ entries << Entry.new(path_string(child_path), li, ri)
214
+ end
215
+ end
216
+ entries
217
+ end
218
+
219
+ def comparable_pair?(left, right)
220
+ left.is_a?(ComparableModel) && right.is_a?(ComparableModel) &&
221
+ left.instance_of?(right.class)
222
+ end
223
+
224
+ def differing?(lval, rval)
225
+ if lval.is_a?(::Array) && rval.is_a?(::Array)
226
+ lval.size != rval.size ||
227
+ lval.zip(rval).any? { |l, r| l != r }
228
+ else
229
+ lval != rval
230
+ end
231
+ end
232
+
233
+ def path_string(path)
234
+ path.reduce("") do |acc, seg|
235
+ seg.is_a?(::Integer) ? "#{acc}[#{seg}]" : "#{acc}.#{seg}"
236
+ end.delete_prefix(".")
237
+ end
238
+ end
239
+
148
240
  # DiffContext handles the comparison between two objects
149
241
  class DiffContext
150
242
  attr_reader :obj1, :obj2, :show_unchanged, :highlight_diff, :use_colors
@@ -556,6 +648,49 @@ type_info = nil)
556
648
  end
557
649
  end
558
650
  end
651
+
652
+ private
653
+
654
+ def unordered_eql?(left, right, seen, ignore_order)
655
+ return true if left.equal?(right)
656
+ return false unless left.instance_of?(right.class)
657
+
658
+ seen[left] ||= {}.compare_by_identity
659
+ return true if seen[left][right]
660
+
661
+ seen[left][right] = true
662
+ left.class.attributes.keys.all? do |attr|
663
+ lval = left.public_send(attr)
664
+ rval = right.public_send(attr)
665
+
666
+ if lval.is_a?(ComparableModel) && rval.is_a?(ComparableModel)
667
+ unordered_eql?(lval, rval, seen, ignore_order)
668
+ elsif ignore_order && lval.is_a?(::Array) && rval.is_a?(::Array)
669
+ unordered_items_equal?(lval, rval, seen)
670
+ else
671
+ lval == rval
672
+ end
673
+ end
674
+ end
675
+
676
+ def unordered_items_equal?(lval, rval, seen)
677
+ return false unless lval.size == rval.size
678
+
679
+ unmatched = rval.dup
680
+ lval.all? do |litem|
681
+ idx = unmatched.index do |ritem|
682
+ if litem.is_a?(ComparableModel) && ritem.is_a?(ComparableModel)
683
+ unordered_eql?(litem, ritem, seen, true)
684
+ else
685
+ litem == ritem
686
+ end
687
+ end
688
+ return false unless idx
689
+
690
+ unmatched.delete_at(idx)
691
+ true
692
+ end
693
+ end
559
694
  end
560
695
 
561
696
  # Generates a tree representation of the object
@@ -3,7 +3,11 @@
3
3
  module Lutaml
4
4
  module Model
5
5
  class Serializable
6
+ # ComparableModel is included at the Serialize module level; its
7
+ # ClassMethods (diff_with_score) must extend the class that
8
+ # model classes actually inherit from (lutaml-model#18).
6
9
  include Serialize
10
+ extend ComparableModel::ClassMethods
7
11
  end
8
12
  end
9
13
  end
@@ -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.41"
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
@@ -11,6 +11,10 @@ module Lutaml
11
11
  # - Content rules -> apply_content_rule
12
12
  # - Raw rules -> apply_raw_rule
13
13
  module RuleApplier
14
+ # The answer is static per rule object; transformations are
15
+ # frozen, so the memo lives here instead — bounded by the
16
+ # distinct rule count (TODO.max-perf/20).
17
+ CUSTOM_ONLY_CACHE = {}.compare_by_identity
14
18
  include SkipLogic
15
19
  include ElementBuilder
16
20
 
@@ -226,8 +230,13 @@ register_id)
226
230
  #
227
231
  # @param rule [CompiledRule] The rule
228
232
  # @return [Boolean] true if custom method only
233
+ # The answer is static per rule object and transformations are
234
+ # frozen — a process-global identity-keyed memo is bounded by
235
+ # the distinct rule count and lives no longer than the rules
236
+ # (240k calls → ~one computation per rule on the ISO-13849
237
+ # serialize profile, TODO.max-perf/20).
229
238
  def custom_method_only?(rule)
230
- rule.custom_method_only?
239
+ CUSTOM_ONLY_CACHE[rule] ||= rule.custom_method_only?
231
240
  end
232
241
 
233
242
  # Extract value for a rule
@@ -262,13 +262,15 @@ module Lutaml
262
262
  # @param model_instance [Object] The model instance
263
263
  # @param options [Hash] Options
264
264
  def apply_ordered_rules(root, model_instance, options)
265
+ # Loop-invariant for the whole ordered walk (see
266
+ # apply_standard_rules): one merge per model, not per rule.
267
+ rule_options = options.merge(current_model: model_instance)
265
268
  apply_rules_in_order(
266
269
  root, model_instance, options,
267
270
  compiled_rules, model_class, register_id
268
271
  ) do |action, rule, value, set_xsi_nil|
269
272
  next if action == :apply_rule && duplicate_element_rules[rule]
270
273
 
271
- rule_options = options.merge(current_model: model_instance)
272
274
  case action
273
275
  when :apply_rule
274
276
  apply_rule(root, rule, model_instance, rule_options, model_class,
@@ -331,11 +333,14 @@ module Lutaml
331
333
  compiled_rules
332
334
  end
333
335
 
336
+ # The merge is loop-invariant (model_instance is fixed for the
337
+ # walk) — hoisted out of the per-rule loop. It was ~220k
338
+ # allocations on the ISO-13849 serialize profile (TODO.max-perf/20).
339
+ rule_options = options.merge(current_model: model_instance)
334
340
  rules.each do |rule|
335
341
  next unless valid_mapping?(rule, options)
336
342
  next if duplicate_element_rules[rule]
337
343
 
338
- rule_options = options.merge(current_model: model_instance)
339
344
  apply_rule(root, rule, model_instance, rule_options, model_class,
340
345
  register_id, register)
341
346
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#17 (order-insensitive equality) and #18 (structured
6
+ # diff), on top of the existing eql?/diff_with_score machinery.
7
+ RSpec.describe "Comparison family" do
8
+ let(:phone_a) { Phone.new(kind: "home", number: "555-0100") }
9
+ let(:phone_b) { Phone.new(kind: "work", number: "555-0177") }
10
+
11
+ before do
12
+ stub_const("Phone", Class.new(Lutaml::Model::Serializable) do
13
+ attribute :kind, :string
14
+ attribute :number, :string
15
+ end)
16
+ stub_const("Person", Class.new(Lutaml::Model::Serializable) do
17
+ attribute :name, :string
18
+ attribute :age, :integer
19
+ attribute :phones, Phone, collection: true
20
+ end)
21
+ end
22
+
23
+ describe "#same_as? (#17)" do
24
+ it "treats reordered collections as equal" do
25
+ a = Person.new(phones: [phone_a, phone_b])
26
+ b = Person.new(phones: [phone_b, phone_a])
27
+ expect(a == b).to be(false)
28
+ expect(a.same_as?(b)).to be(true)
29
+ end
30
+
31
+ it "detects genuinely different collections regardless of order" do
32
+ a = Person.new(phones: [phone_a, phone_b])
33
+ b = Person.new(phones: [phone_a, Phone.new(kind: "fax", number: "9")])
34
+ expect(a.same_as?(b)).to be(false)
35
+ end
36
+
37
+ it "respects strict mode" do
38
+ a = Person.new(phones: [phone_a, phone_b])
39
+ b = Person.new(phones: [phone_b, phone_a])
40
+ expect(a.same_as?(b, ignore_element_order: false)).to be(false)
41
+ end
42
+
43
+ it "compares nested models recursively order-insensitively" do
44
+ box1 = Class.new(Lutaml::Model::Serializable) do
45
+ attribute :people, Person, collection: true
46
+ end
47
+ p1 = Person.new(name: "Ada", phones: [phone_a, phone_b])
48
+ p2 = Person.new(name: "Ada", phones: [phone_b, phone_a])
49
+ expect(box1.new(people: [p1]).same_as?(box1.new(people: [p2]))).to be(true)
50
+ end
51
+ end
52
+
53
+ describe "#diff (#18)" do
54
+ it "returns one entry per differing path" do
55
+ a = Person.new(name: "Ada", age: 36, phones: [phone_a])
56
+ b = Person.new(name: "Ada", age: 37, phones: [phone_b])
57
+ entries = a.diff(b)
58
+
59
+ paths = entries.map(&:path)
60
+ expect(paths).to contain_exactly("age", "phones[0].kind", "phones[0].number")
61
+ end
62
+
63
+ it "reports length differences at the collection level" do
64
+ a = Person.new(phones: [phone_a])
65
+ b = Person.new(phones: [phone_a, phone_b])
66
+ entries = a.diff(b)
67
+
68
+ expect(entries.size).to eq(1)
69
+ expect(entries.first.path).to eq("phones[1]")
70
+ expect(entries.first.right).to eq(phone_b)
71
+ end
72
+
73
+ it "diffs equal objects to nothing" do
74
+ a = Person.new(name: "Ada", age: 36, phones: [phone_a])
75
+ expect(a.diff(a.dup)).to be_empty
76
+ end
77
+ end
78
+
79
+ describe ".diff_with_score (#18, pre-existing)" do
80
+ it "is reachable from model classes" do
81
+ a = Person.new(age: 36)
82
+ b = Person.new(age: 37)
83
+ score, tree = Person.diff_with_score(a, b)
84
+ expect(score).to be_a(Float)
85
+ expect(tree).to include("age")
86
+ end
87
+ end
88
+ end
@@ -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.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1711,6 +1711,7 @@ files:
1711
1711
  - spec/lutaml/model/collection_state_nesting_spec.rb
1712
1712
  - spec/lutaml/model/collection_validation_spec.rb
1713
1713
  - spec/lutaml/model/comparable_model_spec.rb
1714
+ - spec/lutaml/model/comparison_family_spec.rb
1714
1715
  - spec/lutaml/model/compiled_rule_spec.rb
1715
1716
  - spec/lutaml/model/consolidation_spec.rb
1716
1717
  - spec/lutaml/model/context_registry_spec.rb