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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +0 -3
  3. data/.rubocop_todo.yml +49 -52
  4. data/README.adoc +188 -25
  5. data/docs/_guides/xml/namespace-semantics.adoc +2 -0
  6. data/docs/_guides/xml-mapping.adoc +178 -24
  7. data/docs/_guides/xml-namespace-qualification.adoc +142 -0
  8. data/docs/_pages/importable_models.adoc +7 -1
  9. data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
  10. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
  11. data/docs/namespace-management.adoc +2 -0
  12. data/docs/xml-schema-qualification.md +6 -0
  13. data/lib/lutaml/jsonld.rb +1 -4
  14. data/lib/lutaml/model/attribute.rb +4 -0
  15. data/lib/lutaml/model/choice.rb +34 -0
  16. data/lib/lutaml/model/compiled_rule.rb +7 -0
  17. data/lib/lutaml/model/version.rb +1 -1
  18. data/lib/lutaml/model.rb +3 -1
  19. data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
  20. data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
  21. data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
  22. data/lib/lutaml/rdf.rb +3 -0
  23. data/lib/lutaml/turtle/transform.rb +2 -1
  24. data/lib/lutaml/xml/adapter/plan_based_builder.rb +3 -1
  25. data/lib/lutaml/xml/adapter/xml_serializer.rb +14 -4
  26. data/lib/lutaml/xml/adapter.rb +2 -1
  27. data/lib/lutaml/xml/adapter_element.rb +2 -2
  28. data/lib/lutaml/xml/builder/base.rb +2 -1
  29. data/lib/lutaml/xml/data_model.rb +19 -3
  30. data/lib/lutaml/xml/mapping.rb +3 -1
  31. data/lib/lutaml/xml/mapping_rule.rb +28 -2
  32. data/lib/lutaml/xml/model_transform.rb +9 -1
  33. data/lib/lutaml/xml/serialization/instance_methods.rb +16 -9
  34. data/lib/lutaml/xml/transformation/element_builder.rb +39 -26
  35. data/lib/lutaml/xml/transformation/rule_applier.rb +21 -0
  36. data/lib/lutaml/xml/transformation/rule_compiler.rb +12 -3
  37. data/lib/lutaml/yamlld/adapter.rb +25 -0
  38. data/lib/lutaml/yamlld.rb +25 -0
  39. data/spec/lutaml/integration/multi_format_spec.rb +23 -0
  40. data/spec/lutaml/model/attribute_spec.rb +8 -1
  41. data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
  42. data/spec/lutaml/model/custom_model_spec.rb +4 -4
  43. data/spec/lutaml/model/mixed_content_spec.rb +50 -9
  44. data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
  45. data/spec/lutaml/model/ordered_content_spec.rb +3 -3
  46. data/spec/lutaml/model/raw_element_spec.rb +533 -0
  47. data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
  48. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
  49. data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
  50. data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
  51. data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +18 -2
  52. data/spec/lutaml/rdf/mapping_spec.rb +2 -1
  53. data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
  54. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  55. data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
  56. data/spec/lutaml/xml/mapping_spec.rb +4 -4
  57. data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
  58. data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
  59. data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
  60. data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
  61. data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
  62. data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
  63. data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
  64. data/spec/lutaml/yamlld/registration_spec.rb +33 -0
  65. metadata +14 -8
@@ -253,6 +253,15 @@ _register)
253
253
 
254
254
  private
255
255
 
256
+ # Resolve the unified raw mode from mapping rule and attribute
257
+ #
258
+ # @param mapping_rule [Xml::MappingRule] The mapping rule
259
+ # @param attr [Attribute, nil] The attribute (nil for custom methods)
260
+ # @return [Symbol, nil] :element, :content, or nil
261
+ def resolve_raw_mode(mapping_rule, attr)
262
+ mapping_rule.raw || (attr&.raw? ? :content : nil)
263
+ end
264
+
256
265
  # Infer attribute name from mapping rule or custom methods
257
266
  #
258
267
  # @param mapping_rule [Xml::MappingRule] The mapping rule
@@ -321,7 +330,7 @@ register_id, register, attr_name, custom_methods_value)
321
330
  mapping_type: :element,
322
331
  cdata: mapping_rule.cdata,
323
332
  mixed_content: mapping_rule.mixed_content?,
324
- raw: attr.raw?,
333
+ raw: resolve_raw_mode(mapping_rule, attr),
325
334
  render_default: mapping_rule.render_default,
326
335
  value_map: value_map,
327
336
  custom_methods: custom_methods_value,
@@ -350,7 +359,7 @@ custom_methods_value)
350
359
  mapping_type: :element,
351
360
  cdata: mapping_rule.cdata,
352
361
  mixed_content: mapping_rule.mixed_content?,
353
- raw: false,
362
+ raw: mapping_rule.raw,
354
363
  render_default: mapping_rule.render_default,
355
364
  value_map: value_map,
356
365
  custom_methods: custom_methods_value,
@@ -401,7 +410,7 @@ register_id, register, custom_methods_value)
401
410
  mapping_type: :element,
402
411
  cdata: mapping_rule.cdata,
403
412
  mixed_content: mapping_rule.mixed_content?,
404
- raw: attr.raw?,
413
+ raw: resolve_raw_mode(mapping_rule, attr),
405
414
  render_default: mapping_rule.render_default,
406
415
  value_map: value_map,
407
416
  custom_methods: custom_methods_value,
@@ -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
 
@@ -1111,19 +1111,22 @@ RSpec.describe "MixedContent" do
1111
1111
  expect(enum).to be_a(Enumerator)
1112
1112
  end
1113
1113
 
1114
- it "skips whitespace-only text nodes" do
1114
+ it "yields whitespace text nodes in mixed content" do
1115
1115
  parsed = MixedContentSpec::RootMixedContent.from_xml(xml)
1116
1116
 
1117
1117
  results = []
1118
1118
  parsed.each_mixed_content do |node|
1119
- results << node if node.is_a?(String) && node.strip.empty?
1119
+ results << node if node.is_a?(String)
1120
1120
  end
1121
1121
 
1122
- expect(results).to eq([])
1122
+ expect(results).not_to be_empty
1123
+ text_joined = results.join
1124
+ expect(text_joined).to include("Hello")
1125
+ expect(text_joined).to include("and")
1126
+ expect(text_joined).to include("!")
1123
1127
  end
1124
1128
 
1125
1129
  context "with ordered-only content (no mixed)" do
1126
- # Test that ordered content (elements only, no text) works
1127
1130
  let(:xml) do
1128
1131
  <<~XML
1129
1132
  <RootMixedContentNested id="outer">
@@ -1134,15 +1137,17 @@ RSpec.describe "MixedContent" do
1134
1137
  XML
1135
1138
  end
1136
1139
 
1137
- it "yields element objects in document order" do
1140
+ it "yields element values in document order" do
1138
1141
  parsed = MixedContentSpec::RootMixedContentNested.from_xml(xml)
1139
1142
 
1140
1143
  results = []
1141
1144
  parsed.content.each_mixed_content { |node| results << node }
1142
1145
 
1143
- # Should yield element objects in order (bold, italic strings)
1146
+ # RootMixedContent has mixed_content, so whitespace text nodes
1147
+ # ARE yielded alongside element values
1144
1148
  string_results = results.grep(String)
1145
- expect(string_results.length).to eq(2)
1149
+ expect(string_results).to include("first")
1150
+ expect(string_results).to include("second")
1146
1151
  end
1147
1152
  end
1148
1153
 
@@ -1152,6 +1157,42 @@ RSpec.describe "MixedContent" do
1152
1157
  expect(parsed.each_mixed_content.to_a).to eq([])
1153
1158
  end
1154
1159
  end
1160
+
1161
+ context "with ordered-only model (no mixed_content)" do
1162
+ before do
1163
+ stub_const("OrderedOnlyContainer", Class.new(Lutaml::Model::Serializable) do
1164
+ attribute :items, :string, collection: true
1165
+
1166
+ xml do
1167
+ element "container"
1168
+ ordered
1169
+ map_element "item", to: :items
1170
+ end
1171
+ end)
1172
+ end
1173
+
1174
+ it "skips whitespace-only text nodes between elements" do
1175
+ xml = <<~XML
1176
+ <container>
1177
+ <item>first</item>
1178
+ <item>second</item>
1179
+ </container>
1180
+ XML
1181
+
1182
+ parsed = OrderedOnlyContainer.from_xml(xml)
1183
+ results = []
1184
+ parsed.each_mixed_content { |node| results << node }
1185
+
1186
+ # Ordered-only: whitespace between elements is formatting noise, not content
1187
+ whitespace_only = results.select do |n|
1188
+ n.is_a?(String) && n.strip.empty?
1189
+ end
1190
+ expect(whitespace_only).to eq([])
1191
+
1192
+ string_results = results.grep(String)
1193
+ expect(string_results).to eq(%w[first second])
1194
+ end
1195
+ end
1155
1196
  end
1156
1197
 
1157
1198
  # Issue #630: Mutation after deserialization should update serialization output
@@ -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