An ATProto Lexicon validator for Gleam.
1import gleam/json
2import gleeunit
3import gleeunit/should
4import honk/validation/context
5import honk/validation/primary/params
6
7pub fn main() {
8 gleeunit.main()
9}
10
11// Test valid params with boolean property
12pub fn valid_params_boolean_test() {
13 let schema =
14 json.object([
15 #("type", json.string("params")),
16 #(
17 "properties",
18 json.object([
19 #(
20 "isPublic",
21 json.object([
22 #("type", json.string("boolean")),
23 #("description", json.string("Whether the item is public")),
24 ]),
25 ),
26 ]),
27 ),
28 ])
29
30 let ctx = context.builder() |> context.build()
31 case ctx {
32 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
33 Error(_) -> should.fail()
34 }
35}
36
37// Test valid params with multiple property types
38pub fn valid_params_multiple_types_test() {
39 let schema =
40 json.object([
41 #("type", json.string("params")),
42 #(
43 "properties",
44 json.object([
45 #(
46 "limit",
47 json.object([
48 #("type", json.string("integer")),
49 #("minimum", json.int(1)),
50 #("maximum", json.int(100)),
51 ]),
52 ),
53 #(
54 "cursor",
55 json.object([
56 #("type", json.string("string")),
57 #("description", json.string("Pagination cursor")),
58 ]),
59 ),
60 #("includeReplies", json.object([#("type", json.string("boolean"))])),
61 ]),
62 ),
63 ])
64
65 let ctx = context.builder() |> context.build()
66 case ctx {
67 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
68 Error(_) -> should.fail()
69 }
70}
71
72// Test valid params with array property
73pub fn valid_params_with_array_test() {
74 let schema =
75 json.object([
76 #("type", json.string("params")),
77 #(
78 "properties",
79 json.object([
80 #(
81 "tags",
82 json.object([
83 #("type", json.string("array")),
84 #(
85 "items",
86 json.object([
87 #("type", json.string("string")),
88 #("maxLength", json.int(50)),
89 ]),
90 ),
91 ]),
92 ),
93 ]),
94 ),
95 ])
96
97 let ctx = context.builder() |> context.build()
98 case ctx {
99 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
100 Error(_) -> should.fail()
101 }
102}
103
104// Test valid params with required fields
105pub fn valid_params_with_required_test() {
106 let schema =
107 json.object([
108 #("type", json.string("params")),
109 #(
110 "properties",
111 json.object([
112 #(
113 "repo",
114 json.object([
115 #("type", json.string("string")),
116 #("format", json.string("at-identifier")),
117 ]),
118 ),
119 #(
120 "collection",
121 json.object([
122 #("type", json.string("string")),
123 #("format", json.string("nsid")),
124 ]),
125 ),
126 ]),
127 ),
128 #("required", json.array([json.string("repo")], fn(x) { x })),
129 ])
130
131 let ctx = context.builder() |> context.build()
132 case ctx {
133 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
134 Error(_) -> should.fail()
135 }
136}
137
138// Test valid params with unknown type
139pub fn valid_params_with_unknown_test() {
140 let schema =
141 json.object([
142 #("type", json.string("params")),
143 #(
144 "properties",
145 json.object([
146 #("metadata", json.object([#("type", json.string("unknown"))])),
147 ]),
148 ),
149 ])
150
151 let ctx = context.builder() |> context.build()
152 case ctx {
153 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
154 Error(_) -> should.fail()
155 }
156}
157
158// Test invalid: params with object property (not allowed)
159pub fn invalid_params_object_property_test() {
160 let schema =
161 json.object([
162 #("type", json.string("params")),
163 #(
164 "properties",
165 json.object([
166 #(
167 "filter",
168 json.object([
169 #("type", json.string("object")),
170 #("properties", json.object([])),
171 ]),
172 ),
173 ]),
174 ),
175 ])
176
177 let assert Ok(c) = context.builder() |> context.build()
178 params.validate_schema(schema, c) |> should.be_error
179}
180
181// Test invalid: params with blob property (not allowed)
182pub fn invalid_params_blob_property_test() {
183 let schema =
184 json.object([
185 #("type", json.string("params")),
186 #(
187 "properties",
188 json.object([
189 #(
190 "avatar",
191 json.object([
192 #("type", json.string("blob")),
193 #("accept", json.array([json.string("image/*")], fn(x) { x })),
194 ]),
195 ),
196 ]),
197 ),
198 ])
199
200 let assert Ok(c) = context.builder() |> context.build()
201 params.validate_schema(schema, c) |> should.be_error
202}
203
204// Test invalid: required field not in properties
205pub fn invalid_params_required_not_in_properties_test() {
206 let schema =
207 json.object([
208 #("type", json.string("params")),
209 #(
210 "properties",
211 json.object([
212 #("limit", json.object([#("type", json.string("integer"))])),
213 ]),
214 ),
215 #("required", json.array([json.string("cursor")], fn(x) { x })),
216 ])
217
218 let assert Ok(c) = context.builder() |> context.build()
219 params.validate_schema(schema, c) |> should.be_error
220}
221
222// Test invalid: empty property name
223pub fn invalid_params_empty_property_name_test() {
224 let schema =
225 json.object([
226 #("type", json.string("params")),
227 #(
228 "properties",
229 json.object([
230 #("", json.object([#("type", json.string("string"))])),
231 ]),
232 ),
233 ])
234
235 let assert Ok(c) = context.builder() |> context.build()
236 params.validate_schema(schema, c) |> should.be_error
237}
238
239// Test invalid: array with object items (not allowed)
240pub fn invalid_params_array_of_objects_test() {
241 let schema =
242 json.object([
243 #("type", json.string("params")),
244 #(
245 "properties",
246 json.object([
247 #(
248 "filters",
249 json.object([
250 #("type", json.string("array")),
251 #(
252 "items",
253 json.object([
254 #("type", json.string("object")),
255 #("properties", json.object([])),
256 ]),
257 ),
258 ]),
259 ),
260 ]),
261 ),
262 ])
263
264 let assert Ok(c) = context.builder() |> context.build()
265 params.validate_schema(schema, c) |> should.be_error
266}
267
268// Test invalid: wrong type (not "params")
269pub fn invalid_params_wrong_type_test() {
270 let schema =
271 json.object([
272 #("type", json.string("object")),
273 #("properties", json.object([])),
274 ])
275
276 let assert Ok(c) = context.builder() |> context.build()
277 params.validate_schema(schema, c) |> should.be_error
278}
279
280// Test valid: array of integers
281pub fn valid_params_array_of_integers_test() {
282 let schema =
283 json.object([
284 #("type", json.string("params")),
285 #(
286 "properties",
287 json.object([
288 #(
289 "ids",
290 json.object([
291 #("type", json.string("array")),
292 #(
293 "items",
294 json.object([
295 #("type", json.string("integer")),
296 #("minimum", json.int(1)),
297 ]),
298 ),
299 ]),
300 ),
301 ]),
302 ),
303 ])
304
305 let ctx = context.builder() |> context.build()
306 case ctx {
307 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
308 Error(_) -> should.fail()
309 }
310}
311
312// Test valid: array of unknown
313pub fn valid_params_array_of_unknown_test() {
314 let schema =
315 json.object([
316 #("type", json.string("params")),
317 #(
318 "properties",
319 json.object([
320 #(
321 "data",
322 json.object([
323 #("type", json.string("array")),
324 #("items", json.object([#("type", json.string("unknown"))])),
325 ]),
326 ),
327 ]),
328 ),
329 ])
330
331 let ctx = context.builder() |> context.build()
332 case ctx {
333 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok
334 Error(_) -> should.fail()
335 }
336}
337
338// ==================== DATA VALIDATION TESTS ====================
339
340// Test valid data with required parameters
341pub fn valid_data_with_required_params_test() {
342 let schema =
343 json.object([
344 #("type", json.string("params")),
345 #(
346 "properties",
347 json.object([
348 #("repo", json.object([#("type", json.string("string"))])),
349 #("limit", json.object([#("type", json.string("integer"))])),
350 ]),
351 ),
352 #(
353 "required",
354 json.array([json.string("repo"), json.string("limit")], fn(x) { x }),
355 ),
356 ])
357
358 let data =
359 json.object([
360 #("repo", json.string("alice.bsky.social")),
361 #("limit", json.int(50)),
362 ])
363
364 let assert Ok(c) = context.builder() |> context.build()
365 params.validate_data(data, schema, c) |> should.be_ok
366}
367
368// Test valid data with optional parameters
369pub fn valid_data_with_optional_params_test() {
370 let schema =
371 json.object([
372 #("type", json.string("params")),
373 #(
374 "properties",
375 json.object([
376 #("repo", json.object([#("type", json.string("string"))])),
377 #("cursor", json.object([#("type", json.string("string"))])),
378 ]),
379 ),
380 #("required", json.array([json.string("repo")], fn(x) { x })),
381 ])
382
383 // Data has required param but not optional cursor
384 let data = json.object([#("repo", json.string("alice.bsky.social"))])
385
386 let assert Ok(c) = context.builder() |> context.build()
387 params.validate_data(data, schema, c) |> should.be_ok
388}
389
390// Test valid data with all parameter types
391pub fn valid_data_all_types_test() {
392 let schema =
393 json.object([
394 #("type", json.string("params")),
395 #(
396 "properties",
397 json.object([
398 #("name", json.object([#("type", json.string("string"))])),
399 #("count", json.object([#("type", json.string("integer"))])),
400 #("enabled", json.object([#("type", json.string("boolean"))])),
401 #("metadata", json.object([#("type", json.string("unknown"))])),
402 ]),
403 ),
404 ])
405
406 let data =
407 json.object([
408 #("name", json.string("test")),
409 #("count", json.int(42)),
410 #("enabled", json.bool(True)),
411 #("metadata", json.object([#("key", json.string("value"))])),
412 ])
413
414 let assert Ok(c) = context.builder() |> context.build()
415 params.validate_data(data, schema, c) |> should.be_ok
416}
417
418// Test valid data with array parameter
419pub fn valid_data_with_array_test() {
420 let schema =
421 json.object([
422 #("type", json.string("params")),
423 #(
424 "properties",
425 json.object([
426 #(
427 "tags",
428 json.object([
429 #("type", json.string("array")),
430 #("items", json.object([#("type", json.string("string"))])),
431 ]),
432 ),
433 ]),
434 ),
435 ])
436
437 let data =
438 json.object([
439 #(
440 "tags",
441 json.array([json.string("foo"), json.string("bar")], fn(x) { x }),
442 ),
443 ])
444
445 let assert Ok(c) = context.builder() |> context.build()
446 params.validate_data(data, schema, c) |> should.be_ok
447}
448
449// Test invalid data: missing required parameter
450pub fn invalid_data_missing_required_test() {
451 let schema =
452 json.object([
453 #("type", json.string("params")),
454 #(
455 "properties",
456 json.object([
457 #("repo", json.object([#("type", json.string("string"))])),
458 #("limit", json.object([#("type", json.string("integer"))])),
459 ]),
460 ),
461 #("required", json.array([json.string("repo")], fn(x) { x })),
462 ])
463
464 // Data is missing required "repo" parameter
465 let data = json.object([#("limit", json.int(50))])
466
467 let assert Ok(c) = context.builder() |> context.build()
468 params.validate_data(data, schema, c) |> should.be_error
469}
470
471// Test invalid data: wrong type for parameter
472pub fn invalid_data_wrong_type_test() {
473 let schema =
474 json.object([
475 #("type", json.string("params")),
476 #(
477 "properties",
478 json.object([
479 #("limit", json.object([#("type", json.string("integer"))])),
480 ]),
481 ),
482 ])
483
484 // limit should be integer but is string
485 let data = json.object([#("limit", json.string("not a number"))])
486
487 let assert Ok(c) = context.builder() |> context.build()
488 params.validate_data(data, schema, c) |> should.be_error
489}
490
491// Test invalid data: string exceeds maxLength
492pub fn invalid_data_string_too_long_test() {
493 let schema =
494 json.object([
495 #("type", json.string("params")),
496 #(
497 "properties",
498 json.object([
499 #(
500 "name",
501 json.object([
502 #("type", json.string("string")),
503 #("maxLength", json.int(5)),
504 ]),
505 ),
506 ]),
507 ),
508 ])
509
510 // name is longer than maxLength of 5
511 let data = json.object([#("name", json.string("toolongname"))])
512
513 let assert Ok(c) = context.builder() |> context.build()
514 params.validate_data(data, schema, c) |> should.be_error
515}
516
517// Test invalid data: integer below minimum
518pub fn invalid_data_integer_below_minimum_test() {
519 let schema =
520 json.object([
521 #("type", json.string("params")),
522 #(
523 "properties",
524 json.object([
525 #(
526 "count",
527 json.object([
528 #("type", json.string("integer")),
529 #("minimum", json.int(1)),
530 ]),
531 ),
532 ]),
533 ),
534 ])
535
536 // count is below minimum of 1
537 let data = json.object([#("count", json.int(0))])
538
539 let assert Ok(c) = context.builder() |> context.build()
540 params.validate_data(data, schema, c) |> should.be_error
541}
542
543// Test invalid data: array with wrong item type
544pub fn invalid_data_array_wrong_item_type_test() {
545 let schema =
546 json.object([
547 #("type", json.string("params")),
548 #(
549 "properties",
550 json.object([
551 #(
552 "ids",
553 json.object([
554 #("type", json.string("array")),
555 #("items", json.object([#("type", json.string("integer"))])),
556 ]),
557 ),
558 ]),
559 ),
560 ])
561
562 // Array contains strings instead of integers
563 let data =
564 json.object([
565 #(
566 "ids",
567 json.array([json.string("one"), json.string("two")], fn(x) { x }),
568 ),
569 ])
570
571 let assert Ok(c) = context.builder() |> context.build()
572 params.validate_data(data, schema, c) |> should.be_error
573}
574
575// Test valid data with no properties defined (empty schema)
576pub fn valid_data_empty_schema_test() {
577 let schema = json.object([#("type", json.string("params"))])
578
579 let data = json.object([])
580
581 let assert Ok(c) = context.builder() |> context.build()
582 params.validate_data(data, schema, c) |> should.be_ok
583}
584
585// Test valid data allows unknown parameters not in schema
586pub fn valid_data_unknown_parameters_allowed_test() {
587 let schema =
588 json.object([
589 #("type", json.string("params")),
590 #(
591 "properties",
592 json.object([
593 #("repo", json.object([#("type", json.string("string"))])),
594 ]),
595 ),
596 ])
597
598 // Data has "extra" parameter not in schema
599 let data =
600 json.object([
601 #("repo", json.string("alice.bsky.social")),
602 #("extra", json.string("allowed")),
603 ])
604
605 let assert Ok(c) = context.builder() |> context.build()
606 params.validate_data(data, schema, c) |> should.be_ok
607}