lutaml-model 0.8.15 → 0.8.17
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/release.yml +0 -3
- data/.rubocop_todo.yml +49 -52
- data/README.adoc +188 -25
- data/docs/_guides/xml/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-mapping.adoc +178 -24
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- data/docs/_pages/importable_models.adoc +7 -1
- data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
- data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
- data/docs/namespace-management.adoc +2 -0
- data/docs/xml-schema-qualification.md +6 -0
- data/lib/lutaml/jsonld.rb +1 -4
- data/lib/lutaml/model/attribute.rb +4 -0
- data/lib/lutaml/model/choice.rb +34 -0
- data/lib/lutaml/model/compiled_rule.rb +7 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +3 -1
- data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
- data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
- data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
- data/lib/lutaml/rdf.rb +3 -0
- 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/adapter_element.rb +2 -2
- 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 +39 -26
- data/lib/lutaml/xml/transformation/rule_applier.rb +21 -0
- data/lib/lutaml/xml/transformation/rule_compiler.rb +12 -3
- data/lib/lutaml/yamlld/adapter.rb +25 -0
- data/lib/lutaml/yamlld.rb +25 -0
- data/spec/lutaml/integration/multi_format_spec.rb +23 -0
- data/spec/lutaml/model/attribute_spec.rb +8 -1
- data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
- data/spec/lutaml/model/custom_model_spec.rb +4 -4
- data/spec/lutaml/model/mixed_content_spec.rb +50 -9
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/ordered_content_spec.rb +3 -3
- data/spec/lutaml/model/raw_element_spec.rb +533 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
- data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
- data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
- data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
- data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +18 -2
- data/spec/lutaml/rdf/mapping_spec.rb +2 -1
- data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
- data/spec/lutaml/xml/mapping_spec.rb +4 -4
- data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
- data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
- data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
- data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
- data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
- data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
- data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
- data/spec/lutaml/yamlld/registration_spec.rb +33 -0
- metadata +14 -8
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Lutaml
|
|
6
|
-
module
|
|
7
|
-
class
|
|
8
|
-
def model_to_data(instance,
|
|
9
|
-
mapping = extract_mapping(options)
|
|
4
|
+
module Rdf
|
|
5
|
+
class LinkedDataTransform < Lutaml::Rdf::Transform
|
|
6
|
+
def model_to_data(instance, format, options = {})
|
|
7
|
+
mapping = extract_mapping(format, options)
|
|
10
8
|
return {} unless mapping
|
|
11
9
|
|
|
12
10
|
if mapping.rdf_members.any?
|
|
13
|
-
build_graph_document(mapping, instance)
|
|
11
|
+
build_graph_document(mapping, instance, format)
|
|
14
12
|
else
|
|
15
|
-
build_resource_object(mapping, instance)
|
|
13
|
+
build_resource_object(mapping, instance, format)
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def data_to_model(data,
|
|
20
|
-
mapping = extract_mapping(options)
|
|
17
|
+
def data_to_model(data, format, options = {})
|
|
18
|
+
mapping = extract_mapping(format, options)
|
|
21
19
|
return model_class.new unless mapping
|
|
22
20
|
|
|
23
|
-
hash = data.is_a?(
|
|
21
|
+
hash = data.is_a?(Hash) ? data : {}
|
|
24
22
|
|
|
25
23
|
if hash.key?("@graph") && hash["@graph"].is_a?(Array) && !hash["@graph"].empty?
|
|
26
|
-
|
|
27
|
-
first = graph_data.first
|
|
24
|
+
first = hash["@graph"].first
|
|
28
25
|
hash = first.is_a?(Hash) ? first : {}
|
|
29
26
|
end
|
|
30
27
|
|
|
31
|
-
hash =
|
|
28
|
+
hash = strip_linked_data_keywords(hash)
|
|
32
29
|
|
|
33
30
|
attrs = {}
|
|
34
31
|
mapping.rdf_predicates.each do |rule|
|
|
@@ -53,29 +50,29 @@ module Lutaml
|
|
|
53
50
|
|
|
54
51
|
private
|
|
55
52
|
|
|
56
|
-
def extract_mapping(options)
|
|
57
|
-
options[:mappings] || mappings_for(
|
|
53
|
+
def extract_mapping(format, options)
|
|
54
|
+
options[:mappings] || mappings_for(format, lutaml_register)
|
|
58
55
|
end
|
|
59
56
|
|
|
60
|
-
def build_graph_document(mapping, instance)
|
|
61
|
-
context = build_merged_context_recursive(mapping, instance)
|
|
62
|
-
graph = collect_resources(mapping, instance)
|
|
57
|
+
def build_graph_document(mapping, instance, format)
|
|
58
|
+
context = build_merged_context_recursive(mapping, instance, format)
|
|
59
|
+
graph = collect_resources(mapping, instance, format)
|
|
63
60
|
|
|
64
61
|
{ "@context" => context, "@graph" => graph }
|
|
65
62
|
end
|
|
66
63
|
|
|
67
|
-
def collect_resources(mapping, instance)
|
|
64
|
+
def collect_resources(mapping, instance, format)
|
|
68
65
|
graph = []
|
|
69
66
|
|
|
70
|
-
resource = build_resource_data(mapping, instance)
|
|
67
|
+
resource = build_resource_data(mapping, instance, format)
|
|
71
68
|
graph << resource unless resource.empty?
|
|
72
69
|
|
|
73
70
|
mapping.rdf_members.each do |member_rule|
|
|
74
71
|
each_member(instance, member_rule) do |member|
|
|
75
|
-
member_mapping = member_mapping_for(member,
|
|
72
|
+
member_mapping = member_mapping_for(member, format)
|
|
76
73
|
next unless member_mapping
|
|
77
74
|
|
|
78
|
-
child_resources = collect_resources(member_mapping, member)
|
|
75
|
+
child_resources = collect_resources(member_mapping, member, format)
|
|
79
76
|
graph.concat(child_resources)
|
|
80
77
|
end
|
|
81
78
|
end
|
|
@@ -83,17 +80,18 @@ module Lutaml
|
|
|
83
80
|
graph
|
|
84
81
|
end
|
|
85
82
|
|
|
86
|
-
def build_merged_context_recursive(mapping, instance)
|
|
83
|
+
def build_merged_context_recursive(mapping, instance, format)
|
|
87
84
|
context_hash = build_context_from_mapping(mapping).to_hash
|
|
88
85
|
|
|
89
86
|
mapping.rdf_members.each do |member_rule|
|
|
90
87
|
each_member(instance, member_rule) do |member|
|
|
91
|
-
member_mapping = member_mapping_for(member,
|
|
88
|
+
member_mapping = member_mapping_for(member, format)
|
|
92
89
|
next unless member_mapping
|
|
93
90
|
|
|
94
91
|
context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
|
|
95
92
|
|
|
96
|
-
child_ctx = build_merged_context_recursive(member_mapping, member
|
|
93
|
+
child_ctx = build_merged_context_recursive(member_mapping, member,
|
|
94
|
+
format)
|
|
97
95
|
context_hash.merge!(child_ctx)
|
|
98
96
|
end
|
|
99
97
|
end
|
|
@@ -134,13 +132,13 @@ module Lutaml
|
|
|
134
132
|
end
|
|
135
133
|
end
|
|
136
134
|
|
|
137
|
-
def build_resource_object(mapping, instance)
|
|
135
|
+
def build_resource_object(mapping, instance, format)
|
|
138
136
|
context = build_context_from_mapping(mapping).to_hash
|
|
139
|
-
data = build_resource_data(mapping, instance)
|
|
137
|
+
data = build_resource_data(mapping, instance, format)
|
|
140
138
|
{ "@context" => context }.merge(data)
|
|
141
139
|
end
|
|
142
140
|
|
|
143
|
-
def build_resource_data(mapping, instance)
|
|
141
|
+
def build_resource_data(mapping, instance, format)
|
|
144
142
|
result = {}
|
|
145
143
|
|
|
146
144
|
if mapping.rdf_type.any?
|
|
@@ -166,10 +164,10 @@ module Lutaml
|
|
|
166
164
|
mapping.rdf_members.each do |member_rule|
|
|
167
165
|
next unless member_rule.linked?
|
|
168
166
|
|
|
169
|
-
member_refs = collect_member_references(instance, member_rule)
|
|
167
|
+
member_refs = collect_member_references(instance, member_rule, format)
|
|
170
168
|
next if member_refs.empty?
|
|
171
169
|
|
|
172
|
-
key =
|
|
170
|
+
key = member_key(member_rule)
|
|
173
171
|
result[key] = member_refs
|
|
174
172
|
end
|
|
175
173
|
|
|
@@ -178,10 +176,10 @@ module Lutaml
|
|
|
178
176
|
result
|
|
179
177
|
end
|
|
180
178
|
|
|
181
|
-
def collect_member_references(instance, member_rule)
|
|
179
|
+
def collect_member_references(instance, member_rule, format)
|
|
182
180
|
refs = []
|
|
183
181
|
each_member(instance, member_rule) do |member|
|
|
184
|
-
member_mapping = member_mapping_for(member,
|
|
182
|
+
member_mapping = member_mapping_for(member, format)
|
|
185
183
|
next unless member_mapping
|
|
186
184
|
|
|
187
185
|
refs << { "@id" => resolve_subject_uri(member_mapping, member) }
|
|
@@ -189,7 +187,7 @@ module Lutaml
|
|
|
189
187
|
refs
|
|
190
188
|
end
|
|
191
189
|
|
|
192
|
-
def
|
|
190
|
+
def member_key(member_rule)
|
|
193
191
|
if member_rule.predicate_name
|
|
194
192
|
member_rule.predicate_name.to_s
|
|
195
193
|
elsif member_rule.link.is_a?(String)
|
|
@@ -241,7 +239,7 @@ module Lutaml
|
|
|
241
239
|
end
|
|
242
240
|
end
|
|
243
241
|
|
|
244
|
-
def
|
|
242
|
+
def strip_linked_data_keywords(data)
|
|
245
243
|
return data unless data.is_a?(Hash)
|
|
246
244
|
|
|
247
245
|
data.reject { |key, _| key.start_with?("@") }
|
data/lib/lutaml/rdf.rb
CHANGED
|
@@ -15,5 +15,8 @@ module Lutaml
|
|
|
15
15
|
autoload :MemberRule, "#{__dir__}/rdf/member_rule"
|
|
16
16
|
autoload :Namespaces, "#{__dir__}/rdf/namespaces"
|
|
17
17
|
autoload :Transform, "#{__dir__}/rdf/transform"
|
|
18
|
+
autoload :Context, "#{__dir__}/rdf/context"
|
|
19
|
+
autoload :TermDefinition, "#{__dir__}/rdf/term_definition"
|
|
20
|
+
autoload :LinkedDataTransform, "#{__dir__}/rdf/linked_data_transform"
|
|
18
21
|
end
|
|
19
22
|
end
|
|
@@ -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,
|
|
@@ -129,7 +129,7 @@ module Lutaml
|
|
|
129
129
|
next if attr_is_namespace?(attr)
|
|
130
130
|
|
|
131
131
|
ns_prefix = attr.namespace&.prefix
|
|
132
|
-
ns_prefix = nil if ns_prefix
|
|
132
|
+
ns_prefix = nil if ns_prefix && ns_prefix.empty?
|
|
133
133
|
|
|
134
134
|
attr_name = ns_prefix ? "#{ns_prefix}:#{attr.name}" : attr.name
|
|
135
135
|
|
|
@@ -150,7 +150,7 @@ module Lutaml
|
|
|
150
150
|
next if attr_is_namespace?(attr)
|
|
151
151
|
|
|
152
152
|
ns_prefix = attr.namespace&.prefix
|
|
153
|
-
ns_prefix = nil if ns_prefix
|
|
153
|
+
ns_prefix = nil if ns_prefix && ns_prefix.empty?
|
|
154
154
|
|
|
155
155
|
attr_name = ns_prefix ? "#{ns_prefix}:#{attr.name}" : attr.name
|
|
156
156
|
order << attr_name
|
|
@@ -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
|
|
@@ -253,18 +251,6 @@ child_transformation)
|
|
|
253
251
|
value.xml_namespace_prefix = ns_prefix
|
|
254
252
|
end
|
|
255
253
|
|
|
256
|
-
# Also set @__xml_namespace_prefix on the XmlElement for doubly-defined case.
|
|
257
|
-
# This is read by NamespaceCollector to set NamespaceUsage.used_prefix.
|
|
258
|
-
# For doubly-defined: parent has no ns, child's ns_prefix is from @__xml_ns_prefixes
|
|
259
|
-
# (not from parent's @__xml_namespace_prefix).
|
|
260
|
-
# For mixed content: parent's XmlElement already has @__xml_namespace_prefix set.
|
|
261
|
-
if parent_ns_class.nil? && ns_prefix && !ns_prefix.empty? && !child_ns_class
|
|
262
|
-
# Doubly-defined case: parent has no ns, child has ns class.
|
|
263
|
-
# Set on XmlElement so NamespaceCollector reads it.
|
|
264
|
-
# The value will be set on the model instance above (via @__xml_namespace_prefix)
|
|
265
|
-
# and we'll set it on XmlElement too so collection phase picks it up.
|
|
266
|
-
end
|
|
267
|
-
|
|
268
254
|
child_element = child_transformation.transform(value, child_options)
|
|
269
255
|
|
|
270
256
|
# Dual-namespace support: when the child model was deserialized from an element
|
|
@@ -306,15 +292,10 @@ child_transformation)
|
|
|
306
292
|
child_element.name = rule.serialized_name
|
|
307
293
|
end
|
|
308
294
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if parent_element_form_default == :unqualified &&
|
|
314
|
-
parent_ns_class &&
|
|
315
|
-
child_element.namespace_class == parent_ns_class
|
|
316
|
-
child_element.namespace_class = nil
|
|
317
|
-
end
|
|
295
|
+
propagate_form(child_element, rule)
|
|
296
|
+
apply_unqualified_form_default(
|
|
297
|
+
child_element, rule, parent_ns_class, parent_element_form_default
|
|
298
|
+
)
|
|
318
299
|
|
|
319
300
|
child_element
|
|
320
301
|
end
|
|
@@ -358,7 +339,7 @@ child_transformation)
|
|
|
358
339
|
element.xml_namespace_prefix = model_ns_prefix
|
|
359
340
|
end
|
|
360
341
|
|
|
361
|
-
element
|
|
342
|
+
propagate_form(element, rule)
|
|
362
343
|
text = serialize_value(value, rule, rule.attribute_type, nil)
|
|
363
344
|
element.text_content = text if text
|
|
364
345
|
element
|
|
@@ -492,7 +473,7 @@ register_id)
|
|
|
492
473
|
element.xml_namespace_prefix = ns_prefix
|
|
493
474
|
end
|
|
494
475
|
|
|
495
|
-
element
|
|
476
|
+
propagate_form(element, rule)
|
|
496
477
|
|
|
497
478
|
# Handle Hash type - expand hash into child elements
|
|
498
479
|
if value.is_a?(::Hash) && rule.attribute_type == Lutaml::Model::Type::Hash
|
|
@@ -525,6 +506,38 @@ register_id)
|
|
|
525
506
|
end
|
|
526
507
|
end
|
|
527
508
|
|
|
509
|
+
# Copy the rule's form option onto the produced element so downstream
|
|
510
|
+
# rules (ElementFormOptionRule) can read it. Without this propagation
|
|
511
|
+
# the form option would be lost on transformed/fallback/simple paths.
|
|
512
|
+
#
|
|
513
|
+
# @param element [::Lutaml::Xml::DataModel::XmlElement] The element to update
|
|
514
|
+
# @param rule [CompiledRule] The rule carrying the form option
|
|
515
|
+
def propagate_form(element, rule)
|
|
516
|
+
element.form = rule.form if rule.form_set?
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# W3C elementFormDefault: unqualified override.
|
|
520
|
+
#
|
|
521
|
+
# When the parent schema declares elementFormDefault="unqualified" and
|
|
522
|
+
# a locally-declared child lives in the same namespace as the parent,
|
|
523
|
+
# the child MUST NOT be namespace-qualified (XSD spec). An explicit
|
|
524
|
+
# form: :qualified on the rule is the user's per-element opt-out and
|
|
525
|
+
# takes precedence over the schema default.
|
|
526
|
+
#
|
|
527
|
+
# @param element [::Lutaml::Xml::DataModel::XmlElement] The element to update
|
|
528
|
+
# @param rule [CompiledRule] The rule (checked for explicit form override)
|
|
529
|
+
# @param parent_ns_class [Class, nil] Parent's namespace class
|
|
530
|
+
# @param parent_form_default [Symbol, nil] Parent's element_form_default
|
|
531
|
+
def apply_unqualified_form_default(element, rule, parent_ns_class,
|
|
532
|
+
parent_form_default)
|
|
533
|
+
return if rule.form == :qualified
|
|
534
|
+
return if parent_form_default != :unqualified
|
|
535
|
+
return unless parent_ns_class
|
|
536
|
+
return unless element.namespace_class == parent_ns_class
|
|
537
|
+
|
|
538
|
+
element.namespace_class = nil
|
|
539
|
+
end
|
|
540
|
+
|
|
528
541
|
# Apply nil marker to element if needed
|
|
529
542
|
#
|
|
530
543
|
# @param element [XmlElement] The element
|
|
@@ -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
|