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
@@ -2,7 +2,12 @@
2
2
 
3
3
  require_relative "../document"
4
4
  require_relative "../declaration_handler"
5
+ require_relative "../doctype_extractor"
5
6
  require_relative "../polymorphic_value_handler"
7
+ require_relative "xml_parser"
8
+ require_relative "xml_serializer"
9
+ require_relative "plan_based_builder"
10
+ require_relative "namespace_uri_collector"
6
11
 
7
12
  module Lutaml
8
13
  module Xml
@@ -14,56 +19,26 @@ module Lutaml
14
19
  # consistent behavior across adapters.
15
20
  #
16
21
  # Subclasses must implement:
17
- # - self.parse(xml, options) - Parse XML string to document
18
- # - to_xml(options) - Serialize document to XML string
22
+ # - MOXML_ADAPTER - Moxml adapter implementation for parsing
23
+ # - BUILDER_CLASS - Builder implementation for serialization
24
+ # - PARSED_ELEMENT_CLASS - Adapter element class returned by parsing
19
25
  #
20
26
  # @abstract Subclass and implement required methods
21
27
  class BaseAdapter < Document
28
+ extend DocTypeExtractor
29
+ extend AdapterHelpers
30
+ extend XmlParser
22
31
  include DeclarationHandler
23
32
  include PolymorphicValueHandler
33
+ include NamespaceUriCollector
34
+ include XmlSerializer
35
+ include PlanBasedBuilder
24
36
 
25
- # Class methods for element inspection
26
- # These are shared across all adapters
27
-
28
- # Get the local name of an element
29
- #
30
- # @param element [Object] the element to inspect
31
- # @return [String] the element's local name
32
- def self.name_of(element)
33
- element.name
34
- end
35
-
36
- # Get the prefixed name of an element
37
- #
38
- # @param node [Object] the element node
39
- # @return [String] the prefixed name (prefix:localname)
40
- def self.prefixed_name_of(node)
41
- node.prefixed_name
42
- end
43
-
44
- # Get the text content of an element
45
- #
46
- # @param element [Object] the element to get text from
47
- # @return [String] the text content
48
- def self.text_of(element)
49
- element.text
50
- end
51
-
52
- # Get the namespaced name of an element
53
- #
54
- # @param element [Object] the element to inspect
55
- # @return [String] the namespaced name
56
- def self.namespaced_name_of(element)
57
- element.namespaced_name
58
- end
59
-
60
- # Get the order of child elements
61
- #
62
- # @param element [Object] the parent element
63
- # @return [Array] ordered list of children
64
- def self.order_of(element)
65
- element.order
66
- end
37
+ EMPTY_DOCUMENT_ERROR_MESSAGE = "Document has no root element. " \
38
+ "The XML may be empty, contain only whitespace, " \
39
+ "or consist only of an XML declaration."
40
+ EMPTY_DOCUMENT_ERROR_TYPE = :invalid_format
41
+ PARSE_ERROR_CLASS = nil
67
42
 
68
43
  # Convert a Formal Public Identifier (FPI) to a URN per RFC 3151.
69
44
  # FPI examples: "-//OASIS//DTD XML Exchange Table Model 19990315//EN"
@@ -86,11 +61,6 @@ module Lutaml
86
61
  uri.is_a?(String) && uri.start_with?("-//", "+//")
87
62
  end
88
63
 
89
- # Extract processing instructions from a moxml document that appear
90
- # before the root element.
91
- #
92
- # @param moxml_doc [Moxml::Document] the parsed document
93
- # @return [Array<Lutaml::Xml::DataModel::XmlProcessingInstruction>]
94
64
  def self.extract_document_processing_instructions(moxml_doc)
95
65
  pis = []
96
66
  root = moxml_doc.root
@@ -105,15 +75,6 @@ module Lutaml
105
75
  pis
106
76
  end
107
77
 
108
- # Build a namespaced attribute name
109
- #
110
- # @param prefix [String, nil] the namespace prefix
111
- # @param name [String] the attribute name
112
- # @return [String] the qualified attribute name
113
- def self.namespaced_attr_name(prefix, name)
114
- prefix ? "#{prefix}:#{name}" : name
115
- end
116
-
117
78
  # Build a namespaced element name
118
79
  #
119
80
  # @param namespace_uri [String, nil] the namespace URI
@@ -142,6 +103,8 @@ module Lutaml
142
103
  options[:encoding]
143
104
  elsif options.key?(:parse_encoding)
144
105
  options[:parse_encoding]
106
+ elsif @encoding && @encoding.to_s.upcase != "ASCII-8BIT"
107
+ @encoding
145
108
  else
146
109
  "UTF-8"
147
110
  end
@@ -179,6 +142,10 @@ module Lutaml
179
142
  false
180
143
  end
181
144
 
145
+ def order
146
+ root.order
147
+ end
148
+
182
149
  # Get attribute definition for an element and rule
183
150
  #
184
151
  # @param element [Object] the model instance
@@ -240,430 +207,53 @@ module Lutaml
240
207
  def attributes_hash(element)
241
208
  result = Lutaml::Model::MappingHash.new
242
209
 
243
- element.attributes.each_value do |attr|
244
- if attr.unprefixed_name == "schemaLocation"
210
+ attribute_values(element) do |attr|
211
+ if schema_location_attribute?(attr)
245
212
  result["__schema_location"] = {
246
213
  namespace: attr.namespace,
247
- prefix: attr.namespace_prefix,
214
+ prefix: attribute_namespace_prefix(attr),
248
215
  schema_location: attr.value,
249
216
  }
250
217
  else
251
- result[attr.namespaced_name] = attr.value
218
+ result[attribute_hash_name(attr)] = attr.value
252
219
  end
253
220
  end
254
221
 
255
222
  result
256
223
  end
257
224
 
258
- # Add text content to XML builder
259
- #
260
- # @param xml [Builder] the XML builder
261
- # @param value [Object] the value to add
262
- # @param attribute [Attribute, nil] the attribute definition
263
- # @param cdata [Boolean] whether to use CDATA
264
- def add_value(xml, value, attribute, cdata: false)
265
- if !value.nil?
266
- if attribute.nil?
267
- # For delegated attributes where attribute is nil, just use the raw value
268
- xml.add_text(xml, value.to_s, cdata: cdata)
269
- elsif attribute.transform.is_a?(Class) && attribute.transform < Lutaml::Model::ValueTransformer
270
- # Value has already been transformed, use it directly
271
- xml.add_text(xml, value.to_s, cdata: cdata)
272
- else
273
- # Normal serialization through attribute type system
274
- serialized_value = attribute.serialize(value, :xml, register)
275
- if attribute.raw?
276
- xml.add_xml_fragment(xml, value)
277
- elsif serialized_value.is_a?(Hash)
278
- serialized_value.each do |key, val|
279
- xml.create_and_add_element(key) do |element|
280
- element.text(val)
281
- end
282
- end
283
- else
284
- xml.add_text(xml, serialized_value, cdata: cdata)
285
- end
286
- end
287
- end
288
- end
289
-
290
- # Get child plan from parent plan (unified access for both object and hash plans)
291
- #
292
- # @param plan [DeclarationPlan, Hash, nil] the parent plan
293
- # @param attr_name [Symbol] the attribute name
294
- # @return [DeclarationPlan, Hash, nil] the child plan or nil
295
- def child_plan_for(plan, attr_name)
296
- return nil unless plan
297
-
298
- if plan.respond_to?(:child_plan)
299
- # DeclarationPlan object (Nokogiri/Oga)
300
- plan.child_plan(attr_name)
301
- elsif plan.respond_to?(:[])
302
- # Hash-based plan (Ox/REXML)
303
- plan[:children_plans]&.[](attr_name)
304
- end
305
- end
306
-
307
- # Build unordered child elements using prepared namespace declaration plan
308
- #
309
- # This is the shared implementation for all adapters. Adapters may override
310
- # if they need custom behavior.
311
- #
312
- # @param xml [Builder] the XML builder
313
- # @param element [Object] the model instance
314
- # @param plan [DeclarationPlan, Hash] the declaration plan
315
- # @param options [Hash] serialization options
316
- def build_unordered_children_with_plan(xml, element, plan, options)
317
- mapper_class = options[:mapper_class] || element.class
318
- xml_mapping = mapper_class.mappings_for(:xml)
319
-
320
- # Process child elements with their plans (INCLUDING raw_mapping for map all)
321
- mappings = xml_mapping.elements + [xml_mapping.raw_mapping].compact
322
- mappings.each do |element_rule|
323
- next if options[:except]&.include?(element_rule.to)
324
-
325
- # Handle custom methods
326
- if element_rule.custom_methods[:to]
327
- mapper_class.new.send(element_rule.custom_methods[:to], element,
328
- xml.parent, xml)
329
- next
330
- end
331
-
332
- attribute_def = attribute_definition_for(element, element_rule,
333
- mapper_class: mapper_class)
334
-
335
- # For delegated attributes, attribute_def might be nil
336
- next unless attribute_def || element_rule.delegate
337
-
338
- value = attribute_value_for(element, element_rule)
339
- next unless element_rule.render?(value, element)
340
-
341
- # Get child's plan if available
342
- child_plan = child_plan_for(plan, element_rule.to)
343
-
344
- # Check if value is a Collection instance
345
- is_collection_instance = value.is_a?(Lutaml::Model::Collection)
346
-
347
- if value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
348
- handle_nested_elements_with_plan(
349
- xml,
350
- value,
351
- element_rule,
352
- attribute_def,
353
- child_plan,
354
- options,
355
- parent_plan: plan,
356
- )
357
- elsif element_rule.delegate && attribute_def.nil?
358
- # Handle non-model values (strings, etc.) for delegated attributes
359
- add_simple_value(xml, element_rule, value, nil, plan: plan,
360
- mapping: xml_mapping, options: options)
361
- else
362
- add_simple_value(xml, element_rule, value, attribute_def,
363
- plan: plan, mapping: xml_mapping, options: options)
364
- end
365
- end
366
-
367
- # Process content mapping
368
- process_content_mapping(element, xml_mapping.content_mapping,
369
- xml, mapper_class)
370
- end
371
-
372
- # Build ordered child elements using prepared namespace declaration plan
373
- #
374
- # This is the shared implementation for all adapters. Adapters may override
375
- # if they need custom behavior.
376
- #
377
- # @param xml [Builder] the XML builder
378
- # @param element [Object] the model instance
379
- # @param plan [DeclarationPlan, Hash] the declaration plan
380
- # @param options [Hash] serialization options
381
- def build_ordered_element_with_plan(xml, element, plan, options)
382
- mapper_class = options[:mapper_class] || element.class
383
- xml_mapping = mapper_class.mappings_for(:xml)
384
-
385
- index_hash = {}
386
- content = []
387
-
388
- element.element_order.each do |object|
389
- object_key = "#{object.name}-#{object.type}"
390
- index_hash[object_key] ||= -1
391
- curr_index = index_hash[object_key] += 1
392
-
393
- element_rule = xml_mapping.find_by_name(object.name,
394
- type: object.type,
395
- node_type: object.node_type,
396
- namespace_uri: object.namespace_uri)
397
- next if element_rule.nil? || options[:except]&.include?(element_rule.to)
398
-
399
- # Handle custom methods
400
- if element_rule.custom_methods[:to]
401
- mapper_class.new.send(element_rule.custom_methods[:to], element,
402
- xml.parent, xml)
403
- next
404
- end
405
-
406
- # Get attribute definition and value (handle delegation)
407
- attribute_def, value = fetch_attribute_and_value(element,
408
- element_rule, mapper_class)
409
-
410
- next if element_rule == xml_mapping.content_mapping && element_rule.cdata && object.text?
411
-
412
- if element_rule == xml_mapping.content_mapping
413
- process_ordered_content(element, xml_mapping, xml, curr_index,
414
- content)
415
- elsif !value.nil? || element_rule.render_nil?
416
- process_ordered_element(xml, element, element_rule, attribute_def,
417
- value, curr_index, plan, xml_mapping, options)
418
- end
419
- end
420
-
421
- add_ordered_content(xml, content) unless content.empty?
422
- end
423
-
424
225
  private
425
226
 
426
- # Add text or CDATA content to a moxml element.
427
- # Nokogiri overrides add_text_nodes for entity reference preservation.
428
- def add_content_node(element, text, doc, cdata: false)
429
- if cdata
430
- element.add_child(doc.create_cdata(text.to_s))
227
+ def attribute_values(element, &)
228
+ if element.respond_to?(:attributes_each_value)
229
+ element.attributes_each_value(&)
431
230
  else
432
- add_text_nodes(element, text.to_s, doc)
231
+ element.attributes.each_value(&)
433
232
  end
434
233
  end
435
234
 
436
- # Create text node(s) for element content.
437
- # Default: single text node. Nokogiri overrides to split entity references.
438
- def add_text_nodes(element, text, doc)
439
- element.add_child(doc.create_text(text))
235
+ def schema_location_attribute?(attr)
236
+ attr_name = if attr.respond_to?(:unprefixed_name)
237
+ attr.unprefixed_name
238
+ else
239
+ attr.name
240
+ end
241
+ attr_name == "schemaLocation"
440
242
  end
441
243
 
442
- # Apply XML attributes from XmlElement to a moxml element,
443
- # filtering xmlns attributes that are already declared via hoisted_declarations.
444
- def apply_plan_attributes(xml_element, element_node, element)
445
- xml_element.attributes.each_with_index do |xml_attr, idx|
446
- attr_name_str = xml_attr.name.to_s
447
- if attr_name_str.start_with?("xmlns")
448
- apply_xmlns_attribute(attr_name_str, xml_attr.value.to_s,
449
- element_node, element)
450
- next
451
- end
452
-
453
- attr_node = element_node.attribute_nodes[idx]
454
- element[attr_node.qualified_name] = xml_attr.value.to_s
455
- end
456
- end
457
-
458
- def apply_xmlns_attribute(attr_name_str, value, element_node, element)
459
- if attr_name_str.include?(":")
460
- prefix = attr_name_str.split(":", 2).last
461
- unless element_node.hoisted_declarations.key?(prefix)
462
- element.add_namespace(prefix, value)
463
- end
464
- elsif attr_name_str == "xmlns"
465
- unless element_node.hoisted_declarations.key?(nil)
466
- element.add_namespace(nil, value)
467
- end
468
- end
469
- end
470
-
471
- # Fetch attribute definition and value, handling delegation
472
- #
473
- # @param element [Object] the model instance
474
- # @param element_rule [MappingRule] the mapping rule
475
- # @param mapper_class [Class] the mapper class
476
- # @return [Array<(Attribute, Object)>] attribute definition and value tuple
477
- def fetch_attribute_and_value(element, element_rule, mapper_class)
478
- attribute_def = nil
479
- value = nil
480
-
481
- if element_rule.delegate
482
- delegate_obj = element.send(element_rule.delegate)
483
- if delegate_obj.respond_to?(element_rule.to)
484
- attribute_def = delegate_obj.class.attributes[element_rule.to]
485
- value = delegate_obj.send(element_rule.to)
486
- end
487
- else
488
- attribute_def = attribute_definition_for(element, element_rule,
489
- mapper_class: mapper_class)
490
- value = attribute_value_for(element, element_rule)
491
- end
492
-
493
- [attribute_def, value]
494
- end
495
-
496
- # Process content for ordered elements
497
- #
498
- # @param element [Object] the model instance
499
- # @param xml_mapping [Xml::Mapping] the XML mapping
500
- # @param xml [Builder] the XML builder
501
- # @param curr_index [Integer] current index in collection
502
- # @param content [Array] accumulated content strings
503
- def process_ordered_content(element, xml_mapping, xml, curr_index,
504
- content)
505
- text = element.send(xml_mapping.content_mapping.to)
506
- text = text[curr_index] if text.is_a?(Array)
507
-
508
- if element.mixed?
509
- add_mixed_text(xml, text)
244
+ def attribute_namespace_prefix(attr)
245
+ if attr.respond_to?(:namespace_prefix)
246
+ attr.namespace_prefix
510
247
  else
511
- content << text
248
+ attr.namespace&.prefix
512
249
  end
513
250
  end
514
251
 
515
- # Process a single ordered element
516
- #
517
- # @param xml [Builder] the XML builder
518
- # @param element [Object] the model instance
519
- # @param element_rule [MappingRule] the mapping rule
520
- # @param attribute_def [Attribute, nil] the attribute definition
521
- # @param value [Object] the value
522
- # @param curr_index [Integer] current index in collection
523
- # @param plan [DeclarationPlan, Hash] the declaration plan
524
- # @param xml_mapping [Xml::Mapping] the XML mapping
525
- # @param options [Hash] serialization options
526
- def process_ordered_element(xml, element, element_rule, attribute_def,
527
- value, curr_index, plan, xml_mapping, options)
528
- # Handle collection values by index
529
- current_value = if attribute_def&.collection? && value.is_a?(Array)
530
- value[curr_index]
531
- elsif attribute_def&.collection? && value.is_a?(Lutaml::Model::Collection)
532
- value.to_a[curr_index]
533
- else
534
- value
535
- end
536
-
537
- # Get child's plan if available
538
- child_plan = child_plan_for(plan, element_rule.to)
539
-
540
- is_collection_instance = current_value.is_a?(Lutaml::Model::Collection)
541
-
542
- if current_value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
543
- handle_nested_elements_with_plan(
544
- xml,
545
- current_value,
546
- element_rule,
547
- attribute_def,
548
- child_plan,
549
- options,
550
- parent_plan: plan,
551
- )
252
+ def attribute_hash_name(attr)
253
+ if attr.respond_to?(:namespaced_name)
254
+ attr.namespaced_name
552
255
  else
553
- # Apply transformations if attribute_def exists
554
- if attribute_def
555
- current_value = ExportTransformer.call(current_value,
556
- element_rule, attribute_def, format: :xml)
557
- end
558
-
559
- # For mixed content, create elements directly
560
- if element.mixed? && !attribute_def&.raw?
561
- add_mixed_element(xml, element_rule, current_value, attribute_def,
562
- plan: plan, mapping: xml_mapping)
563
- else
564
- add_simple_value(xml, element_rule, current_value,
565
- attribute_def, plan: plan, mapping: xml_mapping, options: options)
566
- end
567
- end
568
- end
569
-
570
- # Add text for mixed content (can be overridden by adapters)
571
- #
572
- # @param xml [Builder] the XML builder
573
- # @param text [String] the text to add
574
- def add_mixed_text(xml, text)
575
- # Default implementation - adapters may override
576
- xml.add_text(xml, text) unless text.nil? || text.to_s.empty?
577
- end
578
-
579
- # Add element for mixed content (can be overridden by adapters)
580
- #
581
- # @param xml [Builder] the XML builder
582
- # @param element_rule [MappingRule] the element rule
583
- # @param value [Object] the value to add
584
- # @param attribute [Attribute, nil] the attribute definition
585
- # @param plan [DeclarationPlan, Hash, nil] the declaration plan
586
- # @param mapping [Xml::Mapping] the XML mapping
587
- def add_mixed_element(xml, element_rule, value, _attribute, _plan:,
588
- _mapping:)
589
- # Default implementation - adapters may override
590
- xml.create_and_add_element(element_rule.name) do |child_element|
591
- child_element.text(value.to_s) unless ::Lutaml::Model::Utils.empty?(value)
592
- end
593
- end
594
-
595
- # Add accumulated content (can be overridden by adapters)
596
- #
597
- # @param xml [Builder] the XML builder
598
- # @param content [Array<String>] accumulated content strings
599
- def add_ordered_content(xml, content)
600
- # Default implementation - adapters may override
601
- xml.add_text(xml, content.join)
602
- end
603
-
604
- # Collect original namespace URIs from a model tree for namespace alias support.
605
- #
606
- # When parsing XML with alias URIs (e.g., "http://.../") against a namespace
607
- # class with canonical URI (e.g., "http://.../reqif.xsd"), the original alias
608
- # URI is stored on the model instance as @__xml_original_namespace_uri.
609
- # This method collects all such mappings from the model tree.
610
- #
611
- # @param model [Object] the model instance to walk
612
- # @param mapping [Xml::Mapping, nil] the mapping for the model
613
- # @return [Hash<String, String>] Mapping of canonical URI => original alias URI
614
- def collect_original_namespace_uris(model, mapping = nil)
615
- original_uris = {}
616
- return original_uris unless model
617
-
618
- collect_from_model(model, mapping, original_uris, Set.new)
619
- original_uris
620
- end
621
-
622
- # Recursively walk model tree to collect original namespace URIs
623
- def collect_from_model(model, mapping, original_uris, visited)
624
- return unless model.is_a?(::Lutaml::Model::Serialize)
625
- return if visited.include?(model.object_id)
626
-
627
- visited.add(model.object_id)
628
-
629
- # Check if this model has an original namespace URI
630
- if model.respond_to?(:original_namespace_uri) && model.original_namespace_uri
631
- original_uri = model.original_namespace_uri
632
- if original_uri && !original_uri.empty?
633
- # Look up the model's namespace class
634
- ns_class = model.class.mappings_for(:xml)&.namespace_class
635
- if ns_class && ns_class.uri != original_uri
636
- # Only store if the canonical URI differs (it's an alias)
637
- original_uris[ns_class.uri] = original_uri
638
- end
639
- end
640
- end
641
-
642
- return unless mapping
643
-
644
- # Recurse into child Serializable attributes
645
- attributes = model.class.attributes
646
- mapping.elements.each do |elem_rule|
647
- attr_def = attributes[elem_rule.to]
648
- next unless attr_def
649
-
650
- child_type = attr_def.type(Lutaml::Model::Config.default_register)
651
- next unless child_type.respond_to?(:<) && child_type < ::Lutaml::Model::Serializable
652
-
653
- child_mapping = child_type.mappings_for(:xml)
654
- next unless child_mapping
655
-
656
- child_instance = model.public_send(elem_rule.to) if model.respond_to?(elem_rule.to)
657
-
658
- if child_instance.is_a?(Array) || child_instance.is_a?(::Lutaml::Model::Collection)
659
- instances = child_instance.is_a?(::Lutaml::Model::Collection) ? child_instance.collection : child_instance
660
- instances.each do |item|
661
- collect_from_model(item, child_mapping, original_uris, visited)
662
- end
663
- elsif child_instance
664
- collect_from_model(child_instance, child_mapping, original_uris,
665
- visited)
666
- end
256
+ self.class.namespaced_attr_name(attr)
667
257
  end
668
258
  end
669
259
  end
@@ -30,23 +30,6 @@ module Lutaml
30
30
  @prefix = normalize_prefix(prefix)
31
31
  end
32
32
 
33
- # Generate unique key for this namespace configuration
34
- #
35
- # The key is based on prefix and URI, ensuring that same config = same key.
36
- # This enables proper deduplication and lookup in hash structures.
37
- #
38
- # @return [String] unique key in format "prefix:uri" or ":uri" for default
39
- def self.to_key
40
- prefix = prefix_default
41
- uri = self.uri
42
-
43
- if prefix && !prefix.empty?
44
- "#{prefix}:#{uri}"
45
- else
46
- ":#{uri}"
47
- end
48
- end
49
-
50
33
  def normalize_prefix(prefix)
51
34
  # Only strip "xmlns:" prefix (e.g., "xmlns:foo" → "foo").
52
35
  # Do NOT strip "xmlns" from prefixes like "xmlns_1.0" (valid NCName).
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Adapter
6
+ # Collects original namespace URIs from a model tree for namespace alias support.
7
+ #
8
+ # When parsing XML with alias URIs (e.g., "http://.../") against a namespace
9
+ # class with canonical URI (e.g., "http://.../reqif.xsd"), the original alias
10
+ # URI is stored on the model instance as @__xml_original_namespace_uri.
11
+ # This module collects all such mappings from the model tree.
12
+ module NamespaceUriCollector
13
+ # @param model [Object] the model instance to walk
14
+ # @param mapping [Xml::Mapping, nil] the mapping for the model
15
+ # @return [Hash<String, String>] Mapping of canonical URI => original alias URI
16
+ def collect_original_namespace_uris(model, mapping = nil)
17
+ original_uris = {}
18
+ return original_uris unless model
19
+
20
+ collect_from_model(model, mapping, original_uris, Set.new)
21
+ original_uris
22
+ end
23
+
24
+ private
25
+
26
+ def collect_from_model(model, mapping, original_uris, visited)
27
+ return unless model.is_a?(::Lutaml::Model::Serialize)
28
+ return if visited.include?(model.object_id)
29
+
30
+ visited.add(model.object_id)
31
+
32
+ if model.respond_to?(:original_namespace_uri) && model.original_namespace_uri
33
+ original_uri = model.original_namespace_uri
34
+ if original_uri && !original_uri.empty?
35
+ ns_class = model.class.mappings_for(:xml)&.namespace_class
36
+ if ns_class && ns_class.uri != original_uri
37
+ original_uris[ns_class.uri] = original_uri
38
+ end
39
+ end
40
+ end
41
+
42
+ return unless mapping
43
+
44
+ attributes = model.class.attributes
45
+ mapping.elements.each do |elem_rule|
46
+ attr_def = attributes[elem_rule.to]
47
+ next unless attr_def
48
+
49
+ child_type = attr_def.type(Lutaml::Model::Config.default_register)
50
+ next unless child_type.respond_to?(:<) && child_type < ::Lutaml::Model::Serializable
51
+
52
+ child_mapping = child_type.mappings_for(:xml)
53
+ next unless child_mapping
54
+
55
+ child_instance = model.public_send(elem_rule.to) if model.respond_to?(elem_rule.to)
56
+
57
+ if child_instance.is_a?(Array) || child_instance.is_a?(::Lutaml::Model::Collection)
58
+ instances = child_instance.is_a?(::Lutaml::Model::Collection) ? child_instance.collection : child_instance
59
+ instances.each do |item|
60
+ collect_from_model(item, child_mapping, original_uris, visited)
61
+ end
62
+ elsif child_instance
63
+ collect_from_model(child_instance, child_mapping, original_uris,
64
+ visited)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end