atproto libraries implementation in ocaml
1(** Lexicon tests for AT Protocol.
2
3 Tests the Lexicon schema parser against the official interop test fixtures.
4*)
5
6open Atproto_lexicon
7
8(** Read fixture JSON file *)
9let read_fixture_json filename =
10 let path = "../fixtures/lexicon/" ^ filename in
11 let ic = open_in path in
12 let content = really_input_string ic (in_channel_length ic) in
13 close_in ic;
14 match Simdjsont.decode Simdjsont.Codec.value content with
15 | Ok json -> json
16 | Error msg -> failwith msg
17
18(** Read catalog lexicon file *)
19let read_catalog_file filename =
20 let path = "../fixtures/lexicon/catalog/" ^ filename in
21 Parser.of_file path
22
23(* === Parser tests === *)
24
25let test_valid_lexicons () =
26 let fixtures = read_fixture_json "lexicon-valid.json" in
27 match fixtures with
28 | Simdjsont.Json.Array items ->
29 List.iter
30 (fun item ->
31 match item with
32 | Simdjsont.Json.Object pairs -> (
33 let name =
34 match List.assoc_opt "name" pairs with
35 | Some (Simdjsont.Json.String s) -> s
36 | _ -> "unknown"
37 in
38 match List.assoc_opt "lexicon" pairs with
39 | Some lexicon_json -> (
40 let lexicon_str = Simdjsont.Json.to_string lexicon_json in
41 match Parser.of_string lexicon_str with
42 | Ok lexicon ->
43 Alcotest.(check bool)
44 (Printf.sprintf "valid: %s has id" name)
45 true
46 (String.length lexicon.id > 0)
47 | Error e ->
48 Alcotest.fail
49 (Printf.sprintf "failed to parse %s: %s" name
50 (Parser.error_to_string e)))
51 | None -> ())
52 | _ -> ())
53 items
54 | _ -> Alcotest.fail "Expected JSON array"
55
56let test_invalid_lexicons () =
57 let fixtures = read_fixture_json "lexicon-invalid.json" in
58 match fixtures with
59 | Simdjsont.Json.Array items ->
60 List.iter
61 (fun item ->
62 match item with
63 | Simdjsont.Json.Object pairs -> (
64 let name =
65 match List.assoc_opt "name" pairs with
66 | Some (Simdjsont.Json.String s) -> s
67 | _ -> "unknown"
68 in
69 match List.assoc_opt "lexicon" pairs with
70 | Some lexicon_json -> (
71 let lexicon_str = Simdjsont.Json.to_string lexicon_json in
72 match Parser.of_string lexicon_str with
73 | Ok _ ->
74 (* Some "invalid" lexicons may be parseable but semantically invalid *)
75 ()
76 | Error _ ->
77 (* Expected to fail *)
78 Alcotest.(check pass)
79 (Printf.sprintf "invalid: %s" name)
80 () ())
81 | None -> ())
82 | _ -> ())
83 items
84 | _ -> Alcotest.fail "Expected JSON array"
85
86(* === Catalog tests === *)
87
88let test_record_lexicon () =
89 match read_catalog_file "record.json" with
90 | Ok lexicon ->
91 Alcotest.(check string) "id" "example.lexicon.record" lexicon.id;
92 Alcotest.(check int) "version" 1 lexicon.version;
93 Alcotest.(check bool)
94 "has description" true
95 (Option.is_some lexicon.description);
96
97 (* Check main definition is a record *)
98 Alcotest.(check bool) "is record" true (Schema.is_record lexicon);
99
100 (* Check we have multiple defs *)
101 Alcotest.(check bool)
102 "has multiple defs" true
103 (List.length lexicon.defs > 1);
104
105 (* Check specific definitions exist *)
106 let def_names =
107 List.map (fun (d : Schema.named_definition) -> d.name) lexicon.defs
108 in
109 Alcotest.(check bool) "has main" true (List.mem "main" def_names);
110 Alcotest.(check bool)
111 "has stringFormats" true
112 (List.mem "stringFormats" def_names);
113 Alcotest.(check bool)
114 "has demoToken" true
115 (List.mem "demoToken" def_names)
116 | Error e -> Alcotest.fail (Parser.error_to_string e)
117
118let test_query_lexicon () =
119 match read_catalog_file "query.json" with
120 | Ok lexicon -> (
121 Alcotest.(check string) "id" "example.lexicon.query" lexicon.id;
122 Alcotest.(check bool) "is query" true (Schema.is_query lexicon);
123
124 (* Check main definition *)
125 match Schema.main_def lexicon with
126 | Some { def = Schema.Query q; _ } ->
127 Alcotest.(check bool)
128 "has parameters" true
129 (Option.is_some q.parameters);
130 Alcotest.(check bool) "has output" true (Option.is_some q.output);
131 Alcotest.(check int) "has 2 errors" 2 (List.length q.errors)
132 | _ -> Alcotest.fail "expected query")
133 | Error e -> Alcotest.fail (Parser.error_to_string e)
134
135let test_procedure_lexicon () =
136 match read_catalog_file "procedure.json" with
137 | Ok lexicon -> (
138 Alcotest.(check string) "id" "example.lexicon.procedure" lexicon.id;
139 Alcotest.(check bool) "is procedure" true (Schema.is_procedure lexicon);
140
141 match Schema.main_def lexicon with
142 | Some { def = Schema.Procedure p; _ } ->
143 Alcotest.(check bool)
144 "has parameters" true
145 (Option.is_some p.parameters);
146 Alcotest.(check bool) "has input" true (Option.is_some p.input);
147 Alcotest.(check bool) "has output" true (Option.is_some p.output)
148 | _ -> Alcotest.fail "expected procedure")
149 | Error e -> Alcotest.fail (Parser.error_to_string e)
150
151let test_subscription_lexicon () =
152 match read_catalog_file "subscription.json" with
153 | Ok lexicon -> (
154 Alcotest.(check string) "id" "example.lexicon.subscription" lexicon.id;
155 Alcotest.(check bool)
156 "is subscription" true
157 (Schema.is_subscription lexicon);
158
159 match Schema.main_def lexicon with
160 | Some { def = Schema.Subscription s; _ } ->
161 Alcotest.(check bool)
162 "has parameters" true
163 (Option.is_some s.parameters);
164 Alcotest.(check bool) "has message" true (Option.is_some s.message);
165 Alcotest.(check int) "has 1 error" 1 (List.length s.errors)
166 | _ -> Alcotest.fail "expected subscription")
167 | Error e -> Alcotest.fail (Parser.error_to_string e)
168
169let test_permission_set_lexicon () =
170 (* Test the permission-set.json catalog file *)
171 match read_catalog_file "permission-set.json" with
172 | Ok lexicon -> (
173 Alcotest.(check string) "id" "example.lexicon.permissionset" lexicon.id;
174
175 match Schema.main_def lexicon with
176 | Some { def = Schema.Permission_set ps; _ } ->
177 Alcotest.(check bool) "has title" true (Option.is_some ps.title);
178 Alcotest.(check int)
179 "has 6 permissions" 6
180 (List.length ps.permissions)
181 | _ -> Alcotest.fail "expected permission-set")
182 | Error e -> Alcotest.fail (Parser.error_to_string e)
183
184(* === Field type tests === *)
185
186(** Helper to find a property in a list *)
187let find_prop name (props : Schema.property list) : Schema.property option =
188 List.find_opt (fun (p : Schema.property) -> p.name = name) props
189
190let test_string_formats () =
191 match read_catalog_file "record.json" with
192 | Ok lexicon -> (
193 (* Find stringFormats def *)
194 let sf_def =
195 List.find_opt
196 (fun (d : Schema.named_definition) -> d.name = "stringFormats")
197 lexicon.defs
198 in
199 match sf_def with
200 | Some { def = Schema.Object_def obj; _ } ->
201 let prop_names =
202 List.map (fun (p : Schema.property) -> p.name) obj.properties
203 in
204 Alcotest.(check bool) "has did" true (List.mem "did" prop_names);
205 Alcotest.(check bool) "has handle" true (List.mem "handle" prop_names);
206 Alcotest.(check bool) "has nsid" true (List.mem "nsid" prop_names);
207 Alcotest.(check bool)
208 "has datetime" true
209 (List.mem "datetime" prop_names);
210 Alcotest.(check bool) "has tid" true (List.mem "tid" prop_names);
211 Alcotest.(check bool)
212 "has recordkey" true
213 (List.mem "recordkey" prop_names)
214 | _ -> Alcotest.fail "expected object def")
215 | Error e -> Alcotest.fail (Parser.error_to_string e)
216
217let test_union_types () =
218 match read_catalog_file "record.json" with
219 | Ok lexicon -> (
220 match Schema.main_def lexicon with
221 | Some { def = Schema.Record r; _ } -> (
222 (* Find union field *)
223 match find_prop "union" r.record.properties with
224 | Some { field = Schema.Union u; _ } -> (
225 Alcotest.(check int) "2 refs" 2 (List.length u.refs);
226 Alcotest.(check bool) "not closed" false u.closed;
227
228 (* Find closedUnion field *)
229 match find_prop "closedUnion" r.record.properties with
230 | Some { field = Schema.Union u2; _ } ->
231 Alcotest.(check bool) "is closed" true u2.closed
232 | _ -> Alcotest.fail "expected closed union field")
233 | _ -> Alcotest.fail "expected union field")
234 | _ -> Alcotest.fail "expected record")
235 | Error e -> Alcotest.fail (Parser.error_to_string e)
236
237let test_integer_constraints () =
238 match read_catalog_file "record.json" with
239 | Ok lexicon -> (
240 match Schema.main_def lexicon with
241 | Some { def = Schema.Record r; _ } -> (
242 (* Find rangeInteger field *)
243 match find_prop "rangeInteger" r.record.properties with
244 | Some { field = Schema.Primitive (Schema.Integer i); _ } -> (
245 Alcotest.(check (option int)) "minimum" (Some 10) i.minimum;
246 Alcotest.(check (option int)) "maximum" (Some 20) i.maximum;
247
248 (* Find enumInteger field *)
249 match find_prop "enumInteger" r.record.properties with
250 | Some { field = Schema.Primitive (Schema.Integer i2); _ } -> (
251 Alcotest.(check bool) "has enum" true (Option.is_some i2.enum);
252 match i2.enum with
253 | Some enums ->
254 Alcotest.(check int) "4 values" 4 (List.length enums)
255 | None -> Alcotest.fail "expected enum")
256 | _ -> Alcotest.fail "expected integer field")
257 | _ -> Alcotest.fail "expected integer field")
258 | _ -> Alcotest.fail "expected record")
259 | Error e -> Alcotest.fail (Parser.error_to_string e)
260
261(* === Validation tests === *)
262
263(** Helper to check if string contains substring *)
264let string_contains haystack needle =
265 let nlen = String.length needle in
266 let hlen = String.length haystack in
267 if nlen > hlen then false
268 else
269 let rec check i =
270 if i > hlen - nlen then false
271 else if String.sub haystack i nlen = needle then true
272 else check (i + 1)
273 in
274 check 0
275
276(** Get the record schema from the catalog *)
277let get_record_schema () =
278 match read_catalog_file "record.json" with
279 | Ok lexicon -> (
280 match Schema.main_def lexicon with
281 | Some { def = Schema.Record r; _ } -> Some r.record
282 | _ -> None)
283 | Error _ -> None
284
285(** Get the full lexicon for ref resolution *)
286let get_record_lexicon () = read_catalog_file "record.json"
287
288(** Create a ref resolver for the given lexicon. Resolves refs like
289 "example.lexicon.record#stringFormats" or "#demoObject" *)
290let make_resolver (lexicon : Schema.lexicon) : Validator.ref_resolver =
291 fun ref_str ->
292 (* Extract the def name from the ref *)
293 let def_name =
294 if String.contains ref_str '#' then
295 let idx = String.rindex ref_str '#' in
296 String.sub ref_str (idx + 1) (String.length ref_str - idx - 1)
297 else ref_str
298 in
299 (* Find the definition in the lexicon *)
300 let def_opt =
301 List.find_opt
302 (fun (d : Schema.named_definition) -> d.name = def_name)
303 lexicon.defs
304 in
305 match def_opt with
306 | Some { def = Schema.Object_def obj; _ } -> Some (Schema.Object obj)
307 | Some { def = Schema.Record r; _ } -> Some (Schema.Object r.record)
308 | _ -> None
309
310let test_valid_records () =
311 match (get_record_schema (), get_record_lexicon ()) with
312 | None, _ -> Alcotest.fail "could not load record schema"
313 | _, Error _ -> Alcotest.fail "could not load lexicon"
314 | Some schema, Ok lexicon -> (
315 let resolver = make_resolver lexicon in
316 let fixtures = read_fixture_json "record-data-valid.json" in
317 match fixtures with
318 | Simdjsont.Json.Array items ->
319 List.iter
320 (fun item ->
321 match item with
322 | Simdjsont.Json.Object pairs -> (
323 let name =
324 match List.assoc_opt "name" pairs with
325 | Some (Simdjsont.Json.String s) -> s
326 | _ -> "unknown"
327 in
328 match List.assoc_opt "data" pairs with
329 | Some data ->
330 let errors =
331 Validator.validate_record ~resolver ~path:[] schema data
332 in
333 if errors <> [] then
334 let err_strs =
335 List.map Validator.error_to_string errors
336 in
337 Alcotest.fail
338 (Printf.sprintf "valid record '%s' failed: %s" name
339 (String.concat "; " err_strs))
340 else
341 Alcotest.(check pass)
342 (Printf.sprintf "valid: %s" name)
343 () ()
344 | None -> ())
345 | _ -> ())
346 items
347 | _ -> Alcotest.fail "Expected JSON array")
348
349let test_invalid_records () =
350 match (get_record_schema (), get_record_lexicon ()) with
351 | None, _ -> Alcotest.fail "could not load record schema"
352 | _, Error _ -> Alcotest.fail "could not load lexicon"
353 | Some schema, Ok lexicon -> (
354 let resolver = make_resolver lexicon in
355 let fixtures = read_fixture_json "record-data-invalid.json" in
356 match fixtures with
357 | Simdjsont.Json.Array items ->
358 List.iter
359 (fun item ->
360 match item with
361 | Simdjsont.Json.Object pairs -> (
362 let name =
363 match List.assoc_opt "name" pairs with
364 | Some (Simdjsont.Json.String s) -> s
365 | _ -> "unknown"
366 in
367 match List.assoc_opt "data" pairs with
368 | Some data ->
369 let errors =
370 Validator.validate_record ~resolver ~path:[] schema data
371 in
372 if errors = [] then
373 Alcotest.fail
374 (Printf.sprintf
375 "invalid record '%s' should have errors" name)
376 else
377 Alcotest.(check pass)
378 (Printf.sprintf "invalid: %s" name)
379 () ()
380 | None -> ())
381 | _ -> ())
382 items
383 | _ -> Alcotest.fail "Expected JSON array")
384
385(* Test specific validation scenarios *)
386let test_required_field_validation () =
387 match get_record_schema () with
388 | None -> Alcotest.fail "could not load record schema"
389 | Some schema ->
390 (* Missing required 'integer' field *)
391 let data =
392 Simdjsont.Json.Object
393 [ ("$type", Simdjsont.Json.String "example.lexicon.record") ]
394 in
395 let errors = Validator.validate_record ~path:[] schema data in
396 Alcotest.(check bool) "has errors" true (errors <> []);
397 let has_required_error =
398 List.exists
399 (fun err ->
400 string_contains (Validator.error_to_string err) "required")
401 errors
402 in
403 Alcotest.(check bool) "has required field error" true has_required_error
404
405let test_type_validation () =
406 match get_record_schema () with
407 | None -> Alcotest.fail "could not load record schema"
408 | Some schema ->
409 (* Wrong type for integer field *)
410 let data =
411 Simdjsont.Json.Object
412 [
413 ("$type", Simdjsont.Json.String "example.lexicon.record");
414 ("integer", Simdjsont.Json.String "not-an-integer");
415 ]
416 in
417 let errors = Validator.validate_record ~path:[] schema data in
418 Alcotest.(check bool) "has errors" true (errors <> []);
419 let has_type_error =
420 List.exists
421 (fun err ->
422 string_contains (Validator.error_to_string err) "expected integer")
423 errors
424 in
425 Alcotest.(check bool) "has type error" true has_type_error
426
427let test_format_validation () =
428 match get_record_schema () with
429 | None -> Alcotest.fail "could not load record schema"
430 | Some schema ->
431 (* Invalid DID format in nested formats object *)
432 let data =
433 Simdjsont.Json.Object
434 [
435 ("$type", Simdjsont.Json.String "example.lexicon.record");
436 ("integer", Simdjsont.Json.Int 1L);
437 ( "formats",
438 Simdjsont.Json.Object
439 [ ("did", Simdjsont.Json.String "invalid-did") ] );
440 ]
441 in
442 let _errors = Validator.validate_record ~path:[] schema data in
443 (* Note: formats.did is a ref, which we currently don't resolve *)
444 (* So this test just ensures no crash *)
445 Alcotest.(check pass) "format validation runs" () ()
446
447let test_constraint_validation () =
448 match get_record_schema () with
449 | None -> Alcotest.fail "could not load record schema"
450 | Some schema ->
451 (* Integer out of range *)
452 let data =
453 Simdjsont.Json.Object
454 [
455 ("$type", Simdjsont.Json.String "example.lexicon.record");
456 ("integer", Simdjsont.Json.Int 1L);
457 ("rangeInteger", Simdjsont.Json.Int 9000L);
458 ]
459 in
460 let errors = Validator.validate_record ~path:[] schema data in
461 Alcotest.(check bool) "has range errors" true (errors <> [])
462
463(* === Test suites === *)
464
465let parser_tests =
466 [
467 Alcotest.test_case "valid lexicons" `Quick test_valid_lexicons;
468 Alcotest.test_case "invalid lexicons" `Quick test_invalid_lexicons;
469 ]
470
471let catalog_tests =
472 [
473 Alcotest.test_case "record lexicon" `Quick test_record_lexicon;
474 Alcotest.test_case "query lexicon" `Quick test_query_lexicon;
475 Alcotest.test_case "procedure lexicon" `Quick test_procedure_lexicon;
476 Alcotest.test_case "subscription lexicon" `Quick test_subscription_lexicon;
477 Alcotest.test_case "permission-set lexicon" `Quick
478 test_permission_set_lexicon;
479 ]
480
481let field_tests =
482 [
483 Alcotest.test_case "string formats" `Quick test_string_formats;
484 Alcotest.test_case "union types" `Quick test_union_types;
485 Alcotest.test_case "integer constraints" `Quick test_integer_constraints;
486 ]
487
488let validation_tests =
489 [
490 Alcotest.test_case "valid records" `Quick test_valid_records;
491 Alcotest.test_case "invalid records" `Quick test_invalid_records;
492 Alcotest.test_case "required field validation" `Quick
493 test_required_field_validation;
494 Alcotest.test_case "type validation" `Quick test_type_validation;
495 Alcotest.test_case "format validation" `Quick test_format_validation;
496 Alcotest.test_case "constraint validation" `Quick test_constraint_validation;
497 ]
498
499(* === Codegen tests === *)
500
501let test_nsid_to_module_name () =
502 Alcotest.(check string)
503 "simple nsid" "App_Bsky_Feed_Post"
504 (Codegen.nsid_to_module_name "app.bsky.feed.post");
505 Alcotest.(check string)
506 "com nsid" "Com_Atproto_Server_CreateSession"
507 (Codegen.nsid_to_module_name "com.atproto.server.createSession")
508
509let test_camel_to_snake () =
510 (* Use the internal function via field_to_ocaml *)
511 Alcotest.(check string)
512 "createdAt" "created_at"
513 (Codegen.ocaml_field_name "createdAt");
514 Alcotest.(check string) "userId" "user_id" (Codegen.ocaml_field_name "userId");
515 Alcotest.(check string) "simple" "simple" (Codegen.ocaml_field_name "simple")
516
517let test_escape_keywords () =
518 Alcotest.(check string)
519 "type keyword" "type_"
520 (Codegen.ocaml_field_name "type");
521 Alcotest.(check string)
522 "module keyword" "module_"
523 (Codegen.ocaml_field_name "module");
524 Alcotest.(check string)
525 "method keyword" "method_"
526 (Codegen.ocaml_field_name "method")
527
528let test_type_signature () =
529 let bool_type =
530 Schema.Primitive
531 (Schema.Boolean { description = None; default = None; const = None })
532 in
533 Alcotest.(check string)
534 "boolean type" "bool"
535 (Codegen.type_signature bool_type);
536
537 let int_type =
538 Schema.Primitive
539 (Schema.Integer
540 {
541 description = None;
542 default = None;
543 const = None;
544 enum = None;
545 minimum = None;
546 maximum = None;
547 })
548 in
549 Alcotest.(check string) "integer type" "int" (Codegen.type_signature int_type);
550
551 let str_type =
552 Schema.Primitive
553 (Schema.String
554 {
555 description = None;
556 default = None;
557 const = None;
558 enum = None;
559 known_values = None;
560 format = None;
561 min_length = None;
562 max_length = None;
563 min_graphemes = None;
564 max_graphemes = None;
565 })
566 in
567 Alcotest.(check string)
568 "string type" "string"
569 (Codegen.type_signature str_type);
570
571 let arr_type =
572 Schema.Array
573 {
574 description = None;
575 items = str_type;
576 min_length = None;
577 max_length = None;
578 }
579 in
580 Alcotest.(check string)
581 "array type" "string list"
582 (Codegen.type_signature arr_type)
583
584let test_generate_record () =
585 match read_catalog_file "record.json" with
586 | Ok lexicon -> (
587 match Codegen.generate lexicon with
588 | Ok code ->
589 (* Check that generated code contains expected elements *)
590 Alcotest.(check bool)
591 "has module" true
592 (String.length code > 0
593 && String.sub code 0 (min 10 (String.length code)) <> "");
594 Alcotest.(check bool) "has type t" true (String.length code > 50)
595 (* Simple sanity check *)
596 | Error e -> Alcotest.fail (Codegen.error_to_string e))
597 | Error e -> Alcotest.fail (Parser.error_to_string e)
598
599let test_generate_query () =
600 match read_catalog_file "query.json" with
601 | Ok lexicon -> (
602 match Codegen.generate lexicon with
603 | Ok code ->
604 Alcotest.(check bool) "generates code" true (String.length code > 0)
605 | Error e -> Alcotest.fail (Codegen.error_to_string e))
606 | Error e -> Alcotest.fail (Parser.error_to_string e)
607
608let test_generate_procedure () =
609 match read_catalog_file "procedure.json" with
610 | Ok lexicon -> (
611 match Codegen.generate lexicon with
612 | Ok code ->
613 Alcotest.(check bool) "generates code" true (String.length code > 0)
614 | Error e -> Alcotest.fail (Codegen.error_to_string e))
615 | Error e -> Alcotest.fail (Parser.error_to_string e)
616
617let test_generate_subscription () =
618 match read_catalog_file "subscription.json" with
619 | Ok lexicon -> (
620 match Codegen.generate lexicon with
621 | Error (Codegen.Unsupported_definition _) ->
622 (* Expected - subscriptions not supported yet *)
623 ()
624 | Error e -> Alcotest.fail ("Wrong error: " ^ Codegen.error_to_string e)
625 | Ok _ -> Alcotest.fail "Expected unsupported error")
626 | Error e -> Alcotest.fail (Parser.error_to_string e)
627
628let test_generate_all () =
629 let lexicons =
630 [
631 read_catalog_file "record.json";
632 read_catalog_file "query.json";
633 read_catalog_file "procedure.json";
634 ]
635 in
636 let parsed =
637 List.filter_map (function Ok l -> Some l | Error _ -> None) lexicons
638 in
639 match Codegen.generate_all parsed with
640 | Ok code ->
641 Alcotest.(check bool)
642 "generates multiple modules" true
643 (String.length code > 100)
644 | Error e -> Alcotest.fail (Codegen.error_to_string e)
645
646let test_error_to_string () =
647 let errors =
648 [
649 Codegen.No_main_definition;
650 Codegen.Unsupported_definition "test";
651 Codegen.Generation_error "test";
652 ]
653 in
654 List.iter
655 (fun e ->
656 let s = Codegen.error_to_string e in
657 Alcotest.(check bool) "error string not empty" true (String.length s > 0))
658 errors
659
660let codegen_tests =
661 [
662 Alcotest.test_case "nsid to module name" `Quick test_nsid_to_module_name;
663 Alcotest.test_case "camel to snake" `Quick test_camel_to_snake;
664 Alcotest.test_case "escape keywords" `Quick test_escape_keywords;
665 Alcotest.test_case "type signature" `Quick test_type_signature;
666 Alcotest.test_case "generate record" `Quick test_generate_record;
667 Alcotest.test_case "generate query" `Quick test_generate_query;
668 Alcotest.test_case "generate procedure" `Quick test_generate_procedure;
669 Alcotest.test_case "generate subscription" `Quick test_generate_subscription;
670 Alcotest.test_case "generate all" `Quick test_generate_all;
671 Alcotest.test_case "error to string" `Quick test_error_to_string;
672 ]
673
674let () =
675 Alcotest.run "atproto-lexicon"
676 [
677 ("parser", parser_tests);
678 ("catalog", catalog_tests);
679 ("fields", field_tests);
680 ("validation", validation_tests);
681 ("codegen", codegen_tests);
682 ]