lutaml-model 0.8.16 → 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +0 -3
  3. data/.rubocop_todo.yml +70 -14
  4. data/docs/_guides/xml/namespace-semantics.adoc +2 -0
  5. data/docs/_guides/xml-namespace-qualification.adoc +142 -0
  6. data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
  7. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
  8. data/docs/namespace-management.adoc +2 -0
  9. data/docs/xml-schema-qualification.md +6 -0
  10. data/lib/lutaml/jsonld.rb +1 -4
  11. data/lib/lutaml/model/choice.rb +34 -0
  12. data/lib/lutaml/model/compiled_rule.rb +7 -0
  13. data/lib/lutaml/model/version.rb +1 -1
  14. data/lib/lutaml/model.rb +1 -0
  15. data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
  16. data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
  17. data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
  18. data/lib/lutaml/rdf.rb +3 -0
  19. data/lib/lutaml/xml/adapter_element.rb +2 -2
  20. data/lib/lutaml/xml/transformation/element_builder.rb +38 -23
  21. data/lib/lutaml/yamlld/adapter.rb +25 -0
  22. data/lib/lutaml/yamlld.rb +25 -0
  23. data/spec/lutaml/integration/multi_format_spec.rb +23 -0
  24. data/spec/lutaml/model/attribute_spec.rb +8 -1
  25. data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
  26. data/spec/lutaml/model/custom_model_spec.rb +4 -4
  27. data/spec/lutaml/model/mixed_content_spec.rb +2 -2
  28. data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
  29. data/spec/lutaml/model/ordered_content_spec.rb +3 -3
  30. data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
  31. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
  32. data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
  33. data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
  34. data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +15 -1
  35. data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
  36. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  37. data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
  38. data/spec/lutaml/xml/mapping_spec.rb +4 -4
  39. data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
  40. data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
  41. data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
  42. data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
  43. data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
  44. data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
  45. data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
  46. data/spec/lutaml/yamlld/registration_spec.rb +33 -0
  47. metadata +13 -8
@@ -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&.empty?
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&.empty?
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
@@ -251,18 +251,6 @@ child_transformation)
251
251
  value.xml_namespace_prefix = ns_prefix
252
252
  end
253
253
 
254
- # Also set @__xml_namespace_prefix on the XmlElement for doubly-defined case.
255
- # This is read by NamespaceCollector to set NamespaceUsage.used_prefix.
256
- # For doubly-defined: parent has no ns, child's ns_prefix is from @__xml_ns_prefixes
257
- # (not from parent's @__xml_namespace_prefix).
258
- # For mixed content: parent's XmlElement already has @__xml_namespace_prefix set.
259
- if parent_ns_class.nil? && ns_prefix && !ns_prefix.empty? && !child_ns_class
260
- # Doubly-defined case: parent has no ns, child has ns class.
261
- # Set on XmlElement so NamespaceCollector reads it.
262
- # The value will be set on the model instance above (via @__xml_namespace_prefix)
263
- # and we'll set it on XmlElement too so collection phase picks it up.
264
- end
265
-
266
254
  child_element = child_transformation.transform(value, child_options)
267
255
 
268
256
  # Dual-namespace support: when the child model was deserialized from an element
@@ -304,15 +292,10 @@ child_transformation)
304
292
  child_element.name = rule.serialized_name
305
293
  end
306
294
 
307
- # W3C elementFormDefault: unqualified override
308
- # When parent's namespace has element_form_default :unqualified and the child's
309
- # namespace is the same as the parent's, override to blank namespace.
310
- # This ensures local elements are not namespace-qualified per W3C spec.
311
- if parent_element_form_default == :unqualified &&
312
- parent_ns_class &&
313
- child_element.namespace_class == parent_ns_class
314
- child_element.namespace_class = nil
315
- end
295
+ propagate_form(child_element, rule)
296
+ apply_unqualified_form_default(
297
+ child_element, rule, parent_ns_class, parent_element_form_default
298
+ )
316
299
 
317
300
  child_element
318
301
  end
@@ -356,7 +339,7 @@ child_transformation)
356
339
  element.xml_namespace_prefix = model_ns_prefix
357
340
  end
358
341
 
359
- element.form = rule.form if rule.form
342
+ propagate_form(element, rule)
360
343
  text = serialize_value(value, rule, rule.attribute_type, nil)
361
344
  element.text_content = text if text
362
345
  element
@@ -490,7 +473,7 @@ register_id)
490
473
  element.xml_namespace_prefix = ns_prefix
491
474
  end
492
475
 
493
- element.form = rule.form if rule.form
476
+ propagate_form(element, rule)
494
477
 
495
478
  # Handle Hash type - expand hash into child elements
496
479
  if value.is_a?(::Hash) && rule.attribute_type == Lutaml::Model::Type::Hash
@@ -523,6 +506,38 @@ register_id)
523
506
  end
524
507
  end
525
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
+
526
541
  # Apply nil marker to element if needed
527
542
  #
528
543
  # @param element [XmlElement] The element
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Lutaml
6
+ module YamlLd
7
+ class Adapter < Lutaml::KeyValue::Document
8
+ PERMITTED_CLASSES = Lutaml::Yaml::Adapter::StandardAdapter::PERMITTED_CLASSES
9
+
10
+ def self.parse(yaml_string, _options = {})
11
+ YAML.safe_load(yaml_string, permitted_classes: PERMITTED_CLASSES)
12
+ end
13
+
14
+ def to_yamlld(options = {})
15
+ attributes_to_serialize =
16
+ if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
17
+ @attributes.to_hash["__root__"]
18
+ else
19
+ @attributes
20
+ end
21
+ YAML.dump(attributes_to_serialize, options)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model"
4
+ require_relative "rdf"
5
+ require_relative "yaml"
6
+
7
+ module Lutaml
8
+ module YamlLd
9
+ autoload :Adapter, "#{__dir__}/yamlld/adapter"
10
+ end
11
+ end
12
+
13
+ # Known limitations (v1):
14
+ # - Single YAML document only; multi-document streams not interpreted as @graph.
15
+ # - YAML anchors/aliases not used to deduplicate @id references.
16
+ # - External @context loading not performed (matches :jsonld behavior).
17
+ Lutaml::Model::FormatRegistry.register(
18
+ :yamlld,
19
+ mapping_class: Lutaml::Rdf::Mapping,
20
+ adapter_class: Lutaml::YamlLd::Adapter,
21
+ transformer: Lutaml::Rdf::LinkedDataTransform,
22
+ key_value: false,
23
+ rdf: true,
24
+ error_types: ["Psych::SyntaxError"],
25
+ )
@@ -3,6 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "lutaml/turtle"
5
5
  require "lutaml/jsonld"
6
+ require "lutaml/yamlld"
6
7
 
7
8
  RSpec.describe "Multi-format model" do
8
9
  before do
@@ -103,4 +104,26 @@ RSpec.describe "Multi-format model" do
103
104
  expect(turtle).to include("@prefix")
104
105
  end
105
106
  end
107
+
108
+ describe "YAML-LD format" do
109
+ it "serializes with @type and @id" do
110
+ yamlld = instance.to_yamlld
111
+ parsed = YAML.safe_load(yamlld)
112
+ expect(parsed["@type"]).to eq("skos:Concept")
113
+ expect(parsed["@id"]).to eq("http://example.org/concept/42")
114
+ expect(parsed["prefLabel"]).to eq("test")
115
+ end
116
+
117
+ it "round-trips" do
118
+ restored = MultiFormatModel.from_yamlld(instance.to_yamlld)
119
+ expect(restored.name).to eq("test")
120
+ expect(restored.code).to eq("42")
121
+ end
122
+
123
+ it "produces the same data model as JSON-LD" do
124
+ from_yamlld = YAML.safe_load(instance.to_yamlld)
125
+ from_jsonld = JSON.parse(instance.to_jsonld)
126
+ expect(from_yamlld).to eq(from_jsonld)
127
+ end
128
+ end
106
129
  end
@@ -191,7 +191,14 @@ RSpec.describe Lutaml::Model::Attribute do
191
191
 
192
192
  Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
193
193
  it "return true if option is `#{option}`" do
194
- expect(validate_options.call({ option => "value" })).to be(true)
194
+ if option == :xsd_type
195
+ expect do
196
+ result = validate_options.call({ option => "value" })
197
+ expect(result).to be(true)
198
+ end.to output.to_stderr
199
+ else
200
+ expect(validate_options.call({ option => "value" })).to be(true)
201
+ end
195
202
  end
196
203
  end
197
204
 
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Model::Choice, "#restrict and #remove_attribute" do
6
+ before do
7
+ stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
8
+ attribute :name, :string
9
+ attribute :age, :integer
10
+ attribute :email, :string
11
+ end)
12
+ end
13
+
14
+ describe "#restrict" do
15
+ it "restricts a predefined attribute inside a choice block" do
16
+ stub_const("ChoiceRestrictModel", Class.new(Lutaml::Model::Serializable) do
17
+ attribute :foo, :string, collection: 1..10
18
+
19
+ choice min: 1, max: 1 do
20
+ restrict :foo, collection: 2..5
21
+ end
22
+ end)
23
+
24
+ attr = ChoiceRestrictModel.attributes[:foo]
25
+ expect(attr.options[:collection]).to eq(2..5)
26
+ choice = ChoiceRestrictModel.choice_attributes.first
27
+ expect(choice.attributes).to include(attr)
28
+ end
29
+
30
+ it "raises UndefinedAttributeError for non-existent attribute" do
31
+ expect do
32
+ stub_const("ChoiceRestrictBadModel", Class.new(Lutaml::Model::Serializable) do
33
+ choice min: 1, max: 1 do
34
+ restrict :nonexistent, collection: 1..2
35
+ end
36
+ end)
37
+ end.to raise_error(Lutaml::Model::UndefinedAttributeError)
38
+ end
39
+
40
+ it "restricts an imported model attribute inside a choice block" do
41
+ stub_const("ChoiceImportModel", Class.new(Lutaml::Model::Serializable) do
42
+ attribute :name, :string
43
+ attribute :age, :integer
44
+
45
+ choice min: 1, max: 1 do
46
+ import_model_attributes ChoiceRestrictParent
47
+ restrict :age, values: [18, 21, 30]
48
+ end
49
+ end)
50
+
51
+ attr = ChoiceImportModel.attributes[:age]
52
+ expect(attr.options[:values]).to eq([18, 21, 30])
53
+ end
54
+
55
+ it "marks the attribute as belonging to the choice" do
56
+ stub_const("ChoiceRestrictOwnedModel", Class.new(Lutaml::Model::Serializable) do
57
+ attribute :tag, :string
58
+
59
+ choice min: 0, max: 1 do
60
+ restrict :tag, values: %w[a b c]
61
+ end
62
+ end)
63
+
64
+ attr = ChoiceRestrictOwnedModel.attributes[:tag]
65
+ expect(attr.options[:choice]).to be_a(described_class)
66
+ end
67
+
68
+ it "does not duplicate the attribute if restrict is called twice" do
69
+ stub_const("ChoiceRestrictDupModel", Class.new(Lutaml::Model::Serializable) do
70
+ attribute :val, :integer
71
+
72
+ choice min: 0, max: 1 do
73
+ restrict :val, values: [1, 2]
74
+ restrict :val, values: [1, 2, 3]
75
+ end
76
+ end)
77
+
78
+ choice = ChoiceRestrictDupModel.choice_attributes.first
79
+ count = choice.attributes.count do |a|
80
+ !a.is_a?(described_class) && a.name == :val
81
+ end
82
+ expect(count).to eq(1)
83
+ expect(ChoiceRestrictDupModel.attributes[:val].options[:values]).to eq([
84
+ 1, 2, 3
85
+ ])
86
+ end
87
+
88
+ it "restricts an inherited attribute in a child class choice" do
89
+ stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
90
+ attribute :tag, :string
91
+ attribute :label, :string
92
+ end)
93
+
94
+ child = Class.new(ChoiceRestrictParent) do
95
+ include Lutaml::Model::Serialize
96
+
97
+ choice min: 1, max: 1 do
98
+ restrict :tag, values: %w[a b c]
99
+ end
100
+ end
101
+
102
+ attr = child.attributes[:tag]
103
+ expect(attr.options[:values]).to eq(%w[a b c])
104
+ choice = child.choice_attributes.first
105
+ expect(choice.flat_attributes.any? { |a| a.name == :tag }).to be(true)
106
+ end
107
+ end
108
+
109
+ describe "#remove_attribute" do
110
+ it "removes an attribute from the choice block" do
111
+ stub_const("ChoiceRemoveModel", Class.new(Lutaml::Model::Serializable) do
112
+ attribute :keep, :string
113
+ attribute :drop, :string
114
+
115
+ choice min: 0, max: 1 do
116
+ attribute :keep, :string
117
+ attribute :drop, :string
118
+ remove_attribute :drop
119
+ end
120
+ end)
121
+
122
+ choice = ChoiceRemoveModel.choice_attributes.first
123
+ attr_names = choice.attributes.filter_map do |a|
124
+ a.is_a?(described_class) ? nil : a.name
125
+ end
126
+ expect(attr_names).to eq([:keep])
127
+ end
128
+
129
+ it "clears the choice option from the removed attribute" do
130
+ stub_const("ChoiceRemoveOptModel", Class.new(Lutaml::Model::Serializable) do
131
+ attribute :item, :string
132
+
133
+ choice min: 0, max: 1 do
134
+ attribute :item, :string
135
+ remove_attribute :item
136
+ end
137
+ end)
138
+
139
+ attr = ChoiceRemoveOptModel.attributes[:item]
140
+ # The top-level :item attribute should still exist without :choice
141
+ # since the one in the choice was a different instance
142
+ expect(attr.options[:choice]).to be_nil
143
+ end
144
+
145
+ it "returns nil if the attribute is not in the choice" do
146
+ result = nil
147
+ stub_const("ChoiceRemoveNotFoundModel", Class.new(Lutaml::Model::Serializable) do
148
+ attribute :x, :string
149
+
150
+ choice min: 0, max: 1 do
151
+ result = remove_attribute(:nonexistent)
152
+ end
153
+ end)
154
+
155
+ expect(result).to be_nil
156
+ end
157
+
158
+ it "invalidates the flat_attributes cache" do
159
+ stub_const("ChoiceRemoveCacheModel", Class.new(Lutaml::Model::Serializable) do
160
+ attribute :a, :string
161
+ attribute :b, :string
162
+
163
+ choice min: 0, max: 1 do
164
+ attribute :a, :string
165
+ attribute :b, :string
166
+ end
167
+ end)
168
+
169
+ choice = ChoiceRemoveCacheModel.choice_attributes.first
170
+ expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a, :b)
171
+ choice.remove_attribute(:b)
172
+ expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a)
173
+ end
174
+
175
+ it "removes an imported attribute from the choice block" do
176
+ source = Class.new(Lutaml::Model::Serializable) do
177
+ attribute :name, :string
178
+ attribute :email, :string
179
+ attribute :phone, :string
180
+ end
181
+ stub_const("ChoiceRemoveImportSource", source)
182
+
183
+ stub_const("ChoiceRemoveImportModel", Class.new(Lutaml::Model::Serializable) do
184
+ choice min: 1, max: 1 do
185
+ import_model_attributes ChoiceRemoveImportSource
186
+ remove_attribute :phone
187
+ end
188
+ end)
189
+
190
+ choice = ChoiceRemoveImportModel.choice_attributes.first
191
+ names = choice.flat_attributes.map(&:name)
192
+ expect(names).to contain_exactly(:name, :email)
193
+ end
194
+
195
+ it "removes a restrict-added attribute from the choice" do
196
+ stub_const("ChoiceRemoveRestrictModel", Class.new(Lutaml::Model::Serializable) do
197
+ attribute :tag, :string
198
+
199
+ choice min: 0, max: 1 do
200
+ restrict :tag, values: %w[a b c]
201
+ remove_attribute :tag
202
+ end
203
+ end)
204
+
205
+ choice = ChoiceRemoveRestrictModel.choice_attributes.first
206
+ expect(choice.flat_attributes).to be_empty
207
+ expect(ChoiceRemoveRestrictModel.attributes[:tag].options[:choice]).to be_nil
208
+ end
209
+
210
+ it "returns the removed attribute" do
211
+ stub_const("ChoiceRemoveReturnModel", Class.new(Lutaml::Model::Serializable) do
212
+ attribute :item, :string
213
+
214
+ choice min: 0, max: 1 do
215
+ attribute :item, :string
216
+ end
217
+ end)
218
+
219
+ choice = ChoiceRemoveReturnModel.choice_attributes.first
220
+ removed = choice.remove_attribute(:item)
221
+ expect(removed).to be_a(Lutaml::Model::Attribute)
222
+ expect(removed.name).to eq(:item)
223
+ end
224
+ end
225
+ end
@@ -153,16 +153,16 @@ module CustomModelSpecs
153
153
  end
154
154
 
155
155
  class MixedWithNestedContent < Lutaml::Model::Serializable
156
- attribute :street, :string, raw: true
157
- attribute :city, :string, raw: true
156
+ attribute :street, :string
157
+ attribute :city, :string
158
158
  attribute :bibdata, Bibdata
159
159
 
160
160
  xml do
161
161
  element "MixedWithNestedContent"
162
162
  mixed_content
163
163
 
164
- map_element "street", to: :street
165
- map_element "city", to: :city
164
+ map_element "street", to: :street, raw: :content
165
+ map_element "city", to: :city, raw: :content
166
166
  map_element "bibdata",
167
167
  to: :bibdata,
168
168
  with: { from: :bibdata_from_xml, to: :bibdata_to_xml }
@@ -107,12 +107,12 @@ module MixedContentSpec
107
107
  end
108
108
 
109
109
  class SpecialCharContentWithRawAndMixedOption < Lutaml::Model::Serializable
110
- attribute :special, :string, raw: true
110
+ attribute :special, :string
111
111
 
112
112
  xml do
113
113
  element "SpecialCharContentWithRawOptionAndMixedOption"
114
114
  mixed_content
115
- map_element :special, to: :special
115
+ map_element :special, to: :special, raw: :content
116
116
  end
117
117
  end
118
118
 
@@ -240,12 +240,12 @@ RSpec.describe MultipleMapping do
240
240
  expect(product1.name).to eq("Coffee Maker")
241
241
  expect(product1.description).to eq("Premium coffee maker")
242
242
  expect(product1.status).to eq("active")
243
- expect(product1.content.to_s).to match(/Some content here/)
243
+ expect(product1.content.to_s).to include("Some content here")
244
244
 
245
245
  expect(product2.name).to eq("Coffee Maker")
246
246
  expect(product2.description).to eq("Premium coffee maker")
247
247
  expect(product2.status).to eq("in-stock")
248
- expect(product2.content.to_s).to match(/Different content/)
248
+ expect(product2.content.to_s).to include("Different content")
249
249
 
250
250
  # Test round-trip: serialize and deserialize should preserve data
251
251
  # (Note: exact XML format differs between adapters due to mixed content whitespace handling)
@@ -253,13 +253,13 @@ RSpec.describe MultipleMapping do
253
253
  expect(round_trip1.name).to eq(product1.name)
254
254
  expect(round_trip1.description).to eq(product1.description)
255
255
  expect(round_trip1.status).to eq(product1.status)
256
- expect(round_trip1.content.to_s).to match(/Some content here/)
256
+ expect(round_trip1.content.to_s).to include("Some content here")
257
257
 
258
258
  round_trip2 = MultipleMapping::Product.from_xml(product2.to_xml)
259
259
  expect(round_trip2.name).to eq(product2.name)
260
260
  expect(round_trip2.description).to eq(product2.description)
261
261
  expect(round_trip2.status).to eq(product2.status)
262
- expect(round_trip2.content.to_s).to match(/Different content/)
262
+ expect(round_trip2.content.to_s).to include("Different content")
263
263
  end
264
264
  end
265
265
 
@@ -102,8 +102,8 @@ RSpec.describe "OrderedContent" do
102
102
  expect(obj.bold).to eq(["bell", "cool"])
103
103
  expect(obj.italic).to eq(["384,400 km"])
104
104
  expect(obj.underline).to eq("craters")
105
- expect(obj.content.first.to_s).to match(/The Earth's Moon rings like a/)
106
- expect(obj.content.join).to match(/Ain't that/)
105
+ expect(obj.content.first.to_s).to include("The Earth's Moon rings like a")
106
+ expect(obj.content.join).to include("Ain't that")
107
107
 
108
108
  # Verify round-trip preserves data
109
109
  # (Note: exact XML format differs between adapters in ordered mode)
@@ -112,7 +112,7 @@ RSpec.describe "OrderedContent" do
112
112
  expect(round_trip.bold).to eq(obj.bold)
113
113
  expect(round_trip.italic).to eq(obj.italic)
114
114
  expect(round_trip.underline).to eq(obj.underline)
115
- expect(round_trip.content.first.to_s).to match(/The Earth's Moon rings like a/)
115
+ expect(round_trip.content.first.to_s).to include("The Earth's Moon rings like a")
116
116
  end
117
117
  end
118
118
 
@@ -23,7 +23,7 @@ RSpec.describe Lutaml::Model::UninitializedClass do
23
23
 
24
24
  describe "#match?" do
25
25
  it "returns false for any argument" do
26
- expect(uninitialized).not_to match /pattern/
26
+ expect(uninitialized).not_to include("pattern")
27
27
  expect(uninitialized).not_to match "string"
28
28
  expect(uninitialized).not_to match nil
29
29
  end
@@ -85,7 +85,7 @@ RSpec.describe "XSD Form Default Patterns" do
85
85
  xml = instance.to_xml
86
86
 
87
87
  # The element should be unqualified (no prefix) in instance
88
- expect(xml).to match(%r{<premium})
88
+ expect(xml).to include("<premium")
89
89
  expect(xml).to include("<elementFormUnqualified")
90
90
  end
91
91
  end
@@ -281,7 +281,7 @@ RSpec.describe "XSD Form Default Patterns" do
281
281
  # and the namespace is already declared on an ancestor
282
282
  # The child element should be in the blank namespace without xmlns="" declaration
283
283
  # because the parent uses prefix format (xmlns:ex) or the namespace is already declared.
284
- expect(result).not_to match(%r{xmlns=""})
284
+ expect(result).not_to include('xmlns=""')
285
285
  end
286
286
 
287
287
  it "serializes root element with namespace declaration" do
@@ -43,7 +43,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
43
43
 
44
44
  expect(xsd).to include('<element name="item">')
45
45
  expect(xsd).to include("<complexType>")
46
- expect(xsd).not_to match(/<complexType name=/)
46
+ expect(xsd).not_to include("<complexType name=")
47
47
  expect(xsd).to include('<attribute name="id" type="xs:string"/>')
48
48
  end
49
49
  end
@@ -68,7 +68,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
68
68
  expect(xsd).to include('<element name="name" type="xs:string"/>')
69
69
  expect(xsd).to include('<element name="value" type="xs:integer"/>')
70
70
  # Should NOT generate standalone element declaration
71
- expect(xsd).not_to match(/<element name="product"/)
71
+ expect(xsd).not_to include('<element name="product"')
72
72
  end
73
73
 
74
74
  it "type-only model can be referenced by other elements" do
@@ -233,7 +233,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
233
233
 
234
234
  xsd = Lutaml::Model::Schema.to_xsd(klass)
235
235
  expect(xsd).to include('<element name="simple">')
236
- expect(xsd).not_to match(/<element name="simple" type=/)
236
+ expect(xsd).not_to include('<element name="simple" type=')
237
237
  end
238
238
 
239
239
  it "Pattern 2: type_name only -> named reusable" do
@@ -252,7 +252,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
252
252
  xsd = Lutaml::Model::Schema.to_xsd(klass)
253
253
  expect(xsd).to include('<complexType name="SimpleType">')
254
254
  # Should not have standalone element declaration (only child elements in sequence are OK)
255
- expect(xsd).not_to match(/<element name="SimpleType"/)
255
+ expect(xsd).not_to include('<element name="SimpleType"')
256
256
  end
257
257
 
258
258
  it "Pattern 3: both -> element with named type" do
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
- require "lutaml/jsonld"
4
+ require "lutaml/rdf"
5
5
 
6
- RSpec.describe Lutaml::JsonLd::Context do
6
+ RSpec.describe Lutaml::Rdf::Context do
7
7
  subject(:ctx) { described_class.new }
8
8
 
9
9
  describe "empty context" do
@@ -2,8 +2,9 @@
2
2
 
3
3
  require "spec_helper"
4
4
  require "lutaml/jsonld"
5
+ require "lutaml/yamlld"
5
6
 
6
- RSpec.describe Lutaml::JsonLd::Transform do
7
+ RSpec.describe Lutaml::Rdf::LinkedDataTransform do
7
8
  before do
8
9
  stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
9
10
  uri "http://www.w3.org/2004/02/skos/core#"
@@ -138,6 +139,19 @@ RSpec.describe Lutaml::JsonLd::Transform do
138
139
  expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
139
140
  expect(parsed["@context"]["ex"]).to eq("http://example.org/")
140
141
  end
142
+
143
+ it "preserves data through model → YAML-LD → model" do
144
+ yaml = instance.to_yamlld
145
+ restored = JsonLdTestModel.from_yamlld(yaml)
146
+ expect(restored.name).to eq("test")
147
+ expect(restored.description).to eq("A test concept")
148
+ end
149
+
150
+ it "produces the same data model from YAML-LD as JSON-LD" do
151
+ from_yamlld = YAML.safe_load(instance.to_yamlld)
152
+ from_jsonld = JSON.parse(instance.to_jsonld)
153
+ expect(from_yamlld).to eq(from_jsonld)
154
+ end
141
155
  end
142
156
 
143
157
  describe "nil attribute values" do
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
- require "lutaml/jsonld"
4
+ require "lutaml/rdf"
5
5
 
6
- RSpec.describe Lutaml::JsonLd::TermDefinition do
6
+ RSpec.describe Lutaml::Rdf::TermDefinition do
7
7
  it "simple term maps to name => id" do
8
8
  td = described_class.new(name: "name", id: "http://example.org/name")
9
9
  expect(td.to_context_hash).to eq("name" => "http://example.org/name")
@@ -119,13 +119,13 @@ RSpec.describe Lutaml::Turtle::Transform do
119
119
  it "serializes integers as native Turtle literals" do
120
120
  instance = TypedModel.new(label: "test", count: 42, active: true)
121
121
  result = instance.to_turtle
122
- expect(result).to match(/skos:notation 42/)
122
+ expect(result).to include("skos:notation 42")
123
123
  end
124
124
 
125
125
  it "serializes booleans as native Turtle literals" do
126
126
  instance = TypedModel.new(label: "test", count: 1, active: true)
127
127
  result = instance.to_turtle
128
- expect(result).to match(/skos:note true/)
128
+ expect(result).to include("skos:note true")
129
129
  end
130
130
  end
131
131