lutaml-model 0.3.24 → 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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +35 -16
- data/README.adoc +274 -28
- data/lib/lutaml/model/attribute.rb +18 -8
- data/lib/lutaml/model/error/type_error.rb +9 -0
- data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
- data/lib/lutaml/model/error/validation_error.rb +0 -1
- data/lib/lutaml/model/error.rb +2 -0
- data/lib/lutaml/model/serialize.rb +6 -1
- data/lib/lutaml/model/type/boolean.rb +38 -0
- data/lib/lutaml/model/type/date.rb +35 -0
- data/lib/lutaml/model/type/date_time.rb +32 -4
- data/lib/lutaml/model/type/decimal.rb +42 -0
- data/lib/lutaml/model/type/float.rb +37 -0
- data/lib/lutaml/model/type/hash.rb +62 -0
- data/lib/lutaml/model/type/integer.rb +41 -0
- data/lib/lutaml/model/type/string.rb +49 -0
- data/lib/lutaml/model/type/time.rb +49 -0
- data/lib/lutaml/model/type/time_without_date.rb +37 -5
- data/lib/lutaml/model/type/value.rb +52 -0
- data/lib/lutaml/model/type.rb +50 -114
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +5 -2
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +2 -1
- data/lib/lutaml/model/xml_adapter/xml_document.rb +0 -2
- data/lutaml-model.gemspec +1 -1
- data/spec/address_spec.rb +170 -0
- data/spec/fixtures/address.rb +33 -0
- data/spec/fixtures/person.rb +73 -0
- data/spec/fixtures/sample_model.rb +40 -0
- data/spec/fixtures/vase.rb +38 -0
- data/spec/fixtures/xml/special_char.xml +13 -0
- data/spec/lutaml/model/attribute_spec.rb +112 -0
- data/spec/lutaml/model/collection_spec.rb +299 -0
- data/spec/lutaml/model/comparable_model_spec.rb +106 -0
- data/spec/lutaml/model/custom_model_spec.rb +410 -0
- data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
- data/spec/lutaml/model/defaults_spec.rb +221 -0
- data/spec/lutaml/model/delegation_spec.rb +340 -0
- data/spec/lutaml/model/inheritance_spec.rb +92 -0
- data/spec/lutaml/model/json_adapter_spec.rb +37 -0
- data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
- data/spec/lutaml/model/map_content_spec.rb +118 -0
- data/spec/lutaml/model/mixed_content_spec.rb +625 -0
- data/spec/lutaml/model/namespace_spec.rb +57 -0
- data/spec/lutaml/model/ordered_content_spec.rb +83 -0
- data/spec/lutaml/model/render_nil_spec.rb +138 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
- data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
- data/spec/lutaml/model/serializable_spec.rb +297 -0
- data/spec/lutaml/model/serializable_validation_spec.rb +85 -0
- data/spec/lutaml/model/simple_model_spec.rb +314 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
- data/spec/lutaml/model/type/boolean_spec.rb +54 -0
- data/spec/lutaml/model/type/date_spec.rb +118 -0
- data/spec/lutaml/model/type/date_time_spec.rb +127 -0
- data/spec/lutaml/model/type/decimal_spec.rb +125 -0
- data/spec/lutaml/model/type/float_spec.rb +191 -0
- data/spec/lutaml/model/type/hash_spec.rb +63 -0
- data/spec/lutaml/model/type/integer_spec.rb +145 -0
- data/spec/lutaml/model/type/string_spec.rb +150 -0
- data/spec/lutaml/model/type/time_spec.rb +142 -0
- data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
- data/spec/lutaml/model/type_spec.rb +276 -0
- data/spec/lutaml/model/utils_spec.rb +79 -0
- data/spec/lutaml/model/validation_spec.rb +83 -0
- data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
- data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
- data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
- data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
- data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
- data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
- data/spec/lutaml/model_spec.rb +1 -0
- data/spec/person_spec.rb +161 -0
- data/spec/spec_helper.rb +33 -0
- 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
|