lutaml-model 0.3.24 → 0.3.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +36 -17
  3. data/README.adoc +396 -30
  4. data/lib/lutaml/model/attribute.rb +52 -27
  5. data/lib/lutaml/model/error/pattern_not_matched_error.rb +17 -0
  6. data/lib/lutaml/model/error/type_error.rb +9 -0
  7. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  8. data/lib/lutaml/model/error/validation_error.rb +0 -1
  9. data/lib/lutaml/model/error.rb +3 -0
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/serialize.rb +10 -5
  12. data/lib/lutaml/model/type/boolean.rb +38 -0
  13. data/lib/lutaml/model/type/date.rb +35 -0
  14. data/lib/lutaml/model/type/date_time.rb +32 -4
  15. data/lib/lutaml/model/type/decimal.rb +42 -0
  16. data/lib/lutaml/model/type/float.rb +37 -0
  17. data/lib/lutaml/model/type/hash.rb +62 -0
  18. data/lib/lutaml/model/type/integer.rb +41 -0
  19. data/lib/lutaml/model/type/string.rb +49 -0
  20. data/lib/lutaml/model/type/time.rb +49 -0
  21. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  22. data/lib/lutaml/model/type/value.rb +52 -0
  23. data/lib/lutaml/model/type.rb +50 -114
  24. data/lib/lutaml/model/validation.rb +2 -1
  25. data/lib/lutaml/model/version.rb +1 -1
  26. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +12 -3
  27. data/lib/lutaml/model/xml_adapter/builder/ox.rb +7 -1
  28. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +3 -1
  29. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +12 -8
  30. data/lib/lutaml/model/xml_adapter/xml_document.rb +6 -8
  31. data/lib/lutaml/model/xml_mapping.rb +6 -2
  32. data/lib/lutaml/model/xml_mapping_rule.rb +7 -1
  33. data/lutaml-model.gemspec +1 -1
  34. data/spec/address_spec.rb +170 -0
  35. data/spec/fixtures/address.rb +33 -0
  36. data/spec/fixtures/person.rb +73 -0
  37. data/spec/fixtures/sample_model.rb +40 -0
  38. data/spec/fixtures/vase.rb +38 -0
  39. data/spec/fixtures/xml/special_char.xml +13 -0
  40. data/spec/lutaml/model/attribute_spec.rb +139 -0
  41. data/spec/lutaml/model/cdata_spec.rb +520 -0
  42. data/spec/lutaml/model/collection_spec.rb +299 -0
  43. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  44. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  45. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  46. data/spec/lutaml/model/defaults_spec.rb +221 -0
  47. data/spec/lutaml/model/delegation_spec.rb +340 -0
  48. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  49. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  50. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  51. data/spec/lutaml/model/map_content_spec.rb +118 -0
  52. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  53. data/spec/lutaml/model/namespace_spec.rb +57 -0
  54. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  55. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  56. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  57. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  58. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  59. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  60. data/spec/lutaml/model/serializable_spec.rb +297 -0
  61. data/spec/lutaml/model/serializable_validation_spec.rb +90 -0
  62. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  63. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  64. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  65. data/spec/lutaml/model/type/date_spec.rb +118 -0
  66. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  67. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  68. data/spec/lutaml/model/type/float_spec.rb +191 -0
  69. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  70. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  71. data/spec/lutaml/model/type/string_spec.rb +150 -0
  72. data/spec/lutaml/model/type/time_spec.rb +142 -0
  73. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  74. data/spec/lutaml/model/type_spec.rb +276 -0
  75. data/spec/lutaml/model/utils_spec.rb +79 -0
  76. data/spec/lutaml/model/validation_spec.rb +83 -0
  77. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  78. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  79. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  80. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  81. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  82. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  83. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  84. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  85. data/spec/lutaml/model_spec.rb +1 -0
  86. data/spec/person_spec.rb +161 -0
  87. data/spec/spec_helper.rb +33 -0
  88. metadata +68 -2
@@ -0,0 +1,625 @@
1
+ # spec/lutaml/model/mixed_content_spec.rb
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model"
5
+ require "lutaml/model/xml_adapter/nokogiri_adapter"
6
+ require "lutaml/model/xml_adapter/ox_adapter"
7
+ require "lutaml/model/xml_adapter/oga_adapter"
8
+ require_relative "../../fixtures/sample_model"
9
+
10
+ module MixedContentSpec
11
+ class PlanetaryBody < Lutaml::Model::Serializable
12
+ attribute :name, :string
13
+ attribute :distance_from_earth, :integer
14
+ xml do
15
+ root "PlanetaryBody"
16
+ map_element "Name", to: :name
17
+ map_element "DistanceFromEarth", to: :distance_from_earth
18
+ end
19
+ end
20
+
21
+ class Source < Lutaml::Model::Serializable
22
+ attribute :content, :string
23
+
24
+ xml do
25
+ root "source"
26
+ map_content to: :content
27
+ end
28
+ end
29
+
30
+ class ElementCitation < Lutaml::Model::Serializable
31
+ attribute :source, Source
32
+
33
+ xml do
34
+ root "element-citation"
35
+ map_element "source", to: :source
36
+ end
37
+ end
38
+
39
+ class Ref < Lutaml::Model::Serializable
40
+ attribute :element_citation, ElementCitation
41
+
42
+ xml do
43
+ root "ref"
44
+ map_element "element-citation", to: :element_citation
45
+ end
46
+ end
47
+
48
+ class RefList < Lutaml::Model::Serializable
49
+ attribute :ref, Ref
50
+
51
+ xml do
52
+ root "ref-list"
53
+ map_element "ref", to: :ref
54
+ end
55
+ end
56
+
57
+ class Back < Lutaml::Model::Serializable
58
+ attribute :ref_list, RefList
59
+
60
+ xml do
61
+ root "back"
62
+ map_element "ref-list", to: :ref_list
63
+ end
64
+ end
65
+
66
+ class Article < Lutaml::Model::Serializable
67
+ attribute :back, Back
68
+
69
+ xml do
70
+ root "article"
71
+ map_element "back", to: :back
72
+ end
73
+ end
74
+
75
+ class SpecialCharContentWithMixedTrue < Lutaml::Model::Serializable
76
+ attribute :content, :string
77
+
78
+ xml do
79
+ root "SpecialCharContentWithMixedTrue", mixed: true
80
+ map_content to: :content
81
+ end
82
+ end
83
+
84
+ class SpecialCharContentWithRawAndMixedOption < Lutaml::Model::Serializable
85
+ attribute :special, :string, raw: true
86
+
87
+ xml do
88
+ root "SpecialCharContentWithRawOptionAndMixedOption", mixed: true
89
+ map_element :special, to: :special
90
+ end
91
+ end
92
+
93
+ class RootMixedContent < Lutaml::Model::Serializable
94
+ attribute :id, :string
95
+ attribute :bold, :string, collection: true
96
+ attribute :italic, :string, collection: true
97
+ attribute :underline, :string
98
+ attribute :content, :string
99
+
100
+ xml do
101
+ root "RootMixedContent", mixed: true
102
+ map_attribute :id, to: :id
103
+ map_element :bold, to: :bold
104
+ map_element :italic, to: :italic
105
+ map_element :underline, to: :underline
106
+ map_content to: :content
107
+ end
108
+ end
109
+
110
+ class RootMixedContentWithModel < Lutaml::Model::Serializable
111
+ attribute :id, :string
112
+ attribute :bold, :string, collection: true
113
+ attribute :italic, :string, collection: true
114
+ attribute :underline, :string
115
+ attribute :planetary_body, PlanetaryBody
116
+ attribute :content, :string
117
+
118
+ xml do
119
+ root "RootMixedContentWithModel", mixed: true
120
+ map_content to: :content
121
+ map_attribute :id, to: :id
122
+ map_element :bold, to: :bold
123
+ map_element :italic, to: :italic
124
+ map_element :underline, to: :underline
125
+ map_element "PlanetaryBody", to: :planetary_body
126
+ end
127
+ end
128
+
129
+ class RootMixedContentNested < Lutaml::Model::Serializable
130
+ attribute :id, :string
131
+ attribute :text, :string
132
+ attribute :content, RootMixedContent
133
+ attribute :sup, :string, collection: true
134
+ attribute :sub, :string, collection: true
135
+
136
+ xml do
137
+ root "RootMixedContentNested", mixed: true
138
+ map_content to: :text
139
+ map_attribute :id, to: :id
140
+ map_element :sup, to: :sup
141
+ map_element :sub, to: :sub
142
+ map_element "MixedContent", to: :content
143
+ end
144
+ end
145
+
146
+ class RootMixedContentNestedWithModel < Lutaml::Model::Serializable
147
+ attribute :id, :string
148
+ attribute :text, :string
149
+ attribute :content, RootMixedContentWithModel
150
+ attribute :sup, :string, collection: true
151
+ attribute :sub, :string, collection: true
152
+
153
+ xml do
154
+ root "RootMixedContentNestedWithModel", mixed: true
155
+
156
+ map_content to: :text
157
+ map_attribute :id, to: :id
158
+ map_element :sup, to: :sup
159
+ map_element :sub, to: :sub
160
+ map_element "MixedContentWithModel", to: :content
161
+ end
162
+ end
163
+
164
+ class TextualSupport < Lutaml::Model::Serializable
165
+ attribute :value, :string
166
+
167
+ xml do
168
+ root "TextualSupport"
169
+
170
+ map_element :value, to: :value
171
+ end
172
+ end
173
+ end
174
+
175
+ RSpec.describe "MixedContent" do
176
+ shared_examples "mixed content behavior" do |adapter_class|
177
+ around do |example|
178
+ old_adapter = Lutaml::Model::Config.xml_adapter
179
+ Lutaml::Model::Config.xml_adapter = adapter_class
180
+ example.run
181
+ ensure
182
+ Lutaml::Model::Config.xml_adapter = old_adapter
183
+ end
184
+
185
+ context "when mixed: true is set at root" do
186
+ let(:xml) do
187
+ <<~XML
188
+ <RootMixedContent id="123">
189
+ The Earth's Moon rings like a <bold>bell</bold> when struck by
190
+ meteroids. Distanced from the Earth by <italic>384,400 km</italic>,
191
+ its surface is covered in <underline>craters</underline>.
192
+ Ain't that <bold>cool</bold>?
193
+ </RootMixedContent>
194
+ XML
195
+ end
196
+
197
+ it "deserializes and serializes mixed content correctly" do
198
+ parsed = MixedContentSpec::RootMixedContent.from_xml(xml)
199
+
200
+ expected_content = [
201
+ "\n The Earth's Moon rings like a ",
202
+ " when struck by\n meteroids. Distanced from the Earth by ",
203
+ ",\n its surface is covered in ",
204
+ ".\n Ain't that ",
205
+ "?\n",
206
+ ]
207
+
208
+ expect(parsed.id).to eq("123")
209
+ expect(parsed.bold).to eq(["bell", "cool"])
210
+ expect(parsed.italic).to eq(["384,400 km"])
211
+ expect(parsed.underline).to eq("craters")
212
+
213
+ parsed.content.each_with_index do |content, index|
214
+ expected_output = expected_content[index]
215
+
216
+ # due to the difference in capturing
217
+ # newlines in ox and nokogiri adapters
218
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
219
+ expected_output = expected_output.gsub(/\n\s*/, " ")
220
+ end
221
+
222
+ expect(content).to eq(expected_output)
223
+ end
224
+
225
+ serialized = parsed.to_xml
226
+ expect(serialized).to be_equivalent_to(xml)
227
+ end
228
+ end
229
+
230
+ context "when mixed: true is set at root with nested model" do
231
+ let(:xml) do
232
+ <<~XML
233
+ <RootMixedContentWithModel id="123">
234
+ The Earth's Moon rings like a <bold>bell</bold> when struck by
235
+ meteroids. Distanced from the Earth by <italic>384,400 km</italic>,
236
+ its surface is covered in <underline>craters</underline>.
237
+ Ain't that <bold>cool</bold>?
238
+ <PlanetaryBody>
239
+ <Name>Moon</Name>
240
+ <DistanceFromEarth>384400</DistanceFromEarth>
241
+ </PlanetaryBody>
242
+ NOTE: The above model content is to be formatted as a table.
243
+ </RootMixedContentWithModel>
244
+ XML
245
+ end
246
+
247
+ it "deserializes and serializes mixed content correctly" do
248
+ parsed = MixedContentSpec::RootMixedContentWithModel.from_xml(xml)
249
+
250
+ expected_content = [
251
+ "\n The Earth's Moon rings like a ",
252
+ " when struck by\n meteroids. Distanced from the Earth by ",
253
+ ",\n its surface is covered in ",
254
+ ".\n Ain't that ",
255
+ "?\n ",
256
+ "\n NOTE: The above model content is to be formatted as a table.\n",
257
+ ]
258
+
259
+ expect(parsed.id).to eq("123")
260
+ expect(parsed.bold).to eq(["bell", "cool"])
261
+ expect(parsed.italic).to eq(["384,400 km"])
262
+ expect(parsed.underline).to eq("craters")
263
+
264
+ parsed.content.each_with_index do |content, index|
265
+ expected_output = expected_content[index]
266
+
267
+ # due to the difference in capturing
268
+ # newlines in ox and nokogiri adapters
269
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
270
+ expected_output = expected_output.gsub(/\n\s*/, " ")
271
+ end
272
+
273
+ expect(content).to eq(expected_output)
274
+ end
275
+
276
+ expect(parsed.planetary_body.name).to eq("Moon")
277
+ expect(parsed.planetary_body.distance_from_earth).to eq(384400)
278
+
279
+ serialized = parsed.to_xml
280
+ expect(serialized).to be_equivalent_to(xml)
281
+ end
282
+ end
283
+
284
+ context "when mixed: true is set for nested content" do
285
+ let(:xml) do
286
+ <<~XML
287
+ <RootMixedContentNested id="outer123">
288
+ The following text is about the Moon.
289
+ <MixedContent id="inner456">
290
+ The Earth's Moon rings like a <bold>bell</bold> when struck by
291
+ meteroids. Distanced from the Earth by <italic>384,400 km</italic>,
292
+ its surface is covered in <underline>craters</underline>.
293
+ Ain't that <bold>cool</bold>?
294
+ </MixedContent>
295
+ <sup>1</sup>: The Moon is not a planet.
296
+ <sup>2</sup>: The Moon's atmosphere is mainly composed of helium in the form of He<sub>2</sub>.
297
+ </RootMixedContentNested>
298
+ XML
299
+ end
300
+
301
+ it "deserializes and serializes mixed content correctly" do
302
+ parsed = MixedContentSpec::RootMixedContentNested.from_xml(xml)
303
+
304
+ expected_content = [
305
+ "\n The Earth's Moon rings like a ",
306
+ " when struck by\n meteroids. Distanced from the Earth by ",
307
+ ",\n its surface is covered in ",
308
+ ".\n Ain't that ",
309
+ "?\n ",
310
+ ]
311
+
312
+ expect(parsed.id).to eq("outer123")
313
+ expect(parsed.sup).to eq(["1", "2"])
314
+ expect(parsed.sub).to eq(["2"])
315
+ expect(parsed.content.id).to eq("inner456")
316
+ expect(parsed.content.bold).to eq(["bell", "cool"])
317
+ expect(parsed.content.italic).to eq(["384,400 km"])
318
+ expect(parsed.content.underline).to eq("craters")
319
+
320
+ parsed.content.content.each_with_index do |content, index|
321
+ expected_output = expected_content[index]
322
+
323
+ # due to the difference in capturing
324
+ # newlines in ox and nokogiri adapters
325
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
326
+ expected_output = expected_output.gsub(/\n\s*/, " ")
327
+ end
328
+
329
+ expect(content).to eq(expected_output)
330
+ end
331
+
332
+ serialized = parsed.to_xml
333
+ expect(serialized).to be_equivalent_to(xml)
334
+ end
335
+ end
336
+
337
+ context "when mixed: true is set for nested content with model" do
338
+ let(:xml) do
339
+ <<~XML
340
+ <RootMixedContentNestedWithModel id="outer123">
341
+ The following text is about the Moon.
342
+ <MixedContentWithModel id="inner456">
343
+ The Earth's Moon rings like a <bold>bell</bold> when struck by
344
+ meteroids. Distanced from the Earth by <italic>384,400 km</italic>,
345
+ its surface is covered in <underline>craters</underline>.
346
+ Ain't that <bold>cool</bold>?
347
+ <PlanetaryBody>
348
+ <Name>Moon</Name>
349
+ <DistanceFromEarth>384400</DistanceFromEarth>
350
+ </PlanetaryBody>
351
+ NOTE: The above model content is to be formatted as a table.
352
+ </MixedContentWithModel>
353
+ <sup>1</sup>: The Moon is not a planet.
354
+ <sup>2</sup>: The Moon's atmosphere is mainly composed of helium in the form of He<sub>2</sub>.
355
+ </RootMixedContentNestedWithModel>
356
+ XML
357
+ end
358
+
359
+ it "deserializes and serializes mixed content correctly" do
360
+ parsed = MixedContentSpec::RootMixedContentNestedWithModel.from_xml(xml)
361
+
362
+ expected_content = [
363
+ "\n The Earth's Moon rings like a ",
364
+ " when struck by\n meteroids. Distanced from the Earth by ",
365
+ ",\n its surface is covered in ",
366
+ ".\n Ain't that ",
367
+ "?\n ",
368
+ "\n NOTE: The above model content is to be formatted as a table.\n ",
369
+ ]
370
+
371
+ expect(parsed.id).to eq("outer123")
372
+ expect(parsed.sup).to eq(["1", "2"])
373
+ expect(parsed.sub).to eq(["2"])
374
+ expect(parsed.content.id).to eq("inner456")
375
+ expect(parsed.content.bold).to eq(["bell", "cool"])
376
+ expect(parsed.content.italic).to eq(["384,400 km"])
377
+ expect(parsed.content.underline).to eq("craters")
378
+
379
+ parsed.content.content.each_with_index do |content, index|
380
+ expected_output = expected_content[index]
381
+
382
+ # due to the difference in capturing
383
+ # newlines in ox and nokogiri adapters
384
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
385
+ expected_output = expected_output.gsub(/\n\s*/, " ")
386
+ end
387
+
388
+ expect(content).to eq(expected_output)
389
+ end
390
+
391
+ expect(parsed.content.planetary_body.name).to eq("Moon")
392
+ expect(parsed.content.planetary_body.distance_from_earth).to eq(384400)
393
+
394
+ serialized = parsed.to_xml
395
+ expect(serialized).to be_equivalent_to(xml)
396
+ end
397
+ end
398
+
399
+ context "when mixed: true is used with map_element" do
400
+ it "raises an error" do
401
+ expect do
402
+ Class.new(Lutaml::Model::Serializable) do
403
+ attribute :id, :string
404
+
405
+ xml do
406
+ root "Invalid"
407
+ map_element :id, to: :id, mixed: true
408
+ end
409
+ end
410
+ end.to raise_error(ArgumentError, /unknown keyword: :mixed/)
411
+ end
412
+ end
413
+
414
+ context "when mixed: true is used with map_attribute" do
415
+ it "raises an error" do
416
+ expect do
417
+ Class.new(Lutaml::Model::Serializable) do
418
+ attribute :id, :string
419
+
420
+ xml do
421
+ root "Invalid"
422
+ map_attribute :id, to: :id, mixed: true
423
+ end
424
+ end
425
+ end.to raise_error(ArgumentError, /unknown keyword: :mixed/)
426
+ end
427
+ end
428
+
429
+ context "when special char used in content with mixed true" do
430
+ let(:xml) do
431
+ <<~XML
432
+ <SpecialCharContentWithMixedTrue>
433
+ Moon&#x0026;Mars Distanced&#x00A9;its &#8212; surface covered &amp; processed
434
+ </SpecialCharContentWithMixedTrue>
435
+ XML
436
+ end
437
+
438
+ describe ".from_xml" do
439
+ let(:expected_content) { "Moon&Mars Distanced©its — surface covered & processed" }
440
+
441
+ it "deserializes special char mixed content correctly" do
442
+ parsed = MixedContentSpec::SpecialCharContentWithMixedTrue.from_xml(xml)
443
+ expect(parsed.content.strip).to eq(expected_content)
444
+ end
445
+ end
446
+
447
+ describe ".to_xml" do
448
+ let(:expected_xml) { "Moon&amp;Mars Distanced©its — surface covered &amp; processed" }
449
+
450
+ it "serializes special char mixed content correctly" do
451
+ parsed = MixedContentSpec::SpecialCharContentWithMixedTrue.from_xml(xml)
452
+ serialized = parsed.to_xml
453
+
454
+ expect(serialized).to include(expected_xml)
455
+ end
456
+ end
457
+ end
458
+
459
+ context "when special char used in content read from xml file" do
460
+ let(:fixture) { File.read(fixture_path("xml/special_char.xml")) }
461
+
462
+ describe ".from_xml" do
463
+ it "deserializes special char mixed content correctly" do
464
+ parsed = MixedContentSpec::Article.from_xml(fixture)
465
+ expect(parsed.back.ref_list.ref.element_citation.source.content).to include("R&D")
466
+ end
467
+ end
468
+
469
+ describe ".to_xml" do
470
+ it "serializes special char mixed content correctly" do
471
+ parsed = MixedContentSpec::Article.from_xml(fixture)
472
+ serialized = parsed.to_xml
473
+
474
+ expect(serialized).to include("R&amp;D")
475
+ end
476
+ end
477
+ end
478
+
479
+ context "when special char entities used with raw true" do
480
+ let(:xml) do
481
+ <<~XML
482
+ <SpecialCharContentWithRawAndMixedOption>
483
+ <special>
484
+ B <p>R&#x0026;C</p>
485
+ C <p>J&#8212;C</p>
486
+ O <p>A &amp; B </p>
487
+ F <p>Z &#x00A9;S</p>
488
+ </special>
489
+ </SpecialCharContentWithRawAndMixedOption>
490
+ XML
491
+ end
492
+
493
+ describe ".from_xml" do
494
+ let(:expected_nokogiri_content) { "B <p>R&amp;C</p>\n C <p>J&#x2014;C</p>\n O <p>A &amp; B </p>\n F <p>Z &#xA9;S</p>" }
495
+ let(:expected_ox_content) { "B <p>R&amp;C</p>\n C <p>J—C</p>\n O <p>A &amp; B </p>\n F <p>Z ©S</p>" }
496
+
497
+ it "deserializes special char mixed content correctly" do
498
+ parsed = MixedContentSpec::SpecialCharContentWithRawAndMixedOption.from_xml(xml)
499
+ expected_content = expected_nokogiri_content
500
+
501
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
502
+ expected_content = expected_ox_content
503
+ parsed.special.force_encoding("UTF-8")
504
+ end
505
+
506
+ expect(parsed.special.strip).to eq(expected_content)
507
+ end
508
+ end
509
+
510
+ describe ".to_xml" do
511
+ let(:expected_nokogiri_xml) do
512
+ <<~XML
513
+ <SpecialCharContentWithRawOptionAndMixedOption><special>
514
+ B &lt;p&gt;R&amp;amp;C&lt;/p&gt;
515
+ C &lt;p&gt;J&amp;#x2014;C&lt;/p&gt;
516
+ O &lt;p&gt;A &amp;amp; B &lt;/p&gt;
517
+ F &lt;p&gt;Z &amp;#xA9;S&lt;/p&gt;
518
+ </special></SpecialCharContentWithRawOptionAndMixedOption>
519
+ XML
520
+ end
521
+
522
+ let(:expected_ox_xml) do
523
+ <<~XML
524
+ <SpecialCharContentWithRawOptionAndMixedOption>
525
+ <special> B &lt;p&gt;R&amp;amp;C&lt;/p&gt;
526
+ C &lt;p&gt;J—C&lt;/p&gt;
527
+ O &lt;p&gt;A &amp;amp; B &lt;/p&gt;
528
+ F &lt;p&gt;Z ©S&lt;/p&gt;
529
+ </special>
530
+ </SpecialCharContentWithRawOptionAndMixedOption>
531
+ XML
532
+ end
533
+
534
+ it "serializes special char mixed content correctly" do
535
+ parsed = MixedContentSpec::SpecialCharContentWithRawAndMixedOption.from_xml(xml)
536
+ serialized = parsed.to_xml
537
+
538
+ expected_output = adapter_class == Lutaml::Model::XmlAdapter::OxAdapter ? expected_ox_xml : expected_nokogiri_xml
539
+ expect(serialized).to be_equivalent_to(expected_output)
540
+ end
541
+ end
542
+ end
543
+
544
+ context "when special char used with raw true, remove & if entity not provided" do
545
+ let(:xml) do
546
+ <<~XML
547
+ <SpecialCharContentWithRawAndMixedOption>
548
+ <special>
549
+ B <p>R&C</p>
550
+ </special>
551
+ </SpecialCharContentWithRawAndMixedOption>
552
+ XML
553
+ end
554
+
555
+ describe ".from_xml" do
556
+ let(:expected_nokogiri_content) { "B <p>R</p>" }
557
+ let(:expected_ox_content) { "B <p>R&amp;C</p>" }
558
+
559
+ it "deserializes special char mixed content correctly" do
560
+ parsed = MixedContentSpec::SpecialCharContentWithRawAndMixedOption.from_xml(xml)
561
+
562
+ expected_output = adapter_class == Lutaml::Model::XmlAdapter::OxAdapter ? expected_ox_content : expected_nokogiri_content
563
+ expect(parsed.special.strip).to eq(expected_output)
564
+ end
565
+ end
566
+
567
+ describe ".to_xml" do
568
+ let(:expected_nokogiri_xml) { "B &lt;p&gt;R&lt;/p&gt;" }
569
+ let(:expected_ox_xml) { "B &lt;p&gt;R&amp;amp;C&lt;/p&gt;" }
570
+
571
+ it "serializes special char mixed content correctly" do
572
+ parsed = MixedContentSpec::SpecialCharContentWithRawAndMixedOption.from_xml(xml)
573
+ serialized = parsed.to_xml
574
+
575
+ expected_output = adapter_class == Lutaml::Model::XmlAdapter::OxAdapter ? expected_ox_xml : expected_nokogiri_xml
576
+ expect(serialized).to include(expected_output)
577
+ end
578
+ end
579
+ end
580
+
581
+ context "when special char used as full entities" do
582
+ let(:xml) do
583
+ <<~XML
584
+ <TextualSupport>
585
+ <value>&lt;computer security&gt; type of operation specified by an access right</value>
586
+ </TextualSupport>
587
+ XML
588
+ end
589
+
590
+ describe ".from_xml" do
591
+ let(:expected_content) { "<computer security> type of operation specified by an access right" }
592
+
593
+ it "deserializes special char mixed content correctly" do
594
+ parsed = MixedContentSpec::TextualSupport.from_xml(xml)
595
+
596
+ expect(parsed.value).to eq(expected_content)
597
+ end
598
+ end
599
+
600
+ describe ".to_xml" do
601
+ let(:expected_xml) { "<TextualSupport>\n <value>&lt;computer security&gt; type of operation specified by an access right</value>\n</TextualSupport>" }
602
+
603
+ it "serializes special char mixed content correctly" do
604
+ parsed = MixedContentSpec::TextualSupport.from_xml(xml)
605
+ serialized = parsed.to_xml
606
+
607
+ expect(serialized.strip).to include(expected_xml)
608
+ end
609
+ end
610
+ end
611
+ end
612
+
613
+ describe Lutaml::Model::XmlAdapter::NokogiriAdapter do
614
+ it_behaves_like "mixed content behavior", described_class
615
+ end
616
+
617
+ describe Lutaml::Model::XmlAdapter::OxAdapter do
618
+ it_behaves_like "mixed content behavior", described_class
619
+ end
620
+
621
+ # Not implemented yet
622
+ xdescribe Lutaml::Model::XmlAdapter::OgaAdapter do
623
+ it_behaves_like "mixed content behavior", described_class
624
+ end
625
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ module NamespaceSpec
4
+ class NestedChild < Lutaml::Model::Serializable
5
+ attribute :name, :string
6
+
7
+ xml do
8
+ root "NestedChild"
9
+
10
+ map_element :name, to: :name
11
+ end
12
+ end
13
+
14
+ class Child < Lutaml::Model::Serializable
15
+ attribute :nested_child, NestedChild
16
+
17
+ xml do
18
+ root "NestedChild"
19
+
20
+ map_element :NestedChild, to: :nested_child
21
+ end
22
+ end
23
+
24
+ class Parent < Lutaml::Model::Serializable
25
+ attribute :child, Child
26
+
27
+ xml do
28
+ root "Parent"
29
+ namespace "https://abc.com"
30
+
31
+ map_element :Child, to: :child
32
+ end
33
+ end
34
+ end
35
+
36
+ RSpec.describe "NamespaceSpec" do
37
+ let(:parsed) { NamespaceSpec::Parent.from_xml(xml) }
38
+ let(:xml) do
39
+ <<~XML
40
+ <Parent xmlns="https://abc.com">
41
+ <Child>
42
+ <NestedChild>
43
+ <name>Rogger moore</name>
44
+ </NestedChild>
45
+ </Child>
46
+ </Parent>
47
+ XML
48
+ end
49
+
50
+ it "parses nested child using root namespace" do
51
+ expect(parsed.child.nested_child.name).to eq("Rogger moore")
52
+ end
53
+
54
+ it "round-trips xml" do
55
+ expect(parsed.to_xml).to be_equivalent_to(xml)
56
+ end
57
+ end