lutaml-model 0.3.25 → 0.3.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -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