lutaml-model 0.3.24 → 0.3.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +35 -16
  3. data/README.adoc +274 -28
  4. data/lib/lutaml/model/attribute.rb +18 -8
  5. data/lib/lutaml/model/error/type_error.rb +9 -0
  6. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  7. data/lib/lutaml/model/error/validation_error.rb +0 -1
  8. data/lib/lutaml/model/error.rb +2 -0
  9. data/lib/lutaml/model/serialize.rb +6 -1
  10. data/lib/lutaml/model/type/boolean.rb +38 -0
  11. data/lib/lutaml/model/type/date.rb +35 -0
  12. data/lib/lutaml/model/type/date_time.rb +32 -4
  13. data/lib/lutaml/model/type/decimal.rb +42 -0
  14. data/lib/lutaml/model/type/float.rb +37 -0
  15. data/lib/lutaml/model/type/hash.rb +62 -0
  16. data/lib/lutaml/model/type/integer.rb +41 -0
  17. data/lib/lutaml/model/type/string.rb +49 -0
  18. data/lib/lutaml/model/type/time.rb +49 -0
  19. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  20. data/lib/lutaml/model/type/value.rb +52 -0
  21. data/lib/lutaml/model/type.rb +50 -114
  22. data/lib/lutaml/model/version.rb +1 -1
  23. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +5 -2
  24. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +2 -1
  25. data/lib/lutaml/model/xml_adapter/xml_document.rb +0 -2
  26. data/lutaml-model.gemspec +1 -1
  27. data/spec/address_spec.rb +170 -0
  28. data/spec/fixtures/address.rb +33 -0
  29. data/spec/fixtures/person.rb +73 -0
  30. data/spec/fixtures/sample_model.rb +40 -0
  31. data/spec/fixtures/vase.rb +38 -0
  32. data/spec/fixtures/xml/special_char.xml +13 -0
  33. data/spec/lutaml/model/attribute_spec.rb +112 -0
  34. data/spec/lutaml/model/collection_spec.rb +299 -0
  35. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  36. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  37. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  38. data/spec/lutaml/model/defaults_spec.rb +221 -0
  39. data/spec/lutaml/model/delegation_spec.rb +340 -0
  40. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  41. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  42. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  43. data/spec/lutaml/model/map_content_spec.rb +118 -0
  44. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  45. data/spec/lutaml/model/namespace_spec.rb +57 -0
  46. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  47. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  48. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  49. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  50. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  51. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  52. data/spec/lutaml/model/serializable_spec.rb +297 -0
  53. data/spec/lutaml/model/serializable_validation_spec.rb +85 -0
  54. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  55. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  56. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  57. data/spec/lutaml/model/type/date_spec.rb +118 -0
  58. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  59. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  60. data/spec/lutaml/model/type/float_spec.rb +191 -0
  61. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  62. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  63. data/spec/lutaml/model/type/string_spec.rb +150 -0
  64. data/spec/lutaml/model/type/time_spec.rb +142 -0
  65. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  66. data/spec/lutaml/model/type_spec.rb +276 -0
  67. data/spec/lutaml/model/utils_spec.rb +79 -0
  68. data/spec/lutaml/model/validation_spec.rb +83 -0
  69. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  70. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  71. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  72. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  73. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  74. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  75. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  76. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  77. data/spec/lutaml/model_spec.rb +1 -0
  78. data/spec/person_spec.rb +161 -0
  79. data/spec/spec_helper.rb +33 -0
  80. metadata +66 -2
@@ -0,0 +1,299 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+
4
+ module CollectionTests
5
+ class Pot < Lutaml::Model::Serializable
6
+ attribute :material, Lutaml::Model::Type::String
7
+
8
+ xml do
9
+ root "pot"
10
+ map_element "material", to: :material
11
+ end
12
+ end
13
+
14
+ class Kiln < Lutaml::Model::Serializable
15
+ attribute :brand, Lutaml::Model::Type::String
16
+ attribute :pots, Pot, collection: 0..2
17
+ attribute :temperatures, Lutaml::Model::Type::Integer, collection: true
18
+ attribute :operators, Lutaml::Model::Type::String, collection: (1..),
19
+ default: -> {
20
+ ["Default Operator"]
21
+ }
22
+ attribute :sensors, Lutaml::Model::Type::String, collection: 1..3,
23
+ default: -> {
24
+ ["Default Sensor"]
25
+ }
26
+
27
+ xml do
28
+ root "kiln"
29
+ map_attribute "brand", to: :brand
30
+ map_element "pot", to: :pots
31
+ map_element "temperature", to: :temperatures
32
+ map_element "operator", to: :operators
33
+ map_element "sensor", to: :sensors
34
+ end
35
+ end
36
+
37
+ class Address < Lutaml::Model::Serializable
38
+ attribute :street, :string
39
+ attribute :city, :string
40
+ attribute :address, Address
41
+
42
+ xml do
43
+ root "address"
44
+ map_element "street", to: :street
45
+ map_element "city", with: { from: :city_from_xml, to: :city_to_xml }
46
+ map_element "address", to: :address
47
+ end
48
+
49
+ def city_from_xml(model, node)
50
+ model.city = node
51
+ end
52
+
53
+ def city_to_xml(model, parent, doc)
54
+ doc.add_element(parent, "<city>#{model.city}</city>")
55
+ end
56
+ end
57
+ end
58
+
59
+ RSpec.describe CollectionTests do
60
+ let(:pots) { [{ material: "clay" }, { material: "ceramic" }] }
61
+ let(:temperatures) { [1200, 1300, 1400] }
62
+ let(:operators) { ["John", "Jane"] }
63
+ let(:sensors) { ["Temp1", "Temp2"] }
64
+ let(:attributes) do
65
+ {
66
+ brand: "Skutt",
67
+ pots: pots,
68
+ temperatures: temperatures,
69
+ operators: operators,
70
+ sensors: sensors,
71
+ }
72
+ end
73
+ let(:model) { CollectionTests::Kiln.new(attributes) }
74
+
75
+ let(:model_xml) do
76
+ <<~XML
77
+ <kiln brand="Skutt">
78
+ <pot>
79
+ <material>clay</material>
80
+ </pot>
81
+ <pot>
82
+ <material>ceramic</material>
83
+ </pot>
84
+ <temperature>1200</temperature>
85
+ <temperature>1300</temperature>
86
+ <temperature>1400</temperature>
87
+ <operator>John</operator>
88
+ <operator>Jane</operator>
89
+ <sensor>Temp1</sensor>
90
+ <sensor>Temp2</sensor>
91
+ </kiln>
92
+ XML
93
+ end
94
+
95
+ it "initializes with default values" do
96
+ default_model = CollectionTests::Kiln.new
97
+ expect(default_model.brand).to be_nil
98
+ expect(default_model.pots).to eq([])
99
+ expect(default_model.temperatures).to eq([])
100
+ expect(default_model.operators).to eq(["Default Operator"])
101
+ expect(default_model.sensors).to eq(["Default Sensor"])
102
+ end
103
+
104
+ it "serializes to XML" do
105
+ expected_xml = model_xml.strip
106
+ expect(model.to_xml.strip).to eq(expected_xml)
107
+ end
108
+
109
+ it "deserializes from XML" do
110
+ sample = CollectionTests::Kiln.from_xml(model_xml)
111
+ expect(sample.brand).to eq("Skutt")
112
+ expect(sample.pots.size).to eq(2)
113
+ expect(sample.pots[0].material).to eq("clay")
114
+ expect(sample.pots[1].material).to eq("ceramic")
115
+ expect(sample.temperatures).to eq([1200, 1300, 1400])
116
+ expect(sample.operators).to eq(["John", "Jane"])
117
+ expect(sample.sensors).to eq(["Temp1", "Temp2"])
118
+ end
119
+
120
+ it "round-trips XML" do
121
+ xml = model.to_xml
122
+ new_model = CollectionTests::Kiln.from_xml(xml)
123
+ expect(new_model.brand).to eq(model.brand)
124
+ expect(new_model.pots.size).to eq(model.pots.size)
125
+ model.pots.each_with_index do |pot, index|
126
+ expect(new_model.pots[index].material).to eq(pot.material)
127
+ end
128
+ expect(new_model.temperatures).to eq(model.temperatures)
129
+ expect(new_model.operators).to eq(model.operators)
130
+ expect(new_model.sensors).to eq(model.sensors)
131
+ end
132
+
133
+ context "when model contains self as attribute" do
134
+ let(:xml) do
135
+ <<~XML
136
+ <address>
137
+ <street>A</street>
138
+ <city>B</city>
139
+ <address>
140
+ <street>C</street>
141
+ <city>D</city>
142
+ </address>
143
+ </address>
144
+ XML
145
+ end
146
+
147
+ it "deserializes from XML" do
148
+ model = CollectionTests::Address.from_xml(xml)
149
+
150
+ expect(model.street).to eq("A")
151
+ expect(model.city).to eq("B")
152
+ expect(model.address.street).to eq("C")
153
+ expect(model.address.city).to eq("D")
154
+ end
155
+
156
+ it "round-trips XML" do
157
+ model = CollectionTests::Address.from_xml(xml)
158
+
159
+ expect(model.to_xml).to be_equivalent_to(xml)
160
+ end
161
+ end
162
+
163
+ context "when collection counts are below given ranges" do
164
+ let(:invalid_attributes) do
165
+ attributes.merge(operators: [], sensors: [])
166
+ end
167
+
168
+ it "raises ValidationError containing CollectionCountOutOfRangeError for operators" do
169
+ kiln = CollectionTests::Kiln.new(invalid_attributes)
170
+ expect do
171
+ kiln.validate!
172
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
173
+ expect(error).to include(Lutaml::Model::CollectionCountOutOfRangeError)
174
+ expect(error.error_messages).to include(a_string_matching(/operators count is 0, must be at least 1/))
175
+ end
176
+ end
177
+
178
+ it "raises ValidationError containing CollectionCountOutOfRangeError for sensors" do
179
+ kiln = CollectionTests::Kiln.new(attributes.merge(sensors: []))
180
+ expect do
181
+ kiln.validate!
182
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
183
+ expect(error).to include(Lutaml::Model::CollectionCountOutOfRangeError)
184
+ expect(error.error_messages).to include(a_string_matching(/sensors count is 0, must be between 1 and 3/))
185
+ end
186
+ end
187
+ end
188
+
189
+ context "when collection counts are below given ranges" do
190
+ let(:invalid_attributes) do
191
+ attributes.merge(operators: [], sensors: [])
192
+ end
193
+
194
+ it "raises CollectionCountOutOfRangeError" do
195
+ kiln = CollectionTests::Kiln.new(invalid_attributes)
196
+ expect do
197
+ kiln.validate!
198
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
199
+ expect(error).to include(Lutaml::Model::CollectionCountOutOfRangeError)
200
+ expect(error.error_messages).to include(a_string_matching(/operators count is 0, must be at least 1/))
201
+ end
202
+ end
203
+ end
204
+
205
+ context "when collection with unbounded maximum exceeds minimum" do
206
+ let(:valid_attributes) do
207
+ attributes.merge(operators: ["John", "Jane", "Jim", "Jessica"])
208
+ end
209
+
210
+ it "creates the model without errors" do
211
+ expect { CollectionTests::Kiln.new(valid_attributes) }.not_to raise_error
212
+ end
213
+ end
214
+
215
+ context "when deserializing XML with invalid collection counts" do
216
+ let(:invalid_xml) do
217
+ <<~XML
218
+ <kiln brand="Skutt">
219
+ <pot>
220
+ <material>clay</material>
221
+ </pot>
222
+ <pot>
223
+ <material>ceramic</material>
224
+ </pot>
225
+ <pot>
226
+ <material>porcelain</material>
227
+ </pot>
228
+ <temperature>1200</temperature>
229
+ <operator>John</operator>
230
+ <sensor>Temp1</sensor>
231
+ <sensor>Temp2</sensor>
232
+ <sensor>Temp3</sensor>
233
+ <sensor>Temp4</sensor>
234
+ </kiln>
235
+ XML
236
+ end
237
+
238
+ it "raises ValidationError containing CollectionCountOutOfRangeError" do
239
+ expect do
240
+ CollectionTests::Kiln.from_xml(invalid_xml).validate!
241
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
242
+ expect(error).to include(Lutaml::Model::CollectionCountOutOfRangeError)
243
+ expect(error.error_messages).to include(a_string_matching(/pots count is 3, must be between 0 and 2/))
244
+ end
245
+ end
246
+ end
247
+
248
+ context "when specifying invalid collection ranges" do
249
+ it "raises an error for a range with only an upper bound" do
250
+ expect do
251
+ Class.new(Lutaml::Model::Serializable) do
252
+ attribute :invalid_range, Lutaml::Model::Type::String, collection: ..3
253
+ end
254
+ end.to raise_error(ArgumentError, /Invalid collection range/)
255
+ end
256
+
257
+ it "raises an error for a range where max is less than min" do
258
+ expect do
259
+ Class.new(Lutaml::Model::Serializable) do
260
+ attribute :invalid_range, Lutaml::Model::Type::String,
261
+ collection: 9..3
262
+ end
263
+ end.to raise_error(ArgumentError, /Invalid collection range/)
264
+ end
265
+
266
+ it "raises an error for a negative range" do
267
+ expect do
268
+ Class.new(Lutaml::Model::Serializable) do
269
+ attribute :invalid_range, Lutaml::Model::Type::String,
270
+ collection: -2..1
271
+ end
272
+ end.to raise_error(ArgumentError, /Invalid collection range/)
273
+ end
274
+
275
+ it "allows a range with only a lower bound" do
276
+ expect do
277
+ Class.new(Lutaml::Model::Serializable) do
278
+ attribute :valid_range, Lutaml::Model::Type::String, collection: 1..
279
+ end
280
+ end.not_to raise_error
281
+ end
282
+
283
+ it "allows a range with both lower and upper bounds" do
284
+ expect do
285
+ Class.new(Lutaml::Model::Serializable) do
286
+ attribute :valid_range, Lutaml::Model::Type::String, collection: 1..3
287
+ end
288
+ end.not_to raise_error
289
+ end
290
+
291
+ it "allows a range with zero as the lower bound" do
292
+ expect do
293
+ Class.new(Lutaml::Model::Serializable) do
294
+ attribute :valid_range, Lutaml::Model::Type::String, collection: 0..3
295
+ end
296
+ end.not_to raise_error
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,106 @@
1
+ require "spec_helper"
2
+
3
+ # Simple model with basic attributes
4
+ class ComparableGlaze < Lutaml::Model::Serializable
5
+ attribute :color, :string
6
+ attribute :temperature, :integer
7
+ attribute :food_safe, :boolean
8
+ end
9
+
10
+ # Model with a nested Serializable object
11
+ class ComparableCeramic < Lutaml::Model::Serializable
12
+ attribute :type, :string
13
+ attribute :glaze, ComparableGlaze
14
+ end
15
+
16
+ # Model with a deeply nested Serializable object
17
+ class ComparableCeramicCollection < Lutaml::Model::Serializable
18
+ attribute :name, :string
19
+ attribute :featured_piece, ComparableCeramic # This creates a two-level nesting
20
+ end
21
+
22
+ RSpec.describe Lutaml::Model::ComparableModel do
23
+ describe "comparisons" do
24
+ context "with simple types (Glaze)" do
25
+ it "compares equal objects with basic attributes" do
26
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
27
+ food_safe: true)
28
+ glaze2 = ComparableGlaze.new(color: "Blue", temperature: 1200,
29
+ food_safe: true)
30
+ expect(glaze1).to eq(glaze2)
31
+ expect(glaze1.hash).to eq(glaze2.hash)
32
+ end
33
+
34
+ it "compares unequal objects with basic attributes" do
35
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
36
+ food_safe: true)
37
+ glaze2 = ComparableGlaze.new(color: "Red", temperature: 1000,
38
+ food_safe: false)
39
+ expect(glaze1).not_to eq(glaze2)
40
+ expect(glaze1.hash).not_to eq(glaze2.hash)
41
+ end
42
+ end
43
+
44
+ context "with nested Serializable objects (Ceramic)" do
45
+ it "compares equal objects with one level of nesting" do
46
+ # Here, we're comparing Ceramic objects that contain Glaze objects
47
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
48
+ food_safe: true)
49
+ glaze2 = ComparableGlaze.new(color: "Blue", temperature: 1200,
50
+ food_safe: true)
51
+ ceramic1 = ComparableCeramic.new(type: "Bowl", glaze: glaze1)
52
+ ceramic2 = ComparableCeramic.new(type: "Bowl", glaze: glaze2)
53
+ expect(ceramic1).to eq(ceramic2)
54
+ expect(ceramic1.hash).to eq(ceramic2.hash)
55
+ end
56
+
57
+ it "compares unequal objects with one level of nesting" do
58
+ # Here, we're comparing Ceramic objects with different Glaze objects
59
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
60
+ food_safe: true)
61
+ glaze2 = ComparableGlaze.new(color: "Red", temperature: 1000,
62
+ food_safe: false)
63
+ ceramic1 = ComparableCeramic.new(type: "Bowl", glaze: glaze1)
64
+ ceramic2 = ComparableCeramic.new(type: "Plate", glaze: glaze2)
65
+ expect(ceramic1).not_to eq(ceramic2)
66
+ expect(ceramic1.hash).not_to eq(ceramic2.hash)
67
+ end
68
+ end
69
+
70
+ context "with deeply nested Serializable objects (CeramicCollection)" do
71
+ it "compares equal objects with two levels of nesting" do
72
+ # This test compares CeramicCollection objects that contain Ceramic objects,
73
+ # which in turn contain Glaze objects - a two-level deep nesting
74
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
75
+ food_safe: true)
76
+ glaze2 = ComparableGlaze.new(color: "Blue", temperature: 1200,
77
+ food_safe: true)
78
+ ceramic1 = ComparableCeramic.new(type: "Bowl", glaze: glaze1)
79
+ ceramic2 = ComparableCeramic.new(type: "Bowl", glaze: glaze2)
80
+ collection1 = ComparableCeramicCollection.new(name: "Blue Collection",
81
+ featured_piece: ceramic1)
82
+ collection2 = ComparableCeramicCollection.new(name: "Blue Collection",
83
+ featured_piece: ceramic2)
84
+ expect(collection1).to eq(collection2)
85
+ expect(collection1.hash).to eq(collection2.hash)
86
+ end
87
+
88
+ it "compares unequal objects with two levels of nesting" do
89
+ # This test compares CeramicCollection objects that are different at every level:
90
+ # the collection name, the ceramic type, and the glaze properties
91
+ glaze1 = ComparableGlaze.new(color: "Blue", temperature: 1200,
92
+ food_safe: true)
93
+ glaze2 = ComparableGlaze.new(color: "Red", temperature: 1000,
94
+ food_safe: false)
95
+ ceramic1 = ComparableCeramic.new(type: "Bowl", glaze: glaze1)
96
+ ceramic2 = ComparableCeramic.new(type: "Plate", glaze: glaze2)
97
+ collection1 = ComparableCeramicCollection.new(name: "Blue Collection",
98
+ featured_piece: ceramic1)
99
+ collection2 = ComparableCeramicCollection.new(name: "Red Collection",
100
+ featured_piece: ceramic2)
101
+ expect(collection1).not_to eq(collection2)
102
+ expect(collection1.hash).not_to eq(collection2.hash)
103
+ end
104
+ end
105
+ end
106
+ end