+41
-15
src/honk.gleam
+41
-15
src/honk.gleam
···
2
2
3
3
import gleam/dict.{type Dict}
4
4
import gleam/json.{type Json}
5
+
import gleam/list
5
6
import gleam/option.{None, Some}
6
7
import gleam/result
7
8
import honk/errors
···
33
34
pub type ValidationError =
34
35
errors.ValidationError
35
36
36
-
/// Main validation function for lexicon documents
37
-
/// Returns Ok(Nil) if all lexicons are valid
38
-
/// Returns Error with a map of lexicon ID to list of error messages
37
+
/// Validates lexicon documents
38
+
///
39
+
/// Validates lexicon structure (id, defs) and ALL definitions within each lexicon.
40
+
/// Each definition in the defs object is validated according to its type.
41
+
///
42
+
/// Returns Ok(Nil) if all lexicons and their definitions are valid.
43
+
/// Returns Error with a map of lexicon ID to list of error messages.
44
+
/// Error messages include the definition name (e.g., "lex.id#defName: error").
39
45
pub fn validate(lexicons: List(Json)) -> Result(Nil, Dict(String, List(String))) {
40
46
// Build validation context
41
47
let builder_result =
···
46
52
Ok(builder) ->
47
53
case context.build(builder) {
48
54
Ok(ctx) -> {
49
-
// Validate each lexicon's main definition
55
+
// Validate ALL definitions in each lexicon
50
56
let error_map =
51
57
dict.fold(ctx.lexicons, dict.new(), fn(errors, lex_id, lexicon) {
52
-
// Validate the main definition if it exists
53
-
case json_helpers.get_field(lexicon.defs, "main") {
54
-
Some(main_def) -> {
55
-
let lex_ctx = context.with_current_lexicon(ctx, lex_id)
56
-
case validate_definition(main_def, lex_ctx) {
57
-
Ok(_) -> errors
58
-
Error(e) ->
59
-
dict.insert(errors, lex_id, [errors.to_string(e)])
58
+
// Get all definition names from the defs object
59
+
let def_keys = json_helpers.get_keys(lexicon.defs)
60
+
let lex_ctx = context.with_current_lexicon(ctx, lex_id)
61
+
62
+
// Validate each definition
63
+
list.fold(def_keys, errors, fn(errors_acc, def_name) {
64
+
case json_helpers.get_field(lexicon.defs, def_name) {
65
+
Some(def) -> {
66
+
case validate_definition(def, lex_ctx) {
67
+
Ok(_) -> errors_acc
68
+
Error(e) -> {
69
+
// Include def name in error for better context
70
+
let error_msg =
71
+
lex_id
72
+
<> "#"
73
+
<> def_name
74
+
<> ": "
75
+
<> errors.to_string(e)
76
+
case dict.get(errors_acc, lex_id) {
77
+
Ok(existing_errors) ->
78
+
dict.insert(errors_acc, lex_id, [
79
+
error_msg,
80
+
..existing_errors
81
+
])
82
+
Error(_) ->
83
+
dict.insert(errors_acc, lex_id, [error_msg])
84
+
}
85
+
}
86
+
}
60
87
}
88
+
None -> errors_acc
61
89
}
62
-
None -> errors
63
-
// No main definition is OK
64
-
}
90
+
})
65
91
})
66
92
67
93
case dict.is_empty(error_map) {
+125
test/end_to_end_test.gleam
+125
test/end_to_end_test.gleam
···
1
+
import gleam/dict
1
2
import gleam/json
3
+
import gleam/list
4
+
import gleam/string
2
5
import gleeunit
3
6
import gleeunit/should
4
7
import honk
···
202
205
honk.validate_string_format("https://example.com", Uri)
203
206
|> should.be_ok
204
207
}
208
+
209
+
// Test lexicon with multiple valid definitions
210
+
pub fn validate_lexicon_multiple_defs_test() {
211
+
let lexicon =
212
+
json.object([
213
+
#("lexicon", json.int(1)),
214
+
#("id", json.string("com.example.multi")),
215
+
#(
216
+
"defs",
217
+
json.object([
218
+
#(
219
+
"main",
220
+
json.object([
221
+
#("type", json.string("record")),
222
+
#("key", json.string("tid")),
223
+
#(
224
+
"record",
225
+
json.object([
226
+
#("type", json.string("object")),
227
+
#("properties", json.object([])),
228
+
]),
229
+
),
230
+
]),
231
+
),
232
+
#(
233
+
"stringFormats",
234
+
json.object([
235
+
#("type", json.string("object")),
236
+
#("properties", json.object([])),
237
+
]),
238
+
),
239
+
#("additionalType", json.object([#("type", json.string("string"))])),
240
+
]),
241
+
),
242
+
])
243
+
244
+
honk.validate([lexicon])
245
+
|> should.be_ok
246
+
}
247
+
248
+
// Test lexicon with only non-main definitions
249
+
pub fn validate_lexicon_no_main_def_test() {
250
+
let lexicon =
251
+
json.object([
252
+
#("lexicon", json.int(1)),
253
+
#("id", json.string("com.example.nomain")),
254
+
#(
255
+
"defs",
256
+
json.object([
257
+
#("customType", json.object([#("type", json.string("string"))])),
258
+
#("anotherType", json.object([#("type", json.string("integer"))])),
259
+
]),
260
+
),
261
+
])
262
+
263
+
honk.validate([lexicon])
264
+
|> should.be_ok
265
+
}
266
+
267
+
// Test lexicon with invalid non-main definition
268
+
pub fn validate_lexicon_invalid_non_main_def_test() {
269
+
let lexicon =
270
+
json.object([
271
+
#("lexicon", json.int(1)),
272
+
#("id", json.string("com.example.invalid")),
273
+
#(
274
+
"defs",
275
+
json.object([
276
+
#(
277
+
"main",
278
+
json.object([
279
+
#("type", json.string("record")),
280
+
#("key", json.string("tid")),
281
+
#(
282
+
"record",
283
+
json.object([
284
+
#("type", json.string("object")),
285
+
#("properties", json.object([])),
286
+
]),
287
+
),
288
+
]),
289
+
),
290
+
#(
291
+
"badDef",
292
+
json.object([
293
+
#("type", json.string("string")),
294
+
#("minLength", json.int(10)),
295
+
#("maxLength", json.int(5)),
296
+
]),
297
+
),
298
+
]),
299
+
),
300
+
])
301
+
302
+
case honk.validate([lexicon]) {
303
+
Error(error_map) -> {
304
+
// Should have error for this lexicon
305
+
case dict.get(error_map, "com.example.invalid") {
306
+
Ok(errors) -> {
307
+
// Error message should include the def name
308
+
list.any(errors, fn(msg) { string.contains(msg, "#badDef") })
309
+
|> should.be_true
310
+
}
311
+
Error(_) -> panic as "Expected error for com.example.invalid"
312
+
}
313
+
}
314
+
Ok(_) -> panic as "Expected validation to fail"
315
+
}
316
+
}
317
+
318
+
// Test empty defs object
319
+
pub fn validate_lexicon_empty_defs_test() {
320
+
let lexicon =
321
+
json.object([
322
+
#("lexicon", json.int(1)),
323
+
#("id", json.string("com.example.empty")),
324
+
#("defs", json.object([])),
325
+
])
326
+
327
+
honk.validate([lexicon])
328
+
|> should.be_ok
329
+
}