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