lutaml-model 0.8.24 → 0.8.28

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.
@@ -4,76 +4,337 @@ module Lutaml
4
4
  module Xml
5
5
  # Hydrates model instances from a Descriptor#walk PlanValue tree,
6
6
  # keyed by each value's producing row name (never position — rows
7
- # for missing elements are simply absent). Builds constructor
8
- # kwargs recursively and instantiates through .new, so typing,
9
- # defaults, and collection semantics all ride the constructor.
7
+ # for missing elements are simply absent). Native rows build
8
+ # constructor kwargs; deferred rows (custom methods, polymorphism,
9
+ # unions) capture their subtree verbatim and interpret it
10
+ # post-walk — the fragment parse runs the existing interpretive
11
+ # machinery on just that island. Delegate rules hydrate
12
+ # post-instance onto their target object. Collection rows route
13
+ # natively when a single one exists, else through callback rows
14
+ # (their values echo name and type_tag; native collection values
15
+ # echo neither — leptris-ruby#220).
10
16
  module PlanHydrator
11
17
  class << self
12
18
  # plan: the compiler's entry for model_class
13
19
  # value: the walk root PlanValue (element)
14
- def call(model_class, plan, value)
15
- kwargs = attributes_kwargs(plan, value)
16
- kwargs.merge!(children_kwargs(model_class, plan, value))
17
- model_class.new(**kwargs)
20
+ # node: the parsed source element (leptris); ordered/mixed
21
+ # plans rebuild element_order from it and ordered children
22
+ # hydrate natively against their own nodes
23
+ def call(model_class, plan, value, parent: nil, node: nil)
24
+ buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
25
+ attr_kwargs = attributes_kwargs(plan, value)
26
+ child_kwargs, children, delegates =
27
+ children_kwargs(model_class, plan, value, buckets)
28
+ plan[:collection_defaults].each do |name|
29
+ next if attr_kwargs.key?(name) || child_kwargs.key?(name)
30
+
31
+ child_kwargs[name] = []
32
+ end
33
+ instance = model_class.new(**attr_kwargs, **child_kwargs)
34
+ instance.lutaml_parent = parent if parent
35
+ instance.lutaml_root ||= parent&.lutaml_root || parent
36
+ instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
37
+ children.each do |child|
38
+ child.lutaml_parent = instance
39
+ child.lutaml_root ||= instance.lutaml_root || instance
40
+ end
41
+ interpret_deferred(model_class, plan, value, instance, node, buckets)
42
+ route_delegates(delegates, instance)
43
+ instance
18
44
  end
19
45
 
20
46
  private
21
47
 
48
+ def register
49
+ Lutaml::Model::Config.default_register
50
+ end
51
+
22
52
  def attributes_kwargs(plan, value)
23
53
  kwargs = {}
24
54
  plan[:attr_rows].each do |rule, attr|
25
55
  v = value.attribute(rule.name.to_s)
26
- kwargs[attr.name.to_sym] = v unless v.nil?
56
+ next if v.nil?
57
+
58
+ v = v.split(rule.delimiter) if rule.delimiter
59
+ if rule.as_list && rule.as_list[:import]
60
+ v = rule.as_list[:import].call(v)
61
+ end
62
+ kwargs[attr.name.to_sym] = v
27
63
  end
28
64
  kwargs
29
65
  end
30
66
 
31
- def children_kwargs(_model_class, plan, value)
32
- register = Lutaml::Model::Config.default_register
33
- grouped = group_children_by_name(value)
34
-
67
+ # Returns [kwargs, hydrated_child_instances, delegate_values]
68
+ # — child instances come back so the caller can decorate
69
+ # parent/root links once the parent exists; delegate values
70
+ # wait for the instance (their target object must exist).
71
+ def children_kwargs(_model_class, plan, value, buckets = nil)
72
+ grouped = group_children(value)
73
+ buckets = nil unless buckets && plan[:needs_nodes]
35
74
  kwargs = {}
36
- plan[:rows].each do |rule, attr, kind|
37
- values = grouped[kind == :collection ? :__collection : rule.name.to_s]
38
- next if values.nil? || values.empty?
39
-
40
- kwargs[attr.name.to_sym] =
41
- case kind
42
- when :scalar
43
- values.first.string_value
44
- when :collection
45
- # One collection-row value per element; its items are
46
- # the individual scalar matches.
47
- values.flat_map do |cv|
48
- Array.new(cv.count) { |i| cv.at(i).string_value }
75
+ children = []
76
+ delegates = []
77
+ spellings = Hash.new { |h, k| h[k] = [] }
78
+ plan[:rows].each do |rule, attr, kind, spelling, delegate|
79
+ case kind
80
+ when :scalar
81
+ # Raw passthrough: the model constructor is the single
82
+ # cast authority — pre-casting here doubled every cast.
83
+ # Class transforms still apply before assignment.
84
+ v = grouped.dig(rule.name.to_s, 0)&.string_value
85
+ unless v.nil?
86
+ v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
87
+ assign(kwargs, delegates, delegate, rule, attr, v)
88
+ end
89
+ when :raw, :custom_method, :polymorphic, :content_deferred
90
+ # interpreted post-instance (interpret_deferred)
91
+ when :content
92
+ assign(kwargs, delegates, delegate, rule, attr,
93
+ content_runs(value))
94
+ when :ordered_deferred
95
+ # With a source node the child hydrates natively: one
96
+ # walk against its own node + element_order from the
97
+ # node's children. Without one (direct PlanHydrator
98
+ # use), the subtree fragment-parses interpretively.
99
+ if buckets
100
+ child_type = attr.type(register)
101
+ child_plan = PlanCompiler.compile(child_type, register)
102
+ items = buckets.fetch(rule.name.to_s, []).map do |n|
103
+ call(child_type, child_plan,
104
+ child_plan[:descriptor].walk(n), node: n)
49
105
  end
50
- when :nested
51
- child_plan = PlanCompiler.compile(attr.type(register),
52
- register)
53
- items = values.map do |v|
54
- call(attr.type(register), child_plan, v)
106
+ unless items.empty?
107
+ children.concat(items)
108
+ assign(kwargs, delegates, delegate, rule, attr,
109
+ attr.collection? ? items : items.first)
55
110
  end
56
- attr.collection? ? items : items.first
57
111
  end
112
+ when :collection_cb
113
+ values = grouped[rule.name.to_s].to_a.map(&:string_value)
114
+ if rule.transform.is_a?(Class)
115
+ values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
116
+ end
117
+ unless values.empty?
118
+ assign(kwargs, delegates, delegate, rule, attr, values)
119
+ end
120
+ when :collection_native
121
+ values = native_collection(value, rule.name.to_s)
122
+ unless values.nil?
123
+ assign(kwargs, delegates, delegate, rule, attr, values)
124
+ end
125
+ when :spelling
126
+ spellings[[rule, attr]] << grouped[spelling.to_s].to_a
127
+ when :nested
128
+ child_type = attr.type(register)
129
+ child_plan = PlanCompiler.compile(child_type, register)
130
+ cursor = if child_plan[:needs_nodes] && buckets
131
+ buckets.fetch(rule.name.to_s, [])
132
+ end
133
+ items = grouped.fetch(rule.name.to_s, []).each_with_index
134
+ .map do |v, i|
135
+ call(child_type, child_plan, v,
136
+ node: cursor && cursor[i])
137
+ end
138
+ next if items.empty?
139
+
140
+ children.concat(items)
141
+ assign(kwargs, delegates, delegate, rule, attr,
142
+ attr.collection? ? items : items.first)
143
+ end
58
144
  end
59
- kwargs
145
+ unless spellings.empty?
146
+ spellings.each do |(rule, attr), groups|
147
+ # Interpretive order for shared-attribute groups is
148
+ # spelling-group order (first spelling's matches, then
149
+ # the next's), not interleaved document order.
150
+ values = groups.compact.flatten.map(&:string_value)
151
+ next if values.empty?
152
+
153
+ delegate = delegate_of(plan, rule)
154
+ assign(kwargs, delegates, delegate, rule, attr,
155
+ attr.collection? ? values : values.first)
156
+ end
157
+ end
158
+ [kwargs, children, delegates]
60
159
  end
61
160
 
62
- # Scalar and nested values echo their producing row's name;
63
- # collection values echo neither name nor type_tag, so with the
64
- # compiler's single-collection-row guard they are attributed by
65
- # kind.
66
- def group_children_by_name(value)
161
+ # Deferred islands: raw subtrees become wrapper elements via a
162
+ # fragment parse, then the interpretive machinery runs on just
163
+ # that island — custom method invocation with an
164
+ # element-shaped argument, or the polymorphic/union cast.
165
+ # With the source node available the island wraps the ORIGINAL
166
+ # node (no fragment parse — the reparse dominated deferred
167
+ # hydration). Ordered children only land here without a node.
168
+ def interpret_deferred(model_class, plan, value, instance, node = nil,
169
+ buckets = nil)
170
+ plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
171
+ case kind
172
+ when :content_deferred
173
+ runs = content_runs(value)
174
+ next if runs.empty?
175
+
176
+ # Non-collection content attrs hold the joined text
177
+ # (the interpretive path assigns element text, the runs
178
+ # concatenated).
179
+ instance.public_send(:"#{attr.name}=", runs.join)
180
+ when :raw
181
+ raws = raw_strings(value, rule)
182
+ next if raws.empty?
183
+
184
+ instance.public_send(:"#{attr.name}=",
185
+ attr.collection? ? raws : raws.first)
186
+ when :custom_method
187
+ elements = deferred_elements(value, rule, buckets)
188
+ next if elements.empty?
189
+
190
+ args = attr.collection? ? elements : elements.first
191
+ rule.deserialize(instance, args,
192
+ model_class.attributes(register),
193
+ model_class)
194
+ when :ordered_deferred
195
+ next if node # hydrated natively in children_kwargs
196
+
197
+ results = deferred_elements(value, rule, buckets).map do |element|
198
+ attr.cast(element, :xml, register,
199
+ lutaml_parent: instance,
200
+ lutaml_root: instance.lutaml_root || instance)
201
+ end
202
+ next if results.empty?
203
+
204
+ instance.public_send(:"#{attr.name}=",
205
+ attr.collection? ? results : results.first)
206
+ when :polymorphic
207
+ results = deferred_elements(value, rule, buckets).map do |element|
208
+ attr.cast(element, :xml, register,
209
+ polymorphic: rule.polymorphic,
210
+ lutaml_parent: instance,
211
+ lutaml_root: instance.lutaml_root || instance)
212
+ end
213
+ next if results.empty?
214
+
215
+ instance.public_send(:"#{attr.name}=",
216
+ attr.collection? ? results : results.first)
217
+ end
218
+ end
219
+ end
220
+
221
+ # Wrapper elements for deferred rows: the ORIGINAL source
222
+ # nodes bridged through moxml's wrapper cache (wrapper
223
+ # construction only, no parse), falling back to a fragment
224
+ # parse of the captured subtree when no node is available.
225
+ # Both lists are document-ordered for the row's name.
226
+ def deferred_elements(value, rule, buckets)
227
+ nodes = buckets && buckets[rule.name.to_s]
228
+ return nodes.map { |n| bridge_element(n) } if nodes
229
+
230
+ raw_strings(value, rule).map { |raw| fragment_element(raw) }
231
+ end
232
+
233
+ def bridge_element(node)
234
+ Lutaml::Xml::LeptrisElement.new(
235
+ Moxml::Node.wrap(node, bridge_context),
236
+ )
237
+ end
238
+
239
+ def bridge_context
240
+ @bridge_context ||= Moxml::Context.new(:leptris)
241
+ end
242
+
243
+ def route_delegates(delegates, instance)
244
+ delegates.each do |rule, delegate_attr, attr, v|
245
+ target = instance.public_send(rule.delegate)
246
+ unless target
247
+ # the interpretive path instantiates absent delegate
248
+ # targets; mirror it
249
+ target = delegate_attr.type(register).new
250
+ instance.public_send(:"#{rule.delegate}=", target)
251
+ end
252
+ target.public_send(:"#{attr.name}=", v)
253
+ end
254
+ end
255
+
256
+ def delegate_of(plan, rule)
257
+ entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
258
+ entry && entry[4]
259
+ end
260
+
261
+ # Delegate rules hold their values for post-instance routing
262
+ # (the target object must exist first); plain rules land in
263
+ # kwargs directly.
264
+ def assign(kwargs, delegates, delegate, rule, attr, value)
265
+ if delegate
266
+ delegates << [rule, delegate, attr, value]
267
+ else
268
+ kwargs[attr.name.to_sym] = value
269
+ end
270
+ end
271
+
272
+ def content_runs(value)
273
+ out = []
274
+ value.count.times do |i|
275
+ c = value.at(i)
276
+ next unless c.name.nil? && c.kind == :collection
277
+
278
+ out.concat(Array.new(c.count) { |j| c.at(j).string_value })
279
+ end
280
+ out
281
+ end
282
+
283
+ # Native collection rows echo their producing row's name
284
+ # (leptris 1.9.178).
285
+ def native_collection(value, name)
286
+ value.count.times do |i|
287
+ c = value.at(i)
288
+ next unless c.kind == :collection && c.name == name
289
+
290
+ return Array.new(c.count) { |j| c.at(j).string_value }
291
+ end
292
+ nil
293
+ end
294
+
295
+ def raw_strings(value, rule)
296
+ out = []
297
+ value.count.times do |idx|
298
+ child = value.at(idx)
299
+ if child.name == rule.name.to_s && child.kind == :raw
300
+ out << child.string_value
301
+ end
302
+ end
303
+ out
304
+ end
305
+
306
+ # Fragment deferral: parse the captured subtree back into a
307
+ # wrapper element. Ancestor namespace context is absent (the
308
+ # compiler only defers on namespace-free model chains).
309
+ def fragment_element(raw)
310
+ Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
311
+ end
312
+
313
+ def group_children(value)
67
314
  grouped = {}
68
315
  value.count.times do |i|
69
316
  child = value.at(i)
70
- key = child.name || (child.kind == :collection ? :__collection : nil)
71
- next if key.nil?
317
+ next if child.name.nil? # content runs, read separately
72
318
 
73
- (grouped[key] ||= []) << child
319
+ (grouped[child.name] ||= []) << child
74
320
  end
75
321
  grouped
76
322
  end
323
+
324
+ # Element children bucketed by local name, document order
325
+ # preserved — the node-side mirror of group_children for
326
+ # ordered-child hydration. Only built when the plan's subtree
327
+ # needs source nodes; namespace-qualified models never get
328
+ # here (the compiler keeps them interpretive).
329
+ def element_buckets(node)
330
+ buckets = {}
331
+ node.children.each do |child|
332
+ next unless child.is_a?(::Leptris::XML::Element)
333
+
334
+ (buckets[child.name] ||= []) << child
335
+ end
336
+ buckets
337
+ end
77
338
  end
78
339
  end
79
340
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ # Reconstructs element_order for ordered/mixed models from the
6
+ # leptris node surface in one native pass, mirroring the
7
+ # interpretive AdapterElement#order contract: text runs, CDATA,
8
+ # comments, PIs, and element entries (local name + namespace
9
+ # uri/prefix) in document order, frozen like the interpretive
10
+ # product.
11
+ module PlanOrder
12
+ TEXT_MARKER = "text"
13
+ CDATA_MARKER = "#cdata-section"
14
+ COMMENT_MARKER = "comment"
15
+
16
+ class << self
17
+ def build(node)
18
+ node.children.filter_map { |child| entry_for(child) }
19
+ .each(&:freeze).freeze
20
+ end
21
+
22
+ private
23
+
24
+ def entry_for(child)
25
+ case child
26
+ when ::Leptris::XML::CDATA
27
+ Element.new("Text", CDATA_MARKER,
28
+ text_content: child.content, node_type: :cdata)
29
+ when ::Leptris::XML::Text
30
+ Element.new("Text", TEXT_MARKER,
31
+ text_content: child.text, node_type: :text)
32
+ when ::Leptris::XML::Comment
33
+ Element.new("Comment", COMMENT_MARKER,
34
+ text_content: child.content, node_type: :comment)
35
+ when ::Leptris::XML::ProcessingInstruction
36
+ Element.new("ProcessingInstruction", child.name,
37
+ text_content: child.content.to_s.sub(/\A\s+/, ""),
38
+ node_type: :processing_instruction)
39
+ when ::Leptris::XML::Element
40
+ ns = child.namespace
41
+ Element.new("Element", child.name,
42
+ node_type: :element,
43
+ namespace_uri: ns&.values&.first,
44
+ namespace_prefix: ns&.keys&.first)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ # Serialize-side mirror of the plan fast path: builds the output
6
+ # tree directly through leptris's fused create_child chain — no
7
+ # XmlElement tree, no DeclarationPlanner pass, no builder context
8
+ # stack, no moxml wrapper minting. Serialized shapes must be the
9
+ # plain ones (attributes, scalars, collections, nested models,
10
+ # raw fragments, content runs); custom-method, polymorphic,
11
+ # union, and multi-spelling rows keep the interpretive serializer
12
+ # (their output flows through hash-shaped custom APIs).
13
+ #
14
+ # Ordered/mixed models serialize from element_order when the
15
+ # instance carries one (parsed models): text runs, comments, and
16
+ # element entries in document order, mirroring the interpretive
17
+ # order applier — including its content-attr-first text reads
18
+ # (mutations to the model show up) and CDATA flattening. Models
19
+ # without element_order fall back interpretively (return nil).
20
+ module PlanSerializer
21
+ SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
22
+ nested ordered_deferred raw content
23
+ content_deferred].freeze
24
+ CONTENT_KINDS = %i[content content_deferred].freeze
25
+
26
+ class << self
27
+ # plan: the compiler's entry for instance's class
28
+ def call(instance, plan)
29
+ return nil if plan[:ordered] && instance.element_order.nil?
30
+
31
+ doc = ::Leptris::XML::Document.create
32
+ root = doc.create_element(plan[:tree][:name])
33
+ doc.root = root
34
+ if plan[:ordered]
35
+ build_ordered(root, instance, plan, doc)
36
+ else
37
+ build(root, instance, plan, doc)
38
+ end
39
+ doc.to_xml(indent: 2, no_decl: true)
40
+ end
41
+
42
+ # Whether the compiled plan is serialize-shaped. Ordered/mixed
43
+ # models serialize through build_ordered from element_order;
44
+ # plain models through plan rows.
45
+ def serializable?(plan)
46
+ plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
47
+ SERIALIZABLE_KINDS.include?(kind)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def register
54
+ Lutaml::Model::Config.default_register
55
+ end
56
+
57
+ def build(element, instance, plan, doc)
58
+ write_attributes(element, instance, plan)
59
+ plan[:rows].each do |rule, attr, kind, spelling, delegate|
60
+ value = value_of(instance, attr, delegate)
61
+ next unless rule.render?(value, instance)
62
+
63
+ case kind
64
+ when :scalar
65
+ add_leaf(element, row_name(rule, spelling),
66
+ attr.serialize(value, :xml, register), doc)
67
+ when :collection_native, :collection_cb
68
+ Array(value).each do |item|
69
+ add_leaf(element, rule.name.to_s,
70
+ attr.serialize(item, :xml, register), doc)
71
+ end
72
+ when :nested, :ordered_deferred
73
+ Array(value).each do |item|
74
+ child_plan = PlanCompiler.compile(item.class, register)
75
+ child = element.create_child(child_plan[:tree][:name])
76
+ if child_plan[:ordered]
77
+ return nil unless build_ordered(child, item, child_plan, doc)
78
+
79
+ else
80
+ build(child, item, child_plan, doc)
81
+ end
82
+ end
83
+ when :raw
84
+ Array(value).each { |raw| element.add_child(raw.to_s) }
85
+ when :content, :content_deferred
86
+ Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
87
+ end
88
+ end
89
+ end
90
+
91
+ # Element_order-driven emission (the interpretive order
92
+ # applier's contract): text runs and comments verbatim, each
93
+ # element entry consuming the next item of its collection
94
+ # (document order). Text reads prefer the content attribute
95
+ # when it lines up with the text-node count, so mutations to
96
+ # the model are reflected; element_order text is the fallback.
97
+ # PIs drop (interpretive parity). Returns nil when a nested
98
+ # ordered child lacks element_order (caller falls back).
99
+ def build_ordered(element, instance, plan, doc)
100
+ write_attributes(element, instance, plan)
101
+ rows_by_name = {}
102
+ plan[:rows].each do |rule, attr, kind, spelling, delegate|
103
+ next unless rule.name
104
+
105
+ rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
106
+ delegate]
107
+ end
108
+ content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
109
+ CONTENT_KINDS.include?(kind) && rule.name.nil?
110
+ end
111
+ order = instance.element_order
112
+ element_indices = ::Hash.new(0)
113
+ content_attr = content_entry && content_entry[1]
114
+ content_value = content_attr &&
115
+ value_of(instance, content_attr, content_entry[4])
116
+ text_node_count = order.count { |o| o.type == "Text" }
117
+ use_content_index = content_value.is_a?(Array) &&
118
+ content_value.length == text_node_count
119
+ content_cdata = content_entry && content_entry[0].cdata
120
+ text_node_index = 0
121
+
122
+ order.each do |object|
123
+ case object.type
124
+ when "Text"
125
+ text = if content_attr && !content_value.nil?
126
+ if use_content_index
127
+ content_value[text_node_index].to_s
128
+ elsif !content_value.is_a?(Array) && text_node_count <= 1
129
+ content_value.to_s
130
+ else
131
+ object.text_content || object.name
132
+ end
133
+ else
134
+ object.text_content || object.name
135
+ end
136
+ text_node_index += 1
137
+ next if text.nil?
138
+ # Mixed content keeps whitespace; ordered-only skips it
139
+ next if content_attr.nil? && text.strip.empty?
140
+
141
+ element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
142
+ when "Comment"
143
+ element.add_child(doc.create_comment(object.text_content))
144
+ when "Element"
145
+ entry = rows_by_name[object.name]
146
+ next unless entry
147
+
148
+ _, attr, kind, _spelling, delegate = entry
149
+ value = value_of(instance, attr, delegate)
150
+ case kind
151
+ when :collection_native, :collection_cb
152
+ index = element_indices[object.name]
153
+ items = Array(value)
154
+ next unless index < items.length
155
+
156
+ element_indices[object.name] += 1
157
+ add_leaf(element, object.name,
158
+ attr.serialize(items[index], :xml, register), doc)
159
+ when :nested, :ordered_deferred
160
+ item = if attr.collection?
161
+ index = element_indices[object.name]
162
+ element_indices[object.name] += 1
163
+ Array(value)[index]
164
+ else
165
+ value
166
+ end
167
+ next unless item
168
+
169
+ child_plan = PlanCompiler.compile(item.class, register)
170
+ child = element.create_child(child_plan[:tree][:name])
171
+ if child_plan[:ordered]
172
+ return nil unless build_ordered(child, item, child_plan, doc)
173
+ else
174
+ build(child, item, child_plan, doc)
175
+ end
176
+ when :raw
177
+ element.add_child(value.to_s) unless value.nil?
178
+ when :scalar
179
+ add_leaf(element, object.name,
180
+ attr.serialize(value, :xml, register), doc)
181
+ end
182
+ end
183
+ end
184
+ true
185
+ end
186
+
187
+ # Attributes in document order when the instance recorded it
188
+ # (parsed models), else plan row order.
189
+ def write_attributes(element, instance, plan)
190
+ recorded = instance.attribute_order
191
+ if recorded && !recorded.empty?
192
+ by_name = {}
193
+ plan[:attr_rows].each do |rule, attr, _sp, delegate|
194
+ by_name[rule.name.to_s] = [rule, attr, delegate]
195
+ end
196
+ names = recorded |
197
+ plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
198
+ names.each do |name|
199
+ write_attribute(element, instance, name, by_name[name])
200
+ end
201
+ else
202
+ plan[:attr_rows].each do |rule, attr, _sp, delegate|
203
+ write_attribute(element, instance, rule.name.to_s,
204
+ [rule, attr, delegate])
205
+ end
206
+ end
207
+ end
208
+
209
+ def write_attribute(element, instance, name, entry)
210
+ return unless entry
211
+
212
+ _rule, attr, delegate = entry
213
+ value = value_of(instance, attr, delegate)
214
+ return if value.nil?
215
+
216
+ element.set_attribute(name,
217
+ attr.serialize(value, :xml, register).to_s)
218
+ end
219
+
220
+ def add_leaf(element, name, value, doc)
221
+ child = element.create_child(name)
222
+ child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
223
+ child
224
+ end
225
+
226
+ def value_of(instance, attr, delegate)
227
+ if delegate
228
+ target = instance.public_send(delegate)
229
+ target&.public_send(attr.name)
230
+ else
231
+ instance.public_send(attr.name)
232
+ end
233
+ end
234
+
235
+ def row_name(rule, spelling)
236
+ (spelling || rule.name).to_s
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end