+3
-1
src/honk.gleam
+3
-1
src/honk.gleam
···
170
170
case json_helpers.get_field(lexicon.defs, "main") {
171
171
Some(main_def) -> {
172
172
let lex_ctx = context.with_current_lexicon(ctx, collection)
173
+
// Set the path to include the definition name
174
+
let def_ctx = context.with_path(lex_ctx, "defs.main")
173
175
// Validate the record data against the main definition
174
-
validation_primary_record.validate_data(record, main_def, lex_ctx)
176
+
validation_primary_record.validate_data(record, main_def, def_ctx)
175
177
}
176
178
None ->
177
179
Error(errors.invalid_schema(
+7
-4
src/honk/validation/field.gleam
+7
-4
src/honk/validation/field.gleam
···
356
356
list.try_fold(field_names, Nil, fn(_, field_name) {
357
357
case json_helpers.get_field(data, field_name) {
358
358
Some(_) -> Ok(Nil)
359
-
None ->
360
-
Error(errors.data_validation(
361
-
def_name <> ": required field '" <> field_name <> "' is missing",
362
-
))
359
+
None -> {
360
+
let message = case def_name {
361
+
"" -> "required field '" <> field_name <> "' is missing"
362
+
_ -> def_name <> ": required field '" <> field_name <> "' is missing"
363
+
}
364
+
Error(errors.data_validation(message))
365
+
}
363
366
}
364
367
})
365
368
}
+199
test/end_to_end_test.gleam
+199
test/end_to_end_test.gleam
···
5
5
import gleeunit
6
6
import gleeunit/should
7
7
import honk
8
+
import honk/errors
8
9
import honk/types.{DateTime, Uri}
9
10
10
11
pub fn main() {
···
327
328
honk.validate([lexicon])
328
329
|> should.be_ok
329
330
}
331
+
332
+
// Test missing required field error message with full defs.main path
333
+
pub fn validate_record_missing_required_field_message_test() {
334
+
let lexicon =
335
+
json.object([
336
+
#("lexicon", json.int(1)),
337
+
#("id", json.string("com.example.post")),
338
+
#(
339
+
"defs",
340
+
json.object([
341
+
#(
342
+
"main",
343
+
json.object([
344
+
#("type", json.string("record")),
345
+
#("key", json.string("tid")),
346
+
#(
347
+
"record",
348
+
json.object([
349
+
#("type", json.string("object")),
350
+
#("required", json.array([json.string("title")], fn(x) { x })),
351
+
#(
352
+
"properties",
353
+
json.object([
354
+
#(
355
+
"title",
356
+
json.object([#("type", json.string("string"))]),
357
+
),
358
+
]),
359
+
),
360
+
]),
361
+
),
362
+
]),
363
+
),
364
+
]),
365
+
),
366
+
])
367
+
368
+
let data = json.object([#("description", json.string("No title"))])
369
+
370
+
let assert Error(error) =
371
+
honk.validate_record([lexicon], "com.example.post", data)
372
+
373
+
let error_message = errors.to_string(error)
374
+
error_message
375
+
|> should.equal(
376
+
"Data validation failed: defs.main: required field 'title' is missing",
377
+
)
378
+
}
379
+
380
+
// Test missing required field in nested object with full path
381
+
pub fn validate_record_nested_missing_required_field_message_test() {
382
+
let lexicon =
383
+
json.object([
384
+
#("lexicon", json.int(1)),
385
+
#("id", json.string("com.example.post")),
386
+
#(
387
+
"defs",
388
+
json.object([
389
+
#(
390
+
"main",
391
+
json.object([
392
+
#("type", json.string("record")),
393
+
#("key", json.string("tid")),
394
+
#(
395
+
"record",
396
+
json.object([
397
+
#("type", json.string("object")),
398
+
#(
399
+
"properties",
400
+
json.object([
401
+
#(
402
+
"title",
403
+
json.object([#("type", json.string("string"))]),
404
+
),
405
+
#(
406
+
"metadata",
407
+
json.object([
408
+
#("type", json.string("object")),
409
+
#(
410
+
"required",
411
+
json.array([json.string("author")], fn(x) { x }),
412
+
),
413
+
#(
414
+
"properties",
415
+
json.object([
416
+
#(
417
+
"author",
418
+
json.object([#("type", json.string("string"))]),
419
+
),
420
+
]),
421
+
),
422
+
]),
423
+
),
424
+
]),
425
+
),
426
+
]),
427
+
),
428
+
]),
429
+
),
430
+
]),
431
+
),
432
+
])
433
+
434
+
let data =
435
+
json.object([
436
+
#("title", json.string("My Post")),
437
+
#("metadata", json.object([#("tags", json.string("tech"))])),
438
+
])
439
+
440
+
let assert Error(error) =
441
+
honk.validate_record([lexicon], "com.example.post", data)
442
+
443
+
let error_message = errors.to_string(error)
444
+
error_message
445
+
|> should.equal(
446
+
"Data validation failed: defs.main.metadata: required field 'author' is missing",
447
+
)
448
+
}
449
+
450
+
// Test schema validation error for non-main definition includes correct path
451
+
pub fn validate_schema_non_main_definition_error_test() {
452
+
let lexicon =
453
+
json.object([
454
+
#("lexicon", json.int(1)),
455
+
#("id", json.string("com.example.test")),
456
+
#(
457
+
"defs",
458
+
json.object([
459
+
#(
460
+
"objectDef",
461
+
json.object([
462
+
#("type", json.string("object")),
463
+
#(
464
+
"properties",
465
+
json.object([
466
+
#(
467
+
"fieldA",
468
+
json.object([
469
+
#("type", json.string("string")),
470
+
// Invalid: maxLength must be an integer, not a string
471
+
#("maxLength", json.string("300")),
472
+
]),
473
+
),
474
+
]),
475
+
),
476
+
]),
477
+
),
478
+
#(
479
+
"recordDef",
480
+
json.object([
481
+
#("type", json.string("record")),
482
+
#("key", json.string("tid")),
483
+
#(
484
+
"record",
485
+
json.object([
486
+
#("type", json.string("object")),
487
+
#(
488
+
"properties",
489
+
json.object([
490
+
#(
491
+
"fieldB",
492
+
json.object([
493
+
#("type", json.string("ref")),
494
+
// Invalid: missing required "ref" field for ref type
495
+
]),
496
+
),
497
+
]),
498
+
),
499
+
]),
500
+
),
501
+
]),
502
+
),
503
+
]),
504
+
),
505
+
])
506
+
507
+
let result = honk.validate([lexicon])
508
+
509
+
// Should have errors
510
+
result |> should.be_error
511
+
512
+
case result {
513
+
Error(error_map) -> {
514
+
// Get errors for this lexicon
515
+
case dict.get(error_map, "com.example.test") {
516
+
Ok(error_list) -> {
517
+
// Should have exactly one error from the recordDef (ref missing 'ref' field)
518
+
error_list
519
+
|> should.equal([
520
+
"com.example.test#recordDef: .record.properties.fieldB: ref missing required 'ref' field",
521
+
])
522
+
}
523
+
Error(_) -> should.fail()
524
+
}
525
+
}
526
+
Ok(_) -> should.fail()
527
+
}
528
+
}
+88
test/integration_test.gleam
+88
test/integration_test.gleam
···
1
1
import gleam/json
2
2
import gleeunit
3
3
import gleeunit/should
4
+
import honk/errors
4
5
import honk/validation/context
5
6
import honk/validation/primary/record
6
7
···
230
231
let result = record.validate_schema(schema, ctx)
231
232
result |> should.be_ok
232
233
}
234
+
235
+
// Test missing required field error message at record root level
236
+
pub fn record_missing_required_field_message_test() {
237
+
let schema =
238
+
json.object([
239
+
#("type", json.string("record")),
240
+
#("key", json.string("tid")),
241
+
#(
242
+
"record",
243
+
json.object([
244
+
#("type", json.string("object")),
245
+
#("required", json.array([json.string("title")], fn(x) { x })),
246
+
#(
247
+
"properties",
248
+
json.object([
249
+
#("title", json.object([#("type", json.string("string"))])),
250
+
]),
251
+
),
252
+
]),
253
+
),
254
+
])
255
+
256
+
let data = json.object([#("description", json.string("No title"))])
257
+
258
+
let assert Ok(ctx) = context.builder() |> context.build
259
+
let assert Error(error) = record.validate_data(data, schema, ctx)
260
+
261
+
let error_message = errors.to_string(error)
262
+
error_message
263
+
|> should.equal("Data validation failed: required field 'title' is missing")
264
+
}
265
+
266
+
// Test missing required field error message in nested object
267
+
pub fn record_nested_missing_required_field_message_test() {
268
+
let schema =
269
+
json.object([
270
+
#("type", json.string("record")),
271
+
#("key", json.string("tid")),
272
+
#(
273
+
"record",
274
+
json.object([
275
+
#("type", json.string("object")),
276
+
#(
277
+
"properties",
278
+
json.object([
279
+
#("title", json.object([#("type", json.string("string"))])),
280
+
#(
281
+
"metadata",
282
+
json.object([
283
+
#("type", json.string("object")),
284
+
#(
285
+
"required",
286
+
json.array([json.string("author")], fn(x) { x }),
287
+
),
288
+
#(
289
+
"properties",
290
+
json.object([
291
+
#(
292
+
"author",
293
+
json.object([#("type", json.string("string"))]),
294
+
),
295
+
#("tags", json.object([#("type", json.string("string"))])),
296
+
]),
297
+
),
298
+
]),
299
+
),
300
+
]),
301
+
),
302
+
]),
303
+
),
304
+
])
305
+
306
+
let data =
307
+
json.object([
308
+
#("title", json.string("My Post")),
309
+
#("metadata", json.object([#("tags", json.string("tech"))])),
310
+
])
311
+
312
+
let assert Ok(ctx) = context.builder() |> context.build
313
+
let assert Error(error) = record.validate_data(data, schema, ctx)
314
+
315
+
let error_message = errors.to_string(error)
316
+
error_message
317
+
|> should.equal(
318
+
"Data validation failed: metadata: required field 'author' is missing",
319
+
)
320
+
}
+25
test/object_validator_test.gleam
+25
test/object_validator_test.gleam
···
1
1
import gleam/json
2
2
import gleeunit
3
3
import gleeunit/should
4
+
import honk/errors
4
5
import honk/validation/context
5
6
import honk/validation/field
6
7
···
74
75
let result = field.validate_object_data(data, schema, ctx)
75
76
result |> should.be_error
76
77
}
78
+
79
+
// Test missing required field error message at root level (no path)
80
+
pub fn missing_required_field_message_root_test() {
81
+
let schema =
82
+
json.object([
83
+
#("type", json.string("object")),
84
+
#(
85
+
"properties",
86
+
json.object([
87
+
#("title", json.object([#("type", json.string("string"))])),
88
+
]),
89
+
),
90
+
#("required", json.array([json.string("title")], fn(x) { x })),
91
+
])
92
+
93
+
let data = json.object([#("other", json.string("value"))])
94
+
95
+
let assert Ok(ctx) = context.builder() |> context.build
96
+
let assert Error(error) = field.validate_object_data(data, schema, ctx)
97
+
98
+
let error_message = errors.to_string(error)
99
+
error_message
100
+
|> should.equal("Data validation failed: required field 'title' is missing")
101
+
}
+8
-6
test/params_validator_test.gleam
+8
-6
test/params_validator_test.gleam
···
436
436
437
437
let data =
438
438
json.object([
439
-
#("tags", json.array([json.string("foo"), json.string("bar")], fn(x) {
440
-
x
441
-
})),
439
+
#(
440
+
"tags",
441
+
json.array([json.string("foo"), json.string("bar")], fn(x) { x }),
442
+
),
442
443
])
443
444
444
445
let assert Ok(c) = context.builder() |> context.build()
···
561
562
// Array contains strings instead of integers
562
563
let data =
563
564
json.object([
564
-
#("ids", json.array([json.string("one"), json.string("two")], fn(x) {
565
-
x
566
-
})),
565
+
#(
566
+
"ids",
567
+
json.array([json.string("one"), json.string("two")], fn(x) { x }),
568
+
),
567
569
])
568
570
569
571
let assert Ok(c) = context.builder() |> context.build()