lutaml-model 0.3.23 → 0.3.25

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.
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 +7 -2
  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,276 @@
1
+ # spec/lutaml/model/type_spec.rb
2
+ require "spec_helper"
3
+ require "bigdecimal"
4
+
5
+ # Test classes for type registration scenarios
6
+ class CustomType < Lutaml::Model::Type::Value
7
+ def self.cast(value)
8
+ value.to_s.upcase
9
+ end
10
+ end
11
+
12
+ class InvalidType
13
+ def self.cast(value)
14
+ value
15
+ end
16
+ end
17
+
18
+ RSpec.describe Lutaml::Model::Type do
19
+ describe "Type System" do
20
+ describe ".register and .lookup" do
21
+ context "with valid types" do
22
+ it "registers and looks up a custom type" do
23
+ described_class.register(:custom, CustomType)
24
+ expect(described_class.lookup(:custom)).to eq(CustomType)
25
+ end
26
+
27
+ it "allows overriding an existing type registration" do
28
+ described_class.register(:custom, CustomType)
29
+ replacement_type = Class.new(Lutaml::Model::Type::Value)
30
+ described_class.register(:custom, replacement_type)
31
+ expect(described_class.lookup(:custom)).to eq(replacement_type)
32
+ end
33
+ end
34
+
35
+ context "with invalid types" do
36
+ it "raises TypeError when registering non-Type::Value class" do
37
+ expect do
38
+ described_class.register(:invalid,
39
+ InvalidType)
40
+ end.to raise_error(Lutaml::Model::TypeError,
41
+ /not a valid Lutaml::Model::Type::Value/)
42
+ end
43
+
44
+ it "raises UnknownTypeError when looking up unregistered type" do
45
+ expect do
46
+ described_class.lookup(:nonexistent)
47
+ end.to raise_error(
48
+ Lutaml::Model::UnknownTypeError, /Unknown type 'nonexistent'/
49
+ )
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "Built-in Types" do
55
+ before do
56
+ described_class.register_builtin_types
57
+ end
58
+
59
+ after do
60
+ described_class.instance_variable_set(:@registry, nil)
61
+ end
62
+
63
+ let(:built_in_types) do
64
+ {
65
+ string: Lutaml::Model::Type::String,
66
+ integer: Lutaml::Model::Type::Integer,
67
+ float: Lutaml::Model::Type::Float,
68
+ date: Lutaml::Model::Type::Date,
69
+ time: Lutaml::Model::Type::Time,
70
+ date_time: Lutaml::Model::Type::DateTime,
71
+ time_without_date: Lutaml::Model::Type::TimeWithoutDate,
72
+ boolean: Lutaml::Model::Type::Boolean,
73
+ hash: Lutaml::Model::Type::Hash,
74
+ }
75
+ end
76
+
77
+ it "has all built-in types registered" do
78
+ built_in_types.each do |type_name, type_class|
79
+ puts described_class
80
+ expect(described_class.lookup(type_name)).to eq(type_class)
81
+ end
82
+ end
83
+
84
+ describe "Type Casting" do
85
+ let(:test_date) { Date.new(2024, 1, 1) }
86
+ let(:test_time) { Time.new(2024, 1, 1, 12, 0, 0) }
87
+ let(:test_date_time) { DateTime.new(2024, 1, 1, 12, 0, 0) }
88
+
89
+ {
90
+ Lutaml::Model::Type::String => { input: 123, expected: "123" },
91
+ Lutaml::Model::Type::Integer => { input: "123", expected: 123 },
92
+ Lutaml::Model::Type::Float => { input: "123.45", expected: 123.45 },
93
+ Lutaml::Model::Type::Date => { input: "2024-01-01",
94
+ expected: Date.new(2024, 1, 1) },
95
+ Lutaml::Model::Type::Time => { input: "2024-01-01T12:00:00",
96
+ expected_hour: 12 },
97
+ Lutaml::Model::Type::DateTime => { input: "2024-01-01T12:00:00",
98
+ expected: DateTime.new(2024, 1, 1,
99
+ 12, 0, 0) },
100
+ Lutaml::Model::Type::Boolean => { input: "true", expected: true },
101
+ Lutaml::Model::Type::Hash => { input: { key: "value" },
102
+ expected: { key: "value" } },
103
+ }.each do |type_class, test_data|
104
+ it "correctly casts #{type_class}" do
105
+ result = type_class.cast(test_data[:input])
106
+ if test_data.key?(:expected_hour)
107
+ expect(result.hour).to eq(test_data[:expected_hour])
108
+ else
109
+ expect(result).to eq(test_data[:expected])
110
+ end
111
+ end
112
+ end
113
+
114
+ it "handles nil values gracefully" do
115
+ built_in_types.each_value do |type_class|
116
+ expect(type_class.cast(nil)).to be_nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "Decimal Type" do
123
+ context "when BigDecimal is available" do
124
+ before do
125
+ require "bigdecimal"
126
+ described_class.register_builtin_types
127
+ end
128
+
129
+ it "registers and uses Decimal type" do
130
+ expect(described_class.lookup(:decimal)).to eq(Lutaml::Model::Type::Decimal)
131
+ expect(Lutaml::Model::Type::Decimal.cast("123.45")).to eq(BigDecimal("123.45"))
132
+ end
133
+
134
+ it "serializes decimal values correctly" do
135
+ value = BigDecimal("123.45")
136
+ expect(Lutaml::Model::Type::Decimal.serialize(value)).to eq("123.45")
137
+ end
138
+ end
139
+
140
+ context "when BigDecimal is not available" do
141
+ before do
142
+ Object.send(:remove_const, :BigDecimal) if defined?(BigDecimal)
143
+ $LOADED_FEATURES.delete_if { |path| path.include?("bigdecimal") }
144
+ end
145
+
146
+ after do
147
+ require "bigdecimal"
148
+ end
149
+
150
+ let(:decimal_class) { described_class.lookup(:decimal) }
151
+
152
+ it "raises TypeNotEnabledError when using Decimal type" do
153
+ expect do
154
+ described_class.lookup(:decimal).cast("123.45")
155
+ end.to raise_error(Lutaml::Model::TypeNotEnabledError)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "Type Usage in Models" do
162
+ class TypeTestModel < Lutaml::Model::Serializable
163
+ attribute :string_symbol, :string
164
+ attribute :string_class, Lutaml::Model::Type::String
165
+ attribute :integer_value, :integer
166
+ attribute :float_value, :float
167
+ attribute :date_value, :date
168
+ attribute :time_value, :time
169
+ attribute :time_without_date_value, :time_without_date
170
+ attribute :date_time_value, :date_time
171
+ attribute :boolean_value, :boolean
172
+ attribute :hash_value, :hash
173
+
174
+ xml do
175
+ root "test"
176
+ map_element "string_symbol", to: :string_symbol
177
+ map_element "string_class", to: :string_class
178
+ map_element "integer", to: :integer_value
179
+ map_element "float", to: :float_value
180
+ map_element "date", to: :date_value
181
+ map_element "time", to: :time_value
182
+ map_element "time_without_date", to: :time_without_date_value
183
+ map_element "date_time", to: :date_time_value
184
+ map_element "boolean", to: :boolean_value
185
+ map_element "hash", to: :hash_value
186
+ end
187
+ end
188
+
189
+ let(:test_instance) do
190
+ TypeTestModel.new(
191
+ string_symbol: "test",
192
+ string_class: "test",
193
+ integer_value: "123",
194
+ float_value: "123.45",
195
+ date_value: "2024-01-01",
196
+ time_value: "12:00:00",
197
+ time_without_date_value: "10:06:15",
198
+ date_time_value: "2024-01-01T12:00:00",
199
+ boolean_value: "true",
200
+ hash_value: { key: "value" },
201
+ )
202
+ end
203
+
204
+ describe "Type Casting in Models" do
205
+ it "correctly casts values using both symbol and class-based types" do
206
+ expect(test_instance.string_symbol).to eq("test")
207
+ expect(test_instance.string_class).to eq("test")
208
+ expect(test_instance.integer_value).to eq(123)
209
+ expect(test_instance.float_value).to eq(123.45)
210
+ expect(test_instance.date_value).to eq(Date.new(2024, 1, 1))
211
+ expect(test_instance.time_value.hour).to eq(12)
212
+ expect(test_instance.date_time_value).to eq(DateTime.new(2024, 1, 1,
213
+ 12, 0, 0))
214
+ expect(test_instance.boolean_value).to be(true)
215
+ expect(test_instance.hash_value).to eq({ key: "value" })
216
+ end
217
+
218
+ it "produces identical results with symbol and class-based definitions" do
219
+ expect(test_instance.string_symbol).to eq(test_instance.string_class)
220
+ end
221
+ end
222
+
223
+ describe "Serialization" do
224
+ it "correctly serializes to XML" do
225
+ expected_xml = <<~XML
226
+ <test>
227
+ <string_symbol>test</string_symbol>
228
+ <string_class>test</string_class>
229
+ <integer>123</integer>
230
+ <float>123.45</float>
231
+ <date>2024-01-01</date>
232
+ <time>#{Time.parse('12:00:00').iso8601}</time>
233
+ <time_without_date>10:06:15</time_without_date>
234
+ <date_time>2024-01-01T12:00:00+00:00</date_time>
235
+ <boolean>true</boolean>
236
+ <hash>
237
+ <key>value</key>
238
+ </hash>
239
+ </test>
240
+ XML
241
+
242
+ expect(test_instance.to_xml).to be_equivalent_to(expected_xml)
243
+ end
244
+
245
+ it "correctly deserializes from XML" do
246
+ xml = <<~XML
247
+ <test>
248
+ <string_symbol>test</string_symbol>
249
+ <string_class>test</string_class>
250
+ <integer>123</integer>
251
+ <float>123.45</float>
252
+ <date>2024-01-01</date>
253
+ <time>12:00:00</time>
254
+ <time_without_date>10:06:15</time_without_date>
255
+ <date_time>2024-01-01T12:00:00</date_time>
256
+ <boolean>true</boolean>
257
+ <hash>
258
+ <key>value</key>
259
+ </hash>
260
+ </test>
261
+ XML
262
+
263
+ deserialized = TypeTestModel.from_xml(xml)
264
+ expect(deserialized.string_symbol).to eq("test")
265
+ expect(deserialized.string_class).to eq("test")
266
+ expect(deserialized.integer_value).to eq(123)
267
+ expect(deserialized.float_value).to eq(123.45)
268
+ expect(deserialized.date_value).to eq(Date.new(2024, 1, 1))
269
+ expect(deserialized.date_time_value).to eq(DateTime.new(2024, 1, 1, 12,
270
+ 0, 0))
271
+ expect(deserialized.boolean_value).to be(true)
272
+ expect(deserialized.hash_value).to eq({ "key" => "value" })
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+ require "lutaml/model/utils"
3
+
4
+ RSpec.describe Lutaml::Model::Utils do
5
+ let(:utils) { described_class }
6
+
7
+ shared_examples "string conversion" do |method, examples|
8
+ describe ".#{method}" do
9
+ examples.each do |input, expected_output|
10
+ context "when input is #{input.nil? ? 'nil' : "'#{input}'"}" do
11
+ it "returns '#{expected_output}'" do
12
+ expect(utils.send(method, input)).to eq(expected_output)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ camel_case_examples = {
20
+ "hello_world" => "HelloWorld",
21
+ "foo_bar_baz" => "FooBarBaz",
22
+ "" => "",
23
+ nil => "",
24
+ "hello_world/foo_bar_baz" => "HelloWorld::FooBarBaz",
25
+ }
26
+
27
+ classify_examples_extra = {
28
+ "hello_world::foo_bar_baz" => "HelloWorld::FooBarBaz",
29
+ }
30
+ classify_examples = camel_case_examples.merge(classify_examples_extra)
31
+
32
+ snake_case_examples = {
33
+ "HelloWorld" => "hello_world",
34
+ "FooBarBaz" => "foo_bar_baz",
35
+ "" => "",
36
+ nil => "",
37
+ "HelloWorld::FooBarBaz" => "hello_world/foo_bar_baz",
38
+ }
39
+
40
+ include_examples "string conversion", :camel_case, camel_case_examples
41
+ include_examples "string conversion", :classify, classify_examples
42
+ include_examples "string conversion", :snake_case, snake_case_examples
43
+
44
+ describe ".deep_dup" do
45
+ let(:original_hash) do
46
+ {
47
+ one: {
48
+ one_one: {
49
+ one_one1: "one",
50
+ one_one2: :two,
51
+ },
52
+ one_two: "12",
53
+ },
54
+ }
55
+ end
56
+
57
+ let(:duplicate_hash) { utils.deep_dup(original_hash) }
58
+
59
+ it "creates deep duplicate of hash" do
60
+ expect(original_hash).to eq(duplicate_hash)
61
+ expect(original_hash.object_id).not_to eq(duplicate_hash.object_id)
62
+
63
+ expect(original_hash[:one]).to eq(duplicate_hash[:one])
64
+ expect(original_hash[:one].object_id).not_to eq(duplicate_hash[:one].object_id)
65
+
66
+ expect(original_hash[:one][:one_one]).to eq(duplicate_hash[:one][:one_one])
67
+ expect(original_hash[:one][:one_one].object_id).not_to eq(duplicate_hash[:one][:one_one].object_id)
68
+
69
+ expect(original_hash[:one][:one_one][:one_one1]).to eq(duplicate_hash[:one][:one_one][:one_one1])
70
+ expect(original_hash[:one][:one_one][:one_one1].object_id).not_to eq(duplicate_hash[:one][:one_one][:one_one1].object_id)
71
+
72
+ # this is a symbol so the object_id will be same
73
+ expect(original_hash[:one][:one_one][:one_one2]).to eq(duplicate_hash[:one][:one_one][:one_one2])
74
+
75
+ expect(original_hash[:one][:one_two]).to eq(duplicate_hash[:one][:one_two])
76
+ expect(original_hash[:one][:one_two].object_id).not_to eq(duplicate_hash[:one][:one_two].object_id)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,83 @@
1
+ # spec/lutaml/model/validation_spec.rb
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Model::Validation do
6
+ class ValidationTestClass < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ attribute :age, :integer
9
+ attribute :email, :string, values: ["test@example.com", "user@example.com"]
10
+ attribute :tags, :string, collection: true
11
+ attribute :role, :string, collection: 1..3
12
+ end
13
+
14
+ let(:valid_instance) do
15
+ ValidationTestClass.new(
16
+ name: "John Doe",
17
+ age: 30,
18
+ email: "test@example.com",
19
+ tags: ["tag1", "tag2"],
20
+ role: ["admin"],
21
+ )
22
+ end
23
+
24
+ describe "#validate" do
25
+ it "returns an empty array for a valid instance" do
26
+ expect(valid_instance.validate).to be_empty
27
+ end
28
+
29
+ xit "returns errors for invalid integer value" do
30
+ instance = ValidationTestClass.new(age: "thirty", role: ["admin"])
31
+ errors = instance.validate
32
+ expect(errors).to include("Invalid value for attribute age: thirty")
33
+ end
34
+
35
+ it "returns errors for value not in allowed set" do
36
+ instance = ValidationTestClass.new(email: "invalid@example.com",
37
+ role: ["admin"])
38
+ expect do
39
+ instance.validate!
40
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
41
+ expect(error.error_messages.join("\n")).to include("email is `invalid@example.com`, must be one of the following [test@example.com, user@example.com]")
42
+ end
43
+ end
44
+
45
+ it "returns errors for invalid collection count" do
46
+ instance = ValidationTestClass.new(role: ["admin", "user", "manager",
47
+ "guest"])
48
+ expect do
49
+ instance.validate!
50
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
51
+ expect(error.error_messages.join("\n")).to include("role count is 4, must be between 1 and 3")
52
+ end
53
+ end
54
+
55
+ xit "returns multiple errors for multiple invalid attributes" do
56
+ instance = ValidationTestClass.new(name: "123", age: "thirty",
57
+ email: "invalid@example.com", role: [])
58
+ expect do
59
+ instance.validate!
60
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
61
+ expect(error.error_messages.join("\n")).to include("Invalid value for attribute age: thirty")
62
+ expect(error.error_messages.join("\n")).to include("email is `invalid@example.com`, must be one of the following [test@example.com, user@example.com]")
63
+ expect(error.error_messages.join("\n")).to include("role count is 0, must be between 1 and 3")
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#validate!" do
69
+ it "does not raise an error for a valid instance" do
70
+ expect { valid_instance.validate! }.not_to raise_error
71
+ end
72
+
73
+ xit "raises a ValidationError with all error messages for an invalid instance" do
74
+ instance = ValidationTestClass.new(name: "test", age: "thirty")
75
+ expect do
76
+ instance.validate!
77
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
78
+ expect(error.error_messages.join("\n")).to include("role count is 0, must be between 1 and 3")
79
+ expect(error.error_messages.join("\n")).to include("Invalid value for attribute age: thirty")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChildMapping
4
+ class Schema < Lutaml::Model::Serializable
5
+ attribute :id, Lutaml::Model::Type::String
6
+ attribute :path, Lutaml::Model::Type::String
7
+ attribute :name, Lutaml::Model::Type::String
8
+ end
9
+
10
+ class ChildMappingClass < Lutaml::Model::Serializable
11
+ attribute :schemas, Schema, collection: true
12
+
13
+ json do
14
+ map "schemas", to: :schemas,
15
+ child_mappings: {
16
+ id: :key,
17
+ path: %i[path link],
18
+ name: %i[path name],
19
+ }
20
+ end
21
+
22
+ yaml do
23
+ map "schemas", to: :schemas,
24
+ child_mappings: {
25
+ id: :key,
26
+ path: %i[path link],
27
+ name: %i[path name],
28
+ }
29
+ end
30
+
31
+ toml do
32
+ map "schemas", to: :schemas,
33
+ child_mappings: {
34
+ id: :key,
35
+ path: %i[path abc],
36
+ name: %i[path name],
37
+ }
38
+ end
39
+ end
40
+ end
41
+
42
+ RSpec.describe ChildMapping do
43
+ let(:mapper) { ChildMapping::ChildMappingClass }
44
+ let(:schema) { ChildMapping::Schema }
45
+
46
+ let(:hash) do
47
+ {
48
+ "schemas" => {
49
+ "foo" => {
50
+ "path" => {
51
+ "link" => "link one",
52
+ "name" => "one",
53
+ },
54
+ },
55
+ "abc" => {
56
+ "path" => {
57
+ "link" => "link two",
58
+ "name" => "two",
59
+ },
60
+ },
61
+ "hello" => {
62
+ "path" => {
63
+ "link" => "link three",
64
+ "name" => "three",
65
+ },
66
+ },
67
+ },
68
+ }
69
+ end
70
+
71
+ let(:expected_ids) { ["foo", "abc", "hello"] }
72
+ let(:expected_paths) { ["link one", "link two", "link three"] }
73
+ let(:expected_names) { ["one", "two", "three"] }
74
+
75
+ context "with json" do
76
+ let(:json) do
77
+ hash.to_json
78
+ end
79
+
80
+ describe ".from_json" do
81
+ it "create model according to json" do
82
+ instance = mapper.from_json(json)
83
+
84
+ expect(instance.schemas.count).to eq(3)
85
+ expect(instance.schemas.map(&:id)).to eq(expected_ids)
86
+ expect(instance.schemas.map(&:path)).to eq(expected_paths)
87
+ expect(instance.schemas.map(&:name)).to eq(expected_names)
88
+ end
89
+ end
90
+
91
+ describe ".to_json" do
92
+ it "converts objects to json" do
93
+ schema1 = schema.new(id: "foo", path: "link one", name: "one")
94
+ schema2 = schema.new(id: "abc", path: "link two", name: "two")
95
+ schema3 = schema.new(id: "hello", path: "link three", name: "three")
96
+
97
+ instance = mapper.new(schemas: [schema1, schema2, schema3])
98
+
99
+ expect(instance.to_json).to eq(json)
100
+ end
101
+ end
102
+ end
103
+
104
+ context "with yaml" do
105
+ let(:yaml) do
106
+ hash.to_yaml
107
+ end
108
+
109
+ describe ".from_yaml" do
110
+ it "create model according to yaml" do
111
+ instance = mapper.from_yaml(yaml)
112
+
113
+ expect(instance.schemas.count).to eq(3)
114
+ expect(instance.schemas.map(&:id)).to eq(expected_ids)
115
+ expect(instance.schemas.map(&:path)).to eq(expected_paths)
116
+ expect(instance.schemas.map(&:name)).to eq(expected_names)
117
+ end
118
+ end
119
+
120
+ describe ".to_yaml" do
121
+ it "converts objects to yaml" do
122
+ schema1 = schema.new(id: "foo", path: "link one", name: "one")
123
+ schema2 = schema.new(id: "abc", path: "link two", name: "two")
124
+ schema3 = schema.new(id: "hello", path: "link three", name: "three")
125
+
126
+ instance = mapper.new(schemas: [schema1, schema2, schema3])
127
+
128
+ expect(instance.to_yaml).to eq(yaml)
129
+ end
130
+ end
131
+ end
132
+
133
+ context "with toml" do
134
+ let(:toml) do
135
+ <<~TOML
136
+ [schemas.foo.path]
137
+ abc = "link one"
138
+ name = "one"
139
+ [schemas.abc.path]
140
+ abc = "link two"
141
+ name = "two"
142
+ [schemas.hello.path]
143
+ abc = "link three"
144
+ name = "three"
145
+ TOML
146
+ end
147
+
148
+ describe ".from_toml" do
149
+ it "create model according to toml" do
150
+ instance = mapper.from_toml(toml)
151
+
152
+ expect(instance.schemas.count).to eq(3)
153
+ expect(instance.schemas.map(&:id)).to eq(expected_ids)
154
+ expect(instance.schemas.map(&:path)).to eq(expected_paths)
155
+ expect(instance.schemas.map(&:name)).to eq(expected_names)
156
+ end
157
+ end
158
+
159
+ describe ".to_toml" do
160
+ it "converts objects to toml" do
161
+ schema1 = schema.new(id: "foo", path: "link one", name: "one")
162
+ schema2 = schema.new(id: "abc", path: "link two", name: "two")
163
+ schema3 = schema.new(id: "hello", path: "link three", name: "three")
164
+
165
+ instance = mapper.new(schemas: [schema1, schema2, schema3])
166
+
167
+ actual = Lutaml::Model::Config.toml_adapter.parse(instance.to_toml)
168
+ expected = Lutaml::Model::Config.toml_adapter.parse(toml)
169
+
170
+ expect(actual.attributes).to eq(expected.attributes)
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+ require "nokogiri"
3
+ require_relative "../../../../lib/lutaml/model/xml_adapter/nokogiri_adapter"
4
+
5
+ RSpec.describe Lutaml::Model::XmlAdapter::NokogiriAdapter do
6
+ let(:xml_string) do
7
+ <<-XML
8
+ <root xmlns="http://example.com/default" xmlns:prefix="http://example.com/prefixed">
9
+ <prefix:child attr="value" prefix:attr1="prefixed_value">Text</prefix:child>
10
+ </root>
11
+ XML
12
+ end
13
+
14
+ let(:document) { described_class.parse(xml_string) }
15
+
16
+ context "when parsing XML with namespaces" do
17
+ let(:child) { document.root.children[1] }
18
+
19
+ it "parses the root element with default namespace" do
20
+ expect(document.root.name).to eq("root")
21
+ expect(document.root.namespace.uri).to eq("http://example.com/default")
22
+ expect(document.root.namespace.prefix).to be_nil
23
+ end
24
+
25
+ it "parses child element with prefixed namespace" do
26
+ expect(child.name).to eq("prefix:child")
27
+ expect(child.namespace.uri).to eq("http://example.com/prefixed")
28
+ expect(child.namespace.prefix).to eq("prefix")
29
+ end
30
+
31
+ it "parses attributes with and without namespaces" do
32
+ expect(child.attributes["attr"].value).to eq("value")
33
+ expect(child.attributes["attr"].namespace).to be_nil
34
+ expect(child.attributes["prefix:attr1"].value).to eq("prefixed_value")
35
+ expect(child.attributes["prefix:attr1"].namespace).to eq("http://example.com/prefixed")
36
+ expect(child.attributes["prefix:attr1"].namespace_prefix).to eq("prefix")
37
+ end
38
+ end
39
+
40
+ context "when generating XML with namespaces" do
41
+ it "generates XML with namespaces correctly" do
42
+ xml_output = document.to_xml
43
+ parsed_output = Nokogiri::XML(xml_output)
44
+
45
+ root = parsed_output.root
46
+ expect(root.name).to eq("root")
47
+ expect(root.namespace.href).to eq("http://example.com/default")
48
+
49
+ child = root.children[1]
50
+ expect(child.name).to eq("child")
51
+ expect(child.namespace.href).to eq("http://example.com/prefixed")
52
+ expect(child.attributes["attr"].value).to eq("value")
53
+ expect(child.attributes["attr1"].value).to eq("prefixed_value")
54
+ end
55
+ end
56
+ end