objective categorical abstract machine language personal data server
at main 3248 lines 127 kB view raw
1open Lexicon_types 2 3(* use Emitter module for output buffer management *) 4type output = Emitter.t 5 6let make_output = Emitter.make 7 8let add_import = Emitter.add_import 9 10let mark_union_generated = Emitter.mark_union_generated 11 12let is_union_generated = Emitter.is_union_generated 13 14let register_union_name = Emitter.register_union_name 15 16let lookup_union_name = Emitter.lookup_union_name 17 18let emit = Emitter.emit 19 20let emitln = Emitter.emitln 21 22let emit_newline = Emitter.emit_newline 23 24(* generate ocaml type for a primitive type *) 25let rec gen_type_ref nsid out (type_def : type_def) : string = 26 match type_def with 27 | String _ -> 28 "string" 29 | Integer {maximum; _} -> ( 30 (* use int64 for large integers *) 31 match maximum with 32 | Some m when m > 1073741823 -> 33 "int64" 34 | _ -> 35 "int" ) 36 | Boolean _ -> 37 "bool" 38 | Bytes _ -> 39 "bytes" 40 | Blob _ -> 41 "Hermes.blob" 42 | CidLink _ -> 43 "Cid.t" 44 | Array {items; _} -> 45 let item_type = gen_type_ref nsid out items in 46 item_type ^ " list" 47 | Object _ -> 48 (* objects should be defined separately *) 49 "object_todo" 50 | Ref {ref_; _} -> 51 gen_ref_type nsid out ref_ 52 | Union {refs; _} -> ( 53 (* generate inline union reference, using registered name if available *) 54 match lookup_union_name out refs with 55 | Some name -> 56 name 57 | None -> 58 gen_union_type_name refs ) 59 | Token _ -> 60 "string" 61 | Unknown _ -> 62 "Yojson.Safe.t" 63 | Query _ | Procedure _ | Subscription _ | Record _ -> 64 "unit (* primary type *)" 65 66(* generate reference to another type *) 67and gen_ref_type nsid out ref_str : string = 68 if String.length ref_str > 0 && ref_str.[0] = '#' then begin 69 (* local ref: #someDef -> someDef *) 70 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in 71 Naming.type_name def_name 72 end 73 else begin 74 (* external ref: com.example.defs#someDef *) 75 match String.split_on_char '#' ref_str with 76 | [ext_nsid; def_name] -> 77 if ext_nsid = nsid then 78 (* ref to same nsid, treat as local *) 79 Naming.type_name def_name 80 else begin 81 (* use flat module names for include_subdirs unqualified *) 82 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 83 add_import out flat_module ; 84 flat_module ^ "." ^ Naming.type_name def_name 85 end 86 | [ext_nsid] -> 87 if ext_nsid = nsid then Naming.type_name "main" 88 else begin 89 (* just nsid, refers to main def *) 90 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 91 add_import out flat_module ; flat_module ^ ".main" 92 end 93 | _ -> 94 "invalid_ref" 95 end 96 97and gen_union_type_name refs = Naming.union_type_name refs 98 99(* generate full type uri for a ref *) 100let gen_type_uri nsid ref_str = 101 if String.length ref_str > 0 && ref_str.[0] = '#' then 102 (* local ref *) 103 nsid ^ ref_str 104 else 105 (* external ref, use as-is *) 106 ref_str 107 108(* collect inline union specs from object properties with context *) 109let rec collect_inline_unions_with_context context acc type_def = 110 match type_def with 111 | Union spec -> 112 (context, spec.refs, spec) :: acc 113 | Array {items; _} -> 114 (* for array items, append _item to context *) 115 collect_inline_unions_with_context (context ^ "_item") acc items 116 | _ -> 117 acc 118 119let collect_inline_unions_from_properties properties = 120 List.fold_left 121 (fun acc (prop_name, (prop : property)) -> 122 collect_inline_unions_with_context prop_name acc prop.type_def ) 123 [] properties 124 125(* generate inline union types that appear in object properties *) 126let gen_inline_unions nsid out properties = 127 let inline_unions = collect_inline_unions_from_properties properties in 128 List.iter 129 (fun (context, refs, spec) -> 130 (* register and use context-based name *) 131 let context_name = Naming.type_name context in 132 register_union_name out refs context_name ; 133 let type_name = context_name in 134 (* skip if already generated *) 135 if not (is_union_generated out type_name) then begin 136 mark_union_generated out type_name ; 137 let is_closed = Option.value spec.closed ~default:false in 138 emitln out (Printf.sprintf "type %s =" type_name) ; 139 List.iter 140 (fun ref_str -> 141 let variant_name = Naming.variant_name_of_ref ref_str in 142 let payload_type = gen_ref_type nsid out ref_str in 143 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 144 refs ; 145 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ; 146 emit_newline out ; 147 (* generate of_yojson function *) 148 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 149 emitln out " let open Yojson.Safe.Util in" ; 150 emitln out " try" ; 151 emitln out " match json |> member \"$type\" |> to_string with" ; 152 List.iter 153 (fun ref_str -> 154 let variant_name = Naming.variant_name_of_ref ref_str in 155 let full_type_uri = gen_type_uri nsid ref_str in 156 let payload_type = gen_ref_type nsid out ref_str in 157 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 158 emitln out 159 (Printf.sprintf " (match %s_of_yojson json with" 160 payload_type ) ; 161 emitln out 162 (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 163 emitln out " | Error e -> Error e)" ) 164 refs ; 165 if is_closed then 166 emitln out " | t -> Error (\"unknown union type: \" ^ t)" 167 else emitln out " | _ -> Ok (Unknown json)" ; 168 emitln out " with _ -> Error \"failed to parse union\"" ; 169 emit_newline out ; 170 (* generate to_yojson function *) 171 emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ; 172 List.iter 173 (fun ref_str -> 174 let variant_name = Naming.variant_name_of_ref ref_str in 175 let full_type_uri = gen_type_uri nsid ref_str in 176 let payload_type = gen_ref_type nsid out ref_str in 177 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 178 emitln out 179 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 180 emitln out 181 (Printf.sprintf 182 " | `Assoc fields -> `Assoc ((\"$type\", `String \ 183 \"%s\") :: fields)" 184 full_type_uri ) ; 185 emitln out " | other -> other)" ) 186 refs ; 187 if not is_closed then emitln out " | Unknown j -> j" ; 188 emit_newline out 189 end ) 190 inline_unions 191 192(* generate object type definition *) 193(* ~first: use "type" if true, "and" if false *) 194(* ~last: add [@@deriving yojson] if true *) 195let gen_object_type ?(first = true) ?(last = true) nsid out name 196 (spec : object_spec) = 197 let required = Option.value spec.required ~default:[] in 198 let nullable = Option.value spec.nullable ~default:[] in 199 let keyword = if first then "type" else "and" in 200 (* handle empty objects as unit *) 201 if spec.properties = [] then begin 202 emitln out (Printf.sprintf "%s %s = unit" keyword (Naming.type_name name)) ; 203 if last then begin 204 emitln out 205 (Printf.sprintf "let %s_of_yojson _ = Ok ()" (Naming.type_name name)) ; 206 emitln out 207 (Printf.sprintf "let %s_to_yojson () = `Assoc []" 208 (Naming.type_name name) ) ; 209 emit_newline out 210 end 211 end 212 else begin 213 (* generate inline union types first, but only if this is the first type *) 214 if first then gen_inline_unions nsid out spec.properties ; 215 emitln out (Printf.sprintf "%s %s =" keyword (Naming.type_name name)) ; 216 emitln out " {" ; 217 List.iter 218 (fun (prop_name, (prop : property)) -> 219 let ocaml_name = Naming.field_name prop_name in 220 let base_type = gen_type_ref nsid out prop.type_def in 221 let is_required = List.mem prop_name required in 222 let is_nullable = List.mem prop_name nullable in 223 let type_str = 224 if is_required && not is_nullable then base_type 225 else base_type ^ " option" 226 in 227 let key_attr = Naming.key_annotation prop_name ocaml_name in 228 let default_attr = 229 if is_required && not is_nullable then "" else " [@default None]" 230 in 231 emitln out 232 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 233 default_attr ) ) 234 spec.properties ; 235 emitln out " }" ; 236 if last then begin 237 emitln out "[@@deriving yojson {strict= false}]" ; 238 emit_newline out 239 end 240 end 241 242(* generate union type definition *) 243let gen_union_type nsid out name (spec : union_spec) = 244 let type_name = Naming.type_name name in 245 let is_closed = Option.value spec.closed ~default:false in 246 emitln out (Printf.sprintf "type %s =" type_name) ; 247 List.iter 248 (fun ref_str -> 249 let variant_name = Naming.variant_name_of_ref ref_str in 250 let payload_type = gen_ref_type nsid out ref_str in 251 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 252 spec.refs ; 253 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ; 254 emit_newline out ; 255 (* generate of_yojson function *) 256 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 257 emitln out " let open Yojson.Safe.Util in" ; 258 emitln out " try" ; 259 emitln out " match json |> member \"$type\" |> to_string with" ; 260 List.iter 261 (fun ref_str -> 262 let variant_name = Naming.variant_name_of_ref ref_str in 263 let full_type_uri = gen_type_uri nsid ref_str in 264 let payload_type = gen_ref_type nsid out ref_str in 265 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 266 emitln out 267 (Printf.sprintf " (match %s_of_yojson json with" payload_type) ; 268 emitln out (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 269 emitln out " | Error e -> Error e)" ) 270 spec.refs ; 271 if is_closed then emitln out " | t -> Error (\"unknown union type: \" ^ t)" 272 else emitln out " | _ -> Ok (Unknown json)" ; 273 emitln out " with _ -> Error \"failed to parse union\"" ; 274 emit_newline out ; 275 (* generate to_yojson function - inject $type field *) 276 emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ; 277 List.iter 278 (fun ref_str -> 279 let variant_name = Naming.variant_name_of_ref ref_str in 280 let full_type_uri = gen_type_uri nsid ref_str in 281 let payload_type = gen_ref_type nsid out ref_str in 282 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 283 emitln out 284 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 285 emitln out 286 (Printf.sprintf 287 " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") :: \ 288 fields)" 289 full_type_uri ) ; 290 emitln out " | other -> other)" ) 291 spec.refs ; 292 if not is_closed then emitln out " | Unknown j -> j" ; 293 emit_newline out 294 295let is_json_encoding encoding = encoding = "application/json" || encoding = "" 296 297let is_bytes_encoding encoding = 298 encoding <> "" && encoding <> "application/json" 299 300(* generate params type for query/procedure *) 301let gen_params_type nsid out (spec : params_spec) = 302 let required = Option.value spec.required ~default:[] in 303 emitln out "type params =" ; 304 emitln out " {" ; 305 List.iter 306 (fun (prop_name, (prop : property)) -> 307 let ocaml_name = Naming.field_name prop_name in 308 let base_type = gen_type_ref nsid out prop.type_def in 309 let is_required = List.mem prop_name required in 310 let type_str = if is_required then base_type else base_type ^ " option" in 311 let key_attr = Naming.key_annotation prop_name ocaml_name in 312 let default_attr = if is_required then "" else " [@default None]" in 313 emitln out 314 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 315 default_attr ) ) 316 spec.properties ; 317 emitln out " }" ; 318 emitln out "[@@deriving yojson {strict= false}]" ; 319 emit_newline out 320 321(* generate output type for query/procedure *) 322let gen_output_type nsid out (body : body_def) = 323 match body.schema with 324 | Some (Object spec) -> 325 (* handle empty objects as unit *) 326 if spec.properties = [] then begin 327 emitln out "type output = unit" ; 328 emitln out "let output_of_yojson _ = Ok ()" ; 329 emitln out "let output_to_yojson () = `Assoc []" ; 330 emit_newline out 331 end 332 else begin 333 (* generate inline union types first *) 334 gen_inline_unions nsid out spec.properties ; 335 let required = Option.value spec.required ~default:[] in 336 let nullable = Option.value spec.nullable ~default:[] in 337 emitln out "type output =" ; 338 emitln out " {" ; 339 List.iter 340 (fun (prop_name, (prop : property)) -> 341 let ocaml_name = Naming.field_name prop_name in 342 let base_type = gen_type_ref nsid out prop.type_def in 343 let is_required = List.mem prop_name required in 344 let is_nullable = List.mem prop_name nullable in 345 let type_str = 346 if is_required && not is_nullable then base_type 347 else base_type ^ " option" 348 in 349 let key_attr = Naming.key_annotation prop_name ocaml_name in 350 let default_attr = 351 if is_required && not is_nullable then "" else " [@default None]" 352 in 353 emitln out 354 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 355 default_attr ) ) 356 spec.properties ; 357 emitln out " }" ; 358 emitln out "[@@deriving yojson {strict= false}]" ; 359 emit_newline out 360 end 361 | Some other_type -> 362 let type_str = gen_type_ref nsid out other_type in 363 emitln out (Printf.sprintf "type output = %s" type_str) ; 364 emitln out "[@@deriving yojson {strict= false}]" ; 365 emit_newline out 366 | None -> 367 emitln out "type output = unit" ; 368 emitln out "let output_of_yojson _ = Ok ()" ; 369 emitln out "let output_to_yojson () = `Null" ; 370 emit_newline out 371 372(* generate query module *) 373let gen_query nsid out name (spec : query_spec) = 374 (* check if output is bytes *) 375 let output_is_bytes = 376 match spec.output with 377 | Some body -> 378 is_bytes_encoding body.encoding 379 | None -> 380 false 381 in 382 emitln out 383 (Printf.sprintf "(** %s *)" (Option.value spec.description ~default:name)) ; 384 emitln out (Printf.sprintf "module %s = struct" (Naming.def_module_name name)) ; 385 emitln out (Printf.sprintf " let nsid = \"%s\"" nsid) ; 386 emit_newline out ; 387 (* generate params type *) 388 ( match spec.parameters with 389 | Some params when params.properties <> [] -> 390 emit out " " ; 391 gen_params_type nsid out params 392 | _ -> 393 emitln out " type params = unit" ; 394 emitln out " let params_to_yojson () = `Assoc []" ; 395 emit_newline out ) ; 396 (* generate output type *) 397 ( if output_is_bytes then begin 398 emitln out " (** raw bytes output with content type *)" ; 399 emitln out " type output = string * string" ; 400 emit_newline out 401 end 402 else 403 match spec.output with 404 | Some body -> 405 emit out " " ; 406 gen_output_type nsid out body 407 | None -> 408 emitln out " type output = unit" ; 409 emitln out " let output_of_yojson _ = Ok ()" ; 410 emit_newline out ) ; 411 (* generate call function *) 412 emitln out " let call" ; 413 ( match spec.parameters with 414 | Some params when params.properties <> [] -> 415 let required = Option.value params.required ~default:[] in 416 List.iter 417 (fun (prop_name, _) -> 418 let ocaml_name = Naming.field_name prop_name in 419 let is_required = List.mem prop_name required in 420 if is_required then emitln out (Printf.sprintf " ~%s" ocaml_name) 421 else emitln out (Printf.sprintf " ?%s" ocaml_name) ) 422 params.properties 423 | _ -> 424 () ) ; 425 emitln out " (client : Hermes.client) : output Lwt.t =" ; 426 ( match spec.parameters with 427 | Some params when params.properties <> [] -> 428 emit out " let params : params = {" ; 429 let fields = 430 List.map 431 (fun (prop_name, _) -> Naming.field_name prop_name) 432 params.properties 433 in 434 emit out (String.concat "; " fields) ; 435 emitln out "} in" ; 436 if output_is_bytes then 437 emitln out 438 " Hermes.query_bytes client nsid (params_to_yojson params)" 439 else 440 emitln out 441 " Hermes.query client nsid (params_to_yojson params) \ 442 output_of_yojson" 443 | _ -> 444 if output_is_bytes then 445 emitln out " Hermes.query_bytes client nsid (`Assoc [])" 446 else 447 emitln out " Hermes.query client nsid (`Assoc []) output_of_yojson" 448 ) ; 449 emitln out "end" ; emit_newline out 450 451(* generate procedure module *) 452let gen_procedure nsid out name (spec : procedure_spec) = 453 (* check if input/output are bytes *) 454 let input_is_bytes = 455 match spec.input with 456 | Some body -> 457 is_bytes_encoding body.encoding 458 | None -> 459 false 460 in 461 let output_is_bytes = 462 match spec.output with 463 | Some body -> 464 is_bytes_encoding body.encoding 465 | None -> 466 false 467 in 468 let input_content_type = 469 match spec.input with 470 | Some body when is_bytes_encoding body.encoding -> 471 body.encoding 472 | _ -> 473 "application/json" 474 in 475 emitln out 476 (Printf.sprintf "(** %s *)" (Option.value spec.description ~default:name)) ; 477 emitln out (Printf.sprintf "module %s = struct" (Naming.def_module_name name)) ; 478 emitln out (Printf.sprintf " let nsid = \"%s\"" nsid) ; 479 emit_newline out ; 480 (* generate params type *) 481 ( match spec.parameters with 482 | Some params when params.properties <> [] -> 483 emit out " " ; 484 gen_params_type nsid out params 485 | _ -> 486 emitln out " type params = unit" ; 487 emitln out " let params_to_yojson () = `Assoc []" ; 488 emit_newline out ) ; 489 (* generate input type; only for json input with schema *) 490 ( if not input_is_bytes then 491 match spec.input with 492 | Some body when body.schema <> None -> 493 emit out " " ; 494 ( match body.schema with 495 | Some (Object spec) -> 496 if spec.properties = [] then begin 497 (* empty object input *) 498 emitln out "type input = unit" ; 499 emitln out " let input_of_yojson _ = Ok ()" ; 500 emitln out " let input_to_yojson () = `Assoc []" 501 end 502 else begin 503 (* generate inline union types first *) 504 gen_inline_unions nsid out spec.properties ; 505 let required = Option.value spec.required ~default:[] in 506 emitln out "type input =" ; 507 emitln out " {" ; 508 List.iter 509 (fun (prop_name, (prop : property)) -> 510 let ocaml_name = Naming.field_name prop_name in 511 let base_type = gen_type_ref nsid out prop.type_def in 512 let is_required = List.mem prop_name required in 513 let type_str = 514 if is_required then base_type else base_type ^ " option" 515 in 516 let key_attr = Naming.key_annotation prop_name ocaml_name in 517 let default_attr = 518 if is_required then "" else " [@default None]" 519 in 520 emitln out 521 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str 522 key_attr default_attr ) ) 523 spec.properties ; 524 emitln out " }" ; 525 emitln out " [@@deriving yojson {strict= false}]" 526 end 527 | Some other_type -> 528 emitln out 529 (Printf.sprintf "type input = %s" 530 (gen_type_ref nsid out other_type) ) ; 531 emitln out " [@@deriving yojson {strict= false}]" 532 | None -> 533 () ) ; 534 emit_newline out 535 | _ -> 536 () ) ; 537 (* generate output type *) 538 ( if output_is_bytes then begin 539 emitln out " (** raw bytes output with content type *)" ; 540 emitln out " type output = (string * string) option" ; 541 emit_newline out 542 end 543 else 544 match spec.output with 545 | Some body -> 546 emit out " " ; 547 gen_output_type nsid out body 548 | None -> 549 emitln out " type output = unit" ; 550 emitln out " let output_of_yojson _ = Ok ()" ; 551 emit_newline out ) ; 552 (* generate call function *) 553 emitln out " let call" ; 554 (* add labeled arguments for parameters *) 555 ( match spec.parameters with 556 | Some params when params.properties <> [] -> 557 let required = Option.value params.required ~default:[] in 558 List.iter 559 (fun (prop_name, _) -> 560 let ocaml_name = Naming.field_name prop_name in 561 let is_required = List.mem prop_name required in 562 if is_required then emitln out (Printf.sprintf " ~%s" ocaml_name) 563 else emitln out (Printf.sprintf " ?%s" ocaml_name) ) 564 params.properties 565 | _ -> 566 () ) ; 567 (* add labeled arguments for input *) 568 ( if input_is_bytes then 569 (* for bytes input, take raw string *) 570 emitln out " ?input" 571 else 572 match spec.input with 573 | Some body -> ( 574 match body.schema with 575 | Some (Object obj_spec) -> 576 let required = Option.value obj_spec.required ~default:[] in 577 List.iter 578 (fun (prop_name, _) -> 579 let ocaml_name = Naming.field_name prop_name in 580 let is_required = List.mem prop_name required in 581 if is_required then 582 emitln out (Printf.sprintf " ~%s" ocaml_name) 583 else emitln out (Printf.sprintf " ?%s" ocaml_name) ) 584 obj_spec.properties 585 | Some _ -> 586 (* non-object input, take as single argument *) 587 emitln out " ~input" 588 | None -> 589 () ) 590 | None -> 591 () ) ; 592 emitln out " (client : Hermes.client) : output Lwt.t =" ; 593 (* build params record *) 594 ( match spec.parameters with 595 | Some params when params.properties <> [] -> 596 emit out " let params = {" ; 597 let fields = 598 List.map 599 (fun (prop_name, _) -> Naming.field_name prop_name) 600 params.properties 601 in 602 emit out (String.concat "; " fields) ; 603 emitln out "} in" 604 | _ -> 605 emitln out " let params = () in" ) ; 606 (* generate the call based on input/output types *) 607 if input_is_bytes then begin 608 (* bytes input - choose between procedure_blob and procedure_bytes *) 609 if output_is_bytes then 610 (* bytes-in, bytes-out: use procedure_bytes *) 611 emitln out 612 (Printf.sprintf 613 " Hermes.procedure_bytes client nsid (params_to_yojson params) \ 614 input ~content_type:\"%s\"" 615 input_content_type ) 616 else if spec.output = None then 617 (* bytes-in, no output: use procedure_bytes and map to unit *) 618 emitln out 619 (Printf.sprintf 620 " let open Lwt.Syntax in\n\ 621 \ let* _ = Hermes.procedure_bytes client nsid (params_to_yojson \ 622 params) input ~content_type:\"%s\" in\n\ 623 \ Lwt.return ()" 624 input_content_type ) 625 else 626 (* bytes-in, json-out: use procedure_blob *) 627 emitln out 628 (Printf.sprintf 629 " Hermes.procedure_blob client nsid (params_to_yojson params) \ 630 (Bytes.of_string (Option.value input ~default:\"\")) \ 631 ~content_type:\"%s\" output_of_yojson" 632 input_content_type ) 633 end 634 else begin 635 (* json input - build input and use procedure *) 636 ( match spec.input with 637 | Some body -> ( 638 match body.schema with 639 | Some (Object obj_spec) -> 640 if obj_spec.properties = [] then 641 (* empty object uses unit *) 642 emitln out " let input = Some (input_to_yojson ()) in" 643 else begin 644 emit out " let input = Some ({" ; 645 let fields = 646 List.map 647 (fun (prop_name, _) -> Naming.field_name prop_name) 648 obj_spec.properties 649 in 650 emit out (String.concat "; " fields) ; 651 emitln out "} |> input_to_yojson) in" 652 end 653 | Some _ -> 654 emitln out " let input = Some (input_to_yojson input) in" 655 | None -> 656 emitln out " let input = None in" ) 657 | None -> 658 emitln out " let input = None in" ) ; 659 emitln out 660 " Hermes.procedure client nsid (params_to_yojson params) input \ 661 output_of_yojson" 662 end ; 663 emitln out "end" ; 664 emit_newline out 665 666(* generate token constant *) 667let gen_token nsid out name (spec : token_spec) = 668 let full_uri = nsid ^ "#" ^ name in 669 emitln out 670 (Printf.sprintf "(** %s *)" (Option.value spec.description ~default:name)) ; 671 emitln out (Printf.sprintf "let %s = \"%s\"" (Naming.type_name name) full_uri) ; 672 emit_newline out 673 674(* generate string type alias (for strings with knownValues) *) 675let gen_string_type out name (spec : string_spec) = 676 let type_name = Naming.type_name name in 677 emitln out 678 (Printf.sprintf "(** string type with known values%s *)" 679 (match spec.description with Some d -> ": " ^ d | None -> "") ) ; 680 emitln out (Printf.sprintf "type %s = string" type_name) ; 681 emitln out (Printf.sprintf "let %s_of_yojson = function" type_name) ; 682 emitln out " | `String s -> Ok s" ; 683 emitln out (Printf.sprintf " | _ -> Error \"%s: expected string\"" type_name) ; 684 emitln out (Printf.sprintf "let %s_to_yojson s = `String s" type_name) ; 685 emit_newline out 686 687let find_sccs = Scc.find_def_sccs 688 689(* helper to check if a def generates a type (vs token/query/procedure) *) 690let is_type_def def = 691 match def.type_def with 692 | Object _ | Union _ | Record _ -> 693 true 694 | String spec when spec.known_values <> None -> 695 true 696 | _ -> 697 false 698 699(* helper to check if a def is an object type (can use [@@deriving yojson]) *) 700let is_object_def def = 701 match def.type_def with Object _ | Record _ -> true | _ -> false 702 703(* generate a single definition *) 704let gen_single_def ?(first = true) ?(last = true) nsid out def = 705 match def.type_def with 706 | Object spec -> 707 gen_object_type ~first ~last nsid out def.name spec 708 | Union spec -> 709 (* unions always generate their own converters, so they're always "complete" *) 710 gen_union_type nsid out def.name spec 711 | Token spec -> 712 gen_token nsid out def.name spec 713 | Query spec -> 714 gen_query nsid out def.name spec 715 | Procedure spec -> 716 gen_procedure nsid out def.name spec 717 | Record spec -> 718 gen_object_type ~first ~last nsid out def.name spec.record 719 | String spec when spec.known_values <> None -> 720 gen_string_type out def.name spec 721 | String _ 722 | Integer _ 723 | Boolean _ 724 | Bytes _ 725 | Blob _ 726 | CidLink _ 727 | Array _ 728 | Ref _ 729 | Unknown _ 730 | Subscription _ -> 731 () 732 733(* generate a group of mutually recursive definitions (SCC) *) 734let gen_scc nsid out scc = 735 match scc with 736 | [] -> 737 () 738 | [def] -> 739 (* single definition, no cycle *) 740 gen_single_def nsid out def 741 | defs -> 742 (* multiple definitions forming a cycle *) 743 (* first, collect and generate all inline unions from all objects in the SCC *) 744 List.iter 745 (fun def -> 746 match def.type_def with 747 | Object spec -> 748 gen_inline_unions nsid out spec.properties 749 | Record spec -> 750 gen_inline_unions nsid out spec.record.properties 751 | _ -> 752 () ) 753 defs ; 754 (* separate object-like types from others *) 755 let obj_defs = List.filter is_object_def defs in 756 let other_defs = List.filter (fun d -> not (is_object_def d)) defs in 757 (* generate other types first (unions, etc.) - they define their own converters *) 758 List.iter (fun def -> gen_single_def nsid out def) other_defs ; 759 (* generate object types as mutually recursive *) 760 let n = List.length obj_defs in 761 List.iteri 762 (fun i def -> 763 let first = i = 0 in 764 let last = i = n - 1 in 765 match def.type_def with 766 | Object spec -> 767 (* skip inline unions since we already generated them above *) 768 let required = Option.value spec.required ~default:[] in 769 let nullable = Option.value spec.nullable ~default:[] in 770 let keyword = if first then "type" else "and" in 771 if spec.properties = [] then begin 772 emitln out 773 (Printf.sprintf "%s %s = unit" keyword 774 (Naming.type_name def.name) ) ; 775 if last then begin 776 (* for empty objects in a recursive group, we still need deriving *) 777 emitln out "[@@deriving yojson {strict= false}]" ; 778 emit_newline out 779 end 780 end 781 else begin 782 emitln out 783 (Printf.sprintf "%s %s =" keyword (Naming.type_name def.name)) ; 784 emitln out " {" ; 785 List.iter 786 (fun (prop_name, (prop : property)) -> 787 let ocaml_name = Naming.field_name prop_name in 788 let base_type = gen_type_ref nsid out prop.type_def in 789 let is_required = List.mem prop_name required in 790 let is_nullable = List.mem prop_name nullable in 791 let type_str = 792 if is_required && not is_nullable then base_type 793 else base_type ^ " option" 794 in 795 let key_attr = Naming.key_annotation prop_name ocaml_name in 796 let default_attr = 797 if is_required && not is_nullable then "" 798 else " [@default None]" 799 in 800 emitln out 801 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str 802 key_attr default_attr ) ) 803 spec.properties ; 804 emitln out " }" ; 805 if last then begin 806 emitln out "[@@deriving yojson {strict= false}]" ; 807 emit_newline out 808 end 809 end 810 | Record spec -> 811 let obj_spec = spec.record in 812 let required = Option.value obj_spec.required ~default:[] in 813 let nullable = Option.value obj_spec.nullable ~default:[] in 814 let keyword = if first then "type" else "and" in 815 if obj_spec.properties = [] then begin 816 emitln out 817 (Printf.sprintf "%s %s = unit" keyword 818 (Naming.type_name def.name) ) ; 819 if last then begin 820 emitln out "[@@deriving yojson {strict= false}]" ; 821 emit_newline out 822 end 823 end 824 else begin 825 emitln out 826 (Printf.sprintf "%s %s =" keyword (Naming.type_name def.name)) ; 827 emitln out " {" ; 828 List.iter 829 (fun (prop_name, (prop : property)) -> 830 let ocaml_name = Naming.field_name prop_name in 831 let base_type = gen_type_ref nsid out prop.type_def in 832 let is_required = List.mem prop_name required in 833 let is_nullable = List.mem prop_name nullable in 834 let type_str = 835 if is_required && not is_nullable then base_type 836 else base_type ^ " option" 837 in 838 let key_attr = Naming.key_annotation prop_name ocaml_name in 839 let default_attr = 840 if is_required && not is_nullable then "" 841 else " [@default None]" 842 in 843 emitln out 844 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str 845 key_attr default_attr ) ) 846 obj_spec.properties ; 847 emitln out " }" ; 848 if last then begin 849 emitln out "[@@deriving yojson {strict= false}]" ; 850 emit_newline out 851 end 852 end 853 | _ -> 854 () ) 855 obj_defs 856 857(* generate complete lexicon module *) 858let gen_lexicon_module (doc : lexicon_doc) : string = 859 let out = make_output () in 860 let nsid = doc.id in 861 (* header *) 862 emitln out (Printf.sprintf "(* generated from %s *)" nsid) ; 863 emit_newline out ; 864 (* find strongly connected components *) 865 let sccs = find_sccs nsid doc.defs in 866 (* generate each SCC *) 867 List.iter (gen_scc nsid out) sccs ; 868 Emitter.contents out 869 870(* get all imports needed for a lexicon *) 871let get_imports (doc : lexicon_doc) : string list = 872 let out = make_output () in 873 let nsid = doc.id in 874 (* traverse all definitions to collect imports *) 875 let rec collect_from_type = function 876 | Array {items; _} -> 877 collect_from_type items 878 | Ref {ref_; _} -> 879 let _ = gen_ref_type nsid out ref_ in 880 () 881 | Union {refs; _} -> 882 List.iter 883 (fun r -> 884 let _ = gen_ref_type nsid out r in 885 () ) 886 refs 887 | Object {properties; _} -> 888 List.iter 889 (fun (_, (prop : property)) -> collect_from_type prop.type_def) 890 properties 891 | Query {parameters; output; _} -> 892 Option.iter 893 (fun p -> 894 List.iter 895 (fun (_, (prop : property)) -> collect_from_type prop.type_def) 896 p.properties ) 897 parameters ; 898 Option.iter (fun o -> Option.iter collect_from_type o.schema) output 899 | Procedure {parameters; input; output; _} -> 900 Option.iter 901 (fun p -> 902 List.iter 903 (fun (_, (prop : property)) -> collect_from_type prop.type_def) 904 p.properties ) 905 parameters ; 906 Option.iter (fun i -> Option.iter collect_from_type i.schema) input ; 907 Option.iter (fun o -> Option.iter collect_from_type o.schema) output 908 | Record {record; _} -> 909 List.iter 910 (fun (_, (prop : property)) -> collect_from_type prop.type_def) 911 record.properties 912 | _ -> 913 () 914 in 915 List.iter (fun def -> collect_from_type def.type_def) doc.defs ; 916 Emitter.get_imports out 917 918(* get external nsid dependencies - delegated to Scc module *) 919let get_external_nsids = Scc.get_external_nsids 920 921(* generate a merged lexicon module from multiple lexicons *) 922let gen_merged_lexicon_module (docs : lexicon_doc list) : string = 923 let out = make_output () in 924 (* collect all nsids in this merged group for local ref detection *) 925 let merged_nsids = List.map (fun d -> d.id) docs in 926 (* header *) 927 emitln out 928 (Printf.sprintf "(* generated from lexicons: %s *)" 929 (String.concat ", " merged_nsids) ) ; 930 emit_newline out ; 931 (* collect all defs from all docs *) 932 let all_defs = 933 List.concat_map 934 (fun doc -> List.map (fun def -> (doc.id, def)) doc.defs) 935 docs 936 in 937 (* collect all inline unions as pseudo-defs for proper ordering *) 938 let rec collect_inline_unions_from_type nsid context acc type_def = 939 match type_def with 940 | Union spec -> 941 (* found an inline union - create pseudo-def entry *) 942 let union_name = Naming.type_name context in 943 (nsid, union_name, spec.refs, spec) :: acc 944 | Array {items; _} -> 945 collect_inline_unions_from_type nsid (context ^ "_item") acc items 946 | Object {properties; _} -> 947 List.fold_left 948 (fun a (prop_name, (prop : property)) -> 949 collect_inline_unions_from_type nsid prop_name a prop.type_def ) 950 acc properties 951 | _ -> 952 acc 953 in 954 let all_inline_unions = 955 List.concat_map 956 (fun (nsid, def) -> 957 match def.type_def with 958 | Object spec -> 959 List.fold_left 960 (fun acc (prop_name, (prop : property)) -> 961 collect_inline_unions_from_type nsid prop_name acc prop.type_def ) 962 [] spec.properties 963 | Record spec -> 964 List.fold_left 965 (fun acc (prop_name, (prop : property)) -> 966 collect_inline_unions_from_type nsid prop_name acc prop.type_def ) 967 [] spec.record.properties 968 | _ -> 969 [] ) 970 all_defs 971 in 972 (* create a lookup for inline unions by their name *) 973 let inline_union_map = Hashtbl.create 64 in 974 List.iter 975 (fun (nsid, name, refs, spec) -> 976 Hashtbl.add inline_union_map 977 (nsid ^ "#__inline__" ^ name) 978 (nsid, name, refs, spec) ) 979 all_inline_unions ; 980 (* detect inline union name collisions - same name but different refs *) 981 let inline_union_name_map = Hashtbl.create 64 in 982 List.iter 983 (fun (nsid, name, refs, _spec) -> 984 let sorted_refs = List.sort String.compare refs in 985 let existing = Hashtbl.find_opt inline_union_name_map name in 986 match existing with 987 | None -> 988 Hashtbl.add inline_union_name_map name [(nsid, sorted_refs)] 989 | Some entries -> 990 (* check if this is a different union (different refs) *) 991 if not (List.exists (fun (_, r) -> r = sorted_refs) entries) then 992 Hashtbl.replace inline_union_name_map name 993 ((nsid, sorted_refs) :: entries) ) 994 all_inline_unions ; 995 let colliding_inline_union_names = 996 Hashtbl.fold 997 (fun name entries acc -> 998 if List.length entries > 1 then name :: acc else acc ) 999 inline_union_name_map [] 1000 in 1001 (* the "host" nsid is the first one - types from here keep short names *) 1002 let host_nsid = List.hd merged_nsids in 1003 (* function to get unique inline union name *) 1004 (* only prefix names from "visiting" nsids, not the host *) 1005 let get_unique_inline_union_name nsid name = 1006 if List.mem name colliding_inline_union_names && nsid <> host_nsid then 1007 Naming.flat_name_of_nsid nsid ^ "_" ^ name 1008 else name 1009 in 1010 (* detect name collisions - names that appear in multiple nsids *) 1011 let name_counts = Hashtbl.create 64 in 1012 List.iter 1013 (fun (nsid, def) -> 1014 let existing = Hashtbl.find_opt name_counts def.name in 1015 match existing with 1016 | None -> 1017 Hashtbl.add name_counts def.name [nsid] 1018 | Some nsids when not (List.mem nsid nsids) -> 1019 Hashtbl.replace name_counts def.name (nsid :: nsids) 1020 | _ -> 1021 () ) 1022 all_defs ; 1023 let colliding_names = 1024 Hashtbl.fold 1025 (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc) 1026 name_counts [] 1027 in 1028 (* function to get unique type name, adding nsid prefix for collisions *) 1029 (* only prefix names from "visiting" nsids, not the host *) 1030 let get_unique_type_name nsid def_name = 1031 if List.mem def_name colliding_names && nsid <> host_nsid then 1032 (* use full nsid as prefix to guarantee uniqueness *) 1033 (* app.bsky.feed.defs#viewerState -> app_bsky_feed_defs_viewer_state *) 1034 let prefix = Naming.flat_name_of_nsid nsid ^ "_" in 1035 Naming.type_name (prefix ^ def_name) 1036 else Naming.type_name def_name 1037 in 1038 (* for merged modules, we need to handle refs differently: 1039 - refs to other nsids in the merged group become local refs 1040 - refs within same nsid stay as local refs *) 1041 (* custom ref type generator that treats merged nsids as local *) 1042 let rec gen_merged_type_ref current_nsid type_def = 1043 match type_def with 1044 | String _ -> 1045 "string" 1046 | Integer {maximum; _} -> ( 1047 match maximum with Some m when m > 1073741823 -> "int64" | _ -> "int" ) 1048 | Boolean _ -> 1049 "bool" 1050 | Bytes _ -> 1051 "bytes" 1052 | Blob _ -> 1053 "Hermes.blob" 1054 | CidLink _ -> 1055 "Cid.t" 1056 | Array {items; _} -> 1057 let item_type = gen_merged_type_ref current_nsid items in 1058 item_type ^ " list" 1059 | Object _ -> 1060 "object_todo" 1061 | Ref {ref_; _} -> 1062 gen_merged_ref_type current_nsid ref_ 1063 | Union {refs; _} -> ( 1064 match lookup_union_name out refs with 1065 | Some name -> 1066 name 1067 | None -> 1068 gen_union_type_name refs ) 1069 | Token _ -> 1070 "string" 1071 | Unknown _ -> 1072 "Yojson.Safe.t" 1073 | Query _ | Procedure _ | Subscription _ | Record _ -> 1074 "unit (* primary type *)" 1075 and gen_merged_ref_type current_nsid ref_str = 1076 if String.length ref_str > 0 && ref_str.[0] = '#' then begin 1077 (* local ref within same nsid *) 1078 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in 1079 get_unique_type_name current_nsid def_name 1080 end 1081 else begin 1082 match String.split_on_char '#' ref_str with 1083 | [ext_nsid; def_name] -> 1084 if List.mem ext_nsid merged_nsids then 1085 (* ref to another nsid in the merged group - use unique name *) 1086 get_unique_type_name ext_nsid def_name 1087 else begin 1088 (* truly external ref *) 1089 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 1090 add_import out flat_module ; 1091 flat_module ^ "." ^ Naming.type_name def_name 1092 end 1093 | [ext_nsid] -> 1094 if List.mem ext_nsid merged_nsids then 1095 get_unique_type_name ext_nsid "main" 1096 else begin 1097 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 1098 add_import out flat_module ; flat_module ^ ".main" 1099 end 1100 | _ -> 1101 "invalid_ref" 1102 end 1103 in 1104 (* generate converter expression for reading a type from json *) 1105 (* returns (converter_expr, needs_result_unwrap) - if needs_result_unwrap is true, caller should apply Result.get_ok *) 1106 let gen_of_yojson_expr current_nsid type_def = 1107 match type_def with 1108 | String _ | Token _ -> 1109 ("to_string", false) 1110 | Integer {maximum; _} -> ( 1111 match maximum with 1112 | Some m when m > 1073741823 -> 1113 ("(fun j -> Int64.of_int (to_int j))", false) 1114 | _ -> 1115 ("to_int", false) ) 1116 | Boolean _ -> 1117 ("to_bool", false) 1118 | Bytes _ -> 1119 ("(fun j -> Bytes.of_string (to_string j))", false) 1120 | Blob _ -> 1121 ("Hermes.blob_of_yojson", true) 1122 | CidLink _ -> 1123 ("Cid.of_yojson", true) 1124 | Array {items; _} -> 1125 let item_type = gen_merged_type_ref current_nsid items in 1126 ( Printf.sprintf 1127 "(fun j -> to_list j |> List.filter_map (fun x -> match \ 1128 %s_of_yojson x with Ok v -> Some v | _ -> None))" 1129 item_type 1130 , false ) 1131 | Ref {ref_; _} -> 1132 let type_name = gen_merged_ref_type current_nsid ref_ in 1133 (type_name ^ "_of_yojson", true) 1134 | Union {refs; _} -> 1135 let type_name = 1136 match lookup_union_name out refs with 1137 | Some n -> 1138 n 1139 | None -> 1140 gen_union_type_name refs 1141 in 1142 (type_name ^ "_of_yojson", true) 1143 | Unknown _ -> 1144 ("(fun j -> j)", false) 1145 | _ -> 1146 ("(fun _ -> failwith \"unsupported type\")", false) 1147 in 1148 (* generate converter expression for writing a type to json *) 1149 let gen_to_yojson_expr current_nsid type_def = 1150 match type_def with 1151 | String _ | Token _ -> 1152 "(fun s -> `String s)" 1153 | Integer {maximum; _} -> ( 1154 match maximum with 1155 | Some m when m > 1073741823 -> 1156 "(fun i -> `Int (Int64.to_int i))" 1157 | _ -> 1158 "(fun i -> `Int i)" ) 1159 | Boolean _ -> 1160 "(fun b -> `Bool b)" 1161 | Bytes _ -> 1162 "(fun b -> `String (Bytes.to_string b))" 1163 | Blob _ -> 1164 "Hermes.blob_to_yojson" 1165 | CidLink _ -> 1166 "Cid.to_yojson" 1167 | Array {items; _} -> 1168 let item_type = gen_merged_type_ref current_nsid items in 1169 Printf.sprintf "(fun l -> `List (List.map %s_to_yojson l))" item_type 1170 | Ref {ref_; _} -> 1171 let type_name = gen_merged_ref_type current_nsid ref_ in 1172 type_name ^ "_to_yojson" 1173 | Union {refs; _} -> 1174 let type_name = 1175 match lookup_union_name out refs with 1176 | Some n -> 1177 n 1178 | None -> 1179 gen_union_type_name refs 1180 in 1181 type_name ^ "_to_yojson" 1182 | Unknown _ -> 1183 "(fun j -> j)" 1184 | _ -> 1185 "(fun _ -> `Null)" 1186 in 1187 (* generate type uri for merged context *) 1188 let gen_merged_type_uri current_nsid ref_str = 1189 if String.length ref_str > 0 && ref_str.[0] = '#' then 1190 current_nsid ^ ref_str 1191 else ref_str 1192 in 1193 (* register inline union names without generating code *) 1194 let register_merged_inline_unions nsid properties = 1195 let rec collect_inline_unions_with_context context acc type_def = 1196 match type_def with 1197 | Union spec -> 1198 (context, spec.refs, spec) :: acc 1199 | Array {items; _} -> 1200 collect_inline_unions_with_context (context ^ "_item") acc items 1201 | _ -> 1202 acc 1203 in 1204 let inline_unions = 1205 List.fold_left 1206 (fun acc (prop_name, (prop : property)) -> 1207 collect_inline_unions_with_context prop_name acc prop.type_def ) 1208 [] properties 1209 in 1210 List.iter 1211 (fun (context, refs, _spec) -> 1212 let base_name = Naming.type_name context in 1213 let unique_name = get_unique_inline_union_name nsid base_name in 1214 register_union_name out refs unique_name ) 1215 inline_unions 1216 in 1217 (* generate object type for merged context *) 1218 let gen_merged_object_type ?(first = true) ?(last = true) current_nsid name 1219 (spec : object_spec) = 1220 let required = Option.value spec.required ~default:[] in 1221 let nullable = Option.value spec.nullable ~default:[] in 1222 let keyword = if first then "type" else "and" in 1223 let type_name = get_unique_type_name current_nsid name in 1224 if spec.properties = [] then begin 1225 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ; 1226 if last then begin 1227 emitln out (Printf.sprintf "let %s_of_yojson _ = Ok ()" type_name) ; 1228 emitln out (Printf.sprintf "let %s_to_yojson () = `Assoc []" type_name) ; 1229 emit_newline out 1230 end 1231 end 1232 else begin 1233 if first then register_merged_inline_unions current_nsid spec.properties ; 1234 emitln out (Printf.sprintf "%s %s =" keyword type_name) ; 1235 emitln out " {" ; 1236 List.iter 1237 (fun (prop_name, (prop : property)) -> 1238 let ocaml_name = Naming.field_name prop_name in 1239 let base_type = gen_merged_type_ref current_nsid prop.type_def in 1240 let is_required = List.mem prop_name required in 1241 let is_nullable = List.mem prop_name nullable in 1242 let type_str = 1243 if is_required && not is_nullable then base_type 1244 else base_type ^ " option" 1245 in 1246 let key_attr = Naming.key_annotation prop_name ocaml_name in 1247 let default_attr = 1248 if is_required && not is_nullable then "" else " [@default None]" 1249 in 1250 emitln out 1251 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 1252 default_attr ) ) 1253 spec.properties ; 1254 emitln out " }" ; 1255 if last then begin 1256 emitln out "[@@deriving yojson {strict= false}]" ; 1257 emit_newline out 1258 end 1259 end 1260 in 1261 (* generate union type for merged context *) 1262 let gen_merged_union_type current_nsid name (spec : union_spec) = 1263 let type_name = get_unique_type_name current_nsid name in 1264 let is_closed = Option.value spec.closed ~default:false in 1265 emitln out (Printf.sprintf "type %s =" type_name) ; 1266 List.iter 1267 (fun ref_str -> 1268 let variant_name = Naming.variant_name_of_ref ref_str in 1269 let payload_type = gen_merged_ref_type current_nsid ref_str in 1270 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 1271 spec.refs ; 1272 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ; 1273 emit_newline out ; 1274 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 1275 emitln out " let open Yojson.Safe.Util in" ; 1276 emitln out " try" ; 1277 emitln out " match json |> member \"$type\" |> to_string with" ; 1278 List.iter 1279 (fun ref_str -> 1280 let variant_name = Naming.variant_name_of_ref ref_str in 1281 let full_type_uri = gen_merged_type_uri current_nsid ref_str in 1282 let payload_type = gen_merged_ref_type current_nsid ref_str in 1283 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 1284 emitln out 1285 (Printf.sprintf " (match %s_of_yojson json with" payload_type) ; 1286 emitln out (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 1287 emitln out " | Error e -> Error e)" ) 1288 spec.refs ; 1289 if is_closed then 1290 emitln out " | t -> Error (\"unknown union type: \" ^ t)" 1291 else emitln out " | _ -> Ok (Unknown json)" ; 1292 emitln out " with _ -> Error \"failed to parse union\"" ; 1293 emit_newline out ; 1294 emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ; 1295 List.iter 1296 (fun ref_str -> 1297 let variant_name = Naming.variant_name_of_ref ref_str in 1298 let full_type_uri = gen_merged_type_uri current_nsid ref_str in 1299 let payload_type = gen_merged_ref_type current_nsid ref_str in 1300 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 1301 emitln out 1302 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 1303 emitln out 1304 (Printf.sprintf 1305 " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") :: \ 1306 fields)" 1307 full_type_uri ) ; 1308 emitln out " | other -> other)" ) 1309 spec.refs ; 1310 if not is_closed then emitln out " | Unknown j -> j" ; 1311 emit_newline out 1312 in 1313 (* collect refs for merged SCC detection, using compound keys (nsid#name) *) 1314 let collect_merged_local_refs current_nsid acc type_def = 1315 let rec aux acc = function 1316 | Array {items; _} -> 1317 aux acc items 1318 | Ref {ref_; _} -> 1319 if String.length ref_ > 0 && ref_.[0] = '#' then 1320 (* local ref: #foo -> current_nsid#foo *) 1321 let def_name = String.sub ref_ 1 (String.length ref_ - 1) in 1322 (current_nsid ^ "#" ^ def_name) :: acc 1323 else begin 1324 match String.split_on_char '#' ref_ with 1325 | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids -> 1326 (* cross-nsid ref within merged group *) 1327 (ext_nsid ^ "#" ^ def_name) :: acc 1328 | _ -> 1329 acc 1330 end 1331 | Union {refs; _} -> 1332 List.fold_left 1333 (fun a r -> 1334 if String.length r > 0 && r.[0] = '#' then 1335 let def_name = String.sub r 1 (String.length r - 1) in 1336 (current_nsid ^ "#" ^ def_name) :: a 1337 else 1338 match String.split_on_char '#' r with 1339 | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids -> 1340 (ext_nsid ^ "#" ^ def_name) :: a 1341 | _ -> 1342 a ) 1343 acc refs 1344 | Object {properties; _} -> 1345 List.fold_left 1346 (fun a (_, (prop : property)) -> aux a prop.type_def) 1347 acc properties 1348 | Record {record; _} -> 1349 List.fold_left 1350 (fun a (_, (prop : property)) -> aux a prop.type_def) 1351 acc record.properties 1352 | Query {parameters; output; _} -> ( 1353 let acc = 1354 match parameters with 1355 | Some params -> 1356 List.fold_left 1357 (fun a (_, (prop : property)) -> aux a prop.type_def) 1358 acc params.properties 1359 | None -> 1360 acc 1361 in 1362 match output with 1363 | Some body -> 1364 Option.fold ~none:acc ~some:(aux acc) body.schema 1365 | None -> 1366 acc ) 1367 | Procedure {parameters; input; output; _} -> ( 1368 let acc = 1369 match parameters with 1370 | Some params -> 1371 List.fold_left 1372 (fun a (_, (prop : property)) -> aux a prop.type_def) 1373 acc params.properties 1374 | None -> 1375 acc 1376 in 1377 let acc = 1378 match input with 1379 | Some body -> 1380 Option.fold ~none:acc ~some:(aux acc) body.schema 1381 | None -> 1382 acc 1383 in 1384 match output with 1385 | Some body -> 1386 Option.fold ~none:acc ~some:(aux acc) body.schema 1387 | None -> 1388 acc ) 1389 | _ -> 1390 acc 1391 in 1392 aux acc type_def 1393 in 1394 (* generate merged SCC *) 1395 let gen_merged_scc scc = 1396 match scc with 1397 | [] -> 1398 () 1399 | [(nsid, def)] -> ( 1400 match def.type_def with 1401 | Object spec -> 1402 gen_merged_object_type nsid def.name spec 1403 | Union spec -> 1404 gen_merged_union_type nsid def.name spec 1405 | Token spec -> 1406 gen_token nsid out def.name spec 1407 | Query spec -> 1408 gen_query nsid out def.name spec 1409 | Procedure spec -> 1410 gen_procedure nsid out def.name spec 1411 | Record spec -> 1412 gen_merged_object_type nsid def.name spec.record 1413 | String spec when spec.known_values <> None -> 1414 gen_string_type out def.name spec 1415 | Array {items; _} -> 1416 (* generate inline union for array items if needed *) 1417 ( match items with 1418 | Union spec -> 1419 let item_type_name = Naming.type_name (def.name ^ "_item") in 1420 register_union_name out spec.refs item_type_name ; 1421 gen_merged_union_type nsid (def.name ^ "_item") spec 1422 | _ -> 1423 () ) ; 1424 (* generate type alias for array *) 1425 let type_name = get_unique_type_name nsid def.name in 1426 let item_type = gen_merged_type_ref nsid items in 1427 emitln out (Printf.sprintf "type %s = %s list" type_name item_type) ; 1428 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 1429 emitln out " let open Yojson.Safe.Util in" ; 1430 emitln out 1431 (Printf.sprintf 1432 " Ok (to_list json |> List.filter_map (fun x -> match \ 1433 %s_of_yojson x with Ok v -> Some v | _ -> None))" 1434 item_type ) ; 1435 emitln out 1436 (Printf.sprintf 1437 "let %s_to_yojson l = `List (List.map %s_to_yojson l)" type_name 1438 item_type ) ; 1439 emit_newline out 1440 | _ -> 1441 () ) 1442 | defs -> 1443 (* multi-def SCC - register inline union names first *) 1444 List.iter 1445 (fun (nsid, def) -> 1446 match def.type_def with 1447 | Object spec -> 1448 register_merged_inline_unions nsid spec.properties 1449 | Record spec -> 1450 register_merged_inline_unions nsid spec.record.properties 1451 | _ -> 1452 () ) 1453 defs ; 1454 let obj_defs = 1455 List.filter 1456 (fun (_, def) -> 1457 match def.type_def with Object _ | Record _ -> true | _ -> false ) 1458 defs 1459 in 1460 let other_defs = 1461 List.filter 1462 (fun (_, def) -> 1463 match def.type_def with Object _ | Record _ -> false | _ -> true ) 1464 defs 1465 in 1466 List.iter 1467 (fun (nsid, def) -> 1468 match def.type_def with 1469 | Union spec -> 1470 gen_merged_union_type nsid def.name spec 1471 | Token spec -> 1472 gen_token nsid out def.name spec 1473 | Query spec -> 1474 gen_query nsid out def.name spec 1475 | Procedure spec -> 1476 gen_procedure nsid out def.name spec 1477 | String spec when spec.known_values <> None -> 1478 gen_string_type out def.name spec 1479 | _ -> 1480 () ) 1481 other_defs ; 1482 let n = List.length obj_defs in 1483 List.iteri 1484 (fun i (nsid, def) -> 1485 let first = i = 0 in 1486 let last = i = n - 1 in 1487 match def.type_def with 1488 | Object spec -> 1489 let required = Option.value spec.required ~default:[] in 1490 let nullable = Option.value spec.nullable ~default:[] in 1491 let keyword = if first then "type" else "and" in 1492 let type_name = get_unique_type_name nsid def.name in 1493 if spec.properties = [] then begin 1494 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ; 1495 if last then begin 1496 emitln out "[@@deriving yojson {strict= false}]" ; 1497 emit_newline out 1498 end 1499 end 1500 else begin 1501 emitln out (Printf.sprintf "%s %s =" keyword type_name) ; 1502 emitln out " {" ; 1503 List.iter 1504 (fun (prop_name, (prop : property)) -> 1505 let ocaml_name = Naming.field_name prop_name in 1506 let base_type = gen_merged_type_ref nsid prop.type_def in 1507 let is_required = List.mem prop_name required in 1508 let is_nullable = List.mem prop_name nullable in 1509 let type_str = 1510 if is_required && not is_nullable then base_type 1511 else base_type ^ " option" 1512 in 1513 let key_attr = 1514 Naming.key_annotation prop_name ocaml_name 1515 in 1516 let default_attr = 1517 if is_required && not is_nullable then "" 1518 else " [@default None]" 1519 in 1520 emitln out 1521 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str 1522 key_attr default_attr ) ) 1523 spec.properties ; 1524 emitln out " }" ; 1525 if last then begin 1526 emitln out "[@@deriving yojson {strict= false}]" ; 1527 emit_newline out 1528 end 1529 end 1530 | Record spec -> 1531 let obj_spec = spec.record in 1532 let required = Option.value obj_spec.required ~default:[] in 1533 let nullable = Option.value obj_spec.nullable ~default:[] in 1534 let keyword = if first then "type" else "and" in 1535 let type_name = get_unique_type_name nsid def.name in 1536 if obj_spec.properties = [] then begin 1537 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ; 1538 if last then begin 1539 emitln out "[@@deriving yojson {strict= false}]" ; 1540 emit_newline out 1541 end 1542 end 1543 else begin 1544 emitln out (Printf.sprintf "%s %s =" keyword type_name) ; 1545 emitln out " {" ; 1546 List.iter 1547 (fun (prop_name, (prop : property)) -> 1548 let ocaml_name = Naming.field_name prop_name in 1549 let base_type = gen_merged_type_ref nsid prop.type_def in 1550 let is_required = List.mem prop_name required in 1551 let is_nullable = List.mem prop_name nullable in 1552 let type_str = 1553 if is_required && not is_nullable then base_type 1554 else base_type ^ " option" 1555 in 1556 let key_attr = 1557 Naming.key_annotation prop_name ocaml_name 1558 in 1559 let default_attr = 1560 if is_required && not is_nullable then "" 1561 else " [@default None]" 1562 in 1563 emitln out 1564 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str 1565 key_attr default_attr ) ) 1566 obj_spec.properties ; 1567 emitln out " }" ; 1568 if last then begin 1569 emitln out "[@@deriving yojson {strict= false}]" ; 1570 emit_newline out 1571 end 1572 end 1573 | _ -> 1574 () ) 1575 obj_defs 1576 in 1577 (* create extended defs that include inline unions as pseudo-entries *) 1578 (* inline union key format: nsid#__inline__name *) 1579 let inline_union_defs = 1580 List.map 1581 (fun (nsid, name, refs, spec) -> 1582 let key = nsid ^ "#__inline__" ^ name in 1583 (* inline unions depend on the types they reference *) 1584 let deps = 1585 List.filter_map 1586 (fun r -> 1587 if String.length r > 0 && r.[0] = '#' then 1588 let def_name = String.sub r 1 (String.length r - 1) in 1589 Some (nsid ^ "#" ^ def_name) 1590 else 1591 match String.split_on_char '#' r with 1592 | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids -> 1593 Some (ext_nsid ^ "#" ^ def_name) 1594 | _ -> 1595 None ) 1596 refs 1597 in 1598 (key, deps, `InlineUnion (nsid, name, refs, spec)) ) 1599 all_inline_unions 1600 in 1601 (* create regular def entries *) 1602 let regular_def_entries = 1603 List.map 1604 (fun (nsid, def) -> 1605 let key = nsid ^ "#" ^ def.name in 1606 let base_deps = collect_merged_local_refs nsid [] def.type_def in 1607 (* add dependencies on inline unions used by this def *) 1608 let inline_deps = 1609 match def.type_def with 1610 | Object spec | Record {record= spec; _} -> 1611 let rec collect_inline_union_deps acc type_def = 1612 match type_def with 1613 | Union _ -> ( 1614 (* this property uses an inline union - find its name *) 1615 match lookup_union_name out [] with 1616 | _ -> 1617 acc (* we'll handle this differently *) ) 1618 | Array {items; _} -> 1619 collect_inline_union_deps acc items 1620 | _ -> 1621 acc 1622 in 1623 List.fold_left 1624 (fun acc (prop_name, (prop : property)) -> 1625 match prop.type_def with 1626 | Union _ -> 1627 let union_name = Naming.type_name prop_name in 1628 (nsid ^ "#__inline__" ^ union_name) :: acc 1629 | Array {items= Union _; _} -> 1630 let union_name = Naming.type_name (prop_name ^ "_item") in 1631 (nsid ^ "#__inline__" ^ union_name) :: acc 1632 | _ -> 1633 collect_inline_union_deps acc prop.type_def ) 1634 [] spec.properties 1635 | _ -> 1636 [] 1637 in 1638 (key, base_deps @ inline_deps, `RegularDef (nsid, def)) ) 1639 all_defs 1640 in 1641 (* combine all entries *) 1642 let all_entries = regular_def_entries @ inline_union_defs in 1643 (* build dependency map *) 1644 let deps_map = List.map (fun (k, deps, _) -> (k, deps)) all_entries in 1645 let entry_map = List.map (fun (k, _, entry) -> (k, entry)) all_entries in 1646 let all_keys = List.map (fun (k, _, _) -> k) all_entries in 1647 (* run Tarjan's algorithm on combined entries *) 1648 let index_counter = ref 0 in 1649 let indices = Hashtbl.create 64 in 1650 let lowlinks = Hashtbl.create 64 in 1651 let on_stack = Hashtbl.create 64 in 1652 let stack = ref [] in 1653 let sccs = ref [] in 1654 let rec strongconnect key = 1655 let index = !index_counter in 1656 incr index_counter ; 1657 Hashtbl.add indices key index ; 1658 Hashtbl.add lowlinks key index ; 1659 Hashtbl.add on_stack key true ; 1660 stack := key :: !stack ; 1661 let successors = 1662 try List.assoc key deps_map |> List.filter (fun k -> List.mem k all_keys) 1663 with Not_found -> [] 1664 in 1665 List.iter 1666 (fun succ -> 1667 if not (Hashtbl.mem indices succ) then begin 1668 strongconnect succ ; 1669 Hashtbl.replace lowlinks key 1670 (min (Hashtbl.find lowlinks key) (Hashtbl.find lowlinks succ)) 1671 end 1672 else if Hashtbl.find_opt on_stack succ = Some true then 1673 Hashtbl.replace lowlinks key 1674 (min (Hashtbl.find lowlinks key) (Hashtbl.find indices succ)) ) 1675 successors ; 1676 if Hashtbl.find lowlinks key = Hashtbl.find indices key then begin 1677 let rec pop_scc acc = 1678 match !stack with 1679 | [] -> 1680 acc 1681 | top :: rest -> 1682 stack := rest ; 1683 Hashtbl.replace on_stack top false ; 1684 if top = key then top :: acc else pop_scc (top :: acc) 1685 in 1686 let scc_keys = pop_scc [] in 1687 let scc_entries = 1688 List.filter_map (fun k -> List.assoc_opt k entry_map) scc_keys 1689 in 1690 if scc_entries <> [] then sccs := scc_entries :: !sccs 1691 end 1692 in 1693 List.iter 1694 (fun key -> if not (Hashtbl.mem indices key) then strongconnect key) 1695 all_keys ; 1696 let ordered_sccs = List.rev !sccs in 1697 (* helper to generate object type definition only (no converters) *) 1698 let gen_object_type_only ?(keyword = "type") nsid name (spec : object_spec) = 1699 let required = Option.value spec.required ~default:[] in 1700 let nullable = Option.value spec.nullable ~default:[] in 1701 let type_name = get_unique_type_name nsid name in 1702 if spec.properties = [] then 1703 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) 1704 else begin 1705 emitln out (Printf.sprintf "%s %s = {" keyword type_name) ; 1706 List.iter 1707 (fun (prop_name, (prop : property)) -> 1708 let ocaml_name = Naming.field_name prop_name in 1709 let base_type = gen_merged_type_ref nsid prop.type_def in 1710 let is_required = List.mem prop_name required in 1711 let is_nullable = List.mem prop_name nullable in 1712 let type_str = 1713 if is_required && not is_nullable then base_type 1714 else base_type ^ " option" 1715 in 1716 let key_attr = Naming.key_annotation prop_name ocaml_name in 1717 let default_attr = 1718 if is_required && not is_nullable then "" else " [@default None]" 1719 in 1720 emitln out 1721 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 1722 default_attr ) ) 1723 spec.properties ; 1724 emitln out "}" 1725 end 1726 in 1727 (* helper to generate inline union type definition only (no converters) *) 1728 let gen_inline_union_type_only ?(keyword = "type") nsid name refs spec = 1729 let is_closed = Option.value spec.closed ~default:false in 1730 emitln out (Printf.sprintf "%s %s =" keyword name) ; 1731 List.iter 1732 (fun ref_str -> 1733 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 1734 let payload_type = gen_merged_ref_type nsid ref_str in 1735 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 1736 refs ; 1737 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" 1738 in 1739 (* helper to generate object converters *) 1740 let gen_object_converters ?(of_keyword = "let") ?(to_keyword = "let") nsid 1741 name (spec : object_spec) = 1742 let required = Option.value spec.required ~default:[] in 1743 let nullable = Option.value spec.nullable ~default:[] in 1744 let type_name = get_unique_type_name nsid name in 1745 if spec.properties = [] then begin 1746 if of_keyword <> "SKIP" then 1747 emitln out 1748 (Printf.sprintf "%s %s_of_yojson _ = Ok ()" of_keyword type_name) ; 1749 if to_keyword <> "SKIP" then 1750 emitln out 1751 (Printf.sprintf "%s %s_to_yojson () = `Assoc []" to_keyword type_name) 1752 end 1753 else begin 1754 (* of_yojson *) 1755 if of_keyword <> "SKIP" then begin 1756 emitln out 1757 (Printf.sprintf "%s %s_of_yojson json =" of_keyword type_name) ; 1758 emitln out " let open Yojson.Safe.Util in" ; 1759 emitln out " try" ; 1760 List.iter 1761 (fun (prop_name, (prop : property)) -> 1762 let ocaml_name = Naming.field_name prop_name in 1763 let conv_expr, needs_unwrap = 1764 gen_of_yojson_expr nsid prop.type_def 1765 in 1766 let is_required = List.mem prop_name required in 1767 let is_nullable = List.mem prop_name nullable in 1768 let is_optional = (not is_required) || is_nullable in 1769 if is_optional then begin 1770 if needs_unwrap then 1771 emitln out 1772 (Printf.sprintf 1773 " let %s = json |> member \"%s\" |> to_option (fun x \ 1774 -> match %s x with Ok v -> Some v | _ -> None) |> \ 1775 Option.join in" 1776 ocaml_name prop_name conv_expr ) 1777 else 1778 emitln out 1779 (Printf.sprintf 1780 " let %s = json |> member \"%s\" |> to_option %s in" 1781 ocaml_name prop_name conv_expr ) 1782 end 1783 else begin 1784 if needs_unwrap then 1785 emitln out 1786 (Printf.sprintf 1787 " let %s = json |> member \"%s\" |> %s |> \ 1788 Result.get_ok in" 1789 ocaml_name prop_name conv_expr ) 1790 else 1791 emitln out 1792 (Printf.sprintf " let %s = json |> member \"%s\" |> %s in" 1793 ocaml_name prop_name conv_expr ) 1794 end ) 1795 spec.properties ; 1796 emit out " Ok { " ; 1797 emit out 1798 (String.concat "; " 1799 (List.map (fun (pn, _) -> Naming.field_name pn) spec.properties) ) ; 1800 emitln out " }" ; 1801 emitln out " with e -> Error (Printexc.to_string e)" ; 1802 emit_newline out 1803 end ; 1804 (* to_yojson *) 1805 if to_keyword <> "SKIP" then begin 1806 emitln out 1807 (Printf.sprintf "%s %s_to_yojson (r : %s) =" to_keyword type_name 1808 type_name ) ; 1809 emitln out " `Assoc [" ; 1810 List.iteri 1811 (fun i (prop_name, (prop : property)) -> 1812 let ocaml_name = Naming.field_name prop_name in 1813 let conv_expr = gen_to_yojson_expr nsid prop.type_def in 1814 let is_required = List.mem prop_name required in 1815 let is_nullable = List.mem prop_name nullable in 1816 let is_optional = (not is_required) || is_nullable in 1817 let comma = 1818 if i < List.length spec.properties - 1 then ";" else "" 1819 in 1820 if is_optional then 1821 emitln out 1822 (Printf.sprintf 1823 " (\"%s\", match r.%s with Some v -> %s v | None -> \ 1824 `Null)%s" 1825 prop_name ocaml_name conv_expr comma ) 1826 else 1827 emitln out 1828 (Printf.sprintf " (\"%s\", %s r.%s)%s" prop_name conv_expr 1829 ocaml_name comma ) ) 1830 spec.properties ; 1831 emitln out " ]" ; 1832 emit_newline out 1833 end 1834 end 1835 in 1836 (* helper to generate inline union converters *) 1837 let gen_inline_union_converters ?(of_keyword = "let") ?(to_keyword = "let") 1838 nsid name refs spec = 1839 let is_closed = Option.value spec.closed ~default:false in 1840 (* of_yojson *) 1841 if of_keyword <> "SKIP" then begin 1842 emitln out (Printf.sprintf "%s %s_of_yojson json =" of_keyword name) ; 1843 emitln out " let open Yojson.Safe.Util in" ; 1844 emitln out " try" ; 1845 emitln out " match json |> member \"$type\" |> to_string with" ; 1846 List.iter 1847 (fun ref_str -> 1848 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 1849 let full_type_uri = gen_merged_type_uri nsid ref_str in 1850 let payload_type = gen_merged_ref_type nsid ref_str in 1851 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 1852 emitln out 1853 (Printf.sprintf " (match %s_of_yojson json with" payload_type) ; 1854 emitln out 1855 (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 1856 emitln out " | Error e -> Error e)" ) 1857 refs ; 1858 if is_closed then 1859 emitln out " | t -> Error (\"unknown union type: \" ^ t)" 1860 else emitln out " | _ -> Ok (Unknown json)" ; 1861 emitln out " with _ -> Error \"failed to parse union\"" ; 1862 emit_newline out 1863 end ; 1864 (* to_yojson *) 1865 if to_keyword <> "SKIP" then begin 1866 emitln out (Printf.sprintf "%s %s_to_yojson = function" to_keyword name) ; 1867 List.iter 1868 (fun ref_str -> 1869 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 1870 let full_type_uri = gen_merged_type_uri nsid ref_str in 1871 let payload_type = gen_merged_ref_type nsid ref_str in 1872 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 1873 emitln out 1874 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 1875 emitln out 1876 (Printf.sprintf 1877 " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") \ 1878 :: fields)" 1879 full_type_uri ) ; 1880 emitln out " | other -> other)" ) 1881 refs ; 1882 if not is_closed then emitln out " | Unknown j -> j" ; 1883 emit_newline out 1884 end 1885 in 1886 (* generate each SCC *) 1887 List.iter 1888 (fun scc -> 1889 (* separate inline unions from regular defs *) 1890 let inline_unions_in_scc = 1891 List.filter_map (function `InlineUnion x -> Some x | _ -> None) scc 1892 in 1893 let regular_defs_in_scc = 1894 List.filter_map (function `RegularDef x -> Some x | _ -> None) scc 1895 in 1896 if inline_unions_in_scc = [] then begin 1897 (* no inline unions - use standard generation with [@@deriving yojson] *) 1898 if regular_defs_in_scc <> [] then gen_merged_scc regular_defs_in_scc 1899 end 1900 else begin 1901 (* has inline unions - generate all types first, then all converters *) 1902 (* register inline union names *) 1903 List.iter 1904 (fun (nsid, name, refs, _spec) -> 1905 let unique_name = get_unique_inline_union_name nsid name in 1906 register_union_name out refs unique_name ; 1907 mark_union_generated out unique_name ) 1908 inline_unions_in_scc ; 1909 (* collect all items to generate *) 1910 let all_items = 1911 List.map (fun x -> `Inline x) inline_unions_in_scc 1912 @ List.map (fun x -> `Regular x) regular_defs_in_scc 1913 in 1914 let n = List.length all_items in 1915 if n = 1 then begin 1916 (* single item - generate normally *) 1917 match List.hd all_items with 1918 | `Inline (nsid, name, refs, spec) -> 1919 let unique_name = get_unique_inline_union_name nsid name in 1920 gen_inline_union_type_only nsid unique_name refs spec ; 1921 emit_newline out ; 1922 gen_inline_union_converters nsid unique_name refs spec 1923 | `Regular (nsid, def) -> ( 1924 match def.type_def with 1925 | Object spec -> 1926 register_merged_inline_unions nsid spec.properties ; 1927 gen_object_type_only nsid def.name spec ; 1928 emit_newline out ; 1929 gen_object_converters nsid def.name spec 1930 | Record rspec -> 1931 register_merged_inline_unions nsid rspec.record.properties ; 1932 gen_object_type_only nsid def.name rspec.record ; 1933 emit_newline out ; 1934 gen_object_converters nsid def.name rspec.record 1935 | _ -> 1936 gen_merged_scc [(nsid, def)] ) 1937 end 1938 else begin 1939 (* multiple items - generate as mutually recursive types *) 1940 (* first pass: register inline unions from objects *) 1941 List.iter 1942 (function 1943 | `Regular (nsid, def) -> ( 1944 match def.type_def with 1945 | Object spec -> 1946 register_merged_inline_unions nsid spec.properties 1947 | Record rspec -> 1948 register_merged_inline_unions nsid rspec.record.properties 1949 | _ -> 1950 () ) 1951 | `Inline _ -> 1952 () ) 1953 all_items ; 1954 (* second pass: generate all type definitions *) 1955 List.iteri 1956 (fun i item -> 1957 let keyword = if i = 0 then "type" else "and" in 1958 match item with 1959 | `Inline (nsid, name, refs, spec) -> 1960 let unique_name = get_unique_inline_union_name nsid name in 1961 gen_inline_union_type_only ~keyword nsid unique_name refs spec 1962 | `Regular (nsid, def) -> ( 1963 match def.type_def with 1964 | Object spec -> 1965 gen_object_type_only ~keyword nsid def.name spec 1966 | Record rspec -> 1967 gen_object_type_only ~keyword nsid def.name rspec.record 1968 | _ -> 1969 () ) ) 1970 all_items ; 1971 emit_newline out ; 1972 (* third pass: generate all _of_yojson converters as mutually recursive *) 1973 List.iteri 1974 (fun i item -> 1975 let of_keyword = if i = 0 then "let rec" else "and" in 1976 match item with 1977 | `Inline (nsid, name, refs, spec) -> 1978 let unique_name = get_unique_inline_union_name nsid name in 1979 gen_inline_union_converters ~of_keyword ~to_keyword:"SKIP" 1980 nsid unique_name refs spec 1981 | `Regular (nsid, def) -> ( 1982 match def.type_def with 1983 | Object spec -> 1984 gen_object_converters ~of_keyword ~to_keyword:"SKIP" nsid 1985 def.name spec 1986 | Record rspec -> 1987 gen_object_converters ~of_keyword ~to_keyword:"SKIP" nsid 1988 def.name rspec.record 1989 | _ -> 1990 () ) ) 1991 all_items ; 1992 (* fourth pass: generate all _to_yojson converters as mutually recursive *) 1993 List.iteri 1994 (fun i item -> 1995 let to_keyword = if i = 0 then "and" else "and" in 1996 match item with 1997 | `Inline (nsid, name, refs, spec) -> 1998 let unique_name = get_unique_inline_union_name nsid name in 1999 gen_inline_union_converters ~of_keyword:"SKIP" ~to_keyword 2000 nsid unique_name refs spec 2001 | `Regular (nsid, def) -> ( 2002 match def.type_def with 2003 | Object spec -> 2004 gen_object_converters ~of_keyword:"SKIP" ~to_keyword nsid 2005 def.name spec 2006 | Record rspec -> 2007 gen_object_converters ~of_keyword:"SKIP" ~to_keyword nsid 2008 def.name rspec.record 2009 | _ -> 2010 () ) ) 2011 all_items 2012 end 2013 end ) 2014 ordered_sccs ; 2015 Emitter.contents out 2016 2017(* generate a re-export stub that selectively exports types from a merged module *) 2018let gen_reexport_stub ~merged_module_name ~all_merged_docs (doc : lexicon_doc) : 2019 string = 2020 let buf = Buffer.create 1024 in 2021 let emit s = Buffer.add_string buf s in 2022 let emitln s = Buffer.add_string buf s ; Buffer.add_char buf '\n' in 2023 (* detect collisions across all merged docs *) 2024 let all_defs = 2025 List.concat_map 2026 (fun d -> List.map (fun def -> (d.id, def)) d.defs) 2027 all_merged_docs 2028 in 2029 let name_counts = Hashtbl.create 64 in 2030 List.iter 2031 (fun (nsid, def) -> 2032 let existing = Hashtbl.find_opt name_counts def.name in 2033 match existing with 2034 | None -> 2035 Hashtbl.add name_counts def.name [nsid] 2036 | Some nsids when not (List.mem nsid nsids) -> 2037 Hashtbl.replace name_counts def.name (nsid :: nsids) 2038 | _ -> 2039 () ) 2040 all_defs ; 2041 let colliding_names = 2042 Hashtbl.fold 2043 (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc) 2044 name_counts [] 2045 in 2046 (* the "host" nsid is the first one - types from here keep short names *) 2047 let host_nsid = (List.hd all_merged_docs).id in 2048 let get_unique_type_name nsid def_name = 2049 if List.mem def_name colliding_names && nsid <> host_nsid then 2050 let prefix = Naming.flat_name_of_nsid nsid ^ "_" in 2051 Naming.type_name (prefix ^ def_name) 2052 else Naming.type_name def_name 2053 in 2054 emitln (Printf.sprintf "(* re-exported from %s *)" merged_module_name) ; 2055 emitln "" ; 2056 List.iter 2057 (fun def -> 2058 let local_type_name = Naming.type_name def.name in 2059 let merged_type_name = get_unique_type_name doc.id def.name in 2060 match def.type_def with 2061 | Object _ | Record _ | Union _ -> 2062 (* type alias and converter aliases *) 2063 emitln 2064 (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name 2065 merged_type_name ) ; 2066 emitln 2067 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 2068 merged_module_name merged_type_name ) ; 2069 emitln 2070 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 2071 merged_module_name merged_type_name ) ; 2072 emit "\n" 2073 | String spec when spec.known_values <> None -> 2074 emitln 2075 (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name 2076 merged_type_name ) ; 2077 emitln 2078 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 2079 merged_module_name merged_type_name ) ; 2080 emitln 2081 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 2082 merged_module_name merged_type_name ) ; 2083 emit "\n" 2084 | Array _ -> 2085 (* re-export array type alias and converters *) 2086 emitln 2087 (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name 2088 merged_type_name ) ; 2089 emitln 2090 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 2091 merged_module_name merged_type_name ) ; 2092 emitln 2093 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 2094 merged_module_name merged_type_name ) ; 2095 emit "\n" 2096 | Token _ -> 2097 emitln 2098 (Printf.sprintf "let %s = %s.%s" local_type_name merged_module_name 2099 merged_type_name ) ; 2100 emit "\n" 2101 | Query _ | Procedure _ -> 2102 let mod_name = Naming.def_module_name def.name in 2103 emitln 2104 (Printf.sprintf "module %s = %s.%s" mod_name merged_module_name 2105 mod_name ) ; 2106 emit "\n" 2107 | _ -> 2108 () ) 2109 doc.defs ; 2110 Buffer.contents buf 2111 2112(* generate a shared module for mutually recursive lexicons *) 2113(* uses Naming.shared_type_name for context-based naming instead of full nsid prefix *) 2114let gen_shared_module (docs : lexicon_doc list) : string = 2115 let out = make_output () in 2116 (* collect all nsids in this shared group *) 2117 let shared_nsids = List.map (fun d -> d.id) docs in 2118 (* header *) 2119 emitln out 2120 (Printf.sprintf "(* shared module for lexicons: %s *)" 2121 (String.concat ", " shared_nsids) ) ; 2122 emit_newline out ; 2123 (* collect all defs from all docs *) 2124 let all_defs = 2125 List.concat_map 2126 (fun doc -> List.map (fun def -> (doc.id, def)) doc.defs) 2127 docs 2128 in 2129 (* detect name collisions - names that appear in multiple nsids *) 2130 let name_counts = Hashtbl.create 64 in 2131 List.iter 2132 (fun (nsid, def) -> 2133 let existing = Hashtbl.find_opt name_counts def.name in 2134 match existing with 2135 | None -> 2136 Hashtbl.add name_counts def.name [nsid] 2137 | Some nsids when not (List.mem nsid nsids) -> 2138 Hashtbl.replace name_counts def.name (nsid :: nsids) 2139 | _ -> 2140 () ) 2141 all_defs ; 2142 let colliding_names = 2143 Hashtbl.fold 2144 (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc) 2145 name_counts [] 2146 in 2147 (* also detect inline union name collisions *) 2148 let rec collect_inline_union_contexts nsid context acc type_def = 2149 match type_def with 2150 | Union spec -> 2151 (nsid, context, spec.refs) :: acc 2152 | Array {items; _} -> 2153 collect_inline_union_contexts nsid (context ^ "_item") acc items 2154 | Object {properties; _} -> 2155 List.fold_left 2156 (fun a (prop_name, (prop : property)) -> 2157 collect_inline_union_contexts nsid prop_name a prop.type_def ) 2158 acc properties 2159 | _ -> 2160 acc 2161 in 2162 let all_inline_union_contexts = 2163 List.concat_map 2164 (fun (nsid, def) -> 2165 match def.type_def with 2166 | Object spec -> 2167 List.fold_left 2168 (fun acc (prop_name, (prop : property)) -> 2169 collect_inline_union_contexts nsid prop_name acc prop.type_def ) 2170 [] spec.properties 2171 | Record rspec -> 2172 List.fold_left 2173 (fun acc (prop_name, (prop : property)) -> 2174 collect_inline_union_contexts nsid prop_name acc prop.type_def ) 2175 [] rspec.record.properties 2176 | _ -> 2177 [] ) 2178 all_defs 2179 in 2180 (* group inline unions by context name *) 2181 let inline_union_by_context = Hashtbl.create 64 in 2182 List.iter 2183 (fun (nsid, context, refs) -> 2184 let key = Naming.type_name context in 2185 let sorted_refs = List.sort String.compare refs in 2186 let existing = Hashtbl.find_opt inline_union_by_context key in 2187 match existing with 2188 | None -> 2189 Hashtbl.add inline_union_by_context key [(nsid, sorted_refs)] 2190 | Some entries -> 2191 (* collision if different nsid OR different refs *) 2192 if 2193 not 2194 (List.exists (fun (n, r) -> n = nsid && r = sorted_refs) entries) 2195 then 2196 Hashtbl.replace inline_union_by_context key 2197 ((nsid, sorted_refs) :: entries) ) 2198 all_inline_union_contexts ; 2199 (* add inline union collisions to colliding_names *) 2200 let colliding_names = 2201 Hashtbl.fold 2202 (fun name entries acc -> 2203 (* collision if more than one entry (different nsid or different refs) *) 2204 if List.length entries > 1 then name :: acc else acc ) 2205 inline_union_by_context colliding_names 2206 in 2207 (* function to get unique type name using shared_type_name for collisions *) 2208 let get_shared_type_name nsid def_name = 2209 if List.mem def_name colliding_names then 2210 (* use context-based name: e.g., feed_viewer_state *) 2211 Naming.shared_type_name nsid def_name 2212 else 2213 (* no collision, use simple name *) 2214 Naming.type_name def_name 2215 in 2216 (* custom ref type generator that treats shared nsids as local *) 2217 let rec gen_shared_type_ref current_nsid type_def = 2218 match type_def with 2219 | String _ -> 2220 "string" 2221 | Integer {maximum; _} -> ( 2222 match maximum with Some m when m > 1073741823 -> "int64" | _ -> "int" ) 2223 | Boolean _ -> 2224 "bool" 2225 | Bytes _ -> 2226 "bytes" 2227 | Blob _ -> 2228 "Hermes.blob" 2229 | CidLink _ -> 2230 "Cid.t" 2231 | Array {items; _} -> 2232 let item_type = gen_shared_type_ref current_nsid items in 2233 item_type ^ " list" 2234 | Object _ -> 2235 "object_todo" 2236 | Ref {ref_; _} -> 2237 gen_shared_ref_type current_nsid ref_ 2238 | Union {refs; _} -> ( 2239 match lookup_union_name out refs with 2240 | Some name -> 2241 name 2242 | None -> 2243 gen_union_type_name refs ) 2244 | Token _ -> 2245 "string" 2246 | Unknown _ -> 2247 "Yojson.Safe.t" 2248 | Query _ | Procedure _ | Subscription _ | Record _ -> 2249 "unit (* primary type *)" 2250 and gen_shared_ref_type current_nsid ref_str = 2251 if String.length ref_str > 0 && ref_str.[0] = '#' then begin 2252 (* local ref within same nsid *) 2253 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in 2254 get_shared_type_name current_nsid def_name 2255 end 2256 else begin 2257 match String.split_on_char '#' ref_str with 2258 | [ext_nsid; def_name] -> 2259 if List.mem ext_nsid shared_nsids then 2260 (* ref to another nsid in the shared group *) 2261 get_shared_type_name ext_nsid def_name 2262 else begin 2263 (* truly external ref *) 2264 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 2265 add_import out flat_module ; 2266 flat_module ^ "." ^ Naming.type_name def_name 2267 end 2268 | [ext_nsid] -> 2269 if List.mem ext_nsid shared_nsids then 2270 get_shared_type_name ext_nsid "main" 2271 else begin 2272 let flat_module = Naming.flat_module_name_of_nsid ext_nsid in 2273 add_import out flat_module ; flat_module ^ ".main" 2274 end 2275 | _ -> 2276 "invalid_ref" 2277 end 2278 in 2279 (* generate type uri for shared context *) 2280 let gen_shared_type_uri current_nsid ref_str = 2281 if String.length ref_str > 0 && ref_str.[0] = '#' then 2282 current_nsid ^ ref_str 2283 else ref_str 2284 in 2285 (* generate converter expression for reading a type from json *) 2286 let gen_shared_of_yojson_expr current_nsid type_def = 2287 match type_def with 2288 | String _ | Token _ -> 2289 ("to_string", false) 2290 | Integer {maximum; _} -> ( 2291 match maximum with 2292 | Some m when m > 1073741823 -> 2293 ("(fun j -> Int64.of_int (to_int j))", false) 2294 | _ -> 2295 ("to_int", false) ) 2296 | Boolean _ -> 2297 ("to_bool", false) 2298 | Bytes _ -> 2299 ("(fun j -> Bytes.of_string (to_string j))", false) 2300 | Blob _ -> 2301 ("Hermes.blob_of_yojson", true) 2302 | CidLink _ -> 2303 ("Cid.of_yojson", true) 2304 | Array {items; _} -> 2305 let item_type = gen_shared_type_ref current_nsid items in 2306 ( Printf.sprintf 2307 "(fun j -> to_list j |> List.filter_map (fun x -> match \ 2308 %s_of_yojson x with Ok v -> Some v | _ -> None))" 2309 item_type 2310 , false ) 2311 | Ref {ref_; _} -> 2312 let type_name = gen_shared_ref_type current_nsid ref_ in 2313 (type_name ^ "_of_yojson", true) 2314 | Union {refs; _} -> 2315 let type_name = 2316 match lookup_union_name out refs with 2317 | Some n -> 2318 n 2319 | None -> 2320 gen_union_type_name refs 2321 in 2322 (type_name ^ "_of_yojson", true) 2323 | Unknown _ -> 2324 ("(fun j -> j)", false) 2325 | _ -> 2326 ("(fun _ -> failwith \"unsupported type\")", false) 2327 in 2328 (* generate converter expression for writing a type to json *) 2329 let gen_shared_to_yojson_expr current_nsid type_def = 2330 match type_def with 2331 | String _ | Token _ -> 2332 "(fun s -> `String s)" 2333 | Integer {maximum; _} -> ( 2334 match maximum with 2335 | Some m when m > 1073741823 -> 2336 "(fun i -> `Int (Int64.to_int i))" 2337 | _ -> 2338 "(fun i -> `Int i)" ) 2339 | Boolean _ -> 2340 "(fun b -> `Bool b)" 2341 | Bytes _ -> 2342 "(fun b -> `String (Bytes.to_string b))" 2343 | Blob _ -> 2344 "Hermes.blob_to_yojson" 2345 | CidLink _ -> 2346 "Cid.to_yojson" 2347 | Array {items; _} -> 2348 let item_type = gen_shared_type_ref current_nsid items in 2349 Printf.sprintf "(fun l -> `List (List.map %s_to_yojson l))" item_type 2350 | Ref {ref_; _} -> 2351 let type_name = gen_shared_ref_type current_nsid ref_ in 2352 type_name ^ "_to_yojson" 2353 | Union {refs; _} -> 2354 let type_name = 2355 match lookup_union_name out refs with 2356 | Some n -> 2357 n 2358 | None -> 2359 gen_union_type_name refs 2360 in 2361 type_name ^ "_to_yojson" 2362 | Unknown _ -> 2363 "(fun j -> j)" 2364 | _ -> 2365 "(fun _ -> `Null)" 2366 in 2367 (* collect inline unions with context-based naming *) 2368 let get_shared_inline_union_name nsid context = 2369 let base_name = Naming.type_name context in 2370 (* check if there's a collision with this inline union name *) 2371 if List.mem base_name colliding_names then 2372 Naming.shared_type_name nsid context 2373 else base_name 2374 in 2375 let register_shared_inline_unions nsid properties = 2376 let rec collect_inline_unions_with_context context acc type_def = 2377 match type_def with 2378 | Union spec -> 2379 (context, spec.refs, spec) :: acc 2380 | Array {items; _} -> 2381 collect_inline_unions_with_context (context ^ "_item") acc items 2382 | _ -> 2383 acc 2384 in 2385 let inline_unions = 2386 List.fold_left 2387 (fun acc (prop_name, (prop : property)) -> 2388 collect_inline_unions_with_context prop_name acc prop.type_def ) 2389 [] properties 2390 in 2391 List.iter 2392 (fun (context, refs, _spec) -> 2393 let unique_name = get_shared_inline_union_name nsid context in 2394 register_union_name out refs unique_name ) 2395 inline_unions 2396 in 2397 (* generate object type for shared context *) 2398 let gen_shared_object_type ?(first = true) ?(last = true) current_nsid name 2399 (spec : object_spec) = 2400 let required = Option.value spec.required ~default:[] in 2401 let nullable = Option.value spec.nullable ~default:[] in 2402 let keyword = if first then "type" else "and" in 2403 let type_name = get_shared_type_name current_nsid name in 2404 if spec.properties = [] then begin 2405 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ; 2406 if last then begin 2407 emitln out (Printf.sprintf "let %s_of_yojson _ = Ok ()" type_name) ; 2408 emitln out (Printf.sprintf "let %s_to_yojson () = `Assoc []" type_name) ; 2409 emit_newline out 2410 end 2411 end 2412 else begin 2413 if first then register_shared_inline_unions current_nsid spec.properties ; 2414 emitln out (Printf.sprintf "%s %s =" keyword type_name) ; 2415 emitln out " {" ; 2416 List.iter 2417 (fun (prop_name, (prop : property)) -> 2418 let ocaml_name = Naming.field_name prop_name in 2419 let base_type = gen_shared_type_ref current_nsid prop.type_def in 2420 let is_required = List.mem prop_name required in 2421 let is_nullable = List.mem prop_name nullable in 2422 let type_str = 2423 if is_required && not is_nullable then base_type 2424 else base_type ^ " option" 2425 in 2426 let key_attr = Naming.key_annotation prop_name ocaml_name in 2427 let default_attr = 2428 if is_required && not is_nullable then "" else " [@default None]" 2429 in 2430 emitln out 2431 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 2432 default_attr ) ) 2433 spec.properties ; 2434 emitln out " }" ; 2435 if last then begin 2436 emitln out "[@@deriving yojson {strict= false}]" ; 2437 emit_newline out 2438 end 2439 end 2440 in 2441 (* generate union type for shared context *) 2442 let gen_shared_union_type current_nsid name (spec : union_spec) = 2443 let type_name = get_shared_type_name current_nsid name in 2444 let is_closed = Option.value spec.closed ~default:false in 2445 emitln out (Printf.sprintf "type %s =" type_name) ; 2446 List.iter 2447 (fun ref_str -> 2448 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2449 let payload_type = gen_shared_ref_type current_nsid ref_str in 2450 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 2451 spec.refs ; 2452 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ; 2453 emit_newline out ; 2454 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 2455 emitln out " let open Yojson.Safe.Util in" ; 2456 emitln out " try" ; 2457 emitln out " match json |> member \"$type\" |> to_string with" ; 2458 List.iter 2459 (fun ref_str -> 2460 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2461 let full_type_uri = gen_shared_type_uri current_nsid ref_str in 2462 let payload_type = gen_shared_ref_type current_nsid ref_str in 2463 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 2464 emitln out 2465 (Printf.sprintf " (match %s_of_yojson json with" payload_type) ; 2466 emitln out (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 2467 emitln out " | Error e -> Error e)" ) 2468 spec.refs ; 2469 if is_closed then 2470 emitln out " | t -> Error (\"unknown union type: \" ^ t)" 2471 else emitln out " | _ -> Ok (Unknown json)" ; 2472 emitln out " with _ -> Error \"failed to parse union\"" ; 2473 emit_newline out ; 2474 emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ; 2475 List.iter 2476 (fun ref_str -> 2477 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2478 let full_type_uri = gen_shared_type_uri current_nsid ref_str in 2479 let payload_type = gen_shared_ref_type current_nsid ref_str in 2480 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 2481 emitln out 2482 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 2483 emitln out 2484 (Printf.sprintf 2485 " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") :: \ 2486 fields)" 2487 full_type_uri ) ; 2488 emitln out " | other -> other)" ) 2489 spec.refs ; 2490 if not is_closed then emitln out " | Unknown j -> j" ; 2491 emit_newline out 2492 in 2493 (* collect refs for shared SCC detection, using compound keys (nsid#name) *) 2494 let collect_shared_local_refs current_nsid acc type_def = 2495 let rec aux acc = function 2496 | Array {items; _} -> 2497 aux acc items 2498 | Ref {ref_; _} -> 2499 if String.length ref_ > 0 && ref_.[0] = '#' then 2500 (* local ref: #foo -> current_nsid#foo *) 2501 let def_name = String.sub ref_ 1 (String.length ref_ - 1) in 2502 (current_nsid ^ "#" ^ def_name) :: acc 2503 else begin 2504 match String.split_on_char '#' ref_ with 2505 | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids -> 2506 (* cross-nsid ref within shared group *) 2507 (ext_nsid ^ "#" ^ def_name) :: acc 2508 | _ -> 2509 acc 2510 end 2511 | Union {refs; _} -> 2512 List.fold_left 2513 (fun a r -> 2514 if String.length r > 0 && r.[0] = '#' then 2515 let def_name = String.sub r 1 (String.length r - 1) in 2516 (current_nsid ^ "#" ^ def_name) :: a 2517 else 2518 match String.split_on_char '#' r with 2519 | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids -> 2520 (ext_nsid ^ "#" ^ def_name) :: a 2521 | _ -> 2522 a ) 2523 acc refs 2524 | Object {properties; _} -> 2525 List.fold_left 2526 (fun a (_, (prop : property)) -> aux a prop.type_def) 2527 acc properties 2528 | Record {record; _} -> 2529 List.fold_left 2530 (fun a (_, (prop : property)) -> aux a prop.type_def) 2531 acc record.properties 2532 | Query {parameters; output; _} -> ( 2533 let acc = 2534 match parameters with 2535 | Some params -> 2536 List.fold_left 2537 (fun a (_, (prop : property)) -> aux a prop.type_def) 2538 acc params.properties 2539 | None -> 2540 acc 2541 in 2542 match output with 2543 | Some body -> 2544 Option.fold ~none:acc ~some:(aux acc) body.schema 2545 | None -> 2546 acc ) 2547 | Procedure {parameters; input; output; _} -> ( 2548 let acc = 2549 match parameters with 2550 | Some params -> 2551 List.fold_left 2552 (fun a (_, (prop : property)) -> aux a prop.type_def) 2553 acc params.properties 2554 | None -> 2555 acc 2556 in 2557 let acc = 2558 match input with 2559 | Some body -> 2560 Option.fold ~none:acc ~some:(aux acc) body.schema 2561 | None -> 2562 acc 2563 in 2564 match output with 2565 | Some body -> 2566 Option.fold ~none:acc ~some:(aux acc) body.schema 2567 | None -> 2568 acc ) 2569 | _ -> 2570 acc 2571 in 2572 aux acc type_def 2573 in 2574 (* generate single shared def *) 2575 let gen_shared_single_def (nsid, def) = 2576 match def.type_def with 2577 | Object spec -> 2578 gen_shared_object_type nsid def.name spec 2579 | Union spec -> 2580 gen_shared_union_type nsid def.name spec 2581 | Token spec -> 2582 gen_token nsid out def.name spec 2583 | Query spec -> 2584 gen_query nsid out def.name spec 2585 | Procedure spec -> 2586 gen_procedure nsid out def.name spec 2587 | Record spec -> 2588 gen_shared_object_type nsid def.name spec.record 2589 | String spec when spec.known_values <> None -> 2590 gen_string_type out def.name spec 2591 | Array {items; _} -> 2592 (* generate inline union for array items if needed *) 2593 ( match items with 2594 | Union spec -> 2595 let item_type_name = Naming.type_name (def.name ^ "_item") in 2596 register_union_name out spec.refs item_type_name ; 2597 gen_shared_union_type nsid (def.name ^ "_item") spec 2598 | _ -> 2599 () ) ; 2600 (* generate type alias for array *) 2601 let type_name = get_shared_type_name nsid def.name in 2602 let item_type = gen_shared_type_ref nsid items in 2603 emitln out (Printf.sprintf "type %s = %s list" type_name item_type) ; 2604 emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ; 2605 emitln out " let open Yojson.Safe.Util in" ; 2606 emitln out 2607 (Printf.sprintf 2608 " Ok (to_list json |> List.filter_map (fun x -> match \ 2609 %s_of_yojson x with Ok v -> Some v | _ -> None))" 2610 item_type ) ; 2611 emitln out 2612 (Printf.sprintf "let %s_to_yojson l = `List (List.map %s_to_yojson l)" 2613 type_name item_type ) ; 2614 emit_newline out 2615 | _ -> 2616 () 2617 in 2618 (* helper to generate object type definition only (no converters) *) 2619 let gen_shared_object_type_only ?(keyword = "type") nsid name 2620 (spec : object_spec) = 2621 let required = Option.value spec.required ~default:[] in 2622 let nullable = Option.value spec.nullable ~default:[] in 2623 let type_name = get_shared_type_name nsid name in 2624 if spec.properties = [] then 2625 emitln out (Printf.sprintf "%s %s = unit" keyword type_name) 2626 else begin 2627 emitln out (Printf.sprintf "%s %s = {" keyword type_name) ; 2628 List.iter 2629 (fun (prop_name, (prop : property)) -> 2630 let ocaml_name = Naming.field_name prop_name in 2631 let base_type = gen_shared_type_ref nsid prop.type_def in 2632 let is_required = List.mem prop_name required in 2633 let is_nullable = List.mem prop_name nullable in 2634 let type_str = 2635 if is_required && not is_nullable then base_type 2636 else base_type ^ " option" 2637 in 2638 let key_attr = Naming.key_annotation prop_name ocaml_name in 2639 let default_attr = 2640 if is_required && not is_nullable then "" else " [@default None]" 2641 in 2642 emitln out 2643 (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr 2644 default_attr ) ) 2645 spec.properties ; 2646 emitln out "}" 2647 end 2648 in 2649 (* helper to generate inline union type definition only *) 2650 let gen_shared_inline_union_type_only ?(keyword = "type") nsid name refs spec 2651 = 2652 let is_closed = Option.value spec.closed ~default:false in 2653 emitln out (Printf.sprintf "%s %s =" keyword name) ; 2654 List.iter 2655 (fun ref_str -> 2656 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2657 let payload_type = gen_shared_ref_type nsid ref_str in 2658 emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) ) 2659 refs ; 2660 if not is_closed then emitln out " | Unknown of Yojson.Safe.t" 2661 in 2662 (* helper to generate object converters *) 2663 let gen_shared_object_converters ?(of_keyword = "let") ?(to_keyword = "let") 2664 nsid name (spec : object_spec) = 2665 let required = Option.value spec.required ~default:[] in 2666 let nullable = Option.value spec.nullable ~default:[] in 2667 let type_name = get_shared_type_name nsid name in 2668 if spec.properties = [] then begin 2669 if of_keyword <> "SKIP" then 2670 emitln out 2671 (Printf.sprintf "%s %s_of_yojson _ = Ok ()" of_keyword type_name) ; 2672 if to_keyword <> "SKIP" then 2673 emitln out 2674 (Printf.sprintf "%s %s_to_yojson () = `Assoc []" to_keyword type_name) 2675 end 2676 else begin 2677 (* of_yojson *) 2678 if of_keyword <> "SKIP" then begin 2679 emitln out 2680 (Printf.sprintf "%s %s_of_yojson json =" of_keyword type_name) ; 2681 emitln out " let open Yojson.Safe.Util in" ; 2682 emitln out " try" ; 2683 List.iter 2684 (fun (prop_name, (prop : property)) -> 2685 let ocaml_name = Naming.field_name prop_name in 2686 let conv_expr, needs_unwrap = 2687 gen_shared_of_yojson_expr nsid prop.type_def 2688 in 2689 let is_required = List.mem prop_name required in 2690 let is_nullable = List.mem prop_name nullable in 2691 let is_optional = (not is_required) || is_nullable in 2692 if is_optional then begin 2693 if needs_unwrap then 2694 emitln out 2695 (Printf.sprintf 2696 " let %s = json |> member \"%s\" |> to_option (fun x \ 2697 -> match %s x with Ok v -> Some v | _ -> None) |> \ 2698 Option.join in" 2699 ocaml_name prop_name conv_expr ) 2700 else 2701 emitln out 2702 (Printf.sprintf 2703 " let %s = json |> member \"%s\" |> to_option %s in" 2704 ocaml_name prop_name conv_expr ) 2705 end 2706 else begin 2707 if needs_unwrap then 2708 emitln out 2709 (Printf.sprintf 2710 " let %s = json |> member \"%s\" |> %s |> \ 2711 Result.get_ok in" 2712 ocaml_name prop_name conv_expr ) 2713 else 2714 emitln out 2715 (Printf.sprintf " let %s = json |> member \"%s\" |> %s in" 2716 ocaml_name prop_name conv_expr ) 2717 end ) 2718 spec.properties ; 2719 emit out " Ok { " ; 2720 emit out 2721 (String.concat "; " 2722 (List.map (fun (pn, _) -> Naming.field_name pn) spec.properties) ) ; 2723 emitln out " }" ; 2724 emitln out " with e -> Error (Printexc.to_string e)" ; 2725 emit_newline out 2726 end ; 2727 (* to_yojson *) 2728 if to_keyword <> "SKIP" then begin 2729 emitln out 2730 (Printf.sprintf "%s %s_to_yojson (r : %s) =" to_keyword type_name 2731 type_name ) ; 2732 emitln out " `Assoc [" ; 2733 List.iteri 2734 (fun i (prop_name, (prop : property)) -> 2735 let ocaml_name = Naming.field_name prop_name in 2736 let conv_expr = gen_shared_to_yojson_expr nsid prop.type_def in 2737 let is_required = List.mem prop_name required in 2738 let is_nullable = List.mem prop_name nullable in 2739 let is_optional = (not is_required) || is_nullable in 2740 let comma = 2741 if i < List.length spec.properties - 1 then ";" else "" 2742 in 2743 if is_optional then 2744 emitln out 2745 (Printf.sprintf 2746 " (\"%s\", match r.%s with Some v -> %s v | None -> \ 2747 `Null)%s" 2748 prop_name ocaml_name conv_expr comma ) 2749 else 2750 emitln out 2751 (Printf.sprintf " (\"%s\", %s r.%s)%s" prop_name conv_expr 2752 ocaml_name comma ) ) 2753 spec.properties ; 2754 emitln out " ]" ; 2755 emit_newline out 2756 end 2757 end 2758 in 2759 (* helper to generate inline union converters *) 2760 let gen_shared_inline_union_converters ?(of_keyword = "let") 2761 ?(to_keyword = "let") nsid name refs spec = 2762 let is_closed = Option.value spec.closed ~default:false in 2763 (* of_yojson *) 2764 if of_keyword <> "SKIP" then begin 2765 emitln out (Printf.sprintf "%s %s_of_yojson json =" of_keyword name) ; 2766 emitln out " let open Yojson.Safe.Util in" ; 2767 emitln out " try" ; 2768 emitln out " match json |> member \"$type\" |> to_string with" ; 2769 List.iter 2770 (fun ref_str -> 2771 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2772 let full_type_uri = gen_shared_type_uri nsid ref_str in 2773 let payload_type = gen_shared_ref_type nsid ref_str in 2774 emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ; 2775 emitln out 2776 (Printf.sprintf " (match %s_of_yojson json with" payload_type) ; 2777 emitln out 2778 (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ; 2779 emitln out " | Error e -> Error e)" ) 2780 refs ; 2781 if is_closed then 2782 emitln out " | t -> Error (\"unknown union type: \" ^ t)" 2783 else emitln out " | _ -> Ok (Unknown json)" ; 2784 emitln out " with _ -> Error \"failed to parse union\"" ; 2785 emit_newline out 2786 end ; 2787 (* to_yojson *) 2788 if to_keyword <> "SKIP" then begin 2789 emitln out (Printf.sprintf "%s %s_to_yojson = function" to_keyword name) ; 2790 List.iter 2791 (fun ref_str -> 2792 let variant_name = Naming.qualified_variant_name_of_ref ref_str in 2793 let full_type_uri = gen_shared_type_uri nsid ref_str in 2794 let payload_type = gen_shared_ref_type nsid ref_str in 2795 emitln out (Printf.sprintf " | %s v ->" variant_name) ; 2796 emitln out 2797 (Printf.sprintf " (match %s_to_yojson v with" payload_type) ; 2798 emitln out 2799 (Printf.sprintf 2800 " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") \ 2801 :: fields)" 2802 full_type_uri ) ; 2803 emitln out " | other -> other)" ) 2804 refs ; 2805 if not is_closed then emitln out " | Unknown j -> j" ; 2806 emit_newline out 2807 end 2808 in 2809 (* collect all inline unions as pseudo-defs for proper ordering *) 2810 let rec collect_inline_unions_from_type nsid context acc type_def = 2811 match type_def with 2812 | Union spec -> 2813 let union_name = get_shared_inline_union_name nsid context in 2814 (nsid, union_name, spec.refs, spec) :: acc 2815 | Array {items; _} -> 2816 collect_inline_unions_from_type nsid (context ^ "_item") acc items 2817 | Object {properties; _} -> 2818 List.fold_left 2819 (fun a (prop_name, (prop : property)) -> 2820 collect_inline_unions_from_type nsid prop_name a prop.type_def ) 2821 acc properties 2822 | _ -> 2823 acc 2824 in 2825 let all_inline_unions = 2826 List.concat_map 2827 (fun (nsid, def) -> 2828 match def.type_def with 2829 | Object spec -> 2830 List.fold_left 2831 (fun acc (prop_name, (prop : property)) -> 2832 collect_inline_unions_from_type nsid prop_name acc prop.type_def ) 2833 [] spec.properties 2834 | Record spec -> 2835 List.fold_left 2836 (fun acc (prop_name, (prop : property)) -> 2837 collect_inline_unions_from_type nsid prop_name acc prop.type_def ) 2838 [] spec.record.properties 2839 | _ -> 2840 [] ) 2841 all_defs 2842 in 2843 (* create inline union entries *) 2844 let inline_union_defs = 2845 List.map 2846 (fun (nsid, name, refs, spec) -> 2847 let key = nsid ^ "#__inline__" ^ name in 2848 let deps = 2849 List.filter_map 2850 (fun r -> 2851 if String.length r > 0 && r.[0] = '#' then 2852 let def_name = String.sub r 1 (String.length r - 1) in 2853 Some (nsid ^ "#" ^ def_name) 2854 else 2855 match String.split_on_char '#' r with 2856 | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids -> 2857 Some (ext_nsid ^ "#" ^ def_name) 2858 | _ -> 2859 None ) 2860 refs 2861 in 2862 (key, deps, `InlineUnion (nsid, name, refs, spec)) ) 2863 all_inline_unions 2864 in 2865 (* create regular def entries *) 2866 let regular_def_entries = 2867 List.map 2868 (fun (nsid, def) -> 2869 let key = nsid ^ "#" ^ def.name in 2870 let base_deps = collect_shared_local_refs nsid [] def.type_def in 2871 let inline_deps = 2872 match def.type_def with 2873 | Object spec | Record {record= spec; _} -> 2874 List.fold_left 2875 (fun acc (prop_name, (prop : property)) -> 2876 match prop.type_def with 2877 | Union _ -> 2878 let union_name = 2879 get_shared_inline_union_name nsid prop_name 2880 in 2881 (nsid ^ "#__inline__" ^ union_name) :: acc 2882 | Array {items= Union _; _} -> 2883 let union_name = 2884 get_shared_inline_union_name nsid (prop_name ^ "_item") 2885 in 2886 (nsid ^ "#__inline__" ^ union_name) :: acc 2887 | _ -> 2888 acc ) 2889 [] spec.properties 2890 | _ -> 2891 [] 2892 in 2893 (key, base_deps @ inline_deps, `RegularDef (nsid, def)) ) 2894 all_defs 2895 in 2896 (* combine all entries *) 2897 let all_entries = regular_def_entries @ inline_union_defs in 2898 let deps_map = List.map (fun (k, deps, _) -> (k, deps)) all_entries in 2899 let entry_map = List.map (fun (k, _, entry) -> (k, entry)) all_entries in 2900 let all_keys = List.map (fun (k, _, _) -> k) all_entries in 2901 (* run Tarjan's algorithm *) 2902 let index_counter = ref 0 in 2903 let indices = Hashtbl.create 64 in 2904 let lowlinks = Hashtbl.create 64 in 2905 let on_stack = Hashtbl.create 64 in 2906 let stack = ref [] in 2907 let sccs = ref [] in 2908 let rec strongconnect key = 2909 let index = !index_counter in 2910 incr index_counter ; 2911 Hashtbl.add indices key index ; 2912 Hashtbl.add lowlinks key index ; 2913 Hashtbl.add on_stack key true ; 2914 stack := key :: !stack ; 2915 let successors = 2916 try List.assoc key deps_map |> List.filter (fun k -> List.mem k all_keys) 2917 with Not_found -> [] 2918 in 2919 List.iter 2920 (fun succ -> 2921 if not (Hashtbl.mem indices succ) then begin 2922 strongconnect succ ; 2923 Hashtbl.replace lowlinks key 2924 (min (Hashtbl.find lowlinks key) (Hashtbl.find lowlinks succ)) 2925 end 2926 else if Hashtbl.find_opt on_stack succ = Some true then 2927 Hashtbl.replace lowlinks key 2928 (min (Hashtbl.find lowlinks key) (Hashtbl.find indices succ)) ) 2929 successors ; 2930 if Hashtbl.find lowlinks key = Hashtbl.find indices key then begin 2931 let rec pop_scc acc = 2932 match !stack with 2933 | [] -> 2934 acc 2935 | top :: rest -> 2936 stack := rest ; 2937 Hashtbl.replace on_stack top false ; 2938 if top = key then top :: acc else pop_scc (top :: acc) 2939 in 2940 let scc_keys = pop_scc [] in 2941 let scc_entries = 2942 List.filter_map (fun k -> List.assoc_opt k entry_map) scc_keys 2943 in 2944 if scc_entries <> [] then sccs := scc_entries :: !sccs 2945 end 2946 in 2947 List.iter 2948 (fun key -> if not (Hashtbl.mem indices key) then strongconnect key) 2949 all_keys ; 2950 let ordered_sccs = List.rev !sccs in 2951 (* generate each SCC *) 2952 List.iter 2953 (fun scc -> 2954 let inline_unions_in_scc = 2955 List.filter_map (function `InlineUnion x -> Some x | _ -> None) scc 2956 in 2957 let regular_defs_in_scc = 2958 List.filter_map (function `RegularDef x -> Some x | _ -> None) scc 2959 in 2960 if inline_unions_in_scc = [] then begin 2961 (* no inline unions - check if we still need mutual recursion *) 2962 match regular_defs_in_scc with 2963 | [] -> 2964 () 2965 | [(nsid, def)] -> 2966 (* single def, generate normally *) 2967 gen_shared_single_def (nsid, def) 2968 | defs -> 2969 (* multiple defs in SCC - need mutual recursion *) 2970 (* filter to only object-like types that can be mutually recursive *) 2971 let obj_defs = 2972 List.filter 2973 (fun (_, def) -> 2974 match def.type_def with 2975 | Object _ | Record _ -> 2976 true 2977 | _ -> 2978 false ) 2979 defs 2980 in 2981 let other_defs = 2982 List.filter 2983 (fun (_, def) -> 2984 match def.type_def with 2985 | Object _ | Record _ -> 2986 false 2987 | _ -> 2988 true ) 2989 defs 2990 in 2991 (* generate non-object types first (they have their own converters) *) 2992 List.iter gen_shared_single_def other_defs ; 2993 (* generate object types as mutually recursive *) 2994 if obj_defs <> [] then begin 2995 (* register inline unions from all objects first *) 2996 List.iter 2997 (fun (nsid, def) -> 2998 match def.type_def with 2999 | Object spec -> 3000 register_shared_inline_unions nsid spec.properties 3001 | Record rspec -> 3002 register_shared_inline_unions nsid rspec.record.properties 3003 | _ -> 3004 () ) 3005 obj_defs ; 3006 (* generate all type definitions *) 3007 List.iteri 3008 (fun i (nsid, def) -> 3009 let keyword = if i = 0 then "type" else "and" in 3010 match def.type_def with 3011 | Object spec -> 3012 gen_shared_object_type_only ~keyword nsid def.name spec 3013 | Record rspec -> 3014 gen_shared_object_type_only ~keyword nsid def.name 3015 rspec.record 3016 | _ -> 3017 () ) 3018 obj_defs ; 3019 emit_newline out ; 3020 (* generate all _of_yojson converters as mutually recursive *) 3021 List.iteri 3022 (fun i (nsid, def) -> 3023 let of_keyword = if i = 0 then "let rec" else "and" in 3024 match def.type_def with 3025 | Object spec -> 3026 gen_shared_object_converters ~of_keyword 3027 ~to_keyword:"SKIP" nsid def.name spec 3028 | Record rspec -> 3029 gen_shared_object_converters ~of_keyword 3030 ~to_keyword:"SKIP" nsid def.name rspec.record 3031 | _ -> 3032 () ) 3033 obj_defs ; 3034 (* generate all _to_yojson converters *) 3035 List.iter 3036 (fun (nsid, def) -> 3037 match def.type_def with 3038 | Object spec -> 3039 gen_shared_object_converters ~of_keyword:"SKIP" 3040 ~to_keyword:"and" nsid def.name spec 3041 | Record rspec -> 3042 gen_shared_object_converters ~of_keyword:"SKIP" 3043 ~to_keyword:"and" nsid def.name rspec.record 3044 | _ -> 3045 () ) 3046 obj_defs 3047 end 3048 end 3049 else begin 3050 (* has inline unions - generate all types first, then all converters *) 3051 List.iter 3052 (fun (_nsid, name, refs, _spec) -> 3053 register_union_name out refs name ; 3054 mark_union_generated out name ) 3055 inline_unions_in_scc ; 3056 let all_items = 3057 List.map (fun x -> `Inline x) inline_unions_in_scc 3058 @ List.map (fun x -> `Regular x) regular_defs_in_scc 3059 in 3060 let n = List.length all_items in 3061 if n = 1 then begin 3062 match List.hd all_items with 3063 | `Inline (nsid, name, refs, spec) -> 3064 gen_shared_inline_union_type_only nsid name refs spec ; 3065 emit_newline out ; 3066 gen_shared_inline_union_converters nsid name refs spec 3067 | `Regular (nsid, def) -> ( 3068 match def.type_def with 3069 | Object spec -> 3070 register_shared_inline_unions nsid spec.properties ; 3071 gen_shared_object_type_only nsid def.name spec ; 3072 emit_newline out ; 3073 gen_shared_object_converters nsid def.name spec 3074 | Record rspec -> 3075 register_shared_inline_unions nsid rspec.record.properties ; 3076 gen_shared_object_type_only nsid def.name rspec.record ; 3077 emit_newline out ; 3078 gen_shared_object_converters nsid def.name rspec.record 3079 | _ -> 3080 gen_shared_single_def (nsid, def) ) 3081 end 3082 else begin 3083 (* multiple items - generate as mutually recursive types *) 3084 List.iter 3085 (function 3086 | `Regular (nsid, def) -> ( 3087 match def.type_def with 3088 | Object spec -> 3089 register_shared_inline_unions nsid spec.properties 3090 | Record rspec -> 3091 register_shared_inline_unions nsid rspec.record.properties 3092 | _ -> 3093 () ) 3094 | `Inline _ -> 3095 () ) 3096 all_items ; 3097 (* generate all type definitions *) 3098 List.iteri 3099 (fun i item -> 3100 let keyword = if i = 0 then "type" else "and" in 3101 match item with 3102 | `Inline (nsid, name, refs, spec) -> 3103 gen_shared_inline_union_type_only ~keyword nsid name refs spec 3104 | `Regular (nsid, def) -> ( 3105 match def.type_def with 3106 | Object spec -> 3107 gen_shared_object_type_only ~keyword nsid def.name spec 3108 | Record rspec -> 3109 gen_shared_object_type_only ~keyword nsid def.name 3110 rspec.record 3111 | _ -> 3112 () ) ) 3113 all_items ; 3114 emit_newline out ; 3115 (* generate all _of_yojson converters *) 3116 List.iteri 3117 (fun i item -> 3118 let of_keyword = if i = 0 then "let rec" else "and" in 3119 match item with 3120 | `Inline (nsid, name, refs, spec) -> 3121 gen_shared_inline_union_converters ~of_keyword 3122 ~to_keyword:"SKIP" nsid name refs spec 3123 | `Regular (nsid, def) -> ( 3124 match def.type_def with 3125 | Object spec -> 3126 gen_shared_object_converters ~of_keyword ~to_keyword:"SKIP" 3127 nsid def.name spec 3128 | Record rspec -> 3129 gen_shared_object_converters ~of_keyword ~to_keyword:"SKIP" 3130 nsid def.name rspec.record 3131 | _ -> 3132 () ) ) 3133 all_items ; 3134 (* generate all _to_yojson converters *) 3135 List.iteri 3136 (fun i item -> 3137 let to_keyword = "and" in 3138 ignore i ; 3139 match item with 3140 | `Inline (nsid, name, refs, spec) -> 3141 gen_shared_inline_union_converters ~of_keyword:"SKIP" 3142 ~to_keyword nsid name refs spec 3143 | `Regular (nsid, def) -> ( 3144 match def.type_def with 3145 | Object spec -> 3146 gen_shared_object_converters ~of_keyword:"SKIP" ~to_keyword 3147 nsid def.name spec 3148 | Record rspec -> 3149 gen_shared_object_converters ~of_keyword:"SKIP" ~to_keyword 3150 nsid def.name rspec.record 3151 | _ -> 3152 () ) ) 3153 all_items 3154 end 3155 end ) 3156 ordered_sccs ; 3157 Emitter.contents out 3158 3159(* generate a re-export module that maps local names to shared module types *) 3160let gen_reexport_module ~shared_module_name ~all_merged_docs (doc : lexicon_doc) 3161 : string = 3162 let buf = Buffer.create 1024 in 3163 let emit s = Buffer.add_string buf s in 3164 let emitln s = Buffer.add_string buf s ; Buffer.add_char buf '\n' in 3165 (* detect collisions across all merged docs *) 3166 let all_defs = 3167 List.concat_map 3168 (fun d -> List.map (fun def -> (d.id, def)) d.defs) 3169 all_merged_docs 3170 in 3171 let name_counts = Hashtbl.create 64 in 3172 List.iter 3173 (fun (nsid, def) -> 3174 let existing = Hashtbl.find_opt name_counts def.name in 3175 match existing with 3176 | None -> 3177 Hashtbl.add name_counts def.name [nsid] 3178 | Some nsids when not (List.mem nsid nsids) -> 3179 Hashtbl.replace name_counts def.name (nsid :: nsids) 3180 | _ -> 3181 () ) 3182 all_defs ; 3183 let colliding_names = 3184 Hashtbl.fold 3185 (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc) 3186 name_counts [] 3187 in 3188 (* function to get shared type name (context-based for collisions) *) 3189 let get_shared_type_name nsid def_name = 3190 if List.mem def_name colliding_names then 3191 Naming.shared_type_name nsid def_name 3192 else Naming.type_name def_name 3193 in 3194 emitln (Printf.sprintf "(* re-exported from %s *)" shared_module_name) ; 3195 emitln "" ; 3196 List.iter 3197 (fun def -> 3198 let local_type_name = Naming.type_name def.name in 3199 let shared_type_name = get_shared_type_name doc.id def.name in 3200 match def.type_def with 3201 | Object _ | Record _ | Union _ -> 3202 emitln 3203 (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name 3204 shared_type_name ) ; 3205 emitln 3206 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 3207 shared_module_name shared_type_name ) ; 3208 emitln 3209 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 3210 shared_module_name shared_type_name ) ; 3211 emit "\n" 3212 | String spec when spec.known_values <> None -> 3213 emitln 3214 (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name 3215 shared_type_name ) ; 3216 emitln 3217 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 3218 shared_module_name shared_type_name ) ; 3219 emitln 3220 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 3221 shared_module_name shared_type_name ) ; 3222 emit "\n" 3223 | Array _ -> 3224 emitln 3225 (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name 3226 shared_type_name ) ; 3227 emitln 3228 (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name 3229 shared_module_name shared_type_name ) ; 3230 emitln 3231 (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name 3232 shared_module_name shared_type_name ) ; 3233 emit "\n" 3234 | Token _ -> 3235 emitln 3236 (Printf.sprintf "let %s = %s.%s" local_type_name shared_module_name 3237 shared_type_name ) ; 3238 emit "\n" 3239 | Query _ | Procedure _ -> 3240 let mod_name = Naming.def_module_name def.name in 3241 emitln 3242 (Printf.sprintf "module %s = %s.%s" mod_name shared_module_name 3243 mod_name ) ; 3244 emit "\n" 3245 | _ -> 3246 () ) 3247 doc.defs ; 3248 Buffer.contents buf