lutaml-model 0.8.9 → 0.8.11

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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +1 -0
  3. data/.rubocop_todo.yml +52 -22
  4. data/README.adoc +43 -0
  5. data/docs/_guides/jsonld-serialization.adoc +3 -1
  6. data/docs/_guides/rdf-serialization.adoc +94 -8
  7. data/docs/_guides/turtle-serialization.adoc +17 -4
  8. data/lib/lutaml/jsonld/transform.rb +70 -24
  9. data/lib/lutaml/key_value/transform.rb +5 -5
  10. data/lib/lutaml/key_value/transformation/collection_serializer.rb +25 -11
  11. data/lib/lutaml/key_value/transformation/value_serializer.rb +7 -7
  12. data/lib/lutaml/key_value/transformation.rb +27 -17
  13. data/lib/lutaml/model/adapter_resolver.rb +4 -6
  14. data/lib/lutaml/model/attribute.rb +26 -23
  15. data/lib/lutaml/model/cached_type_resolver.rb +10 -9
  16. data/lib/lutaml/model/cli.rb +1 -1
  17. data/lib/lutaml/model/collection.rb +4 -4
  18. data/lib/lutaml/model/comparable_model.rb +11 -11
  19. data/lib/lutaml/model/config.rb +1 -1
  20. data/lib/lutaml/model/consolidation/dispatcher.rb +1 -1
  21. data/lib/lutaml/model/consolidation/pattern_chunker.rb +3 -3
  22. data/lib/lutaml/model/format_registry.rb +6 -4
  23. data/lib/lutaml/model/global_context.rb +2 -2
  24. data/lib/lutaml/model/global_register.rb +1 -1
  25. data/lib/lutaml/model/instrumentation.rb +1 -1
  26. data/lib/lutaml/model/mapping/mapping_rule.rb +3 -3
  27. data/lib/lutaml/model/mapping/model_mapping.rb +1 -1
  28. data/lib/lutaml/model/mapping/model_mapping_rule.rb +1 -1
  29. data/lib/lutaml/model/register.rb +3 -3
  30. data/lib/lutaml/model/render_policy.rb +11 -17
  31. data/lib/lutaml/model/runtime_compatibility.rb +0 -1
  32. data/lib/lutaml/model/schema/xml_compiler/group.rb +1 -1
  33. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +1 -1
  34. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -2
  35. data/lib/lutaml/model/schema/xml_compiler.rb +14 -14
  36. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -1
  37. data/lib/lutaml/model/serialize/deserialization_context.rb +50 -0
  38. data/lib/lutaml/model/serialize/format_conversion.rb +2 -2
  39. data/lib/lutaml/model/serialize/initialization.rb +44 -7
  40. data/lib/lutaml/model/serialize/model_import.rb +1 -1
  41. data/lib/lutaml/model/serialize.rb +8 -1
  42. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -1
  43. data/lib/lutaml/model/store.rb +77 -24
  44. data/lib/lutaml/model/transformation_registry.rb +1 -1
  45. data/lib/lutaml/model/type_context.rb +7 -1
  46. data/lib/lutaml/model/type_resolver.rb +1 -6
  47. data/lib/lutaml/model/utils.rb +19 -6
  48. data/lib/lutaml/model/validation_framework.rb +1 -1
  49. data/lib/lutaml/model/value_transformer.rb +2 -2
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/rdf/mapping.rb +19 -13
  52. data/lib/lutaml/rdf/mapping_rule.rb +19 -2
  53. data/lib/lutaml/rdf/member_rule.rb +19 -2
  54. data/lib/lutaml/rdf/transform.rb +20 -11
  55. data/lib/lutaml/turtle/transform.rb +125 -53
  56. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -1
  57. data/lib/lutaml/xml/adapter/base_adapter.rb +10 -14
  58. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +3 -3
  59. data/lib/lutaml/xml/adapter/plan_based_builder.rb +14 -14
  60. data/lib/lutaml/xml/adapter/xml_serializer.rb +3 -3
  61. data/lib/lutaml/xml/configurable.rb +2 -1
  62. data/lib/lutaml/xml/data_model.rb +2 -2
  63. data/lib/lutaml/xml/decisions/decision_context.rb +3 -3
  64. data/lib/lutaml/xml/decisions/rules/element_form_default_unqualified_rule.rb +1 -1
  65. data/lib/lutaml/xml/decisions/rules/element_form_option_rule.rb +1 -1
  66. data/lib/lutaml/xml/decisions/rules/used_prefix_rule.rb +1 -1
  67. data/lib/lutaml/xml/declaration_plan.rb +2 -2
  68. data/lib/lutaml/xml/declaration_planner.rb +12 -13
  69. data/lib/lutaml/xml/document.rb +13 -13
  70. data/lib/lutaml/xml/format_chooser.rb +3 -3
  71. data/lib/lutaml/xml/hoisting_algorithm.rb +1 -1
  72. data/lib/lutaml/xml/mapping.rb +2 -2
  73. data/lib/lutaml/xml/mapping_rule.rb +16 -3
  74. data/lib/lutaml/xml/model_transform.rb +17 -19
  75. data/lib/lutaml/xml/namespace_collector.rb +10 -10
  76. data/lib/lutaml/xml/namespace_declaration.rb +2 -2
  77. data/lib/lutaml/xml/namespace_declaration_data.rb +5 -8
  78. data/lib/lutaml/xml/namespace_scope_config.rb +3 -2
  79. data/lib/lutaml/xml/namespace_type_resolver.rb +4 -4
  80. data/lib/lutaml/xml/nokogiri/element.rb +2 -2
  81. data/lib/lutaml/xml/polymorphic_value_handler.rb +1 -1
  82. data/lib/lutaml/xml/schema/xsd/base.rb +7 -7
  83. data/lib/lutaml/xml/schema/xsd/choice.rb +2 -2
  84. data/lib/lutaml/xml/schema/xsd/complex_type.rb +5 -5
  85. data/lib/lutaml/xml/schema/xsd/errors/message_builder.rb +3 -3
  86. data/lib/lutaml/xml/schema/xsd/group.rb +2 -2
  87. data/lib/lutaml/xml/schema/xsd/sequence.rb +2 -2
  88. data/lib/lutaml/xml/schema/xsd_schema.rb +5 -5
  89. data/lib/lutaml/xml/serialization/format_conversion.rb +4 -3
  90. data/lib/lutaml/xml/transformation/element_builder.rb +4 -2
  91. data/lib/lutaml/xml/transformation/rule_applier.rb +2 -2
  92. data/lib/lutaml/xml/transformation/value_serializer.rb +4 -6
  93. data/lib/lutaml/xml/transformation.rb +4 -4
  94. data/lib/lutaml/xml/type/configurable.rb +0 -4
  95. data/lib/lutaml/xml/xml_element.rb +21 -13
  96. data/lutaml-model.gemspec +1 -1
  97. data/spec/lutaml/jsonld/transform_spec.rb +239 -0
  98. data/spec/lutaml/model/cached_type_resolver_spec.rb +3 -3
  99. data/spec/lutaml/model/optimization_spec.rb +228 -0
  100. data/spec/lutaml/model/store_spec.rb +195 -0
  101. data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
  102. data/spec/lutaml/rdf/mapping_spec.rb +74 -4
  103. data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
  104. data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
  105. data/spec/lutaml/turtle/mapping_spec.rb +2 -2
  106. data/spec/lutaml/turtle/transform_spec.rb +315 -0
  107. data/spec/lutaml/xml/data_model_spec.rb +10 -28
  108. metadata +7 -4
@@ -41,7 +41,7 @@ module Lutaml
41
41
  resolved_element_order&.each do |child|
42
42
  if child.is_a?(Element)
43
43
  array << child
44
- elsif child.respond_to?(:child_elements)
44
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
45
45
  child.child_elements(array)
46
46
  end
47
47
  end
@@ -54,7 +54,7 @@ module Lutaml
54
54
  resolved_element_order&.any? do |child|
55
55
  if child.is_a?(Element)
56
56
  reference_matches?(element_name, child.ref || child.name)
57
- elsif child.respond_to?(:find_elements_used)
57
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
58
58
  child.find_elements_used(element_name)
59
59
  end
60
60
  end || false
@@ -105,7 +105,7 @@ except: DIRECT_CHILD_ELEMENTS_EXCEPTION)
105
105
  resolved_element_order&.each do |child|
106
106
  if child.is_a?(Element)
107
107
  array << child
108
- elsif child.respond_to?(:child_elements)
108
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
109
109
  child.child_elements(array)
110
110
  end
111
111
  end
@@ -118,7 +118,7 @@ except: DIRECT_CHILD_ELEMENTS_EXCEPTION)
118
118
  resolved_element_order&.any? do |child|
119
119
  if child.is_a?(Element)
120
120
  reference_matches?(element_name, child.ref || child.name)
121
- elsif child.respond_to?(:find_elements_used)
121
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
122
122
  child.find_elements_used(element_name)
123
123
  end
124
124
  end || false
@@ -139,9 +139,9 @@ except: DIRECT_CHILD_ELEMENTS_EXCEPTION)
139
139
  # Get elements from the primary content model (sequence, choice, or all).
140
140
  # @return [Array<Element>] Elements exposed by the active content model
141
141
  def elements
142
- return sequence.element if sequence.respond_to?(:element)
143
- return choice.element if choice.respond_to?(:element)
144
- return all.element if all.respond_to?(:element)
142
+ return sequence.element if sequence
143
+ return choice.element if choice
144
+ return all.element if all
145
145
 
146
146
  []
147
147
  end
@@ -28,8 +28,8 @@ module Lutaml
28
28
  parts = []
29
29
  parts << header
30
30
  parts << context_details if @error.context && !@error.context.to_h.empty?
31
- parts << suggestions_section if @error.respond_to?(:suggestions) && @error.suggestions.any?
32
- parts << troubleshooting_section if @error.respond_to?(:troubleshooting_tips) && @error.troubleshooting_tips.any?
31
+ parts << suggestions_section if @error.is_a?(EnhancedError) && @error.suggestions.any?
32
+ parts << troubleshooting_section if @error.is_a?(EnhancedError) && @error.troubleshooting_tips.any?
33
33
  parts.compact.join("\n\n")
34
34
  end
35
35
 
@@ -39,7 +39,7 @@ module Lutaml
39
39
  #
40
40
  # @return [String] Error header
41
41
  def header
42
- severity = @error.respond_to?(:severity) ? @error.severity.to_s.upcase : "ERROR"
42
+ severity = @error.is_a?(EnhancedError) ? @error.severity.to_s.upcase : "ERROR"
43
43
  "#{severity}: #{@error.message}"
44
44
  end
45
45
 
@@ -44,7 +44,7 @@ module Lutaml
44
44
  group.resolved_element_order&.each do |child|
45
45
  if child.is_a?(Element)
46
46
  array << child
47
- elsif child.respond_to?(:child_elements)
47
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
48
48
  child.child_elements(array)
49
49
  end
50
50
  end
@@ -57,7 +57,7 @@ module Lutaml
57
57
  resolved_element_order&.any? do |child|
58
58
  if child.is_a?(Element)
59
59
  reference_matches?(element_name, child.ref || child.name)
60
- elsif child.respond_to?(:find_elements_used)
60
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
61
61
  child.find_elements_used(element_name)
62
62
  end
63
63
  end || false
@@ -41,7 +41,7 @@ module Lutaml
41
41
  resolved_element_order&.each do |child|
42
42
  if child.is_a?(Element)
43
43
  array << child
44
- elsif child.respond_to?(:child_elements)
44
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
45
45
  child.child_elements(array)
46
46
  end
47
47
  end
@@ -54,7 +54,7 @@ module Lutaml
54
54
  resolved_element_order&.any? do |child|
55
55
  if child.is_a?(Element)
56
56
  reference_matches?(element_name, child.ref || child.name)
57
- elsif child.respond_to?(:find_elements_used)
57
+ elsif child.is_a?(Group) || child.is_a?(Sequence) || child.is_a?(Choice) || child.is_a?(ComplexType)
58
58
  child.find_elements_used(element_name)
59
59
  end
60
60
  end || false
@@ -82,7 +82,7 @@ module Lutaml
82
82
  attr_type = attr.type(register)
83
83
 
84
84
  # Validate Type::Value xsd_type
85
- if attr_type.respond_to?(:xsd_type)
85
+ if attr_type.is_a?(Class) && attr_type < Lutaml::Model::Type::Value
86
86
  type_name = attr_type.xsd_type
87
87
  classification = classify_xsd_type(type_name, klass, register)
88
88
 
@@ -413,7 +413,7 @@ _mapping_rule = nil)
413
413
  return attr.options[:xsd_type] if attr.options[:xsd_type]
414
414
 
415
415
  # 2. Check if type has xsd_type method (Type-level)
416
- if attr_type.respond_to?(:xsd_type)
416
+ if attr_type.is_a?(Class) && attr_type < Lutaml::Model::Type::Value
417
417
  # Special handling for Reference type
418
418
  if attr_type == Lutaml::Model::Type::Reference
419
419
  target_xsd_type = get_target_xsd_type(attr, register)
@@ -466,7 +466,7 @@ _mapping_rule = nil)
466
466
 
467
467
  # Get namespace info from Model class (Serializable)
468
468
  def get_model_namespace_info(klass)
469
- mapping = klass.respond_to?(:mappings_for) ? klass.mappings_for(:xml) : nil
469
+ mapping = klass.is_a?(Class) && klass.include?(Lutaml::Model::Serialize) ? klass.mappings_for(:xml) : nil
470
470
  return {} unless mapping
471
471
 
472
472
  {
@@ -487,8 +487,8 @@ _mapping_rule = nil)
487
487
 
488
488
  # XmlNamespace class
489
489
  {
490
- uri: ns.respond_to?(:uri) ? ns.uri : nil,
491
- prefix: ns.respond_to?(:prefix_default) ? ns.prefix_default : nil,
490
+ uri: ns.is_a?(Class) && ns < Lutaml::Xml::Namespace ? ns.uri : nil,
491
+ prefix: ns.is_a?(Class) && ns < Lutaml::Xml::Namespace ? ns.prefix_default : nil,
492
492
  class: ns,
493
493
  }
494
494
  end
@@ -97,7 +97,7 @@ module Lutaml
97
97
  raise Lutaml::Model::NoRootMappingError.new(self) unless valid
98
98
 
99
99
  options[:encoding] = doc.encoding
100
- if doc.respond_to?(:doctype) && doc.doctype
100
+ if doc.is_a?(Lutaml::Xml::Document) && doc.doctype
101
101
  options[:doctype] = doc.doctype
102
102
  end
103
103
  end
@@ -214,7 +214,7 @@ module Lutaml
214
214
  def process_xml_mapping_class_inheritance(mapping_class, &block)
215
215
  # Start with a copy of the parent class's XML mapping (if any).
216
216
  parent_class = superclass_with_xml_mapping(self)
217
- parent_xml_mapping = if parent_class.respond_to?(:mappings)
217
+ parent_xml_mapping = if parent_class.is_a?(Class) && parent_class.include?(Lutaml::Model::Serialize)
218
218
  parent_class.mappings[:xml]
219
219
  end
220
220
  @xml_mapping = if parent_xml_mapping
@@ -224,7 +224,8 @@ module Lutaml
224
224
  end
225
225
 
226
226
  # Get the parent mapping instance (DSL already evaluated via xml_mapping_instance)
227
- parent_mapping = if mapping_class.respond_to?(:xml_mapping_instance) &&
227
+ parent_mapping = if mapping_class.is_a?(Class) &&
228
+ (mapping_class < Lutaml::Xml::Mapping || mapping_class.include?(Lutaml::Model::Serialize)) &&
228
229
  mapping_class.xml_mapping_instance
229
230
  mapping_class.xml_mapping_instance
230
231
  else
@@ -91,7 +91,7 @@ parent_element_form_default)
91
91
  if attr_type.is_a?(Class) && attr_type.include?(Lutaml::Model::Serialize)
92
92
  attr_mapping = attr_type.mappings_for(:xml)
93
93
  attr_ns = attr_mapping&.namespace_class
94
- attr_ns_param = attr_mapping&.send(:namespace_param)
94
+ attr_ns_param = attr_mapping&.namespace_param
95
95
  # Use child's explicit namespace if it differs from parent's
96
96
  # If child has no namespace declaration (nil) or explicit blank (:blank),
97
97
  # do NOT inherit parent's namespace - return nil
@@ -165,7 +165,9 @@ is_polymorphic_subtype)
165
165
  if polymorphic_config.is_a?(Hash)
166
166
  poly_attr = polymorphic_config[:attribute]
167
167
  poly_class_map = polymorphic_config[:class_map]
168
- poly_value = value.send(poly_attr) if poly_attr && value.respond_to?(poly_attr)
168
+ poly_value = if poly_attr && value.is_a?(Lutaml::Model::Serialize) && value.class.attributes.key?(poly_attr)
169
+ value.public_send(poly_attr)
170
+ end
169
171
  if poly_value && poly_class_map && (klass_name = poly_class_map[poly_value.to_s])
170
172
  Object.const_get(klass_name)
171
173
  else
@@ -241,8 +241,8 @@ register_id)
241
241
  def apply_custom_method(parent, rule, model_class, model_instance)
242
242
  wrapper = ::Lutaml::Xml::CustomMethodWrapper.new(parent)
243
243
  mapper_instance = model_class.new
244
- mapper_instance.send(rule.custom_methods[:to], model_instance,
245
- parent, wrapper)
244
+ mapper_instance.public_send(rule.custom_methods[:to], model_instance,
245
+ parent, wrapper)
246
246
  end
247
247
 
248
248
  # Apply element value handling collections
@@ -54,7 +54,7 @@ module Lutaml
54
54
  end
55
55
 
56
56
  # Use type's serialization if available
57
- if rule.attribute_type.respond_to?(:serialize)
57
+ if rule.attribute_type.is_a?(Class) && rule.attribute_type < Lutaml::Model::Type::Value
58
58
  rule.attribute_type.serialize(value)
59
59
  else
60
60
  value.to_s
@@ -101,7 +101,7 @@ module Lutaml
101
101
  # @param attribute_type [Class, nil] The attribute type
102
102
  # @return [Boolean] true if custom Value type
103
103
  def custom_value_type?(attribute_type)
104
- attribute_type.respond_to?(:new) &&
104
+ attribute_type.is_a?(Class) &&
105
105
  attribute_type < Lutaml::Model::Type::Value
106
106
  end
107
107
 
@@ -112,14 +112,12 @@ module Lutaml
112
112
  # @return [String, nil] Serialized value or nil
113
113
  def serialize_custom_value(value, attribute_type)
114
114
  # Skip wrapping if value is already the correct type
115
- if value.is_a?(attribute_type) && value.respond_to?(:to_xml)
115
+ if value.is_a?(attribute_type)
116
116
  return value.to_xml
117
117
  end
118
118
 
119
119
  wrapped_value = attribute_type.new(value)
120
- if wrapped_value.respond_to?(:to_xml)
121
- wrapped_value.to_xml
122
- end
120
+ wrapped_value.to_xml
123
121
  end
124
122
 
125
123
  # Find attribute definition from model class
@@ -139,9 +139,9 @@ module Lutaml
139
139
  # @param model_instance [Object] The model instance
140
140
  def handle_schema_location(root, model_instance)
141
141
  # Case 1: SchemaLocation object (programmatic creation)
142
- if model_instance.respond_to?(:schema_location) && model_instance.schema_location
142
+ if model_instance.is_a?(Lutaml::Model::Serialize) && model_instance.schema_location
143
143
  schema_loc = model_instance.schema_location
144
- if schema_loc.respond_to?(:to_xml_attributes)
144
+ if schema_loc.is_a?(Lutaml::Xml::SchemaLocation)
145
145
  schema_attrs = schema_loc.to_xml_attributes
146
146
  schema_attrs.each do |attr_name, attr_value|
147
147
  attr = ::Lutaml::Xml::DataModel::XmlAttribute.new(attr_name,
@@ -150,7 +150,7 @@ module Lutaml
150
150
  end
151
151
  end
152
152
  # Case 2: @raw_schema_location string (from parsing/round-trip)
153
- elsif model_instance.respond_to?(:raw_schema_location) && model_instance.raw_schema_location
153
+ elsif model_instance.is_a?(Lutaml::Model::Serialize) && model_instance.raw_schema_location
154
154
  raw_schema_loc = model_instance.raw_schema_location
155
155
  if raw_schema_loc && !raw_schema_loc.empty?
156
156
  add_raw_schema_location(root, raw_schema_loc)
@@ -292,7 +292,7 @@ module Lutaml
292
292
  # @param model_instance [Object] The model instance
293
293
  # @param options [Hash] Options
294
294
  def apply_standard_rules(root, model_instance, options)
295
- attr_order = model_instance.respond_to?(:attribute_order) &&
295
+ attr_order = model_instance.is_a?(Lutaml::Model::Serialize) &&
296
296
  model_instance.attribute_order
297
297
 
298
298
  rules = if attr_order
@@ -104,9 +104,7 @@ module Lutaml
104
104
  # @return [String, nil] parent's xsd_type if set
105
105
  def inherited_xsd_type
106
106
  return nil if superclass == Lutaml::Model::Type::Value
107
- return nil unless superclass.respond_to?(:xsd_type)
108
107
 
109
- # Get parent's @xsd_type directly (not default_xsd_type)
110
108
  parent_xsd = superclass.instance_variable_get(:@xsd_type)
111
109
  parent_xsd || superclass.inherited_xsd_type
112
110
  end
@@ -116,7 +114,6 @@ module Lutaml
116
114
  # @return [Class, Symbol, nil] parent's namespace class if set
117
115
  def inherited_namespace
118
116
  return nil if superclass == Lutaml::Model::Type::Value
119
- return nil unless superclass.respond_to?(:xml_mapping)
120
117
 
121
118
  parent_mapping = superclass.xml_mapping
122
119
  parent_mapping&.namespace_class || superclass.inherited_namespace
@@ -136,7 +133,6 @@ module Lutaml
136
133
  # @return [Lutaml::Xml::Type::ValueXmlMapping] the new XML mapping
137
134
  def inherit_xml_mapping_from_parent
138
135
  return create_xml_mapping if superclass == Lutaml::Model::Type::Value
139
- return create_xml_mapping unless superclass.respond_to?(:xml_mapping)
140
136
 
141
137
  parent_mapping = superclass.xml_mapping
142
138
  return create_xml_mapping unless parent_mapping
@@ -157,7 +157,7 @@ module Lutaml
157
157
  def name
158
158
  return @name unless namespace_prefix
159
159
 
160
- @prefixed_name ||= "#{namespace_prefix}:#{@name}" # rubocop:disable Naming/MemoizedInstanceVariableName
160
+ @prefixed_name ||= -"#{namespace_prefix}:#{@name}" # rubocop:disable Naming/MemoizedInstanceVariableName
161
161
  end
162
162
 
163
163
  def namespaced_name
@@ -174,15 +174,18 @@ module Lutaml
174
174
  # 3. Fall back to namespaces[nil] if exists
175
175
  # 4. Return unprefixed name
176
176
  ns = namespaces
177
- if namespace_prefix && ns[namespace_prefix]
178
- "#{ns[namespace_prefix].uri}:#{@name}"
179
- elsif @default_namespace
180
- "#{@default_namespace}:#{@name}"
181
- elsif ns[nil]
182
- "#{ns[nil].uri}:#{@name}"
183
- else
184
- @name
185
- end
177
+ raw = if namespace_prefix && ns[namespace_prefix]
178
+ "#{ns[namespace_prefix].uri}:#{@name}"
179
+ elsif @default_namespace
180
+ "#{@default_namespace}:#{@name}"
181
+ elsif ns[nil]
182
+ "#{ns[nil].uri}:#{@name}"
183
+ else
184
+ @name
185
+ end
186
+ # Deduplicate the string to reduce GC pressure from repeated
187
+ # namespace URIs across thousands of elements.
188
+ -raw
186
189
  end
187
190
  end
188
191
 
@@ -236,7 +239,8 @@ module Lutaml
236
239
 
237
240
  @namespace_uri = begin
238
241
  ns = namespace
239
- ns&.uri
242
+ uri = ns&.uri
243
+ uri ? -uri : nil
240
244
  end
241
245
  end
242
246
 
@@ -296,9 +300,13 @@ module Lutaml
296
300
 
297
301
  def text
298
302
  return @text if children.empty?
299
- return text_children.map(&:text) if content_bearing_children_count > 1
303
+ return @computed_text if defined?(@computed_text)
300
304
 
301
- text_children.map(&:text).join
305
+ @computed_text = if content_bearing_children_count > 1
306
+ text_children.map(&:text)
307
+ else
308
+ text_children.map(&:text).join
309
+ end
302
310
  end
303
311
 
304
312
  def cdata
data/lutaml-model.gemspec CHANGED
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "canon"
37
37
  spec.add_dependency "concurrent-ruby"
38
38
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
39
- spec.add_dependency "moxml", ">= 0.1.16"
39
+ spec.add_dependency "moxml", ">= 0.1.20"
40
40
  spec.add_dependency "ostruct"
41
41
  spec.add_dependency "rubyzip", "~> 2.3"
42
42
  spec.add_dependency "thor"
@@ -208,4 +208,243 @@ RSpec.describe Lutaml::JsonLd::Transform do
208
208
  expect(restored.tags).to eq(["en", "fr"])
209
209
  end
210
210
  end
211
+
212
+ describe "multiple types" do
213
+ before do
214
+ stub_const("DctermsTestNs", Class.new(Lutaml::Rdf::Namespace) do
215
+ uri "http://purl.org/dc/terms/"
216
+ prefix "dcterms"
217
+ end)
218
+
219
+ stub_const("MultiTypeJsonLdModel", Class.new(Lutaml::Model::Serializable) do
220
+ attribute :name, :string
221
+
222
+ rdf do
223
+ namespace TestSkosNs, DctermsTestNs
224
+
225
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
226
+
227
+ type ["skos:Concept", "dcterms:Agent"]
228
+
229
+ predicate :name, namespace: TestExNs, to: :name
230
+ end
231
+ end)
232
+ end
233
+
234
+ it "generates @type as array for multiple types" do
235
+ instance = MultiTypeJsonLdModel.new(name: "multi")
236
+ parsed = JSON.parse(instance.to_jsonld)
237
+ expect(parsed["@type"]).to eq(["skos:Concept", "dcterms:Agent"])
238
+ end
239
+
240
+ it "generates @type as string for single type" do
241
+ instance = JsonLdTestModel.new(name: "single")
242
+ parsed = JSON.parse(instance.to_jsonld)
243
+ expect(parsed["@type"]).to eq("skos:Concept")
244
+ end
245
+ end
246
+
247
+ describe "URI reference predicates" do
248
+ before do
249
+ stub_const("UriRefJsonLdModel", Class.new(Lutaml::Model::Serializable) do
250
+ attribute :name, :string
251
+ attribute :related, :string, collection: true
252
+
253
+ rdf do
254
+ namespace TestSkosNs, TestExNs
255
+
256
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
257
+
258
+ type "skos:Concept"
259
+
260
+ predicate :name, namespace: TestExNs, to: :name
261
+ predicate :related, namespace: TestSkosNs, to: :related,
262
+ uri_reference: true
263
+ end
264
+ end)
265
+ end
266
+
267
+ it "generates @type @id in context for uri_reference predicates" do
268
+ instance = UriRefJsonLdModel.new(name: "test", related: ["skos:other"])
269
+ parsed = JSON.parse(instance.to_jsonld)
270
+ expect(parsed["@context"]["related"]).to eq({
271
+ "@id" => "http://www.w3.org/2004/02/skos/core#related",
272
+ "@type" => "@id",
273
+ })
274
+ end
275
+
276
+ it "serializes URI reference as @id object" do
277
+ instance = UriRefJsonLdModel.new(name: "test", related: ["skos:other"])
278
+ parsed = JSON.parse(instance.to_jsonld)
279
+ expect(parsed["related"]).to eq([{ "@id" => "skos:other" }])
280
+ end
281
+
282
+ it "serializes single URI reference value as @id object" do
283
+ stub_const("SingleUriRefModel", Class.new(Lutaml::Model::Serializable) do
284
+ attribute :name, :string
285
+ attribute :link, :string
286
+
287
+ rdf do
288
+ namespace TestSkosNs, TestExNs
289
+
290
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
291
+
292
+ type "skos:Concept"
293
+
294
+ predicate :name, namespace: TestExNs, to: :name
295
+ predicate :related, namespace: TestSkosNs, to: :link,
296
+ uri_reference: true
297
+ end
298
+ end)
299
+
300
+ instance = SingleUriRefModel.new(name: "test", link: "skos:something")
301
+ parsed = JSON.parse(instance.to_jsonld)
302
+ expect(parsed["related"]).to eq({ "@id" => "skos:something" })
303
+ end
304
+ end
305
+
306
+ describe "member linking predicates" do
307
+ before do
308
+ stub_const("JsonLdChildModel", Class.new(Lutaml::Model::Serializable) do
309
+ attribute :cid, :string
310
+ attribute :label, :string
311
+
312
+ rdf do
313
+ namespace TestSkosNs, TestExNs
314
+
315
+ subject { |m| "http://example.org/item/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
316
+
317
+ type "skos:Concept"
318
+
319
+ predicate :prefLabel, namespace: TestSkosNs, to: :label
320
+ end
321
+ end)
322
+
323
+ stub_const("JsonLdParentModel", Class.new(Lutaml::Model::Serializable) do
324
+ attribute :name, :string
325
+ attribute :children, JsonLdChildModel, collection: true
326
+
327
+ rdf do
328
+ namespace TestSkosNs, TestExNs
329
+
330
+ subject { |m| "http://example.org/group/#{m.name}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
331
+
332
+ type "skos:Collection"
333
+
334
+ predicate :prefLabel, namespace: TestSkosNs, to: :name
335
+
336
+ members :children,
337
+ predicate_name: :member,
338
+ namespace: TestSkosNs
339
+ end
340
+ end)
341
+ end
342
+
343
+ it "includes linking term in @context with @type @id" do
344
+ parent = JsonLdParentModel.new(
345
+ name: "grp1",
346
+ children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
347
+ )
348
+ parsed = JSON.parse(parent.to_jsonld)
349
+ expect(parsed["@context"]["member"]).to eq({
350
+ "@id" => "http://www.w3.org/2004/02/skos/core#member",
351
+ "@type" => "@id",
352
+ })
353
+ end
354
+
355
+ it "generates @id references for linked members in @graph" do
356
+ parent = JsonLdParentModel.new(
357
+ name: "grp1",
358
+ children: [
359
+ JsonLdChildModel.new(cid: "a", label: "Alpha"),
360
+ JsonLdChildModel.new(cid: "b", label: "Beta"),
361
+ ],
362
+ )
363
+ parsed = JSON.parse(parent.to_jsonld)
364
+ parent_resource = parsed["@graph"].find { |r| r["@type"] }
365
+ expect(parent_resource["member"]).to eq([
366
+ { "@id" => "http://example.org/item/a" },
367
+ { "@id" => "http://example.org/item/b" },
368
+ ])
369
+ end
370
+
371
+ it "includes member resources in @graph" do
372
+ parent = JsonLdParentModel.new(
373
+ name: "grp1",
374
+ children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
375
+ )
376
+ parsed = JSON.parse(parent.to_jsonld)
377
+ member = parsed["@graph"].find { |r| r["prefLabel"] == "Alpha" }
378
+ expect(member).not_to be_nil
379
+ expect(member["@id"]).to eq("http://example.org/item/a")
380
+ end
381
+
382
+ it "merges child namespaces into @context" do
383
+ parent = JsonLdParentModel.new(
384
+ name: "grp1",
385
+ children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
386
+ )
387
+ parsed = JSON.parse(parent.to_jsonld)
388
+ expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
389
+ expect(parsed["@context"]["ex"]).to eq("http://example.org/")
390
+ end
391
+
392
+ it "omits linking key when members have no linking predicate" do
393
+ stub_const("UnlinkedChild", Class.new(Lutaml::Model::Serializable) do
394
+ attribute :label, :string
395
+
396
+ rdf do
397
+ namespace TestSkosNs
398
+
399
+ subject { |m| "http://example.org/#{m.label}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
400
+
401
+ predicate :prefLabel, namespace: TestSkosNs, to: :label
402
+ end
403
+ end)
404
+
405
+ stub_const("UnlinkedParent", Class.new(Lutaml::Model::Serializable) do
406
+ attribute :name, :string
407
+ attribute :items, UnlinkedChild, collection: true
408
+
409
+ rdf do
410
+ namespace TestSkosNs
411
+
412
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
413
+
414
+ predicate :prefLabel, namespace: TestSkosNs, to: :name
415
+
416
+ members :items
417
+ end
418
+ end)
419
+
420
+ parent = UnlinkedParent.new(
421
+ name: "grp",
422
+ items: [UnlinkedChild.new(label: "a")],
423
+ )
424
+ parsed = JSON.parse(parent.to_jsonld)
425
+ expect(parsed["@context"]).not_to have_key("member")
426
+ end
427
+ end
428
+
429
+ describe "empty type array" do
430
+ before do
431
+ stub_const("NoTypeJsonLdModel", Class.new(Lutaml::Model::Serializable) do
432
+ attribute :name, :string
433
+
434
+ rdf do
435
+ namespace TestExNs
436
+
437
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
438
+
439
+ predicate :name, namespace: TestExNs, to: :name
440
+ end
441
+ end)
442
+ end
443
+
444
+ it "omits @type when no types declared" do
445
+ instance = NoTypeJsonLdModel.new(name: "test")
446
+ parsed = JSON.parse(instance.to_jsonld)
447
+ expect(parsed).not_to have_key("@type")
448
+ end
449
+ end
211
450
  end
@@ -129,14 +129,14 @@ RSpec.describe Lutaml::Model::CachedTypeResolver do
129
129
  expect(result2).to eq(result1)
130
130
  end
131
131
 
132
- it "passes through Class objects without caching" do
132
+ it "caches Class objects with substitution applied" do
133
133
  klass = Class.new
134
134
  result = resolver.resolve(klass, context)
135
135
  expect(result).to eq(klass)
136
136
 
137
- # Should not be cached
137
+ # Should be cached
138
138
  stats = resolver.cache_stats
139
- expect(stats[:size]).to eq(0)
139
+ expect(stats[:size]).to eq(1)
140
140
  end
141
141
 
142
142
  it "raises UnknownTypeError for unknown types" do