lutaml-model 0.8.5 → 0.8.7
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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.rubocop_todo.yml +97 -22
- data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
- data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
- data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
- data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
- data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
- data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
- data/lib/lutaml/xml/adapter.rb +0 -1
- data/lib/lutaml/xml/adapter_element.rb +7 -1
- data/lib/lutaml/xml/builder/base.rb +0 -1
- data/lib/lutaml/xml/data_model.rb +9 -1
- data/lib/lutaml/xml/document.rb +3 -1
- data/lib/lutaml/xml/element.rb +13 -10
- data/lib/lutaml/xml/schema/xsd/schema.rb +8 -5
- data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
- data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
- data/lib/lutaml/xml/xml_element.rb +24 -20
- data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
- data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
- data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
- data/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb +20 -0
- data/spec/lutaml/xml/schema/xsd/spec_helper.rb +1 -0
- data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +9 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -19,15 +19,24 @@ module Lutaml
|
|
|
19
19
|
attr_accessor :element_order, :attribute_order, :schema_location,
|
|
20
20
|
:encoding, :doctype
|
|
21
21
|
|
|
22
|
-
# Store pre-collected namespace data for lazy plan building.
|
|
23
|
-
# This is a plain Hash (no adapter objects) collected during from_xml.
|
|
24
|
-
attr_accessor :pending_namespace_data
|
|
25
|
-
|
|
26
22
|
# Store root element reference for truly lazy plan building.
|
|
27
23
|
# Set in :lazy mode during deserialization; consumed on first to_xml.
|
|
28
24
|
# Released after plan is built to allow GC of the DOM tree.
|
|
29
25
|
attr_accessor :pending_plan_root_element
|
|
30
26
|
|
|
27
|
+
# Clear all internal XML parse state on this instance.
|
|
28
|
+
#
|
|
29
|
+
# Call before re-serializing a previously-parsed instance when the
|
|
30
|
+
# serialization context has changed (e.g. different namespace config,
|
|
31
|
+
# different XML document structure).
|
|
32
|
+
#
|
|
33
|
+
# @return [self]
|
|
34
|
+
def clear_xml_parse_state!
|
|
35
|
+
@import_declaration_plan = nil
|
|
36
|
+
@pending_plan_root_element = nil
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
# XML namespace metadata for doubly-defined and alias support.
|
|
32
41
|
# These carry information from deserialization to serialization.
|
|
33
42
|
# Accessor methods use the @__ prefixed ivars for backward compatibility.
|
|
@@ -74,8 +83,8 @@ module Lutaml
|
|
|
74
83
|
# Build or return the cached declaration plan.
|
|
75
84
|
#
|
|
76
85
|
# When import_declaration_plan: :lazy (default), builds the plan from
|
|
77
|
-
#
|
|
78
|
-
#
|
|
86
|
+
# the stored element reference on first call. No-op when no pending
|
|
87
|
+
# element exists (:eager already set, :skip, or programmatic creation).
|
|
79
88
|
#
|
|
80
89
|
# @return [DeclarationPlan, nil] The plan or nil
|
|
81
90
|
def import_declaration_plan
|
|
@@ -178,8 +187,7 @@ module Lutaml
|
|
|
178
187
|
|
|
179
188
|
# Extend INTERNAL_ATTRIBUTES with XML-specific ones
|
|
180
189
|
def pretty_print_instance_variables
|
|
181
|
-
xml_internals = %i[@import_declaration_plan @
|
|
182
|
-
@pending_namespace_data @pending_plan_root_element
|
|
190
|
+
xml_internals = %i[@import_declaration_plan @pending_plan_root_element
|
|
183
191
|
@__xml_namespace_prefix
|
|
184
192
|
@__xml_ns_prefixes @__xml_original_namespace_uri
|
|
185
193
|
@xml_declaration @raw_schema_location]
|
|
@@ -232,11 +240,6 @@ module Lutaml
|
|
|
232
240
|
options[:xml_declaration] = @xml_declaration
|
|
233
241
|
end
|
|
234
242
|
|
|
235
|
-
# Pass input namespaces for Namespace Preservation
|
|
236
|
-
if instance_variable_defined?(:@xml_input_namespaces) && @xml_input_namespaces&.any?
|
|
237
|
-
options[:input_namespaces] = @xml_input_namespaces
|
|
238
|
-
end
|
|
239
|
-
|
|
240
243
|
# Pass stored DeclarationPlan for format preservation.
|
|
241
244
|
if import_declaration_plan
|
|
242
245
|
options[:stored_xml_declaration_plan] = import_declaration_plan
|
|
@@ -253,39 +256,27 @@ module Lutaml
|
|
|
253
256
|
|
|
254
257
|
private
|
|
255
258
|
|
|
256
|
-
# Build declaration plan from
|
|
257
|
-
#
|
|
258
|
-
# on first access.
|
|
259
|
+
# Build declaration plan from stored element reference (lazy mode).
|
|
260
|
+
# Called by import_declaration_plan getter on first access.
|
|
259
261
|
# @return [DeclarationPlan, nil]
|
|
260
262
|
def build_pending_declaration_plan
|
|
261
|
-
|
|
262
|
-
if @pending_plan_root_element
|
|
263
|
-
element = @pending_plan_root_element
|
|
264
|
-
@pending_plan_root_element = nil # Release reference (allows GC of DOM)
|
|
265
|
-
ns_data = Lutaml::Xml::ModelTransform.collect_element_namespaces(element)
|
|
266
|
-
if ns_data && !ns_data.empty?
|
|
267
|
-
xml_mapping = self.class.mappings_for(:xml)
|
|
268
|
-
return Lutaml::Xml::DeclarationPlan.from_input_with_locations(ns_data,
|
|
269
|
-
xml_mapping)
|
|
270
|
-
end
|
|
271
|
-
return nil
|
|
272
|
-
end
|
|
263
|
+
return nil unless @pending_plan_root_element
|
|
273
264
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
element = @pending_plan_root_element
|
|
266
|
+
@pending_plan_root_element = nil
|
|
267
|
+
ns_data = Lutaml::Xml::ModelTransform.collect_element_namespaces(element)
|
|
268
|
+
return nil unless ns_data && !ns_data.empty?
|
|
277
269
|
|
|
278
|
-
@pending_namespace_data = nil
|
|
279
270
|
xml_mapping = self.class.mappings_for(:xml)
|
|
280
|
-
Lutaml::Xml::DeclarationPlan.from_input_with_locations(
|
|
271
|
+
Lutaml::Xml::DeclarationPlan.from_input_with_locations(ns_data,
|
|
281
272
|
xml_mapping)
|
|
282
273
|
end
|
|
283
274
|
|
|
284
275
|
def set_ordering(attrs)
|
|
285
|
-
return unless attrs.
|
|
276
|
+
return unless attrs.is_a?(Lutaml::Xml::XmlElement)
|
|
286
277
|
|
|
287
278
|
@element_order = attrs.item_order
|
|
288
|
-
@attribute_order = attrs.attribute_order
|
|
279
|
+
@attribute_order = attrs.attribute_order
|
|
289
280
|
end
|
|
290
281
|
|
|
291
282
|
def set_schema_location(attrs)
|
|
@@ -13,14 +13,10 @@ module Lutaml
|
|
|
13
13
|
# is called with a block, the new element becomes the context for the duration
|
|
14
14
|
# of the block, allowing nested element creation.
|
|
15
15
|
class CustomMethodWrapper
|
|
16
|
-
# Initialize the wrapper
|
|
17
|
-
#
|
|
18
16
|
# @param parent [XmlDataModel::XmlElement] Parent element to add children to
|
|
19
|
-
|
|
20
|
-
def initialize(parent, rule)
|
|
17
|
+
def initialize(parent)
|
|
21
18
|
@parent = parent
|
|
22
|
-
@
|
|
23
|
-
@context_stack = [parent] # Stack of context elements for nested creation
|
|
19
|
+
@context_stack = [parent]
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
# Get the current context element (top of stack)
|
|
@@ -70,9 +66,12 @@ module Lutaml
|
|
|
70
66
|
|
|
71
67
|
if element_or_string.is_a?(String)
|
|
72
68
|
add_xml_fragment_or_raw_content(parent, element_or_string)
|
|
73
|
-
|
|
74
|
-
# Add as child element
|
|
69
|
+
elsif element_or_string.is_a?(::Lutaml::Xml::DataModel::XmlElement)
|
|
75
70
|
parent.add_child(element_or_string)
|
|
71
|
+
else
|
|
72
|
+
raise TypeError,
|
|
73
|
+
"add_element expects a String or XmlElement, got " \
|
|
74
|
+
"#{element_or_string.class}. Call .to_xml on the element first."
|
|
76
75
|
end
|
|
77
76
|
element_or_string
|
|
78
77
|
end
|
|
@@ -81,7 +80,7 @@ module Lutaml
|
|
|
81
80
|
require "moxml" unless defined?(Moxml)
|
|
82
81
|
fragment_doc = Moxml.new.parse(fragment_string, fragment: true)
|
|
83
82
|
add_fragment_children_to_parent(fragment_doc, parent)
|
|
84
|
-
rescue LoadError
|
|
83
|
+
rescue LoadError
|
|
85
84
|
append_raw_content(parent, fragment_string)
|
|
86
85
|
end
|
|
87
86
|
|
|
@@ -135,12 +134,12 @@ module Lutaml
|
|
|
135
134
|
|
|
136
135
|
# Add text to element (mimics old adapter API)
|
|
137
136
|
#
|
|
138
|
-
# @param element [XmlDataModel::XmlElement, CustomMethodWrapper, nil]
|
|
137
|
+
# @param element [XmlDataModel::XmlElement, CustomMethodWrapper, nil]
|
|
138
|
+
# Element to add text to. When the wrapper itself or nil is passed,
|
|
139
|
+
# text is added to the current context element.
|
|
139
140
|
# @param text [String] Text content
|
|
140
141
|
def add_text(element, text)
|
|
141
|
-
|
|
142
|
-
# or when element is nil (add to current context)
|
|
143
|
-
target = if element.is_a?(CustomMethodWrapper) || element.nil?
|
|
142
|
+
target = if element == self || element.nil?
|
|
144
143
|
current_context
|
|
145
144
|
else
|
|
146
145
|
element
|
|
@@ -169,32 +168,14 @@ module Lutaml
|
|
|
169
168
|
# @yield [ElementWrapper] The created element for customization
|
|
170
169
|
# @return [XmlElement] The created element
|
|
171
170
|
def create_and_add_element(name, attributes: {})
|
|
172
|
-
|
|
173
|
-
element = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
174
|
-
|
|
175
|
-
# Add attributes if provided
|
|
176
|
-
attributes&.each do |attr_name, attr_value|
|
|
177
|
-
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
178
|
-
attr_name.to_s, attr_value.to_s
|
|
179
|
-
)
|
|
180
|
-
element.add_attribute(attr)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Add to current context
|
|
171
|
+
element = self.class.build_element(name, attributes)
|
|
184
172
|
current_context.add_child(element)
|
|
185
173
|
|
|
186
174
|
if block_given?
|
|
187
|
-
# Push this element as the new context for nested operations
|
|
188
175
|
push_context(element)
|
|
189
|
-
|
|
190
176
|
begin
|
|
191
|
-
|
|
192
|
-
wrapped_element = ElementWrapper.new(element, self)
|
|
193
|
-
|
|
194
|
-
# Yield for customization (e.g., adding text, more nested elements)
|
|
195
|
-
yield wrapped_element
|
|
177
|
+
yield ElementWrapper.new(element, self)
|
|
196
178
|
ensure
|
|
197
|
-
# Restore previous context
|
|
198
179
|
pop_context
|
|
199
180
|
end
|
|
200
181
|
end
|
|
@@ -216,13 +197,7 @@ module Lutaml
|
|
|
216
197
|
# @param text [String] Text content
|
|
217
198
|
# @param cdata [Boolean, Hash] Whether to use CDATA (true or {cdata: true})
|
|
218
199
|
def add_text(_self, text, cdata: false)
|
|
219
|
-
|
|
220
|
-
use_cdata = if cdata.is_a?(Hash)
|
|
221
|
-
cdata[:cdata] || false
|
|
222
|
-
else
|
|
223
|
-
cdata
|
|
224
|
-
end
|
|
225
|
-
|
|
200
|
+
use_cdata = cdata.is_a?(Hash) ? cdata[:cdata] || false : cdata
|
|
226
201
|
@element.text_content = text
|
|
227
202
|
@element.cdata = use_cdata
|
|
228
203
|
end
|
|
@@ -234,29 +209,33 @@ module Lutaml
|
|
|
234
209
|
# @yield [ElementWrapper] The created element for customization
|
|
235
210
|
# @return [XmlElement] The created element
|
|
236
211
|
def create_and_add_element(name, attributes: {})
|
|
237
|
-
|
|
238
|
-
child = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
239
|
-
|
|
240
|
-
# Add attributes if provided
|
|
241
|
-
attributes&.each do |attr_name, attr_value|
|
|
242
|
-
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
243
|
-
attr_name.to_s, attr_value.to_s
|
|
244
|
-
)
|
|
245
|
-
child.add_attribute(attr)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Add to this element
|
|
212
|
+
child = CustomMethodWrapper.build_element(name, attributes)
|
|
249
213
|
@element.add_child(child)
|
|
250
214
|
|
|
251
215
|
if block_given?
|
|
252
|
-
|
|
253
|
-
wrapped_child = ElementWrapper.new(child, @parent_wrapper)
|
|
254
|
-
yield wrapped_child
|
|
216
|
+
yield ElementWrapper.new(child, @parent_wrapper)
|
|
255
217
|
end
|
|
256
218
|
|
|
257
219
|
child
|
|
258
220
|
end
|
|
259
221
|
end
|
|
222
|
+
|
|
223
|
+
# Shared factory: create an XmlElement with optional attributes.
|
|
224
|
+
# Public so ElementWrapper can call it without an instance.
|
|
225
|
+
#
|
|
226
|
+
# @param name [String] Element name
|
|
227
|
+
# @param attributes [Hash] Optional attributes
|
|
228
|
+
# @return [DataModel::XmlElement]
|
|
229
|
+
def self.build_element(name, attributes)
|
|
230
|
+
element = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
231
|
+
attributes&.each do |attr_name, attr_value|
|
|
232
|
+
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
233
|
+
attr_name.to_s, attr_value.to_s
|
|
234
|
+
)
|
|
235
|
+
element.add_attribute(attr)
|
|
236
|
+
end
|
|
237
|
+
element
|
|
238
|
+
end
|
|
260
239
|
end
|
|
261
240
|
end
|
|
262
241
|
end
|
|
@@ -239,7 +239,7 @@ register_id)
|
|
|
239
239
|
# @param model_class [Class] The model class
|
|
240
240
|
# @param model_instance [Object] The model instance
|
|
241
241
|
def apply_custom_method(parent, rule, model_class, model_instance)
|
|
242
|
-
wrapper = ::Lutaml::Xml::CustomMethodWrapper.new(parent
|
|
242
|
+
wrapper = ::Lutaml::Xml::CustomMethodWrapper.new(parent)
|
|
243
243
|
mapper_instance = model_class.new
|
|
244
244
|
mapper_instance.send(rule.custom_methods[:to], model_instance,
|
|
245
245
|
parent, wrapper)
|
|
@@ -257,33 +257,26 @@ module Lutaml
|
|
|
257
257
|
return @order_cache if @order_cache
|
|
258
258
|
|
|
259
259
|
@order_cache = children.filter_map do |child|
|
|
260
|
-
if child.
|
|
260
|
+
if child.cdata?
|
|
261
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
262
|
+
text_content: child.text,
|
|
263
|
+
node_type: :cdata)
|
|
264
|
+
elsif child.text?
|
|
261
265
|
next if child.text.nil?
|
|
262
266
|
|
|
263
|
-
# For text nodes:
|
|
264
|
-
# - name is "text" for backward compatibility with tests
|
|
265
|
-
# - text_content contains the actual text for round-trip serialization
|
|
266
|
-
# - node_type explicitly marks this as a text node
|
|
267
267
|
Lutaml::Xml::Element.new("Text", "text",
|
|
268
268
|
text_content: child.text,
|
|
269
269
|
node_type: :text)
|
|
270
|
-
elsif child.cdata?
|
|
271
|
-
# For CDATA sections:
|
|
272
|
-
# - name is "#cdata-section" for backward compatibility
|
|
273
|
-
# - text_content contains the actual CDATA content
|
|
274
|
-
# - node_type explicitly marks this as CDATA
|
|
275
|
-
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
276
|
-
text_content: child.text,
|
|
277
|
-
node_type: :cdata)
|
|
278
270
|
elsif child.comment?
|
|
279
271
|
Lutaml::Xml::Element.new("Comment", "comment",
|
|
280
272
|
text_content: child.text,
|
|
281
273
|
node_type: :comment)
|
|
274
|
+
elsif child.processing_instruction?
|
|
275
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
276
|
+
child.unprefixed_name,
|
|
277
|
+
text_content: child.text,
|
|
278
|
+
node_type: :processing_instruction)
|
|
282
279
|
else
|
|
283
|
-
# For regular elements:
|
|
284
|
-
# - name is the actual element name
|
|
285
|
-
# - node_type explicitly marks this as an element
|
|
286
|
-
# - namespace_uri and namespace_prefix preserve namespace info for rule matching
|
|
287
280
|
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
288
281
|
node_type: :element,
|
|
289
282
|
namespace_uri: child.namespace_uri,
|
|
@@ -303,14 +296,14 @@ module Lutaml
|
|
|
303
296
|
|
|
304
297
|
def text
|
|
305
298
|
return @text if children.empty?
|
|
306
|
-
return text_children.map(&:text) if
|
|
299
|
+
return text_children.map(&:text) if content_bearing_children_count > 1
|
|
307
300
|
|
|
308
301
|
text_children.map(&:text).join
|
|
309
302
|
end
|
|
310
303
|
|
|
311
304
|
def cdata
|
|
312
305
|
return @text if children.empty?
|
|
313
|
-
return cdata_children.map(&:text) if
|
|
306
|
+
return cdata_children.map(&:text) if content_bearing_children_count > 1
|
|
314
307
|
|
|
315
308
|
cdata_children.map(&:text).join
|
|
316
309
|
end
|
|
@@ -330,7 +323,8 @@ module Lutaml
|
|
|
330
323
|
|
|
331
324
|
@element_children = children.reject do |child|
|
|
332
325
|
child.is_a?(String) || child.is_a?(Symbol) ||
|
|
333
|
-
(child.is_a?(XmlElement) &&
|
|
326
|
+
(child.is_a?(XmlElement) &&
|
|
327
|
+
(child.text? || child.processing_instruction?))
|
|
334
328
|
end
|
|
335
329
|
end
|
|
336
330
|
|
|
@@ -389,6 +383,8 @@ module Lutaml
|
|
|
389
383
|
|
|
390
384
|
@children_index = {}
|
|
391
385
|
@children.each do |child|
|
|
386
|
+
next if child.is_a?(XmlElement) && child.processing_instruction?
|
|
387
|
+
|
|
392
388
|
key = child.namespaced_name
|
|
393
389
|
@children_index[key] ||= []
|
|
394
390
|
@children_index[key] << child
|
|
@@ -432,6 +428,14 @@ module Lutaml
|
|
|
432
428
|
|
|
433
429
|
private
|
|
434
430
|
|
|
431
|
+
# Count children that bear content (excludes processing instructions).
|
|
432
|
+
# Used to determine if content is mixed (multiple content nodes).
|
|
433
|
+
def content_bearing_children_count
|
|
434
|
+
children.count do |child|
|
|
435
|
+
!child.is_a?(XmlElement) || !child.processing_instruction?
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
435
439
|
# Backward compatibility: infer node_type from name
|
|
436
440
|
# This allows old code that doesn't pass node_type to still work
|
|
437
441
|
def infer_node_type_from_name(name)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model"
|
|
5
|
+
require "lutaml/xml"
|
|
6
|
+
require "lutaml/xml/adapter/nokogiri_adapter"
|
|
7
|
+
require "lutaml/xml/adapter/ox_adapter"
|
|
8
|
+
require "lutaml/xml/adapter/oga_adapter"
|
|
9
|
+
require "lutaml/xml/adapter/rexml_adapter"
|
|
10
|
+
|
|
11
|
+
# Regression tests for BaseAdapter refactoring.
|
|
12
|
+
# Each test guards a specific bug fix to prevent re-introduction.
|
|
13
|
+
# Run against all 4 adapters to ensure consistent behavior.
|
|
14
|
+
RSpec.shared_examples "base adapter regressions" do |adapter_class|
|
|
15
|
+
let(:adapter) { adapter_class }
|
|
16
|
+
|
|
17
|
+
describe "CDATA preservation in mixed content" do
|
|
18
|
+
let(:xml_with_cdata) do
|
|
19
|
+
"<root><![CDATA[Hello]]><b>world</b><![CDATA[!]]></root>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "preserves CDATA wrapping for mixed content string nodes" do
|
|
23
|
+
doc = adapter.parse(xml_with_cdata)
|
|
24
|
+
output = doc.to_xml
|
|
25
|
+
|
|
26
|
+
expect(output).to include("<![CDATA[Hello]]>")
|
|
27
|
+
expect(output).to include("<![CDATA[!]]>")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "XML comment preservation in serialization" do
|
|
32
|
+
let(:xml_with_comment) do
|
|
33
|
+
"<root><a/><!--middle comment--><b/></root>"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "preserves comments during round-trip" do
|
|
37
|
+
doc = adapter.parse(xml_with_comment)
|
|
38
|
+
output = doc.to_xml
|
|
39
|
+
|
|
40
|
+
expect(output).to include("<!--middle comment-->")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "processing instructions do not affect text shape" do
|
|
45
|
+
let(:xml_with_pi_and_text) do
|
|
46
|
+
"<root>hello<?pi data?></root>"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:xml_with_only_pi) do
|
|
50
|
+
"<root><?pi data?></root>"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "returns text as a string when PI is alongside text" do
|
|
54
|
+
doc = adapter.parse(xml_with_pi_and_text)
|
|
55
|
+
|
|
56
|
+
# text should be a string, not an array —
|
|
57
|
+
# PIs should not make plain text look like mixed content
|
|
58
|
+
expect(doc.text).to be_a(String)
|
|
59
|
+
expect(doc.text).to eq("hello")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "returns empty string for text when element has only PI" do
|
|
63
|
+
doc = adapter.parse(xml_with_only_pi)
|
|
64
|
+
|
|
65
|
+
expect(doc.text).to be_a(String)
|
|
66
|
+
expect(doc.text).to eq("")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "processing instructions excluded from parse_element" do
|
|
71
|
+
let(:xml_with_pi) do
|
|
72
|
+
"<root><?foo ignored?><bar>content</bar></root>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "does not include PI in elements hash" do
|
|
76
|
+
doc = adapter.parse(xml_with_pi)
|
|
77
|
+
hash = doc.to_h
|
|
78
|
+
|
|
79
|
+
elements = hash["elements"]
|
|
80
|
+
expect(elements).to have_key("bar")
|
|
81
|
+
# PI should not appear as an element key
|
|
82
|
+
expect(elements.keys).not_to include("foo")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "ASCII-8BIT encoding round-trip" do
|
|
87
|
+
let(:binary_xml) { "<root>\xC2\xB5</root>".b }
|
|
88
|
+
|
|
89
|
+
it "round-trips binary-tagged UTF-8 content without encoding error" do
|
|
90
|
+
doc = adapter.parse(binary_xml)
|
|
91
|
+
|
|
92
|
+
expect { doc.to_xml }.not_to raise_error
|
|
93
|
+
expect(doc.to_xml).to include("\u00B5") # micro sign
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "does not use ASCII-8BIT as output encoding" do
|
|
97
|
+
doc = adapter.parse(binary_xml)
|
|
98
|
+
output = doc.to_xml
|
|
99
|
+
|
|
100
|
+
expect(output.encoding).not_to eq(Encoding::ASCII_8BIT)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe "namespaced_attr_name helper arity" do
|
|
105
|
+
it "resolves single-arg namespaced_attr_name from AdapterHelpers" do
|
|
106
|
+
# This verifies the AdapterHelpers version (single Moxml attribute arg)
|
|
107
|
+
# is accessible and not shadowed by a 2-arg def self. method
|
|
108
|
+
expect(adapter).to respond_to(:namespaced_attr_name)
|
|
109
|
+
|
|
110
|
+
# The method should accept 1 argument (a Moxml attribute object)
|
|
111
|
+
method = adapter.method(:namespaced_attr_name)
|
|
112
|
+
expect(method.arity).to eq(1)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe "XmlParser helper methods are private" do
|
|
117
|
+
it "does not expose normalize_xml_for_parse as a public class method" do
|
|
118
|
+
expect(adapter.public_methods).not_to include(:normalize_xml_for_parse)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "does not expose parse_with_moxml as a public class method" do
|
|
122
|
+
expect(adapter.public_methods).not_to include(:parse_with_moxml)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "does not expose raise_empty_document_error as a public class method" do
|
|
126
|
+
expect(adapter.public_methods).not_to include(:raise_empty_document_error)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "exposes parse as a public class method" do
|
|
130
|
+
expect(adapter.public_methods).to include(:parse)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
RSpec.describe Lutaml::Xml::Adapter::BaseAdapter do
|
|
136
|
+
context "with NokogiriAdapter" do
|
|
137
|
+
it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::NokogiriAdapter
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context "with OxAdapter" do
|
|
141
|
+
it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::OxAdapter
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context "with OgaAdapter" do
|
|
145
|
+
it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::OgaAdapter
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
context "with RexmlAdapter" do
|
|
149
|
+
it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::RexmlAdapter
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "lutaml/model"
|
|
3
|
+
require "lutaml/xml"
|
|
4
|
+
require "lutaml/xml/adapter/nokogiri_adapter"
|
|
5
|
+
require "lutaml/xml/adapter/ox_adapter"
|
|
6
|
+
require "lutaml/xml/adapter/oga_adapter"
|
|
7
|
+
require "lutaml/xml/adapter/rexml_adapter"
|
|
8
|
+
|
|
9
|
+
module XmlAdapterSharedFeaturesSpec
|
|
10
|
+
class ProcessingInstructionLookupElement < Lutaml::Model::Serializable
|
|
11
|
+
attribute :foo, :string
|
|
12
|
+
|
|
13
|
+
xml do
|
|
14
|
+
root "root"
|
|
15
|
+
map_element "foo", to: :foo
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RSpec.describe "XML adapter order metadata" do
|
|
21
|
+
shared_examples "consistent order metadata" do |adapter_class|
|
|
22
|
+
let(:xml) do
|
|
23
|
+
"<root>before<![CDATA[cdata text]]><?pi data?><child/>after</root>"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:document) { adapter_class.parse(xml) }
|
|
27
|
+
|
|
28
|
+
let(:expected_order) do
|
|
29
|
+
[
|
|
30
|
+
["Text", "text", :text, "before"],
|
|
31
|
+
["Text", "#cdata-section", :cdata, "cdata text"],
|
|
32
|
+
["ProcessingInstruction", "pi", :processing_instruction, "data"],
|
|
33
|
+
["Element", "child", :element, nil],
|
|
34
|
+
["Text", "text", :text, "after"],
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def order_data(order)
|
|
39
|
+
order.map do |item|
|
|
40
|
+
[item.type, item.name, item.node_type, item.text_content]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "uses the same order representation for root, document, and class order" do
|
|
45
|
+
expect(order_data(document.root.order)).to eq(expected_order)
|
|
46
|
+
expect(order_data(document.order)).to eq(expected_order)
|
|
47
|
+
expect(order_data(adapter_class.order_of(document.root))).to eq(expected_order)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "parses processing instructions as first-class XML nodes" do
|
|
51
|
+
instruction = document.root.children.find(&:processing_instruction?)
|
|
52
|
+
|
|
53
|
+
expect(instruction).not_to be_nil
|
|
54
|
+
expect(instruction.name).to eq("pi")
|
|
55
|
+
expect(instruction.text).to eq("data")
|
|
56
|
+
expect(instruction.node_type).to eq(:processing_instruction)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "preserves processing instructions and CDATA when serializing parsed XML" do
|
|
60
|
+
output = document.to_xml
|
|
61
|
+
|
|
62
|
+
expect(output).to include("<?pi data?>")
|
|
63
|
+
expect(output).to include("<![CDATA[cdata text]]>")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "uses the same order representation through to_h item_order" do
|
|
67
|
+
parsed_hash = nil
|
|
68
|
+
|
|
69
|
+
expect { parsed_hash = document.to_h }.not_to raise_error
|
|
70
|
+
expect(order_data(parsed_hash.item_order)).to eq(expected_order)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
shared_examples "consistent shared adapter features" do |adapter_class|
|
|
75
|
+
around do |example|
|
|
76
|
+
old_adapter = Lutaml::Model::Config.xml_adapter
|
|
77
|
+
Lutaml::Model::Config.xml_adapter = adapter_class
|
|
78
|
+
example.run
|
|
79
|
+
ensure
|
|
80
|
+
Lutaml::Model::Config.xml_adapter = old_adapter
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "uses shared parse metadata preservation" do
|
|
84
|
+
xml = <<~XML
|
|
85
|
+
<?xml version="1.0"?>
|
|
86
|
+
<!DOCTYPE root SYSTEM "root.dtd">
|
|
87
|
+
<root><child/></root>
|
|
88
|
+
XML
|
|
89
|
+
|
|
90
|
+
document = adapter_class.parse(xml)
|
|
91
|
+
|
|
92
|
+
expect(document.xml_declaration[:had_declaration]).to be true
|
|
93
|
+
expect(document.doctype).to include(name: "root",
|
|
94
|
+
public_id: nil,
|
|
95
|
+
system_id: "root.dtd")
|
|
96
|
+
expect(document.to_xml(declaration: true)).to include(
|
|
97
|
+
'<!DOCTYPE root SYSTEM "root.dtd">',
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "parses binary UTF-8 XML input through shared normalization" do
|
|
102
|
+
document = adapter_class.parse("<root><name>μ</name></root>".b)
|
|
103
|
+
|
|
104
|
+
expect(document.root.name).to eq("root")
|
|
105
|
+
expect(document.root.element_children.first.text).to eq("μ")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "transcodes non-UTF-8 strings from their declared encoding" do
|
|
109
|
+
xml = "<root><name>\xC2\xA3</name></root>".b
|
|
110
|
+
xml.force_encoding("ISO-8859-1")
|
|
111
|
+
document = adapter_class.parse(xml)
|
|
112
|
+
|
|
113
|
+
expect(document.root.element_children.first.text).to eq("£")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "preserves processing instructions without matching them as elements" do
|
|
117
|
+
xml = "<root><?foo ignored?><foo>bar</foo></root>"
|
|
118
|
+
document = adapter_class.parse(xml)
|
|
119
|
+
|
|
120
|
+
expect(document.root.children.any?(&:processing_instruction?)).to be true
|
|
121
|
+
expect(document.root.element_children.map(&:text)).to eq(["bar"])
|
|
122
|
+
expect(document.root.find_children_by_name("foo").map(&:text)).to eq(["bar"])
|
|
123
|
+
expect(
|
|
124
|
+
XmlAdapterSharedFeaturesSpec::ProcessingInstructionLookupElement
|
|
125
|
+
.from_xml(xml)
|
|
126
|
+
.foo,
|
|
127
|
+
).to eq("bar")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe Lutaml::Xml::Adapter::NokogiriAdapter do
|
|
132
|
+
it_behaves_like "consistent order metadata", described_class
|
|
133
|
+
it_behaves_like "consistent shared adapter features", described_class
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe Lutaml::Xml::Adapter::OxAdapter do
|
|
137
|
+
it_behaves_like "consistent order metadata", described_class
|
|
138
|
+
it_behaves_like "consistent shared adapter features", described_class
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe Lutaml::Xml::Adapter::OgaAdapter do
|
|
142
|
+
it_behaves_like "consistent order metadata", described_class
|
|
143
|
+
it_behaves_like "consistent shared adapter features", described_class
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe Lutaml::Xml::Adapter::RexmlAdapter do
|
|
147
|
+
it_behaves_like "consistent order metadata", described_class
|
|
148
|
+
it_behaves_like "consistent shared adapter features", described_class
|
|
149
|
+
end
|
|
150
|
+
end
|