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
|
@@ -19,15 +19,24 @@ module Lutaml
|
|
|
19
19
|
attr_accessor :element_order, :attribute_order, :schema_location,
|
|
20
20
|
:encoding, :doctype
|
|
21
21
|
|
|
22
|
-
# Store pre-collected namespace data for lazy plan building.
|
|
23
|
-
# This is a plain Hash (no adapter objects) collected during from_xml.
|
|
24
|
-
attr_accessor :pending_namespace_data
|
|
25
|
-
|
|
26
22
|
# Store root element reference for truly lazy plan building.
|
|
27
23
|
# Set in :lazy mode during deserialization; consumed on first to_xml.
|
|
28
24
|
# Released after plan is built to allow GC of the DOM tree.
|
|
29
25
|
attr_accessor :pending_plan_root_element
|
|
30
26
|
|
|
27
|
+
# Clear all internal XML parse state on this instance.
|
|
28
|
+
#
|
|
29
|
+
# Call before re-serializing a previously-parsed instance when the
|
|
30
|
+
# serialization context has changed (e.g. different namespace config,
|
|
31
|
+
# different XML document structure).
|
|
32
|
+
#
|
|
33
|
+
# @return [self]
|
|
34
|
+
def clear_xml_parse_state!
|
|
35
|
+
@import_declaration_plan = nil
|
|
36
|
+
@pending_plan_root_element = nil
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
# XML namespace metadata for doubly-defined and alias support.
|
|
32
41
|
# These carry information from deserialization to serialization.
|
|
33
42
|
# Accessor methods use the @__ prefixed ivars for backward compatibility.
|
|
@@ -74,8 +83,8 @@ module Lutaml
|
|
|
74
83
|
# Build or return the cached declaration plan.
|
|
75
84
|
#
|
|
76
85
|
# When import_declaration_plan: :lazy (default), builds the plan from
|
|
77
|
-
#
|
|
78
|
-
#
|
|
86
|
+
# the stored element reference on first call. No-op when no pending
|
|
87
|
+
# element exists (:eager already set, :skip, or programmatic creation).
|
|
79
88
|
#
|
|
80
89
|
# @return [DeclarationPlan, nil] The plan or nil
|
|
81
90
|
def import_declaration_plan
|
|
@@ -178,8 +187,7 @@ module Lutaml
|
|
|
178
187
|
|
|
179
188
|
# Extend INTERNAL_ATTRIBUTES with XML-specific ones
|
|
180
189
|
def pretty_print_instance_variables
|
|
181
|
-
xml_internals = %i[@import_declaration_plan @
|
|
182
|
-
@pending_namespace_data @pending_plan_root_element
|
|
190
|
+
xml_internals = %i[@import_declaration_plan @pending_plan_root_element
|
|
183
191
|
@__xml_namespace_prefix
|
|
184
192
|
@__xml_ns_prefixes @__xml_original_namespace_uri
|
|
185
193
|
@xml_declaration @raw_schema_location]
|
|
@@ -232,11 +240,6 @@ module Lutaml
|
|
|
232
240
|
options[:xml_declaration] = @xml_declaration
|
|
233
241
|
end
|
|
234
242
|
|
|
235
|
-
# Pass input namespaces for Namespace Preservation
|
|
236
|
-
if instance_variable_defined?(:@xml_input_namespaces) && @xml_input_namespaces&.any?
|
|
237
|
-
options[:input_namespaces] = @xml_input_namespaces
|
|
238
|
-
end
|
|
239
|
-
|
|
240
243
|
# Pass stored DeclarationPlan for format preservation.
|
|
241
244
|
if import_declaration_plan
|
|
242
245
|
options[:stored_xml_declaration_plan] = import_declaration_plan
|
|
@@ -253,39 +256,27 @@ module Lutaml
|
|
|
253
256
|
|
|
254
257
|
private
|
|
255
258
|
|
|
256
|
-
# Build declaration plan from
|
|
257
|
-
#
|
|
258
|
-
# on first access.
|
|
259
|
+
# Build declaration plan from stored element reference (lazy mode).
|
|
260
|
+
# Called by import_declaration_plan getter on first access.
|
|
259
261
|
# @return [DeclarationPlan, nil]
|
|
260
262
|
def build_pending_declaration_plan
|
|
261
|
-
|
|
262
|
-
if @pending_plan_root_element
|
|
263
|
-
element = @pending_plan_root_element
|
|
264
|
-
@pending_plan_root_element = nil # Release reference (allows GC of DOM)
|
|
265
|
-
ns_data = Lutaml::Xml::ModelTransform.collect_element_namespaces(element)
|
|
266
|
-
if ns_data && !ns_data.empty?
|
|
267
|
-
xml_mapping = self.class.mappings_for(:xml)
|
|
268
|
-
return Lutaml::Xml::DeclarationPlan.from_input_with_locations(ns_data,
|
|
269
|
-
xml_mapping)
|
|
270
|
-
end
|
|
271
|
-
return nil
|
|
272
|
-
end
|
|
263
|
+
return nil unless @pending_plan_root_element
|
|
273
264
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
element = @pending_plan_root_element
|
|
266
|
+
@pending_plan_root_element = nil
|
|
267
|
+
ns_data = Lutaml::Xml::ModelTransform.collect_element_namespaces(element)
|
|
268
|
+
return nil unless ns_data && !ns_data.empty?
|
|
277
269
|
|
|
278
|
-
@pending_namespace_data = nil
|
|
279
270
|
xml_mapping = self.class.mappings_for(:xml)
|
|
280
|
-
Lutaml::Xml::DeclarationPlan.from_input_with_locations(
|
|
271
|
+
Lutaml::Xml::DeclarationPlan.from_input_with_locations(ns_data,
|
|
281
272
|
xml_mapping)
|
|
282
273
|
end
|
|
283
274
|
|
|
284
275
|
def set_ordering(attrs)
|
|
285
|
-
return unless attrs.
|
|
276
|
+
return unless attrs.is_a?(Lutaml::Xml::XmlElement)
|
|
286
277
|
|
|
287
278
|
@element_order = attrs.item_order
|
|
288
|
-
@attribute_order = attrs.attribute_order
|
|
279
|
+
@attribute_order = attrs.attribute_order
|
|
289
280
|
end
|
|
290
281
|
|
|
291
282
|
def set_schema_location(attrs)
|
|
@@ -13,14 +13,10 @@ module Lutaml
|
|
|
13
13
|
# is called with a block, the new element becomes the context for the duration
|
|
14
14
|
# of the block, allowing nested element creation.
|
|
15
15
|
class CustomMethodWrapper
|
|
16
|
-
# Initialize the wrapper
|
|
17
|
-
#
|
|
18
16
|
# @param parent [XmlDataModel::XmlElement] Parent element to add children to
|
|
19
|
-
|
|
20
|
-
def initialize(parent, rule)
|
|
17
|
+
def initialize(parent)
|
|
21
18
|
@parent = parent
|
|
22
|
-
@
|
|
23
|
-
@context_stack = [parent] # Stack of context elements for nested creation
|
|
19
|
+
@context_stack = [parent]
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
# Get the current context element (top of stack)
|
|
@@ -70,9 +66,12 @@ module Lutaml
|
|
|
70
66
|
|
|
71
67
|
if element_or_string.is_a?(String)
|
|
72
68
|
add_xml_fragment_or_raw_content(parent, element_or_string)
|
|
73
|
-
|
|
74
|
-
# Add as child element
|
|
69
|
+
elsif element_or_string.is_a?(::Lutaml::Xml::DataModel::XmlElement)
|
|
75
70
|
parent.add_child(element_or_string)
|
|
71
|
+
else
|
|
72
|
+
raise TypeError,
|
|
73
|
+
"add_element expects a String or XmlElement, got " \
|
|
74
|
+
"#{element_or_string.class}. Call .to_xml on the element first."
|
|
76
75
|
end
|
|
77
76
|
element_or_string
|
|
78
77
|
end
|
|
@@ -81,7 +80,7 @@ module Lutaml
|
|
|
81
80
|
require "moxml" unless defined?(Moxml)
|
|
82
81
|
fragment_doc = Moxml.new.parse(fragment_string, fragment: true)
|
|
83
82
|
add_fragment_children_to_parent(fragment_doc, parent)
|
|
84
|
-
rescue LoadError
|
|
83
|
+
rescue LoadError
|
|
85
84
|
append_raw_content(parent, fragment_string)
|
|
86
85
|
end
|
|
87
86
|
|
|
@@ -135,12 +134,12 @@ module Lutaml
|
|
|
135
134
|
|
|
136
135
|
# Add text to element (mimics old adapter API)
|
|
137
136
|
#
|
|
138
|
-
# @param element [XmlDataModel::XmlElement, CustomMethodWrapper, nil]
|
|
137
|
+
# @param element [XmlDataModel::XmlElement, CustomMethodWrapper, nil]
|
|
138
|
+
# Element to add text to. When the wrapper itself or nil is passed,
|
|
139
|
+
# text is added to the current context element.
|
|
139
140
|
# @param text [String] Text content
|
|
140
141
|
def add_text(element, text)
|
|
141
|
-
|
|
142
|
-
# or when element is nil (add to current context)
|
|
143
|
-
target = if element.is_a?(CustomMethodWrapper) || element.nil?
|
|
142
|
+
target = if element == self || element.nil?
|
|
144
143
|
current_context
|
|
145
144
|
else
|
|
146
145
|
element
|
|
@@ -169,32 +168,14 @@ module Lutaml
|
|
|
169
168
|
# @yield [ElementWrapper] The created element for customization
|
|
170
169
|
# @return [XmlElement] The created element
|
|
171
170
|
def create_and_add_element(name, attributes: {})
|
|
172
|
-
|
|
173
|
-
element = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
174
|
-
|
|
175
|
-
# Add attributes if provided
|
|
176
|
-
attributes&.each do |attr_name, attr_value|
|
|
177
|
-
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
178
|
-
attr_name.to_s, attr_value.to_s
|
|
179
|
-
)
|
|
180
|
-
element.add_attribute(attr)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Add to current context
|
|
171
|
+
element = self.class.build_element(name, attributes)
|
|
184
172
|
current_context.add_child(element)
|
|
185
173
|
|
|
186
174
|
if block_given?
|
|
187
|
-
# Push this element as the new context for nested operations
|
|
188
175
|
push_context(element)
|
|
189
|
-
|
|
190
176
|
begin
|
|
191
|
-
|
|
192
|
-
wrapped_element = ElementWrapper.new(element, self)
|
|
193
|
-
|
|
194
|
-
# Yield for customization (e.g., adding text, more nested elements)
|
|
195
|
-
yield wrapped_element
|
|
177
|
+
yield ElementWrapper.new(element, self)
|
|
196
178
|
ensure
|
|
197
|
-
# Restore previous context
|
|
198
179
|
pop_context
|
|
199
180
|
end
|
|
200
181
|
end
|
|
@@ -216,13 +197,7 @@ module Lutaml
|
|
|
216
197
|
# @param text [String] Text content
|
|
217
198
|
# @param cdata [Boolean, Hash] Whether to use CDATA (true or {cdata: true})
|
|
218
199
|
def add_text(_self, text, cdata: false)
|
|
219
|
-
|
|
220
|
-
use_cdata = if cdata.is_a?(Hash)
|
|
221
|
-
cdata[:cdata] || false
|
|
222
|
-
else
|
|
223
|
-
cdata
|
|
224
|
-
end
|
|
225
|
-
|
|
200
|
+
use_cdata = cdata.is_a?(Hash) ? cdata[:cdata] || false : cdata
|
|
226
201
|
@element.text_content = text
|
|
227
202
|
@element.cdata = use_cdata
|
|
228
203
|
end
|
|
@@ -234,29 +209,33 @@ module Lutaml
|
|
|
234
209
|
# @yield [ElementWrapper] The created element for customization
|
|
235
210
|
# @return [XmlElement] The created element
|
|
236
211
|
def create_and_add_element(name, attributes: {})
|
|
237
|
-
|
|
238
|
-
child = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
239
|
-
|
|
240
|
-
# Add attributes if provided
|
|
241
|
-
attributes&.each do |attr_name, attr_value|
|
|
242
|
-
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
243
|
-
attr_name.to_s, attr_value.to_s
|
|
244
|
-
)
|
|
245
|
-
child.add_attribute(attr)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Add to this element
|
|
212
|
+
child = CustomMethodWrapper.build_element(name, attributes)
|
|
249
213
|
@element.add_child(child)
|
|
250
214
|
|
|
251
215
|
if block_given?
|
|
252
|
-
|
|
253
|
-
wrapped_child = ElementWrapper.new(child, @parent_wrapper)
|
|
254
|
-
yield wrapped_child
|
|
216
|
+
yield ElementWrapper.new(child, @parent_wrapper)
|
|
255
217
|
end
|
|
256
218
|
|
|
257
219
|
child
|
|
258
220
|
end
|
|
259
221
|
end
|
|
222
|
+
|
|
223
|
+
# Shared factory: create an XmlElement with optional attributes.
|
|
224
|
+
# Public so ElementWrapper can call it without an instance.
|
|
225
|
+
#
|
|
226
|
+
# @param name [String] Element name
|
|
227
|
+
# @param attributes [Hash] Optional attributes
|
|
228
|
+
# @return [DataModel::XmlElement]
|
|
229
|
+
def self.build_element(name, attributes)
|
|
230
|
+
element = Lutaml::Xml::DataModel::XmlElement.new(name)
|
|
231
|
+
attributes&.each do |attr_name, attr_value|
|
|
232
|
+
attr = Lutaml::Xml::DataModel::XmlAttribute.new(
|
|
233
|
+
attr_name.to_s, attr_value.to_s
|
|
234
|
+
)
|
|
235
|
+
element.add_attribute(attr)
|
|
236
|
+
end
|
|
237
|
+
element
|
|
238
|
+
end
|
|
260
239
|
end
|
|
261
240
|
end
|
|
262
241
|
end
|
|
@@ -239,7 +239,7 @@ register_id)
|
|
|
239
239
|
# @param model_class [Class] The model class
|
|
240
240
|
# @param model_instance [Object] The model instance
|
|
241
241
|
def apply_custom_method(parent, rule, model_class, model_instance)
|
|
242
|
-
wrapper = ::Lutaml::Xml::CustomMethodWrapper.new(parent
|
|
242
|
+
wrapper = ::Lutaml::Xml::CustomMethodWrapper.new(parent)
|
|
243
243
|
mapper_instance = model_class.new
|
|
244
244
|
mapper_instance.send(rule.custom_methods[:to], model_instance,
|
|
245
245
|
parent, wrapper)
|
|
@@ -257,33 +257,26 @@ module Lutaml
|
|
|
257
257
|
return @order_cache if @order_cache
|
|
258
258
|
|
|
259
259
|
@order_cache = children.filter_map do |child|
|
|
260
|
-
if child.
|
|
260
|
+
if child.cdata?
|
|
261
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
262
|
+
text_content: child.text,
|
|
263
|
+
node_type: :cdata)
|
|
264
|
+
elsif child.text?
|
|
261
265
|
next if child.text.nil?
|
|
262
266
|
|
|
263
|
-
# For text nodes:
|
|
264
|
-
# - name is "text" for backward compatibility with tests
|
|
265
|
-
# - text_content contains the actual text for round-trip serialization
|
|
266
|
-
# - node_type explicitly marks this as a text node
|
|
267
267
|
Lutaml::Xml::Element.new("Text", "text",
|
|
268
268
|
text_content: child.text,
|
|
269
269
|
node_type: :text)
|
|
270
|
-
elsif child.cdata?
|
|
271
|
-
# For CDATA sections:
|
|
272
|
-
# - name is "#cdata-section" for backward compatibility
|
|
273
|
-
# - text_content contains the actual CDATA content
|
|
274
|
-
# - node_type explicitly marks this as CDATA
|
|
275
|
-
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
276
|
-
text_content: child.text,
|
|
277
|
-
node_type: :cdata)
|
|
278
270
|
elsif child.comment?
|
|
279
271
|
Lutaml::Xml::Element.new("Comment", "comment",
|
|
280
272
|
text_content: child.text,
|
|
281
273
|
node_type: :comment)
|
|
274
|
+
elsif child.processing_instruction?
|
|
275
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
276
|
+
child.unprefixed_name,
|
|
277
|
+
text_content: child.text,
|
|
278
|
+
node_type: :processing_instruction)
|
|
282
279
|
else
|
|
283
|
-
# For regular elements:
|
|
284
|
-
# - name is the actual element name
|
|
285
|
-
# - node_type explicitly marks this as an element
|
|
286
|
-
# - namespace_uri and namespace_prefix preserve namespace info for rule matching
|
|
287
280
|
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
288
281
|
node_type: :element,
|
|
289
282
|
namespace_uri: child.namespace_uri,
|
|
@@ -303,14 +296,14 @@ module Lutaml
|
|
|
303
296
|
|
|
304
297
|
def text
|
|
305
298
|
return @text if children.empty?
|
|
306
|
-
return text_children.map(&:text) if
|
|
299
|
+
return text_children.map(&:text) if content_bearing_children_count > 1
|
|
307
300
|
|
|
308
301
|
text_children.map(&:text).join
|
|
309
302
|
end
|
|
310
303
|
|
|
311
304
|
def cdata
|
|
312
305
|
return @text if children.empty?
|
|
313
|
-
return cdata_children.map(&:text) if
|
|
306
|
+
return cdata_children.map(&:text) if content_bearing_children_count > 1
|
|
314
307
|
|
|
315
308
|
cdata_children.map(&:text).join
|
|
316
309
|
end
|
|
@@ -330,7 +323,8 @@ module Lutaml
|
|
|
330
323
|
|
|
331
324
|
@element_children = children.reject do |child|
|
|
332
325
|
child.is_a?(String) || child.is_a?(Symbol) ||
|
|
333
|
-
(child.is_a?(XmlElement) &&
|
|
326
|
+
(child.is_a?(XmlElement) &&
|
|
327
|
+
(child.text? || child.processing_instruction?))
|
|
334
328
|
end
|
|
335
329
|
end
|
|
336
330
|
|
|
@@ -389,6 +383,8 @@ module Lutaml
|
|
|
389
383
|
|
|
390
384
|
@children_index = {}
|
|
391
385
|
@children.each do |child|
|
|
386
|
+
next if child.is_a?(XmlElement) && child.processing_instruction?
|
|
387
|
+
|
|
392
388
|
key = child.namespaced_name
|
|
393
389
|
@children_index[key] ||= []
|
|
394
390
|
@children_index[key] << child
|
|
@@ -432,6 +428,14 @@ module Lutaml
|
|
|
432
428
|
|
|
433
429
|
private
|
|
434
430
|
|
|
431
|
+
# Count children that bear content (excludes processing instructions).
|
|
432
|
+
# Used to determine if content is mixed (multiple content nodes).
|
|
433
|
+
def content_bearing_children_count
|
|
434
|
+
children.count do |child|
|
|
435
|
+
!child.is_a?(XmlElement) || !child.processing_instruction?
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
435
439
|
# Backward compatibility: infer node_type from name
|
|
436
440
|
# This allows old code that doesn't pass node_type to still work
|
|
437
441
|
def infer_node_type_from_name(name)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/rdf"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "RDF Namespace edge cases" do
|
|
7
|
+
describe "Namespace immutability" do
|
|
8
|
+
it "prevents URI change after initial set" do
|
|
9
|
+
ns_class = Class.new(Lutaml::Rdf::Namespace)
|
|
10
|
+
ns_class.uri "http://example.org/"
|
|
11
|
+
expect { ns_class.uri "http://other.org/" }.to raise_error(FrozenError)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "prevents prefix change after initial set" do
|
|
15
|
+
ns_class = Class.new(Lutaml::Rdf::Namespace)
|
|
16
|
+
ns_class.prefix "ex"
|
|
17
|
+
expect { ns_class.prefix "other" }.to raise_error(FrozenError)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "NamespaceSet collision detection" do
|
|
22
|
+
it "raises when adding two different classes with same prefix" do
|
|
23
|
+
ns1 = Class.new(Lutaml::Rdf::Namespace)
|
|
24
|
+
ns1.uri "http://one.org/"
|
|
25
|
+
ns1.prefix "ex"
|
|
26
|
+
|
|
27
|
+
ns2 = Class.new(Lutaml::Rdf::Namespace)
|
|
28
|
+
ns2.uri "http://two.org/"
|
|
29
|
+
ns2.prefix "ex"
|
|
30
|
+
|
|
31
|
+
set = Lutaml::Rdf::NamespaceSet.new(ns1)
|
|
32
|
+
expect { set.add(ns2) }.to raise_error(ArgumentError, /conflicts/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "allows adding the same class twice" do
|
|
36
|
+
ns = Class.new(Lutaml::Rdf::Namespace)
|
|
37
|
+
ns.uri "http://example.org/"
|
|
38
|
+
ns.prefix "ex"
|
|
39
|
+
|
|
40
|
+
set = Lutaml::Rdf::NamespaceSet.new(ns)
|
|
41
|
+
expect { set.add(ns) }.not_to raise_error
|
|
42
|
+
expect(set.size).to eq(1)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "NamespaceSet edge cases" do
|
|
47
|
+
it "returns nil for unknown prefix lookup" do
|
|
48
|
+
set = Lutaml::Rdf::NamespaceSet.new
|
|
49
|
+
expect(set["unknown"]).to be_nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "returns nil for unknown URI compaction" do
|
|
53
|
+
set = Lutaml::Rdf::NamespaceSet.new
|
|
54
|
+
expect(set.compact("http://unknown.org/thing")).to be_nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "handles empty namespace set" do
|
|
58
|
+
set = Lutaml::Rdf::NamespaceSet.new
|
|
59
|
+
expect(set.size).to eq(0)
|
|
60
|
+
expect(set.empty?).to be(true)
|
|
61
|
+
expect(set.to_a).to eq([])
|
|
62
|
+
expect(set.to_hash).to eq({})
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "returns value as-is when no colon in compact IRI" do
|
|
66
|
+
set = Lutaml::Rdf::NamespaceSet.new
|
|
67
|
+
expect(set.resolve_compact_iri("plain_name")).to eq("plain_name")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "Iri value object edge cases" do
|
|
72
|
+
it "stores frozen string value" do
|
|
73
|
+
iri = Lutaml::Rdf::Iri.new("http://example.org/")
|
|
74
|
+
expect(iri.value).to be_frozen
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "compares with Comparable" do
|
|
78
|
+
a = Lutaml::Rdf::Iri.new("http://a.org/")
|
|
79
|
+
b = Lutaml::Rdf::Iri.new("http://b.org/")
|
|
80
|
+
expect(a < b).to be(true)
|
|
81
|
+
expect(b < a).to be(false)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "returns nil compact when no namespace matches" do
|
|
85
|
+
iri = Lutaml::Rdf::Iri.new("http://unknown.org/thing")
|
|
86
|
+
set = Lutaml::Rdf::NamespaceSet.new
|
|
87
|
+
expect(iri.compact(set)).to be_nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "Literal value object edge cases" do
|
|
92
|
+
it "plain literal has no datatype or language" do
|
|
93
|
+
lit = Lutaml::Rdf::Literal.new("hello")
|
|
94
|
+
expect(lit.datatype).to be_nil
|
|
95
|
+
expect(lit.language).to be_nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "handles empty string value" do
|
|
99
|
+
lit = Lutaml::Rdf::Literal.new("")
|
|
100
|
+
expect(lit.to_turtle).to eq('""')
|
|
101
|
+
expect(lit.to_jsonld_term).to eq("")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "escapes tabs in Turtle output" do
|
|
105
|
+
lit = Lutaml::Rdf::Literal.new("tab\there")
|
|
106
|
+
expect(lit.to_turtle).to include("\\t")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/turtle"
|
|
5
|
+
require "lutaml/jsonld"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "Multi-format model" do
|
|
8
|
+
before do
|
|
9
|
+
stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
10
|
+
uri "http://www.w3.org/2004/02/skos/core#"
|
|
11
|
+
prefix "skos"
|
|
12
|
+
end)
|
|
13
|
+
|
|
14
|
+
stub_const("MultiFormatModel", Class.new(Lutaml::Model::Serializable) do
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :description, :string
|
|
17
|
+
attribute :code, :string
|
|
18
|
+
|
|
19
|
+
json do
|
|
20
|
+
map "name", to: :name
|
|
21
|
+
map "description", to: :description
|
|
22
|
+
map "code", to: :code
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
rdf do
|
|
26
|
+
namespace TestSkosNs
|
|
27
|
+
|
|
28
|
+
subject { |m| "http://example.org/concept/#{m.code}" } # rubocop:disable RSpec/NamedSubject
|
|
29
|
+
|
|
30
|
+
type "skos:Concept"
|
|
31
|
+
|
|
32
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :name
|
|
33
|
+
predicate :definition, namespace: TestSkosNs, to: :description
|
|
34
|
+
predicate :notation, namespace: TestSkosNs, to: :code
|
|
35
|
+
end
|
|
36
|
+
end)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
let(:instance) do
|
|
40
|
+
MultiFormatModel.new(name: "test", description: "desc", code: "42")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "JSON format" do
|
|
44
|
+
it "serializes without @context" do
|
|
45
|
+
json = instance.to_json
|
|
46
|
+
parsed = JSON.parse(json)
|
|
47
|
+
expect(parsed).not_to have_key("@context")
|
|
48
|
+
expect(parsed["name"]).to eq("test")
|
|
49
|
+
expect(parsed["code"]).to eq("42")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "round-trips" do
|
|
53
|
+
restored = MultiFormatModel.from_json(instance.to_json)
|
|
54
|
+
expect(restored.name).to eq("test")
|
|
55
|
+
expect(restored.code).to eq("42")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "JSON-LD format" do
|
|
60
|
+
it "serializes with @type and @id" do
|
|
61
|
+
jsonld = instance.to_jsonld
|
|
62
|
+
parsed = JSON.parse(jsonld)
|
|
63
|
+
expect(parsed["@type"]).to eq("skos:Concept")
|
|
64
|
+
expect(parsed["@id"]).to eq("http://example.org/concept/42")
|
|
65
|
+
expect(parsed["prefLabel"]).to eq("test")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "round-trips" do
|
|
69
|
+
restored = MultiFormatModel.from_jsonld(instance.to_jsonld)
|
|
70
|
+
expect(restored.name).to eq("test")
|
|
71
|
+
expect(restored.code).to eq("42")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "Turtle format" do
|
|
76
|
+
it "serializes with prefixes and type" do
|
|
77
|
+
turtle = instance.to_turtle
|
|
78
|
+
expect(turtle).to include("@prefix skos:")
|
|
79
|
+
expect(turtle).to include("a skos:Concept")
|
|
80
|
+
expect(turtle).to include("<http://example.org/concept/42>")
|
|
81
|
+
expect(turtle).to include("skos:prefLabel \"test\"")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "round-trips" do
|
|
85
|
+
restored = MultiFormatModel.from_turtle(instance.to_turtle)
|
|
86
|
+
expect(restored.name).to eq("test")
|
|
87
|
+
expect(restored.code).to eq("42")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "cross-format independence" do
|
|
92
|
+
it "JSON serialization does not affect JSON-LD" do
|
|
93
|
+
json_parsed = JSON.parse(instance.to_json)
|
|
94
|
+
jsonld_parsed = JSON.parse(instance.to_jsonld)
|
|
95
|
+
expect(json_parsed).not_to have_key("@type")
|
|
96
|
+
expect(jsonld_parsed).to have_key("@type")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "JSON-LD serialization does not affect Turtle" do
|
|
100
|
+
instance.to_jsonld
|
|
101
|
+
turtle = instance.to_turtle
|
|
102
|
+
expect(turtle).not_to include("@context")
|
|
103
|
+
expect(turtle).to include("@prefix")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|