objective categorical abstract machine language personal data server
at main 564 lines 20 kB view raw
1open Alcotest 2open Hermes_cli 3 4let contains s1 s2 = 5 try 6 let len = String.length s2 in 7 for i = 0 to String.length s1 - len do 8 if String.sub s1 i len = s2 then raise Exit 9 done ; 10 false 11 with Exit -> true 12 13(* create a simple lexicon doc for testing *) 14let make_lexicon id defs = 15 {Lexicon_types.lexicon= 1; id; revision= None; description= None; defs} 16 17let make_def name type_def = {Lexicon_types.name; type_def} 18 19let make_object_spec properties required = 20 { Lexicon_types.properties 21 ; required= Some required 22 ; nullable= None 23 ; description= None } 24 25let make_property type_def = {Lexicon_types.type_def; description= None} 26 27let string_type = 28 Lexicon_types.String 29 { format= None 30 ; min_length= None 31 ; max_length= None 32 ; min_graphemes= None 33 ; max_graphemes= None 34 ; known_values= None 35 ; enum= None 36 ; const= None 37 ; default= None 38 ; description= None } 39 40let int_type = 41 Lexicon_types.Integer 42 { minimum= None 43 ; maximum= None 44 ; enum= None 45 ; const= None 46 ; default= None 47 ; description= None } 48 49let[@warning "-32"] _bool_type = 50 Lexicon_types.Boolean {const= None; default= None; description= None} 51 52(* test generating a simple object type *) 53let test_gen_simple_object () = 54 let obj_spec = 55 make_object_spec 56 [("name", make_property string_type); ("age", make_property int_type)] 57 ["name"; "age"] 58 in 59 let doc = 60 make_lexicon "com.example.test" 61 [make_def "main" (Lexicon_types.Object obj_spec)] 62 in 63 let code = Codegen.gen_lexicon_module doc in 64 check bool "contains type main" true (contains code "type main =") ; 65 check bool "contains name field" true (contains code "name: string") ; 66 check bool "contains age field" true (contains code "age: int") ; 67 check bool "contains deriving" true (contains code "[@@deriving yojson") 68 69(* test generating object with optional fields *) 70let test_gen_optional_fields () = 71 let obj_spec = 72 make_object_spec 73 [ ("required_field", make_property string_type) 74 ; ("optional_field", make_property string_type) ] 75 ["required_field"] 76 (* only required_field is required *) 77 in 78 let doc = 79 make_lexicon "com.example.optional" 80 [make_def "main" (Lexicon_types.Object obj_spec)] 81 in 82 let code = Codegen.gen_lexicon_module doc in 83 check bool "required not option" true 84 (contains code "required_field: string;") ; 85 check bool "optional is option" true 86 (contains code "optional_field: string option") 87 88(* test generating with key annotation *) 89let test_gen_key_annotation () = 90 let obj_spec = 91 make_object_spec [("firstName", make_property string_type)] ["firstName"] 92 in 93 let doc = 94 make_lexicon "com.example.key" 95 [make_def "main" (Lexicon_types.Object obj_spec)] 96 in 97 let code = Codegen.gen_lexicon_module doc in 98 check bool "has snake_case field" true (contains code "first_name:") ; 99 check bool "has key annotation" true (contains code "[@key \"firstName\"]") 100 101(* test generating union type *) 102let test_gen_union_type () = 103 let union_spec = 104 { Lexicon_types.refs= ["#typeA"; "#typeB"] 105 ; closed= Some false 106 ; description= None } 107 in 108 let doc = 109 make_lexicon "com.example.union" 110 [make_def "result" (Lexicon_types.Union union_spec)] 111 in 112 let code = Codegen.gen_lexicon_module doc in 113 check bool "contains type result" true (contains code "type result_ =") ; 114 check bool "contains TypeA variant" true (contains code "| TypeA of") ; 115 check bool "contains TypeB variant" true (contains code "| TypeB of") ; 116 check bool "contains Unknown (open)" true 117 (contains code "| Unknown of Yojson.Safe.t") 118 119(* test generating closed union *) 120let test_gen_closed_union () = 121 let union_spec = 122 { Lexicon_types.refs= ["#typeA"; "#typeB"] 123 ; closed= Some true 124 ; description= None } 125 in 126 let doc = 127 make_lexicon "com.example.closed" 128 [make_def "result" (Lexicon_types.Union union_spec)] 129 in 130 let code = Codegen.gen_lexicon_module doc in 131 check bool "no Unknown variant" false (contains code "| Unknown of") 132 133(* test generating query module *) 134let test_gen_query_module () = 135 let params_spec = 136 { Lexicon_types.properties= [("userId", make_property string_type)] 137 ; required= Some ["userId"] 138 ; description= None } 139 in 140 let output_schema = 141 Lexicon_types.Object 142 (make_object_spec [("name", make_property string_type)] ["name"]) 143 in 144 let output_body = 145 { Lexicon_types.encoding= "application/json" 146 ; schema= Some output_schema 147 ; description= None } 148 in 149 let query_spec = 150 { Lexicon_types.parameters= Some params_spec 151 ; output= Some output_body 152 ; errors= None 153 ; description= Some "Get user by ID" } 154 in 155 let doc = 156 make_lexicon "com.example.getUser" 157 [make_def "main" (Lexicon_types.Query query_spec)] 158 in 159 let code = Codegen.gen_lexicon_module doc in 160 check bool "contains module Main" true (contains code "module Main = struct") ; 161 check bool "contains nsid" true 162 (contains code "let nsid = \"com.example.getUser\"") ; 163 check bool "contains type params" true (contains code "type params =") ; 164 check bool "contains type output" true (contains code "type output =") ; 165 check bool "contains call function" true (contains code "let call") ; 166 check bool "contains ~user_id param" true (contains code "~user_id") ; 167 check bool "calls Hermes.query" true (contains code "Hermes.query") 168 169(* test generating procedure module *) 170let test_gen_procedure_module () = 171 let input_schema = 172 Lexicon_types.Object 173 (make_object_spec 174 [ ("name", make_property string_type) 175 ; ("email", make_property string_type) ] 176 ["name"; "email"] ) 177 in 178 let input_body = 179 { Lexicon_types.encoding= "application/json" 180 ; schema= Some input_schema 181 ; description= None } 182 in 183 let output_schema = 184 Lexicon_types.Object 185 (make_object_spec [("id", make_property string_type)] ["id"]) 186 in 187 let output_body = 188 { Lexicon_types.encoding= "application/json" 189 ; schema= Some output_schema 190 ; description= None } 191 in 192 let proc_spec = 193 { Lexicon_types.parameters= None 194 ; input= Some input_body 195 ; output= Some output_body 196 ; errors= None 197 ; description= Some "Create user" } 198 in 199 let doc = 200 make_lexicon "com.example.createUser" 201 [make_def "main" (Lexicon_types.Procedure proc_spec)] 202 in 203 let code = Codegen.gen_lexicon_module doc in 204 check bool "contains module Main" true (contains code "module Main = struct") ; 205 check bool "contains type input" true (contains code "type input =") ; 206 check bool "contains type output" true (contains code "type output =") ; 207 check bool "contains call function" true (contains code "let call") ; 208 check bool "contains ~name param" true (contains code "~name") ; 209 check bool "contains ~email param" true (contains code "~email") ; 210 check bool "calls Hermes.procedure" true (contains code "Hermes.procedure") 211 212(* test type ordering with dependencies *) 213let test_type_ordering () = 214 (* create types where typeB depends on typeA *) 215 let type_a_spec = 216 make_object_spec [("value", make_property string_type)] ["value"] 217 in 218 let type_b_spec = 219 make_object_spec 220 [ ( "a" 221 , make_property (Lexicon_types.Ref {ref_= "#typeA"; description= None}) 222 ) ] 223 ["a"] 224 in 225 let doc = 226 make_lexicon "com.example.order" 227 [ make_def "typeB" (Lexicon_types.Object type_b_spec) 228 ; make_def "typeA" (Lexicon_types.Object type_a_spec) ] 229 in 230 let code = Codegen.gen_lexicon_module doc in 231 (* typeA should appear before typeB in the generated code *) 232 let pos_a = 233 try Some (Str.search_forward (Str.regexp "type type_a") code 0) 234 with Not_found -> None 235 in 236 let pos_b = 237 try Some (Str.search_forward (Str.regexp "type type_b") code 0) 238 with Not_found -> None 239 in 240 match (pos_a, pos_b) with 241 | Some a, Some b -> 242 check bool "typeA before typeB" true (a < b) 243 | _ -> 244 fail "both types should be present" 245 246(* test generating token *) 247let test_gen_token () = 248 let token_spec : Lexicon_types.token_spec = 249 {description= Some "A token value"} 250 in 251 let doc = 252 make_lexicon "com.example.tokens" 253 [make_def "myToken" (Lexicon_types.Token token_spec)] 254 in 255 let code = Codegen.gen_lexicon_module doc in 256 check bool "contains let my_token" true (contains code "let my_token =") ; 257 check bool "contains full URI" true 258 (contains code "com.example.tokens#myToken") 259 260(* test generating inline union (union as property type) *) 261let test_gen_inline_union () = 262 let union_type = 263 Lexicon_types.Union 264 {refs= ["#typeA"; "#typeB"]; closed= Some false; description= None} 265 in 266 let obj_spec = 267 make_object_spec [("status", make_property union_type)] ["status"] 268 in 269 let doc = 270 make_lexicon "com.example.inline" 271 [make_def "main" (Lexicon_types.Object obj_spec)] 272 in 273 let code = Codegen.gen_lexicon_module doc in 274 (* inline union should get its own type named after the property *) 275 check bool "contains type status" true (contains code "type status =") ; 276 check bool "contains TypeA variant" true (contains code "| TypeA of") ; 277 check bool "contains TypeB variant" true (contains code "| TypeB of") ; 278 (* main type should reference the inline union *) 279 check bool "main uses status type" true (contains code "status: status") 280 281(* test generating inline union in array (field_item context) *) 282let test_gen_inline_union_in_array () = 283 let union_type = 284 Lexicon_types.Union 285 {refs= ["#typeA"; "#typeB"]; closed= Some true; description= None} 286 in 287 let array_type = 288 Lexicon_types.Array 289 {items= union_type; min_length= None; max_length= None; description= None} 290 in 291 let obj_spec = 292 make_object_spec [("items", make_property array_type)] ["items"] 293 in 294 let doc = 295 make_lexicon "com.example.arrayunion" 296 [make_def "main" (Lexicon_types.Object obj_spec)] 297 in 298 let code = Codegen.gen_lexicon_module doc in 299 (* inline union in array should be named field_item *) 300 check bool "contains type items_item" true (contains code "type items_item =") ; 301 check bool "items is items_item list" true (contains code "items_item list") 302 303(* test generating empty object as unit *) 304let test_gen_empty_object () = 305 let empty_spec = 306 { Lexicon_types.properties= [] 307 ; required= None 308 ; nullable= None 309 ; description= None } 310 in 311 let doc = 312 make_lexicon "com.example.empty" 313 [make_def "main" (Lexicon_types.Object empty_spec)] 314 in 315 let code = Codegen.gen_lexicon_module doc in 316 check bool "contains type main = unit" true (contains code "type main = unit") ; 317 check bool "contains main_of_yojson _ = Ok ()" true 318 (contains code "main_of_yojson _ = Ok ()") 319 320(* test generating nullable fields (different from optional) *) 321let test_gen_nullable_fields () = 322 let obj_spec = 323 { Lexicon_types.properties= 324 [ ("required_nullable", make_property string_type) 325 ; ("required_not_nullable", make_property string_type) ] 326 ; required= Some ["required_nullable"; "required_not_nullable"] 327 ; nullable= Some ["required_nullable"] 328 ; description= None } 329 in 330 let doc = 331 make_lexicon "com.example.nullable" 332 [make_def "main" (Lexicon_types.Object obj_spec)] 333 in 334 let code = Codegen.gen_lexicon_module doc in 335 (* required + nullable = option *) 336 check bool "nullable is option" true 337 (contains code "required_nullable: string option") ; 338 (* required + not nullable = not option *) 339 check bool "not nullable is not option" true 340 (contains code "required_not_nullable: string;") 341 342(* test generating mutually recursive types *) 343let test_gen_mutually_recursive () = 344 (* typeA has a field of typeB, typeB has a field of typeA *) 345 let type_a_spec = 346 make_object_spec 347 [ ("name", make_property string_type) 348 ; ( "b" 349 , make_property (Lexicon_types.Ref {ref_= "#typeB"; description= None}) 350 ) ] 351 ["name"] 352 in 353 let type_b_spec = 354 make_object_spec 355 [ ("value", make_property int_type) 356 ; ( "a" 357 , make_property (Lexicon_types.Ref {ref_= "#typeA"; description= None}) 358 ) ] 359 ["value"] 360 in 361 let doc = 362 make_lexicon "com.example.recursive" 363 [ make_def "typeA" (Lexicon_types.Object type_a_spec) 364 ; make_def "typeB" (Lexicon_types.Object type_b_spec) ] 365 in 366 let code = Codegen.gen_lexicon_module doc in 367 (* should use "type ... and ..." syntax *) 368 check bool "has type keyword" true (contains code "type type_a =") ; 369 check bool "has and keyword" true (contains code "and type_b =") ; 370 (* deriving should appear after the last type in the group *) 371 check bool "has deriving after and block" true 372 (contains code "[@@deriving yojson") 373 374(* test generating record type *) 375let test_gen_record () = 376 let record_spec : Lexicon_types.record_spec = 377 { key= "tid" 378 ; record= make_object_spec [("text", make_property string_type)] ["text"] 379 ; description= Some "A simple record" } 380 in 381 let doc = 382 make_lexicon "com.example.record" 383 [make_def "main" (Lexicon_types.Record record_spec)] 384 in 385 let code = Codegen.gen_lexicon_module doc in 386 check bool "contains type main" true (contains code "type main =") ; 387 check bool "contains text field" true (contains code "text: string") 388 389(* test generating external ref *) 390let test_gen_external_ref () = 391 let obj_spec = 392 make_object_spec 393 [ ( "user" 394 , make_property 395 (Lexicon_types.Ref {ref_= "com.other.defs#user"; description= None}) 396 ) ] 397 ["user"] 398 in 399 let doc = 400 make_lexicon "com.example.extref" 401 [make_def "main" (Lexicon_types.Object obj_spec)] 402 in 403 let code = Codegen.gen_lexicon_module doc in 404 (* should generate qualified module reference *) 405 check bool "contains qualified ref" true (contains code "Com_other_defs.user") 406 407(* test generating string type with known values *) 408let test_gen_string_known_values () = 409 let string_spec : Lexicon_types.string_spec = 410 { format= None 411 ; min_length= None 412 ; max_length= None 413 ; min_graphemes= None 414 ; max_graphemes= None 415 ; known_values= Some ["pending"; "active"; "completed"] 416 ; enum= None 417 ; const= None 418 ; default= None 419 ; description= Some "Status values" } 420 in 421 let doc = 422 make_lexicon "com.example.status" 423 [make_def "status" (Lexicon_types.String string_spec)] 424 in 425 let code = Codegen.gen_lexicon_module doc in 426 check bool "contains type status = string" true 427 (contains code "type status = string") ; 428 check bool "contains status_of_yojson" true (contains code "status_of_yojson") 429 430(* test generating permission-set module *) 431let test_gen_permission_set () = 432 let perm1 : Lexicon_types.lex_permission = 433 { resource= "rpc" 434 ; extra= 435 [("lxm", `List [`String "com.example.foo"]); ("inheritAud", `Bool true)] 436 } 437 in 438 let perm2 : Lexicon_types.lex_permission = 439 { resource= "repo" 440 ; extra= [("collection", `List [`String "com.example.data"])] } 441 in 442 let ps_spec : Lexicon_types.permission_set_spec = 443 { title= Some "Test Permissions" 444 ; title_lang= Some [("de", "Test Berechtigungen")] 445 ; detail= Some "Access to test features" 446 ; detail_lang= None 447 ; permissions= [perm1; perm2] 448 ; description= None } 449 in 450 let doc = 451 make_lexicon "com.example.perms" 452 [make_def "main" (Lexicon_types.PermissionSet ps_spec)] 453 in 454 let code = Codegen.gen_lexicon_module doc in 455 check bool "contains type permission" true (contains code "type permission =") ; 456 check bool "contains resource field" true (contains code "resource: string") ; 457 check bool "contains lxm field" true (contains code "lxm: string list option") ; 458 check bool "contains inherit_aud field" true 459 (contains code "inherit_aud: bool option") ; 460 check bool "contains type main" true (contains code "type main =") ; 461 check bool "contains title field" true (contains code "title: string option") ; 462 check bool "contains permissions field" true 463 (contains code "permissions: permission list") ; 464 check bool "contains deriving" true (contains code "[@@deriving yojson") 465 466(* test generating query with bytes output (like getBlob) *) 467let test_gen_query_bytes_output () = 468 let params_spec = 469 { Lexicon_types.properties= 470 [("did", make_property string_type); ("cid", make_property string_type)] 471 ; required= Some ["did"; "cid"] 472 ; description= None } 473 in 474 let output_body = 475 { Lexicon_types.encoding= "*/*" (* bytes output *) 476 ; schema= None 477 ; description= None } 478 in 479 let query_spec = 480 { Lexicon_types.parameters= Some params_spec 481 ; output= Some output_body 482 ; errors= None 483 ; description= Some "Get a blob" } 484 in 485 let doc = 486 make_lexicon "com.atproto.sync.getBlob" 487 [make_def "main" (Lexicon_types.Query query_spec)] 488 in 489 let code = Codegen.gen_lexicon_module doc in 490 check bool "contains module Main" true (contains code "module Main = struct") ; 491 check bool "output is bytes * string tuple" true 492 (contains code "type output = bytes * string") ; 493 check bool "calls Hermes.query_bytes" true 494 (contains code "Hermes.query_bytes") 495 496(* test generating procedure with bytes input (like importRepo) *) 497let test_gen_procedure_bytes_input () = 498 let input_body = 499 { Lexicon_types.encoding= "application/vnd.ipld.car" (* bytes input *) 500 ; schema= None 501 ; description= None } 502 in 503 let proc_spec = 504 { Lexicon_types.parameters= None 505 ; input= Some input_body 506 ; output= None 507 ; errors= None 508 ; description= Some "Import a repo" } 509 in 510 let doc = 511 make_lexicon "com.atproto.repo.importRepo" 512 [make_def "main" (Lexicon_types.Procedure proc_spec)] 513 in 514 let code = Codegen.gen_lexicon_module doc in 515 check bool "contains module Main" true (contains code "module Main = struct") ; 516 check bool "has ?input param" true (contains code "?input") ; 517 check bool "calls Hermes.procedure_bytes" true 518 (contains code "Hermes.procedure_bytes") ; 519 check bool "has content_type" true (contains code "application/vnd.ipld.car") 520 521(** tests *) 522 523let object_tests = 524 [ ("simple object", `Quick, test_gen_simple_object) 525 ; ("optional fields", `Quick, test_gen_optional_fields) 526 ; ("key annotation", `Quick, test_gen_key_annotation) 527 ; ("empty object", `Quick, test_gen_empty_object) 528 ; ("nullable fields", `Quick, test_gen_nullable_fields) 529 ; ("external ref", `Quick, test_gen_external_ref) 530 ; ("record type", `Quick, test_gen_record) ] 531 532let union_tests = 533 [ ("open union", `Quick, test_gen_union_type) 534 ; ("closed union", `Quick, test_gen_closed_union) 535 ; ("inline union", `Quick, test_gen_inline_union) 536 ; ("inline union in array", `Quick, test_gen_inline_union_in_array) ] 537 538let xrpc_tests = 539 [ ("query module", `Quick, test_gen_query_module) 540 ; ("procedure module", `Quick, test_gen_procedure_module) 541 ; ("query with bytes output", `Quick, test_gen_query_bytes_output) 542 ; ("procedure with bytes input", `Quick, test_gen_procedure_bytes_input) ] 543 544let ordering_tests = 545 [ ("type ordering", `Quick, test_type_ordering) 546 ; ("mutually recursive", `Quick, test_gen_mutually_recursive) ] 547 548let token_tests = [("token generation", `Quick, test_gen_token)] 549 550let string_tests = 551 [("string with known values", `Quick, test_gen_string_known_values)] 552 553let permission_set_tests = 554 [("generate permission-set", `Quick, test_gen_permission_set)] 555 556let () = 557 run "Codegen" 558 [ ("objects", object_tests) 559 ; ("unions", union_tests) 560 ; ("xrpc", xrpc_tests) 561 ; ("ordering", ordering_tests) 562 ; ("tokens", token_tests) 563 ; ("strings", string_tests) 564 ; ("permission-set", permission_set_tests) ]