lluminary 0.2.0 → 0.2.2

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.
@@ -169,4 +169,263 @@ RSpec.describe Lluminary::SchemaModel do
169
169
  )
170
170
  end
171
171
  end
172
+
173
+ describe "boolean field validation" do
174
+ let(:fields) do
175
+ { active: { type: :boolean, description: "Whether the item is active" } }
176
+ end
177
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
178
+
179
+ it "accepts true values" do
180
+ instance = model_class.new(active: true)
181
+ expect(instance.valid?).to be true
182
+ expect(instance.errors.full_messages).to be_empty
183
+ end
184
+
185
+ it "accepts false values" do
186
+ instance = model_class.new(active: false)
187
+ expect(instance.valid?).to be true
188
+ expect(instance.errors.full_messages).to be_empty
189
+ end
190
+
191
+ it "accepts nil values" do
192
+ instance = model_class.new(active: nil)
193
+ expect(instance.valid?).to be true
194
+ expect(instance.errors.full_messages).to be_empty
195
+ end
196
+
197
+ it "returns errors for non-boolean values" do
198
+ instance = model_class.new(active: "true")
199
+ expect(instance.valid?).to be false
200
+ expect(instance.errors.full_messages).to contain_exactly(
201
+ "Active must be true or false"
202
+ )
203
+
204
+ instance = model_class.new(active: 1)
205
+ expect(instance.valid?).to be false
206
+ expect(instance.errors.full_messages).to contain_exactly(
207
+ "Active must be true or false"
208
+ )
209
+ end
210
+
211
+ it "can be required using presence validation" do
212
+ validations = [[[:active], { presence: true }]]
213
+ model_class_with_presence =
214
+ described_class.build(fields: fields, validations: validations)
215
+ instance = model_class_with_presence.new(active: nil)
216
+ expect(instance.valid?).to be false
217
+ expect(instance.errors.full_messages).to contain_exactly(
218
+ "Active can't be blank"
219
+ )
220
+ end
221
+ end
222
+
223
+ describe "hash field with array validation" do
224
+ let(:fields) do
225
+ {
226
+ config: {
227
+ type: :hash,
228
+ description: "Configuration",
229
+ fields: {
230
+ name: {
231
+ type: :string,
232
+ description: nil
233
+ },
234
+ tags: {
235
+ type: :array,
236
+ description: nil,
237
+ element_type: {
238
+ type: :string,
239
+ description: nil
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ end
246
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
247
+
248
+ it "validates arrays inside hashes" do
249
+ instance =
250
+ model_class.new(
251
+ config: {
252
+ name: "test",
253
+ tags: ["valid", 123, "also valid"]
254
+ }
255
+ )
256
+ expect(instance.valid?).to be false
257
+ expect(instance.errors.full_messages).to contain_exactly(
258
+ "Config[tags][1] must be a String"
259
+ )
260
+ end
261
+ end
262
+
263
+ describe "nested hash validation" do
264
+ let(:fields) do
265
+ {
266
+ config: {
267
+ type: :hash,
268
+ description: nil,
269
+ fields: {
270
+ name: {
271
+ type: :string,
272
+ description: nil
273
+ },
274
+ database: {
275
+ type: :hash,
276
+ description: nil,
277
+ fields: {
278
+ host: {
279
+ type: :string,
280
+ description: nil
281
+ },
282
+ port: {
283
+ type: :integer,
284
+ description: nil
285
+ },
286
+ credentials: {
287
+ type: :hash,
288
+ description: nil,
289
+ fields: {
290
+ username: {
291
+ type: :string,
292
+ description: nil
293
+ },
294
+ password: {
295
+ type: :string,
296
+ description: nil
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ end
306
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
307
+
308
+ it "validates nested hashes" do
309
+ instance =
310
+ model_class.new(
311
+ config: {
312
+ name: "test",
313
+ database: {
314
+ host: 123, # should be string
315
+ port: "80", # should be integer
316
+ credentials: {
317
+ username: 456, # should be string
318
+ password: 789 # should be string
319
+ }
320
+ }
321
+ }
322
+ )
323
+ expect(instance.valid?).to be false
324
+ expect(instance.errors.full_messages).to contain_exactly(
325
+ "Config[database][host] must be a String",
326
+ "Config[database][port] must be an Integer",
327
+ "Config[database][credentials][username] must be a String",
328
+ "Config[database][credentials][password] must be a String"
329
+ )
330
+ end
331
+ end
332
+
333
+ describe "hash type enforcement" do
334
+ let(:fields) do
335
+ {
336
+ config: {
337
+ type: :hash,
338
+ description: nil,
339
+ fields: {
340
+ host: {
341
+ type: :string,
342
+ description: nil
343
+ }
344
+ }
345
+ }
346
+ }
347
+ end
348
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
349
+
350
+ it "validates that value is a hash" do
351
+ instance = model_class.new(config: "not a hash")
352
+ expect(instance.valid?).to be false
353
+ expect(instance.errors.full_messages).to contain_exactly(
354
+ "Config must be a Hash"
355
+ )
356
+ end
357
+ end
358
+
359
+ describe "array of hashes validation" do
360
+ let(:fields) do
361
+ {
362
+ users: {
363
+ type: :array,
364
+ description: nil,
365
+ element_type: {
366
+ type: :hash,
367
+ description: nil,
368
+ fields: {
369
+ name: {
370
+ type: :string,
371
+ description: nil
372
+ },
373
+ age: {
374
+ type: :integer,
375
+ description: nil
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ end
382
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
383
+
384
+ it "validates hashes inside arrays" do
385
+ instance =
386
+ model_class.new(
387
+ users: [
388
+ { name: "Alice", age: 30 },
389
+ { name: 123, age: "invalid" }, # name should be string, age should be integer
390
+ { name: "Bob", age: 25 }
391
+ ]
392
+ )
393
+ expect(instance.valid?).to be false
394
+ expect(instance.errors.full_messages).to contain_exactly(
395
+ "Users[1][name] must be a String",
396
+ "Users[1][age] must be an Integer"
397
+ )
398
+ end
399
+ end
400
+
401
+ describe "float field validation" do
402
+ let(:fields) { { score: { type: :float, description: "The score" } } }
403
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
404
+
405
+ it "accepts float values" do
406
+ instance = model_class.new(score: 3.14)
407
+ expect(instance.valid?).to be true
408
+ expect(instance.errors.full_messages).to be_empty
409
+ end
410
+
411
+ it "accepts nil values" do
412
+ instance = model_class.new(score: nil)
413
+ expect(instance.valid?).to be true
414
+ expect(instance.errors.full_messages).to be_empty
415
+ end
416
+
417
+ it "returns errors for non-float values" do
418
+ instance = model_class.new(score: "not a float")
419
+ expect(instance.valid?).to be false
420
+ expect(instance.errors.full_messages).to contain_exactly(
421
+ "Score must be a float"
422
+ )
423
+
424
+ instance = model_class.new(score: 42)
425
+ expect(instance.valid?).to be false
426
+ expect(instance.errors.full_messages).to contain_exactly(
427
+ "Score must be a float"
428
+ )
429
+ end
430
+ end
172
431
  end
@@ -10,6 +10,58 @@ RSpec.describe Lluminary::Schema do
10
10
  end
11
11
  end
12
12
 
13
+ describe "#validate" do
14
+ it "registers a custom validation method" do
15
+ schema.validate(:validate_something)
16
+ expect(schema.custom_validations).to eq(
17
+ [{ method: :validate_something, description: nil }]
18
+ )
19
+ end
20
+
21
+ it "can register multiple validation methods" do
22
+ schema.validate(:validate_something)
23
+ schema.validate(:validate_something_else)
24
+ expect(schema.custom_validations).to eq(
25
+ [
26
+ { method: :validate_something, description: nil },
27
+ { method: :validate_something_else, description: nil }
28
+ ]
29
+ )
30
+ end
31
+
32
+ it "registers a custom validation method with a description" do
33
+ schema.validate(
34
+ :validate_score_range,
35
+ description: "Score must be between 0 and 100"
36
+ )
37
+ expect(schema.custom_validations).to eq(
38
+ [
39
+ {
40
+ method: :validate_score_range,
41
+ description: "Score must be between 0 and 100"
42
+ }
43
+ ]
44
+ )
45
+ end
46
+
47
+ it "registers multiple custom validations with and without descriptions" do
48
+ schema.validate(
49
+ :validate_score_range,
50
+ description: "Score must be between 0 and 100"
51
+ )
52
+ schema.validate(:validate_score_parity)
53
+ expect(schema.custom_validations).to eq(
54
+ [
55
+ {
56
+ method: :validate_score_range,
57
+ description: "Score must be between 0 and 100"
58
+ },
59
+ { method: :validate_score_parity, description: nil }
60
+ ]
61
+ )
62
+ end
63
+ end
64
+
13
65
  describe "#string" do
14
66
  it "adds a string field to the schema" do
15
67
  schema.string(:name)
@@ -213,12 +265,6 @@ RSpec.describe Lluminary::Schema do
213
265
  )
214
266
  end
215
267
 
216
- it "validates array elements" do
217
- schema.array(:numbers) { integer }
218
- errors = schema.validate(numbers: [1, "2", 3])
219
- expect(errors).to contain_exactly("Numbers[1] must be an Integer")
220
- end
221
-
222
268
  it "supports hashes inside arrays" do
223
269
  schema.array(:users) do
224
270
  hash do
@@ -248,29 +294,6 @@ RSpec.describe Lluminary::Schema do
248
294
  }
249
295
  )
250
296
  end
251
-
252
- it "validates hashes inside arrays" do
253
- schema.array(:users) do
254
- hash do
255
- string :name
256
- integer :age
257
- end
258
- end
259
-
260
- errors =
261
- schema.validate(
262
- users: [
263
- { name: "Alice", age: 30 },
264
- { name: 123, age: "invalid" }, # name should be string, age should be integer
265
- { name: "Bob", age: 25 }
266
- ]
267
- )
268
-
269
- expect(errors).to contain_exactly(
270
- "Users[1][name] must be a String",
271
- "Users[1][age] must be an Integer"
272
- )
273
- end
274
297
  end
275
298
 
276
299
  describe "#hash" do
@@ -307,33 +330,6 @@ RSpec.describe Lluminary::Schema do
307
330
  )
308
331
  end
309
332
 
310
- it "validates hash values" do
311
- schema.hash(:config) do
312
- string :host
313
- integer :port
314
- end
315
-
316
- errors =
317
- schema.validate(
318
- config: {
319
- host: 123, # should be string
320
- port: "80" # should be integer
321
- }
322
- )
323
-
324
- expect(errors).to contain_exactly(
325
- "Config[host] must be a String",
326
- "Config[port] must be an Integer"
327
- )
328
- end
329
-
330
- it "validates that value is a hash" do
331
- schema.hash(:config) { string :host }
332
-
333
- errors = schema.validate(config: "not a hash")
334
- expect(errors).to contain_exactly("Config must be a Hash")
335
- end
336
-
337
333
  it "supports nested hashes" do
338
334
  schema.hash(:config) do
339
335
  string :name
@@ -389,42 +385,6 @@ RSpec.describe Lluminary::Schema do
389
385
  )
390
386
  end
391
387
 
392
- it "validates nested hashes" do
393
- schema.hash(:config) do
394
- string :name
395
- hash :database do
396
- string :host
397
- integer :port
398
- hash :credentials do
399
- string :username
400
- string :password
401
- end
402
- end
403
- end
404
-
405
- errors =
406
- schema.validate(
407
- config: {
408
- name: "test",
409
- database: {
410
- host: 123, # should be string
411
- port: "80", # should be integer
412
- credentials: {
413
- username: 456, # should be string
414
- password: 789 # should be string
415
- }
416
- }
417
- }
418
- )
419
-
420
- expect(errors).to contain_exactly(
421
- "Config[database][host] must be a String",
422
- "Config[database][port] must be an Integer",
423
- "Config[database][credentials][username] must be a String",
424
- "Config[database][credentials][password] must be a String"
425
- )
426
- end
427
-
428
388
  it "supports arrays inside hashes" do
429
389
  schema.hash(:config) do
430
390
  string :name
@@ -454,25 +414,6 @@ RSpec.describe Lluminary::Schema do
454
414
  }
455
415
  )
456
416
  end
457
-
458
- it "validates arrays inside hashes" do
459
- schema.hash(:config) do
460
- string :name
461
- array :tags do
462
- string
463
- end
464
- end
465
-
466
- errors =
467
- schema.validate(
468
- config: {
469
- name: "test",
470
- tags: ["valid", 123, "also valid"] # second element should be string
471
- }
472
- )
473
-
474
- expect(errors).to contain_exactly("Config[tags][1] must be a String")
475
- end
476
417
  end
477
418
 
478
419
  describe "primitive types" do
@@ -509,136 +450,6 @@ RSpec.describe Lluminary::Schema do
509
450
  second_call = schema.fields
510
451
  expect(first_call).to be(second_call)
511
452
  end
512
-
513
- context "with datetime fields" do
514
- let(:schema) { described_class.new.tap { |s| s.datetime(:start_time) } }
515
-
516
- it "accepts DateTime values" do
517
- errors = schema.validate(start_time: DateTime.now)
518
- expect(errors).to be_empty
519
- end
520
-
521
- it "accepts nil values" do
522
- errors = schema.validate(start_time: nil)
523
- expect(errors).to be_empty
524
- end
525
-
526
- it "returns errors for non-DateTime values" do
527
- errors = schema.validate(start_time: "2024-01-01")
528
- expect(errors).to contain_exactly("Start time must be a DateTime")
529
- end
530
-
531
- it "can be required using presence validation" do
532
- schema.validates :start_time, presence: true
533
- errors = schema.validate(start_time: nil)
534
- expect(errors).to contain_exactly("Start time can't be blank")
535
- end
536
- end
537
- end
538
-
539
- describe "#validate" do
540
- let(:schema) do
541
- described_class.new.tap do |s|
542
- s.string(:name)
543
- s.integer(:age)
544
- end
545
- end
546
-
547
- it "returns no errors when all values match their field types" do
548
- errors = schema.validate(name: "John", age: 30)
549
- expect(errors).to be_empty
550
- end
551
-
552
- it "returns errors for type mismatches" do
553
- errors = schema.validate(name: 123, age: "30")
554
- expect(errors).to contain_exactly(
555
- "Name must be a String",
556
- "Age must be an Integer"
557
- )
558
- end
559
-
560
- context "with boolean fields" do
561
- let(:schema) { described_class.new.tap { |s| s.boolean(:active) } }
562
-
563
- it "accepts true values" do
564
- errors = schema.validate(active: true)
565
- expect(errors).to be_empty
566
- end
567
-
568
- it "accepts false values" do
569
- errors = schema.validate(active: false)
570
- expect(errors).to be_empty
571
- end
572
-
573
- it "accepts nil values" do
574
- errors = schema.validate(active: nil)
575
- expect(errors).to be_empty
576
- end
577
-
578
- it "returns errors for non-boolean values" do
579
- errors = schema.validate(active: "true")
580
- expect(errors).to contain_exactly("Active must be true or false")
581
-
582
- errors = schema.validate(active: 1)
583
- expect(errors).to contain_exactly("Active must be true or false")
584
- end
585
-
586
- it "can be required using presence validation" do
587
- schema.validates :active, presence: true
588
- errors = schema.validate(active: nil)
589
- expect(errors).to contain_exactly("Active can't be blank")
590
- end
591
- end
592
-
593
- context "with string fields" do
594
- let(:schema) { described_class.new.tap { |s| s.string(:name) } }
595
-
596
- it "accepts string values" do
597
- errors = schema.validate(name: "John")
598
- expect(errors).to be_empty
599
- end
600
-
601
- it "accepts nil values" do
602
- errors = schema.validate(name: nil)
603
- expect(errors).to be_empty
604
- end
605
-
606
- it "returns errors for non-string values" do
607
- errors = schema.validate(name: 123)
608
- expect(errors).to contain_exactly("Name must be a String")
609
- end
610
-
611
- it "can be required using presence validation" do
612
- schema.validates :name, presence: true
613
- errors = schema.validate(name: nil)
614
- expect(errors).to contain_exactly("Name can't be blank")
615
- end
616
- end
617
-
618
- context "with integer fields" do
619
- let(:schema) { described_class.new.tap { |s| s.integer(:age) } }
620
-
621
- it "accepts integer values" do
622
- errors = schema.validate(age: 30)
623
- expect(errors).to be_empty
624
- end
625
-
626
- it "accepts nil values" do
627
- errors = schema.validate(age: nil)
628
- expect(errors).to be_empty
629
- end
630
-
631
- it "returns errors for non-integer values" do
632
- errors = schema.validate(age: "30")
633
- expect(errors).to contain_exactly("Age must be an Integer")
634
- end
635
-
636
- it "can be required using presence validation" do
637
- schema.validates :age, presence: true
638
- errors = schema.validate(age: nil)
639
- expect(errors).to contain_exactly("Age can't be blank")
640
- end
641
- end
642
453
  end
643
454
 
644
455
  describe "ActiveModel validations" do
@@ -688,4 +499,32 @@ RSpec.describe Lluminary::Schema do
688
499
  expect(instance.valid?).to be true
689
500
  end
690
501
  end
502
+
503
+ describe "custom method validations" do
504
+ it "passes custom validations to schema model" do
505
+ schema = described_class.new
506
+ schema.integer(:score)
507
+ schema.validate(:validate_score_range)
508
+
509
+ model_class = schema.schema_model
510
+ method_names =
511
+ model_class.custom_validation_methods.map { |v| v[:method] }
512
+ expect(method_names).to contain_exactly(:validate_score_range)
513
+ end
514
+
515
+ it "accepts multiple custom validations" do
516
+ schema = described_class.new
517
+ schema.integer(:score)
518
+ schema.validate(:validate_score_range)
519
+ schema.validate(:validate_score_parity)
520
+
521
+ model_class = schema.schema_model
522
+ method_names =
523
+ model_class.custom_validation_methods.map { |v| v[:method] }
524
+ expect(method_names).to contain_exactly(
525
+ :validate_score_range,
526
+ :validate_score_parity
527
+ )
528
+ end
529
+ end
691
530
  end