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,856 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oga"
2
4
  require "moxml/adapter/oga"
5
+ require_relative "base_adapter"
3
6
 
4
7
  module Lutaml
5
8
  module Xml
6
9
  module Adapter
7
10
  class OgaAdapter < BaseAdapter
8
- extend DocTypeExtractor
9
- extend AdapterHelpers
10
-
11
- TEXT_CLASSES = [Moxml::Text, Moxml::Cdata].freeze
12
-
13
- def self.parse(xml, options = {})
14
- enc = encoding(xml, options)
15
- # Oga requires UTF-8 encoded input; convert from other encodings
16
- xml = xml.encode("UTF-8") unless xml.encoding == Encoding::UTF_8
17
- parsed = Moxml::Adapter::Oga.parse(xml, encoding: enc)
18
- root_element = parsed.root
19
-
20
- # Validate that we have a root element
21
- if root_element.nil?
22
- raise Lutaml::Model::InvalidFormatError.new(
23
- :xml,
24
- "Document has no root element. " \
25
- "The XML may be empty, contain only whitespace, " \
26
- "or consist only of an XML declaration.",
27
- )
28
- end
29
-
30
- # Extract DOCTYPE information
31
- # Moxml/Oga doesn't directly expose DOCTYPE, extract from raw XML
32
- doctype_info = extract_doctype_from_xml(xml)
33
-
34
- @root = Oga::Element.new(root_element)
35
- @root.processing_instructions = extract_document_processing_instructions(parsed)
36
- new(@root, enc, doctype: doctype_info)
37
- end
38
-
39
- def to_xml(options = {})
40
- builder_options = {}
41
- encoding = determine_encoding(options)
42
- builder_options[:encoding] = encoding if encoding
43
-
44
- builder = Builder::Oga.build(builder_options) do |xml|
45
- if @root.is_a?(Oga::Element)
46
- @root.build_xml(xml)
47
- elsif @root.is_a?(Lutaml::Xml::DataModel::XmlElement)
48
- # XmlDataModel MUST go through Three-Phase Architecture
49
- mapper_class = options[:mapper_class] || @root.class
50
- xml_mapping = mapper_class.mappings_for(:xml)
51
-
52
- # Phase 1: Collect namespace needs from XmlElement tree
53
- collector = NamespaceCollector.new(register)
54
- needs = collector.collect(@root, xml_mapping,
55
- mapper_class: mapper_class)
56
-
57
- # Phase 2: Plan namespace declarations with hoisting
58
- planner = DeclarationPlanner.new(register)
59
- plan = planner.plan(@root, xml_mapping, needs,
60
- options: options)
61
-
62
- # Phase 3: Build with plan (TREE-BASED for XmlElement)
63
- build_xml_element_with_plan(xml, @root, plan, options)
64
- else
65
- # THREE-PHASE ARCHITECTURE
66
- mapper_class = options[:mapper_class] || @root.class
67
- xml_mapping = mapper_class.mappings_for(:xml)
68
-
69
- # Check if model has map_all with custom methods
70
- # Custom methods work with model instances, not XmlElement trees
71
- has_custom_map_all = xml_mapping.raw_mapping&.custom_methods &&
72
- xml_mapping.raw_mapping.custom_methods[:to]
73
-
74
- if has_custom_map_all
75
- # Use legacy path for custom methods
76
- collector = NamespaceCollector.new(register)
77
- needs = collector.collect(@root, xml_mapping,
78
- mapper_class: mapper_class)
79
-
80
- planner = DeclarationPlanner.new(register)
81
- plan = planner.plan(@root, xml_mapping, needs, options: options)
82
-
83
- build_element_with_plan(xml, @root, plan, options)
84
- else
85
- # Step 1: Transform model to XmlElement tree
86
- transformation = mapper_class.transformation_for(:xml, register)
87
- xml_element = transformation.transform(@root, options)
88
-
89
- # Step 2: Collect namespace needs from XmlElement tree
90
- collector = NamespaceCollector.new(register)
91
- needs = collector.collect(xml_element, xml_mapping,
92
- mapper_class: mapper_class)
93
-
94
- # Step 3: Plan declarations (builds ElementNode tree)
95
- planner = DeclarationPlanner.new(register)
96
- plan = planner.plan(xml_element, xml_mapping, needs,
97
- options: options)
98
-
99
- # Step 4: Render using tree (NEW - parallel traversal)
100
- build_xml_element_with_plan(xml, xml_element, plan, options)
101
- end
102
- end
103
- end
104
- xml_data = builder.to_xml
105
-
106
- result = ""
107
- # Use DeclarationHandler methods instead of Document#declaration
108
- # Include declaration when encoding is specified OR when declaration is requested
109
- if (options[:encoding] && !options[:encoding].nil?) || options[:declaration]
110
- result += generate_declaration(options)
111
- end
112
-
113
- # Add DOCTYPE if present - use DeclarationHandler method
114
- doctype_to_use = options[:doctype] || @doctype
115
- if doctype_to_use && !options[:omit_doctype]
116
- result += generate_doctype_declaration(doctype_to_use)
117
- end
118
-
119
- result += xml_data
120
-
121
- # Encode result to desired encoding (Oga outputs UTF-8 by default)
122
- if encoding && encoding != Encoding::UTF_8
123
- result = result.encode(encoding)
124
- end
125
-
126
- result
127
- end
128
-
129
- # Build element using prepared namespace declaration plan
130
- #
131
- # @param xml [Builder] the XML builder
132
- # @param element [Object] the model instance
133
- # @param plan [Hash] the declaration plan from DeclarationPlanner
134
- # @param options [Hash] serialization options
135
- def build_element_with_plan(xml, element, plan, options = {})
136
- mapper_class = options[:mapper_class] || element.class
137
- xml_mapping = mapper_class.mappings_for(:xml)
138
- return xml unless xml_mapping
139
-
140
- # TYPE-ONLY MODELS: No element wrapper, serialize children directly
141
- # BUT if we have a tag_name in options, that means parent wants a wrapper
142
- plan ||= {
143
- namespaces: {},
144
- children_plans: {},
145
- type_namespaces: {},
146
- }
147
-
148
- if xml_mapping.no_element?
149
- # If parent provided a tag_name, create that wrapper first
150
- if options[:tag_name]
151
- xml.create_and_add_element(options[:tag_name]) do |inner_xml|
152
- # Serialize type-only model's children inside parent's wrapper
153
- xml_mapping.elements.each do |element_rule|
154
- next if options[:except]&.include?(element_rule.to)
155
-
156
- attribute_def = mapper_class.attributes[element_rule.to]
157
- next unless attribute_def
158
-
159
- value = element.send(element_rule.to)
160
- next unless element_rule.render?(value, element)
161
-
162
- # For type-only models, children plans may not be available
163
- # Serialize children directly
164
- if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
165
- # Nested model - recursively build it
166
- child_plan = plan.child_plan(element_rule.to) || DeclarationPlan.empty
167
- build_element_with_plan(
168
- inner_xml,
169
- value,
170
- child_plan,
171
- { mapper_class: attribute_def.type(register),
172
- tag_name: element_rule.name },
173
- )
174
- else
175
- # Simple value - create element directly
176
- inner_xml.create_and_add_element(element_rule.name) do
177
- add_value(inner_xml, value, attribute_def,
178
- cdata: element_rule.cdata)
179
- end
180
- end
181
- end
182
- end
183
- else
184
- # No wrapper at all - serialize children directly (for root-level type-only)
185
- xml_mapping.elements.each do |element_rule|
186
- next if options[:except]&.include?(element_rule.to)
187
-
188
- attribute_def = mapper_class.attributes[element_rule.to]
189
- next unless attribute_def
190
-
191
- value = element.send(element_rule.to)
192
- next unless element_rule.render?(value, element)
193
-
194
- child_plan = plan.child_plan(element_rule.to)
195
-
196
- if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
197
- handle_nested_elements_with_plan(
198
- xml,
199
- value,
200
- element_rule,
201
- attribute_def,
202
- child_plan,
203
- options,
204
- )
205
- else
206
- add_simple_value(xml, element_rule, value, attribute_def,
207
- plan: plan, mapping: xml_mapping)
208
- end
209
- end
210
- end
211
- return xml
212
- end
213
-
214
- # Use xmlns declarations from plan
215
- attributes = {}
216
-
217
- # Apply namespace declarations from plan using extracted module
218
- attributes.merge!(NamespaceDeclarationBuilder.build_xmlns_attributes(plan))
219
-
220
- # Add regular attributes (non-xmlns)
221
-
222
- xml_mapping.attributes.each do |attribute_rule|
223
- next if attribute_rule.custom_methods[:to] ||
224
- options[:except]&.include?(attribute_rule.to)
225
-
226
- mapping_rule_name = if attribute_rule.multiple_mappings?
227
- attribute_rule.name.first
228
- else
229
- attribute_rule.name
230
- end
231
-
232
- attr = attribute_definition_for(element, attribute_rule,
233
- mapper_class: mapper_class)
234
- value = attribute_rule.to_value_for(element)
235
-
236
- # Handle as_list and delimiter BEFORE serialization for array values
237
- # These features convert arrays to delimited strings before serialization
238
- if value.is_a?(Array)
239
- if attribute_rule.as_list && attribute_rule.as_list[:export]
240
- value = attribute_rule.as_list[:export].call(value)
241
- elsif attribute_rule.delimiter
242
- value = value.join(attribute_rule.delimiter)
243
- end
244
- end
245
-
246
- value = attr.serialize(value, :xml, register) if attr
247
- value = ExportTransformer.call(value, attribute_rule, attr,
248
- format: :xml)
249
-
250
- if render_element?(attribute_rule, element, value)
251
- # Resolve attribute namespace using extracted module
252
- ns_info = AttributeNamespaceResolver.resolve(
253
- rule: attribute_rule,
254
- attribute: attr,
255
- plan: plan,
256
- mapper_class: mapper_class,
257
- register: register,
258
- )
259
-
260
- # Build qualified attribute name based on W3C semantics
261
- attr_name = AttributeNamespaceResolver.build_qualified_name(
262
- ns_info,
263
- mapping_rule_name,
264
- attribute_rule,
265
- )
266
- attributes[attr_name] = value ? value.to_s : value
267
-
268
- # Add local xmlns declaration if needed
269
- if ns_info[:needs_local_declaration]
270
- attributes[ns_info[:local_xmlns_attr]] =
271
- ns_info[:local_xmlns_uri]
272
- end
273
- end
274
- end
275
-
276
- # Add schema_location attribute from ElementNode if present
277
- # This is for the plan-based path where schema_location_attr is computed during planning
278
- attributes.merge!(plan.root_node.schema_location_attr) if plan&.root_node&.schema_location_attr
279
-
280
- # Determine prefix from plan using extracted module
281
- prefix_info = ElementPrefixResolver.resolve(mapping: xml_mapping,
282
- plan: plan)
283
- prefix = prefix_info[:prefix]
284
-
285
- tag_name = options[:tag_name] || xml_mapping.root_element
286
- return if options[:except]&.include?(tag_name)
287
-
288
- xml.create_and_add_element(tag_name, prefix: prefix,
289
- attributes: attributes.compact) do
290
- if ordered?(element, options.merge(mapper_class: mapper_class))
291
- build_ordered_element_with_plan(xml, element, plan,
292
- options.merge(
293
- mapper_class: mapper_class,
294
- parent_ns_decl: prefix_info[:ns_decl],
295
- ))
296
- else
297
- build_unordered_children_with_plan(xml, element, plan,
298
- options.merge(
299
- mapper_class: mapper_class,
300
- parent_ns_decl: prefix_info[:ns_decl],
301
- ))
302
- end
303
- end
304
- end
305
-
306
- # Build XML from XmlDataModel::XmlElement structure
307
- #
308
- # @param xml [Builder] XML builder
309
- # @param element [XmlDataModel::XmlElement] element to build
310
- # @param parent_uses_default_ns [Boolean] parent uses default namespace format
311
- # @param parent_element_form_default [Symbol] parent's element_form_default
312
- # @param parent_namespace_class [Class] parent's namespace class
313
- def build_xml_element(xml, element, parent_uses_default_ns: false,
314
- parent_element_form_default: nil, parent_namespace_class: nil)
315
- # Prepare attributes hash
316
- attributes = {}
317
-
318
- # Determine if attributes should be qualified based on element's namespace
319
- element_ns_class = element.namespace_class
320
- attribute_form_default = element_ns_class&.attribute_form_default || :unqualified
321
- element_prefix = element_ns_class&.prefix_default
322
-
323
- # Get element_form_default for children
324
- this_element_form_default = element_ns_class&.element_form_default || :unqualified
325
-
326
- # Add regular attributes
327
- element.attributes.each do |attr|
328
- # Determine attribute name with namespace consideration
329
- attr_name = if attr.namespace_class
330
- # Check if attribute is in SAME namespace as element
331
- if attr.namespace_class == element_ns_class && attribute_form_default == :unqualified
332
- # Same namespace + unqualified → NO prefix (W3C rule)
333
- attr.name
334
- else
335
- # Different namespace OR qualified → use prefix
336
- attr_prefix = attr.namespace_class.prefix_default
337
- attr_prefix ? "#{attr_prefix}:#{attr.name}" : attr.name
338
- end
339
- elsif attribute_form_default == :qualified && element_prefix
340
- # Attribute inherits element's namespace when qualified
341
- "#{element_prefix}:#{attr.name}"
342
- else
343
- # Unqualified attribute
344
- attr.name
345
- end
346
- attributes[attr_name] = attr.value
347
- end
348
-
349
- # Determine element name with namespace prefix
350
- tag_name = element.name
351
-
352
- # Priority 2.5: Child namespace different from parent's default namespace
353
- # MUST use prefix format to distinguish from parent
354
- child_needs_prefix = if element_ns_class && parent_namespace_class &&
355
- element_ns_class != parent_namespace_class && parent_uses_default_ns
356
- element_prefix # Use child's prefix
357
- end
358
-
359
- # CRITICAL FIX: element_form_default: :qualified means child elements inherit parent's namespace PREFIX
360
- # even when child has NO explicit namespace_class
361
- prefix = if child_needs_prefix
362
- # Priority 2.5 takes precedence
363
- child_needs_prefix
364
- elsif element_ns_class && element_prefix
365
- # Element has explicit prefix_default - use prefix format
366
- element_prefix
367
- elsif !element_ns_class && parent_element_form_default == :qualified && parent_namespace_class&.prefix_default
368
- # Child has NO namespace, but parent has :qualified form_default
369
- # Child should INHERIT parent's namespace PREFIX
370
- parent_namespace_class.prefix_default
371
- else
372
- # No prefix (default format or no parent namespace)
373
- nil
374
- end
375
-
376
- # Track if THIS element uses default namespace format for children
377
- this_element_uses_default_ns = false
378
-
379
- # Add namespace declaration if element has namespace
380
- if element.namespace_class
381
- ns_uri = element.namespace_class.uri
382
-
383
- if prefix
384
- attributes["xmlns:#{prefix}"] = ns_uri
385
- # W3C Compliance: xmlns="" only needed for blank namespace children
386
- # Prefixed children are already in different namespace from parent's default
387
- else
388
- attributes["xmlns"] = ns_uri
389
- this_element_uses_default_ns = true
390
- end
391
- elsif parent_uses_default_ns
392
- # W3C Compliance: Element has no namespace (blank namespace)
393
- # Check if should inherit parent's namespace based on element_form_default
394
- # Parent uses default namespace format
395
- if parent_element_form_default == :qualified
396
- # Child should INHERIT parent's namespace - no xmlns="" needed
397
- # The child is in parent namespace (qualified)
398
- else
399
- # Parent's element_form_default is :unqualified - child in blank namespace
400
- # Add xmlns="" to explicitly opt out of parent's default namespace
401
- attributes["xmlns"] = ""
402
- end
403
- end
404
-
405
- # Check if element was created from nil value with render_nil option
406
- # Add xsi:nil="true" attribute for W3C compliance
407
- if element.respond_to?(:xsi_nil) && element.xsi_nil
408
- attributes["xsi:nil"] = true
409
- end
410
-
411
- # Create element
412
- xml.create_and_add_element(tag_name, attributes: attributes,
413
- prefix: prefix) do |inner_xml|
414
- # Handle raw content (map_all directive)
415
- # If @raw_content exists, add as raw XML
416
- has_raw_content = false
417
- if element.respond_to?(:raw_content)
418
- raw_content = element.raw_content
419
- if raw_content && !raw_content.to_s.empty?
420
- inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
421
- has_raw_content = true
422
- end
423
- end
424
-
425
- # Skip text content and children if we have raw content
426
- unless has_raw_content
427
- # Add text content if present
428
- if element.text_content
429
- if element.cdata
430
- inner_xml.cdata(element.text_content.to_s)
431
- else
432
- inner_xml.text(element.text_content.to_s)
433
- end
434
- end
435
-
436
- # Recursively build child elements, passing namespace context
437
- element.children.each do |child|
438
- if child.is_a?(Lutaml::Xml::DataModel::XmlElement)
439
- build_xml_element(inner_xml, child,
440
- parent_uses_default_ns: this_element_uses_default_ns,
441
- parent_element_form_default: this_element_form_default,
442
- parent_namespace_class: element_ns_class)
443
- elsif child.is_a?(String)
444
- inner_xml.text(child)
445
- end
446
- end
447
- end
448
- end
449
- end
450
-
451
- # Build XML from XmlDataModel::XmlElement using DeclarationPlan tree (PARALLEL TRAVERSAL)
452
- #
453
- # Uses moxml APIs exclusively — no native ::Oga::XML::* classes.
454
- #
455
- # @param xml [Builder] XML builder (provides document access)
456
- # @param xml_element [XmlDataModel::XmlElement] Element content
457
- # @param plan [DeclarationPlan] Declaration plan with tree structure
458
- # @param options [Hash] Serialization options
459
- def build_xml_element_with_plan(xml, xml_element, plan, _options = {})
460
- moxml_doc = xml.doc
461
-
462
- root_element = build_moxml_node(xml_element, plan.root_node,
463
- plan.global_prefix_registry,
464
- moxml_doc, plan: plan)
465
- moxml_doc.root = root_element
466
-
467
- # Add processing instructions before the root element.
468
- # reverse_each + add_previous_sibling maintains original order.
469
- xml_element.processing_instructions.reverse_each do |pi|
470
- pi_node = moxml_doc.create_processing_instruction(pi.target,
471
- pi.content)
472
- root_element.add_previous_sibling(pi_node)
473
- end
474
- end
475
-
476
- private
477
-
478
- # Recursively build moxml element tree using moxml APIs exclusively.
479
- #
480
- # @param xml_element [XmlDataModel::XmlElement] Content
481
- # @param element_node [ElementNode] Decisions
482
- # @param global_registry [Hash] Global prefix registry (URI => prefix)
483
- # @param moxml_doc [Moxml::Document] Document for creating nodes
484
- # @param parent [Moxml::Element, nil] Parent element for xmlns deduplication
485
- # @param plan [DeclarationPlan] Declaration plan with original namespace URIs
486
- # @return [Moxml::Element] Created node
487
- def build_moxml_node(xml_element, element_node, global_registry,
488
- moxml_doc, parent = nil, plan: nil)
489
- qualified_name = element_node.qualified_name
490
-
491
- # 1. Create moxml element
492
- element = moxml_doc.create_element(qualified_name)
493
-
494
- # 2. Add hoisted xmlns declarations (filter duplicates from parent)
495
- original_ns_uris = plan&.original_namespace_uris || {}
496
- element_node.hoisted_declarations.each do |key, uri|
497
- next if uri == "http://www.w3.org/XML/1998/namespace"
498
-
499
- effective_uri = if self.class.fpi?(uri)
500
- self.class.fpi_to_urn(uri)
501
- else
502
- original_ns_uris[uri] || uri
503
- end
504
-
505
- prefix = key
506
- next if parent && parent_has_xmlns_in_chain?(parent, prefix,
507
- effective_uri)
508
-
509
- xmlns_name = prefix ? "xmlns:#{prefix}" : "xmlns"
510
- add_moxml_attribute(element, xmlns_name, effective_uri)
511
- end
512
-
513
- # 3. Add regular attributes by INDEX (PARALLEL TRAVERSAL)
514
- apply_plan_attributes(xml_element, element_node, element)
515
-
516
- # xsi:nil attribute for W3C compliance
517
- if xml_element.respond_to?(:xsi_nil) && xml_element.xsi_nil
518
- add_moxml_attribute(element, "xsi:nil", "true")
519
- end
520
-
521
- # schema_location attribute from ElementNode
522
- element_node.schema_location_attr&.each do |attr_name, attr_value|
523
- add_moxml_attribute(element, attr_name, attr_value)
524
- end
525
-
526
- # W3C Compliance: xmlns="" for blank namespace opt-out
527
- if element_node.needs_xmlns_blank
528
- add_moxml_attribute(element, "xmlns", "")
529
- end
530
-
531
- # Handle raw content (map_all directive)
532
- if xml_element.respond_to?(:raw_content)
533
- raw_content = xml_element.raw_content
534
- if raw_content && !raw_content.to_s.empty?
535
- parsed_fragment = moxml_doc.context.parse("<wrapper>#{raw_content}</wrapper>")
536
- parsed_fragment.root&.children&.each do |child_node|
537
- element.add_child(child_node)
538
- end
539
- return element
540
- end
541
- end
542
-
543
- # 5. Add text content if present
544
- if xml_element.text_content
545
- add_content_node(element, xml_element.text_content,
546
- moxml_doc, cdata: xml_element.cdata)
547
- end
548
-
549
- # 6. Recursively build children by INDEX (PARALLEL TRAVERSAL)
550
- child_element_index = 0
551
- xml_element.children.each do |xml_child|
552
- case xml_child
553
- when Lutaml::Xml::DataModel::XmlElement
554
- child_node = element_node.element_nodes[child_element_index]
555
- child_element_index += 1
556
-
557
- child_element = build_moxml_node(xml_child, child_node,
558
- global_registry, moxml_doc,
559
- element, plan: plan)
560
- element.add_child(child_element)
561
- when String
562
- add_content_node(element, xml_child, moxml_doc,
563
- cdata: xml_element.cdata && !xml_child.strip.empty?)
564
- when ::Lutaml::Xml::DataModel::XmlComment
565
- comment_node = moxml_doc.create_comment(xml_child.content)
566
- element.add_child(comment_node)
567
- end
568
- end
569
-
570
- element
571
- end
572
-
573
- def add_moxml_attribute(element, name, value)
574
- name = name.to_s
575
- value = value.to_s
576
-
577
- if name == "xmlns"
578
- element.add_namespace(nil, value)
579
- elsif name.start_with?("xmlns:")
580
- element.add_namespace(name.split(":", 2).last, value)
581
- else
582
- element[name] = value
583
- end
584
- end
585
-
586
- # Check if immediate parent element has xmlns declaration
587
- #
588
- # @param element [Moxml::Element] parent element to check
589
- # @param prefix [String, nil] namespace prefix (nil for default namespace)
590
- # @param uri [String] namespace URI
591
- # @return [Boolean] true if immediate parent has matching xmlns
592
- def parent_has_xmlns_in_chain?(element, prefix, uri)
593
- return false unless element
594
-
595
- xmlns_name = prefix ? "xmlns:#{prefix}" : "xmlns"
596
- existing_xmlns = element.attributes.find do |attr|
597
- attr.name.to_s == xmlns_name && attr.value == uri
598
- end
599
- !existing_xmlns.nil?
600
- end
601
-
602
- public
603
-
604
- # NOTE: build_unordered_children_with_plan and build_ordered_element_with_plan
605
- # are inherited from BaseAdapter - no need to override
606
-
607
- # Handle nested model elements with plan
608
- def handle_nested_elements_with_plan(xml, value, rule, attribute, plan,
609
- options, parent_plan: nil)
610
- element_options = options.merge(
611
- rule: rule,
612
- attribute: attribute,
613
- tag_name: rule.name,
614
- mapper_class: attribute.type(register), # Override with child's type
615
- )
616
-
617
- if value.is_a?(Lutaml::Model::Collection)
618
- items = value.collection
619
- attr_type = attribute.type(register)
620
-
621
- if attr_type <= Lutaml::Model::Type::Value
622
- # Simple types - use add_simple_value for each item
623
- items.each do |val|
624
- xml_mapping = options[:mapper_class]&.mappings_for(:xml)
625
- add_simple_value(xml, rule, val, attribute, plan: parent_plan,
626
- mapping: xml_mapping, options: options)
627
- end
628
- else
629
- # Model types - build elements with plans
630
- items.each do |val|
631
- # For polymorphic collections, use each item's actual class
632
- item_mapper_class = if polymorphic_value?(attribute, val)
633
- val.class
634
- else
635
- attribute.type(register)
636
- end
637
-
638
- # CRITICAL: Collect and plan for each item individually
639
- item_mapping = item_mapper_class.mappings_for(:xml)
640
- if item_mapping
641
- collector = NamespaceCollector.new(register)
642
- item_needs = collector.collect(val, item_mapping)
643
-
644
- planner = DeclarationPlanner.new(register)
645
- item_plan = planner.plan(val, item_mapping, item_needs,
646
- parent_plan: parent_plan, options: options)
647
- else
648
- item_plan = plan
649
- end
650
-
651
- item_options = element_options.merge(mapper_class: item_mapper_class)
652
- build_element_with_plan(xml, val, item_plan, item_options)
653
- end
654
- end
655
- return
656
- end
657
-
658
- case value
659
- when Array
660
- value.each do |val|
661
- # For polymorphic arrays, use each item's actual class
662
- item_mapper_class = if polymorphic_value?(attribute, val)
663
- val.class
664
- else
665
- attribute.type(register)
666
- end
667
-
668
- # CRITICAL: Collect and plan for each array item individually
669
- item_mapping = item_mapper_class.mappings_for(:xml)
670
- if item_mapping
671
- collector = NamespaceCollector.new(register)
672
- item_needs = collector.collect(val, item_mapping)
673
-
674
- planner = DeclarationPlanner.new(register)
675
- item_plan = planner.plan(val, item_mapping, item_needs,
676
- parent_plan: parent_plan, options: options)
677
- else
678
- item_plan = plan
679
- end
680
-
681
- item_options = element_options.merge(mapper_class: item_mapper_class)
682
- if item_plan
683
- build_element_with_plan(xml, val, item_plan, item_options)
684
- else
685
- build_element(xml, val, item_options)
686
- end
687
- end
688
- else
689
- build_element_with_plan(xml, value, plan, element_options)
690
- end
691
- end
692
-
693
- # Add simple (non-model) values to XML
694
- def add_simple_value(xml, rule, value, attribute, plan: nil,
695
- mapping: nil)
696
- # Handle array values by creating multiple elements
697
- if value.is_a?(Array)
698
- value.each do |val|
699
- add_simple_value(xml, rule, val, attribute, plan: plan,
700
- mapping: mapping)
701
- end
702
- return
703
- end
704
-
705
- # Determine prefix for this element based on namespace rules
706
- # Initialize namespace resolver
707
- resolver = NamespaceResolver.new(register)
708
-
709
- # Extract parent_uses_default_ns from options or calculate it
710
- parent_uses_default_ns = options[:parent_uses_default_ns]
711
- if parent_uses_default_ns.nil?
712
- parent_uses_default_ns = if mapping&.namespace_class && plan
713
- key = mapping.namespace_class.to_key
714
- ns_decl = plan.namespace(key)
715
- ns_decl&.declared_here? && ns_decl.default_format?
716
- else
717
- false
718
- end
719
- end
720
-
721
- # Resolve namespace using the resolver
722
- ns_result = resolver.resolve_for_element(rule, attribute, mapping,
723
- plan, options)
724
- ns_result[:prefix]
725
- type_ns_info = ns_result[:ns_info]
726
-
727
- # BUG FIX #49: Check if child element is in same namespace as parent
728
- # If yes, inherit parent's format (default vs prefix)
729
-
730
- # Get parent's namespace URI
731
- parent_ns_class = options[:parent_namespace_class]
732
- parent_ns_decl = options[:parent_ns_decl]
733
- parent_ns_uri = parent_ns_class&.uri
734
-
735
- # Get child's resolved namespace URI
736
- child_ns_uri = ns_result[:uri]
737
-
738
- # Initialize resolved_prefix from namespace resolution
739
- resolved_prefix = ns_result[:prefix]
740
-
741
- # CRITICAL FIX FOR NATIVE TYPE NAMESPACE INHERITANCE:
742
- # Elements without explicit namespace declaration should NOT inherit
743
- # parent's prefix format. They should be in blank namespace.
744
- #
745
- # Check if this is a native type without explicit namespace:
746
- # 1. No namespace directive on the mapping rule
747
- # 2. Attribute type doesn't have namespace_class (native type like :string)
748
- element_has_no_explicit_ns = !rule.namespace_set?
749
- type_class = attribute&.type(register)
750
- type_has_no_ns = !(type_class.is_a?(Class) && type_class <= Lutaml::Model::Type::Value) ||
751
- !type_class&.namespace_class
752
-
753
- # If native type with no explicit namespace, DON'T inherit parent's prefix
754
- if element_has_no_explicit_ns && type_has_no_ns
755
- # Native type - force blank namespace (no prefix)
756
- resolved_prefix = nil
757
- # Check if parent uses default format - if so, need xmlns="" to opt out
758
- parent_ns_decl&.default_format?
759
- # Only inherit format if child is in SAME namespace as parent (matching URIs)
760
- elsif parent_ns_class && parent_ns_decl &&
761
- child_ns_uri && parent_ns_uri &&
762
- child_ns_uri == parent_ns_uri
763
- # Same namespace URI - inherit parent's format
764
- resolved_prefix = if parent_ns_decl.prefix_format?
765
- parent_ns_decl.prefix
766
- else
767
- # Parent uses default format, child should too (no prefix)
768
- nil
769
- end
770
- end
771
-
772
- # Prepare attributes with xmlns if needed
773
- attributes = {}
774
-
775
- # W3C COMPLIANCE: Use resolver to determine xmlns="" requirement
776
- if resolver.xmlns_blank_required?(ns_result, parent_uses_default_ns)
777
- attributes["xmlns"] = ""
778
- end
779
-
780
- # Check if this namespace needs local declaration (out of scope)
781
- if resolved_prefix && plan&.namespaces
782
- ns_entry = plan.namespaces.values.find do |ns_decl|
783
- ns_decl.ns_object.prefix_default == resolved_prefix ||
784
- (type_ns_info && type_ns_info[:uri] && ns_decl.ns_object.uri == type_ns_info[:uri])
785
- end
786
-
787
- if ns_entry&.local_on_use?
788
- xmlns_attr = resolved_prefix ? "xmlns:#{resolved_prefix}" : "xmlns"
789
- attributes[xmlns_attr] = ns_entry.ns_object.uri
790
- end
791
- end
792
-
793
- if value.nil?
794
- xml.create_and_add_element(rule.name,
795
- attributes: attributes.merge({ "xsi:nil" => true }),
796
- prefix: resolved_prefix)
797
- elsif ::Lutaml::Model::Utils.empty?(value)
798
- xml.create_and_add_element(rule.name,
799
- attributes: attributes.empty? ? nil : attributes,
800
- prefix: resolved_prefix)
801
- elsif rule.raw_mapping?
802
- xml.add_xml_fragment(xml, value)
803
- else
804
- xml.create_and_add_element(rule.name,
805
- attributes: attributes.empty? ? nil : attributes,
806
- prefix: resolved_prefix) do
807
- add_value(xml, value, attribute, cdata: rule.cdata)
808
- end
809
- end
810
- end
811
-
812
- def attributes_hash(element)
813
- result = Lutaml::Model::MappingHash.new
814
-
815
- element.attributes_each_value do |attr|
816
- if attr.name == "schemaLocation"
817
- result["__schema_location"] = {
818
- namespace: attr.namespace,
819
- prefix: attr.namespace.prefix,
820
- schema_location: attr.value,
821
- }
822
- else
823
- result[self.class.namespaced_attr_name(attr)] = attr.value
824
- end
825
- end
826
-
827
- result
828
- end
829
-
830
- # NOTE: name_of, prefixed_name_of, namespaced_attr_name, namespaced_name_of
831
- # are provided by AdapterHelpers module via extend
832
-
833
- def self.text_of(element)
834
- element.text
835
- end
836
-
837
- def order
838
- children.filter_map do |child|
839
- if child.text?
840
- next if child.text.nil?
841
-
842
- Element.new("Text", "text", text_content: child.text)
843
- elsif child.comment?
844
- Element.new("Comment", "comment",
845
- text_content: child.content,
846
- node_type: :comment)
847
- else
848
- Element.new("Element", child.unprefixed_name)
849
- end
850
- end
851
- end
852
-
853
- # order_of is inherited from BaseAdapter (delegates to element.order)
11
+ MOXML_ADAPTER = Moxml::Adapter::Oga
12
+ BUILDER_CLASS = Builder::Oga
13
+ PARSED_ELEMENT_CLASS = Oga::Element
854
14
  end
855
15
  end
856
16
  end