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.
- 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/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/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +9 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Adapter
|
|
6
|
+
# Class methods for parsing XML input.
|
|
7
|
+
#
|
|
8
|
+
# Extracted from BaseAdapter — parsing is a distinct lifecycle phase
|
|
9
|
+
# with no instance state dependency.
|
|
10
|
+
#
|
|
11
|
+
# Subclasses must define:
|
|
12
|
+
# - MOXML_ADAPTER — Moxml adapter class for parsing
|
|
13
|
+
# - PARSED_ELEMENT_CLASS — element wrapper class
|
|
14
|
+
# - PARSE_ERROR_CLASS — error class to rescue (nil to skip)
|
|
15
|
+
# - EMPTY_DOCUMENT_ERROR_MESSAGE — error message for empty docs
|
|
16
|
+
# - EMPTY_DOCUMENT_ERROR_TYPE — :invalid_format or :parse_exception
|
|
17
|
+
module XmlParser
|
|
18
|
+
def parse(xml, options = {})
|
|
19
|
+
parse_encoding = encoding(xml, options)
|
|
20
|
+
raw_xml = xml
|
|
21
|
+
xml = normalize_xml_for_parse(xml)
|
|
22
|
+
parsed = parse_with_moxml(xml, parse_encoding)
|
|
23
|
+
root_element = parsed.root
|
|
24
|
+
|
|
25
|
+
raise_empty_document_error if root_element.nil?
|
|
26
|
+
|
|
27
|
+
root = self::PARSED_ELEMENT_CLASS.new(root_element)
|
|
28
|
+
doc_pis = extract_document_processing_instructions(parsed)
|
|
29
|
+
root.processing_instructions = doc_pis unless doc_pis.empty?
|
|
30
|
+
new(root, parse_encoding, **parse_document_options(raw_xml))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def normalize_xml_for_parse(xml)
|
|
36
|
+
return xml unless xml.is_a?(String)
|
|
37
|
+
return xml if xml.encoding == Encoding::UTF_8 && xml.valid_encoding?
|
|
38
|
+
|
|
39
|
+
if xml.encoding == Encoding::ASCII_8BIT
|
|
40
|
+
normalized_xml = xml.dup
|
|
41
|
+
normalized_xml.force_encoding(Encoding::UTF_8)
|
|
42
|
+
return normalized_xml if normalized_xml.valid_encoding?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
xml.encode(Encoding::UTF_8,
|
|
46
|
+
invalid: :replace,
|
|
47
|
+
undef: :replace,
|
|
48
|
+
replace: "?")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_with_moxml(xml, parse_encoding)
|
|
52
|
+
parse_error_class = self::PARSE_ERROR_CLASS
|
|
53
|
+
unless parse_error_class
|
|
54
|
+
return self::MOXML_ADAPTER.parse(xml,
|
|
55
|
+
encoding: parse_encoding)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
self::MOXML_ADAPTER.parse(xml, encoding: parse_encoding)
|
|
60
|
+
rescue parse_error_class => e
|
|
61
|
+
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_document_options(xml)
|
|
66
|
+
{
|
|
67
|
+
doctype: extract_doctype_from_xml(xml),
|
|
68
|
+
xml_declaration: DeclarationHandler.extract_xml_declaration(xml),
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def raise_empty_document_error
|
|
73
|
+
message = self::EMPTY_DOCUMENT_ERROR_MESSAGE
|
|
74
|
+
|
|
75
|
+
case self::EMPTY_DOCUMENT_ERROR_TYPE
|
|
76
|
+
when :parse_exception
|
|
77
|
+
require "rexml/document"
|
|
78
|
+
raise REXML::ParseException.new(message)
|
|
79
|
+
else
|
|
80
|
+
raise Lutaml::Model::InvalidFormatError.new(:xml, message)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Adapter
|
|
6
|
+
# Handles the XML serialization pipeline.
|
|
7
|
+
#
|
|
8
|
+
# Responsible for converting model instances and XmlElement trees
|
|
9
|
+
# into XML output via builder objects. Includes the top-level
|
|
10
|
+
# `to_xml` entry point and supporting methods for rendering
|
|
11
|
+
# XmlElement structures with namespace declaration plans.
|
|
12
|
+
module XmlSerializer
|
|
13
|
+
# Add text content to XML builder
|
|
14
|
+
#
|
|
15
|
+
# @param xml [Builder] the XML builder
|
|
16
|
+
# @param value [Object] the value to add
|
|
17
|
+
# @param attribute [Attribute, nil] the attribute definition
|
|
18
|
+
# @param cdata [Boolean] whether to use CDATA
|
|
19
|
+
def add_value(xml, value, attribute, cdata: false)
|
|
20
|
+
if !value.nil?
|
|
21
|
+
if attribute.nil?
|
|
22
|
+
# For delegated attributes where attribute is nil, just use the raw value
|
|
23
|
+
xml.add_text(xml, value.to_s, cdata: cdata)
|
|
24
|
+
elsif attribute.transform.is_a?(Class) && attribute.transform < Lutaml::Model::ValueTransformer
|
|
25
|
+
# Value has already been transformed, use it directly
|
|
26
|
+
xml.add_text(xml, value.to_s, cdata: cdata)
|
|
27
|
+
else
|
|
28
|
+
# Normal serialization through attribute type system
|
|
29
|
+
serialized_value = attribute.serialize(value, :xml, register)
|
|
30
|
+
if attribute.raw?
|
|
31
|
+
xml.add_xml_fragment(xml, value)
|
|
32
|
+
elsif serialized_value.is_a?(Hash)
|
|
33
|
+
serialized_value.each do |key, val|
|
|
34
|
+
xml.create_and_add_element(key) do |element|
|
|
35
|
+
element.text(val)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
xml.add_text(xml, serialized_value, cdata: cdata)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_xml(options = {})
|
|
46
|
+
# Accept xml_declaration from options if present (for model serialization)
|
|
47
|
+
@xml_declaration = options[:xml_declaration] if options[:xml_declaration]
|
|
48
|
+
|
|
49
|
+
encoding = determine_encoding(options)
|
|
50
|
+
builder_options = {}
|
|
51
|
+
builder_options[:encoding] = encoding if encoding
|
|
52
|
+
|
|
53
|
+
builder = self.class::BUILDER_CLASS.build(builder_options) do |xml|
|
|
54
|
+
if root.is_a?(self.class::PARSED_ELEMENT_CLASS)
|
|
55
|
+
root.build_xml(xml)
|
|
56
|
+
else
|
|
57
|
+
build_serializable_xml(xml, options)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
finalize_adapter_xml(builder.to_xml, encoding, options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_serializable_xml(xml, options)
|
|
65
|
+
original_model = nil
|
|
66
|
+
xml_element = transformable_xml_element(options) do |model|
|
|
67
|
+
original_model = model
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if xml_element
|
|
71
|
+
render_xml_element(xml, xml_element, original_model, options)
|
|
72
|
+
else
|
|
73
|
+
render_legacy_model(xml, options)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Build XML from XmlDataModel::XmlElement structure with a declaration plan
|
|
78
|
+
#
|
|
79
|
+
# @param builder [Builder] XML builder
|
|
80
|
+
# @param xml_element [XmlDataModel::XmlElement] root element
|
|
81
|
+
# @param plan [DeclarationPlan] the declaration plan
|
|
82
|
+
# @param options [Hash] serialization options
|
|
83
|
+
def build_xml_element_with_plan(builder, xml_element, plan,
|
|
84
|
+
options = {})
|
|
85
|
+
# Add processing instructions before the root element
|
|
86
|
+
if xml_element.respond_to?(:processing_instructions)
|
|
87
|
+
xml_element.processing_instructions.each do |pi|
|
|
88
|
+
builder.add_processing_instruction(pi.target, pi.content)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
build_plan_node(builder, xml_element, plan.root_node, plan: plan,
|
|
93
|
+
options: options)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def transformable_xml_element(options)
|
|
99
|
+
return root if root.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
100
|
+
|
|
101
|
+
mapper_class = options[:mapper_class] || root.class
|
|
102
|
+
xml_mapping = mapper_class.mappings_for(:xml)
|
|
103
|
+
|
|
104
|
+
return nil if xml_mapping.raw_mapping&.custom_methods&.[](:to)
|
|
105
|
+
|
|
106
|
+
yield(root)
|
|
107
|
+
mapper_class.transformation_for(:xml, register).transform(root,
|
|
108
|
+
options)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_xml_element(xml, xml_element, original_model, options)
|
|
112
|
+
mapper_class = options[:mapper_class] || xml_element.class
|
|
113
|
+
mapping = mapper_class.mappings_for(:xml)
|
|
114
|
+
plan = declaration_plan_for(
|
|
115
|
+
xml_element,
|
|
116
|
+
mapping,
|
|
117
|
+
options_with_original_namespace_data(options, original_model,
|
|
118
|
+
xml_element),
|
|
119
|
+
mapper_class,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
render_options = options.merge(is_root_element: true)
|
|
123
|
+
render_options[:original_model] = original_model if original_model
|
|
124
|
+
build_xml_element_with_plan(xml, xml_element, plan, render_options)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render_legacy_model(xml, options)
|
|
128
|
+
mapper_class = options[:mapper_class] || root.class
|
|
129
|
+
xml_mapping = mapper_class.mappings_for(:xml)
|
|
130
|
+
plan = declaration_plan_for(root, xml_mapping, options, mapper_class)
|
|
131
|
+
|
|
132
|
+
build_element_with_plan(xml, root, plan, options)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def declaration_plan_for(element, mapping, options, mapper_class)
|
|
136
|
+
needs = NamespaceCollector.new(register).collect(
|
|
137
|
+
element, mapping, mapper_class: mapper_class
|
|
138
|
+
)
|
|
139
|
+
DeclarationPlanner.new(register).plan(element, mapping, needs,
|
|
140
|
+
options: options)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def options_with_original_namespace_data(options, original_model,
|
|
144
|
+
xml_element)
|
|
145
|
+
original_ns_uris = {}
|
|
146
|
+
stored_plan = nil
|
|
147
|
+
|
|
148
|
+
if original_model
|
|
149
|
+
mapping_for_original = options[:mapper_class]&.mappings_for(:xml) ||
|
|
150
|
+
original_model.class.mappings_for(:xml)
|
|
151
|
+
original_ns_uris = collect_original_namespace_uris(
|
|
152
|
+
original_model, mapping_for_original
|
|
153
|
+
)
|
|
154
|
+
if original_model.is_a?(Lutaml::Model::Serialize)
|
|
155
|
+
stored_plan = original_model.import_declaration_plan
|
|
156
|
+
end
|
|
157
|
+
elsif xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
158
|
+
original_ns_uri = xml_element.original_namespace_uri
|
|
159
|
+
if original_ns_uri
|
|
160
|
+
mapper_class = options[:mapper_class] || xml_element.class
|
|
161
|
+
xml_mapping = begin
|
|
162
|
+
mapper_class.mappings_for(:xml)
|
|
163
|
+
rescue StandardError
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
if xml_mapping&.namespace_class
|
|
167
|
+
canonical_uri = xml_mapping.namespace_class.uri
|
|
168
|
+
if canonical_uri != original_ns_uri
|
|
169
|
+
original_ns_uris[canonical_uri] =
|
|
170
|
+
original_ns_uri
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
options_with_original_ns = options.merge(
|
|
177
|
+
__original_namespace_uris: original_ns_uris,
|
|
178
|
+
)
|
|
179
|
+
if stored_plan
|
|
180
|
+
options_with_original_ns[:stored_xml_declaration_plan] =
|
|
181
|
+
stored_plan
|
|
182
|
+
end
|
|
183
|
+
options_with_original_ns
|
|
184
|
+
end
|
|
185
|
+
|
|
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
|
+
def text_content_for_xml(value)
|
|
206
|
+
::Moxml::Adapter::Base.preprocess_entities(value.to_s)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_plan_node(xml, xml_element, element_node, plan: nil,
|
|
210
|
+
options: {}, previous_sibling_had_xmlns_blank: false)
|
|
211
|
+
qualified_name = element_node.qualified_name
|
|
212
|
+
attributes = {}
|
|
213
|
+
|
|
214
|
+
original_ns_uris = plan&.original_namespace_uris || {}
|
|
215
|
+
element_node.hoisted_declarations.each do |key, uri|
|
|
216
|
+
next if uri == "http://www.w3.org/XML/1998/namespace"
|
|
217
|
+
|
|
218
|
+
effective_uri = if self.class.fpi?(uri)
|
|
219
|
+
self.class.fpi_to_urn(uri)
|
|
220
|
+
else
|
|
221
|
+
original_ns_uris[uri] || uri
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
xmlns_name = key ? "xmlns:#{key}" : "xmlns"
|
|
225
|
+
attributes[xmlns_name] = effective_uri
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
xml_element.attributes.each_with_index do |xml_attr, idx|
|
|
229
|
+
attr_node = element_node.attribute_nodes[idx]
|
|
230
|
+
attributes[attr_node.qualified_name] = xml_attr.value.to_s
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if xml_element.respond_to?(:xsi_nil) && xml_element.xsi_nil
|
|
234
|
+
attributes["xsi:nil"] = "true"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
attributes.merge!(element_node.schema_location_attr) if element_node.schema_location_attr
|
|
238
|
+
needs_xmlns_blank = element_node.needs_xmlns_blank &&
|
|
239
|
+
(options[:pretty] ? !previous_sibling_had_xmlns_blank : true)
|
|
240
|
+
attributes["xmlns"] = "" if needs_xmlns_blank
|
|
241
|
+
|
|
242
|
+
xml.create_and_add_element(qualified_name, attributes: attributes) do
|
|
243
|
+
if xml_element.respond_to?(:raw_content)
|
|
244
|
+
raw_content = xml_element.raw_content
|
|
245
|
+
if raw_content && !raw_content.to_s.empty?
|
|
246
|
+
xml.add_xml_fragment(xml, raw_content.to_s)
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
child_element_index = 0
|
|
252
|
+
previous_child_had_xmlns_blank = false
|
|
253
|
+
xml_element.children.each do |xml_child|
|
|
254
|
+
case xml_child
|
|
255
|
+
when Lutaml::Xml::DataModel::XmlElement
|
|
256
|
+
child_node = element_node.element_nodes[child_element_index]
|
|
257
|
+
child_element_index += 1
|
|
258
|
+
|
|
259
|
+
build_plan_node(
|
|
260
|
+
xml,
|
|
261
|
+
xml_child,
|
|
262
|
+
child_node,
|
|
263
|
+
plan: plan,
|
|
264
|
+
options: options,
|
|
265
|
+
previous_sibling_had_xmlns_blank: previous_child_had_xmlns_blank,
|
|
266
|
+
)
|
|
267
|
+
previous_child_had_xmlns_blank ||= child_node.needs_xmlns_blank
|
|
268
|
+
when Lutaml::Xml::DataModel::XmlComment
|
|
269
|
+
xml.add_comment(xml_child.content)
|
|
270
|
+
when String
|
|
271
|
+
if xml_element.cdata
|
|
272
|
+
xml.cdata(xml_child.to_s)
|
|
273
|
+
else
|
|
274
|
+
xml.text(text_content_for_xml(xml_child))
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if xml_element.text_content
|
|
280
|
+
if xml_element.cdata
|
|
281
|
+
xml.cdata(xml_element.text_content.to_s)
|
|
282
|
+
else
|
|
283
|
+
xml.text(text_content_for_xml(xml_element.text_content))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
data/lib/lutaml/xml/adapter.rb
CHANGED
|
@@ -4,7 +4,6 @@ module Lutaml
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Adapter namespace for XML adapter internal classes
|
|
6
6
|
module Adapter
|
|
7
|
-
autoload :XmlSerialization, "#{__dir__}/adapter/xml_serialization"
|
|
8
7
|
autoload :AdapterHelpers, "#{__dir__}/adapter/adapter_helpers"
|
|
9
8
|
autoload :BaseAdapter, "#{__dir__}/adapter/base_adapter"
|
|
10
9
|
autoload :NamespaceData, "#{__dir__}/adapter/namespace_data"
|
|
@@ -15,6 +15,7 @@ module Lutaml
|
|
|
15
15
|
when Moxml::Cdata then :cdata
|
|
16
16
|
when Moxml::Text then :text
|
|
17
17
|
when Moxml::Comment then :comment
|
|
18
|
+
when Moxml::ProcessingInstruction then :processing_instruction
|
|
18
19
|
else :element
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -50,6 +51,10 @@ module Lutaml
|
|
|
50
51
|
EncodingNormalizer.normalize_to_utf8(node.content)
|
|
51
52
|
when Moxml::Comment
|
|
52
53
|
EncodingNormalizer.normalize_to_utf8(node.content)
|
|
54
|
+
when Moxml::ProcessingInstruction
|
|
55
|
+
EncodingNormalizer.normalize_to_utf8(
|
|
56
|
+
node.content.to_s.sub(/\A\s+/, ""),
|
|
57
|
+
)
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
name = adapter_class.name_of(node)
|
|
@@ -85,6 +90,8 @@ module Lutaml
|
|
|
85
90
|
builder.add_text(builder.current_node, @text.to_s, cdata: true)
|
|
86
91
|
elsif comment?
|
|
87
92
|
builder.add_comment(builder.current_node, @text.to_s)
|
|
93
|
+
elsif processing_instruction?
|
|
94
|
+
builder.add_processing_instruction(name, @text.to_s)
|
|
88
95
|
elsif text? && !element?
|
|
89
96
|
builder.add_text(builder.current_node, build_text_for_xml.to_s)
|
|
90
97
|
else
|
|
@@ -166,7 +173,6 @@ module Lutaml
|
|
|
166
173
|
return [] unless node.children
|
|
167
174
|
|
|
168
175
|
node.children.filter_map do |child|
|
|
169
|
-
next if child.is_a?(Moxml::ProcessingInstruction)
|
|
170
176
|
next if (child.is_a?(Moxml::Text) || child.is_a?(Moxml::Cdata)) && child.content.empty?
|
|
171
177
|
|
|
172
178
|
self.class.new(child, parent: self,
|
|
@@ -96,7 +96,6 @@ module Lutaml
|
|
|
96
96
|
def add_processing_instruction(target, content)
|
|
97
97
|
pi = @doc.create_processing_instruction(target.to_s, content.to_s)
|
|
98
98
|
if current_element.is_a?(Moxml::Document)
|
|
99
|
-
# Add before root element or at end if no root
|
|
100
99
|
root_node = current_element.root
|
|
101
100
|
if root_node
|
|
102
101
|
root_node.add_previous_sibling(pi)
|
|
@@ -82,9 +82,17 @@ module Lutaml
|
|
|
82
82
|
|
|
83
83
|
# Add a child element or text node
|
|
84
84
|
#
|
|
85
|
-
# @param child [XmlElement, String] Child to add
|
|
85
|
+
# @param child [XmlElement, String, XmlComment] Child to add
|
|
86
86
|
# @return [self]
|
|
87
|
+
# @raise [TypeError] if child is not a supported type
|
|
87
88
|
def add_child(child)
|
|
89
|
+
unless child.is_a?(XmlElement) || child.is_a?(String) ||
|
|
90
|
+
child.is_a?(XmlComment)
|
|
91
|
+
raise TypeError,
|
|
92
|
+
"XmlElement#add_child expects XmlElement, String, or " \
|
|
93
|
+
"XmlComment, got #{child.class}"
|
|
94
|
+
end
|
|
95
|
+
|
|
88
96
|
@children << child
|
|
89
97
|
self
|
|
90
98
|
end
|
data/lib/lutaml/xml/document.rb
CHANGED
|
@@ -98,6 +98,7 @@ module Lutaml
|
|
|
98
98
|
|
|
99
99
|
element.children.each do |child|
|
|
100
100
|
next if child.respond_to?(:comment?) && child.comment?
|
|
101
|
+
next if child.respond_to?(:processing_instruction?) && child.processing_instruction?
|
|
101
102
|
|
|
102
103
|
if klass&.<= Serialize
|
|
103
104
|
attr = klass.attribute_for_child(self.class.name_of(child),
|
|
@@ -252,7 +253,8 @@ module Lutaml
|
|
|
252
253
|
# EntityReference nodes are text-like and should not trigger Array return.
|
|
253
254
|
# For text + entity without elements, return joined String.
|
|
254
255
|
has_element_children = @root.children.any? do |child|
|
|
255
|
-
!child.text? && !entity_reference_node?(child)
|
|
256
|
+
!child.text? && !entity_reference_node?(child) &&
|
|
257
|
+
!(child.respond_to?(:processing_instruction?) && child.processing_instruction?)
|
|
256
258
|
end
|
|
257
259
|
return @root.text_children.map(&:text) if has_element_children
|
|
258
260
|
|
data/lib/lutaml/xml/element.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Lutaml
|
|
|
11
11
|
# @param type [String] "Text" or "Element" (deprecated, use node_type)
|
|
12
12
|
# @param name [String] The element name or text marker
|
|
13
13
|
# @param text_content [String, nil] Actual text content for text nodes
|
|
14
|
-
# @param node_type [Symbol, nil] The node type (:text, :cdata, :element, :comment)
|
|
14
|
+
# @param node_type [Symbol, nil] The node type (:text, :cdata, :element, :comment, :processing_instruction)
|
|
15
15
|
# @param namespace_uri [String, nil] The namespace URI of this element
|
|
16
16
|
# @param namespace_prefix [String, nil] The namespace prefix of this element
|
|
17
17
|
def initialize(type, name, text_content: nil, node_type: nil,
|
|
@@ -38,18 +38,21 @@ module Lutaml
|
|
|
38
38
|
@node_type == :cdata
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Check if this is a comment node
|
|
42
|
-
def comment?
|
|
43
|
-
@node_type == :comment
|
|
44
|
-
end
|
|
45
|
-
|
|
46
41
|
# Check if this is a regular element
|
|
47
42
|
def element?
|
|
48
43
|
@node_type == :element
|
|
49
44
|
end
|
|
50
45
|
|
|
46
|
+
def processing_instruction?
|
|
47
|
+
@node_type == :processing_instruction
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def comment?
|
|
51
|
+
@node_type == :comment
|
|
52
|
+
end
|
|
53
|
+
|
|
51
54
|
def element_tag
|
|
52
|
-
@name unless text? || cdata? || comment?
|
|
55
|
+
@name unless text? || cdata? || comment? || processing_instruction?
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def eql?(other)
|
|
@@ -76,14 +79,14 @@ module Lutaml
|
|
|
76
79
|
def infer_node_type(type, name)
|
|
77
80
|
return :text if type == "Text" && name != "#cdata-section"
|
|
78
81
|
return :cdata if name == "#cdata-section" || (type == "Text" && name == "#cdata-section")
|
|
79
|
-
return :
|
|
82
|
+
return :processing_instruction if type == "ProcessingInstruction"
|
|
80
83
|
|
|
81
84
|
:element
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
def register_liquid_methods
|
|
85
|
-
%i[text?
|
|
86
|
-
cdata? namespace_uri namespace_prefix].each do |attr_name|
|
|
88
|
+
%i[text? element_tag type name text_content node_type
|
|
89
|
+
cdata? processing_instruction? namespace_uri namespace_prefix].each do |attr_name|
|
|
87
90
|
self.class.register_drop_method(attr_name)
|
|
88
91
|
end
|
|
89
92
|
|
|
@@ -262,7 +262,7 @@ module Lutaml
|
|
|
262
262
|
parent = klass.superclass
|
|
263
263
|
return nil unless parent < Lutaml::Model::Serializable
|
|
264
264
|
|
|
265
|
-
parent_mapping = parent.mappings[:xml]
|
|
265
|
+
parent_mapping = parent.mappings[:xml]
|
|
266
266
|
return parent if parent_mapping
|
|
267
267
|
|
|
268
268
|
superclass_with_xml_mapping(parent)
|
|
@@ -274,7 +274,7 @@ module Lutaml
|
|
|
274
274
|
all_ns = (existing_ns + parent_ns).uniq
|
|
275
275
|
@xml_mapping.namespace_scope(all_ns) if all_ns.any?
|
|
276
276
|
|
|
277
|
-
if parent_mapping.
|
|
277
|
+
if parent_mapping.is_a?(Lutaml::Xml::Mapping) &&
|
|
278
278
|
(parent_ns_config = parent_mapping.namespace_scope_config) &&
|
|
279
279
|
parent_ns_config.any?
|
|
280
280
|
existing_ns_config = @xml_mapping.namespace_scope_config || []
|
|
@@ -284,59 +284,36 @@ module Lutaml
|
|
|
284
284
|
end
|
|
285
285
|
|
|
286
286
|
def inherit_xml_elements(parent_mapping)
|
|
287
|
-
parent_mapping
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@xml_mapping.elements_hash[key] =
|
|
291
|
-
rule.deep_dup
|
|
292
|
-
elsif existing.is_a?(Array) && rule.is_a?(Array)
|
|
293
|
-
merged = existing + rule.reject do |r|
|
|
294
|
-
existing.any? do |e|
|
|
295
|
-
e.eql?(r)
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
@xml_mapping.elements_hash[key] = merged
|
|
299
|
-
elsif existing.is_a?(Array)
|
|
300
|
-
existing << rule.deep_dup unless existing.any? do |e|
|
|
301
|
-
e.eql?(rule)
|
|
302
|
-
end
|
|
303
|
-
elsif rule.is_a?(Array)
|
|
304
|
-
unless rule.any? { |r| r.eql?(existing) }
|
|
305
|
-
@xml_mapping.elements_hash[key] =
|
|
306
|
-
[existing, *rule]
|
|
307
|
-
end
|
|
308
|
-
elsif !existing.eql?(rule)
|
|
309
|
-
@xml_mapping.elements_hash[key] =
|
|
310
|
-
[existing, rule.deep_dup]
|
|
311
|
-
end
|
|
312
|
-
end
|
|
287
|
+
inherit_xml_mapping_hash(parent_mapping,
|
|
288
|
+
:mapping_elements_hash,
|
|
289
|
+
@xml_mapping.elements_hash)
|
|
313
290
|
end
|
|
314
291
|
|
|
315
292
|
def inherit_xml_attributes(parent_mapping)
|
|
316
|
-
parent_mapping
|
|
317
|
-
|
|
293
|
+
inherit_xml_mapping_hash(parent_mapping,
|
|
294
|
+
:mapping_attributes_hash,
|
|
295
|
+
@xml_mapping.attributes_hash)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def inherit_xml_mapping_hash(parent_mapping, source_method, target_hash)
|
|
299
|
+
parent_mapping.public_send(source_method).each do |key, rule|
|
|
300
|
+
existing = target_hash[key]
|
|
318
301
|
if existing.nil?
|
|
319
|
-
|
|
320
|
-
rule.deep_dup
|
|
302
|
+
target_hash[key] = rule.deep_dup
|
|
321
303
|
elsif existing.is_a?(Array) && rule.is_a?(Array)
|
|
322
|
-
|
|
323
|
-
existing.any?
|
|
324
|
-
e.eql?(r)
|
|
325
|
-
end
|
|
304
|
+
target_hash[key] = existing + rule.reject do |r|
|
|
305
|
+
existing.any? { |e| e.eql?(r) }
|
|
326
306
|
end
|
|
327
|
-
@xml_mapping.attributes_hash[key] = merged
|
|
328
307
|
elsif existing.is_a?(Array)
|
|
329
308
|
existing << rule.deep_dup unless existing.any? do |e|
|
|
330
309
|
e.eql?(rule)
|
|
331
310
|
end
|
|
332
311
|
elsif rule.is_a?(Array)
|
|
333
|
-
unless rule.any?
|
|
334
|
-
|
|
335
|
-
[existing, *rule]
|
|
312
|
+
target_hash[key] = [existing, *rule] unless rule.any? do |r|
|
|
313
|
+
r.eql?(existing)
|
|
336
314
|
end
|
|
337
315
|
elsif !existing.eql?(rule)
|
|
338
|
-
|
|
339
|
-
[existing, rule.deep_dup]
|
|
316
|
+
target_hash[key] = [existing, rule.deep_dup]
|
|
340
317
|
end
|
|
341
318
|
end
|
|
342
319
|
end
|