lutaml-model 0.8.15 → 0.8.16

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.
@@ -0,0 +1,533 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module RawElementSpec
6
+ class SvgContainer < Lutaml::Model::Serializable
7
+ attribute :svg_data, :string
8
+
9
+ xml do
10
+ element "container"
11
+ map_element "svg", to: :svg_data, raw: :element
12
+ end
13
+ end
14
+
15
+ class MathContainer < Lutaml::Model::Serializable
16
+ attribute :formula, :string
17
+
18
+ xml do
19
+ element "container"
20
+ map_element "math", to: :formula, raw: :element
21
+ end
22
+ end
23
+
24
+ class MixedContainer < Lutaml::Model::Serializable
25
+ attribute :name, :string
26
+ attribute :embedded, :string
27
+
28
+ xml do
29
+ element "container"
30
+ map_element "name", to: :name
31
+ map_element "foreign", to: :embedded, raw: :element
32
+ end
33
+ end
34
+
35
+ class NestedRawContainer < Lutaml::Model::Serializable
36
+ attribute :name, :string
37
+ attribute :embedded, :string
38
+
39
+ xml do
40
+ element "container"
41
+ map_element "name", to: :name
42
+ map_element "embedded", to: :embedded, raw: :element
43
+ end
44
+ end
45
+
46
+ class MultiRawContainer < Lutaml::Model::Serializable
47
+ attribute :name, :string
48
+ attribute :svg_data, :string
49
+ attribute :math_data, :string
50
+
51
+ xml do
52
+ element "container"
53
+ map_element "name", to: :name
54
+ map_element "svg", to: :svg_data, raw: :element
55
+ map_element "math", to: :math_data, raw: :element
56
+ end
57
+ end
58
+
59
+ class CollectionRawContainer < Lutaml::Model::Serializable
60
+ attribute :name, :string
61
+ attribute :fragments, :string, collection: true
62
+
63
+ xml do
64
+ element "container"
65
+ map_element "name", to: :name
66
+ map_element "fragment", to: :fragments, raw: :element
67
+ end
68
+ end
69
+
70
+ class EmptyRawContainer < Lutaml::Model::Serializable
71
+ attribute :name, :string
72
+ attribute :embedded, :string
73
+
74
+ xml do
75
+ element "container"
76
+ map_element "name", to: :name
77
+ map_element "foreign", to: :embedded, raw: :element
78
+ end
79
+ end
80
+
81
+ class NormalContainer < Lutaml::Model::Serializable
82
+ attribute :data, :string
83
+
84
+ xml do
85
+ element "container"
86
+ map_element "data", to: :data
87
+ end
88
+ end
89
+
90
+ RSpec.describe "map_element raw: :element" do
91
+ describe "deserialization" do
92
+ describe "foreign namespace element capture" do
93
+ let(:svg_xml) do
94
+ <<~XML
95
+ <container>
96
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
97
+ <rect x="0" y="0" width="100" height="100" fill="red"/>
98
+ </svg>
99
+ </container>
100
+ XML
101
+ end
102
+
103
+ it "captures the full SVG element including its own tags" do
104
+ doc = SvgContainer.from_xml(svg_xml)
105
+ expect(doc.svg_data).to include("<svg")
106
+ expect(doc.svg_data).to include("</svg>")
107
+ expect(doc.svg_data).to include("viewBox")
108
+ expect(doc.svg_data).to include("<rect")
109
+ end
110
+
111
+ it "preserves SVG namespace attribute" do
112
+ doc = SvgContainer.from_xml(svg_xml)
113
+ expect(doc.svg_data).to include("xmlns=\"http://www.w3.org/2000/svg\"")
114
+ end
115
+
116
+ it "preserves child elements" do
117
+ doc = SvgContainer.from_xml(svg_xml)
118
+ expect(doc.svg_data).to include("<rect")
119
+ expect(doc.svg_data).to include("fill=\"red\"")
120
+ end
121
+ end
122
+
123
+ describe "non-namespaced element capture" do
124
+ let(:math_xml) do
125
+ "<container><math><mfrac><mi>x</mi><mn>2</mn></mfrac></math></container>"
126
+ end
127
+
128
+ it "captures the full element" do
129
+ doc = MathContainer.from_xml(math_xml)
130
+ expect(doc.formula).to include("<math>")
131
+ expect(doc.formula).to include("</math>")
132
+ expect(doc.formula).to include("<mfrac>")
133
+ end
134
+ end
135
+
136
+ describe "mixed mapped and raw elements" do
137
+ let(:mixed_xml) do
138
+ <<~XML
139
+ <container>
140
+ <name>test</name>
141
+ <foreign attr="value">
142
+ <child>content</child>
143
+ </foreign>
144
+ </container>
145
+ XML
146
+ end
147
+
148
+ it "parses normal elements normally" do
149
+ doc = MixedContainer.from_xml(mixed_xml)
150
+ expect(doc.name).to eq("test")
151
+ end
152
+
153
+ it "captures raw elements as full XML" do
154
+ doc = MixedContainer.from_xml(mixed_xml)
155
+ expect(doc.embedded).to include("<foreign")
156
+ expect(doc.embedded).to include("</foreign>")
157
+ expect(doc.embedded).to include("attr=\"value\"")
158
+ expect(doc.embedded).to include("<child>content</child>")
159
+ end
160
+ end
161
+
162
+ describe "nested XML in raw element" do
163
+ let(:nested_xml) do
164
+ <<~XML
165
+ <container>
166
+ <name>test</name>
167
+ <embedded>
168
+ <level1>
169
+ <level2 attr="deep">text</level2>
170
+ </level1>
171
+ </embedded>
172
+ </container>
173
+ XML
174
+ end
175
+
176
+ it "preserves full nesting" do
177
+ doc = NestedRawContainer.from_xml(nested_xml)
178
+ expect(doc.embedded).to include("<level1>")
179
+ expect(doc.embedded).to include("<level2 attr=\"deep\">text</level2>")
180
+ expect(doc.embedded).to include("</level1>")
181
+ end
182
+ end
183
+
184
+ describe "multiple raw: :element mappings" do
185
+ let(:multi_xml) do
186
+ <<~XML
187
+ <container>
188
+ <name>mixed content</name>
189
+ <svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>
190
+ <math><mi>&#x3C0;</mi></math>
191
+ </container>
192
+ XML
193
+ end
194
+
195
+ it "captures each raw element independently" do
196
+ doc = MultiRawContainer.from_xml(multi_xml)
197
+ expect(doc.name).to eq("mixed content")
198
+ expect(doc.svg_data).to include("<svg")
199
+ expect(doc.svg_data).to include("<circle")
200
+ expect(doc.math_data).to include("<math>")
201
+ expect(doc.math_data).to include("<mi>")
202
+ end
203
+ end
204
+
205
+ describe "collection raw: :element" do
206
+ let(:collection_xml) do
207
+ <<~XML
208
+ <container>
209
+ <name>multi</name>
210
+ <fragment id="1">first</fragment>
211
+ <fragment id="2">second</fragment>
212
+ <fragment id="3"><nested>third</nested></fragment>
213
+ </container>
214
+ XML
215
+ end
216
+
217
+ it "captures each occurrence as a collection item" do
218
+ doc = CollectionRawContainer.from_xml(collection_xml)
219
+ expect(doc.name).to eq("multi")
220
+ expect(doc.fragments.length).to eq(3)
221
+ expect(doc.fragments[0]).to include("<fragment")
222
+ expect(doc.fragments[0]).to include("first")
223
+ expect(doc.fragments[1]).to include("second")
224
+ expect(doc.fragments[2]).to include("<nested>third</nested>")
225
+ end
226
+ end
227
+
228
+ describe "missing raw element" do
229
+ it "returns nil when raw element is absent" do
230
+ doc = SvgContainer.from_xml("<container><other>text</other></container>")
231
+ expect(doc.svg_data).to be_nil
232
+ end
233
+ end
234
+
235
+ describe "empty raw element" do
236
+ it "captures self-closing empty element" do
237
+ doc = EmptyRawContainer.from_xml("<container><name>test</name><foreign/></container>")
238
+ expect(doc.embedded).to include("<foreign")
239
+ expect(doc.embedded).to include("/")
240
+ end
241
+ end
242
+
243
+ describe "XML special characters" do
244
+ it "preserves entities in raw element content" do
245
+ xml = "<container><foreign>a &amp; b &lt; c</foreign></container>"
246
+ doc = MixedContainer.from_xml("<container><name>test</name>#{xml.match(/<container>(.*)<\/container>/m)[1]}</container>")
247
+ expect(doc.embedded).to include("<foreign")
248
+ end
249
+ end
250
+
251
+ describe "namespace matching" do
252
+ it "captures element with xmlns declaration on itself" do
253
+ doc = SvgContainer.from_xml(
254
+ '<container><svg xmlns="http://www.w3.org/2000/svg"><rect/></svg></container>',
255
+ )
256
+ expect(doc.svg_data).to include("<svg")
257
+ expect(doc.svg_data).to include('xmlns="http://www.w3.org/2000/svg"')
258
+ end
259
+
260
+ it "captures element with no namespace" do
261
+ doc = SvgContainer.from_xml(
262
+ "<container><svg><rect/></svg></container>",
263
+ )
264
+ expect(doc.svg_data).to include("<svg")
265
+ expect(doc.svg_data).to include("<rect")
266
+ end
267
+
268
+ it "captures explicitly prefixed element" do
269
+ doc = SvgContainer.from_xml(
270
+ '<container xmlns:s="http://www.w3.org/2000/svg"><s:svg><s:rect/></s:svg></container>',
271
+ )
272
+ expect(doc.svg_data).to include("<s:svg")
273
+ expect(doc.svg_data).to include("<s:rect")
274
+ end
275
+ end
276
+ end
277
+
278
+ describe "serialization" do
279
+ describe "round-trip with SVG" do
280
+ let(:svg_xml) do
281
+ <<~XML
282
+ <container>
283
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
284
+ <rect x="0" y="0" width="100" height="100" fill="red"/>
285
+ </svg>
286
+ </container>
287
+ XML
288
+ end
289
+
290
+ it "preserves SVG data through parse -> serialize -> parse" do
291
+ doc = SvgContainer.from_xml(svg_xml)
292
+ output = doc.to_xml
293
+ doc2 = SvgContainer.from_xml(output)
294
+ expect(doc2.svg_data).to include("<svg")
295
+ expect(doc2.svg_data).to include("</svg>")
296
+ expect(doc2.svg_data).to include("viewBox")
297
+ expect(doc2.svg_data).to include("<rect")
298
+ expect(doc2.svg_data).to include("xmlns=\"http://www.w3.org/2000/svg\"")
299
+ end
300
+ end
301
+
302
+ describe "round-trip with mixed content" do
303
+ let(:mixed_xml) do
304
+ '<container><name>test</name><foreign attr="value"><child>content</child></foreign></container>'
305
+ end
306
+
307
+ it "preserves both normal and raw elements" do
308
+ doc = MixedContainer.from_xml(mixed_xml)
309
+ output = doc.to_xml
310
+ doc2 = MixedContainer.from_xml(output)
311
+ expect(doc2.name).to eq("test")
312
+ expect(doc2.embedded).to include("<foreign")
313
+ expect(doc2.embedded).to include("attr=\"value\"")
314
+ expect(doc2.embedded).to include("<child>content</child>")
315
+ end
316
+ end
317
+
318
+ describe "round-trip with nested raw XML" do
319
+ let(:nested_xml) do
320
+ '<container><name>test</name><embedded><level1><level2 attr="deep">text</level2></level1></embedded></container>'
321
+ end
322
+
323
+ it "preserves full nesting depth" do
324
+ doc = NestedRawContainer.from_xml(nested_xml)
325
+ output = doc.to_xml
326
+ doc2 = NestedRawContainer.from_xml(output)
327
+ expect(doc2.embedded).to include("<level1>")
328
+ expect(doc2.embedded).to include("<level2 attr=\"deep\">text</level2>")
329
+ expect(doc2.embedded).to include("</level1>")
330
+ end
331
+ end
332
+
333
+ describe "building from model instance" do
334
+ it "serializes a programmatically created model" do
335
+ model = SvgContainer.new(svg_data: '<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>')
336
+ xml = model.to_xml
337
+ expect(xml).to include("<svg")
338
+ expect(xml).to include("<circle")
339
+ expect(xml).to include("</svg>")
340
+ end
341
+
342
+ it "round-trips a programmatically created model" do
343
+ original_svg = '<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>'
344
+ model = SvgContainer.new(svg_data: original_svg)
345
+ xml = model.to_xml
346
+ doc = SvgContainer.from_xml(xml)
347
+ expect(doc.svg_data).to include("<svg")
348
+ expect(doc.svg_data).to include("<circle")
349
+ expect(doc.svg_data).to include("</svg>")
350
+ end
351
+
352
+ it "does not double-escape raw element content" do
353
+ model = SvgContainer.new(svg_data: "<svg><rect/></svg>")
354
+ xml = model.to_xml
355
+ expect(xml).not_to include("&lt;svg")
356
+ expect(xml).not_to include("&gt;")
357
+ end
358
+ end
359
+
360
+ describe "round-trip with collection" do
361
+ let(:collection_xml) do
362
+ <<~XML
363
+ <container>
364
+ <name>multi</name>
365
+ <fragment id="1">first</fragment>
366
+ <fragment id="2">second</fragment>
367
+ </container>
368
+ XML
369
+ end
370
+
371
+ it "preserves all collection items" do
372
+ doc = CollectionRawContainer.from_xml(collection_xml)
373
+ output = doc.to_xml
374
+ doc2 = CollectionRawContainer.from_xml(output)
375
+ expect(doc2.fragments.length).to eq(2)
376
+ expect(doc2.fragments[0]).to include("first")
377
+ expect(doc2.fragments[1]).to include("second")
378
+ end
379
+ end
380
+
381
+ describe "nil and empty values" do
382
+ it "omits raw element when value is nil" do
383
+ model = SvgContainer.new(svg_data: nil)
384
+ xml = model.to_xml
385
+ expect(xml).not_to include("<svg")
386
+ end
387
+
388
+ it "omits raw element when value is empty string" do
389
+ model = SvgContainer.new(svg_data: "")
390
+ xml = model.to_xml
391
+ expect(xml).not_to include("<svg")
392
+ end
393
+ end
394
+
395
+ describe "XML special characters round-trip" do
396
+ it "preserves angle brackets in raw element content without double-escaping" do
397
+ doc = MixedContainer.from_xml(
398
+ '<container><name>test</name><foreign><child a="1">x &amp; y</child></foreign></container>',
399
+ )
400
+ output = doc.to_xml
401
+ expect(output).to include("<child")
402
+ expect(output).not_to include("&lt;child")
403
+ doc2 = MixedContainer.from_xml(output)
404
+ expect(doc2.embedded).to include("<child")
405
+ expect(doc2.embedded).to include("x &amp; y")
406
+ end
407
+ end
408
+ end
409
+
410
+ describe "default behavior (no raw)" do
411
+ it "does not capture raw XML for normal map_element" do
412
+ doc = NormalContainer.from_xml("<container><data>text</data></container>")
413
+ expect(doc.data).to eq("text")
414
+ expect(doc.data).not_to include("<data>")
415
+ end
416
+
417
+ it "serializes text content normally" do
418
+ model = NormalContainer.new(data: "hello")
419
+ xml = model.to_xml
420
+ expect(xml).to include("<data>hello</data>")
421
+ expect(xml).not_to include("&lt;")
422
+ end
423
+ end
424
+
425
+ describe "MappingRule attributes" do
426
+ it "defaults raw to nil" do
427
+ rule = MixedContainer.mappings_for(:xml).elements.find do |r|
428
+ r.to == :name
429
+ end
430
+ expect(rule.raw).to be_nil
431
+ end
432
+
433
+ it "sets raw to :element when specified" do
434
+ rule = MixedContainer.mappings_for(:xml).elements.find do |r|
435
+ r.to == :embedded
436
+ end
437
+ expect(rule.raw).to eq(:element)
438
+ end
439
+
440
+ it "provides raw_element backward-compat alias" do
441
+ rule = MixedContainer.mappings_for(:xml).elements.find do |r|
442
+ r.to == :embedded
443
+ end
444
+ expect(rule.raw_element?).to be(true)
445
+ end
446
+
447
+ it "propagates raw through deep_dup" do
448
+ rule = MixedContainer.mappings_for(:xml).elements.find do |r|
449
+ r.to == :embedded
450
+ end
451
+ dup = rule.deep_dup
452
+ expect(dup.raw).to eq(:element)
453
+ end
454
+ end
455
+
456
+ describe "raw parameter validation" do
457
+ it "accepts :element" do
458
+ expect do
459
+ Lutaml::Xml::MappingRule.new("test", to: :x, raw: :element)
460
+ end.not_to raise_error
461
+ end
462
+
463
+ it "accepts :content" do
464
+ expect do
465
+ Lutaml::Xml::MappingRule.new("test", to: :x, raw: :content)
466
+ end.not_to raise_error
467
+ end
468
+
469
+ it "accepts nil" do
470
+ rule = Lutaml::Xml::MappingRule.new("test", to: :x, raw: nil)
471
+ expect(rule.raw).to be_nil
472
+ end
473
+
474
+ it "rejects invalid values" do
475
+ expect do
476
+ Lutaml::Xml::MappingRule.new("test", to: :x, raw: :invalid)
477
+ end.to raise_error(ArgumentError, /raw must be :element or :content/)
478
+ end
479
+
480
+ it "deprecates raw: true with warning" do
481
+ expect do
482
+ Lutaml::Xml::MappingRule.new("test", to: :x, raw: true)
483
+ end.to output(/DEPRECATED.*raw: :element/).to_stderr
484
+ end
485
+
486
+ it "treats raw: true as :element" do
487
+ rule = nil
488
+ expect do
489
+ rule = Lutaml::Xml::MappingRule.new("test", to: :x, raw: true)
490
+ end.to output(/DEPRECATED/).to_stderr
491
+ expect(rule.raw).to eq(:element)
492
+ end
493
+ end
494
+ end
495
+
496
+ # raw: :content mode — inner XML capture
497
+ module RawContentSpec
498
+ class StreetContainer < Lutaml::Model::Serializable
499
+ attribute :street, :string
500
+
501
+ xml do
502
+ element "address"
503
+ map_element "street", to: :street, raw: :content
504
+ end
505
+ end
506
+
507
+ RSpec.describe "map_element raw: :content" do
508
+ describe "deserialization" do
509
+ it "captures inner XML content" do
510
+ doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
511
+ expect(doc.street).to include("<b>123</b>")
512
+ expect(doc.street).to include("Main St")
513
+ end
514
+
515
+ it "does not include wrapper tags" do
516
+ doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
517
+ expect(doc.street).not_to include("<street>")
518
+ expect(doc.street).not_to include("</street>")
519
+ end
520
+ end
521
+
522
+ describe "serialization" do
523
+ it "round-trips inner XML content" do
524
+ doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
525
+ output = doc.to_xml
526
+ doc2 = StreetContainer.from_xml(output)
527
+ expect(doc2.street).to include("<b>123</b>")
528
+ expect(doc2.street).to include("Main St")
529
+ end
530
+ end
531
+ end
532
+ end
533
+ end
@@ -59,7 +59,8 @@ RSpec.describe Lutaml::Rdf::Mapping do
59
59
 
60
60
  it "flattens nested arrays" do
61
61
  mapping.types(["skos:Concept", "owl:Thing"], "foaf:Person")
62
- expect(mapping.rdf_type).to eq(["skos:Concept", "owl:Thing", "foaf:Person"])
62
+ expect(mapping.rdf_type).to eq(["skos:Concept", "owl:Thing",
63
+ "foaf:Person"])
63
64
  end
64
65
 
65
66
  it "overwrites previous types on subsequent call" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.15
4
+ version: 0.8.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-01 00:00:00.000000000 Z
11
+ date: 2026-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1628,6 +1628,7 @@ files:
1628
1628
  - spec/lutaml/model/ordered_content_spec.rb
1629
1629
  - spec/lutaml/model/polymorphic_spec.rb
1630
1630
  - spec/lutaml/model/processing_instruction_spec.rb
1631
+ - spec/lutaml/model/raw_element_spec.rb
1631
1632
  - spec/lutaml/model/register_key_value_spec.rb
1632
1633
  - spec/lutaml/model/register_methods_spec.rb
1633
1634
  - spec/lutaml/model/register_spec.rb