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 +4 -4
- data/lib/lutaml/model/comparable_model.rb +135 -0
- data/lib/lutaml/model/serializable.rb +4 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
- data/lib/lutaml/model/store.rb +19 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +16 -3
- data/lib/lutaml/xml/adapter/xml_serializer.rb +13 -2
- data/lib/lutaml/xml/adapter_element.rb +7 -0
- data/lib/lutaml/xml/mapping.rb +8 -3
- data/lib/lutaml/xml/model_transform.rb +25 -5
- data/lib/lutaml/xml/transformation/rule_applier.rb +10 -1
- data/lib/lutaml/xml/transformation.rb +7 -2
- data/spec/lutaml/model/comparison_family_spec.rb +88 -0
- data/spec/lutaml/model/map_all_spec.rb +23 -19
- data/spec/lutaml/model/store_spec.rb +7 -1
- data/spec/lutaml/xml/mapping_spec.rb +6 -7
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fc47a2bee1b20995c17426890ae3fdc3f9db24317a8cb0c83ea3e81f13372be
|
|
4
|
+
data.tar.gz: 72b52030485d681b901b077b75195a40d7779c0ec12df982e8bc7f49b3ef9dad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/lutaml/model/store.rb
CHANGED
|
@@ -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
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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)
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -979,9 +979,14 @@ module Lutaml
|
|
|
979
979
|
"is allowed with #{TYPES[:all_content]}"
|
|
980
980
|
end
|
|
981
981
|
|
|
982
|
-
if
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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 "
|
|
189
|
-
it "
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
195
|
-
StandardError,
|
|
196
|
-
"map_all is not allowed with other mappings",
|
|
197
|
-
)
|
|
198
|
-
end
|
|
199
|
+
end
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 "
|
|
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.
|
|
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 "
|
|
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(
|
|
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.
|
|
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
|