lutaml-model 0.8.5 → 0.8.7

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +4 -1
  3. data/.rubocop_todo.yml +97 -22
  4. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
  5. data/lib/lutaml/model/version.rb +1 -1
  6. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
  7. data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
  8. data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
  9. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
  10. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
  11. data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
  12. data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
  13. data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
  14. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
  15. data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
  16. data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
  17. data/lib/lutaml/xml/adapter.rb +0 -1
  18. data/lib/lutaml/xml/adapter_element.rb +7 -1
  19. data/lib/lutaml/xml/builder/base.rb +0 -1
  20. data/lib/lutaml/xml/data_model.rb +9 -1
  21. data/lib/lutaml/xml/document.rb +3 -1
  22. data/lib/lutaml/xml/element.rb +13 -10
  23. data/lib/lutaml/xml/schema/xsd/schema.rb +8 -5
  24. data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
  25. data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
  26. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
  27. data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
  28. data/lib/lutaml/xml/xml_element.rb +24 -20
  29. data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
  30. data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
  31. data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
  32. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
  33. data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
  34. data/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb +20 -0
  35. data/spec/lutaml/xml/schema/xsd/spec_helper.rb +1 -0
  36. data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
  37. metadata +9 -3
  38. data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
@@ -0,0 +1,929 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Adapter
6
+ # Builds XML elements from model instances using namespace declaration plans.
7
+ #
8
+ # Handles both ordered and unordered child serialization, nested model
9
+ # elements, simple values, and namespace resolution. This module is the
10
+ # core of model-to-XML conversion when a DeclarationPlan drives the output.
11
+ module PlanBasedBuilder
12
+ # Build element using prepared namespace declaration plan
13
+ #
14
+ # @param xml [Builder] the XML builder
15
+ # @param element [Object] the model instance
16
+ # @param plan [DeclarationPlan] the declaration plan from DeclarationPlanner
17
+ # @param options [Hash] serialization options
18
+ def build_element_with_plan(xml, element, plan, options = {})
19
+ plan ||= DeclarationPlan.empty
20
+ mapper_class = options[:mapper_class] || element.class
21
+
22
+ unless mapper_class.is_a?(Class) &&
23
+ mapper_class.include?(Lutaml::Model::Serialize)
24
+ tag_name = options[:tag_name] || "element"
25
+ xml.create_and_add_element(tag_name) do |inner_xml|
26
+ inner_xml.text(text_content_for_xml(element))
27
+ end
28
+ return xml
29
+ end
30
+
31
+ xml_mapping = mapper_class.mappings_for(:xml)
32
+ return xml unless xml_mapping
33
+
34
+ # TYPE-ONLY MODELS: No element wrapper, serialize children directly
35
+ # BUT if we have a tag_name in options, that means parent wants a wrapper
36
+ if xml_mapping.no_element?
37
+ build_type_only_element(xml, element, xml_mapping, plan, options,
38
+ mapper_class)
39
+ return xml
40
+ end
41
+
42
+ # Use xmlns declarations from plan
43
+ attributes = {}
44
+
45
+ # Apply namespace declarations from plan using extracted module
46
+ attributes.merge!(NamespaceDeclarationBuilder.build_xmlns_attributes(plan))
47
+
48
+ # Collect attribute custom methods to call after element creation
49
+ attribute_custom_methods = []
50
+
51
+ # Add regular attributes (non-xmlns)
52
+ xml_mapping.attributes.each do |attribute_rule|
53
+ next if options[:except]&.include?(attribute_rule.to)
54
+
55
+ # Collect custom methods for later execution (after element is created)
56
+ if attribute_rule.custom_methods[:to]
57
+ attribute_custom_methods << attribute_rule
58
+ next
59
+ end
60
+
61
+ mapping_rule_name = if attribute_rule.multiple_mappings?
62
+ attribute_rule.name.first
63
+ else
64
+ attribute_rule.name
65
+ end
66
+
67
+ attr = attribute_definition_for(element, attribute_rule,
68
+ mapper_class: mapper_class)
69
+ value = attribute_rule.to_value_for(element)
70
+
71
+ # Handle as_list and delimiter BEFORE serialization for array values
72
+ # These features convert arrays to delimited strings before serialization
73
+ if value.is_a?(Array)
74
+ if attribute_rule.as_list && attribute_rule.as_list[:export]
75
+ value = attribute_rule.as_list[:export].call(value)
76
+ elsif attribute_rule.delimiter
77
+ value = value.join(attribute_rule.delimiter)
78
+ end
79
+ end
80
+
81
+ value = attr.serialize(value, :xml, register) if attr
82
+ value = ExportTransformer.call(value, attribute_rule, attr,
83
+ format: :xml)
84
+
85
+ if render_element?(attribute_rule, element, value)
86
+ # Resolve attribute namespace using extracted module
87
+ ns_info = AttributeNamespaceResolver.resolve(
88
+ rule: attribute_rule,
89
+ attribute: attr,
90
+ plan: plan,
91
+ mapper_class: mapper_class,
92
+ register: register,
93
+ )
94
+
95
+ # Build qualified attribute name based on W3C semantics
96
+ attr_name = AttributeNamespaceResolver.build_qualified_name(
97
+ ns_info,
98
+ mapping_rule_name,
99
+ attribute_rule,
100
+ )
101
+ attributes[attr_name] = value ? value.to_s : value
102
+
103
+ # Add local xmlns declaration if needed
104
+ if ns_info[:needs_local_declaration]
105
+ attributes[ns_info[:local_xmlns_attr]] =
106
+ ns_info[:local_xmlns_uri]
107
+ end
108
+ end
109
+ end
110
+
111
+ # Add schema_location attribute from ElementNode if present
112
+ attributes.merge!(plan.root_node.schema_location_attr) if plan&.root_node&.schema_location_attr
113
+
114
+ # Determine prefix from plan using extracted module
115
+ prefix_info = ElementPrefixResolver.resolve(mapping: xml_mapping,
116
+ plan: plan)
117
+ prefix = prefix_info[:prefix]
118
+ ns_decl = if xml_mapping.namespace_class
119
+ plan.namespace_for_class(xml_mapping.namespace_class)
120
+ end
121
+
122
+ # Check if element's own namespace needs local declaration (out of scope)
123
+ if ns_decl&.local_on_use?
124
+ xmlns_attr = prefix ? "xmlns:#{prefix}" : "xmlns"
125
+ attributes[xmlns_attr] = ns_decl.uri
126
+ end
127
+
128
+ # W3C COMPLIANCE: Detect if element needs xmlns="" using extracted module
129
+ if BlankNamespaceHandler.needs_xmlns_blank?(mapping: xml_mapping,
130
+ options: options)
131
+ attributes["xmlns"] = ""
132
+ end
133
+
134
+ # Native type inheritance fix: handle local_on_use xmlns="" even if parents uses default format
135
+ xmlns_prefix = nil
136
+ xmlns_ns = nil
137
+ if xml_mapping&.namespace_class && plan
138
+ xmlns_ns = plan.namespace_for_class(xml_mapping.namespace_class)
139
+ xmlns_prefix = xmlns_ns&.prefix
140
+ end
141
+ if xmlns_ns&.local_on_use? && !xml_mapping.namespace_uri
142
+ attributes["xmlns:#{xmlns_prefix}"] =
143
+ xmlns_ns&.uri || xml_mapping.namespace_uri
144
+ end
145
+
146
+ tag_name = options[:tag_name] || xml_mapping.root_element
147
+ return if options[:except]&.include?(tag_name)
148
+
149
+ # Track if THIS element uses default namespace format
150
+ # Children will need this info to know if they should add xmlns=""
151
+ this_element_uses_default_ns = xml_mapping.namespace_class &&
152
+ plan.namespace_for_class(xml_mapping.namespace_class)&.default_format?
153
+
154
+ # Get element_form_default from this element's namespace for children
155
+ parent_element_form_default = xml_mapping.namespace_class&.element_form_default
156
+
157
+ xml.create_and_add_element(tag_name, attributes: attributes.compact,
158
+ prefix: prefix) do |inner_xml|
159
+ # Call attribute custom methods now that element is created
160
+ attribute_custom_methods.each do |attribute_rule|
161
+ mapper_class.new.send(attribute_rule.custom_methods[:to],
162
+ element, inner_xml.parent, inner_xml)
163
+ end
164
+
165
+ if ordered?(element, options.merge(mapper_class: mapper_class))
166
+ build_ordered_element_with_plan(inner_xml, element, plan,
167
+ options.merge(
168
+ mapper_class: mapper_class,
169
+ parent_prefix: prefix,
170
+ parent_uses_default_ns: this_element_uses_default_ns,
171
+ parent_element_form_default: parent_element_form_default,
172
+ parent_ns_decl: ns_decl,
173
+ ))
174
+ else
175
+ build_unordered_children_with_plan(inner_xml, element, plan,
176
+ options.merge(
177
+ mapper_class: mapper_class,
178
+ parent_prefix: prefix,
179
+ parent_uses_default_ns: this_element_uses_default_ns,
180
+ parent_element_form_default: parent_element_form_default,
181
+ parent_ns_decl: ns_decl,
182
+ ))
183
+ end
184
+ end
185
+ end
186
+
187
+ # Build XML from XmlDataModel::XmlElement structure
188
+ #
189
+ # @param xml [Builder] XML builder
190
+ # @param element [XmlDataModel::XmlElement] element to build
191
+ # @param parent_uses_default_ns [Boolean] parent uses default namespace format
192
+ # @param parent_element_form_default [Symbol] parent's element_form_default
193
+ # @param parent_namespace_class [Class] parent's namespace class
194
+ # @param plan [DeclarationPlan, nil] optional declaration plan for xmlns=""
195
+ # @param xml_mapping [Xml::Mapping] optional mapping for namespace resolution
196
+ def build_xml_element(xml, element, parent_uses_default_ns: false,
197
+ parent_element_form_default: nil, parent_namespace_class: nil, plan: nil, xml_mapping: nil)
198
+ # Prepare attributes hash
199
+ attributes = {}
200
+
201
+ # Get element's namespace class
202
+ element_ns_class = element.namespace_class
203
+ attribute_form_default = element_ns_class&.attribute_form_default || :unqualified
204
+ element_prefix = element_ns_class&.prefix_default
205
+
206
+ # Get element_form_default for children
207
+ this_element_form_default = element_ns_class&.element_form_default || :unqualified
208
+
209
+ # Add regular attributes
210
+ element.attributes.each do |attr|
211
+ # Determine attribute name with namespace consideration
212
+ attr_name = if attr.namespace_class
213
+ # Check if attribute is in SAME namespace as element
214
+ if attr.namespace_class == element_ns_class && attribute_form_default == :unqualified
215
+ # Same namespace + unqualified -> NO prefix (W3C rule)
216
+ attr.name
217
+ else
218
+ # Different namespace OR qualified -> use prefix
219
+ attr_prefix = attr.namespace_class.prefix_default
220
+ attr_prefix ? "#{attr_prefix}:#{attr.name}" : attr.name
221
+ end
222
+ elsif attribute_form_default == :qualified && element_prefix
223
+ # Attribute inherits element's namespace when qualified
224
+ "#{element_prefix}:#{attr.name}"
225
+ else
226
+ # Unqualified attribute
227
+ attr.name
228
+ end
229
+ # Ensure attribute value is a string
230
+ attributes[attr_name] = attr.value.to_s
231
+ end
232
+
233
+ # Determine element name with namespace prefix
234
+ tag_name = element.name
235
+
236
+ # Priority 2.5: Child namespace different from parent's default namespace
237
+ # MUST use prefix format to distinguish from parent
238
+ child_needs_prefix = if element_ns_class && parent_namespace_class &&
239
+ element_ns_class != parent_namespace_class && parent_uses_default_ns
240
+ element_prefix # Use child's prefix
241
+ end
242
+
243
+ # FIX: Read prefix from plan if available, otherwise use fallback logic
244
+ prefix = if child_needs_prefix
245
+ # Priority 2.5 takes precedence
246
+ child_needs_prefix
247
+ elsif plan && element_ns_class
248
+ # Read format decision from DeclarationPlan
249
+ ns_info = ElementPrefixResolver.resolve(
250
+ mapping: xml_mapping,
251
+ plan: plan,
252
+ )
253
+ ns_info[:prefix]
254
+ elsif element_ns_class && element_prefix
255
+ # Fallback: Element has explicit prefix_default - use prefix format
256
+ element_prefix
257
+ end
258
+
259
+ # Track if THIS element uses default namespace format for children
260
+ this_element_uses_default_ns = false
261
+
262
+ # Add namespace declaration if element has namespace
263
+ if element.namespace_class
264
+ ns_uri = element.namespace_class.uri
265
+
266
+ # Check if namespace is already declared by parent (hoisting optimization)
267
+ # This works for BOTH default and prefix format parents
268
+ ns_already_declared = parent_namespace_class && parent_namespace_class.uri == ns_uri
269
+
270
+ if prefix && !ns_already_declared
271
+ attributes["xmlns:#{prefix}"] = ns_uri
272
+ # W3C Compliance: xmlns="" only needed for blank namespace children
273
+ # Prefixed children are already in different namespace from parent's default
274
+ elsif !prefix && !ns_already_declared
275
+ attributes["xmlns"] = ns_uri
276
+ this_element_uses_default_ns = true
277
+ end
278
+ elsif plan && DeclarationPlanQuery.element_needs_xmlns_blank?(plan,
279
+ element)
280
+ # W3C Compliance: Element has no namespace (blank namespace)
281
+ attributes["xmlns"] = ""
282
+ elsif !plan
283
+ # Fallback logic when no plan is available
284
+ if parent_uses_default_ns
285
+ if parent_element_form_default == :qualified
286
+ # Child should INHERIT parent's namespace - no xmlns="" needed
287
+ else
288
+ # Parent's element_form_default is :unqualified - child in blank namespace
289
+ attributes["xmlns"] = ""
290
+ end
291
+ end
292
+ end
293
+
294
+ # Check if element was created from nil value with render_nil option
295
+ if element.respond_to?(:xsi_nil) && element.xsi_nil
296
+ attributes["xsi:nil"] = true
297
+ end
298
+
299
+ # Create element
300
+ xml.create_and_add_element(tag_name, attributes: attributes,
301
+ prefix: prefix) do |inner_xml|
302
+ # Handle raw content (map_all directive)
303
+ has_raw_content = false
304
+ if element.respond_to?(:raw_content)
305
+ raw_content = element.raw_content
306
+ 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
309
+ end
310
+ end
311
+
312
+ # Skip text content and children if we have raw content
313
+ unless has_raw_content
314
+ # Add text content if present
315
+ if element.text_content
316
+ if element.cdata
317
+ inner_xml.cdata(element.text_content.to_s)
318
+ else
319
+ inner_xml.text(text_content_for_xml(element.text_content))
320
+ end
321
+ end
322
+
323
+ # Recursively build child elements, passing namespace context and plan
324
+ element.children.each do |child|
325
+ case child
326
+ when Lutaml::Xml::DataModel::XmlElement
327
+ build_xml_element(inner_xml, child,
328
+ parent_uses_default_ns: this_element_uses_default_ns,
329
+ parent_element_form_default: this_element_form_default,
330
+ parent_namespace_class: element_ns_class,
331
+ plan: plan,
332
+ xml_mapping: xml_mapping)
333
+ when Lutaml::Xml::DataModel::XmlComment
334
+ inner_xml.add_comment(child.content)
335
+ when String
336
+ if element.cdata
337
+ inner_xml.cdata(child.to_s)
338
+ else
339
+ inner_xml.text(text_content_for_xml(child))
340
+ end
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ # Build unordered child elements using prepared namespace declaration plan
348
+ #
349
+ # @param xml [Builder] the XML builder
350
+ # @param element [Object] the model instance
351
+ # @param plan [DeclarationPlan, Hash] the declaration plan
352
+ # @param options [Hash] serialization options
353
+ def build_unordered_children_with_plan(xml, element, plan, options)
354
+ mapper_class = options[:mapper_class] || element.class
355
+ xml_mapping = mapper_class.mappings_for(:xml)
356
+
357
+ # Process child elements with their plans (INCLUDING raw_mapping for map all)
358
+ mappings = xml_mapping.elements + [xml_mapping.raw_mapping].compact
359
+ mappings.each do |element_rule|
360
+ next if options[:except]&.include?(element_rule.to)
361
+
362
+ # Handle custom methods
363
+ if element_rule.custom_methods[:to]
364
+ mapper_class.new.send(element_rule.custom_methods[:to], element,
365
+ xml.parent, xml)
366
+ next
367
+ end
368
+
369
+ attribute_def = attribute_definition_for(element, element_rule,
370
+ mapper_class: mapper_class)
371
+
372
+ # For delegated attributes, attribute_def might be nil
373
+ next unless attribute_def || element_rule.delegate
374
+
375
+ value = attribute_value_for(element, element_rule)
376
+ next unless element_rule.render?(value, element)
377
+
378
+ # Get child's plan if available
379
+ child_plan = child_plan_for(plan, element_rule.to)
380
+
381
+ # Check if value is a Collection instance
382
+ is_collection_instance = value.is_a?(Lutaml::Model::Collection)
383
+
384
+ if value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
385
+ handle_nested_elements_with_plan(
386
+ xml,
387
+ value,
388
+ element_rule,
389
+ attribute_def,
390
+ child_plan,
391
+ options,
392
+ parent_plan: plan,
393
+ )
394
+ elsif element_rule.delegate && attribute_def.nil?
395
+ # Handle non-model values (strings, etc.) for delegated attributes
396
+ add_simple_value(xml, element_rule, value, nil, plan: plan,
397
+ mapping: xml_mapping, options: options)
398
+ else
399
+ add_simple_value(xml, element_rule, value, attribute_def,
400
+ plan: plan, mapping: xml_mapping, options: options)
401
+ end
402
+ end
403
+
404
+ # Process content mapping
405
+ process_content_mapping(element, xml_mapping.content_mapping,
406
+ xml, mapper_class)
407
+ end
408
+
409
+ # Build ordered child elements using prepared namespace declaration plan
410
+ #
411
+ # @param xml [Builder] the XML builder
412
+ # @param element [Object] the model instance
413
+ # @param plan [DeclarationPlan, Hash] the declaration plan
414
+ # @param options [Hash] serialization options
415
+ def build_ordered_element_with_plan(xml, element, plan, options)
416
+ mapper_class = options[:mapper_class] || element.class
417
+ xml_mapping = mapper_class.mappings_for(:xml)
418
+
419
+ index_hash = {}
420
+ content = []
421
+
422
+ element.element_order.each do |object|
423
+ object_key = "#{object.name}-#{object.type}"
424
+ index_hash[object_key] ||= -1
425
+ curr_index = index_hash[object_key] += 1
426
+
427
+ element_rule = xml_mapping.find_by_name(object.name,
428
+ type: object.type,
429
+ node_type: object.node_type,
430
+ namespace_uri: object.namespace_uri)
431
+ next if element_rule.nil? || options[:except]&.include?(element_rule.to)
432
+
433
+ # Handle custom methods
434
+ if element_rule.custom_methods[:to]
435
+ mapper_class.new.send(element_rule.custom_methods[:to], element,
436
+ xml.parent, xml)
437
+ next
438
+ end
439
+
440
+ # Get attribute definition and value (handle delegation)
441
+ attribute_def, value = fetch_attribute_and_value(element,
442
+ element_rule, mapper_class)
443
+
444
+ next if element_rule == xml_mapping.content_mapping && element_rule.cdata && object.text?
445
+
446
+ if element_rule == xml_mapping.content_mapping
447
+ process_ordered_content(element, xml_mapping, xml, curr_index,
448
+ content)
449
+ elsif !value.nil? || element_rule.render_nil?
450
+ process_ordered_element(xml, element, element_rule, attribute_def,
451
+ value, curr_index, plan, xml_mapping, options)
452
+ end
453
+ end
454
+
455
+ add_ordered_content(xml, content) unless content.empty?
456
+ end
457
+
458
+ private
459
+
460
+ def build_type_only_element(xml, element, xml_mapping, plan, options,
461
+ mapper_class)
462
+ if options[:tag_name]
463
+ xml.create_and_add_element(options[:tag_name]) do |inner_xml|
464
+ # Serialize type-only model's children inside parent's wrapper
465
+ xml_mapping.elements.each do |element_rule|
466
+ next if options[:except]&.include?(element_rule.to)
467
+
468
+ attribute_def = mapper_class.attributes[element_rule.to]
469
+ next unless attribute_def
470
+
471
+ value = element.send(element_rule.to)
472
+ next unless element_rule.render?(value, element)
473
+
474
+ # For type-only models, children plans may not be available
475
+ # Serialize children directly
476
+ if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
477
+ # Nested model - recursively build it
478
+ child_plan = plan.child_plan(element_rule.to) || DeclarationPlan.empty
479
+ build_element_with_plan(
480
+ inner_xml,
481
+ value,
482
+ child_plan,
483
+ { mapper_class: attribute_def.type(register),
484
+ tag_name: element_rule.name },
485
+ )
486
+ else
487
+ # Simple value - create element directly
488
+ inner_xml.create_and_add_element(element_rule.name) do
489
+ add_value(inner_xml, value, attribute_def,
490
+ cdata: element_rule.cdata)
491
+ end
492
+ end
493
+ end
494
+ end
495
+ else
496
+ # No wrapper at all - serialize children directly (for root-level type-only)
497
+ xml_mapping.elements.each do |element_rule|
498
+ next if options[:except]&.include?(element_rule.to)
499
+
500
+ attribute_def = mapper_class.attributes[element_rule.to]
501
+ next unless attribute_def
502
+
503
+ value = element.send(element_rule.to)
504
+ next unless element_rule.render?(value, element)
505
+
506
+ child_plan = plan.child_plan(element_rule.to)
507
+
508
+ if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
509
+ handle_nested_elements_with_plan(
510
+ xml,
511
+ value,
512
+ element_rule,
513
+ attribute_def,
514
+ child_plan,
515
+ options,
516
+ )
517
+ else
518
+ add_simple_value(xml, element_rule, value, attribute_def,
519
+ plan: plan, mapping: xml_mapping, options: options)
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ def handle_nested_elements_with_plan(xml, value, rule, attribute, plan,
526
+ options, parent_plan: nil)
527
+ element_options = options.merge(
528
+ rule: rule,
529
+ attribute: attribute,
530
+ tag_name: rule.name,
531
+ mapper_class: attribute.type(register), # Override with child's type
532
+ )
533
+
534
+ # Handle Collection instances
535
+ if value.is_a?(Lutaml::Model::Collection)
536
+ build_collection_elements(xml, value, attribute, rule,
537
+ element_options, parent_plan, options)
538
+ return
539
+ end
540
+
541
+ case value
542
+ when Array
543
+ build_array_elements(xml, value, attribute, rule, element_options,
544
+ plan, parent_plan, options)
545
+ else
546
+ build_element_with_plan(xml, value, plan, element_options)
547
+ end
548
+ end
549
+
550
+ def build_collection_elements(xml, value, attribute, rule,
551
+ element_options, parent_plan, options)
552
+ items = value.collection
553
+ attr_type = attribute.type(register)
554
+
555
+ if attr_type <= Lutaml::Model::Type::Value
556
+ # Simple types - use add_simple_value for each item
557
+ items.each do |val|
558
+ xml_mapping = options[:mapper_class]&.mappings_for(:xml)
559
+ add_simple_value(xml, rule, val, attribute, plan: parent_plan,
560
+ mapping: xml_mapping, options: options)
561
+ end
562
+ else
563
+ # Model types - build elements with plans
564
+ items.each do |val|
565
+ item_plan = plan_for_collection_item(val, attribute, parent_plan,
566
+ options)
567
+ item_mapper_class = if polymorphic_value?(attribute, val)
568
+ val.class
569
+ else
570
+ attribute.type(register)
571
+ end
572
+ item_options = element_options.merge(mapper_class: item_mapper_class)
573
+ build_element_with_plan(xml, val, item_plan, item_options)
574
+ end
575
+ end
576
+ end
577
+
578
+ def build_array_elements(xml, value, attribute, _rule, element_options,
579
+ _plan, parent_plan, options)
580
+ value.each do |val|
581
+ item_mapper_class = if polymorphic_value?(attribute, val)
582
+ val.class
583
+ else
584
+ attribute.type(register)
585
+ end
586
+
587
+ item_plan = plan_for_collection_item(val, attribute, parent_plan,
588
+ options)
589
+ item_options = element_options.merge(mapper_class: item_mapper_class)
590
+ build_element_with_plan(xml, val,
591
+ item_plan || DeclarationPlan.empty, item_options)
592
+ end
593
+ end
594
+
595
+ def plan_for_collection_item(val, attribute, parent_plan, options)
596
+ item_mapper_class = if polymorphic_value?(attribute, val)
597
+ val.class
598
+ else
599
+ attribute.type(register)
600
+ end
601
+
602
+ item_mapping = item_mapper_class.mappings_for(:xml)
603
+ return nil unless item_mapping
604
+
605
+ # Transform model to XmlElement tree
606
+ transformation = item_mapper_class.transformation_for(:xml, register)
607
+ xml_element = transformation.transform(val, options)
608
+
609
+ # Collect namespace needs from XmlElement tree
610
+ collector = NamespaceCollector.new(register)
611
+ item_needs = collector.collect(xml_element, item_mapping,
612
+ mapper_class: item_mapper_class)
613
+
614
+ # Plan with XmlElement tree (not model instance)
615
+ planner = DeclarationPlanner.new(register)
616
+ planner.plan(xml_element, item_mapping, item_needs,
617
+ parent_plan: parent_plan, options: options)
618
+ end
619
+
620
+ # Add simple (non-model) values to XML
621
+ def add_simple_value(xml, rule, value, attribute, plan: nil,
622
+ mapping: nil, options: {})
623
+ value = rule.render_value_for(value) if rule
624
+
625
+ if value.is_a?(Array)
626
+ if value.empty?
627
+ if rule.render_empty?
628
+ if rule.render_empty_as_nil?
629
+ xml.create_and_add_element(rule.name,
630
+ attributes: { "xsi:nil" => true },
631
+ prefix: nil)
632
+ else
633
+ xml.create_and_add_element(rule.name,
634
+ attributes: nil,
635
+ prefix: nil)
636
+ end
637
+ end
638
+ return
639
+ end
640
+
641
+ value.each do |val|
642
+ add_simple_value(xml, rule, val, attribute, plan: plan,
643
+ mapping: mapping, options: options)
644
+ end
645
+ return
646
+ end
647
+
648
+ resolved_prefix, attributes = resolve_simple_value_namespace(
649
+ rule, attribute, mapping, plan, options
650
+ )
651
+
652
+ render_simple_value_element(xml, rule, value, attribute,
653
+ resolved_prefix, attributes)
654
+ end
655
+
656
+ def resolve_simple_value_namespace(rule, attribute, mapping, plan,
657
+ options)
658
+ resolver = NamespaceResolver.new(register)
659
+
660
+ # Extract parent_uses_default_ns from options or calculate it
661
+ parent_uses_default_ns = options[:parent_uses_default_ns]
662
+ if parent_uses_default_ns.nil?
663
+ parent_uses_default_ns = if mapping&.namespace_class && plan
664
+ DeclarationPlanQuery.declared_at_root_default_format?(plan,
665
+ mapping.namespace_class)
666
+ else
667
+ false
668
+ end
669
+ end
670
+
671
+ # Resolve namespace using the resolver
672
+ ns_result = resolver.resolve_for_element(rule, attribute, mapping,
673
+ plan, options)
674
+ resolved_prefix = ns_result[:prefix]
675
+ type_ns_info = ns_result[:ns_info]
676
+
677
+ # CRITICAL FIX: Type namespace format inheritance for namespace_scope
678
+ type_ns_class = if attribute && !rule.namespace_set?
679
+ type_class = attribute.type(register)
680
+ type_class.namespace_class if type_class.is_a?(Class) && type_class <= Lutaml::Model::Type::Value
681
+ end
682
+
683
+ format_from_stored_plan = false
684
+
685
+ if type_ns_class
686
+ check_plan = plan || options[:stored_xml_declaration_plan]
687
+ if check_plan
688
+ stored_ns_decl = check_plan.namespaces.values.find do |decl|
689
+ decl.uri == type_ns_class.uri
690
+ end
691
+ if stored_ns_decl
692
+ resolved_prefix = if stored_ns_decl.local_on_use? || stored_ns_decl.prefix_format?
693
+ stored_ns_decl.prefix
694
+ end
695
+ format_from_stored_plan = true
696
+ end
697
+ end
698
+ end
699
+
700
+ # BUG FIX #49: Check if child element is in same namespace as parent
701
+ unless format_from_stored_plan
702
+ element_has_no_explicit_ns = !rule.namespace_set?
703
+ type_class = attribute&.type(register)
704
+ type_has_no_ns = !(type_class.is_a?(Class) && type_class <= Lutaml::Model::Type::Value) ||
705
+ !type_class&.namespace_class
706
+
707
+ parent_ns_class = options[:parent_namespace_class]
708
+ parent_ns_decl = options[:parent_ns_decl]
709
+ parent_ns_uri = parent_ns_class&.uri
710
+ child_ns_uri = ns_result[:uri]
711
+
712
+ resolved_prefix = if element_has_no_explicit_ns && type_has_no_ns
713
+ nil
714
+ elsif parent_ns_class && parent_ns_decl &&
715
+ child_ns_uri && parent_ns_uri &&
716
+ child_ns_uri == parent_ns_uri
717
+ if parent_ns_decl.prefix_format?
718
+ parent_ns_decl.prefix
719
+ end
720
+ else
721
+ ns_result[:prefix]
722
+ end
723
+ end
724
+
725
+ # Prepare attributes for element creation
726
+ attributes = {}
727
+
728
+ # W3C COMPLIANCE: Use resolver to determine xmlns="" requirement
729
+ if resolver.xmlns_blank_required?(ns_result, parent_uses_default_ns)
730
+ attributes["xmlns"] = ""
731
+ end
732
+
733
+ # Check if this namespace needs local declaration (out of scope)
734
+ if resolved_prefix && plan&.namespaces
735
+ ns_entry = plan.namespaces.values.find do |ns_decl|
736
+ ns_decl.ns_object.prefix_default == resolved_prefix ||
737
+ (type_ns_info && type_ns_info[:uri] && ns_decl.ns_object.uri == type_ns_info[:uri])
738
+ end
739
+
740
+ if ns_entry&.local_on_use?
741
+ xmlns_attr = resolved_prefix ? "xmlns:#{resolved_prefix}" : "xmlns"
742
+ attributes[xmlns_attr] = ns_entry.ns_object.uri
743
+ end
744
+ end
745
+
746
+ [resolved_prefix, attributes]
747
+ end
748
+
749
+ def render_simple_value_element(xml, rule, value, attribute,
750
+ resolved_prefix, attributes)
751
+ if value.nil?
752
+ if rule.render_nil_as_blank? || rule.render_nil_as_empty?
753
+ xml.create_and_add_element(rule.name,
754
+ attributes: attributes.empty? ? nil : attributes,
755
+ prefix: resolved_prefix)
756
+ else
757
+ xml.create_and_add_element(rule.name,
758
+ attributes: attributes.merge({ "xsi:nil" => true }),
759
+ prefix: resolved_prefix)
760
+ end
761
+ elsif ::Lutaml::Model::Utils.uninitialized?(value)
762
+ nil
763
+ elsif ::Lutaml::Model::Utils.empty?(value)
764
+ xml.create_and_add_element(rule.name,
765
+ attributes: attributes.empty? ? nil : attributes,
766
+ prefix: resolved_prefix)
767
+ elsif rule.raw_mapping?
768
+ xml.add_xml_fragment(xml, value)
769
+ elsif value.is_a?(::Hash) && attribute&.type(register) == Lutaml::Model::Type::Hash
770
+ xml.create_and_add_element(rule.name,
771
+ attributes: attributes.empty? ? nil : attributes,
772
+ prefix: resolved_prefix) do
773
+ value.each do |key, val|
774
+ xml.create_and_add_element(key.to_s) do
775
+ xml.add_text(xml, val.to_s)
776
+ end
777
+ end
778
+ end
779
+ else
780
+ xml.create_and_add_element(rule.name,
781
+ attributes: attributes.empty? ? nil : attributes,
782
+ prefix: resolved_prefix) do
783
+ add_value(xml, value, attribute, cdata: rule.cdata)
784
+ end
785
+ end
786
+ end
787
+
788
+ # Get child plan from parent plan
789
+ #
790
+ # @param plan [DeclarationPlan, nil] the parent plan
791
+ # @param attr_name [Symbol] the attribute name
792
+ # @return [DeclarationPlan, nil] the child plan or nil
793
+ def child_plan_for(plan, attr_name)
794
+ plan&.child_plan(attr_name)
795
+ end
796
+
797
+ # Fetch attribute definition and value, handling delegation
798
+ #
799
+ # @param element [Object] the model instance
800
+ # @param element_rule [MappingRule] the mapping rule
801
+ # @param mapper_class [Class] the mapper class
802
+ # @return [Array<(Attribute, Object)>] attribute definition and value tuple
803
+ def fetch_attribute_and_value(element, element_rule, mapper_class)
804
+ attribute_def = nil
805
+ value = nil
806
+
807
+ if element_rule.delegate
808
+ delegate_obj = element.send(element_rule.delegate)
809
+ if delegate_obj.respond_to?(element_rule.to)
810
+ attribute_def = delegate_obj.class.attributes[element_rule.to]
811
+ value = delegate_obj.send(element_rule.to)
812
+ end
813
+ else
814
+ attribute_def = attribute_definition_for(element, element_rule,
815
+ mapper_class: mapper_class)
816
+ value = attribute_value_for(element, element_rule)
817
+ end
818
+
819
+ [attribute_def, value]
820
+ end
821
+
822
+ # Process content for ordered elements
823
+ #
824
+ # @param element [Object] the model instance
825
+ # @param xml_mapping [Xml::Mapping] the XML mapping
826
+ # @param xml [Builder] the XML builder
827
+ # @param curr_index [Integer] current index in collection
828
+ # @param content [Array] accumulated content strings
829
+ def process_ordered_content(element, xml_mapping, xml, curr_index,
830
+ content)
831
+ text = element.send(xml_mapping.content_mapping.to)
832
+ text = text[curr_index] if text.is_a?(Array)
833
+
834
+ if element.mixed?
835
+ add_mixed_text(xml, text)
836
+ else
837
+ content << text
838
+ end
839
+ end
840
+
841
+ # Process a single ordered element
842
+ #
843
+ # @param xml [Builder] the XML builder
844
+ # @param element [Object] the model instance
845
+ # @param element_rule [MappingRule] the mapping rule
846
+ # @param attribute_def [Attribute, nil] the attribute definition
847
+ # @param value [Object] the value
848
+ # @param curr_index [Integer] current index in collection
849
+ # @param plan [DeclarationPlan, Hash] the declaration plan
850
+ # @param xml_mapping [Xml::Mapping] the XML mapping
851
+ # @param options [Hash] serialization options
852
+ def process_ordered_element(xml, element, element_rule, attribute_def,
853
+ value, curr_index, plan, xml_mapping, options)
854
+ # Handle collection values by index
855
+ current_value = if attribute_def&.collection? && value.is_a?(Array)
856
+ value[curr_index]
857
+ elsif attribute_def&.collection? && value.is_a?(Lutaml::Model::Collection)
858
+ value.to_a[curr_index]
859
+ else
860
+ value
861
+ end
862
+
863
+ # Get child's plan if available
864
+ child_plan = child_plan_for(plan, element_rule.to)
865
+
866
+ is_collection_instance = current_value.is_a?(Lutaml::Model::Collection)
867
+
868
+ if current_value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
869
+ handle_nested_elements_with_plan(
870
+ xml,
871
+ current_value,
872
+ element_rule,
873
+ attribute_def,
874
+ child_plan,
875
+ options,
876
+ parent_plan: plan,
877
+ )
878
+ else
879
+ # Apply transformations if attribute_def exists
880
+ if attribute_def
881
+ current_value = ExportTransformer.call(current_value,
882
+ element_rule, attribute_def, format: :xml)
883
+ end
884
+
885
+ # For mixed content, create elements directly
886
+ if element.mixed? && !attribute_def&.raw?
887
+ add_mixed_element(xml, element_rule, current_value, attribute_def,
888
+ plan: plan, mapping: xml_mapping)
889
+ else
890
+ add_simple_value(xml, element_rule, current_value,
891
+ attribute_def, plan: plan, mapping: xml_mapping, options: options)
892
+ end
893
+ end
894
+ end
895
+
896
+ # Add text for mixed content (can be overridden by adapters)
897
+ #
898
+ # @param xml [Builder] the XML builder
899
+ # @param text [String] the text to add
900
+ def add_mixed_text(xml, text)
901
+ xml.add_text(xml, text) unless text.nil? || text.to_s.empty?
902
+ end
903
+
904
+ # Add element for mixed content (can be overridden by adapters)
905
+ #
906
+ # @param xml [Builder] the XML builder
907
+ # @param element_rule [MappingRule] the element rule
908
+ # @param value [Object] the value to add
909
+ # @param attribute [Attribute, nil] the attribute definition
910
+ # @param plan [DeclarationPlan, Hash, nil] the declaration plan
911
+ # @param mapping [Xml::Mapping] the XML mapping
912
+ def add_mixed_element(xml, element_rule, value, _attribute,
913
+ plan: nil, mapping: nil) # rubocop:disable Lint/UnusedMethodArgument
914
+ xml.create_and_add_element(element_rule.name) do |child_element|
915
+ child_element.text(value.to_s) unless ::Lutaml::Model::Utils.empty?(value)
916
+ end
917
+ end
918
+
919
+ # Add accumulated content (can be overridden by adapters)
920
+ #
921
+ # @param xml [Builder] the XML builder
922
+ # @param content [Array<String>] accumulated content strings
923
+ def add_ordered_content(xml, content)
924
+ xml.add_text(xml, content.join)
925
+ end
926
+ end
927
+ end
928
+ end
929
+ end