lutaml-model 0.8.11 → 0.8.13

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/opal.yml +31 -0
  3. data/.rspec-opal +5 -0
  4. data/.rubocop_todo.yml +45 -34
  5. data/README.adoc +126 -104
  6. data/RELEASE_NOTES.adoc +3 -3
  7. data/benchmark/quick_benchmark.rb +2 -2
  8. data/benchmark/serialization_benchmark.rb +4 -4
  9. data/docs/_guides/advanced-mapping.adoc +1 -1
  10. data/docs/_guides/character-encoding.adoc +3 -3
  11. data/docs/_guides/index.adoc +4 -0
  12. data/docs/_guides/missing-values-handling.adoc +6 -6
  13. data/docs/_guides/ooxml-examples.adoc +7 -7
  14. data/docs/_guides/opal.adoc +221 -0
  15. data/docs/_guides/value-transformations.adoc +7 -7
  16. data/docs/_guides/xml/namespace-presentation.adoc +1 -1
  17. data/docs/_guides/xml/namespace-semantics.adoc +15 -15
  18. data/docs/_guides/xml/type-namespaces.adoc +9 -9
  19. data/docs/_guides/xml-mapping.adoc +32 -26
  20. data/docs/_guides/xml-namespace-qualification.adoc +4 -4
  21. data/docs/_guides/xml-namespaces.adoc +2 -2
  22. data/docs/_guides/xml_mappings/04_xml_namespace_class.adoc +18 -18
  23. data/docs/_guides/xml_mappings/05_common_patterns.adoc +16 -16
  24. data/docs/_guides/xml_mappings/06_migration_guide.adoc +5 -5
  25. data/docs/_guides/xml_mappings/07_best_practices.adoc +13 -12
  26. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +2 -2
  27. data/docs/_pages/attributes.adoc +2 -2
  28. data/docs/_pages/collections.adoc +26 -20
  29. data/docs/_pages/configuration.adoc +9 -4
  30. data/docs/_pages/consolidation-mapping.adoc +4 -4
  31. data/docs/_pages/importable_models.adoc +14 -13
  32. data/docs/_pages/index.adoc +1 -0
  33. data/docs/_pages/quick-start.adoc +1 -1
  34. data/docs/_pages/serialization_adapters.adoc +3 -2
  35. data/docs/_pages/value_types.adoc +10 -10
  36. data/docs/_references/custom_registers.adoc +7 -7
  37. data/docs/_references/format-independent-features.adoc +4 -4
  38. data/docs/_references/instance-serialization.adoc +1 -1
  39. data/docs/_references/parent-root-context.adoc +3 -3
  40. data/docs/_tutorials/basic-model-definition.adoc +1 -1
  41. data/docs/_tutorials/first-xml-serialization.adoc +4 -4
  42. data/docs/_tutorials/lutaml-xml-architecture.adoc +4 -4
  43. data/docs/_tutorials/validation-basics.adoc +1 -1
  44. data/docs/_tutorials/working-with-collections.adoc +2 -2
  45. data/docs/_tutorials/xml-namespaces-basics.adoc +1 -1
  46. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +29 -29
  47. data/docs/cli_compare.adoc +1 -1
  48. data/docs/index.adoc +2 -1
  49. data/docs/namespace-management.adoc +14 -14
  50. data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
  51. data/lib/lutaml/json/adapter/mapping.rb +2 -4
  52. data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
  53. data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
  54. data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
  55. data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
  56. data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
  57. data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
  58. data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
  59. data/lib/lutaml/key_value/mapping.rb +35 -10
  60. data/lib/lutaml/model/adapter_resolver.rb +5 -8
  61. data/lib/lutaml/model/collection.rb +11 -11
  62. data/lib/lutaml/model/error/no_root_mapping_error.rb +6 -5
  63. data/lib/lutaml/model/error/no_root_namespace_error.rb +6 -5
  64. data/lib/lutaml/model/error/type_only_mapping_error.rb +13 -0
  65. data/lib/lutaml/model/error/type_only_namespace_error.rb +12 -0
  66. data/lib/lutaml/model/mapping/mapping.rb +12 -0
  67. data/lib/lutaml/model/version.rb +1 -1
  68. data/lib/lutaml/model.rb +3 -0
  69. data/lib/lutaml/toml/adapter/mapping.rb +2 -4
  70. data/lib/lutaml/xml/adapter/base_adapter.rb +0 -9
  71. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +0 -1
  72. data/lib/lutaml/xml/adapter/oga_adapter.rb +0 -1
  73. data/lib/lutaml/xml/adapter/ox_adapter.rb +0 -1
  74. data/lib/lutaml/xml/adapter/rexml_adapter.rb +0 -1
  75. data/lib/lutaml/xml/adapter/xml_serializer.rb +42 -22
  76. data/lib/lutaml/xml/adapter.rb +4 -0
  77. data/lib/lutaml/xml/builder/base.rb +64 -25
  78. data/lib/lutaml/xml/builder/nokogiri.rb +0 -2
  79. data/lib/lutaml/xml/builder/oga.rb +0 -2
  80. data/lib/lutaml/xml/builder/ox.rb +0 -2
  81. data/lib/lutaml/xml/builder/rexml.rb +0 -2
  82. data/lib/lutaml/xml/builder.rb +1 -0
  83. data/lib/lutaml/xml/configurable.rb +2 -2
  84. data/lib/lutaml/xml/declaration_handler.rb +3 -105
  85. data/lib/lutaml/xml/mapping.rb +3 -3
  86. data/lib/lutaml/xml/schema/xsd/documentation.rb +1 -1
  87. data/lib/lutaml/xml/schema/xsd.rb +5 -4
  88. data/lib/lutaml/xml/schema.rb +8 -5
  89. data/lib/lutaml/xml/serialization/collection_ext.rb +7 -7
  90. data/lib/lutaml/xml/serialization/format_conversion.rb +1 -1
  91. data/lib/lutaml/xml/serialization/instance_methods.rb +1 -1
  92. data/lib/lutaml/xml/xml_orderable.rb +17 -0
  93. data/lib/lutaml/xml.rb +9 -13
  94. data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
  95. data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
  96. data/lib/tasks/memory_profile.rb +2 -2
  97. data/lib/tasks/performance_benchmark.rb +5 -5
  98. data/lutaml-model.gemspec +1 -1
  99. data/spec/lutaml/key_value/transformation/rule_compiler_spec.rb +1 -1
  100. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +1 -1
  101. data/spec/lutaml/model/attribute_collection_spec.rb +1 -1
  102. data/spec/lutaml/model/cli_spec.rb +1 -1
  103. data/spec/lutaml/model/collection_spec.rb +1 -1
  104. data/spec/lutaml/model/collection_validation_spec.rb +6 -6
  105. data/spec/lutaml/model/consolidation_spec.rb +8 -8
  106. data/spec/lutaml/model/custom_collection_spec.rb +3 -3
  107. data/spec/lutaml/model/default_register_spec.rb +23 -23
  108. data/spec/lutaml/model/delegation_spec.rb +3 -10
  109. data/spec/lutaml/model/derived_attribute_serialization_spec.rb +1 -1
  110. data/spec/lutaml/model/dynamic_attribute_spec.rb +2 -2
  111. data/spec/lutaml/model/enum_spec.rb +1 -1
  112. data/spec/lutaml/model/group_spec.rb +12 -12
  113. data/spec/lutaml/model/lazy_collection_spec.rb +4 -4
  114. data/spec/lutaml/model/mixed_content_spec.rb +2 -2
  115. data/spec/lutaml/model/namespace_versioning_spec.rb +4 -4
  116. data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
  117. data/spec/lutaml/model/processing_instruction_spec.rb +11 -11
  118. data/spec/lutaml/model/register_methods_spec.rb +2 -2
  119. data/spec/lutaml/model/render_empty_spec.rb +1 -1
  120. data/spec/lutaml/model/serialize_perf_guard_spec.rb +1 -1
  121. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +1 -1
  122. data/spec/lutaml/model/transformation_builder_spec.rb +2 -2
  123. data/spec/lutaml/model/xml_decoupling_spec.rb +3 -3
  124. data/spec/lutaml/model/xsd_patterns_spec.rb +2 -3
  125. data/spec/lutaml/xml/adapter/order_spec.rb +1 -1
  126. data/spec/lutaml/xml/clear_parse_state_spec.rb +1 -1
  127. data/spec/lutaml/xml/content_model_validation_spec.rb +4 -2
  128. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +5 -5
  129. data/spec/lutaml/xml/enhanced_mapping_spec.rb +2 -1
  130. data/spec/lutaml/xml/entity_fragmentation_spec.rb +5 -5
  131. data/spec/lutaml/xml/indent_spec.rb +109 -0
  132. data/spec/lutaml/xml/line_ending_spec.rb +66 -0
  133. data/spec/lutaml/xml/mapping_finalization_guard_spec.rb +2 -2
  134. data/spec/lutaml/xml/model_transform_guard_spec.rb +4 -4
  135. data/spec/lutaml/xml/namespace_alias_spec.rb +4 -4
  136. data/spec/lutaml/xml/namespace_aware_parsing_spec.rb +3 -3
  137. data/spec/lutaml/xml/namespace_bound_element_roundtrip_spec.rb +2 -2
  138. data/spec/lutaml/xml/namespace_format_preservation_spec.rb +1 -1
  139. data/spec/lutaml/xml/namespace_inheritance_spec.rb +3 -3
  140. data/spec/lutaml/xml/namespace_preservation_spec.rb +5 -5
  141. data/spec/lutaml/xml/opal_xml_spec.rb +145 -0
  142. data/spec/lutaml/xml/pipeline_integration_spec.rb +145 -0
  143. data/spec/lutaml/xml/schema_primer_spec.rb +5 -5
  144. data/spec/lutaml/xml/transformation_spec.rb +20 -20
  145. data/spec/lutaml/xml/type_namespace/collector_spec.rb +1 -1
  146. data/spec/lutaml/xml/type_namespace/planner_spec.rb +3 -3
  147. data/spec/lutaml/xml/xml_spec.rb +64 -13
  148. data/spec/support/opal.rb +6 -0
  149. metadata +16 -4
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:toml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
 
16
14
  def validate!(key, to, with, render_nil, render_empty)
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:yaml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:yaml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -21,20 +21,45 @@ module Lutaml
21
21
  @finalized
22
22
  end
23
23
 
24
+ # Set the wrapper key for key-value serialization (JSON, YAML, TOML).
25
+ #
26
+ # When set, serialized output wraps all instances under this key
27
+ # (e.g., `key "items"` produces `{"items": [...]}`).
28
+ # When not called (or called with nil), instances are serialized directly
29
+ # at the top level (e.g., `[...]`).
30
+ #
31
+ # @param name [String, nil] the wrapper key name
32
+ def key(name = nil)
33
+ @key = name
34
+ end
35
+
36
+ def key_name
37
+ @key
38
+ end
39
+
40
+ # @deprecated Use {#key} instead. In key-value formats, the wrapper is a key, not a root.
24
41
  def root(name = nil)
25
- @root = name
42
+ @key = name
26
43
  end
27
44
 
45
+ # @deprecated Omit key call instead. Not calling key means no wrapper key.
28
46
  def no_root
29
- @root = nil
47
+ @key = nil
48
+ end
49
+
50
+ # Returns true when no wrapper key is set (instances serialized at top level).
51
+ def no_key?
52
+ @key.nil?
30
53
  end
31
54
 
55
+ # @deprecated Use {#no_key?} instead.
32
56
  def no_root?
33
- @root.nil?
57
+ no_key?
34
58
  end
35
59
 
60
+ # @deprecated Use {#key_name} instead.
36
61
  def root_name
37
- @root
62
+ @key
38
63
  end
39
64
 
40
65
  def map(
@@ -104,7 +129,7 @@ module Lutaml
104
129
 
105
130
  def map_instances(to:, polymorphic: {})
106
131
  @instance = to
107
- map(root_name || to, to: to, polymorphic: polymorphic)
132
+ map(key_name || to, to: to, polymorphic: polymorphic)
108
133
  map_to_instance
109
134
  end
110
135
 
@@ -125,7 +150,7 @@ module Lutaml
125
150
  def map_to_instance
126
151
  return if !instance_mapping?
127
152
 
128
- mapping_name = name_for_mapping(nil, root_name || @instance)
153
+ mapping_name = name_for_mapping(nil, key_name || @instance)
129
154
  @mappings[mapping_name].child_mappings = @key_mapping.merge(@value_mapping)
130
155
  end
131
156
 
@@ -218,17 +243,17 @@ module Lutaml
218
243
  end
219
244
 
220
245
  # Writers for deep_dup in subclasses
221
- attr_writer :mappings, :register_mappings
246
+ attr_writer :register_mappings
222
247
 
223
248
  def deep_dup
224
- self.class.new(@format).tap do |new_mapping|
249
+ dup_instance.tap do |new_mapping|
225
250
  new_mapping.mappings = duplicate_mappings
226
251
  new_mapping.register_mappings = Lutaml::Model::Utils.deep_dup(@register_mappings)
227
252
  end
228
253
  end
229
254
 
230
- def duplicate_mappings
231
- Lutaml::Model::Utils.deep_dup(@mappings)
255
+ def dup_instance
256
+ self.class.new(@format)
232
257
  end
233
258
 
234
259
  def find_by_to(to)
@@ -370,15 +370,12 @@ module Lutaml
370
370
 
371
371
  # Detect available XML adapter.
372
372
  #
373
- # @return [Symbol, nil] :nokogiri, :ox, :oga, :rexml, or nil
373
+ # Delegates to moxml which is the authority on XML adapter
374
+ # availability and platform constraints (Opal, MRI, etc.).
375
+ #
376
+ # @return [Symbol] adapter type name
374
377
  def detect_xml_adapter
375
- return :oga if RuntimeCompatibility.opal?
376
- return :nokogiri if Utils.safe_load("nokogiri", :Nokogiri)
377
- return :ox if Utils.safe_load("ox", :Ox)
378
- return :oga if Utils.safe_load("oga", :Oga)
379
- return :rexml if Utils.safe_load("rexml", :REXML)
380
-
381
- nil
378
+ Moxml::Config.runtime_default_adapter
382
379
  end
383
380
 
384
381
  # Detect available TOML adapter.
@@ -310,8 +310,8 @@ module Lutaml
310
310
  def to(format, instance, options = {})
311
311
  mappings = mappings_for(format)
312
312
 
313
- if mappings.no_root? && collection_no_root_to?(format)
314
- collection_no_root_to(format, mappings, instance, options)
313
+ if mappings.no_root? && collection_unwrapped_to?(format)
314
+ collection_unwrapped_to(format, mappings, instance, options)
315
315
  else
316
316
  super(format, instance, options.merge(collection: true))
317
317
  end
@@ -322,7 +322,7 @@ module Lutaml
322
322
  data = super
323
323
 
324
324
  if !collection_structured_format?(format) && mappings.no_root? && !mappings.root_mapping
325
- unwrap_no_root_data(data)
325
+ unwrap_unwrapped_data(data)
326
326
  else
327
327
  data
328
328
  end
@@ -332,7 +332,7 @@ module Lutaml
332
332
  mappings = mappings_for(format)
333
333
 
334
334
  if collection_structured_format?(format) && mappings.no_root?
335
- data = wrap_no_root_input(format, mappings, data)
335
+ data = wrap_unwrapped_input(format, mappings, data)
336
336
  end
337
337
 
338
338
  super(format, data, options.merge(from_collection: true))
@@ -355,21 +355,21 @@ module Lutaml
355
355
  false
356
356
  end
357
357
 
358
- # Hook: returns true if this format handles no_root serialization specially.
358
+ # Hook: returns true if this format handles unwrapped serialization specially.
359
359
  # XML overrides to return true for :xml format.
360
- def collection_no_root_to?(_format)
360
+ def collection_unwrapped_to?(_format)
361
361
  false
362
362
  end
363
363
 
364
- # Hook for structured-format no_root serialization (e.g., XML).
364
+ # Hook for unwrapped serialization (e.g., XML).
365
365
  # XML overrides to serialize each mapping separately.
366
- def collection_no_root_to(_format, _mappings, _instance, _options)
366
+ def collection_unwrapped_to(_format, _mappings, _instance, _options)
367
367
  raise NotImplementedError
368
368
  end
369
369
 
370
- # Hook for structured-format no_root input wrapping (e.g., XML).
370
+ # Hook for wrapping unwrapped input (e.g., XML).
371
371
  # XML overrides to wrap raw data in a fake root tag.
372
- def wrap_no_root_input(_format, _mappings, data)
372
+ def wrap_unwrapped_input(_format, _mappings, data)
373
373
  data
374
374
  end
375
375
 
@@ -379,7 +379,7 @@ module Lutaml
379
379
 
380
380
  private
381
381
 
382
- def unwrap_no_root_data(data)
382
+ def unwrap_unwrapped_data(data)
383
383
  # Convert KeyValueElement to Hash if needed
384
384
  hash = data.is_a?(Hash) ? data : data.to_hash
385
385
  # Handle "__root__" wrapper for key-value formats (created by transformation)
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "type_only_mapping_error"
4
+
1
5
  module Lutaml
2
6
  module Model
3
- class NoRootMappingError < Error
4
- def initialize(model)
5
- super("#{model} has `no_root`, it allowed only for reusable models")
6
- end
7
- end
7
+ # @deprecated Use {TypeOnlyMappingError} instead.
8
+ NoRootMappingError = TypeOnlyMappingError
8
9
  end
9
10
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "type_only_namespace_error"
4
+
1
5
  module Lutaml
2
6
  module Model
3
- class NoRootNamespaceError < Error
4
- def to_s
5
- "Cannot assign namespace to `no_root`"
6
- end
7
- end
7
+ # @deprecated Use {TypeOnlyNamespaceError} instead.
8
+ NoRootNamespaceError = TypeOnlyNamespaceError
8
9
  end
9
10
  end
@@ -0,0 +1,13 @@
1
+ module Lutaml
2
+ module Model
3
+ class TypeOnlyMappingError < Error
4
+ def initialize(model)
5
+ super("#{model} is a type-only model (no element declared), " \
6
+ "it can only be used as an embedded type through a parent model.")
7
+ end
8
+ end
9
+
10
+ # @deprecated Use {TypeOnlyMappingError} instead.
11
+ NoRootMappingError = TypeOnlyMappingError
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Lutaml
2
+ module Model
3
+ class TypeOnlyNamespaceError < Error
4
+ def to_s
5
+ "Cannot assign namespace to a type-only model (no element declared)."
6
+ end
7
+ end
8
+
9
+ # @deprecated Use {TypeOnlyNamespaceError} instead.
10
+ NoRootNamespaceError = TypeOnlyNamespaceError
11
+ end
12
+ end
@@ -3,6 +3,8 @@ module Lutaml
3
3
  class Mapping
4
4
  include DeepDupable
5
5
 
6
+ attr_writer :mappings
7
+
6
8
  def initialize
7
9
  @mappings = []
8
10
  @listeners = {} # target => [Listener, ...]
@@ -11,6 +13,16 @@ module Lutaml
11
13
  @mappings_imported = ::Hash.new { |h, k| h[k] = false }
12
14
  end
13
15
 
16
+ def deep_dup
17
+ duped = self.class.new
18
+ duped.mappings = duplicate_mappings
19
+ duped
20
+ end
21
+
22
+ def duplicate_mappings
23
+ Lutaml::Model::Utils.deep_dup(@mappings)
24
+ end
25
+
14
26
  # Get listeners for a specific target (element name/key).
15
27
  #
16
28
  # @param target [String, Symbol] The element name or key
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.11"
5
+ VERSION = "0.8.13"
6
6
  end
7
7
  end
data/lib/lutaml/model.rb CHANGED
@@ -149,6 +149,7 @@ module Lutaml
149
149
  "#{__dir__}/model/error/incorrect_sequence_error"
150
150
  autoload :ChoiceUpperBoundError,
151
151
  "#{__dir__}/model/error/choice_upper_bound_error"
152
+ autoload :TypeOnlyMappingError, "#{__dir__}/model/error/type_only_mapping_error"
152
153
  autoload :NoRootMappingError, "#{__dir__}/model/error/no_root_mapping_error"
153
154
  autoload :ImportModelWithRootError,
154
155
  "#{__dir__}/model/error/import_model_with_root_error"
@@ -160,6 +161,8 @@ module Lutaml
160
161
  "#{__dir__}/model/error/choice_lower_bound_error"
161
162
  autoload :NoMappingFoundError,
162
163
  "#{__dir__}/model/error/no_mapping_found_error"
164
+ autoload :TypeOnlyNamespaceError,
165
+ "#{__dir__}/model/error/type_only_namespace_error"
163
166
  autoload :NoRootNamespaceError,
164
167
  "#{__dir__}/model/error/no_root_namespace_error"
165
168
  autoload :PolymorphicError, "#{__dir__}/model/error/polymorphic_error"
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:toml)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
 
17
15
  def validate!(key, to, with, render_nil, render_empty)
@@ -1,14 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../document"
4
- require_relative "../declaration_handler"
5
- require_relative "../doctype_extractor"
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"
11
-
12
3
  module Lutaml
13
4
  module Xml
14
5
  module Adapter
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "moxml"
4
4
  require "moxml/adapter/nokogiri"
5
- require_relative "base_adapter"
6
5
 
7
6
  module Lutaml
8
7
  module Xml
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "oga"
4
4
  require "moxml/adapter/oga"
5
- require_relative "base_adapter"
6
5
 
7
6
  module Lutaml
8
7
  module Xml
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "moxml/adapter/ox"
4
- require_relative "base_adapter"
5
4
 
6
5
  module Lutaml
7
6
  module Xml
@@ -3,7 +3,6 @@
3
3
  require "rexml/document"
4
4
  require "moxml"
5
5
  require "moxml/adapter/rexml"
6
- require_relative "base_adapter"
7
6
 
8
7
  module Lutaml
9
8
  module Xml
@@ -49,6 +49,35 @@ module Lutaml
49
49
  encoding = determine_encoding(options)
50
50
  builder_options = {}
51
51
  builder_options[:encoding] = encoding if encoding
52
+ builder_options[:line_ending] = options[:line_ending] if options.key?(:line_ending)
53
+ builder_options[:indent] = options[:indent] if options.key?(:indent)
54
+
55
+ # Pass doctype to builder for document-level insertion
56
+ doctype_to_use = options[:doctype] || @doctype
57
+ if doctype_to_use && !options[:omit_doctype]
58
+ builder_options[:doctype] = doctype_to_use
59
+ end
60
+
61
+ # Pass declaration info to builder
62
+ if should_include_declaration?(options)
63
+ builder_options[:include_declaration] = true
64
+ builder_options[:xml_declaration] = @xml_declaration || {}
65
+ if options.key?(:standalone)
66
+ if options[:standalone] == :preserve
67
+ # Keep original standalone from parsed declaration (may be nil)
68
+ else
69
+ builder_options[:xml_declaration][:standalone] = standalone_value(options[:standalone])
70
+ end
71
+ end
72
+ if options[:declaration].is_a?(String)
73
+ builder_options[:xml_declaration][:version] = options[:declaration]
74
+ elsif options[:declaration] == true
75
+ builder_options[:xml_declaration][:version] = "1.0"
76
+ end
77
+ builder_options[:xml_declaration][:encoding] = encoding if options.key?(:encoding) && encoding
78
+ elsif options[:encoding] && !options[:encoding].nil?
79
+ builder_options[:force_declaration] = true
80
+ end
52
81
 
53
82
  builder = self.class::BUILDER_CLASS.build(builder_options) do |xml|
54
83
  if root.is_a?(self.class::PARSED_ELEMENT_CLASS)
@@ -58,7 +87,7 @@ module Lutaml
58
87
  end
59
88
  end
60
89
 
61
- finalize_adapter_xml(builder.to_xml, encoding, options)
90
+ builder.to_xml
62
91
  end
63
92
 
64
93
  def build_serializable_xml(xml, options)
@@ -95,6 +124,14 @@ module Lutaml
95
124
 
96
125
  private
97
126
 
127
+ def standalone_value(value)
128
+ case value
129
+ when true then "yes"
130
+ when false then "no"
131
+ else value.to_s
132
+ end
133
+ end
134
+
98
135
  def transformable_xml_element(options)
99
136
  return root if root.is_a?(Lutaml::Xml::DataModel::XmlElement)
100
137
 
@@ -183,27 +220,8 @@ module Lutaml
183
220
  options_with_original_ns
184
221
  end
185
222
 
186
- def finalize_adapter_xml(xml_data, encoding, options)
187
- result = ""
188
- if (options[:encoding] && !options[:encoding].nil?) ||
189
- should_include_declaration?(options)
190
- result += generate_declaration(options)
191
- end
192
-
193
- doctype_to_use = options[:doctype] || @doctype
194
- if doctype_to_use && !options[:omit_doctype]
195
- result += generate_doctype_declaration(doctype_to_use)
196
- end
197
-
198
- result += xml_data
199
- if encoding && result.encoding.to_s.upcase != encoding.to_s.upcase
200
- result = result.encode(encoding)
201
- end
202
- result
203
- end
204
-
205
223
  def text_content_for_xml(value)
206
- ::Moxml::Adapter::Base.preprocess_entities(value.to_s)
224
+ ::Moxml.preprocess_entities(value.to_s)
207
225
  end
208
226
 
209
227
  def build_plan_node(xml, xml_element, element_node, plan: nil,
@@ -212,7 +230,9 @@ module Lutaml
212
230
  attributes = {}
213
231
 
214
232
  original_ns_uris = plan&.original_namespace_uris || {}
215
- element_node.hoisted_declarations.each do |key, uri|
233
+ element_node.hoisted_declarations.sort_by do |prefix, _uri|
234
+ prefix.nil? ? "" : prefix.to_s
235
+ end.each do |key, uri|
216
236
  next if uri == "http://www.w3.org/XML/1998/namespace"
217
237
 
218
238
  effective_uri = if self.class.fpi?(uri)
@@ -7,6 +7,10 @@ module Lutaml
7
7
  autoload :AdapterHelpers, "#{__dir__}/adapter/adapter_helpers"
8
8
  autoload :BaseAdapter, "#{__dir__}/adapter/base_adapter"
9
9
  autoload :NamespaceData, "#{__dir__}/adapter/namespace_data"
10
+ autoload :XmlParser, "#{__dir__}/adapter/xml_parser"
11
+ autoload :XmlSerializer, "#{__dir__}/adapter/xml_serializer"
12
+ autoload :PlanBasedBuilder, "#{__dir__}/adapter/plan_based_builder"
13
+ autoload :NamespaceUriCollector, "#{__dir__}/adapter/namespace_uri_collector"
10
14
  autoload :OgaAdapter, "#{__dir__}/adapter/oga_adapter"
11
15
  Lutaml::Model::RuntimeCompatibility.autoload_native(
12
16
  self,
@@ -7,15 +7,63 @@ module Lutaml
7
7
  module Builder
8
8
  # Base builder for XML construction using moxml.
9
9
  # All adapter-specific builders inherit from this class.
10
+ #
11
+ # The builder creates XML documents through moxml's document model.
12
+ # Declaration, doctype, indentation, and line endings are handled
13
+ # by moxml — no manual string assembly.
10
14
  class Base
11
15
  def self.build(options = {})
12
16
  context = Moxml.new(moxml_backend)
13
17
  if Lutaml::Model::RuntimeCompatibility.opal?
14
18
  context.config.namespace_validation_mode = :lenient
15
19
  end
20
+
21
+ encoding_value = options.delete(:encoding)
22
+ context.config.default_indent = options.delete(:indent) if options.key?(:indent)
23
+ context.config.default_line_ending = options.delete(:line_ending) if options.key?(:line_ending)
24
+
16
25
  doc = context.create_document
26
+
27
+ # Capture doctype — added after root to avoid Ox incompatibility
28
+ doctype = options.delete(:doctype)
29
+
17
30
  instance = new(doc, context, options)
31
+ instance.encoding = encoding_value if encoding_value
18
32
  yield(instance) if block_given?
33
+
34
+ # Add doctype before root (after build block sets root)
35
+ if doctype && doc.root
36
+ dt = doc.create_doctype(
37
+ doctype[:name],
38
+ doctype[:public_id],
39
+ doctype[:system_id],
40
+ )
41
+ doc.add_child(dt)
42
+ end
43
+
44
+ # Handle declaration — configure it on the document so moxml
45
+ # serializes it natively (works across all adapters)
46
+ xml_decl = options.delete(:xml_declaration) || {}
47
+ include_decl = options.delete(:include_declaration)
48
+ force_decl = options.delete(:force_declaration)
49
+
50
+ if include_decl
51
+ version = xml_decl[:version] || "1.0"
52
+ encoding = xml_decl[:encoding]
53
+ encoding ||= "UTF-8" unless xml_decl[:had_declaration]
54
+ standalone = xml_decl[:standalone]
55
+ decl = doc.create_declaration(version, encoding, standalone)
56
+ doc.add_child(decl)
57
+ instance.declaration_mode = :default
58
+ elsif force_decl
59
+ decl_encoding = encoding_value || "UTF-8"
60
+ decl = doc.create_declaration("1.0", decl_encoding, nil)
61
+ doc.add_child(decl)
62
+ instance.declaration_mode = :default
63
+ else
64
+ instance.declaration_mode = :none
65
+ end
66
+
19
67
  instance
20
68
  end
21
69
 
@@ -24,13 +72,15 @@ module Lutaml
24
72
  nil
25
73
  end
26
74
 
27
- attr_reader :doc, :encoding
75
+ attr_reader :doc
76
+ attr_accessor :encoding, :declaration_mode
28
77
 
29
78
  def initialize(doc, context, options = {})
30
79
  @doc = doc
31
80
  @context = context
32
81
  @encoding = options[:encoding]
33
82
  @current_stack = [doc]
83
+ @declaration_mode = :none
34
84
  end
35
85
 
36
86
  def current_element
@@ -116,33 +166,15 @@ module Lutaml
116
166
  add_cdata(current_element, content)
117
167
  end
118
168
 
119
- def to_s
169
+ def to_xml
120
170
  return "" unless @doc.root
121
171
 
122
- # Serialize full document content (including PIs before root)
123
- # Check if there are any nodes before the root element
124
- doc_children = @doc.children
125
- has_pi_or_comment = doc_children.any? do |child|
126
- child.is_a?(Moxml::ProcessingInstruction) || child.is_a?(Moxml::Comment)
127
- end
128
-
129
- if has_pi_or_comment
130
- # Serialize each top-level node individually
131
- parts = doc_children.map do |child|
132
- if child == @doc.root
133
- child.to_xml(declaration: false, expand_empty: false)
134
- else
135
- child.to_xml
136
- end
137
- end
138
- parts.join("\n")
139
- else
140
- @doc.root.to_xml(declaration: false, expand_empty: false)
141
- end
142
- end
172
+ result = if @declaration_mode == :none && !has_document_level_nodes?
173
+ @doc.root.to_xml(declaration: false, expand_empty: false)
174
+ else
175
+ @doc.to_xml(declaration: @declaration_mode == :default, expand_empty: false)
176
+ end
143
177
 
144
- def to_xml
145
- result = to_s
146
178
  result = result.encode(encoding) if encoding && result.encoding.to_s != encoding
147
179
  result
148
180
  end
@@ -158,6 +190,13 @@ module Lutaml
158
190
 
159
191
  private
160
192
 
193
+ def has_document_level_nodes?
194
+ @doc.children.any? do |child|
195
+ child != @doc.root &&
196
+ !child.is_a?(Moxml::Text)
197
+ end
198
+ end
199
+
161
200
  def resolve_target(element)
162
201
  element.is_a?(self.class) || element.is_a?(Base) ? element.current_element : element
163
202
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
-
5
3
  module Lutaml
6
4
  module Xml
7
5
  module Builder
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
-
5
3
  module Lutaml
6
4
  module Xml
7
5
  module Builder
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
-
5
3
  module Lutaml
6
4
  module Xml
7
5
  module Builder