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