lutaml-model 0.8.15 → 0.8.16
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/.rubocop_todo.yml +14 -73
- data/README.adoc +188 -25
- data/docs/_guides/xml-mapping.adoc +178 -24
- data/docs/_pages/importable_models.adoc +7 -1
- data/lib/lutaml/model/attribute.rb +4 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -1
- data/lib/lutaml/turtle/transform.rb +2 -1
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +3 -1
- data/lib/lutaml/xml/adapter/xml_serializer.rb +14 -4
- data/lib/lutaml/xml/adapter.rb +2 -1
- data/lib/lutaml/xml/builder/base.rb +2 -1
- data/lib/lutaml/xml/data_model.rb +19 -3
- data/lib/lutaml/xml/mapping.rb +3 -1
- data/lib/lutaml/xml/mapping_rule.rb +28 -2
- data/lib/lutaml/xml/model_transform.rb +9 -1
- data/lib/lutaml/xml/serialization/instance_methods.rb +16 -9
- data/lib/lutaml/xml/transformation/element_builder.rb +1 -3
- data/lib/lutaml/xml/transformation/rule_applier.rb +21 -0
- data/lib/lutaml/xml/transformation/rule_compiler.rb +12 -3
- data/spec/lutaml/jsonld/transform_spec.rb +3 -1
- data/spec/lutaml/model/mixed_content_spec.rb +48 -7
- data/spec/lutaml/model/raw_element_spec.rb +533 -0
- data/spec/lutaml/rdf/mapping_spec.rb +2 -1
- metadata +3 -2
|
@@ -665,6 +665,10 @@ instance_object = nil)
|
|
|
665
665
|
def process_options!
|
|
666
666
|
validate_options!(@options)
|
|
667
667
|
@raw = !!@options[:raw]
|
|
668
|
+
if @raw
|
|
669
|
+
warn "[DEPRECATED] attribute :#{name}, :string, raw: true is deprecated. " \
|
|
670
|
+
"Use map_element \"name\", to: :#{name}, raw: :content instead."
|
|
671
|
+
end
|
|
668
672
|
@validations = @options[:validations]
|
|
669
673
|
set_default_for_collection if collection?
|
|
670
674
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -149,7 +149,8 @@ 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,
|
|
152
|
+
autoload :TypeOnlyMappingError,
|
|
153
|
+
"#{__dir__}/model/error/type_only_mapping_error"
|
|
153
154
|
autoload :NoRootMappingError, "#{__dir__}/model/error/no_root_mapping_error"
|
|
154
155
|
autoload :ImportModelWithRootError,
|
|
155
156
|
"#{__dir__}/model/error/import_model_with_root_error"
|
|
@@ -66,7 +66,8 @@ module Lutaml
|
|
|
66
66
|
emit_type_statements(graph, subject_uri, mapping)
|
|
67
67
|
emit_predicate_statements(graph, subject_uri, instance, mapping)
|
|
68
68
|
emit_member_link_statements(graph, subject_uri, instance, mapping)
|
|
69
|
-
additional_resource_triples(instance, subject_uri,
|
|
69
|
+
additional_resource_triples(instance, subject_uri,
|
|
70
|
+
mapping).each do |stmt|
|
|
70
71
|
graph << stmt
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -332,6 +332,8 @@ module Lutaml
|
|
|
332
332
|
xml_mapping: xml_mapping)
|
|
333
333
|
when Lutaml::Xml::DataModel::XmlComment
|
|
334
334
|
inner_xml.add_comment(child.content)
|
|
335
|
+
when Lutaml::Xml::DataModel::XmlRawFragment
|
|
336
|
+
inner_xml.add_xml_fragment(inner_xml, child.content)
|
|
335
337
|
when String
|
|
336
338
|
if element.cdata
|
|
337
339
|
inner_xml.cdata(child.to_s)
|
|
@@ -764,7 +766,7 @@ module Lutaml
|
|
|
764
766
|
xml.create_and_add_element(rule.name,
|
|
765
767
|
attributes: attributes.empty? ? nil : attributes,
|
|
766
768
|
prefix: resolved_prefix)
|
|
767
|
-
elsif rule.raw_mapping?
|
|
769
|
+
elsif rule.raw_mapping? || rule.raw == :element
|
|
768
770
|
xml.add_xml_fragment(xml, value)
|
|
769
771
|
elsif value.is_a?(::Hash) && attribute&.type(register) == Lutaml::Model::Type::Hash
|
|
770
772
|
xml.create_and_add_element(rule.name,
|
|
@@ -49,7 +49,10 @@ module Lutaml
|
|
|
49
49
|
encoding = determine_encoding(options)
|
|
50
50
|
builder_options = {}
|
|
51
51
|
builder_options[:encoding] = encoding if encoding
|
|
52
|
-
|
|
52
|
+
if options.key?(:line_ending)
|
|
53
|
+
builder_options[:line_ending] =
|
|
54
|
+
options[:line_ending]
|
|
55
|
+
end
|
|
53
56
|
builder_options[:indent] = options[:indent] if options.key?(:indent)
|
|
54
57
|
|
|
55
58
|
# Pass doctype to builder for document-level insertion
|
|
@@ -66,15 +69,20 @@ module Lutaml
|
|
|
66
69
|
if options[:standalone] == :preserve
|
|
67
70
|
# Keep original standalone from parsed declaration (may be nil)
|
|
68
71
|
else
|
|
69
|
-
builder_options[:xml_declaration][:standalone] =
|
|
72
|
+
builder_options[:xml_declaration][:standalone] =
|
|
73
|
+
standalone_value(options[:standalone])
|
|
70
74
|
end
|
|
71
75
|
end
|
|
72
76
|
if options[:declaration].is_a?(String)
|
|
73
|
-
builder_options[:xml_declaration][:version] =
|
|
77
|
+
builder_options[:xml_declaration][:version] =
|
|
78
|
+
options[:declaration]
|
|
74
79
|
elsif options[:declaration] == true
|
|
75
80
|
builder_options[:xml_declaration][:version] = "1.0"
|
|
76
81
|
end
|
|
77
|
-
|
|
82
|
+
if options.key?(:encoding) && encoding
|
|
83
|
+
builder_options[:xml_declaration][:encoding] =
|
|
84
|
+
encoding
|
|
85
|
+
end
|
|
78
86
|
elsif options[:encoding] && !options[:encoding].nil?
|
|
79
87
|
builder_options[:force_declaration] = true
|
|
80
88
|
end
|
|
@@ -287,6 +295,8 @@ module Lutaml
|
|
|
287
295
|
previous_child_had_xmlns_blank ||= child_node.needs_xmlns_blank
|
|
288
296
|
when Lutaml::Xml::DataModel::XmlComment
|
|
289
297
|
xml.add_comment(xml_child.content)
|
|
298
|
+
when Lutaml::Xml::DataModel::XmlRawFragment
|
|
299
|
+
xml.add_xml_fragment(xml, xml_child.content)
|
|
290
300
|
when String
|
|
291
301
|
if xml_element.cdata
|
|
292
302
|
xml.cdata(xml_child.to_s)
|
data/lib/lutaml/xml/adapter.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Lutaml
|
|
|
10
10
|
autoload :XmlParser, "#{__dir__}/adapter/xml_parser"
|
|
11
11
|
autoload :XmlSerializer, "#{__dir__}/adapter/xml_serializer"
|
|
12
12
|
autoload :PlanBasedBuilder, "#{__dir__}/adapter/plan_based_builder"
|
|
13
|
-
autoload :NamespaceUriCollector,
|
|
13
|
+
autoload :NamespaceUriCollector,
|
|
14
|
+
"#{__dir__}/adapter/namespace_uri_collector"
|
|
14
15
|
autoload :OgaAdapter, "#{__dir__}/adapter/oga_adapter"
|
|
15
16
|
Lutaml::Model::RuntimeCompatibility.autoload_native(
|
|
16
17
|
self,
|
|
@@ -172,7 +172,8 @@ module Lutaml
|
|
|
172
172
|
result = if @declaration_mode == :none && !has_document_level_nodes?
|
|
173
173
|
@doc.root.to_xml(declaration: false, expand_empty: false)
|
|
174
174
|
else
|
|
175
|
-
@doc.to_xml(declaration: @declaration_mode == :default,
|
|
175
|
+
@doc.to_xml(declaration: @declaration_mode == :default,
|
|
176
|
+
expand_empty: false)
|
|
176
177
|
end
|
|
177
178
|
|
|
178
179
|
result = result.encode(encoding) if encoding && result.encoding.to_s != encoding
|
|
@@ -87,10 +87,10 @@ module Lutaml
|
|
|
87
87
|
# @raise [TypeError] if child is not a supported type
|
|
88
88
|
def add_child(child)
|
|
89
89
|
unless child.is_a?(XmlElement) || child.is_a?(String) ||
|
|
90
|
-
child.is_a?(XmlComment)
|
|
90
|
+
child.is_a?(XmlComment) || child.is_a?(XmlRawFragment)
|
|
91
91
|
raise TypeError,
|
|
92
|
-
"XmlElement#add_child expects XmlElement, String,
|
|
93
|
-
"XmlComment, got #{child.class}"
|
|
92
|
+
"XmlElement#add_child expects XmlElement, String, " \
|
|
93
|
+
"XmlComment, or XmlRawFragment, got #{child.class}"
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
@children << child
|
|
@@ -261,6 +261,22 @@ module Lutaml
|
|
|
261
261
|
end
|
|
262
262
|
end
|
|
263
263
|
|
|
264
|
+
# Represents a raw XML fragment that should be serialized as-is.
|
|
265
|
+
#
|
|
266
|
+
# Used by raw_element mappings to embed complete XML elements (e.g., SVG,
|
|
267
|
+
# MathML) without parsing, wrapping, or escaping.
|
|
268
|
+
class XmlRawFragment
|
|
269
|
+
attr_reader :content
|
|
270
|
+
|
|
271
|
+
def initialize(content)
|
|
272
|
+
@content = content.to_s
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def to_s
|
|
276
|
+
@content
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
264
280
|
# Represents an XML comment in the data model tree.
|
|
265
281
|
# Stored as a child of XmlElement alongside String (text) and XmlElement children.
|
|
266
282
|
class XmlComment
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -510,7 +510,8 @@ module Lutaml
|
|
|
510
510
|
form: nil,
|
|
511
511
|
documentation: nil,
|
|
512
512
|
xsd_type: (xsd_type_provided = false
|
|
513
|
-
nil)
|
|
513
|
+
nil),
|
|
514
|
+
raw: nil
|
|
514
515
|
)
|
|
515
516
|
validate!(
|
|
516
517
|
name, to, with, render_nil, render_empty, type: TYPES[:element]
|
|
@@ -562,6 +563,7 @@ module Lutaml
|
|
|
562
563
|
value_map: value_map,
|
|
563
564
|
form: form,
|
|
564
565
|
documentation: documentation,
|
|
566
|
+
raw: raw,
|
|
565
567
|
)
|
|
566
568
|
# Store rules with the same element name in an array to support
|
|
567
569
|
# multiple mapping rules for the same element name with different target types
|
|
@@ -10,7 +10,8 @@ module Lutaml
|
|
|
10
10
|
:as_list,
|
|
11
11
|
:delimiter,
|
|
12
12
|
:form,
|
|
13
|
-
:documentation
|
|
13
|
+
:documentation,
|
|
14
|
+
:raw
|
|
14
15
|
|
|
15
16
|
# Writers for deep_dup (preserves exact object references)
|
|
16
17
|
attr_accessor :namespace, :prefix, :namespace_class
|
|
@@ -39,7 +40,8 @@ module Lutaml
|
|
|
39
40
|
as_list: nil,
|
|
40
41
|
delimiter: nil,
|
|
41
42
|
form: nil,
|
|
42
|
-
documentation: nil
|
|
43
|
+
documentation: nil,
|
|
44
|
+
raw: nil
|
|
43
45
|
)
|
|
44
46
|
super(
|
|
45
47
|
name,
|
|
@@ -76,6 +78,7 @@ module Lutaml
|
|
|
76
78
|
@delimiter = delimiter
|
|
77
79
|
@form = validate_form(form)
|
|
78
80
|
@documentation = documentation
|
|
81
|
+
@raw = validate_raw(raw)
|
|
79
82
|
|
|
80
83
|
# Memoize prefixed_name at initialization for performance
|
|
81
84
|
# This is safe because prefix and name are immutable after initialization
|
|
@@ -103,6 +106,10 @@ module Lutaml
|
|
|
103
106
|
@static_namespace_option ||= { default_namespace: namespace }.freeze
|
|
104
107
|
end
|
|
105
108
|
|
|
109
|
+
def raw_element?
|
|
110
|
+
@raw == :element
|
|
111
|
+
end
|
|
112
|
+
|
|
106
113
|
def content_mapping?
|
|
107
114
|
name.nil?
|
|
108
115
|
end
|
|
@@ -225,6 +232,7 @@ module Lutaml
|
|
|
225
232
|
delimiter: @delimiter,
|
|
226
233
|
form: @form,
|
|
227
234
|
documentation: @documentation,
|
|
235
|
+
raw: @raw,
|
|
228
236
|
).tap do |dup_rule|
|
|
229
237
|
# Manually preserve the exact @namespace_class object to avoid
|
|
230
238
|
# recreating anonymous classes (which would have different object_ids)
|
|
@@ -573,6 +581,24 @@ form_default = :unqualified)
|
|
|
573
581
|
|
|
574
582
|
form
|
|
575
583
|
end
|
|
584
|
+
|
|
585
|
+
def validate_raw(raw)
|
|
586
|
+
return nil if raw.nil? || raw == false
|
|
587
|
+
|
|
588
|
+
valid_raw = %i[element content]
|
|
589
|
+
if raw == true
|
|
590
|
+
warn "[DEPRECATED] raw: true on map_element is deprecated, " \
|
|
591
|
+
"use raw: :element instead."
|
|
592
|
+
return :element
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
unless valid_raw.include?(raw)
|
|
596
|
+
raise ArgumentError,
|
|
597
|
+
"raw must be :element or :content, got #{raw.inspect}"
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
raw
|
|
601
|
+
end
|
|
576
602
|
end
|
|
577
603
|
end
|
|
578
604
|
end
|
|
@@ -668,6 +668,12 @@ _effective_register)
|
|
|
668
668
|
!child_ns_prefix && rule_names.any? do |rn|
|
|
669
669
|
((colon = rn.rindex(":")) ? rn[(colon + 1)..] : rn) == child.unprefixed_name
|
|
670
670
|
end
|
|
671
|
+
elsif !rule_namespace_set && (!child_ns_prefix || rule.raw == :element)
|
|
672
|
+
# For simple types (String, etc.) with no namespace constraint,
|
|
673
|
+
# match by unprefixed name. Handles elements in foreign namespaces
|
|
674
|
+
# (e.g., SVG inside <image>). raw_element rules match regardless
|
|
675
|
+
# of prefix — the intent is to capture any element with that name.
|
|
676
|
+
child.unprefixed_name == rule_name_str
|
|
671
677
|
else
|
|
672
678
|
false
|
|
673
679
|
end
|
|
@@ -765,7 +771,9 @@ _effective_register)
|
|
|
765
771
|
end
|
|
766
772
|
|
|
767
773
|
values << cast_result
|
|
768
|
-
elsif
|
|
774
|
+
elsif rule.raw == :element
|
|
775
|
+
values << child.to_xml
|
|
776
|
+
elsif rule.raw == :content || attr.raw?
|
|
769
777
|
values << inner_xml_of(child)
|
|
770
778
|
else
|
|
771
779
|
return nil if rule.render_nil_as_nil? && child.nil_element?
|
|
@@ -155,23 +155,30 @@ module Lutaml
|
|
|
155
155
|
# Using ::Hash to avoid conflict with Lutaml::Model::Hash
|
|
156
156
|
collection_indices = ::Hash.new(0)
|
|
157
157
|
|
|
158
|
+
content_is_mixed = mixed?
|
|
159
|
+
|
|
158
160
|
element_order.each do |el|
|
|
159
161
|
if el.text?
|
|
160
|
-
# Text node - yield the text content (skip whitespace-only)
|
|
161
162
|
text = el.text_content
|
|
162
|
-
|
|
163
|
+
if text && !text.empty? && (content_is_mixed || !text.strip.empty?)
|
|
164
|
+
# Mixed content: all text is significant (e.g. "Hello " before <b>)
|
|
165
|
+
# Ordered-only: skip whitespace-only text (indentation between elements)
|
|
166
|
+
yield(text)
|
|
167
|
+
end
|
|
163
168
|
elsif el.element?
|
|
164
169
|
# Element node - look up mapped collection and get next item
|
|
165
170
|
attr_name = element_to_attr[el.name]
|
|
166
171
|
next unless attr_name
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
val = send(attr_name)
|
|
174
|
+
obj = if val.is_a?(Array)
|
|
175
|
+
index = collection_indices[attr_name]
|
|
176
|
+
collection_indices[attr_name] += 1
|
|
177
|
+
val[index]
|
|
178
|
+
elsif val.is_a?(Lutaml::Model::Serializable)
|
|
179
|
+
collection_indices[attr_name] += 1
|
|
180
|
+
collection_indices[attr_name] == 1 ? val : nil
|
|
181
|
+
end
|
|
175
182
|
yield(obj) if obj
|
|
176
183
|
end
|
|
177
184
|
end
|
|
@@ -112,8 +112,6 @@ parent_element_form_default)
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
private
|
|
116
|
-
|
|
117
115
|
# Create element for nested model
|
|
118
116
|
#
|
|
119
117
|
# @param rule [CompiledRule] The rule
|
|
@@ -212,7 +210,7 @@ child_transformation)
|
|
|
212
210
|
# with different URIs) -> child has its own ns, use child's prefix_default
|
|
213
211
|
# - Child's namespace is self-declared through its attribute TYPE (different from parent)
|
|
214
212
|
# -> child's XmlElement gets its own ns, use child's prefix_default
|
|
215
|
-
child_ns_class = if value.
|
|
213
|
+
child_ns_class = if value.is_a?(::Lutaml::Model::Serialize)
|
|
216
214
|
value.class.mappings_for(:xml)&.namespace_class
|
|
217
215
|
end
|
|
218
216
|
ns_prefix = nil
|
|
@@ -84,6 +84,12 @@ register_id, register)
|
|
|
84
84
|
return
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# raw: :element — value is a complete XML element string, inject directly
|
|
88
|
+
if rule.raw == :element
|
|
89
|
+
add_raw_element_fragments(parent, value)
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
87
93
|
# Extract parent's namespace info for element_form_default inheritance
|
|
88
94
|
parent_ns_class = parent.namespace_class
|
|
89
95
|
# Only pass element_form_default VALUE if it was explicitly set
|
|
@@ -201,6 +207,21 @@ register_id)
|
|
|
201
207
|
|
|
202
208
|
private
|
|
203
209
|
|
|
210
|
+
# Add raw element fragments to parent, handling single values and collections.
|
|
211
|
+
#
|
|
212
|
+
# @param parent [XmlElement] Parent element
|
|
213
|
+
# @param value [Object, Array] Raw XML string(s)
|
|
214
|
+
def add_raw_element_fragments(parent, value)
|
|
215
|
+
return if value.nil?
|
|
216
|
+
|
|
217
|
+
items = value.is_a?(Array) ? value : [value]
|
|
218
|
+
items.each do |item|
|
|
219
|
+
next if item.nil? || item.to_s.empty?
|
|
220
|
+
|
|
221
|
+
parent.add_child(::Lutaml::Xml::DataModel::XmlRawFragment.new(item.to_s))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
204
225
|
# Check if rule is custom-method-only (no real attribute)
|
|
205
226
|
#
|
|
206
227
|
# @param rule [CompiledRule] The rule
|
|
@@ -253,6 +253,15 @@ _register)
|
|
|
253
253
|
|
|
254
254
|
private
|
|
255
255
|
|
|
256
|
+
# Resolve the unified raw mode from mapping rule and attribute
|
|
257
|
+
#
|
|
258
|
+
# @param mapping_rule [Xml::MappingRule] The mapping rule
|
|
259
|
+
# @param attr [Attribute, nil] The attribute (nil for custom methods)
|
|
260
|
+
# @return [Symbol, nil] :element, :content, or nil
|
|
261
|
+
def resolve_raw_mode(mapping_rule, attr)
|
|
262
|
+
mapping_rule.raw || (attr&.raw? ? :content : nil)
|
|
263
|
+
end
|
|
264
|
+
|
|
256
265
|
# Infer attribute name from mapping rule or custom methods
|
|
257
266
|
#
|
|
258
267
|
# @param mapping_rule [Xml::MappingRule] The mapping rule
|
|
@@ -321,7 +330,7 @@ register_id, register, attr_name, custom_methods_value)
|
|
|
321
330
|
mapping_type: :element,
|
|
322
331
|
cdata: mapping_rule.cdata,
|
|
323
332
|
mixed_content: mapping_rule.mixed_content?,
|
|
324
|
-
raw: attr
|
|
333
|
+
raw: resolve_raw_mode(mapping_rule, attr),
|
|
325
334
|
render_default: mapping_rule.render_default,
|
|
326
335
|
value_map: value_map,
|
|
327
336
|
custom_methods: custom_methods_value,
|
|
@@ -350,7 +359,7 @@ custom_methods_value)
|
|
|
350
359
|
mapping_type: :element,
|
|
351
360
|
cdata: mapping_rule.cdata,
|
|
352
361
|
mixed_content: mapping_rule.mixed_content?,
|
|
353
|
-
raw:
|
|
362
|
+
raw: mapping_rule.raw,
|
|
354
363
|
render_default: mapping_rule.render_default,
|
|
355
364
|
value_map: value_map,
|
|
356
365
|
custom_methods: custom_methods_value,
|
|
@@ -401,7 +410,7 @@ register_id, register, custom_methods_value)
|
|
|
401
410
|
mapping_type: :element,
|
|
402
411
|
cdata: mapping_rule.cdata,
|
|
403
412
|
mixed_content: mapping_rule.mixed_content?,
|
|
404
|
-
raw: attr
|
|
413
|
+
raw: resolve_raw_mode(mapping_rule, attr),
|
|
405
414
|
render_default: mapping_rule.render_default,
|
|
406
415
|
value_map: value_map,
|
|
407
416
|
custom_methods: custom_methods_value,
|
|
@@ -492,7 +492,9 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
492
492
|
],
|
|
493
493
|
)
|
|
494
494
|
parsed = JSON.parse(parent.to_jsonld)
|
|
495
|
-
parent_resource = parsed["@graph"].find
|
|
495
|
+
parent_resource = parsed["@graph"].find do |r|
|
|
496
|
+
r["@type"] == "skos:Collection"
|
|
497
|
+
end
|
|
496
498
|
expect(parent_resource["member"]).to eq([
|
|
497
499
|
{ "@id" => "http://example.org/item/a" },
|
|
498
500
|
{ "@id" => "http://example.org/item/b" },
|
|
@@ -1111,19 +1111,22 @@ RSpec.describe "MixedContent" do
|
|
|
1111
1111
|
expect(enum).to be_a(Enumerator)
|
|
1112
1112
|
end
|
|
1113
1113
|
|
|
1114
|
-
it "
|
|
1114
|
+
it "yields whitespace text nodes in mixed content" do
|
|
1115
1115
|
parsed = MixedContentSpec::RootMixedContent.from_xml(xml)
|
|
1116
1116
|
|
|
1117
1117
|
results = []
|
|
1118
1118
|
parsed.each_mixed_content do |node|
|
|
1119
|
-
results << node if node.is_a?(String)
|
|
1119
|
+
results << node if node.is_a?(String)
|
|
1120
1120
|
end
|
|
1121
1121
|
|
|
1122
|
-
expect(results).
|
|
1122
|
+
expect(results).not_to be_empty
|
|
1123
|
+
text_joined = results.join
|
|
1124
|
+
expect(text_joined).to include("Hello")
|
|
1125
|
+
expect(text_joined).to include("and")
|
|
1126
|
+
expect(text_joined).to include("!")
|
|
1123
1127
|
end
|
|
1124
1128
|
|
|
1125
1129
|
context "with ordered-only content (no mixed)" do
|
|
1126
|
-
# Test that ordered content (elements only, no text) works
|
|
1127
1130
|
let(:xml) do
|
|
1128
1131
|
<<~XML
|
|
1129
1132
|
<RootMixedContentNested id="outer">
|
|
@@ -1134,15 +1137,17 @@ RSpec.describe "MixedContent" do
|
|
|
1134
1137
|
XML
|
|
1135
1138
|
end
|
|
1136
1139
|
|
|
1137
|
-
it "yields element
|
|
1140
|
+
it "yields element values in document order" do
|
|
1138
1141
|
parsed = MixedContentSpec::RootMixedContentNested.from_xml(xml)
|
|
1139
1142
|
|
|
1140
1143
|
results = []
|
|
1141
1144
|
parsed.content.each_mixed_content { |node| results << node }
|
|
1142
1145
|
|
|
1143
|
-
#
|
|
1146
|
+
# RootMixedContent has mixed_content, so whitespace text nodes
|
|
1147
|
+
# ARE yielded alongside element values
|
|
1144
1148
|
string_results = results.grep(String)
|
|
1145
|
-
expect(string_results
|
|
1149
|
+
expect(string_results).to include("first")
|
|
1150
|
+
expect(string_results).to include("second")
|
|
1146
1151
|
end
|
|
1147
1152
|
end
|
|
1148
1153
|
|
|
@@ -1152,6 +1157,42 @@ RSpec.describe "MixedContent" do
|
|
|
1152
1157
|
expect(parsed.each_mixed_content.to_a).to eq([])
|
|
1153
1158
|
end
|
|
1154
1159
|
end
|
|
1160
|
+
|
|
1161
|
+
context "with ordered-only model (no mixed_content)" do
|
|
1162
|
+
before do
|
|
1163
|
+
stub_const("OrderedOnlyContainer", Class.new(Lutaml::Model::Serializable) do
|
|
1164
|
+
attribute :items, :string, collection: true
|
|
1165
|
+
|
|
1166
|
+
xml do
|
|
1167
|
+
element "container"
|
|
1168
|
+
ordered
|
|
1169
|
+
map_element "item", to: :items
|
|
1170
|
+
end
|
|
1171
|
+
end)
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
it "skips whitespace-only text nodes between elements" do
|
|
1175
|
+
xml = <<~XML
|
|
1176
|
+
<container>
|
|
1177
|
+
<item>first</item>
|
|
1178
|
+
<item>second</item>
|
|
1179
|
+
</container>
|
|
1180
|
+
XML
|
|
1181
|
+
|
|
1182
|
+
parsed = OrderedOnlyContainer.from_xml(xml)
|
|
1183
|
+
results = []
|
|
1184
|
+
parsed.each_mixed_content { |node| results << node }
|
|
1185
|
+
|
|
1186
|
+
# Ordered-only: whitespace between elements is formatting noise, not content
|
|
1187
|
+
whitespace_only = results.select do |n|
|
|
1188
|
+
n.is_a?(String) && n.strip.empty?
|
|
1189
|
+
end
|
|
1190
|
+
expect(whitespace_only).to eq([])
|
|
1191
|
+
|
|
1192
|
+
string_results = results.grep(String)
|
|
1193
|
+
expect(string_results).to eq(%w[first second])
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1155
1196
|
end
|
|
1156
1197
|
|
|
1157
1198
|
# Issue #630: Mutation after deserialization should update serialization output
|