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