lutaml-model 0.8.5 → 0.8.6

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 (35) 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/serialization/format_conversion.rb +19 -42
  24. data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
  25. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
  26. data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
  27. data/lib/lutaml/xml/xml_element.rb +24 -20
  28. data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
  29. data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
  30. data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
  31. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
  32. data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
  33. data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
  34. metadata +9 -3
  35. data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
@@ -1,875 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rexml/document"
2
4
  require "moxml"
3
5
  require "moxml/adapter/rexml"
6
+ require_relative "base_adapter"
4
7
 
5
8
  module Lutaml
6
9
  module Xml
7
10
  module Adapter
8
11
  class RexmlAdapter < BaseAdapter
9
- extend AdapterHelpers
10
-
11
- TEXT_CLASSES = [Moxml::Text, Moxml::Cdata].freeze
12
-
13
- def self.parse(xml, options = {})
14
- parse_encoding = encoding(xml, options)
15
- xml = normalize_xml_for_rexml(xml)
16
-
17
- parsed = Moxml::Adapter::Rexml.parse(xml, encoding: parse_encoding)
18
- root_element = parsed.root
19
-
20
- if root_element.nil?
21
- raise REXML::ParseException.new(
22
- "Malformed XML: Unable to parse the provided XML document. " \
23
- "The document structure is invalid or incomplete.",
24
- )
25
- end
26
-
27
- @root = Rexml::Element.new(root_element)
28
- @root.processing_instructions = extract_document_processing_instructions(parsed)
29
- new(@root, parse_encoding)
30
- end
31
-
32
- def to_xml(options = {})
33
- encoding = determine_encoding(options)
34
- builder_options = encoding ? { encoding: encoding } : {}
35
-
36
- builder = Builder::Rexml.build(builder_options) do |xml|
37
- if @root.is_a?(Rexml::Element)
38
- # Case A: Old parsed XML (from Rexml::Element) - use build_xml
39
- @root.build_xml(xml)
40
- else
41
- # Cases B & C: XmlElement or Model instance
42
- # ARCHITECTURE: Normalize to XmlElement, then use single rendering path
43
-
44
- # Determine the source (XmlElement or model instance)
45
- original_model = nil
46
-
47
- xml_element = if @root.is_a?(Lutaml::Xml::DataModel::XmlElement)
48
- # Case B: Already an XmlElement
49
- @root
50
- else
51
- # Case C: Model instance - check for custom methods first
52
- mapper_class = options[:mapper_class] || @root.class
53
- xml_mapping = mapper_class.mappings_for(:xml)
54
-
55
- # Check if model has map_all with custom methods
56
- # Custom methods work with model instances, not XmlElement trees
57
- has_custom_map_all = xml_mapping.raw_mapping&.custom_methods &&
58
- xml_mapping.raw_mapping.custom_methods[:to]
59
-
60
- if has_custom_map_all
61
- # Use legacy path for custom methods - don't transform
62
- nil
63
- else
64
- # Transform model to XmlElement tree
65
- original_model = @root
66
- transformation = mapper_class.transformation_for(
67
- :xml, register
68
- )
69
- transformation.transform(@root, options)
70
- end
71
- end
72
-
73
- if xml_element
74
- # Modern path: Use XmlElement + DeclarationPlan tree
75
- mapper_class = options[:mapper_class] || xml_element.class
76
- mapping = mapper_class.mappings_for(:xml)
77
-
78
- # Phase 1: Collect namespace needs from XmlElement tree
79
- collector = NamespaceCollector.new(register)
80
- needs = collector.collect(xml_element, mapping,
81
- mapper_class: mapper_class)
82
-
83
- # Phase 2: Plan namespace declarations (builds ElementNode tree)
84
- planner = DeclarationPlanner.new(register)
85
- plan = planner.plan(xml_element, mapping, needs,
86
- options: options)
87
-
88
- # Phase 3: Render using XmlElement + DeclarationPlan
89
- render_options = options.merge(is_root_element: true)
90
- if original_model
91
- render_options[:original_model] =
92
- original_model
93
- end
94
- build_xml_element_with_plan(xml, xml_element, plan,
95
- render_options)
96
- else
97
- # Legacy path: Model instance with custom methods
98
- mapper_class = options[:mapper_class] || @root.class
99
- xml_mapping = mapper_class.mappings_for(:xml)
100
-
101
- collector = NamespaceCollector.new(register)
102
- needs = collector.collect(@root, xml_mapping)
103
-
104
- planner = DeclarationPlanner.new(register)
105
- plan = planner.plan(@root, xml_mapping, needs, options: options)
106
-
107
- build_element_with_plan(xml, @root, plan, options)
108
- end
109
- end
110
- end
111
-
112
- xml_data = builder.to_xml
113
- options[:declaration] ? declaration(options) + xml_data : xml_data
114
- end
115
-
116
- # Build XML from XmlDataModel::XmlElement using DeclarationPlan tree (PARALLEL TRAVERSAL)
117
- #
118
- # @param xml [Builder::Rexml] XML builder
119
- # @param xml_element [XmlDataModel::XmlElement] Element content
120
- # @param plan [DeclarationPlan] Declaration plan with tree structure
121
- # @param options [Hash] Serialization options
122
- def build_xml_element_with_plan(xml, xml_element, plan, _options = {})
123
- # Add processing instructions before root element
124
- xml_element.processing_instructions.each do |pi|
125
- xml.add_processing_instruction(pi.target, pi.content)
126
- end
127
-
128
- build_rexml_element(xml, xml_element, plan.root_node,
129
- plan.global_prefix_registry, plan)
130
- end
131
-
132
- private
133
-
134
- # Recursively build REXML elements (PARALLEL TRAVERSAL)
135
- #
136
- # @param xml [Builder::Rexml] XML builder
137
- # @param xml_element [XmlDataModel::XmlElement] Content
138
- # @param element_node [ElementNode] Decisions
139
- # @param global_registry [Hash] Global prefix registry (URI => prefix)
140
- # @return [void]
141
- def build_rexml_element(xml, xml_element, element_node,
142
- global_registry, plan)
143
- qualified_name = element_node.qualified_name
144
-
145
- # 1. Collect attributes (xmlns declarations + regular attributes)
146
- attributes = {}
147
-
148
- # 2. Add hoisted xmlns declarations
149
- original_ns_uris = plan&.original_namespace_uris || {}
150
- element_node.hoisted_declarations.each do |key, uri|
151
- next if uri == "http://www.w3.org/XML/1998/namespace"
152
-
153
- # Convert FPI to URN if necessary (REXML requires valid URI)
154
- effective_uri = if self.class.fpi?(uri)
155
- self.class.fpi_to_urn(uri)
156
- else
157
- original_ns_uris[uri] || uri
158
- end
159
- xmlns_name = key ? "xmlns:#{key}" : "xmlns"
160
- attributes[xmlns_name] = effective_uri
161
- end
162
-
163
- # 3. Add regular attributes by INDEX (PARALLEL TRAVERSAL)
164
- xml_element.attributes.each_with_index do |xml_attr, idx|
165
- attr_node = element_node.attribute_nodes[idx]
166
- attributes[attr_node.qualified_name] = xml_attr.value.to_s
167
- end
168
-
169
- # 4. Add xsi:nil if needed
170
- if xml_element.respond_to?(:xsi_nil) && xml_element.xsi_nil
171
- attributes["xsi:nil"] = "true"
172
- end
173
-
174
- # Add schema_location attribute from ElementNode if present
175
- attributes.merge!(element_node.schema_location_attr) if element_node.schema_location_attr
176
-
177
- # 5. Add xmlns="" if element needs to opt out of parent's default namespace
178
- if element_node.needs_xmlns_blank
179
- attributes["xmlns"] = ""
180
- end
181
-
182
- # 6. Create element with qualified name and attributes
183
- xml.create_and_add_element(qualified_name,
184
- attributes: attributes) do |inner_xml|
185
- # 7. Handle raw content (map_all directive)
186
- if xml_element.respond_to?(:raw_content)
187
- raw_content = xml_element.raw_content
188
- if raw_content && !raw_content.to_s.empty?
189
- inner_xml.text(raw_content.to_s)
190
- return
191
- end
192
- end
193
-
194
- # 8. Add text content if present
195
- if xml_element.text_content
196
- if xml_element.cdata
197
- inner_xml.cdata(xml_element.text_content.to_s)
198
- else
199
- inner_xml.text(xml_element.text_content.to_s)
200
- end
201
- end
202
-
203
- # 9. Recursively build children by INDEX (PARALLEL TRAVERSAL)
204
- child_element_index = 0
205
- xml_element.children.each do |xml_child|
206
- case xml_child
207
- when Lutaml::Xml::DataModel::XmlElement
208
- child_node = element_node.element_nodes[child_element_index]
209
- child_element_index += 1
210
-
211
- build_rexml_element(inner_xml, xml_child, child_node,
212
- global_registry, plan)
213
- when String
214
- inner_xml.text(xml_child)
215
- when ::Lutaml::Xml::DataModel::XmlComment
216
- inner_xml.comment(xml_child.content)
217
- end
218
- end
219
- end
220
- end
221
-
222
- public
223
-
224
- def attributes_hash(element)
225
- result = Lutaml::Model::MappingHash.new
226
-
227
- element.attributes.each_value do |attr|
228
- if attr.name == "schemaLocation"
229
- result["__schema_location"] = {
230
- namespace: attr.namespace,
231
- prefix: attr.namespace.prefix,
232
- schema_location: attr.value,
233
- }
234
- else
235
- result[self.class.namespaced_attr_name(attr)] = attr.value
236
- end
237
- end
238
-
239
- result
240
- end
241
-
242
- # NOTE: name_of, prefixed_name_of, namespaced_attr_name, namespaced_name_of
243
- # are provided by AdapterHelpers module via extend
244
-
245
- def self.text_of(element)
246
- element.content
247
- end
248
-
249
- def order
250
- children.filter_map do |child|
251
- if child.text?
252
- next if child.text.nil?
253
-
254
- Element.new("Text", child.unprefixed_name)
255
- elsif child.comment?
256
- Element.new("Comment", "comment",
257
- text_content: child.content,
258
- node_type: :comment)
259
- else
260
- Element.new("Element", child.unprefixed_name)
261
- end
262
- end
263
- end
264
-
265
- def self.order_of(element)
266
- element.children.map do |child|
267
- instance_args = if TEXT_CLASSES.include?(child.class)
268
- ["Text", "text"]
269
- else
270
- ["Element", name_of(child)]
271
- end
272
- Element.new(*instance_args)
273
- end
274
- end
275
-
276
- def self.normalize_xml_for_rexml(xml)
277
- return xml unless xml.is_a?(String) && xml.encoding.to_s != "UTF-8"
278
-
279
- xml.encode("UTF-8")
280
- end
281
-
282
- def build_element_with_plan(xml, element, plan, options = {})
283
- mapper_class = options[:mapper_class] || element.class
284
- xml_mapping = mapper_class.mappings_for(:xml)
285
- return xml unless xml_mapping
286
-
287
- plan ||= {
288
- namespaces: {},
289
- children_plans: {},
290
- type_namespaces: {},
291
- }
292
- # TYPE-ONLY MODELS: No element wrapper, serialize children directly
293
- # BUT if we have a tag_name in options, that means parent wants a wrapper
294
- if xml_mapping.no_element?
295
- # If parent provided a tag_name, create that wrapper first
296
- if options[:tag_name]
297
- xml.create_and_add_element(options[:tag_name]) do |inner_xml|
298
- # Serialize type-only model's children inside parent's wrapper
299
- xml_mapping.elements.each do |element_rule|
300
- next if options[:except]&.include?(element_rule.to)
301
-
302
- attribute_def = mapper_class.attributes[element_rule.to]
303
- next unless attribute_def
304
-
305
- value = element.send(element_rule.to)
306
- next unless element_rule.render?(value, element)
307
-
308
- # For type-only models, children plans may not be available
309
- # Serialize children directly
310
- if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
311
- # Nested model - recursively build it
312
- child_plan = child_plan_for(plan, element_rule.to) || {
313
- namespaces: {},
314
- children_plans: {},
315
- type_namespaces: {},
316
- }
317
- build_element_with_plan(
318
- inner_xml,
319
- value,
320
- child_plan,
321
- { mapper_class: attribute_def.type(register),
322
- tag_name: element_rule.name },
323
- )
324
- else
325
- # Simple value - create element directly
326
- inner_xml.create_and_add_element(element_rule.name) do
327
- add_value(inner_xml, value, attribute_def,
328
- cdata: element_rule.cdata)
329
- end
330
- end
331
- end
332
- end
333
- else
334
- # No wrapper at all - serialize children directly (for root-level type-only)
335
- xml_mapping.elements.each do |element_rule|
336
- next if options[:except]&.include?(element_rule.to)
337
-
338
- attribute_def = mapper_class.attributes[element_rule.to]
339
- next unless attribute_def
340
-
341
- value = element.send(element_rule.to)
342
- next unless element_rule.render?(value, element)
343
-
344
- child_plan = child_plan_for(plan, element_rule.to)
345
-
346
- if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
347
- handle_nested_elements_with_plan(
348
- xml,
349
- value,
350
- element_rule,
351
- attribute_def,
352
- child_plan,
353
- options,
354
- )
355
- else
356
- add_simple_value(xml, element_rule, value, attribute_def,
357
- plan: plan, mapping: xml_mapping)
358
- end
359
- end
360
- end
361
- return xml
362
- end
363
-
364
- # Use xmlns declarations from plan
365
- attributes = {}
366
-
367
- # Apply namespace declarations from plan
368
- plan[:namespaces]&.each_value do |ns_config|
369
- next unless ns_config[:declared_at] == :here
370
-
371
- ns_class = ns_config[:ns_object]
372
-
373
- # Parse the ready-to-use declaration string
374
- decl = ns_config[:xmlns_declaration]
375
- if decl.start_with?("xmlns:")
376
- # Prefixed namespace: "xmlns:prefix=\"uri\""
377
- prefix = decl[/xmlns:(\w+)=/, 1]
378
- attributes["xmlns:#{prefix}"] = ns_class.uri
379
- else
380
- # Default namespace: "xmlns=\"uri\""
381
- attributes["xmlns"] = ns_class.uri
382
- end
383
- end
384
-
385
- # Collect attribute custom methods to call after element creation
386
- attribute_custom_methods = []
387
-
388
- # Add regular attributes (non-xmlns)
389
- xml_mapping.attributes.each do |attribute_rule|
390
- next if options[:except]&.include?(attribute_rule.to)
391
-
392
- # Collect custom methods for later execution (after element is created)
393
- if attribute_rule.custom_methods[:to]
394
- attribute_custom_methods << attribute_rule
395
- next
396
- end
397
-
398
- mapping_rule_name = if attribute_rule.multiple_mappings?
399
- attribute_rule.name.first
400
- else
401
- attribute_rule.name
402
- end
403
-
404
- attr = attribute_definition_for(element, attribute_rule,
405
- mapper_class: mapper_class)
406
- value = attribute_rule.to_value_for(element)
407
- value = attr.serialize(value, :xml, register) if attr
408
- value = ExportTransformer.call(value, attribute_rule, attr,
409
- format: :xml)
410
- value = value&.join(attribute_rule.delimiter) if attribute_rule.delimiter
411
-
412
- if attribute_rule.as_list && attribute_rule.as_list[:export]
413
- value = attribute_rule.as_list[:export].call(value)
414
- end
415
-
416
- if render_element?(attribute_rule, element, value)
417
- # Resolve attribute namespace from plan
418
- ns_info = resolve_attribute_namespace(attribute_rule, attr,
419
- options.merge(mapper_class: mapper_class))
420
- attr_name = if ns_info[:prefix]
421
- "#{ns_info[:prefix]}:#{mapping_rule_name}"
422
- else
423
- attribute_rule.prefixed_name
424
- end
425
- attributes[attr_name] = value ? value.to_s : value
426
- end
427
- end
428
-
429
- # Add schema_location attribute from ElementNode if present
430
- # This is for the plan-based path where schema_location_attr is computed during planning
431
- attributes.merge!(plan.root_node.schema_location_attr) if plan.respond_to?(:root_node) && plan.root_node&.schema_location_attr
432
-
433
- # Determine prefix from plan
434
- prefix = nil
435
- option_rule = options[:rule]
436
- namespace_class = if option_rule&.prefix_set? || option_rule&.namespace_set?
437
- option_rule.namespace_class
438
- else
439
- xml_mapping.namespace_class
440
- end
441
- if namespace_class
442
- key = namespace_class.to_key
443
- ns_config = plan[:namespaces][key]
444
-
445
- if ns_config && ns_config[:format] == :prefix
446
- # Use prefix from the plan's namespace object (may be custom override)
447
- prefix = ns_config[:ns_object].prefix_default
448
- end
449
- end
450
-
451
- tag_name = options[:tag_name] || xml_mapping.root_element
452
- return if options[:except]&.include?(tag_name)
453
-
454
- xml.create_and_add_element(tag_name, prefix: prefix,
455
- attributes: attributes.compact) do
456
- # Call attribute custom methods now that element is created
457
- attribute_custom_methods.each do |attribute_rule|
458
- mapper_class.new.send(attribute_rule.custom_methods[:to],
459
- element, xml.parent, xml)
460
- end
461
-
462
- if ordered?(element, options.merge(mapper_class: mapper_class))
463
- build_ordered_element_with_plan(xml, element, plan,
464
- options.merge(mapper_class: mapper_class, parent_prefix: prefix))
465
- else
466
- build_unordered_children_with_plan(xml, element, plan,
467
- options.merge(mapper_class: mapper_class, parent_prefix: prefix))
468
- end
469
- end
470
- end
471
-
472
- # NOTE: build_unordered_children_with_plan and build_ordered_element_with_plan
473
- # are inherited from BaseAdapter and use child_plan_for for unified plan access
474
-
475
- def handle_nested_elements_with_plan(xml, value, rule, attribute, plan,
476
- options)
477
- element_options = options.merge(
478
- rule: rule,
479
- attribute: attribute,
480
- tag_name: rule.name,
481
- mapper_class: attribute.type(register), # Override with child's type
482
- )
483
-
484
- if value.is_a?(Lutaml::Model::Collection)
485
- value.collection.each do |val|
486
- build_element_with_plan(xml, val, plan, element_options)
487
- end
488
- return
489
- end
490
-
491
- case value
492
- when Array
493
- value.each do |val|
494
- if plan
495
- build_element_with_plan(xml, val, plan, element_options)
496
- else
497
- # Fallback for cases without plan
498
- build_element(xml, val, element_options)
499
- end
500
- end
501
- else
502
- if plan
503
- build_element_with_plan(xml, value, plan, element_options)
504
- else
505
- # Fallback for cases without plan
506
- build_element(xml, value, element_options)
507
- end
508
- end
509
- end
510
-
511
- def add_simple_value(xml, rule, value, attribute, plan: nil,
512
- mapping: nil)
513
- # Apply value_map transformation BEFORE checking if should render
514
- value = rule.render_value_for(value) if rule
515
-
516
- # Handle array values by creating multiple elements
517
- if value.is_a?(Array)
518
- # For empty arrays, check if we should render based on render_empty option
519
- if value.empty?
520
- # Only create element if render_empty is set to render (not :omit)
521
- if rule.render_empty?
522
- # Create single empty element for the collection
523
- # Determine how to render based on render_empty option
524
- if rule.render_empty_as_nil?
525
- # render_empty: :as_nil
526
- xml.create_and_add_element(rule.name,
527
- attributes: { "xsi:nil" => true },
528
- prefix: nil)
529
- else
530
- # render_empty: :as_blank or :as_empty
531
- xml.create_and_add_element(rule.name,
532
- attributes: nil,
533
- prefix: nil)
534
- end
535
- end
536
- # Don't iterate over empty array
537
- return
538
- end
539
-
540
- # Non-empty array: create element for each value
541
- value.each do |val|
542
- add_simple_value(xml, rule, val, attribute, plan: plan,
543
- mapping: mapping)
544
- end
545
- return
546
- end
547
-
548
- # Get form_default from parent's schema (namespace class)
549
- form_default = mapping&.namespace_class&.element_form_default || :qualified
550
-
551
- # Resolve element's namespace first to know which namespace we're dealing with
552
- temp_ns_info = rule.resolve_namespace(
553
- attr: attribute,
554
- register: register,
555
- parent_ns_uri: mapping&.namespace_uri,
556
- parent_ns_class: mapping&.namespace_class,
557
- form_default: form_default,
558
- use_prefix: false, # Temporary, just to get namespace
559
- parent_prefix: nil,
560
- )
561
-
562
- element_ns_uri = temp_ns_info[:uri]
563
-
564
- # NAMESPACE RESOLUTION: Determine if element should use prefix
565
- # Cases:
566
- # 1. namespace: :inherit → always use parent prefix
567
- # 2. Type namespace → use Type's namespace from plan
568
- # 3. Parent uses prefix format AND element has no explicit/type namespace → inherit parent
569
- # 4. Element has namespace matching parent → check plan[:namespaces][ns_class]
570
- # 5. Element has explicit namespace: nil → NO prefix ever
571
-
572
- use_prefix = false
573
- parent_prefix = nil
574
-
575
- # PRIORITY: Check explicit form and prefix options FIRST
576
- # These override all other considerations
577
- if rule.qualified?
578
- # Explicit form: :qualified - element MUST use prefix
579
- use_prefix = true
580
- # Find appropriate prefix for the element's namespace
581
- if element_ns_uri && plan && plan[:namespaces]
582
- ns_entry = plan[:namespaces].find do |_key, ns_config|
583
- ns_config[:ns_object].uri == element_ns_uri
584
- end
585
- if ns_entry
586
- _key, ns_config = ns_entry
587
- parent_prefix = ns_config[:ns_object].prefix_default
588
- end
589
- end
590
- elsif rule.unqualified?
591
- # Explicit form: :unqualified - element MUST NOT use prefix
592
- use_prefix = false
593
- parent_prefix = nil
594
- elsif rule.prefix_set?
595
- # Explicit prefix option - element should use specified prefix
596
- use_prefix = true
597
- # If prefix is a string, use it; if true, use namespace's default prefix
598
- if rule.prefix.is_a?(String)
599
- parent_prefix = rule.prefix
600
- elsif element_ns_uri && plan && plan[:namespaces]
601
- ns_entry = plan[:namespaces].find do |_key, ns_config|
602
- ns_config[:ns_object].uri == element_ns_uri
603
- end
604
- if ns_entry
605
- _key, ns_config = ns_entry
606
- parent_prefix = ns_config[:ns_object].prefix_default
607
- end
608
- end
609
- elsif rule.namespace_param == :inherit
610
- # Case 1: Explicit :inherit - always use parent format
611
- use_prefix = true
612
- if plan && mapping&.namespace_class
613
- key = mapping.namespace_class.to_key
614
- ns_config = plan[:namespaces][key]
615
- if ns_config && ns_config[:format] == :prefix
616
- # CRITICAL: Use the ns_object from plan (may be override with custom prefix)
617
- parent_prefix = ns_config[:ns_object].prefix_default
618
- end
619
- end
620
- elsif plan && plan[:type_namespaces] && plan[:type_namespaces][rule.to]
621
- # Case 2: Type namespace - this attribute's type defines its own namespace
622
- # Priority: Type namespace takes precedence over parent inheritance
623
- type_ns_class = plan[:type_namespaces][rule.to]
624
- key = type_ns_class.to_key
625
- ns_config = plan[:namespaces][key]
626
- if ns_config && ns_config[:format] == :prefix
627
- use_prefix = true
628
- # CRITICAL: Use ns_object from plan (may be override with custom prefix)
629
- parent_prefix = ns_config[:ns_object].prefix_default
630
- end
631
- elsif !rule.namespace_set? && !element_ns_uri && mapping&.namespace_class && plan
632
- # Case 3: NEW - Format Matching Rule
633
- # When parent uses prefix format AND element has no explicit namespace AND no type namespace,
634
- # element inherits parent's namespace and prefix for consistent formatting.
635
- # This handles the test case where children should match parent's serialization format.
636
- # IMPORTANT: Only applies when element_form_default is :qualified
637
- key = mapping.namespace_class.to_key
638
- ns_config = plan[:namespaces][key]
639
- if ns_config && ns_config[:format] == :prefix && form_default == :qualified
640
- # Parent is using prefix format AND schema requires qualified elements
641
- use_prefix = true
642
- parent_prefix = ns_config[:ns_object].prefix_default
643
- # Override element_ns_uri to parent's URI for proper resolution
644
- element_ns_uri = mapping.namespace_uri
645
- end
646
- elsif element_ns_uri
647
- # Case 4: Element has explicit namespace - check if it's in prefix mode
648
- # Need to find the namespace class by URI to look up config
649
- if plan && plan[:namespaces]
650
- # Find namespace entry that matches this URI
651
- ns_entry = plan[:namespaces].find do |_key, ns_config|
652
- ns_config[:ns_object].uri == element_ns_uri
653
- end
654
- if ns_entry
655
- _key, ns_config = ns_entry
656
- use_prefix = ns_config[:format] == :prefix
657
- parent_prefix = ns_config[:ns_object].prefix_default if use_prefix
658
- end
659
- end
660
- elsif !rule.namespace_set? && element_ns_uri && element_ns_uri == mapping&.namespace_uri
661
- # Case 5: Element has SAME namespace as parent (not nil, not unqualified)
662
- # Element has a resolved namespace that matches parent -> inherit parent format
663
- # Truly unqualified elements (element_ns_uri.nil?) do NOT inherit
664
- if plan && mapping&.namespace_class
665
- key = mapping.namespace_class.to_key
666
- ns_config = plan[:namespaces][key]
667
- if ns_config && ns_config[:format] == :prefix
668
- use_prefix = true
669
- # CRITICAL: Use the ns_object from plan (may be override with custom prefix)
670
- parent_prefix = ns_config[:ns_object].prefix_default
671
- end
672
- end
673
- end
674
- # Case 6: explicit namespace: nil is handled by namespace_set? && namespace_param == nil
675
- # Case 7: truly unqualified (element_ns_uri.nil?) falls through with use_prefix = false
676
-
677
- # Now resolve with correct use_prefix
678
- ns_info = rule.resolve_namespace(
679
- attr: attribute,
680
- register: register,
681
- parent_ns_uri: mapping&.namespace_uri,
682
- parent_ns_class: mapping&.namespace_class,
683
- form_default: form_default,
684
- use_prefix: use_prefix,
685
- parent_prefix: parent_prefix,
686
- )
687
-
688
- # Use resolved namespace directly, BUT handle special cases:
689
- # 1. namespace: :inherit → ALWAYS use parent prefix (resolved has parent URI)
690
- # 2. Truly unqualified elements (element_ns_uri==nil) → NO prefix unless :inherit
691
- resolved_prefix = if rule.namespace_param == :inherit || (use_prefix && parent_prefix)
692
- parent_prefix
693
- else
694
- ns_info[:prefix]
695
- end
696
-
697
- # Prepare attributes (no xmlns declaration - handled by DeclarationPlanner)
698
- attributes = {}
699
-
700
- # Check if this namespace needs local declaration (out of scope)
701
- if resolved_prefix && plan && plan[:namespaces]
702
- # Find the namespace config for this prefix/URI
703
- ns_entry = plan[:namespaces].find do |_key, ns_config|
704
- ns_config[:ns_object].prefix_default == resolved_prefix ||
705
- (ns_info[:uri] && ns_config[:ns_object].uri == ns_info[:uri])
706
- end
707
-
708
- if ns_entry
709
- _key, ns_config = ns_entry
710
- # If namespace is marked for local declaration, add xmlns attribute
711
- if ns_config[:declared_at] == :local_on_use
712
- xmlns_attr = "xmlns:#{resolved_prefix}"
713
- attributes[xmlns_attr] = ns_config[:ns_object].uri
714
- end
715
- end
716
- end
717
-
718
- if value.nil?
719
- # Check render_nil option to determine how to render nil value
720
- if rule.render_nil_as_blank? || rule.render_nil_as_empty?
721
- # render_nil: :as_blank or :as_empty - create blank element without xsi:nil
722
- xml.create_and_add_element(rule.name,
723
- attributes: attributes,
724
- prefix: resolved_prefix)
725
- else
726
- # render_nil: :as_nil or default - create element with xsi:nil="true"
727
- xml.create_and_add_element(rule.name,
728
- attributes: attributes.merge({ "xsi:nil" => true }),
729
- prefix: resolved_prefix)
730
- end
731
- elsif ::Lutaml::Model::Utils.uninitialized?(value)
732
- # Handle uninitialized values - don't try to serialize them as text
733
- # This should not normally happen as render? should filter these out
734
- # But if render_omitted is set, we might reach here
735
- nil
736
- elsif ::Lutaml::Model::Utils.empty?(value)
737
- xml.create_and_add_element(rule.name,
738
- attributes: attributes,
739
- prefix: resolved_prefix)
740
- elsif rule.raw_mapping?
741
- xml.add_xml_fragment(xml, value)
742
- elsif value.is_a?(::Hash) && attribute&.type(register) == Lutaml::Model::Type::Hash
743
- # Check if value is Hash type that needs wrapper - do this BEFORE any wrapping/serialization
744
- # Value is already transformed by ExportTransformer before reaching here
745
- xml.create_and_add_element(rule.name,
746
- attributes: attributes,
747
- prefix: resolved_prefix) do
748
- value.each do |key, val|
749
- xml.create_and_add_element(key.to_s) do
750
- xml.add_text(xml, val.to_s)
751
- end
752
- end
753
- end
754
- else
755
- xml.create_and_add_element(rule.name,
756
- attributes: attributes,
757
- prefix: resolved_prefix) do
758
- add_value(xml, value, attribute, cdata: rule.cdata)
759
- end
760
- end
761
- end
762
-
763
- # Build XML from XmlElement structure
764
- #
765
- # @param xml [Builder] the XML builder
766
-
767
- private
768
-
769
- def determine_encoding(options)
770
- options[:encoding] ||
771
- options[:parse_encoding] ||
772
- @encoding ||
773
- "UTF-8"
774
- end
775
-
776
- def build_ordered_element(builder, element, options = {})
777
- mapper_class = determine_mapper_class(element, options)
778
- xml_mapping = mapper_class.mappings_for(:xml)
779
- return builder unless xml_mapping
780
-
781
- attributes = build_attributes(element, xml_mapping, options).compact
782
- prefix = determine_namespace_prefix(options, xml_mapping)
783
- tag_name = options[:tag_name] || xml_mapping.root_element
784
-
785
- builder.create_and_add_element(tag_name, prefix: prefix,
786
- attributes: attributes) do |el|
787
- process_element_order(el, element, xml_mapping, mapper_class,
788
- options)
789
- end
790
- end
791
-
792
- def process_element_order(builder, element, xml_mapping, mapper_class,
793
- options)
794
- index_hash = {}
795
- content = []
796
-
797
- element.element_order.each do |object|
798
- process_ordered_object(builder, element, object, xml_mapping, mapper_class,
799
- index_hash, content, options)
800
- end
801
-
802
- builder.text(content.join)
803
- end
804
-
805
- def process_ordered_object(builder, element, object, xml_mapping, mapper_class,
806
- index_hash, content, options)
807
- curr_index = increment_object_index(index_hash, object)
808
- element_rule = xml_mapping.find_by_name(object.name,
809
- type: object.type,
810
- node_type: object.node_type)
811
-
812
- return if skip_element_rule?(element_rule, options)
813
-
814
- attribute_def = attribute_definition_for(element, element_rule,
815
- mapper_class: mapper_class)
816
- value = attribute_value_for(element, element_rule)
817
-
818
- return if skip_cdata_text?(element_rule, xml_mapping, object)
819
-
820
- handle_ordered_element_content(builder, element, element_rule, xml_mapping,
821
- attribute_def, value, curr_index, content, options, mapper_class)
822
- end
823
-
824
- def increment_object_index(index_hash, object)
825
- object_key = "#{object.name}-#{object.type}"
826
- index_hash[object_key] ||= -1
827
- index_hash[object_key] += 1
828
- end
829
-
830
- def skip_element_rule?(element_rule, options)
831
- element_rule.nil? || options[:except]&.include?(element_rule.to)
832
- end
833
-
834
- def skip_cdata_text?(element_rule, xml_mapping, object)
835
- element_rule == xml_mapping.content_mapping && element_rule.cdata && object.text?
836
- end
837
-
838
- def handle_ordered_element_content(builder, element, element_rule, xml_mapping,
839
- attribute_def, value, curr_index, content, options, mapper_class)
840
- if element_rule == xml_mapping.content_mapping
841
- handle_ordered_content_text(builder, element, element_rule,
842
- xml_mapping, curr_index, content)
843
- elsif !value.nil? || element_rule.render_nil?
844
- add_ordered_element_value(builder, element, attribute_def, value, curr_index,
845
- element_rule, options, mapper_class)
846
- end
847
- end
848
-
849
- def handle_ordered_content_text(builder, element, element_rule,
850
- xml_mapping, curr_index, content)
851
- text = xml_mapping.content_mapping.serialize(element)
852
- text = text[curr_index] if text.is_a?(Array)
853
-
854
- if element.mixed?
855
- if element_rule.cdata
856
- return builder.cdata(text)
857
- else
858
- return builder.text(text)
859
- end
860
- end
861
-
862
- content << text
863
- end
864
-
865
- def add_ordered_element_value(builder, element, attribute_def, value, curr_index,
866
- element_rule, options, mapper_class)
867
- value = value[curr_index] if attribute_def.collection?
868
-
869
- add_to_xml(builder, element, nil, value,
870
- options.merge(attribute: attribute_def, rule: element_rule,
871
- mapper_class: mapper_class))
872
- end
12
+ MOXML_ADAPTER = Moxml::Adapter::Rexml
13
+ BUILDER_CLASS = Builder::Rexml
14
+ PARSED_ELEMENT_CLASS = Rexml::Element
15
+ EMPTY_DOCUMENT_ERROR_MESSAGE = "Malformed XML: Unable to parse " \
16
+ "the provided XML document. The document structure is invalid " \
17
+ "or incomplete."
18
+ EMPTY_DOCUMENT_ERROR_TYPE = :parse_exception
873
19
  end
874
20
  end
875
21
  end