lutaml-model 0.8.43 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe2357c36c2a199a3788090dffd420874612482e21e7ac678bd5f1e6fdd575ce
4
- data.tar.gz: a3d601af07558b2915dce9f4df383c00b905505c8fbcd192a89ad88b3e66ea49
3
+ metadata.gz: 57902ec64a7b7915f601d0525d747ec742c78a18bd7e78469c0af41aa35d2101
4
+ data.tar.gz: a27ee76ad1f66aaacb324c5ba79f75dca698c3f729c2e61706ed4cbe85c0b255
5
5
  SHA512:
6
- metadata.gz: c82bb3761127b4beb8f2d267a60d107d69eb509cd2fd094763eed5ab5d0814497252db4cd1311709072ab829b156aac46f839852bcc4f20f37e627aa7b5daea7
7
- data.tar.gz: 32aa679d229c03ccccf0ee8f1359f9559309b92f5493a0207fd5a495e0f8ef1a0cc3255a0e4e30c2e7c5319e8910a8863286116d6a0e4bfe72af1a0b8be2c71f
6
+ metadata.gz: fd6d918fcd42d655682bc769f9c047860a3bba698258137f638cafd789f7b8ee0e176c89a12c84f09310245aaa227bc33d3c344c255591b14527a691acca6088
7
+ data.tar.gz: 36bf72d194bc8ed3e5ddb8837c55f5dd3170006ecf37cfc26004f46133fc6684e267d56123577e06b9ca9fe2d3dfcab838a21b6bd3b884409613ea364276329e
@@ -592,5 +592,39 @@ writes that attribute).
592
592
 
593
593
  Co-occurrence semantics: several `when_attribute` rules may share the
594
594
  element name; each target attribute fills independently, so declare each
595
- as `collection: true` when the element repeats. Rules without
596
- `when_attribute` keep claiming every occurrence of their name as before.
595
+ as `collection: true` when the element repeats. A rule without
596
+ `when_attribute` that shares the name only claims occurrences no
597
+ discriminator rule claimed, so every occurrence is captured exactly once.
598
+
599
+ `when_attribute` and `polymorphic:` are two different dispatch axes over
600
+ the same discriminator idea — pick by what the discriminator selects:
601
+
602
+ * `polymorphic: { attribute:, class_map: }` — the value selects the
603
+ **class** each occurrence hydrates into; ONE attribute holds every
604
+ occurrence. See the polymorphic models tutorial.
605
+ * `when_attribute:` — the value selects **which attribute** claims the
606
+ occurrence; the declared type stays fixed.
607
+
608
+ They compose: a rule may partition with `when_attribute` while its
609
+ attribute dispatches classes with `polymorphic`.
610
+
611
+ Under an `ordered` mapping, serialization preserves the original
612
+ interleaving: `element_order` records each occurrence's attributes, and
613
+ the ordered walk routes every occurrence back to the rule whose
614
+ discriminator it satisfies.
615
+
616
+ [source,ruby]
617
+ ----
618
+ xml do
619
+ element "requirement"
620
+ ordered
621
+ map_element "component", to: :guidance,
622
+ when_attribute: { "type" => "guidance" }
623
+ map_element "component", to: :purpose,
624
+ when_attribute: { "type" => "purpose" }
625
+ end
626
+ ----
627
+
628
+ Given `<component type="guidance">`, `<component type="purpose">`,
629
+ `<component type="guidance">` in the source, `to_xml` re-emits exactly
630
+ that guidance, purpose, guidance sequence.
@@ -9,6 +9,8 @@ nav_order: 8
9
9
 
10
10
  Polymorphic attributes allow an attribute to accept multiple types of values. This is useful when attributes share common characteristics but have type-specific behaviors.
11
11
 
12
+ NOTE: Polymorphism dispatches the *class* of each hydrated item into one attribute. If the discriminator instead needs to route occurrences of a shared element name into *different attributes* (e.g. `component[@type='guidance']` → `guidance`, `component[@type='purpose']` → `purpose`), that is `when_attribute:` in the advanced mapping guide — the two compose on one rule.
13
+
12
14
  == Understanding polymorphism
13
15
 
14
16
  A polymorphic attribute can accept instances of different classes that share a common base class.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.43"
5
+ VERSION = "0.8.44"
6
6
  end
7
7
  end
@@ -302,7 +302,8 @@ module Lutaml
302
302
  Lutaml::Xml::Element.new("Element", child.unprefixed_name,
303
303
  node_type: :element,
304
304
  namespace_uri: child.namespace_uri,
305
- namespace_prefix: child.namespace_prefix)
305
+ namespace_prefix: child.namespace_prefix,
306
+ attributes: order_entry_attributes(child))
306
307
  end
307
308
  end
308
309
  end.each(&:freeze).freeze
@@ -4,7 +4,7 @@ module Lutaml
4
4
  include Lutaml::Model::Liquefiable
5
5
 
6
6
  attr_reader :type, :name, :text_content, :node_type, :namespace_uri,
7
- :namespace_prefix
7
+ :namespace_prefix, :attributes
8
8
 
9
9
  # Create a new Element for order tracking
10
10
  #
@@ -14,8 +14,13 @@ module Lutaml
14
14
  # @param node_type [Symbol, nil] The node type (:text, :cdata, :element, :comment, :processing_instruction)
15
15
  # @param namespace_uri [String, nil] The namespace URI of this element
16
16
  # @param namespace_prefix [String, nil] The namespace prefix of this element
17
+ # @param attributes [::Hash{String=>String}, nil] The element's
18
+ # attributes keyed by namespaced name. Only element entries parsed
19
+ # from a document carry them, so `when_attribute` rules can tell
20
+ # same-name entries apart on ordered serialization.
17
21
  def initialize(type, name, text_content: nil, node_type: nil,
18
- namespace_uri: nil, namespace_prefix: nil)
22
+ namespace_uri: nil, namespace_prefix: nil,
23
+ attributes: nil)
19
24
  @type = type # "Text" or "Element" - deprecated, kept for backward compatibility
20
25
  @name = name
21
26
  # Infer node_type from type for backward compatibility if not provided
@@ -26,6 +31,7 @@ module Lutaml
26
31
  @text_content ||= name if text? || cdata?
27
32
  @namespace_uri = namespace_uri
28
33
  @namespace_prefix = namespace_prefix
34
+ @attributes = attributes&.freeze
29
35
  end
30
36
 
31
37
  # Check if this is a text content node (not CDATA)
@@ -1053,11 +1053,24 @@ _effective_register)
1053
1053
 
1054
1054
  # lutaml-model#88: attribute-value discriminator — among the
1055
1055
  # name-matched occurrences, keep only those whose sibling
1056
- # attributes carry the expected values.
1056
+ # attributes carry the expected values. A plain rule sharing the
1057
+ # name with discriminator rules gets the complement — occurrences
1058
+ # no discriminator claimed — so each occurrence is captured
1059
+ # exactly once, mirroring ordered-serialization routing.
1057
1060
  if rule.when_attribute?
1058
1061
  children = children.select do |child|
1059
1062
  rule.matches_when_attribute?(child)
1060
1063
  end
1064
+ else
1065
+ siblings = session.when_attribute_siblings_by_name
1066
+ unless siblings.empty?
1067
+ claimed = rule_names.filter_map { |n| siblings[n] }.flatten
1068
+ unless claimed.empty?
1069
+ children = children.reject do |child|
1070
+ claimed.any? { |sibling| sibling.matches_when_attribute?(child) }
1071
+ end
1072
+ end
1073
+ end
1061
1074
  end
1062
1075
 
1063
1076
  # Performance: Cache rule method check before loop to avoid repeated method calls
@@ -65,6 +65,28 @@ module Lutaml
65
65
  end
66
66
  end
67
67
 
68
+ # lutaml-model#88: element wire names partitioned by `when_attribute`
69
+ # rules, as name -> discriminator rules. A plain rule on a
70
+ # partitioned name captures only occurrences no discriminator
71
+ # claimed — the mirror of ordered-serialization routing, so each
72
+ # occurrence is captured exactly once instead of double-captured by
73
+ # the plain rule and its discriminator sibling. Empty unless the
74
+ # mapping uses when_attribute at all; non-Serialize custom models
75
+ # have no mappings to partition.
76
+ def when_attribute_siblings_by_name
77
+ @when_attribute_siblings_by_name ||=
78
+ if instance_is_serialize && (mapping = xml_mapping)
79
+ mapping.mappings.each_with_object({}) do |rule, index|
80
+ pairs = rule.when_attribute
81
+ next if pairs.nil? || pairs.empty?
82
+
83
+ (index[rule.name.to_s] ||= []) << rule
84
+ end
85
+ else
86
+ {}
87
+ end
88
+ end
89
+
68
90
  def model_class
69
91
  @model_class ||= instance.class
70
92
  end
@@ -220,25 +220,40 @@ module Lutaml
220
220
  # accepts a nil rule namespace, namespace aliases and name aliases.
221
221
  # When more than one rule would claim the entry, reconciling would
222
222
  # feed the value to the wrong rule, so leave the rule alone.
223
+ # Rules distinguished by `when_attribute` are separate rules: their
224
+ # inserted entries carry the discriminator attributes, so they
225
+ # resolve back correctly.
223
226
  def unambiguous?(rule, element_rules)
227
+ discriminator = rule_discriminator(rule)
224
228
  matches = element_rules.select do |candidate|
225
229
  matches_element_rule?(candidate, rule.serialized_name,
226
- rule.namespace_class&.uri)
230
+ rule.namespace_class&.uri) &&
231
+ rule_discriminator(candidate) == discriminator
227
232
  end
228
233
 
229
234
  matches == [rule]
230
235
  end
231
236
 
232
237
  def new_order_entry(rule)
238
+ discriminator = rule_discriminator(rule)
233
239
  ::Lutaml::Xml::Element.new(
234
240
  "Element",
235
241
  rule.serialized_name,
236
242
  node_type: :element,
237
243
  namespace_uri: rule.namespace_class&.uri,
238
244
  namespace_prefix: nil,
245
+ attributes: discriminator && discriminator_attributes(discriminator),
239
246
  )
240
247
  end
241
248
 
249
+ # The discriminator pairs as order-entry attributes, so an inserted
250
+ # entry routes back to its own rule at emit time and re-parses.
251
+ def discriminator_attributes(pairs)
252
+ pairs.each_with_object({}) do |(name, value), attrs|
253
+ attrs[name.to_s] = value.to_s
254
+ end
255
+ end
256
+
242
257
  # Where a rule's new entries go:
243
258
  # - after the last entry that already resolves to it, so extra
244
259
  # collection items follow the existing ones and any interleaved
@@ -122,6 +122,14 @@ model_class, register_id)
122
122
 
123
123
  # Find the mapping rule for an element from element_order
124
124
  #
125
+ # Same-name entries under different `when_attribute` discriminators
126
+ # (all `<component>`s, distinguished by `type="guidance"` vs
127
+ # `type="purpose"`) route to the rule whose discriminator pairs the
128
+ # entry's recorded attributes satisfy. Entries carrying no
129
+ # attributes — built before the entry format carried them, or
130
+ # inserted by hand — fall back to the first plain rule, which is
131
+ # the pre-discriminator behavior.
132
+ #
125
133
  # @param object [Xml::Element] Element from element_order
126
134
  # @param compiled_rules [Array<CompiledRule>] The compiled rules
127
135
  # @return [CompiledRule, nil] The matching rule or nil
@@ -131,11 +139,23 @@ model_class, register_id)
131
139
 
132
140
  object_ns_uri = object.namespace_uri # nil if old element_order (backward compat)
133
141
 
134
- compiled_rules.find do |r|
135
- r.is_a?(::Lutaml::Model::CompiledRule) &&
142
+ first = nil
143
+ shared_name = false
144
+ compiled_rules.each do |r|
145
+ next unless r.is_a?(::Lutaml::Model::CompiledRule) &&
136
146
  r.option(:mapping_type) == :element &&
137
147
  matches_element_rule?(r, object.name, object_ns_uri)
148
+
149
+ if first.nil?
150
+ first = r
151
+ else
152
+ shared_name = true
153
+ break
154
+ end
138
155
  end
156
+ return first unless shared_name
157
+
158
+ resolve_shared_name_rule(object, compiled_rules, object_ns_uri)
139
159
  end
140
160
 
141
161
  private
@@ -175,6 +195,57 @@ model_class, register_id)
175
195
  rule.matches_name?(name) && rule_ns_matches
176
196
  end
177
197
 
198
+ # Pick among rules sharing a serialized name when the entry's
199
+ # recorded attributes are the only thing that tells them apart.
200
+ #
201
+ # A discriminator rule whose pairs the entry satisfies wins over
202
+ # any plain rule regardless of declaration order, because a plain
203
+ # rule would claim EVERY same-name entry including discriminated
204
+ # ones. Entries matching no discriminator pair — an unknown value,
205
+ # or an entry without attributes — fall back to the first plain
206
+ # rule.
207
+ def resolve_shared_name_rule(object, compiled_rules, object_ns_uri)
208
+ plain = nil
209
+ compiled_rules.each do |r|
210
+ next unless r.is_a?(::Lutaml::Model::CompiledRule) &&
211
+ r.option(:mapping_type) == :element &&
212
+ matches_element_rule?(r, object.name, object_ns_uri)
213
+
214
+ pairs = rule_discriminator(r)
215
+ return r if pairs && entry_matches_when_attribute?(object, pairs)
216
+
217
+ plain ||= r if pairs.nil?
218
+ end
219
+ plain
220
+ end
221
+
222
+ # The rule's `when_attribute` discriminator pairs, or nil for a
223
+ # plain rule. Shared by OrderedApplier and OrderReconciler since
224
+ # both run on the same transform host.
225
+ #
226
+ # @param rule [CompiledRule]
227
+ # @return [::Hash, nil]
228
+ def rule_discriminator(rule)
229
+ pairs = rule.option(:when_attribute)
230
+ pairs && !pairs.empty? ? pairs : nil
231
+ end
232
+
233
+ # Whether an order entry's recorded attributes satisfy a rule's
234
+ # discriminator pairs. Mirrors Xml::MappingRule#matches_when_attribute?
235
+ # — non-nil and string-equal — against the namespaced-name keying
236
+ # the entries record.
237
+ def entry_matches_when_attribute?(object, pairs)
238
+ return false unless object.is_a?(::Lutaml::Xml::Element)
239
+
240
+ attrs = object.attributes
241
+ return false if attrs.nil?
242
+
243
+ pairs.all? do |name, expected|
244
+ actual = attrs[name.to_s]
245
+ !actual.nil? && actual.to_s == expected.to_s
246
+ end
247
+ end
248
+
178
249
  def process_element_order_item(object, root, model_instance, options,
179
250
  compiled_rules, _mapping, element_indices,
180
251
  content_rule = nil, text_node_index = 0,
@@ -284,11 +284,30 @@ module Lutaml
284
284
  Lutaml::Xml::Element.new("Element", child.unprefixed_name,
285
285
  node_type: :element,
286
286
  namespace_uri: child.namespace_uri,
287
- namespace_prefix: child.namespace_prefix)
287
+ namespace_prefix: child.namespace_prefix,
288
+ attributes: order_entry_attributes(child))
288
289
  end
289
290
  end.each(&:freeze).freeze
290
291
  end
291
292
 
293
+ # The attributes an element-order entry records, keyed by namespaced
294
+ # name — the same key find_attribute_value indexes. Same-name entries
295
+ # under different `when_attribute` discriminators need them to route
296
+ # back to the right rule on ordered serialization. nil for attribute-
297
+ # less children keeps the common entry allocation-free.
298
+ #
299
+ # @param child [XmlElement] An element child of this node
300
+ # @return [::Hash{String=>String}, nil]
301
+ def order_entry_attributes(child)
302
+ return if child.attributes.empty?
303
+
304
+ attrs = {}
305
+ child.attributes.each_value do |attr|
306
+ attrs[attr.namespaced_name] = attr.value
307
+ end
308
+ attrs.freeze
309
+ end
310
+
292
311
  def root
293
312
  self
294
313
  end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#88: `when_attribute` partitioning under `ordered` mappings.
6
+ # The ordered walk routes each element_order entry by its recorded
7
+ # attributes, so `<component type="guidance">` and `<component
8
+ # type="purpose">` interleave on to_xml exactly as the source document did.
9
+ class WhenOrderedComponent < 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 WhenOrderedRequirement < Lutaml::Model::Serializable
19
+ attribute :guidance, WhenOrderedComponent, collection: true
20
+ attribute :purpose, WhenOrderedComponent, collection: true
21
+ attribute :test_method, WhenOrderedComponent, collection: true
22
+
23
+ xml do
24
+ element "requirement"
25
+ ordered
26
+ map_element "component", to: :guidance,
27
+ when_attribute: { "type" => "guidance" }
28
+ map_element "component", to: :purpose,
29
+ when_attribute: { "type" => "purpose" }
30
+ map_element "component", to: :test_method,
31
+ when_attribute: { type: "test-method" }
32
+ end
33
+ end
34
+
35
+ RSpec.describe "when_attribute under ordered mappings" do
36
+ before do
37
+ stub_const("WhenOrdered::Component", WhenOrderedComponent)
38
+ stub_const("WhenOrdered::Requirement", WhenOrderedRequirement)
39
+ end
40
+
41
+ let(:xml) do
42
+ <<~XML
43
+ <requirement>
44
+ <component type="guidance"><text>g1</text></component>
45
+ <component type="purpose"><text>p1</text></component>
46
+ <component type="guidance"><text>g2</text></component>
47
+ <component type="test-method"><text>t1</text></component>
48
+ </requirement>
49
+ XML
50
+ end
51
+
52
+ def serialized_types(doc)
53
+ doc.scan(/<component type="([^"]+)">/).flatten
54
+ end
55
+
56
+ it "parses partitioned values" do
57
+ req = WhenOrderedRequirement.from_xml(xml)
58
+
59
+ expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
60
+ expect(req.purpose.map(&:text)).to eq(["p1"])
61
+ expect(req.test_method.map(&:text)).to eq(["t1"])
62
+ end
63
+
64
+ it "records the discriminator attributes on element_order entries" do
65
+ req = WhenOrderedRequirement.from_xml(xml)
66
+ entries = req.element_order.select { |e| e.type == "Element" }
67
+
68
+ expect(entries.map(&:attributes)).to eq([
69
+ { "type" => "guidance" },
70
+ { "type" => "purpose" },
71
+ { "type" => "guidance" },
72
+ { "type" => "test-method" },
73
+ ])
74
+ end
75
+
76
+ it "round-trips the original interleaving" do
77
+ round = WhenOrderedRequirement.from_xml(xml).to_xml
78
+
79
+ expect(serialized_types(round)).to eq(
80
+ %w[guidance purpose guidance test-method],
81
+ )
82
+ end
83
+
84
+ it "survives a re-parse of the ordered output" do
85
+ once = WhenOrderedRequirement.from_xml(xml)
86
+ twice = WhenOrderedRequirement.from_xml(once.to_xml)
87
+
88
+ expect(twice.guidance.map(&:text)).to eq(%w[g1 g2])
89
+ expect(twice.purpose.map(&:text)).to eq(["p1"])
90
+ expect(twice.test_method.map(&:text)).to eq(["t1"])
91
+ end
92
+
93
+ it "routes appended items to the right attribute on ordered output" do
94
+ req = WhenOrderedRequirement.from_xml(xml)
95
+ req.guidance << WhenOrderedComponent.new(text: "g3")
96
+
97
+ out = req.to_xml
98
+ # Reconciliation places new entries after the rule's last existing
99
+ # occurrence (the OrderReconciler contract), not at document end.
100
+ expect(serialized_types(out)).to eq(
101
+ %w[guidance purpose guidance guidance test-method],
102
+ )
103
+
104
+ reparsed = WhenOrderedRequirement.from_xml(out)
105
+ expect(reparsed.guidance.map(&:text)).to eq(%w[g1 g2 g3])
106
+ expect(reparsed.purpose.map(&:text)).to eq(["p1"])
107
+ end
108
+
109
+ it "drops occurrences with an unknown discriminator from ordered output" do
110
+ with_unknown = WhenOrderedRequirement.from_xml(<<~XML)
111
+ <requirement>
112
+ <component type="guidance"><text>g1</text></component>
113
+ <component type="unknown"><text>x</text></component>
114
+ <component type="purpose"><text>p1</text></component>
115
+ </requirement>
116
+ XML
117
+
118
+ expect(with_unknown.guidance.map(&:text)).to eq(["g1"])
119
+ expect(with_unknown.purpose.map(&:text)).to eq(["p1"])
120
+ expect(serialized_types(with_unknown.to_xml)).to eq(%w[guidance purpose])
121
+ end
122
+
123
+ it "falls back to the plain rule for legacy attribute-less entries" do
124
+ both = Class.new(Lutaml::Model::Serializable) do
125
+ attribute :plain, :string
126
+ attribute :special, :string
127
+
128
+ xml do
129
+ element "holder"
130
+ ordered
131
+ map_element "item", to: :plain
132
+ map_element "item", to: :special, when_attribute: { "kind" => "special" }
133
+ end
134
+ end
135
+ stub_const("WhenOrdered::Holder", both)
136
+
137
+ doc = both.from_xml(
138
+ "<holder><item>a</item><item kind=\"special\">b</item></holder>",
139
+ )
140
+
141
+ expect(doc.plain).to eq("a")
142
+ expect(doc.special).to eq("b")
143
+
144
+ out = doc.to_xml
145
+ expect(out.scan(%r{<item[^>]*>([^<]*)</item>})).to eq([["a"], ["b"]])
146
+ expect(out).to include('kind="special"')
147
+ end
148
+ end
@@ -130,4 +130,96 @@ RSpec.describe "when_attribute discriminator mappings" do
130
130
  end
131
131
  end.to raise_error(ArgumentError)
132
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
133
225
  end
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.43
4
+ version: 0.8.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -2006,6 +2006,7 @@ files:
2006
2006
  - spec/lutaml/xml/validate_xml_with_constructs_spec.rb
2007
2007
  - spec/lutaml/xml/validate_xml_with_spec.rb
2008
2008
  - spec/lutaml/xml/w3c_types_spec.rb
2009
+ - spec/lutaml/xml/when_attribute_ordered_spec.rb
2009
2010
  - spec/lutaml/xml/when_attribute_spec.rb
2010
2011
  - spec/lutaml/xml/xml_adapter_spec.rb
2011
2012
  - spec/lutaml/xml/xml_declaration_spec.rb