lluminary 0.1.4 → 0.2.1

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,10 +265,154 @@ 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")
268
+ it "supports hashes inside arrays" do
269
+ schema.array(:users) do
270
+ hash do
271
+ string :name
272
+ integer :age
273
+ end
274
+ end
275
+
276
+ expect(schema.fields[:users]).to eq(
277
+ {
278
+ type: :array,
279
+ description: nil,
280
+ element_type: {
281
+ type: :hash,
282
+ description: nil,
283
+ fields: {
284
+ name: {
285
+ type: :string,
286
+ description: nil
287
+ },
288
+ age: {
289
+ type: :integer,
290
+ description: nil
291
+ }
292
+ }
293
+ }
294
+ }
295
+ )
296
+ end
297
+ end
298
+
299
+ describe "#hash" do
300
+ it "requires a block for hash fields" do
301
+ expect { schema.hash(:config) }.to raise_error(
302
+ ArgumentError,
303
+ "Hash fields must be defined with a block"
304
+ )
305
+ end
306
+
307
+ it "adds a hash field with nested fields to the schema" do
308
+ schema.hash(:config, description: "Configuration") do
309
+ string :host, description: "The server hostname"
310
+ integer :port
311
+ end
312
+
313
+ expect(schema.fields).to eq(
314
+ {
315
+ config: {
316
+ type: :hash,
317
+ description: "Configuration",
318
+ fields: {
319
+ host: {
320
+ type: :string,
321
+ description: "The server hostname"
322
+ },
323
+ port: {
324
+ type: :integer,
325
+ description: nil
326
+ }
327
+ }
328
+ }
329
+ }
330
+ )
331
+ end
332
+
333
+ it "supports nested hashes" do
334
+ schema.hash(:config) do
335
+ string :name
336
+ hash :database do
337
+ string :host
338
+ integer :port
339
+ hash :credentials do
340
+ string :username
341
+ string :password
342
+ end
343
+ end
344
+ end
345
+
346
+ expect(schema.fields[:config]).to eq(
347
+ {
348
+ type: :hash,
349
+ description: nil,
350
+ fields: {
351
+ name: {
352
+ type: :string,
353
+ description: nil
354
+ },
355
+ database: {
356
+ type: :hash,
357
+ description: nil,
358
+ fields: {
359
+ host: {
360
+ type: :string,
361
+ description: nil
362
+ },
363
+ port: {
364
+ type: :integer,
365
+ description: nil
366
+ },
367
+ credentials: {
368
+ type: :hash,
369
+ description: nil,
370
+ fields: {
371
+ username: {
372
+ type: :string,
373
+ description: nil
374
+ },
375
+ password: {
376
+ type: :string,
377
+ description: nil
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ )
386
+ end
387
+
388
+ it "supports arrays inside hashes" do
389
+ schema.hash(:config) do
390
+ string :name
391
+ array :tags do
392
+ string
393
+ end
394
+ end
395
+
396
+ expect(schema.fields[:config]).to eq(
397
+ {
398
+ type: :hash,
399
+ description: nil,
400
+ fields: {
401
+ name: {
402
+ type: :string,
403
+ description: nil
404
+ },
405
+ tags: {
406
+ type: :array,
407
+ description: nil,
408
+ element_type: {
409
+ type: :string,
410
+ description: nil
411
+ }
412
+ }
413
+ }
414
+ }
415
+ )
220
416
  end
221
417
  end
222
418
 
@@ -254,136 +450,6 @@ RSpec.describe Lluminary::Schema do
254
450
  second_call = schema.fields
255
451
  expect(first_call).to be(second_call)
256
452
  end
257
-
258
- context "with datetime fields" do
259
- let(:schema) { described_class.new.tap { |s| s.datetime(:start_time) } }
260
-
261
- it "accepts DateTime values" do
262
- errors = schema.validate(start_time: DateTime.now)
263
- expect(errors).to be_empty
264
- end
265
-
266
- it "accepts nil values" do
267
- errors = schema.validate(start_time: nil)
268
- expect(errors).to be_empty
269
- end
270
-
271
- it "returns errors for non-DateTime values" do
272
- errors = schema.validate(start_time: "2024-01-01")
273
- expect(errors).to contain_exactly("Start time must be a DateTime")
274
- end
275
-
276
- it "can be required using presence validation" do
277
- schema.validates :start_time, presence: true
278
- errors = schema.validate(start_time: nil)
279
- expect(errors).to contain_exactly("Start time can't be blank")
280
- end
281
- end
282
- end
283
-
284
- describe "#validate" do
285
- let(:schema) do
286
- described_class.new.tap do |s|
287
- s.string(:name)
288
- s.integer(:age)
289
- end
290
- end
291
-
292
- it "returns no errors when all values match their field types" do
293
- errors = schema.validate(name: "John", age: 30)
294
- expect(errors).to be_empty
295
- end
296
-
297
- it "returns errors for type mismatches" do
298
- errors = schema.validate(name: 123, age: "30")
299
- expect(errors).to contain_exactly(
300
- "Name must be a String",
301
- "Age must be an Integer"
302
- )
303
- end
304
-
305
- context "with boolean fields" do
306
- let(:schema) { described_class.new.tap { |s| s.boolean(:active) } }
307
-
308
- it "accepts true values" do
309
- errors = schema.validate(active: true)
310
- expect(errors).to be_empty
311
- end
312
-
313
- it "accepts false values" do
314
- errors = schema.validate(active: false)
315
- expect(errors).to be_empty
316
- end
317
-
318
- it "accepts nil values" do
319
- errors = schema.validate(active: nil)
320
- expect(errors).to be_empty
321
- end
322
-
323
- it "returns errors for non-boolean values" do
324
- errors = schema.validate(active: "true")
325
- expect(errors).to contain_exactly("Active must be true or false")
326
-
327
- errors = schema.validate(active: 1)
328
- expect(errors).to contain_exactly("Active must be true or false")
329
- end
330
-
331
- it "can be required using presence validation" do
332
- schema.validates :active, presence: true
333
- errors = schema.validate(active: nil)
334
- expect(errors).to contain_exactly("Active can't be blank")
335
- end
336
- end
337
-
338
- context "with string fields" do
339
- let(:schema) { described_class.new.tap { |s| s.string(:name) } }
340
-
341
- it "accepts string values" do
342
- errors = schema.validate(name: "John")
343
- expect(errors).to be_empty
344
- end
345
-
346
- it "accepts nil values" do
347
- errors = schema.validate(name: nil)
348
- expect(errors).to be_empty
349
- end
350
-
351
- it "returns errors for non-string values" do
352
- errors = schema.validate(name: 123)
353
- expect(errors).to contain_exactly("Name must be a String")
354
- end
355
-
356
- it "can be required using presence validation" do
357
- schema.validates :name, presence: true
358
- errors = schema.validate(name: nil)
359
- expect(errors).to contain_exactly("Name can't be blank")
360
- end
361
- end
362
-
363
- context "with integer fields" do
364
- let(:schema) { described_class.new.tap { |s| s.integer(:age) } }
365
-
366
- it "accepts integer values" do
367
- errors = schema.validate(age: 30)
368
- expect(errors).to be_empty
369
- end
370
-
371
- it "accepts nil values" do
372
- errors = schema.validate(age: nil)
373
- expect(errors).to be_empty
374
- end
375
-
376
- it "returns errors for non-integer values" do
377
- errors = schema.validate(age: "30")
378
- expect(errors).to contain_exactly("Age must be an Integer")
379
- end
380
-
381
- it "can be required using presence validation" do
382
- schema.validates :age, presence: true
383
- errors = schema.validate(age: nil)
384
- expect(errors).to contain_exactly("Age can't be blank")
385
- end
386
- end
387
453
  end
388
454
 
389
455
  describe "ActiveModel validations" do
@@ -433,4 +499,32 @@ RSpec.describe Lluminary::Schema do
433
499
  expect(instance.valid?).to be true
434
500
  end
435
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
436
530
  end