lutaml-model 0.8.33 → 0.8.34
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.yml +5 -0
- data/Gemfile +4 -0
- data/README.adoc +39 -0
- data/docs/conversion_caching.adoc +143 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/model/attribute.rb +89 -0
- data/lib/lutaml/model/config.rb +13 -0
- data/lib/lutaml/model/configuration.rb +23 -0
- data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
- data/lib/lutaml/model/error/length_error.rb +19 -0
- data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/restriction_error.rb +8 -0
- data/lib/lutaml/model/error/total_digits_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type.rb +2 -0
- data/lib/lutaml/model/restriction_validation.rb +437 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
- data/lib/lutaml/model/schema/templates.rb +2 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
- data/lib/lutaml/model/serialize/builder.rb +4 -5
- data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
- data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
- data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
- data/lib/lutaml/model/serialize/initialization.rb +4 -2
- data/lib/lutaml/model/serialize.rb +61 -0
- data/lib/lutaml/model/services/type/validator.rb +23 -0
- data/lib/lutaml/model/type/string.rb +28 -3
- data/lib/lutaml/model/type/value.rb +231 -0
- data/lib/lutaml/model/type.rb +2 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +16 -0
- data/lib/lutaml/xml/model_transform.rb +4 -1
- data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
- data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
- data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/xml/transformation.rb +2 -0
- data/lib/lutaml/xml/transformation_support.rb +1 -0
- data/spec/fixtures/xml/restriction_facets.xsd +46 -0
- data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
- data/spec/lutaml/model/attribute_spec.rb +7 -2
- data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
- data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
- data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
- data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
- data/spec/lutaml/model/phantom_value_spec.rb +799 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
- data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
- data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
- data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
- data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
- data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
- data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
- metadata +27 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Attribute value restrictions" do
|
|
6
|
+
describe "numeric min/max bounds" do
|
|
7
|
+
before do
|
|
8
|
+
stub_const("BoundedInteger", Class.new(Lutaml::Model::Serializable) do
|
|
9
|
+
attribute :age, :integer, min: 0, max: 120
|
|
10
|
+
end)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "passes validation for a value within the range" do
|
|
14
|
+
expect(BoundedInteger.new(age: 30).validate).to be_empty
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "collects a MinInclusiveError when below the minimum" do
|
|
18
|
+
instance = BoundedInteger.new(age: -1)
|
|
19
|
+
errors = instance.validate
|
|
20
|
+
|
|
21
|
+
expect(errors.size).to eq(1)
|
|
22
|
+
expect(errors.first).to be_a(Lutaml::Model::MinInclusiveError)
|
|
23
|
+
expect(errors.first.to_s).to include("age")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "collects a MaxInclusiveError when above the maximum" do
|
|
27
|
+
instance = BoundedInteger.new(age: 200)
|
|
28
|
+
errors = instance.validate
|
|
29
|
+
|
|
30
|
+
expect(errors.first).to be_a(Lutaml::Model::MaxInclusiveError)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "raises ValidationError from validate! for an out-of-range value" do
|
|
34
|
+
instance = BoundedInteger.new(age: -1)
|
|
35
|
+
|
|
36
|
+
expect { instance.validate! }
|
|
37
|
+
.to raise_error(Lutaml::Model::ValidationError)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "numeric bounds across numeric types" do
|
|
42
|
+
it "enforces bounds on Decimal values" do
|
|
43
|
+
stub_const("BoundedDecimal", Class.new(Lutaml::Model::Serializable) do
|
|
44
|
+
attribute :score, :decimal, min: 0, max: 100
|
|
45
|
+
end)
|
|
46
|
+
|
|
47
|
+
expect(BoundedDecimal.new(score: 50).validate).to be_empty
|
|
48
|
+
expect(BoundedDecimal.new(score: 150).validate.first)
|
|
49
|
+
.to be_a(Lutaml::Model::MaxInclusiveError)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "enforces bounds on Float values" do
|
|
53
|
+
stub_const("BoundedFloat", Class.new(Lutaml::Model::Serializable) do
|
|
54
|
+
attribute :ratio, :float, min: 0.0, max: 1.0
|
|
55
|
+
end)
|
|
56
|
+
|
|
57
|
+
expect(BoundedFloat.new(ratio: 0.5).validate).to be_empty
|
|
58
|
+
expect(BoundedFloat.new(ratio: 1.5).validate.first)
|
|
59
|
+
.to be_a(Lutaml::Model::MaxInclusiveError)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "signed: false" do
|
|
64
|
+
before do
|
|
65
|
+
stub_const("UnsignedInteger", Class.new(Lutaml::Model::Serializable) do
|
|
66
|
+
attribute :count, :integer, signed: false
|
|
67
|
+
end)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "accepts zero" do
|
|
71
|
+
expect(UnsignedInteger.new(count: 0).validate).to be_empty
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "accepts positive values" do
|
|
75
|
+
expect(UnsignedInteger.new(count: 42).validate).to be_empty
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "rejects negative values" do
|
|
79
|
+
expect(UnsignedInteger.new(count: -1).validate.first)
|
|
80
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "merges with an explicit min by taking the greater bound" do
|
|
84
|
+
stub_const("FlooredInteger", Class.new(Lutaml::Model::Serializable) do
|
|
85
|
+
attribute :count, :integer, min: -10, signed: false
|
|
86
|
+
end)
|
|
87
|
+
|
|
88
|
+
expect(FlooredInteger.new(count: 0).validate).to be_empty
|
|
89
|
+
expect(FlooredInteger.new(count: -1).validate.first)
|
|
90
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "temporal bounds (Layer 1)" do
|
|
95
|
+
before do
|
|
96
|
+
stub_const("DatedEvent", Class.new(Lutaml::Model::Serializable) do
|
|
97
|
+
attribute :on, :date,
|
|
98
|
+
min: Date.new(2000, 1, 1), max: Date.new(2020, 12, 31)
|
|
99
|
+
end)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "passes for a date within the range" do
|
|
103
|
+
expect(DatedEvent.new(on: "2010-06-15").validate).to be_empty
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "collects a MinInclusiveError before the minimum date" do
|
|
107
|
+
expect(DatedEvent.new(on: "1999-12-31").validate.first)
|
|
108
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "collects a MaxInclusiveError after the maximum date" do
|
|
112
|
+
expect(DatedEvent.new(on: "2021-01-01").validate.first)
|
|
113
|
+
.to be_a(Lutaml::Model::MaxInclusiveError)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "bounds declared as strings are cast to the resolved type (issue #191)" do
|
|
118
|
+
it "compares a string date bound against a cast Date value" do
|
|
119
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
120
|
+
attribute :on, :date, min: "2000-01-01"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
expect(model.new(on: Date.new(2010, 1, 1)).validate).to be_empty
|
|
124
|
+
expect(model.new(on: Date.new(1990, 1, 1)).validate.first)
|
|
125
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "compares a string decimal bound against a cast Decimal value" do
|
|
129
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
130
|
+
attribute :price, :decimal, min: "1.5", max: "9.5"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
expect(model.new(price: "5.0").validate).to be_empty
|
|
134
|
+
expect(model.new(price: "0.5").validate.first)
|
|
135
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "compares a string integer bound against a cast Integer value" do
|
|
139
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
140
|
+
attribute :age, :integer, min: "0", max: "120"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
expect(model.new(age: 30).validate).to be_empty
|
|
144
|
+
expect(model.new(age: 200).validate.first)
|
|
145
|
+
.to be_a(Lutaml::Model::MaxInclusiveError)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "raises rather than silently dropping a bound that cannot be cast" do
|
|
149
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
150
|
+
attribute :age, :integer, min: "not-a-number"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
expect { model.new(age: 5).validate }
|
|
154
|
+
.to raise_error(ArgumentError, /not a valid/)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "string length bounds" do
|
|
159
|
+
before do
|
|
160
|
+
stub_const("BoundedString", Class.new(Lutaml::Model::Serializable) do
|
|
161
|
+
attribute :name, :string, min_length: 1, max_length: 5
|
|
162
|
+
end)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "passes validation for a length within the range" do
|
|
166
|
+
expect(BoundedString.new(name: "abc").validate).to be_empty
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "rejects a value shorter than min_length" do
|
|
170
|
+
expect(BoundedString.new(name: "").validate.first)
|
|
171
|
+
.to be_a(Lutaml::Model::MinLengthError)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "rejects a value longer than max_length" do
|
|
175
|
+
expect(BoundedString.new(name: "abcdef").validate.first)
|
|
176
|
+
.to be_a(Lutaml::Model::MaxLengthError)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "rejects the empty string when min_length is 1" do
|
|
180
|
+
errors = BoundedString.new(name: "").validate
|
|
181
|
+
|
|
182
|
+
expect(errors.size).to eq(1)
|
|
183
|
+
expect(errors.first).to be_a(Lutaml::Model::MinLengthError)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "length bounds on a non-::String string-derived type (issue #191)" do
|
|
188
|
+
# Type::Uri is string-derived (Uri < String) but its cast value is a ::URI,
|
|
189
|
+
# which has no #length. Length facets must measure the lexical (serialized)
|
|
190
|
+
# form, not the raw cast object, or validation crashes with NoMethodError.
|
|
191
|
+
it "measures the lexical form and passes a URI within range" do
|
|
192
|
+
stub_const("UriInRange", Class.new(Lutaml::Model::Serializable) do
|
|
193
|
+
attribute :homepage, :uri, min_length: 5, max_length: 100
|
|
194
|
+
end)
|
|
195
|
+
|
|
196
|
+
instance = UriInRange.new(homepage: URI.parse("http://example.com"))
|
|
197
|
+
|
|
198
|
+
expect(instance.validate).to be_empty
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "collects a MaxLengthError (never NoMethodError) for a too-long URI" do
|
|
202
|
+
stub_const("UriTooLong", Class.new(Lutaml::Model::Serializable) do
|
|
203
|
+
attribute :homepage, :uri, max_length: 5
|
|
204
|
+
end)
|
|
205
|
+
|
|
206
|
+
instance = UriTooLong.new(homepage: URI.parse("http://example.com"))
|
|
207
|
+
|
|
208
|
+
expect(instance.validate.first).to be_a(Lutaml::Model::MaxLengthError)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "renders the length error message without crashing on the ::URI value" do
|
|
212
|
+
stub_const("UriTooLong", Class.new(Lutaml::Model::Serializable) do
|
|
213
|
+
attribute :homepage, :uri, max_length: 5
|
|
214
|
+
end)
|
|
215
|
+
|
|
216
|
+
instance = UriTooLong.new(homepage: URI.parse("http://example.com"))
|
|
217
|
+
|
|
218
|
+
expect { instance.validate! }
|
|
219
|
+
.to raise_error(Lutaml::Model::ValidationError, /homepage/)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
describe "lazy validation" do
|
|
224
|
+
before do
|
|
225
|
+
stub_const("LazyModel", Class.new(Lutaml::Model::Serializable) do
|
|
226
|
+
attribute :age, :integer, min: 0, max: 120
|
|
227
|
+
attribute :name, :string, min_length: 1
|
|
228
|
+
end)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it "does not raise when an out-of-range value is assigned" do
|
|
232
|
+
expect { LazyModel.new(age: -5, name: "") }.not_to raise_error
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "only surfaces the error on validate!" do
|
|
236
|
+
instance = LazyModel.new(age: -5, name: "ok")
|
|
237
|
+
|
|
238
|
+
expect { instance.validate! }
|
|
239
|
+
.to raise_error(Lutaml::Model::ValidationError)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
describe "configuration guards" do
|
|
244
|
+
it "rejects numeric bounds on a string attribute" do
|
|
245
|
+
stub_const("MisconfiguredString", Class.new(Lutaml::Model::Serializable) do
|
|
246
|
+
attribute :name, :string, min: 0
|
|
247
|
+
end)
|
|
248
|
+
|
|
249
|
+
expect { MisconfiguredString.new(name: "a").validate }
|
|
250
|
+
.to raise_error(ArgumentError, /only allowed for numeric types/)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "rejects length bounds on a numeric attribute" do
|
|
254
|
+
stub_const("MisconfiguredInteger", Class.new(Lutaml::Model::Serializable) do
|
|
255
|
+
attribute :age, :integer, min_length: 1
|
|
256
|
+
end)
|
|
257
|
+
|
|
258
|
+
expect { MisconfiguredInteger.new(age: 1).validate }
|
|
259
|
+
.to raise_error(ArgumentError, /only allowed for :string type/)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "rejects signed: true on a string attribute" do
|
|
263
|
+
stub_const("SignedString", Class.new(Lutaml::Model::Serializable) do
|
|
264
|
+
attribute :name, :string, signed: true
|
|
265
|
+
end)
|
|
266
|
+
|
|
267
|
+
expect { SignedString.new(name: "a").validate }
|
|
268
|
+
.to raise_error(ArgumentError, /only allowed for numeric types/)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "treats signed: true on a numeric attribute as a no-op" do
|
|
272
|
+
stub_const("SignedInteger", Class.new(Lutaml::Model::Serializable) do
|
|
273
|
+
attribute :count, :integer, signed: true
|
|
274
|
+
end)
|
|
275
|
+
|
|
276
|
+
expect(SignedInteger.new(count: -5).validate).to be_empty
|
|
277
|
+
expect { SignedInteger.new(count: -5).validate! }.not_to raise_error
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "accepts negative values when signed is not given (signed defaults to true)" do
|
|
281
|
+
stub_const("DefaultInteger", Class.new(Lutaml::Model::Serializable) do
|
|
282
|
+
attribute :count, :integer
|
|
283
|
+
end)
|
|
284
|
+
|
|
285
|
+
expect(DefaultInteger.new(count: -5).validate).to be_empty
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "rejects a negative min_length at attribute definition" do
|
|
289
|
+
expect do
|
|
290
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
291
|
+
attribute :name, :string, min_length: -1
|
|
292
|
+
end
|
|
293
|
+
end.to raise_error(ArgumentError,
|
|
294
|
+
/`min_length` must be a non-negative Integer/)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it "rejects a negative max_length at attribute definition" do
|
|
298
|
+
expect do
|
|
299
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
300
|
+
attribute :name, :string, max_length: -1
|
|
301
|
+
end
|
|
302
|
+
end.to raise_error(ArgumentError,
|
|
303
|
+
/`max_length` must be a non-negative Integer/)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it "rejects a non-Integer min_length at attribute definition" do
|
|
307
|
+
expect do
|
|
308
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
309
|
+
attribute :name, :string, min_length: "5"
|
|
310
|
+
end
|
|
311
|
+
end.to raise_error(ArgumentError,
|
|
312
|
+
/`min_length` must be a non-negative Integer/)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "rejects a Float max_length at attribute definition" do
|
|
316
|
+
expect do
|
|
317
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
318
|
+
attribute :name, :string, max_length: 2.5
|
|
319
|
+
end
|
|
320
|
+
end.to raise_error(ArgumentError,
|
|
321
|
+
/`max_length` must be a non-negative Integer/)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "rejects a non-boolean signed at attribute definition" do
|
|
325
|
+
expect do
|
|
326
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
327
|
+
attribute :count, :integer, signed: "false"
|
|
328
|
+
end
|
|
329
|
+
end.to raise_error(ArgumentError, /`signed` must be true or false/)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "accepts signed: false at attribute definition" do
|
|
333
|
+
expect do
|
|
334
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
335
|
+
attribute :count, :integer, signed: false
|
|
336
|
+
end
|
|
337
|
+
end.not_to raise_error
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
describe "collections (per-element restrictions)" do
|
|
342
|
+
before do
|
|
343
|
+
stub_const("BoundedCollection", Class.new(Lutaml::Model::Serializable) do
|
|
344
|
+
attribute :ages, :integer, collection: true, min: 0
|
|
345
|
+
end)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "passes when every element satisfies a Layer-1 numeric bound" do
|
|
349
|
+
expect(BoundedCollection.new(ages: [0, 5, 120]).validate).to be_empty
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "collects a MinInclusiveError when an element violates the bound" do
|
|
353
|
+
errors = BoundedCollection.new(ages: [1, -5, 10]).validate
|
|
354
|
+
|
|
355
|
+
expect(errors.size).to eq(1)
|
|
356
|
+
expect(errors.first).to be_a(Lutaml::Model::MinInclusiveError)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it "raises from validate! when an element is out of range" do
|
|
360
|
+
expect { BoundedCollection.new(ages: [1, -5]).validate! }
|
|
361
|
+
.to raise_error(Lutaml::Model::ValidationError)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
it "skips nil elements, checking only present ones" do
|
|
365
|
+
expect(BoundedCollection.new(ages: [nil, 5]).validate).to be_empty
|
|
366
|
+
expect(BoundedCollection.new(ages: [nil, -5]).validate.first)
|
|
367
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "applies the facet to a scalar assigned to a collection attribute" do
|
|
371
|
+
expect(BoundedCollection.new(ages: 5).validate).to be_empty
|
|
372
|
+
expect(BoundedCollection.new(ages: -5).validate.first)
|
|
373
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it "enforces a Layer-2 inclusive facet per element" do
|
|
377
|
+
stub_const("Percentage", Class.new(Lutaml::Model::Type::Integer) do
|
|
378
|
+
inclusive min: 0, max: 100
|
|
379
|
+
end)
|
|
380
|
+
stub_const("Report", Class.new(Lutaml::Model::Serializable) do
|
|
381
|
+
attribute :scores, Percentage, collection: true
|
|
382
|
+
end)
|
|
383
|
+
|
|
384
|
+
expect(Report.new(scores: [10, 50, 100]).validate).to be_empty
|
|
385
|
+
expect(Report.new(scores: [10, 150]).validate.first)
|
|
386
|
+
.to be_a(Lutaml::Model::MaxInclusiveError)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it "enforces a Layer-1 min_length per string element" do
|
|
390
|
+
stub_const("Names", Class.new(Lutaml::Model::Serializable) do
|
|
391
|
+
attribute :tags, :string, collection: true, min_length: 2
|
|
392
|
+
end)
|
|
393
|
+
|
|
394
|
+
expect(Names.new(tags: %w[ab cde]).validate).to be_empty
|
|
395
|
+
expect(Names.new(tags: %w[ab c]).validate.first)
|
|
396
|
+
.to be_a(Lutaml::Model::MinLengthError)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it "enforces a Layer-2 pattern per string element" do
|
|
400
|
+
stub_const("Code", Class.new(Lutaml::Model::Type::String) do
|
|
401
|
+
pattern '\A[a-z]+\z'
|
|
402
|
+
end)
|
|
403
|
+
stub_const("Codes", Class.new(Lutaml::Model::Serializable) do
|
|
404
|
+
attribute :list, Code, collection: true
|
|
405
|
+
end)
|
|
406
|
+
|
|
407
|
+
expect(Codes.new(list: %w[abc def]).validate).to be_empty
|
|
408
|
+
expect(Codes.new(list: %w[abc DEF]).validate.first)
|
|
409
|
+
.to be_a(Lutaml::Model::PatternNotMatchedError)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it "enforces a Layer-2 enumeration per element" do
|
|
413
|
+
stub_const("Status", Class.new(Lutaml::Model::Type::String) do
|
|
414
|
+
enumeration "active", "inactive"
|
|
415
|
+
end)
|
|
416
|
+
stub_const("Flags", Class.new(Lutaml::Model::Serializable) do
|
|
417
|
+
attribute :states, Status, collection: true
|
|
418
|
+
end)
|
|
419
|
+
|
|
420
|
+
expect(Flags.new(states: %w[active inactive]).validate).to be_empty
|
|
421
|
+
expect(Flags.new(states: %w[active deleted]).validate.first)
|
|
422
|
+
.to be_a(Lutaml::Model::InvalidValueError)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
it "enforces a digit facet per element" do
|
|
426
|
+
stub_const("Money", Class.new(Lutaml::Model::Type::Decimal) do
|
|
427
|
+
total_digits 5
|
|
428
|
+
end)
|
|
429
|
+
stub_const("Ledger", Class.new(Lutaml::Model::Serializable) do
|
|
430
|
+
attribute :amounts, Money, collection: true
|
|
431
|
+
end)
|
|
432
|
+
|
|
433
|
+
expect(Ledger.new(amounts: ["12.34", "1.5"]).validate).to be_empty
|
|
434
|
+
expect(Ledger.new(amounts: ["12.34", "123456"]).validate.first)
|
|
435
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it "governs item count via the collection range, not the length facet" do
|
|
439
|
+
stub_const("Sized", Class.new(Lutaml::Model::Serializable) do
|
|
440
|
+
attribute :list, :string, collection: 2..3, min_length: 3
|
|
441
|
+
end)
|
|
442
|
+
|
|
443
|
+
expect(Sized.new(list: %w[abc def]).validate).to be_empty
|
|
444
|
+
expect(Sized.new(list: %w[abc de]).validate.first)
|
|
445
|
+
.to be_a(Lutaml::Model::MinLengthError)
|
|
446
|
+
expect(Sized.new(list: %w[abc def ghi jkl]).validate.first)
|
|
447
|
+
.to be_a(Lutaml::Model::CollectionCountOutOfRangeError)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
describe "backward compatibility" do
|
|
452
|
+
it "keeps values: enumeration validation working" do
|
|
453
|
+
stub_const("EnumModel", Class.new(Lutaml::Model::Serializable) do
|
|
454
|
+
attribute :status, :string, values: %w[active inactive]
|
|
455
|
+
end)
|
|
456
|
+
|
|
457
|
+
expect(EnumModel.new(status: "active").validate).to be_empty
|
|
458
|
+
expect(EnumModel.new(status: "unknown").validate.first)
|
|
459
|
+
.to be_a(Lutaml::Model::InvalidValueError)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
it "keeps pattern: validation working" do
|
|
463
|
+
stub_const("PatternModel", Class.new(Lutaml::Model::Serializable) do
|
|
464
|
+
attribute :code, :string, pattern: /\A[a-z]+\z/
|
|
465
|
+
end)
|
|
466
|
+
|
|
467
|
+
expect(PatternModel.new(code: "abc").validate).to be_empty
|
|
468
|
+
expect(PatternModel.new(code: "ABC").validate.first)
|
|
469
|
+
.to be_a(Lutaml::Model::PatternNotMatchedError)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
@@ -189,15 +189,20 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
189
189
|
describe "#validate_options!" do
|
|
190
190
|
let(:validate_options) { name_attr.method(:validate_options!) }
|
|
191
191
|
|
|
192
|
+
# Options whose value is type-checked at definition need a valid sample;
|
|
193
|
+
# the rest accept any value here (applicability is validated lazily).
|
|
194
|
+
let(:option_sample_values) { { min_length: 1, max_length: 1, signed: true } }
|
|
195
|
+
|
|
192
196
|
Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
|
|
193
197
|
it "return true if option is `#{option}`" do
|
|
198
|
+
value = option_sample_values.fetch(option, "value")
|
|
194
199
|
if option == :xsd_type
|
|
195
200
|
expect do
|
|
196
|
-
result = validate_options.call({ option =>
|
|
201
|
+
result = validate_options.call({ option => value })
|
|
197
202
|
expect(result).to be(true)
|
|
198
203
|
end.to output.to_stderr
|
|
199
204
|
else
|
|
200
|
-
expect(validate_options.call({ option =>
|
|
205
|
+
expect(validate_options.call({ option => value })).to be(true)
|
|
201
206
|
end
|
|
202
207
|
end
|
|
203
208
|
end
|