lutaml-model 0.3.25 → 0.3.27

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.
@@ -47,6 +47,7 @@ module Lutaml
47
47
  render_default: false,
48
48
  with: {},
49
49
  delegate: nil,
50
+ cdata: false,
50
51
  namespace: (namespace_set = false
51
52
  nil),
52
53
  prefix: (prefix_set = false
@@ -61,6 +62,7 @@ module Lutaml
61
62
  render_default: render_default,
62
63
  with: with,
63
64
  delegate: delegate,
65
+ cdata: cdata,
64
66
  namespace: namespace,
65
67
  default_namespace: namespace_uri,
66
68
  prefix: prefix,
@@ -109,7 +111,8 @@ module Lutaml
109
111
  render_default: false,
110
112
  with: {},
111
113
  delegate: nil,
112
- mixed: false
114
+ mixed: false,
115
+ cdata: false
113
116
  )
114
117
  validate!("content", to, with)
115
118
 
@@ -121,6 +124,7 @@ module Lutaml
121
124
  with: with,
122
125
  delegate: delegate,
123
126
  mixed_content: mixed,
127
+ cdata: cdata,
124
128
  )
125
129
  end
126
130
 
@@ -211,7 +215,7 @@ module Lutaml
211
215
  end
212
216
 
213
217
  def find_by_name(name)
214
- if name.to_s == "text"
218
+ if ["text", "#cdata-section"].include?(name.to_s)
215
219
  content_mapping
216
220
  else
217
221
  mappings.detect do |rule|
@@ -3,7 +3,7 @@ require_relative "mapping_rule"
3
3
  module Lutaml
4
4
  module Model
5
5
  class XmlMappingRule < MappingRule
6
- attr_reader :namespace, :prefix, :mixed_content, :default_namespace
6
+ attr_reader :namespace, :prefix, :mixed_content, :default_namespace, :cdata
7
7
 
8
8
  def initialize(
9
9
  name,
@@ -15,6 +15,7 @@ module Lutaml
15
15
  namespace: nil,
16
16
  prefix: nil,
17
17
  mixed_content: false,
18
+ cdata: false,
18
19
  namespace_set: false,
19
20
  prefix_set: false,
20
21
  attribute: false,
@@ -38,6 +39,7 @@ module Lutaml
38
39
  end
39
40
  @prefix = prefix
40
41
  @mixed_content = mixed_content
42
+ @cdata = cdata
41
43
 
42
44
  @default_namespace = default_namespace
43
45
 
@@ -61,6 +63,10 @@ module Lutaml
61
63
  name == "__raw_mapping"
62
64
  end
63
65
 
66
+ def content_key
67
+ cdata ? "#cdata-section" : "text"
68
+ end
69
+
64
70
  def mixed_content?
65
71
  !!@mixed_content
66
72
  end
@@ -33,6 +33,33 @@ RSpec.describe Lutaml::Model::Attribute do
33
33
  .to("avatar.png")
34
34
  end
35
35
 
36
+ describe "#validate_options!" do
37
+ let(:validate_options) { name_attr.method(:validate_options!) }
38
+
39
+ Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
40
+ it "return true if option is `#{option}`" do
41
+ expect(validate_options.call({ option => "value" })).to be(true)
42
+ end
43
+ end
44
+
45
+ it "raise exception if option is not allowed" do
46
+ expect do
47
+ validate_options.call({ foo: "bar" })
48
+ end.to raise_error(StandardError, "Invalid options given for `name` [:foo]")
49
+ end
50
+
51
+ it "raise exception if pattern is given with non string type" do
52
+ age_attr = described_class.new("age", :integer)
53
+
54
+ expect do
55
+ age_attr.send(:validate_options!, { pattern: /[A-Za-z ]/ })
56
+ end.to raise_error(
57
+ StandardError,
58
+ "Invalid option `pattern` given for `age`, `pattern` is only allowed for :string type",
59
+ )
60
+ end
61
+ end
62
+
36
63
  describe "#validate_type!" do
37
64
  let(:validate_type) { name_attr.method(:validate_type!) }
38
65
 
@@ -0,0 +1,520 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+ require "lutaml/model/xml_adapter/nokogiri_adapter"
4
+ require "lutaml/model/xml_adapter/ox_adapter"
5
+
6
+ module CDATA
7
+ class Beta < Lutaml::Model::Serializable
8
+ attribute :element1, :string
9
+
10
+ xml do
11
+ root "beta"
12
+ map_content to: :element1, cdata: true
13
+ end
14
+ end
15
+
16
+ class Alpha < Lutaml::Model::Serializable
17
+ attribute :element1, :string
18
+ attribute :element2, :string
19
+ attribute :element3, :string
20
+ attribute :beta, Beta
21
+
22
+ xml do
23
+ root "alpha"
24
+
25
+ map_element "element1", to: :element1, cdata: false
26
+ map_element "element2", to: :element2, cdata: true
27
+ map_element "element3", to: :element3, cdata: false
28
+ map_element "beta", to: :beta
29
+ end
30
+ end
31
+
32
+ class Address < Lutaml::Model::Serializable
33
+ attribute :street, :string
34
+ attribute :city, :string
35
+ attribute :house, :string
36
+ attribute :address, Address
37
+
38
+ xml do
39
+ root "address"
40
+ map_element "street", to: :street
41
+ map_element "city", with: { from: :city_from_xml, to: :city_to_xml }, cdata: true
42
+ map_element "house", with: { from: :house_from_xml, to: :house_to_xml }, cdata: false
43
+ map_element "address", to: :address
44
+ end
45
+
46
+ def house_from_xml(model, node)
47
+ model.house = node
48
+ end
49
+
50
+ def house_to_xml(model, _parent, doc)
51
+ doc.create_and_add_element("house") do |element|
52
+ element.add_text(element, model.house, cdata: false)
53
+ end
54
+ end
55
+
56
+ def city_from_xml(model, node)
57
+ model.city = node
58
+ end
59
+
60
+ def city_to_xml(model, _parent, doc)
61
+ doc.create_and_add_element("city") do |element|
62
+ element.add_text(element, model.city, cdata: true)
63
+ end
64
+ end
65
+ end
66
+
67
+ class CustomModelChild
68
+ attr_accessor :street, :city
69
+ end
70
+
71
+ class CustomModelParent
72
+ attr_accessor :first_name, :middle_name, :last_name, :child_mapper
73
+
74
+ def name
75
+ "#{first_name} #{last_name}"
76
+ end
77
+ end
78
+
79
+ class CustomModelChildMapper < Lutaml::Model::Serializable
80
+ model CustomModelChild
81
+
82
+ attribute :street, Lutaml::Model::Type::String
83
+ attribute :city, Lutaml::Model::Type::String
84
+
85
+ xml do
86
+ map_element :street, to: :street, cdata: true
87
+ map_element :city, to: :city, cdata: true
88
+ end
89
+ end
90
+
91
+ class CustomModelParentMapper < Lutaml::Model::Serializable
92
+ model CustomModelParent
93
+
94
+ attribute :first_name, Lutaml::Model::Type::String
95
+ attribute :middle_name, Lutaml::Model::Type::String
96
+ attribute :last_name, Lutaml::Model::Type::String
97
+ attribute :child_mapper, CustomModelChildMapper
98
+
99
+ xml do
100
+ root "CustomModelParent"
101
+ map_element :first_name, to: :first_name, cdata: true
102
+ map_element :middle_name, to: :middle_name, cdata: true
103
+ map_element :last_name, to: :last_name, cdata: false
104
+ map_element :CustomModelChild, with: { to: :child_to_xml, from: :child_from_xml }, cdata: true
105
+ end
106
+
107
+ def child_to_xml(model, _parent, doc)
108
+ doc.create_and_add_element("CustomModelChild") do |child_el|
109
+ child_el.create_and_add_element("street") do |street_el|
110
+ street_el.add_text(street_el, model.child_mapper.street, cdata: true)
111
+ end
112
+ child_el.create_and_add_element("city") do |city_el|
113
+ city_el.add_text(city_el, model.child_mapper.city, cdata: true)
114
+ end
115
+ end
116
+ end
117
+
118
+ def child_from_xml(model, value)
119
+ model.child_mapper ||= CustomModelChild.new
120
+
121
+ model.child_mapper.street = value["street"].text
122
+ model.child_mapper.city = value["city"].text
123
+ end
124
+ end
125
+
126
+ class RootMixedContent < Lutaml::Model::Serializable
127
+ attribute :id, :string
128
+ attribute :bold, :string, collection: true
129
+ attribute :italic, :string, collection: true
130
+ attribute :underline, :string
131
+ attribute :content, :string
132
+
133
+ xml do
134
+ root "RootMixedContent", mixed: true
135
+ map_attribute :id, to: :id
136
+ map_element :bold, to: :bold, cdata: true
137
+ map_element :italic, to: :italic, cdata: true
138
+ map_element :underline, to: :underline, cdata: true
139
+ map_content to: :content, cdata: true
140
+ end
141
+ end
142
+
143
+ class RootMixedContentNested < Lutaml::Model::Serializable
144
+ attribute :id, :string
145
+ attribute :data, :string
146
+ attribute :content, RootMixedContent
147
+ attribute :sup, :string, collection: true
148
+ attribute :sub, :string, collection: true
149
+
150
+ xml do
151
+ root "RootMixedContentNested", mixed: true
152
+ map_content to: :data, cdata: true
153
+ map_attribute :id, to: :id
154
+ map_element :sup, to: :sup, cdata: true
155
+ map_element :sub, to: :sub, cdata: false
156
+ map_element "MixedContent", to: :content
157
+ end
158
+ end
159
+
160
+ class DefaultValue < Lutaml::Model::Serializable
161
+ attribute :name, :string, default: -> { "Default Value" }
162
+ attribute :temperature, :integer, default: -> { 1050 }
163
+ attribute :opacity, :string, default: -> { "Opaque" }
164
+ attribute :content, :string, default: -> { " " }
165
+
166
+ xml do
167
+ root "DefaultValue"
168
+ map_element "name", to: :name, render_default: true, cdata: true
169
+ map_element "temperature", to: :temperature, render_default: true, cdata: true
170
+ map_element "opacity", to: :opacity, cdata: false, render_default: true
171
+ map_content to: :content, cdata: true, render_default: true
172
+ end
173
+ end
174
+ end
175
+
176
+ RSpec.describe "CDATA" do
177
+ let(:parent_mapper) { CDATA::CustomModelParentMapper }
178
+ let(:child_mapper) { CDATA::CustomModelChildMapper }
179
+ let(:parent_model) { CDATA::CustomModelParent }
180
+ let(:child_model) { CDATA::CustomModelChild }
181
+
182
+ shared_examples "cdata behavior" do |adapter_class|
183
+ around do |example|
184
+ old_adapter = Lutaml::Model::Config.xml_adapter
185
+ Lutaml::Model::Config.xml_adapter = adapter_class
186
+ example.run
187
+ ensure
188
+ Lutaml::Model::Config.xml_adapter = old_adapter
189
+ end
190
+
191
+ context "with CDATA option" do
192
+ let(:xml) do
193
+ <<~XML.strip
194
+ <alpha>
195
+ <element1><![CDATA[foo]]></element1>
196
+ <element2><![CDATA[one]]></element2>
197
+ <element2><![CDATA[two]]></element2>
198
+ <element2><![CDATA[three]]></element2>
199
+ <element3>bar</element3>
200
+ <beta><![CDATA[child]]></beta>
201
+ </alpha>
202
+ XML
203
+ end
204
+
205
+ let(:expected_xml) do
206
+ <<~XML.strip
207
+ <alpha>
208
+ <element1>foo</element1>
209
+ <element2>
210
+ <![CDATA[one]]>
211
+ </element2>
212
+ <element2>
213
+ <![CDATA[two]]>
214
+ </element2>
215
+ <element2>
216
+ <![CDATA[three]]>
217
+ </element2>
218
+ <element3>bar</element3>
219
+ <beta>
220
+ <![CDATA[child]]>
221
+ </beta>
222
+ </alpha>
223
+ XML
224
+ end
225
+
226
+ it "maps xml to object" do
227
+ instance = CDATA::Alpha.from_xml(xml)
228
+
229
+ expect(instance.element1).to eq("foo")
230
+ expect(instance.element2).to eq(%w[one two three])
231
+ expect(instance.element3).to eq("bar")
232
+ expect(instance.beta.element1).to eq("child")
233
+ end
234
+
235
+ it "converts objects to xml" do
236
+ instance = CDATA::Alpha.new(
237
+ element1: "foo",
238
+ element2: %w[one two three],
239
+ element3: "bar",
240
+ beta: CDATA::Beta.new(element1: "child"),
241
+ )
242
+
243
+ expect(instance.to_xml).to be_equivalent_to(expected_xml)
244
+ end
245
+ end
246
+
247
+ context "with custom methods" do
248
+ let(:xml) do
249
+ <<~XML
250
+ <address>
251
+ <street>A</street>
252
+ <city><![CDATA[B]]></city>
253
+ <house><![CDATA[H]]></house>
254
+ <address>
255
+ <street>C</street>
256
+ <city><![CDATA[D]]></city>
257
+ <house><![CDATA[G]]></house>
258
+ </address>
259
+ </address>
260
+ XML
261
+ end
262
+
263
+ let(:expected_xml) do
264
+ <<~XML
265
+ <address>
266
+ <street>A</street>
267
+ <city>
268
+ <![CDATA[B]]>
269
+ </city>
270
+ <house>H</house>
271
+ <address>
272
+ <street>C</street>
273
+ <city>
274
+ <![CDATA[D]]>
275
+ </city>
276
+ <house>G</house>
277
+ </address>
278
+ </address>
279
+ XML
280
+ end
281
+
282
+ it "round-trips XML" do
283
+ model = CDATA::Address.from_xml(xml)
284
+ expect(model.to_xml).to be_equivalent_to(expected_xml)
285
+ end
286
+ end
287
+
288
+ context "with custom models" do
289
+ let(:input_xml) do
290
+ <<~XML
291
+ <CustomModelParent>
292
+ <first_name><![CDATA[John]]></first_name>
293
+ <last_name><![CDATA[Doe]]></last_name>
294
+ <CustomModelChild>
295
+ <street><![CDATA[Oxford Street]]></street>
296
+ <city><![CDATA[London]]></city>
297
+ </CustomModelChild>
298
+ </CustomModelParent>
299
+ XML
300
+ end
301
+
302
+ let(:expected_nokogiri_xml) do
303
+ <<~XML
304
+ <CustomModelParent>
305
+ <first_name><![CDATA[John]]></first_name>
306
+ <last_name>Doe</last_name>
307
+ <CustomModelChild>
308
+ <street><![CDATA[Oxford Street]]></street>
309
+ <city><![CDATA[London]]></city>
310
+ </CustomModelChild>
311
+ </CustomModelParent>
312
+ XML
313
+ end
314
+
315
+ let(:expected_ox_xml) do
316
+ <<~XML
317
+ <CustomModelParent>
318
+ <first_name>
319
+ <![CDATA[John]]>
320
+ </first_name>
321
+ <last_name>Doe</last_name>
322
+ <CustomModelChild>
323
+ <street>
324
+ <![CDATA[Oxford Street]]>
325
+ </street>
326
+ <city>
327
+ <![CDATA[London]]>
328
+ </city>
329
+ </CustomModelChild>
330
+ </CustomModelParent>
331
+ XML
332
+ end
333
+
334
+ describe ".from_xml" do
335
+ it "maps XML content to custom model using custom methods" do
336
+ instance = parent_mapper.from_xml(input_xml)
337
+
338
+ expect(instance.class).to eq(parent_model)
339
+ expect(instance.first_name).to eq("John")
340
+ expect(instance.last_name).to eq("Doe")
341
+ expect(instance.name).to eq("John Doe")
342
+
343
+ expect(instance.child_mapper.class).to eq(child_model)
344
+ expect(instance.child_mapper.street).to eq("Oxford Street")
345
+ expect(instance.child_mapper.city).to eq("London")
346
+ end
347
+ end
348
+
349
+ describe ".to_xml" do
350
+ it "with correct model converts objects to xml using custom methods" do
351
+ instance = parent_mapper.from_xml(input_xml)
352
+ result_xml = parent_mapper.to_xml(instance)
353
+
354
+ expected_output = adapter_class == Lutaml::Model::XmlAdapter::OxAdapter ? expected_ox_xml : expected_nokogiri_xml
355
+
356
+ expect(result_xml.strip).to eq(expected_output.strip)
357
+ end
358
+ end
359
+ end
360
+
361
+ context "when mixed: true is set for nested content" do
362
+ let(:xml) do
363
+ <<~XML
364
+ <RootMixedContentNested id="outer123">
365
+ <![CDATA[The following text is about the Moon.]]>
366
+ <MixedContent id="inner456">
367
+ <![CDATA[The Earth's Moon rings like a ]]>
368
+ <bold><![CDATA[bell]]></bold>
369
+ <![CDATA[ when struck by meteroids. Distanced from the Earth by ]]>
370
+ <italic><![CDATA[384,400 km]]></italic>,
371
+ <![CDATA[ ,its surface is covered in ]]>
372
+ <underline><![CDATA[craters]]></underline>.
373
+ <![CDATA[ .Ain't that ]]>
374
+ <bold><![CDATA[cool]]></bold>
375
+ <![CDATA[ ? ]]>
376
+ </MixedContent>
377
+ <sup><![CDATA[1]]></sup>: <![CDATA[The Moon is not a planet.]]>
378
+ <sup><![CDATA[2]]></sup>: <![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]><sub><![CDATA[2]]></sub>.
379
+ </RootMixedContentNested>
380
+ XML
381
+ end
382
+
383
+ expected_xml = "<RootMixedContentNested id=\"outer123\"><![CDATA[The following text is about the Moon.]]><MixedContent id=\"inner456\"><![CDATA[The Earth's Moon rings like a ]]><bold><![CDATA[bell]]></bold><![CDATA[ when struck by meteroids. Distanced from the Earth by ]]><italic><![CDATA[384,400 km]]></italic><![CDATA[ ,its surface is covered in ]]><underline><![CDATA[craters]]></underline><![CDATA[ .Ain't that ]]><bold><![CDATA[cool]]></bold><![CDATA[ ? ]]></MixedContent><sup><![CDATA[1]]></sup><![CDATA[The Moon is not a planet.]]><sup><![CDATA[2]]></sup><![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]><sub>2</sub></RootMixedContentNested>"
384
+
385
+ expected_ox_xml = <<~XML
386
+ <RootMixedContentNested id="outer123">
387
+ <![CDATA[The following text is about the Moon.]]>
388
+ <MixedContent id="inner456">
389
+ <![CDATA[The Earth's Moon rings like a ]]>
390
+ <bold>
391
+ <![CDATA[bell]]>
392
+ </bold>
393
+ <![CDATA[ when struck by meteroids. Distanced from the Earth by ]]>
394
+ <italic>
395
+ <![CDATA[384,400 km]]>
396
+ </italic>
397
+ <![CDATA[ ,its surface is covered in ]]>
398
+ <underline>
399
+ <![CDATA[craters]]>
400
+ </underline>
401
+ <![CDATA[ .Ain't that ]]>
402
+ <bold>
403
+ <![CDATA[cool]]>
404
+ </bold>
405
+ <![CDATA[ ? ]]>
406
+ </MixedContent>
407
+ <sup>
408
+ <![CDATA[1]]>
409
+ </sup>
410
+ <![CDATA[The Moon is not a planet.]]>
411
+ <sup>
412
+ <![CDATA[2]]>
413
+ </sup>
414
+ <![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]>
415
+ <sub>2</sub>
416
+ </RootMixedContentNested>
417
+ XML
418
+
419
+ it "deserializes and serializes mixed content correctly" do
420
+ parsed = CDATA::RootMixedContentNested.from_xml(xml)
421
+
422
+ expected_content = [
423
+ "The Earth's Moon rings like a ",
424
+ " when struck by meteroids. Distanced from the Earth by ",
425
+ " ,its surface is covered in ",
426
+ " .Ain't that ",
427
+ " ? ",
428
+ ]
429
+
430
+ expect(parsed.id).to eq("outer123")
431
+ expect(parsed.sup).to eq(["1", "2"])
432
+ expect(parsed.sub).to eq(["2"])
433
+ expect(parsed.content.id).to eq("inner456")
434
+ expect(parsed.content.bold).to eq(["bell", "cool"])
435
+ expect(parsed.content.italic).to eq(["384,400 km"])
436
+ expect(parsed.content.underline).to eq("craters")
437
+
438
+ parsed.content.content.each_with_index do |content, index|
439
+ expected_output = expected_content[index]
440
+
441
+ # due to the difference in capturing
442
+ # newlines in ox and nokogiri adapters
443
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
444
+ expected_xml = expected_ox_xml
445
+ end
446
+
447
+ expect(content).to eq(expected_output)
448
+ end
449
+
450
+ serialized = parsed.to_xml
451
+ expect(serialized).to eq(expected_xml)
452
+ end
453
+ end
454
+
455
+ context "when defualt: true is set for attributes default values" do
456
+ let(:xml) do
457
+ <<~XML
458
+ <DefaultValue>
459
+ <![CDATA[The following text is about the Moon]]>
460
+ <temperature>
461
+ <![CDATA[500]]>
462
+ </temperature>
463
+ <![CDATA[The Moon's atmosphere is mainly composed of helium in the form]]>
464
+ </DefaultValue>
465
+ XML
466
+ end
467
+
468
+ expected_xml = "<DefaultValue><name><![CDATA[Default Value]]></name><temperature><![CDATA[500]]></temperature><opacity>Opaque</opacity><![CDATA[The following text is about the MoonThe Moon's atmosphere is mainly composed of helium in the form]]></DefaultValue>"
469
+
470
+ expected_ox_xml = <<~XML
471
+ <DefaultValue>
472
+ <name>
473
+ <![CDATA[Default Value]]>
474
+ </name>
475
+ <temperature>
476
+ <![CDATA[500]]>
477
+ </temperature>
478
+ <opacity>Opaque</opacity>
479
+ <![CDATA[The following text is about the MoonThe Moon's atmosphere is mainly composed of helium in the form]]>
480
+ </DefaultValue>
481
+ XML
482
+
483
+ it "deserializes and serializes mixed content correctly" do
484
+ parsed = CDATA::DefaultValue.from_xml(xml)
485
+
486
+ expected_content = [
487
+ "The following text is about the Moon",
488
+ "The Moon's atmosphere is mainly composed of helium in the form",
489
+ ]
490
+
491
+ expect(parsed.name).to eq("Default Value")
492
+ expect(parsed.opacity).to eq("Opaque")
493
+ expect(parsed.temperature).to eq(500)
494
+
495
+ parsed.content.each_with_index do |content, index|
496
+ expected_output = expected_content[index]
497
+
498
+ # due to the difference in capturing
499
+ # newlines in ox and nokogiri adapters
500
+ if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter
501
+ expected_xml = expected_ox_xml
502
+ end
503
+
504
+ expect(content).to eq(expected_output)
505
+ end
506
+
507
+ serialized = parsed.to_xml
508
+ expect(serialized).to eq(expected_xml)
509
+ end
510
+ end
511
+ end
512
+
513
+ describe Lutaml::Model::XmlAdapter::NokogiriAdapter do
514
+ it_behaves_like "cdata behavior", described_class
515
+ end
516
+
517
+ describe Lutaml::Model::XmlAdapter::OxAdapter do
518
+ it_behaves_like "cdata behavior", described_class
519
+ end
520
+ end
@@ -148,11 +148,11 @@ RSpec.describe Delegation do
148
148
  end
149
149
 
150
150
  it "provides XML declaration with UTF-8 encoding" \
151
- "if encoding: true option provided" do
151
+ "if encoding: 'UTF-8' option provided" do
152
152
  xml_data = delegation.to_xml(
153
153
  pretty: true,
154
154
  declaration: true,
155
- encoding: true,
155
+ encoding: "UTF-8",
156
156
  )
157
157
  expect(xml_data).to include('<?xml version="1.0" encoding="UTF-8"?>')
158
158
  end