lutaml-model 0.8.4 → 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 +5 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +91 -22
- data/Gemfile +2 -0
- data/README.adoc +114 -2
- data/docs/_guides/index.adoc +18 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -0
- data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
- data/docs/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- data/lib/lutaml/model/format_registry.rb +10 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +6 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -0
- 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/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- 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 +58 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Adapter
|
|
6
|
+
# Builds XML elements from model instances using namespace declaration plans.
|
|
7
|
+
#
|
|
8
|
+
# Handles both ordered and unordered child serialization, nested model
|
|
9
|
+
# elements, simple values, and namespace resolution. This module is the
|
|
10
|
+
# core of model-to-XML conversion when a DeclarationPlan drives the output.
|
|
11
|
+
module PlanBasedBuilder
|
|
12
|
+
# Build element using prepared namespace declaration plan
|
|
13
|
+
#
|
|
14
|
+
# @param xml [Builder] the XML builder
|
|
15
|
+
# @param element [Object] the model instance
|
|
16
|
+
# @param plan [DeclarationPlan] the declaration plan from DeclarationPlanner
|
|
17
|
+
# @param options [Hash] serialization options
|
|
18
|
+
def build_element_with_plan(xml, element, plan, options = {})
|
|
19
|
+
plan ||= DeclarationPlan.empty
|
|
20
|
+
mapper_class = options[:mapper_class] || element.class
|
|
21
|
+
|
|
22
|
+
unless mapper_class.is_a?(Class) &&
|
|
23
|
+
mapper_class.include?(Lutaml::Model::Serialize)
|
|
24
|
+
tag_name = options[:tag_name] || "element"
|
|
25
|
+
xml.create_and_add_element(tag_name) do |inner_xml|
|
|
26
|
+
inner_xml.text(text_content_for_xml(element))
|
|
27
|
+
end
|
|
28
|
+
return xml
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
xml_mapping = mapper_class.mappings_for(:xml)
|
|
32
|
+
return xml unless xml_mapping
|
|
33
|
+
|
|
34
|
+
# TYPE-ONLY MODELS: No element wrapper, serialize children directly
|
|
35
|
+
# BUT if we have a tag_name in options, that means parent wants a wrapper
|
|
36
|
+
if xml_mapping.no_element?
|
|
37
|
+
build_type_only_element(xml, element, xml_mapping, plan, options,
|
|
38
|
+
mapper_class)
|
|
39
|
+
return xml
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Use xmlns declarations from plan
|
|
43
|
+
attributes = {}
|
|
44
|
+
|
|
45
|
+
# Apply namespace declarations from plan using extracted module
|
|
46
|
+
attributes.merge!(NamespaceDeclarationBuilder.build_xmlns_attributes(plan))
|
|
47
|
+
|
|
48
|
+
# Collect attribute custom methods to call after element creation
|
|
49
|
+
attribute_custom_methods = []
|
|
50
|
+
|
|
51
|
+
# Add regular attributes (non-xmlns)
|
|
52
|
+
xml_mapping.attributes.each do |attribute_rule|
|
|
53
|
+
next if options[:except]&.include?(attribute_rule.to)
|
|
54
|
+
|
|
55
|
+
# Collect custom methods for later execution (after element is created)
|
|
56
|
+
if attribute_rule.custom_methods[:to]
|
|
57
|
+
attribute_custom_methods << attribute_rule
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
mapping_rule_name = if attribute_rule.multiple_mappings?
|
|
62
|
+
attribute_rule.name.first
|
|
63
|
+
else
|
|
64
|
+
attribute_rule.name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr = attribute_definition_for(element, attribute_rule,
|
|
68
|
+
mapper_class: mapper_class)
|
|
69
|
+
value = attribute_rule.to_value_for(element)
|
|
70
|
+
|
|
71
|
+
# Handle as_list and delimiter BEFORE serialization for array values
|
|
72
|
+
# These features convert arrays to delimited strings before serialization
|
|
73
|
+
if value.is_a?(Array)
|
|
74
|
+
if attribute_rule.as_list && attribute_rule.as_list[:export]
|
|
75
|
+
value = attribute_rule.as_list[:export].call(value)
|
|
76
|
+
elsif attribute_rule.delimiter
|
|
77
|
+
value = value.join(attribute_rule.delimiter)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
value = attr.serialize(value, :xml, register) if attr
|
|
82
|
+
value = ExportTransformer.call(value, attribute_rule, attr,
|
|
83
|
+
format: :xml)
|
|
84
|
+
|
|
85
|
+
if render_element?(attribute_rule, element, value)
|
|
86
|
+
# Resolve attribute namespace using extracted module
|
|
87
|
+
ns_info = AttributeNamespaceResolver.resolve(
|
|
88
|
+
rule: attribute_rule,
|
|
89
|
+
attribute: attr,
|
|
90
|
+
plan: plan,
|
|
91
|
+
mapper_class: mapper_class,
|
|
92
|
+
register: register,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Build qualified attribute name based on W3C semantics
|
|
96
|
+
attr_name = AttributeNamespaceResolver.build_qualified_name(
|
|
97
|
+
ns_info,
|
|
98
|
+
mapping_rule_name,
|
|
99
|
+
attribute_rule,
|
|
100
|
+
)
|
|
101
|
+
attributes[attr_name] = value ? value.to_s : value
|
|
102
|
+
|
|
103
|
+
# Add local xmlns declaration if needed
|
|
104
|
+
if ns_info[:needs_local_declaration]
|
|
105
|
+
attributes[ns_info[:local_xmlns_attr]] =
|
|
106
|
+
ns_info[:local_xmlns_uri]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Add schema_location attribute from ElementNode if present
|
|
112
|
+
attributes.merge!(plan.root_node.schema_location_attr) if plan&.root_node&.schema_location_attr
|
|
113
|
+
|
|
114
|
+
# Determine prefix from plan using extracted module
|
|
115
|
+
prefix_info = ElementPrefixResolver.resolve(mapping: xml_mapping,
|
|
116
|
+
plan: plan)
|
|
117
|
+
prefix = prefix_info[:prefix]
|
|
118
|
+
ns_decl = if xml_mapping.namespace_class
|
|
119
|
+
plan.namespace_for_class(xml_mapping.namespace_class)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if element's own namespace needs local declaration (out of scope)
|
|
123
|
+
if ns_decl&.local_on_use?
|
|
124
|
+
xmlns_attr = prefix ? "xmlns:#{prefix}" : "xmlns"
|
|
125
|
+
attributes[xmlns_attr] = ns_decl.uri
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# W3C COMPLIANCE: Detect if element needs xmlns="" using extracted module
|
|
129
|
+
if BlankNamespaceHandler.needs_xmlns_blank?(mapping: xml_mapping,
|
|
130
|
+
options: options)
|
|
131
|
+
attributes["xmlns"] = ""
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Native type inheritance fix: handle local_on_use xmlns="" even if parents uses default format
|
|
135
|
+
xmlns_prefix = nil
|
|
136
|
+
xmlns_ns = nil
|
|
137
|
+
if xml_mapping&.namespace_class && plan
|
|
138
|
+
xmlns_ns = plan.namespace_for_class(xml_mapping.namespace_class)
|
|
139
|
+
xmlns_prefix = xmlns_ns&.prefix
|
|
140
|
+
end
|
|
141
|
+
if xmlns_ns&.local_on_use? && !xml_mapping.namespace_uri
|
|
142
|
+
attributes["xmlns:#{xmlns_prefix}"] =
|
|
143
|
+
xmlns_ns&.uri || xml_mapping.namespace_uri
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
tag_name = options[:tag_name] || xml_mapping.root_element
|
|
147
|
+
return if options[:except]&.include?(tag_name)
|
|
148
|
+
|
|
149
|
+
# Track if THIS element uses default namespace format
|
|
150
|
+
# Children will need this info to know if they should add xmlns=""
|
|
151
|
+
this_element_uses_default_ns = xml_mapping.namespace_class &&
|
|
152
|
+
plan.namespace_for_class(xml_mapping.namespace_class)&.default_format?
|
|
153
|
+
|
|
154
|
+
# Get element_form_default from this element's namespace for children
|
|
155
|
+
parent_element_form_default = xml_mapping.namespace_class&.element_form_default
|
|
156
|
+
|
|
157
|
+
xml.create_and_add_element(tag_name, attributes: attributes.compact,
|
|
158
|
+
prefix: prefix) do |inner_xml|
|
|
159
|
+
# Call attribute custom methods now that element is created
|
|
160
|
+
attribute_custom_methods.each do |attribute_rule|
|
|
161
|
+
mapper_class.new.send(attribute_rule.custom_methods[:to],
|
|
162
|
+
element, inner_xml.parent, inner_xml)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if ordered?(element, options.merge(mapper_class: mapper_class))
|
|
166
|
+
build_ordered_element_with_plan(inner_xml, element, plan,
|
|
167
|
+
options.merge(
|
|
168
|
+
mapper_class: mapper_class,
|
|
169
|
+
parent_prefix: prefix,
|
|
170
|
+
parent_uses_default_ns: this_element_uses_default_ns,
|
|
171
|
+
parent_element_form_default: parent_element_form_default,
|
|
172
|
+
parent_ns_decl: ns_decl,
|
|
173
|
+
))
|
|
174
|
+
else
|
|
175
|
+
build_unordered_children_with_plan(inner_xml, element, plan,
|
|
176
|
+
options.merge(
|
|
177
|
+
mapper_class: mapper_class,
|
|
178
|
+
parent_prefix: prefix,
|
|
179
|
+
parent_uses_default_ns: this_element_uses_default_ns,
|
|
180
|
+
parent_element_form_default: parent_element_form_default,
|
|
181
|
+
parent_ns_decl: ns_decl,
|
|
182
|
+
))
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Build XML from XmlDataModel::XmlElement structure
|
|
188
|
+
#
|
|
189
|
+
# @param xml [Builder] XML builder
|
|
190
|
+
# @param element [XmlDataModel::XmlElement] element to build
|
|
191
|
+
# @param parent_uses_default_ns [Boolean] parent uses default namespace format
|
|
192
|
+
# @param parent_element_form_default [Symbol] parent's element_form_default
|
|
193
|
+
# @param parent_namespace_class [Class] parent's namespace class
|
|
194
|
+
# @param plan [DeclarationPlan, nil] optional declaration plan for xmlns=""
|
|
195
|
+
# @param xml_mapping [Xml::Mapping] optional mapping for namespace resolution
|
|
196
|
+
def build_xml_element(xml, element, parent_uses_default_ns: false,
|
|
197
|
+
parent_element_form_default: nil, parent_namespace_class: nil, plan: nil, xml_mapping: nil)
|
|
198
|
+
# Prepare attributes hash
|
|
199
|
+
attributes = {}
|
|
200
|
+
|
|
201
|
+
# Get element's namespace class
|
|
202
|
+
element_ns_class = element.namespace_class
|
|
203
|
+
attribute_form_default = element_ns_class&.attribute_form_default || :unqualified
|
|
204
|
+
element_prefix = element_ns_class&.prefix_default
|
|
205
|
+
|
|
206
|
+
# Get element_form_default for children
|
|
207
|
+
this_element_form_default = element_ns_class&.element_form_default || :unqualified
|
|
208
|
+
|
|
209
|
+
# Add regular attributes
|
|
210
|
+
element.attributes.each do |attr|
|
|
211
|
+
# Determine attribute name with namespace consideration
|
|
212
|
+
attr_name = if attr.namespace_class
|
|
213
|
+
# Check if attribute is in SAME namespace as element
|
|
214
|
+
if attr.namespace_class == element_ns_class && attribute_form_default == :unqualified
|
|
215
|
+
# Same namespace + unqualified -> NO prefix (W3C rule)
|
|
216
|
+
attr.name
|
|
217
|
+
else
|
|
218
|
+
# Different namespace OR qualified -> use prefix
|
|
219
|
+
attr_prefix = attr.namespace_class.prefix_default
|
|
220
|
+
attr_prefix ? "#{attr_prefix}:#{attr.name}" : attr.name
|
|
221
|
+
end
|
|
222
|
+
elsif attribute_form_default == :qualified && element_prefix
|
|
223
|
+
# Attribute inherits element's namespace when qualified
|
|
224
|
+
"#{element_prefix}:#{attr.name}"
|
|
225
|
+
else
|
|
226
|
+
# Unqualified attribute
|
|
227
|
+
attr.name
|
|
228
|
+
end
|
|
229
|
+
# Ensure attribute value is a string
|
|
230
|
+
attributes[attr_name] = attr.value.to_s
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Determine element name with namespace prefix
|
|
234
|
+
tag_name = element.name
|
|
235
|
+
|
|
236
|
+
# Priority 2.5: Child namespace different from parent's default namespace
|
|
237
|
+
# MUST use prefix format to distinguish from parent
|
|
238
|
+
child_needs_prefix = if element_ns_class && parent_namespace_class &&
|
|
239
|
+
element_ns_class != parent_namespace_class && parent_uses_default_ns
|
|
240
|
+
element_prefix # Use child's prefix
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# FIX: Read prefix from plan if available, otherwise use fallback logic
|
|
244
|
+
prefix = if child_needs_prefix
|
|
245
|
+
# Priority 2.5 takes precedence
|
|
246
|
+
child_needs_prefix
|
|
247
|
+
elsif plan && element_ns_class
|
|
248
|
+
# Read format decision from DeclarationPlan
|
|
249
|
+
ns_info = ElementPrefixResolver.resolve(
|
|
250
|
+
mapping: xml_mapping,
|
|
251
|
+
plan: plan,
|
|
252
|
+
)
|
|
253
|
+
ns_info[:prefix]
|
|
254
|
+
elsif element_ns_class && element_prefix
|
|
255
|
+
# Fallback: Element has explicit prefix_default - use prefix format
|
|
256
|
+
element_prefix
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Track if THIS element uses default namespace format for children
|
|
260
|
+
this_element_uses_default_ns = false
|
|
261
|
+
|
|
262
|
+
# Add namespace declaration if element has namespace
|
|
263
|
+
if element.namespace_class
|
|
264
|
+
ns_uri = element.namespace_class.uri
|
|
265
|
+
|
|
266
|
+
# Check if namespace is already declared by parent (hoisting optimization)
|
|
267
|
+
# This works for BOTH default and prefix format parents
|
|
268
|
+
ns_already_declared = parent_namespace_class && parent_namespace_class.uri == ns_uri
|
|
269
|
+
|
|
270
|
+
if prefix && !ns_already_declared
|
|
271
|
+
attributes["xmlns:#{prefix}"] = ns_uri
|
|
272
|
+
# W3C Compliance: xmlns="" only needed for blank namespace children
|
|
273
|
+
# Prefixed children are already in different namespace from parent's default
|
|
274
|
+
elsif !prefix && !ns_already_declared
|
|
275
|
+
attributes["xmlns"] = ns_uri
|
|
276
|
+
this_element_uses_default_ns = true
|
|
277
|
+
end
|
|
278
|
+
elsif plan && DeclarationPlanQuery.element_needs_xmlns_blank?(plan,
|
|
279
|
+
element)
|
|
280
|
+
# W3C Compliance: Element has no namespace (blank namespace)
|
|
281
|
+
attributes["xmlns"] = ""
|
|
282
|
+
elsif !plan
|
|
283
|
+
# Fallback logic when no plan is available
|
|
284
|
+
if parent_uses_default_ns
|
|
285
|
+
if parent_element_form_default == :qualified
|
|
286
|
+
# Child should INHERIT parent's namespace - no xmlns="" needed
|
|
287
|
+
else
|
|
288
|
+
# Parent's element_form_default is :unqualified - child in blank namespace
|
|
289
|
+
attributes["xmlns"] = ""
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Check if element was created from nil value with render_nil option
|
|
295
|
+
if element.respond_to?(:xsi_nil) && element.xsi_nil
|
|
296
|
+
attributes["xsi:nil"] = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Create element
|
|
300
|
+
xml.create_and_add_element(tag_name, attributes: attributes,
|
|
301
|
+
prefix: prefix) do |inner_xml|
|
|
302
|
+
# Handle raw content (map_all directive)
|
|
303
|
+
has_raw_content = false
|
|
304
|
+
if element.respond_to?(:raw_content)
|
|
305
|
+
raw_content = element.raw_content
|
|
306
|
+
if raw_content && !raw_content.to_s.empty?
|
|
307
|
+
inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
|
|
308
|
+
has_raw_content = true
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Skip text content and children if we have raw content
|
|
313
|
+
unless has_raw_content
|
|
314
|
+
# Add text content if present
|
|
315
|
+
if element.text_content
|
|
316
|
+
if element.cdata
|
|
317
|
+
inner_xml.cdata(element.text_content.to_s)
|
|
318
|
+
else
|
|
319
|
+
inner_xml.text(text_content_for_xml(element.text_content))
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Recursively build child elements, passing namespace context and plan
|
|
324
|
+
element.children.each do |child|
|
|
325
|
+
case child
|
|
326
|
+
when Lutaml::Xml::DataModel::XmlElement
|
|
327
|
+
build_xml_element(inner_xml, child,
|
|
328
|
+
parent_uses_default_ns: this_element_uses_default_ns,
|
|
329
|
+
parent_element_form_default: this_element_form_default,
|
|
330
|
+
parent_namespace_class: element_ns_class,
|
|
331
|
+
plan: plan,
|
|
332
|
+
xml_mapping: xml_mapping)
|
|
333
|
+
when Lutaml::Xml::DataModel::XmlComment
|
|
334
|
+
inner_xml.add_comment(child.content)
|
|
335
|
+
when String
|
|
336
|
+
if element.cdata
|
|
337
|
+
inner_xml.cdata(child.to_s)
|
|
338
|
+
else
|
|
339
|
+
inner_xml.text(text_content_for_xml(child))
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Build unordered child elements using prepared namespace declaration plan
|
|
348
|
+
#
|
|
349
|
+
# @param xml [Builder] the XML builder
|
|
350
|
+
# @param element [Object] the model instance
|
|
351
|
+
# @param plan [DeclarationPlan, Hash] the declaration plan
|
|
352
|
+
# @param options [Hash] serialization options
|
|
353
|
+
def build_unordered_children_with_plan(xml, element, plan, options)
|
|
354
|
+
mapper_class = options[:mapper_class] || element.class
|
|
355
|
+
xml_mapping = mapper_class.mappings_for(:xml)
|
|
356
|
+
|
|
357
|
+
# Process child elements with their plans (INCLUDING raw_mapping for map all)
|
|
358
|
+
mappings = xml_mapping.elements + [xml_mapping.raw_mapping].compact
|
|
359
|
+
mappings.each do |element_rule|
|
|
360
|
+
next if options[:except]&.include?(element_rule.to)
|
|
361
|
+
|
|
362
|
+
# Handle custom methods
|
|
363
|
+
if element_rule.custom_methods[:to]
|
|
364
|
+
mapper_class.new.send(element_rule.custom_methods[:to], element,
|
|
365
|
+
xml.parent, xml)
|
|
366
|
+
next
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
attribute_def = attribute_definition_for(element, element_rule,
|
|
370
|
+
mapper_class: mapper_class)
|
|
371
|
+
|
|
372
|
+
# For delegated attributes, attribute_def might be nil
|
|
373
|
+
next unless attribute_def || element_rule.delegate
|
|
374
|
+
|
|
375
|
+
value = attribute_value_for(element, element_rule)
|
|
376
|
+
next unless element_rule.render?(value, element)
|
|
377
|
+
|
|
378
|
+
# Get child's plan if available
|
|
379
|
+
child_plan = child_plan_for(plan, element_rule.to)
|
|
380
|
+
|
|
381
|
+
# Check if value is a Collection instance
|
|
382
|
+
is_collection_instance = value.is_a?(Lutaml::Model::Collection)
|
|
383
|
+
|
|
384
|
+
if value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
|
|
385
|
+
handle_nested_elements_with_plan(
|
|
386
|
+
xml,
|
|
387
|
+
value,
|
|
388
|
+
element_rule,
|
|
389
|
+
attribute_def,
|
|
390
|
+
child_plan,
|
|
391
|
+
options,
|
|
392
|
+
parent_plan: plan,
|
|
393
|
+
)
|
|
394
|
+
elsif element_rule.delegate && attribute_def.nil?
|
|
395
|
+
# Handle non-model values (strings, etc.) for delegated attributes
|
|
396
|
+
add_simple_value(xml, element_rule, value, nil, plan: plan,
|
|
397
|
+
mapping: xml_mapping, options: options)
|
|
398
|
+
else
|
|
399
|
+
add_simple_value(xml, element_rule, value, attribute_def,
|
|
400
|
+
plan: plan, mapping: xml_mapping, options: options)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Process content mapping
|
|
405
|
+
process_content_mapping(element, xml_mapping.content_mapping,
|
|
406
|
+
xml, mapper_class)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Build ordered child elements using prepared namespace declaration plan
|
|
410
|
+
#
|
|
411
|
+
# @param xml [Builder] the XML builder
|
|
412
|
+
# @param element [Object] the model instance
|
|
413
|
+
# @param plan [DeclarationPlan, Hash] the declaration plan
|
|
414
|
+
# @param options [Hash] serialization options
|
|
415
|
+
def build_ordered_element_with_plan(xml, element, plan, options)
|
|
416
|
+
mapper_class = options[:mapper_class] || element.class
|
|
417
|
+
xml_mapping = mapper_class.mappings_for(:xml)
|
|
418
|
+
|
|
419
|
+
index_hash = {}
|
|
420
|
+
content = []
|
|
421
|
+
|
|
422
|
+
element.element_order.each do |object|
|
|
423
|
+
object_key = "#{object.name}-#{object.type}"
|
|
424
|
+
index_hash[object_key] ||= -1
|
|
425
|
+
curr_index = index_hash[object_key] += 1
|
|
426
|
+
|
|
427
|
+
element_rule = xml_mapping.find_by_name(object.name,
|
|
428
|
+
type: object.type,
|
|
429
|
+
node_type: object.node_type,
|
|
430
|
+
namespace_uri: object.namespace_uri)
|
|
431
|
+
next if element_rule.nil? || options[:except]&.include?(element_rule.to)
|
|
432
|
+
|
|
433
|
+
# Handle custom methods
|
|
434
|
+
if element_rule.custom_methods[:to]
|
|
435
|
+
mapper_class.new.send(element_rule.custom_methods[:to], element,
|
|
436
|
+
xml.parent, xml)
|
|
437
|
+
next
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Get attribute definition and value (handle delegation)
|
|
441
|
+
attribute_def, value = fetch_attribute_and_value(element,
|
|
442
|
+
element_rule, mapper_class)
|
|
443
|
+
|
|
444
|
+
next if element_rule == xml_mapping.content_mapping && element_rule.cdata && object.text?
|
|
445
|
+
|
|
446
|
+
if element_rule == xml_mapping.content_mapping
|
|
447
|
+
process_ordered_content(element, xml_mapping, xml, curr_index,
|
|
448
|
+
content)
|
|
449
|
+
elsif !value.nil? || element_rule.render_nil?
|
|
450
|
+
process_ordered_element(xml, element, element_rule, attribute_def,
|
|
451
|
+
value, curr_index, plan, xml_mapping, options)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
add_ordered_content(xml, content) unless content.empty?
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
private
|
|
459
|
+
|
|
460
|
+
def build_type_only_element(xml, element, xml_mapping, plan, options,
|
|
461
|
+
mapper_class)
|
|
462
|
+
if options[:tag_name]
|
|
463
|
+
xml.create_and_add_element(options[:tag_name]) do |inner_xml|
|
|
464
|
+
# Serialize type-only model's children inside parent's wrapper
|
|
465
|
+
xml_mapping.elements.each do |element_rule|
|
|
466
|
+
next if options[:except]&.include?(element_rule.to)
|
|
467
|
+
|
|
468
|
+
attribute_def = mapper_class.attributes[element_rule.to]
|
|
469
|
+
next unless attribute_def
|
|
470
|
+
|
|
471
|
+
value = element.send(element_rule.to)
|
|
472
|
+
next unless element_rule.render?(value, element)
|
|
473
|
+
|
|
474
|
+
# For type-only models, children plans may not be available
|
|
475
|
+
# Serialize children directly
|
|
476
|
+
if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
|
|
477
|
+
# Nested model - recursively build it
|
|
478
|
+
child_plan = plan.child_plan(element_rule.to) || DeclarationPlan.empty
|
|
479
|
+
build_element_with_plan(
|
|
480
|
+
inner_xml,
|
|
481
|
+
value,
|
|
482
|
+
child_plan,
|
|
483
|
+
{ mapper_class: attribute_def.type(register),
|
|
484
|
+
tag_name: element_rule.name },
|
|
485
|
+
)
|
|
486
|
+
else
|
|
487
|
+
# Simple value - create element directly
|
|
488
|
+
inner_xml.create_and_add_element(element_rule.name) do
|
|
489
|
+
add_value(inner_xml, value, attribute_def,
|
|
490
|
+
cdata: element_rule.cdata)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
else
|
|
496
|
+
# No wrapper at all - serialize children directly (for root-level type-only)
|
|
497
|
+
xml_mapping.elements.each do |element_rule|
|
|
498
|
+
next if options[:except]&.include?(element_rule.to)
|
|
499
|
+
|
|
500
|
+
attribute_def = mapper_class.attributes[element_rule.to]
|
|
501
|
+
next unless attribute_def
|
|
502
|
+
|
|
503
|
+
value = element.send(element_rule.to)
|
|
504
|
+
next unless element_rule.render?(value, element)
|
|
505
|
+
|
|
506
|
+
child_plan = plan.child_plan(element_rule.to)
|
|
507
|
+
|
|
508
|
+
if value && attribute_def.type(register)&.<=(Lutaml::Model::Serialize)
|
|
509
|
+
handle_nested_elements_with_plan(
|
|
510
|
+
xml,
|
|
511
|
+
value,
|
|
512
|
+
element_rule,
|
|
513
|
+
attribute_def,
|
|
514
|
+
child_plan,
|
|
515
|
+
options,
|
|
516
|
+
)
|
|
517
|
+
else
|
|
518
|
+
add_simple_value(xml, element_rule, value, attribute_def,
|
|
519
|
+
plan: plan, mapping: xml_mapping, options: options)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def handle_nested_elements_with_plan(xml, value, rule, attribute, plan,
|
|
526
|
+
options, parent_plan: nil)
|
|
527
|
+
element_options = options.merge(
|
|
528
|
+
rule: rule,
|
|
529
|
+
attribute: attribute,
|
|
530
|
+
tag_name: rule.name,
|
|
531
|
+
mapper_class: attribute.type(register), # Override with child's type
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Handle Collection instances
|
|
535
|
+
if value.is_a?(Lutaml::Model::Collection)
|
|
536
|
+
build_collection_elements(xml, value, attribute, rule,
|
|
537
|
+
element_options, parent_plan, options)
|
|
538
|
+
return
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
case value
|
|
542
|
+
when Array
|
|
543
|
+
build_array_elements(xml, value, attribute, rule, element_options,
|
|
544
|
+
plan, parent_plan, options)
|
|
545
|
+
else
|
|
546
|
+
build_element_with_plan(xml, value, plan, element_options)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def build_collection_elements(xml, value, attribute, rule,
|
|
551
|
+
element_options, parent_plan, options)
|
|
552
|
+
items = value.collection
|
|
553
|
+
attr_type = attribute.type(register)
|
|
554
|
+
|
|
555
|
+
if attr_type <= Lutaml::Model::Type::Value
|
|
556
|
+
# Simple types - use add_simple_value for each item
|
|
557
|
+
items.each do |val|
|
|
558
|
+
xml_mapping = options[:mapper_class]&.mappings_for(:xml)
|
|
559
|
+
add_simple_value(xml, rule, val, attribute, plan: parent_plan,
|
|
560
|
+
mapping: xml_mapping, options: options)
|
|
561
|
+
end
|
|
562
|
+
else
|
|
563
|
+
# Model types - build elements with plans
|
|
564
|
+
items.each do |val|
|
|
565
|
+
item_plan = plan_for_collection_item(val, attribute, parent_plan,
|
|
566
|
+
options)
|
|
567
|
+
item_mapper_class = if polymorphic_value?(attribute, val)
|
|
568
|
+
val.class
|
|
569
|
+
else
|
|
570
|
+
attribute.type(register)
|
|
571
|
+
end
|
|
572
|
+
item_options = element_options.merge(mapper_class: item_mapper_class)
|
|
573
|
+
build_element_with_plan(xml, val, item_plan, item_options)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def build_array_elements(xml, value, attribute, _rule, element_options,
|
|
579
|
+
_plan, parent_plan, options)
|
|
580
|
+
value.each do |val|
|
|
581
|
+
item_mapper_class = if polymorphic_value?(attribute, val)
|
|
582
|
+
val.class
|
|
583
|
+
else
|
|
584
|
+
attribute.type(register)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
item_plan = plan_for_collection_item(val, attribute, parent_plan,
|
|
588
|
+
options)
|
|
589
|
+
item_options = element_options.merge(mapper_class: item_mapper_class)
|
|
590
|
+
build_element_with_plan(xml, val,
|
|
591
|
+
item_plan || DeclarationPlan.empty, item_options)
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def plan_for_collection_item(val, attribute, parent_plan, options)
|
|
596
|
+
item_mapper_class = if polymorphic_value?(attribute, val)
|
|
597
|
+
val.class
|
|
598
|
+
else
|
|
599
|
+
attribute.type(register)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
item_mapping = item_mapper_class.mappings_for(:xml)
|
|
603
|
+
return nil unless item_mapping
|
|
604
|
+
|
|
605
|
+
# Transform model to XmlElement tree
|
|
606
|
+
transformation = item_mapper_class.transformation_for(:xml, register)
|
|
607
|
+
xml_element = transformation.transform(val, options)
|
|
608
|
+
|
|
609
|
+
# Collect namespace needs from XmlElement tree
|
|
610
|
+
collector = NamespaceCollector.new(register)
|
|
611
|
+
item_needs = collector.collect(xml_element, item_mapping,
|
|
612
|
+
mapper_class: item_mapper_class)
|
|
613
|
+
|
|
614
|
+
# Plan with XmlElement tree (not model instance)
|
|
615
|
+
planner = DeclarationPlanner.new(register)
|
|
616
|
+
planner.plan(xml_element, item_mapping, item_needs,
|
|
617
|
+
parent_plan: parent_plan, options: options)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Add simple (non-model) values to XML
|
|
621
|
+
def add_simple_value(xml, rule, value, attribute, plan: nil,
|
|
622
|
+
mapping: nil, options: {})
|
|
623
|
+
value = rule.render_value_for(value) if rule
|
|
624
|
+
|
|
625
|
+
if value.is_a?(Array)
|
|
626
|
+
if value.empty?
|
|
627
|
+
if rule.render_empty?
|
|
628
|
+
if rule.render_empty_as_nil?
|
|
629
|
+
xml.create_and_add_element(rule.name,
|
|
630
|
+
attributes: { "xsi:nil" => true },
|
|
631
|
+
prefix: nil)
|
|
632
|
+
else
|
|
633
|
+
xml.create_and_add_element(rule.name,
|
|
634
|
+
attributes: nil,
|
|
635
|
+
prefix: nil)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
return
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
value.each do |val|
|
|
642
|
+
add_simple_value(xml, rule, val, attribute, plan: plan,
|
|
643
|
+
mapping: mapping, options: options)
|
|
644
|
+
end
|
|
645
|
+
return
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
resolved_prefix, attributes = resolve_simple_value_namespace(
|
|
649
|
+
rule, attribute, mapping, plan, options
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
render_simple_value_element(xml, rule, value, attribute,
|
|
653
|
+
resolved_prefix, attributes)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def resolve_simple_value_namespace(rule, attribute, mapping, plan,
|
|
657
|
+
options)
|
|
658
|
+
resolver = NamespaceResolver.new(register)
|
|
659
|
+
|
|
660
|
+
# Extract parent_uses_default_ns from options or calculate it
|
|
661
|
+
parent_uses_default_ns = options[:parent_uses_default_ns]
|
|
662
|
+
if parent_uses_default_ns.nil?
|
|
663
|
+
parent_uses_default_ns = if mapping&.namespace_class && plan
|
|
664
|
+
DeclarationPlanQuery.declared_at_root_default_format?(plan,
|
|
665
|
+
mapping.namespace_class)
|
|
666
|
+
else
|
|
667
|
+
false
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Resolve namespace using the resolver
|
|
672
|
+
ns_result = resolver.resolve_for_element(rule, attribute, mapping,
|
|
673
|
+
plan, options)
|
|
674
|
+
resolved_prefix = ns_result[:prefix]
|
|
675
|
+
type_ns_info = ns_result[:ns_info]
|
|
676
|
+
|
|
677
|
+
# CRITICAL FIX: Type namespace format inheritance for namespace_scope
|
|
678
|
+
type_ns_class = if attribute && !rule.namespace_set?
|
|
679
|
+
type_class = attribute.type(register)
|
|
680
|
+
type_class.namespace_class if type_class.is_a?(Class) && type_class <= Lutaml::Model::Type::Value
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
format_from_stored_plan = false
|
|
684
|
+
|
|
685
|
+
if type_ns_class
|
|
686
|
+
check_plan = plan || options[:stored_xml_declaration_plan]
|
|
687
|
+
if check_plan
|
|
688
|
+
stored_ns_decl = check_plan.namespaces.values.find do |decl|
|
|
689
|
+
decl.uri == type_ns_class.uri
|
|
690
|
+
end
|
|
691
|
+
if stored_ns_decl
|
|
692
|
+
resolved_prefix = if stored_ns_decl.local_on_use? || stored_ns_decl.prefix_format?
|
|
693
|
+
stored_ns_decl.prefix
|
|
694
|
+
end
|
|
695
|
+
format_from_stored_plan = true
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# BUG FIX #49: Check if child element is in same namespace as parent
|
|
701
|
+
unless format_from_stored_plan
|
|
702
|
+
element_has_no_explicit_ns = !rule.namespace_set?
|
|
703
|
+
type_class = attribute&.type(register)
|
|
704
|
+
type_has_no_ns = !(type_class.is_a?(Class) && type_class <= Lutaml::Model::Type::Value) ||
|
|
705
|
+
!type_class&.namespace_class
|
|
706
|
+
|
|
707
|
+
parent_ns_class = options[:parent_namespace_class]
|
|
708
|
+
parent_ns_decl = options[:parent_ns_decl]
|
|
709
|
+
parent_ns_uri = parent_ns_class&.uri
|
|
710
|
+
child_ns_uri = ns_result[:uri]
|
|
711
|
+
|
|
712
|
+
resolved_prefix = if element_has_no_explicit_ns && type_has_no_ns
|
|
713
|
+
nil
|
|
714
|
+
elsif parent_ns_class && parent_ns_decl &&
|
|
715
|
+
child_ns_uri && parent_ns_uri &&
|
|
716
|
+
child_ns_uri == parent_ns_uri
|
|
717
|
+
if parent_ns_decl.prefix_format?
|
|
718
|
+
parent_ns_decl.prefix
|
|
719
|
+
end
|
|
720
|
+
else
|
|
721
|
+
ns_result[:prefix]
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Prepare attributes for element creation
|
|
726
|
+
attributes = {}
|
|
727
|
+
|
|
728
|
+
# W3C COMPLIANCE: Use resolver to determine xmlns="" requirement
|
|
729
|
+
if resolver.xmlns_blank_required?(ns_result, parent_uses_default_ns)
|
|
730
|
+
attributes["xmlns"] = ""
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Check if this namespace needs local declaration (out of scope)
|
|
734
|
+
if resolved_prefix && plan&.namespaces
|
|
735
|
+
ns_entry = plan.namespaces.values.find do |ns_decl|
|
|
736
|
+
ns_decl.ns_object.prefix_default == resolved_prefix ||
|
|
737
|
+
(type_ns_info && type_ns_info[:uri] && ns_decl.ns_object.uri == type_ns_info[:uri])
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
if ns_entry&.local_on_use?
|
|
741
|
+
xmlns_attr = resolved_prefix ? "xmlns:#{resolved_prefix}" : "xmlns"
|
|
742
|
+
attributes[xmlns_attr] = ns_entry.ns_object.uri
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
[resolved_prefix, attributes]
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def render_simple_value_element(xml, rule, value, attribute,
|
|
750
|
+
resolved_prefix, attributes)
|
|
751
|
+
if value.nil?
|
|
752
|
+
if rule.render_nil_as_blank? || rule.render_nil_as_empty?
|
|
753
|
+
xml.create_and_add_element(rule.name,
|
|
754
|
+
attributes: attributes.empty? ? nil : attributes,
|
|
755
|
+
prefix: resolved_prefix)
|
|
756
|
+
else
|
|
757
|
+
xml.create_and_add_element(rule.name,
|
|
758
|
+
attributes: attributes.merge({ "xsi:nil" => true }),
|
|
759
|
+
prefix: resolved_prefix)
|
|
760
|
+
end
|
|
761
|
+
elsif ::Lutaml::Model::Utils.uninitialized?(value)
|
|
762
|
+
nil
|
|
763
|
+
elsif ::Lutaml::Model::Utils.empty?(value)
|
|
764
|
+
xml.create_and_add_element(rule.name,
|
|
765
|
+
attributes: attributes.empty? ? nil : attributes,
|
|
766
|
+
prefix: resolved_prefix)
|
|
767
|
+
elsif rule.raw_mapping?
|
|
768
|
+
xml.add_xml_fragment(xml, value)
|
|
769
|
+
elsif value.is_a?(::Hash) && attribute&.type(register) == Lutaml::Model::Type::Hash
|
|
770
|
+
xml.create_and_add_element(rule.name,
|
|
771
|
+
attributes: attributes.empty? ? nil : attributes,
|
|
772
|
+
prefix: resolved_prefix) do
|
|
773
|
+
value.each do |key, val|
|
|
774
|
+
xml.create_and_add_element(key.to_s) do
|
|
775
|
+
xml.add_text(xml, val.to_s)
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
else
|
|
780
|
+
xml.create_and_add_element(rule.name,
|
|
781
|
+
attributes: attributes.empty? ? nil : attributes,
|
|
782
|
+
prefix: resolved_prefix) do
|
|
783
|
+
add_value(xml, value, attribute, cdata: rule.cdata)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Get child plan from parent plan
|
|
789
|
+
#
|
|
790
|
+
# @param plan [DeclarationPlan, nil] the parent plan
|
|
791
|
+
# @param attr_name [Symbol] the attribute name
|
|
792
|
+
# @return [DeclarationPlan, nil] the child plan or nil
|
|
793
|
+
def child_plan_for(plan, attr_name)
|
|
794
|
+
plan&.child_plan(attr_name)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Fetch attribute definition and value, handling delegation
|
|
798
|
+
#
|
|
799
|
+
# @param element [Object] the model instance
|
|
800
|
+
# @param element_rule [MappingRule] the mapping rule
|
|
801
|
+
# @param mapper_class [Class] the mapper class
|
|
802
|
+
# @return [Array<(Attribute, Object)>] attribute definition and value tuple
|
|
803
|
+
def fetch_attribute_and_value(element, element_rule, mapper_class)
|
|
804
|
+
attribute_def = nil
|
|
805
|
+
value = nil
|
|
806
|
+
|
|
807
|
+
if element_rule.delegate
|
|
808
|
+
delegate_obj = element.send(element_rule.delegate)
|
|
809
|
+
if delegate_obj.respond_to?(element_rule.to)
|
|
810
|
+
attribute_def = delegate_obj.class.attributes[element_rule.to]
|
|
811
|
+
value = delegate_obj.send(element_rule.to)
|
|
812
|
+
end
|
|
813
|
+
else
|
|
814
|
+
attribute_def = attribute_definition_for(element, element_rule,
|
|
815
|
+
mapper_class: mapper_class)
|
|
816
|
+
value = attribute_value_for(element, element_rule)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
[attribute_def, value]
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Process content for ordered elements
|
|
823
|
+
#
|
|
824
|
+
# @param element [Object] the model instance
|
|
825
|
+
# @param xml_mapping [Xml::Mapping] the XML mapping
|
|
826
|
+
# @param xml [Builder] the XML builder
|
|
827
|
+
# @param curr_index [Integer] current index in collection
|
|
828
|
+
# @param content [Array] accumulated content strings
|
|
829
|
+
def process_ordered_content(element, xml_mapping, xml, curr_index,
|
|
830
|
+
content)
|
|
831
|
+
text = element.send(xml_mapping.content_mapping.to)
|
|
832
|
+
text = text[curr_index] if text.is_a?(Array)
|
|
833
|
+
|
|
834
|
+
if element.mixed?
|
|
835
|
+
add_mixed_text(xml, text)
|
|
836
|
+
else
|
|
837
|
+
content << text
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# Process a single ordered element
|
|
842
|
+
#
|
|
843
|
+
# @param xml [Builder] the XML builder
|
|
844
|
+
# @param element [Object] the model instance
|
|
845
|
+
# @param element_rule [MappingRule] the mapping rule
|
|
846
|
+
# @param attribute_def [Attribute, nil] the attribute definition
|
|
847
|
+
# @param value [Object] the value
|
|
848
|
+
# @param curr_index [Integer] current index in collection
|
|
849
|
+
# @param plan [DeclarationPlan, Hash] the declaration plan
|
|
850
|
+
# @param xml_mapping [Xml::Mapping] the XML mapping
|
|
851
|
+
# @param options [Hash] serialization options
|
|
852
|
+
def process_ordered_element(xml, element, element_rule, attribute_def,
|
|
853
|
+
value, curr_index, plan, xml_mapping, options)
|
|
854
|
+
# Handle collection values by index
|
|
855
|
+
current_value = if attribute_def&.collection? && value.is_a?(Array)
|
|
856
|
+
value[curr_index]
|
|
857
|
+
elsif attribute_def&.collection? && value.is_a?(Lutaml::Model::Collection)
|
|
858
|
+
value.to_a[curr_index]
|
|
859
|
+
else
|
|
860
|
+
value
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# Get child's plan if available
|
|
864
|
+
child_plan = child_plan_for(plan, element_rule.to)
|
|
865
|
+
|
|
866
|
+
is_collection_instance = current_value.is_a?(Lutaml::Model::Collection)
|
|
867
|
+
|
|
868
|
+
if current_value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
|
|
869
|
+
handle_nested_elements_with_plan(
|
|
870
|
+
xml,
|
|
871
|
+
current_value,
|
|
872
|
+
element_rule,
|
|
873
|
+
attribute_def,
|
|
874
|
+
child_plan,
|
|
875
|
+
options,
|
|
876
|
+
parent_plan: plan,
|
|
877
|
+
)
|
|
878
|
+
else
|
|
879
|
+
# Apply transformations if attribute_def exists
|
|
880
|
+
if attribute_def
|
|
881
|
+
current_value = ExportTransformer.call(current_value,
|
|
882
|
+
element_rule, attribute_def, format: :xml)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# For mixed content, create elements directly
|
|
886
|
+
if element.mixed? && !attribute_def&.raw?
|
|
887
|
+
add_mixed_element(xml, element_rule, current_value, attribute_def,
|
|
888
|
+
plan: plan, mapping: xml_mapping)
|
|
889
|
+
else
|
|
890
|
+
add_simple_value(xml, element_rule, current_value,
|
|
891
|
+
attribute_def, plan: plan, mapping: xml_mapping, options: options)
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
# Add text for mixed content (can be overridden by adapters)
|
|
897
|
+
#
|
|
898
|
+
# @param xml [Builder] the XML builder
|
|
899
|
+
# @param text [String] the text to add
|
|
900
|
+
def add_mixed_text(xml, text)
|
|
901
|
+
xml.add_text(xml, text) unless text.nil? || text.to_s.empty?
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Add element for mixed content (can be overridden by adapters)
|
|
905
|
+
#
|
|
906
|
+
# @param xml [Builder] the XML builder
|
|
907
|
+
# @param element_rule [MappingRule] the element rule
|
|
908
|
+
# @param value [Object] the value to add
|
|
909
|
+
# @param attribute [Attribute, nil] the attribute definition
|
|
910
|
+
# @param plan [DeclarationPlan, Hash, nil] the declaration plan
|
|
911
|
+
# @param mapping [Xml::Mapping] the XML mapping
|
|
912
|
+
def add_mixed_element(xml, element_rule, value, _attribute,
|
|
913
|
+
plan: nil, mapping: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
914
|
+
xml.create_and_add_element(element_rule.name) do |child_element|
|
|
915
|
+
child_element.text(value.to_s) unless ::Lutaml::Model::Utils.empty?(value)
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# Add accumulated content (can be overridden by adapters)
|
|
920
|
+
#
|
|
921
|
+
# @param xml [Builder] the XML builder
|
|
922
|
+
# @param content [Array<String>] accumulated content strings
|
|
923
|
+
def add_ordered_content(xml, content)
|
|
924
|
+
xml.add_text(xml, content.join)
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
end
|