lutaml-model 0.3.25 → 0.3.26

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 923c122ff4459f02c1949dc88ee8df94b93db6ce23c00c9da2159f6b376da3a0
4
- data.tar.gz: aa537363d67a5383cdbd280cfa9ddee0a3c5de0cb151b79b2e195a030bc0061d
3
+ metadata.gz: 1dc709174fab4f4df2214f08b536e0b1aded39f0c9f48a18eb5a4a53da3fc4b6
4
+ data.tar.gz: 9eea83160b8210f84d4b2f9baf2ab577ca91d2a737eaf98b175d52385e6fb5d3
5
5
  SHA512:
6
- metadata.gz: a0da4f4a87c8fe4a3951d302df5797594675f83616ea52b4582f417683d4f55076c57ad3d6f3018119c0a6021497bf6832b150ebf695d1fa8d4304da2040b641
7
- data.tar.gz: 0d4d6d783728b53d453c23e2cfcfcd08d9eac83d94b4652d3bdee95c19d9d9bf68e233dea6d29d9478d4214271dd800be93b8f8e11a59cf9f9cd46d744dd2002
6
+ metadata.gz: 1e75326adeb9b8804f1bcc99104707edc3656e870d16c8af22e07e32ead9f4d78cd87eb372ebb834f2a5d942d58819e0fd3800e23a8099cf951360765223cd25
7
+ data.tar.gz: ecc145fb2d9250cf855243c7b9cf468919f2530eed03039bd6c7a521fa534327a1a67c5348237b3d5959e20fa0fc2b2cadc87e57139f451610cd2d7d695f251b
data/.rubocop_todo.yml CHANGED
@@ -73,7 +73,7 @@ Metrics/MethodLength:
73
73
  # Offense count: 7
74
74
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
75
75
  Metrics/ParameterLists:
76
- Max: 13
76
+ Max: 14
77
77
 
78
78
  # Offense count: 23
79
79
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
data/README.adoc CHANGED
@@ -543,8 +543,15 @@ end
543
543
  ====
544
544
 
545
545
 
546
+ === Attribute value validation
547
+
548
+ ==== General
549
+
550
+ There are several mechanisms to validate attribute values in Lutaml::Model.
551
+
552
+
546
553
  [[attribute-enumeration]]
547
- === Attribute as an enumeration
554
+ ==== Values of an enumeration
548
555
 
549
556
  An attribute can be defined as an enumeration by using the `values` directive.
550
557
 
@@ -641,6 +648,44 @@ acceptance of the newly updated component.
641
648
  ====
642
649
 
643
650
 
651
+ ==== String values restricted to patterns
652
+
653
+ An attribute that accepts a string value accepts value validation using regular
654
+ expressions.
655
+
656
+ Syntax:
657
+
658
+ [source,ruby]
659
+ ----
660
+ attribute :name_of_attribute, :string, pattern: /regex/
661
+ ----
662
+
663
+ .Using the `pattern` option to restrict the value of an attribute
664
+ [example]
665
+ ====
666
+ In this example, the `color` attribute takes hex color values such as `#ccddee`.
667
+
668
+ A regular expression can be used to validate values assigned to the attribute.
669
+ In this case, it is `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/`.
670
+
671
+ [source,ruby]
672
+ ----
673
+ class Glaze < Lutaml::Model::Serializable
674
+ attribute :color, :string, pattern: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
675
+ end
676
+ ----
677
+
678
+ [source,ruby]
679
+ ----
680
+ > Glaze.new(color: '#ff0000').color
681
+ > # "#ff0000"
682
+ > Glaze.new(color: '#ff000').color
683
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'color'
684
+ ----
685
+ ====
686
+
687
+
688
+
644
689
  === Attribute value default and rendering defaults
645
690
 
646
691
  Specify default values for attributes using the `default` option.
@@ -771,7 +816,8 @@ class Person < Lutaml::Model::Serializable
771
816
  end
772
817
  ----
773
818
 
774
- For the following xml
819
+ For the following XML snippet:
820
+
775
821
  [source,xml]
776
822
  ----
777
823
  <Person>
@@ -1144,6 +1190,80 @@ end
1144
1190
  ====
1145
1191
 
1146
1192
 
1193
+ ==== CDATA nodes
1194
+
1195
+ CDATA is an XML feature that allows the inclusion of text that may contain
1196
+ characters that are unescaped in XML.
1197
+
1198
+ While CDATA is not preferred in XML, it is sometimes necessary to handle CDATA
1199
+ nodes for both input and output.
1200
+
1201
+ NOTE: The W3C XML Recommendation explicitly encourages escaping characters over
1202
+ usage of CDATA.
1203
+
1204
+ Lutaml::Model supports the handling of CDATA nodes in XML in the following
1205
+ behavior:
1206
+
1207
+ . When an attribute contains a CDATA node with no text:
1208
+ ** On reading: The node (CDATA or text) is read as its value.
1209
+ ** On writing: The value is written as its native type.
1210
+
1211
+ . When an XML mapping sets `cdata: true` on `map_element` or `map_content`:
1212
+ ** On reading: The node (CDATA or text) is read as its value.
1213
+ ** On writing: The value is written as a CDATA node.
1214
+
1215
+ . When an XML mapping sets `cdata: false` on `map_element` or `map_content`:
1216
+ ** On reading: The node (CDATA or text) is read as its value.
1217
+ ** On writing: The value is written as a text node (string).
1218
+
1219
+
1220
+ Syntax:
1221
+
1222
+ [source,ruby]
1223
+ ----
1224
+ xml do
1225
+ map_content to: :name_of_attribute, cdata: (true | false)
1226
+ map_element :name, to: :name, cdata: (true | false)
1227
+ end
1228
+ ----
1229
+
1230
+ .Using `cdata` to map CDATA content
1231
+ [example]
1232
+ ====
1233
+ The following class will parse the XML snippet below:
1234
+
1235
+ [source,ruby]
1236
+ ----
1237
+ class Example < Lutaml::Model::Serializable
1238
+ attribute :name, :string
1239
+ attribute :description, :string
1240
+ attribute :title, :string
1241
+ attribute :note, :string
1242
+
1243
+ xml do
1244
+ root 'example'
1245
+ map_element :name, to: :name, cdata: true
1246
+ map_content to: :description, cdata: true
1247
+ map_element :title, to: :title, cdata: false
1248
+ map_element :note, to: :note, cdata: false
1249
+ end
1250
+ end
1251
+ ----
1252
+
1253
+ [source,xml]
1254
+ ----
1255
+ <example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title><![CDATA[Lutaml]]></title><note>Careful</note></example>
1256
+ ----
1257
+
1258
+ [source,ruby]
1259
+ ----
1260
+ > Example.from_xml(xml)
1261
+ > #<Example:0x0000000104ac7240 @name="John" @description="here is the description" @title="Lutaml" @note="Careful">
1262
+ > Example.new(name: "John", description: "here is the description", title: "Lutaml", note: "Careful").to_xml
1263
+ > #<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title>Lutaml</title><note>Careful</note></example>
1264
+ ----
1265
+ ====
1266
+
1147
1267
 
1148
1268
  ==== Example for mapping
1149
1269
 
@@ -9,6 +9,7 @@ module Lutaml
9
9
  delegate
10
10
  collection
11
11
  values
12
+ pattern
12
13
  ].freeze
13
14
 
14
15
  def initialize(name, type, options = {})
@@ -87,23 +88,12 @@ module Lutaml
87
88
  cast_value(value)
88
89
  end
89
90
 
90
- def enum_values
91
- @options.key?(:values) ? @options[:values] : []
91
+ def pattern
92
+ options[:pattern]
92
93
  end
93
94
 
94
- # Check if the value to be assigned is valid for the attribute
95
- #
96
- # Currently there are 2 validations
97
- # 1. Value should be from the values list if they are defined
98
- # e.g values: ["foo", "bar"] is set then any other value for this
99
- # attribute will raise `Lutaml::Model::InvalidValueError`
100
- #
101
- # 2. Value count should be between the collection range if defined
102
- # e.g if collection: 0..5 is set then the value greater then 5
103
- # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
104
- def validate_value!(value)
105
- valid_value!(value)
106
- valid_collection!(value)
95
+ def enum_values
96
+ @options.key?(:values) ? @options[:values] : []
107
97
  end
108
98
 
109
99
  def valid_value!(value)
@@ -123,14 +113,32 @@ module Lutaml
123
113
  options[:values].include?(value)
124
114
  end
125
115
 
126
- def validate_value!(value)
127
- # return true if none of the validations are present
128
- return true if enum_values.empty? && singular?
116
+ def valid_pattern!(value)
117
+ return true unless type == Lutaml::Model::Type::String
118
+ return true unless pattern
119
+
120
+ unless pattern.match?(value)
121
+ raise Lutaml::Model::PatternNotMatchedError.new(name, pattern, value)
122
+ end
129
123
 
124
+ true
125
+ end
126
+
127
+ # Check if the value to be assigned is valid for the attribute
128
+ #
129
+ # Currently there are 2 validations
130
+ # 1. Value should be from the values list if they are defined
131
+ # e.g values: ["foo", "bar"] is set then any other value for this
132
+ # attribute will raise `Lutaml::Model::InvalidValueError`
133
+ #
134
+ # 2. Value count should be between the collection range if defined
135
+ # e.g if collection: 0..5 is set then the value greater then 5
136
+ # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
137
+ def validate_value!(value)
130
138
  # Use the default value if the value is nil
131
139
  value = default if value.nil?
132
140
 
133
- valid_value!(value) && valid_collection!(value)
141
+ valid_value!(value) && valid_collection!(value) && valid_pattern!(value)
134
142
  end
135
143
 
136
144
  def validate_collection_range
@@ -229,6 +237,13 @@ module Lutaml
229
237
  raise StandardError,
230
238
  "Invalid options given for `#{name}` #{invalid_opts}"
231
239
  end
240
+
241
+ if options.key?(:pattern) && type != Lutaml::Model::Type::String
242
+ raise StandardError,
243
+ "Invalid option `pattern` given for `#{name}`, `pattern` is only allowed for :string type"
244
+ end
245
+
246
+ true
232
247
  end
233
248
 
234
249
  def validate_type!(type)
@@ -0,0 +1,17 @@
1
+ module Lutaml
2
+ module Model
3
+ class PatternNotMatchedError < Error
4
+ def initialize(attr_name, pattern, value)
5
+ @attr_name = attr_name
6
+ @pattern = pattern
7
+ @value = value
8
+
9
+ super()
10
+ end
11
+
12
+ def to_s
13
+ "#{@attr_name}: \"#{@value}\" does not match #{@pattern.inspect}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -7,6 +7,7 @@ end
7
7
 
8
8
  require_relative "error/invalid_value_error"
9
9
  require_relative "error/incorrect_mapping_argument_error"
10
+ require_relative "error/pattern_not_matched_error"
10
11
  require_relative "error/unknown_adapter_type_error"
11
12
  require_relative "error/collection_count_out_of_range_error"
12
13
  require_relative "error/validation_error"
@@ -28,6 +28,14 @@ module Lutaml
28
28
  @item_order = order
29
29
  end
30
30
 
31
+ def text
32
+ self["#cdata-section"] || self["text"]
33
+ end
34
+
35
+ def text?
36
+ key?("#cdata-section") || key?("text")
37
+ end
38
+
31
39
  def ordered?
32
40
  @ordered
33
41
  end
@@ -343,7 +343,7 @@ module Lutaml
343
343
  value = if rule.raw_mapping?
344
344
  doc.node.inner_xml
345
345
  elsif rule.content_mapping?
346
- doc["text"]
346
+ doc[rule.content_key]
347
347
  elsif doc.key_exist?(rule.namespaced_name(options[:default_namespace]))
348
348
  doc.fetch(rule.namespaced_name(options[:default_namespace]))
349
349
  else
@@ -399,12 +399,12 @@ module Lutaml
399
399
 
400
400
  value = if value.is_a?(Array)
401
401
  value.map do |v|
402
- text_hash?(attr, v) ? v["text"] : v
402
+ text_hash?(attr, v) ? v.text : v
403
403
  end
404
404
  elsif attr&.raw? && value
405
405
  value.node.children.map(&:to_xml).join
406
406
  elsif text_hash?(attr, value)
407
- value["text"]
407
+ value.text
408
408
  else
409
409
  value
410
410
  end
@@ -428,7 +428,7 @@ module Lutaml
428
428
 
429
429
  def text_hash?(attr, value)
430
430
  return false unless value.is_a?(Hash)
431
- return value.keys == ["text"] unless attr
431
+ return value.one? && value.text? unless attr
432
432
 
433
433
  !(attr.type <= Serialize) && attr.type != Lutaml::Model::Type::Hash
434
434
  end
@@ -8,7 +8,8 @@ module Lutaml
8
8
  begin
9
9
  attr.validate_value!(value)
10
10
  rescue Lutaml::Model::InvalidValueError,
11
- Lutaml::Model::CollectionCountOutOfRangeError => e
11
+ Lutaml::Model::CollectionCountOutOfRangeError,
12
+ PatternNotMatchedError => e
12
13
  errors << e
13
14
  end
14
15
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.3.25"
5
+ VERSION = "0.3.26"
6
6
  end
7
7
  end
@@ -51,7 +51,9 @@ module Lutaml
51
51
  end
52
52
  end
53
53
 
54
- def add_text(element, text)
54
+ def add_text(element, text, cdata: false)
55
+ return add_cdata(element, text) if cdata
56
+
55
57
  if element.is_a?(self.class)
56
58
  element = element.xml.parent
57
59
  end
@@ -60,6 +62,10 @@ module Lutaml
60
62
  element.add_child(text_node)
61
63
  end
62
64
 
65
+ def add_cdata(element, value)
66
+ element.cdata(value)
67
+ end
68
+
63
69
  def add_namespace_prefix(prefix)
64
70
  xml[prefix] if prefix
65
71
 
@@ -66,10 +66,16 @@ module Lutaml
66
66
  xml.text(text)
67
67
  end
68
68
 
69
- def add_text(element, text)
69
+ def add_text(element, text, cdata: false)
70
+ return element.cdata(text) if cdata
71
+
70
72
  element << text
71
73
  end
72
74
 
75
+ def add_cdata(element, value)
76
+ element.cdata(value)
77
+ end
78
+
73
79
  # Add XML namespace to document
74
80
  #
75
81
  # Ox doesn't support XML namespaces so we only save the
@@ -74,10 +74,12 @@ module Lutaml
74
74
  value = attribute_value_for(element, element_rule)
75
75
 
76
76
  if element_rule == xml_mapping.content_mapping
77
+ next if element_rule.cdata && name == "text"
78
+
77
79
  text = xml_mapping.content_mapping.serialize(element)
78
80
  text = text[curr_index] if text.is_a?(Array)
79
81
 
80
- next prefixed_xml.text(text) if element.mixed?
82
+ next prefixed_xml.add_text(xml, text, cdata: element_rule.cdata) if element.mixed?
81
83
 
82
84
  content << text
83
85
  elsif !value.nil? || element_rule.render_nil?
@@ -56,15 +56,15 @@ module Lutaml
56
56
  mapper_class: mapper_class)
57
57
  value = attribute_value_for(element, element_rule)
58
58
 
59
+ next if element_rule == xml_mapping.content_mapping && element_rule.cdata && name == "text"
60
+
59
61
  if element_rule == xml_mapping.content_mapping
60
62
  text = element.send(xml_mapping.content_mapping.to)
61
63
  text = text[curr_index] if text.is_a?(Array)
62
64
 
63
- if element.mixed?
64
- el.add_text(el, text)
65
- else
66
- content << text
67
- end
65
+ next el.add_text(el, text, cdata: element_rule.cdata) if element.mixed?
66
+
67
+ content << text
68
68
  elsif !value.nil? || element_rule.render_nil?
69
69
  value = value[curr_index] if attribute_def.collection?
70
70
 
@@ -88,10 +88,13 @@ module Lutaml
88
88
 
89
89
  class OxElement < XmlElement
90
90
  def initialize(node, root_node: nil)
91
- if node.is_a?(String)
91
+ case node
92
+ when String
92
93
  super("text", {}, [], node, parent_document: root_node)
93
- elsif node.is_a?(Ox::Comment)
94
+ when Ox::Comment
94
95
  super("comment", {}, [], node.value, parent_document: root_node)
96
+ when Ox::CData
97
+ super("#cdata-section", {}, [], node.value, parent_document: root_node)
95
98
  else
96
99
  namespace_attributes(node.attributes).each do |(name, value)|
97
100
  if root_node
@@ -152,16 +152,16 @@ module Lutaml
152
152
  )
153
153
  elsif rule.prefix_set?
154
154
  xml.create_and_add_element(rule.name, prefix: prefix) do
155
- add_value(xml, value, attribute)
155
+ add_value(xml, value, attribute, cdata: rule.cdata)
156
156
  end
157
157
  else
158
158
  xml.create_and_add_element(rule.name) do
159
- add_value(xml, value, attribute)
159
+ add_value(xml, value, attribute, cdata: rule.cdata)
160
160
  end
161
161
  end
162
162
  end
163
163
 
164
- def add_value(xml, value, attribute)
164
+ def add_value(xml, value, attribute, cdata: false)
165
165
  if !value.nil?
166
166
  serialized_value = attribute.type.serialize(value)
167
167
 
@@ -172,7 +172,7 @@ module Lutaml
172
172
  end
173
173
  end
174
174
  else
175
- xml.add_text(xml, serialized_value)
175
+ xml.add_text(xml, serialized_value, cdata: cdata)
176
176
  end
177
177
  end
178
178
  end
@@ -244,7 +244,7 @@ module Lutaml
244
244
  value = attribute_value_for(element, rule)
245
245
  return unless render_element?(rule, element, value)
246
246
 
247
- xml.add_text(xml, value)
247
+ xml.add_text(xml, value, cdata: rule.cdata)
248
248
  end
249
249
 
250
250
  def process_content_mapping(element, content_rule, xml)
@@ -261,7 +261,7 @@ module Lutaml
261
261
  text = content_rule.serialize(element)
262
262
  text = text.join if text.is_a?(Array)
263
263
 
264
- xml.add_text(xml, text)
264
+ xml.add_text(xml, text, cdata: content_rule.cdata)
265
265
  end
266
266
  end
267
267
 
@@ -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
@@ -2,6 +2,7 @@ require "spec_helper"
2
2
 
3
3
  class TestSerializable < Lutaml::Model::Serializable
4
4
  attribute :name, :string, values: ["Alice", "Bob", "Charlie"]
5
+ attribute :email, :string, pattern: /.*?\S+@.+\.\S+/
5
6
  attribute :age, :integer, collection: 1..3
6
7
 
7
8
  xml do
@@ -27,9 +28,12 @@ class TestSerializable < Lutaml::Model::Serializable
27
28
  end
28
29
 
29
30
  RSpec.describe Lutaml::Model::Serializable do
30
- let(:valid_instance) { TestSerializable.new(name: "Alice", age: [30]) }
31
+ let(:valid_instance) do
32
+ TestSerializable.new(name: "Alice", age: [30], email: "alice@gmail.com")
33
+ end
34
+
31
35
  let(:invalid_instance) do
32
- TestSerializable.new(name: "David", age: [25, 30, 35, 40])
36
+ TestSerializable.new(name: "David", age: [25, 30, 35, 40], email: "david@gmail")
33
37
  end
34
38
 
35
39
  describe "serialization methods" do
@@ -66,8 +70,9 @@ RSpec.describe Lutaml::Model::Serializable do
66
70
  it "returns errors for invalid attributes" do
67
71
  errors = invalid_instance.validate
68
72
  expect(errors).not_to be_empty
69
- expect(errors.first).to be_a(Lutaml::Model::InvalidValueError)
70
- expect(errors.last).to be_a(Lutaml::Model::CollectionCountOutOfRangeError)
73
+ expect(errors[0]).to be_a(Lutaml::Model::InvalidValueError)
74
+ expect(errors[1]).to be_a(Lutaml::Model::PatternNotMatchedError)
75
+ expect(errors[2]).to be_a(Lutaml::Model::CollectionCountOutOfRangeError)
71
76
  end
72
77
  end
73
78
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.25
4
+ version: 0.3.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -59,6 +59,7 @@ files:
59
59
  - lib/lutaml/model/error/collection_count_out_of_range_error.rb
60
60
  - lib/lutaml/model/error/incorrect_mapping_argument_error.rb
61
61
  - lib/lutaml/model/error/invalid_value_error.rb
62
+ - lib/lutaml/model/error/pattern_not_matched_error.rb
62
63
  - lib/lutaml/model/error/type_error.rb
63
64
  - lib/lutaml/model/error/type_not_enabled_error.rb
64
65
  - lib/lutaml/model/error/unknown_adapter_type_error.rb
@@ -127,6 +128,7 @@ files:
127
128
  - spec/fixtures/vase.rb
128
129
  - spec/fixtures/xml/special_char.xml
129
130
  - spec/lutaml/model/attribute_spec.rb
131
+ - spec/lutaml/model/cdata_spec.rb
130
132
  - spec/lutaml/model/collection_spec.rb
131
133
  - spec/lutaml/model/comparable_model_spec.rb
132
134
  - spec/lutaml/model/custom_model_spec.rb