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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/Gemfile +4 -0
  4. data/README.adoc +39 -0
  5. data/docs/conversion_caching.adoc +143 -0
  6. data/lib/compat/opal/lutaml_model_boot.rb +15 -0
  7. data/lib/lutaml/model/attribute.rb +89 -0
  8. data/lib/lutaml/model/config.rb +13 -0
  9. data/lib/lutaml/model/configuration.rb +23 -0
  10. data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
  11. data/lib/lutaml/model/error/length_error.rb +19 -0
  12. data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
  13. data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
  14. data/lib/lutaml/model/error/max_length_error.rb +20 -0
  15. data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
  16. data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
  17. data/lib/lutaml/model/error/min_length_error.rb +20 -0
  18. data/lib/lutaml/model/error/restriction_error.rb +8 -0
  19. data/lib/lutaml/model/error/total_digits_error.rb +20 -0
  20. data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
  21. data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
  22. data/lib/lutaml/model/error/type.rb +2 -0
  23. data/lib/lutaml/model/restriction_validation.rb +437 -0
  24. data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
  25. data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
  26. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
  27. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
  28. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
  29. data/lib/lutaml/model/schema/templates.rb +2 -0
  30. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
  31. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
  32. data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
  33. data/lib/lutaml/model/serialize/builder.rb +4 -5
  34. data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
  35. data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
  36. data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
  37. data/lib/lutaml/model/serialize/initialization.rb +4 -2
  38. data/lib/lutaml/model/serialize.rb +61 -0
  39. data/lib/lutaml/model/services/type/validator.rb +23 -0
  40. data/lib/lutaml/model/type/string.rb +28 -3
  41. data/lib/lutaml/model/type/value.rb +231 -0
  42. data/lib/lutaml/model/type.rb +2 -0
  43. data/lib/lutaml/model/validation.rb +2 -1
  44. data/lib/lutaml/model/version.rb +1 -1
  45. data/lib/lutaml/model.rb +16 -0
  46. data/lib/lutaml/xml/model_transform.rb +4 -1
  47. data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
  48. data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
  49. data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
  50. data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
  51. data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
  52. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
  53. data/lib/lutaml/xml/transformation.rb +2 -0
  54. data/lib/lutaml/xml/transformation_support.rb +1 -0
  55. data/spec/fixtures/xml/restriction_facets.xsd +46 -0
  56. data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
  57. data/spec/lutaml/model/attribute_spec.rb +7 -2
  58. data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
  59. data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
  60. data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
  61. data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
  62. data/spec/lutaml/model/phantom_value_spec.rb +799 -0
  63. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
  64. data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
  65. data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
  66. data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
  67. data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
  68. data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
  69. data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
  70. data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
  71. 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 => "value" })
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 => "value" })).to be(true)
205
+ expect(validate_options.call({ option => value })).to be(true)
201
206
  end
202
207
  end
203
208
  end