lutaml-model 0.8.42 → 0.8.44

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/docs/_guides/advanced-mapping.adoc +66 -0
  3. data/docs/_tutorials/polymorphic-models-tutorial.adoc +2 -0
  4. data/lib/lutaml/key_value/mapping.rb +3 -1
  5. data/lib/lutaml/key_value/mapping_rule.rb +4 -1
  6. data/lib/lutaml/key_value/transform.rb +2 -0
  7. data/lib/lutaml/key_value/transformation/rule_compiler.rb +3 -0
  8. data/lib/lutaml/model/collection_handler.rb +11 -1
  9. data/lib/lutaml/model/mapping/mapping_rule.rb +23 -1
  10. data/lib/lutaml/model/serialize/initialization.rb +7 -2
  11. data/lib/lutaml/model/serialize.rb +12 -3
  12. data/lib/lutaml/model/version.rb +1 -1
  13. data/lib/lutaml/xml/adapter_element.rb +2 -1
  14. data/lib/lutaml/xml/element.rb +8 -2
  15. data/lib/lutaml/xml/mapping.rb +20 -1
  16. data/lib/lutaml/xml/mapping_rule.rb +13 -1
  17. data/lib/lutaml/xml/model_transform.rb +69 -5
  18. data/lib/lutaml/xml/parse_session.rb +33 -2
  19. data/lib/lutaml/xml/transformation/element_builder.rb +25 -6
  20. data/lib/lutaml/xml/transformation/order_reconciler.rb +16 -1
  21. data/lib/lutaml/xml/transformation/ordered_applier.rb +73 -2
  22. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
  23. data/lib/lutaml/xml/xml_element.rb +20 -1
  24. data/spec/lutaml/key_value/transformation/rule_compiler_spec.rb +16 -6
  25. data/spec/lutaml/key_value/transformation_spec.rb +4 -1
  26. data/spec/lutaml/model/attributes_memo_clear_race_spec.rb +90 -0
  27. data/spec/lutaml/model/hydrate_only_mapping_spec.rb +47 -0
  28. data/spec/lutaml/xml/when_attribute_ordered_spec.rb +148 -0
  29. data/spec/lutaml/xml/when_attribute_spec.rb +225 -0
  30. metadata +6 -2
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#88: attribute-value dispatch on a shared wire name.
6
+ # `map_element "component", to: :guidance, when_attribute: { "type" =>
7
+ # "guidance" }` selects only the occurrences whose sibling attribute carries
8
+ # the expected value, and re-emits the discriminator on serialization.
9
+ class WhenAttrComponent < Lutaml::Model::Serializable
10
+ attribute :text, :string
11
+
12
+ xml do
13
+ element "component"
14
+ map_element "text", to: :text
15
+ end
16
+ end
17
+
18
+ class WhenAttrRequirement < Lutaml::Model::Serializable
19
+ attribute :guidance, WhenAttrComponent, collection: true
20
+ attribute :purpose, WhenAttrComponent, collection: true
21
+ attribute :test_method, WhenAttrComponent, collection: true
22
+
23
+ xml do
24
+ element "requirement"
25
+ map_element "component", to: :guidance,
26
+ when_attribute: { "type" => "guidance" }
27
+ map_element "component", to: :purpose,
28
+ when_attribute: { "type" => "purpose" }
29
+ map_element "component", to: :test_method,
30
+ when_attribute: { type: "test-method" }
31
+ end
32
+ end
33
+
34
+ RSpec.describe "when_attribute discriminator mappings" do
35
+ before do
36
+ stub_const("WhenAttr::Component", WhenAttrComponent)
37
+ stub_const("WhenAttr::Requirement", WhenAttrRequirement)
38
+ end
39
+
40
+ let(:xml) do
41
+ <<~XML
42
+ <requirement>
43
+ <component type="guidance"><text>g1</text></component>
44
+ <component type="purpose"><text>p1</text></component>
45
+ <component type="guidance"><text>g2</text></component>
46
+ <component type="test-method"><text>t1</text></component>
47
+ </requirement>
48
+ XML
49
+ end
50
+
51
+ it "dispatches occurrences by the discriminator value" do
52
+ req = WhenAttr::Requirement.from_xml(xml)
53
+
54
+ expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
55
+ expect(req.purpose.map(&:text)).to eq(["p1"])
56
+ expect(req.test_method.map(&:text)).to eq(["t1"])
57
+ end
58
+
59
+ it "keeps document order within each target attribute" do
60
+ req = WhenAttr::Requirement.from_xml(xml)
61
+ expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
62
+ end
63
+
64
+ it "drops occurrences whose discriminator is not covered" do
65
+ doc = '<requirement><component type="unknown"><text>x</text></component></requirement>'
66
+ req = WhenAttr::Requirement.from_xml(doc)
67
+
68
+ expect(req.guidance).to be_empty
69
+ expect(req.purpose).to be_empty
70
+ end
71
+
72
+ it "re-emits the discriminator attribute on serialization" do
73
+ round = WhenAttr::Requirement.from_xml(WhenAttr::Requirement.from_xml(xml).to_xml)
74
+
75
+ expect(round.guidance.map(&:text)).to eq(%w[g1 g2])
76
+ expect(round.purpose.map(&:text)).to eq(["p1"])
77
+ expect(round.test_method.map(&:text)).to eq(["t1"])
78
+
79
+ serialized = WhenAttr::Requirement.from_xml(xml).to_xml
80
+ expect(serialized).to include('type="guidance"')
81
+ expect(serialized).to include('type="purpose"')
82
+ expect(serialized).to include('type="test-method"')
83
+ end
84
+
85
+ it "does not emit a discriminator for ordinary rules" do
86
+ plain = Class.new(Lutaml::Model::Serializable) do
87
+ attribute :text, :string
88
+
89
+ xml do
90
+ element "plain"
91
+ map_element "text", to: :text
92
+ end
93
+ end
94
+
95
+ expect(plain.from_xml("<plain><text>t</text></plain>").to_xml)
96
+ .not_to include("type=")
97
+ end
98
+
99
+ it "survives deep_dup of mappings" do
100
+ mapping = WhenAttr::Requirement.mappings_for(:xml)
101
+ duped = mapping.deep_dup.mappings.find do |r|
102
+ r.to == :guidance
103
+ end
104
+
105
+ expect(duped.when_attribute).to eq("type" => "guidance")
106
+ end
107
+
108
+ it "rejects non-string/symbol discriminator shapes" do
109
+ expect do
110
+ Class.new(Lutaml::Model::Serializable) do
111
+ attribute :x, :string
112
+
113
+ xml do
114
+ element "x"
115
+ map_element "x", to: :x, when_attribute: { "type" => 42 }
116
+ end
117
+ end
118
+ end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
119
+ end
120
+
121
+ it "is rejected on attribute mappings" do
122
+ expect do
123
+ Class.new(Lutaml::Model::Serializable) do
124
+ attribute :x, :string
125
+
126
+ xml do
127
+ element "x"
128
+ map_attribute "x", to: :x, when_attribute: { "type" => "guidance" }
129
+ end
130
+ end
131
+ end.to raise_error(ArgumentError)
132
+ end
133
+
134
+ # Orthogonality guard: `when_attribute` partitions occurrences across
135
+ # attributes; `polymorphic` dispatches the class of each hydrated item.
136
+ # Different axes over the same discriminator concept — they compose on
137
+ # one rule rather than substitute for each other.
138
+ describe "vs polymorphic dispatch" do
139
+ before do
140
+ stub_const("AxisComp", Class.new(Lutaml::Model::Serializable) do
141
+ attribute :text, :string
142
+ end)
143
+ stub_const("AxisGuidanceComp", Class.new(AxisComp))
144
+ stub_const("AxisPurposeComp", Class.new(AxisComp))
145
+ end
146
+
147
+ let(:axes_xml) do
148
+ <<~XML
149
+ <req>
150
+ <component type="guidance"><text>g1</text></component>
151
+ <component type="purpose"><text>p1</text></component>
152
+ <component type="guidance"><text>g2</text></component>
153
+ </req>
154
+ XML
155
+ end
156
+
157
+ it "polymorphic keeps one attribute and dispatches classes" do
158
+ poly = Class.new(Lutaml::Model::Serializable) do
159
+ attribute :components, AxisComp, collection: true
160
+
161
+ xml do
162
+ element "req"
163
+ map_element "component", to: :components, polymorphic: {
164
+ attribute: "type",
165
+ class_map: {
166
+ "guidance" => "AxisGuidanceComp",
167
+ "purpose" => "AxisPurposeComp",
168
+ },
169
+ }
170
+ end
171
+ end
172
+
173
+ parsed = poly.from_xml(axes_xml)
174
+ expect(parsed.components.map(&:class))
175
+ .to eq([AxisGuidanceComp, AxisPurposeComp, AxisGuidanceComp])
176
+ end
177
+
178
+ it "when_attribute keeps one class and partitions attributes" do
179
+ partitioned = Class.new(Lutaml::Model::Serializable) do
180
+ attribute :guidance, AxisComp, collection: true
181
+ attribute :purpose, AxisComp, collection: true
182
+
183
+ xml do
184
+ element "req"
185
+ map_element "component", to: :guidance,
186
+ when_attribute: { "type" => "guidance" }
187
+ map_element "component", to: :purpose,
188
+ when_attribute: { "type" => "purpose" }
189
+ end
190
+ end
191
+
192
+ parsed = partitioned.from_xml(axes_xml)
193
+ expect(parsed.guidance.map(&:text)).to eq(%w[g1 g2])
194
+ expect(parsed.purpose.map(&:text)).to eq(["p1"])
195
+ expect(parsed.guidance.first.class).to eq(AxisComp)
196
+ end
197
+
198
+ it "composes: partition and class dispatch on the same rule" do
199
+ both = Class.new(Lutaml::Model::Serializable) do
200
+ attribute :guidance, AxisComp, collection: true
201
+ attribute :purpose, AxisComp, collection: true
202
+
203
+ xml do
204
+ element "req"
205
+ map_element "component", to: :guidance,
206
+ when_attribute: { "type" => "guidance" },
207
+ polymorphic: {
208
+ attribute: "type",
209
+ class_map: { "guidance" => "AxisGuidanceComp" },
210
+ }
211
+ map_element "component", to: :purpose,
212
+ when_attribute: { "type" => "purpose" },
213
+ polymorphic: {
214
+ attribute: "type",
215
+ class_map: { "purpose" => "AxisPurposeComp" },
216
+ }
217
+ end
218
+ end
219
+
220
+ parsed = both.from_xml(axes_xml)
221
+ expect(parsed.guidance.map(&:class)).to eq([AxisGuidanceComp, AxisGuidanceComp])
222
+ expect(parsed.purpose.map(&:class)).to eq([AxisPurposeComp])
223
+ end
224
+ end
225
+ 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.42
4
+ version: 0.8.44
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-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1695,6 +1695,7 @@ files:
1695
1695
  - spec/lutaml/model/attribute_form_default_spec.rb
1696
1696
  - spec/lutaml/model/attribute_restriction_spec.rb
1697
1697
  - spec/lutaml/model/attribute_spec.rb
1698
+ - spec/lutaml/model/attributes_memo_clear_race_spec.rb
1698
1699
  - spec/lutaml/model/boot_manifest_spec.rb
1699
1700
  - spec/lutaml/model/cached_type_resolver_spec.rb
1700
1701
  - spec/lutaml/model/cardinality_strict_spec.rb
@@ -1737,6 +1738,7 @@ files:
1737
1738
  - spec/lutaml/model/global_register_spec.rb
1738
1739
  - spec/lutaml/model/group_spec.rb
1739
1740
  - spec/lutaml/model/hash/adapter_spec.rb
1741
+ - spec/lutaml/model/hydrate_only_mapping_spec.rb
1740
1742
  - spec/lutaml/model/import_registry_spec.rb
1741
1743
  - spec/lutaml/model/included_spec.rb
1742
1744
  - spec/lutaml/model/inheritance_spec.rb
@@ -2004,6 +2006,8 @@ files:
2004
2006
  - spec/lutaml/xml/validate_xml_with_constructs_spec.rb
2005
2007
  - spec/lutaml/xml/validate_xml_with_spec.rb
2006
2008
  - spec/lutaml/xml/w3c_types_spec.rb
2009
+ - spec/lutaml/xml/when_attribute_ordered_spec.rb
2010
+ - spec/lutaml/xml/when_attribute_spec.rb
2007
2011
  - spec/lutaml/xml/xml_adapter_spec.rb
2008
2012
  - spec/lutaml/xml/xml_declaration_spec.rb
2009
2013
  - spec/lutaml/xml/xml_element_guard_spec.rb