···11open Hermes_cli
2233-(* recursively find all json files in a directory *)
44-let find_json_files dir =
55- let rec aux acc path =
66- if Sys.is_directory path then
77- Sys.readdir path |> Array.to_list
88- |> List.map (Filename.concat path)
33+(* recursively find all json files in a path (file or directory) *)
44+let find_json_files path =
55+ let rec aux acc p =
66+ if Sys.is_directory p then
77+ Sys.readdir p |> Array.to_list
88+ |> List.map (Filename.concat p)
99 |> List.fold_left aux acc
1010- else if Filename.check_suffix path ".json" then path :: acc
1010+ else if Filename.check_suffix p ".json" then p :: acc
1111 else acc
1212 in
1313- aux [] dir
1313+ aux [] path
14141515(* generate module structure from lexicons *)
1616-let generate ~input_dir ~output_dir ~module_name =
1616+let generate ~inputs ~output_dir ~module_name =
1717 (* create output directory *)
1818 if not (Sys.file_exists output_dir) then Sys.mkdir output_dir 0o755 ;
1919- (* find all lexicon files *)
2020- let files = find_json_files input_dir in
1919+ (* find all lexicon files from all inputs *)
2020+ let files = List.concat_map find_json_files inputs in
2121 Printf.printf "Found %d lexicon files\n" (List.length files) ;
2222 (* parse all files *)
2323 let lexicons =
···3333 files
3434 in
3535 Printf.printf "Successfully parsed %d lexicons\n" (List.length lexicons) ;
3636- (* group by namespace, all but last segment *)
3737- let by_namespace = Hashtbl.create 64 in
3636+ (* find file-level SCCs to detect cross-file cycles *)
3737+ let sccs = Scc.find_file_sccs lexicons in
3838+ Printf.printf "Found %d file-level SCCs\n" (List.length sccs) ;
3939+ (* track shared module index for unique naming *)
4040+ let shared_index = ref 0 in
4141+ (* generate each SCC *)
3842 List.iter
3939- (fun doc ->
4040- let segments = String.split_on_char '.' doc.Lexicon_types.id in
4141- match List.rev segments with
4242- | _last :: rest ->
4343- let ns = String.concat "." (List.rev rest) in
4444- let existing =
4545- try Hashtbl.find by_namespace ns with Not_found -> []
4646- in
4747- Hashtbl.replace by_namespace ns (doc :: existing)
4343+ (fun scc ->
4444+ match scc with
4845 | [] ->
4949- () )
5050- lexicons ;
5151- (* generate file for each lexicon *)
5252- List.iter
5353- (fun doc ->
5454- let code = Codegen.gen_lexicon_module doc in
5555- let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in
5656- let full_path = Filename.concat output_dir rel_path in
5757- (* write file *)
5858- let oc = open_out full_path in
5959- output_string oc code ;
6060- close_out oc ;
6161- Printf.printf " Generated: %s\n" rel_path )
6262- lexicons ;
4646+ ()
4747+ | [doc] ->
4848+ (* single file, no cycle - generate normally *)
4949+ let code = Codegen.gen_lexicon_module doc in
5050+ let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in
5151+ let full_path = Filename.concat output_dir rel_path in
5252+ let oc = open_out full_path in
5353+ output_string oc code ;
5454+ close_out oc ;
5555+ Printf.printf " Generated: %s\n" rel_path
5656+ | docs ->
5757+ (* multiple files forming a cycle - use shared module strategy *)
5858+ incr shared_index ;
5959+ let nsids = List.map (fun d -> d.Lexicon_types.id) docs in
6060+ Printf.printf " Cyclic lexicons: %s\n" (String.concat ", " nsids) ;
6161+ (* sort for consistent ordering *)
6262+ let sorted_docs =
6363+ List.sort
6464+ (fun a b ->
6565+ String.compare a.Lexicon_types.id b.Lexicon_types.id )
6666+ docs
6767+ in
6868+ (* generate shared module with all types *)
6969+ let shared_module_name = Naming.shared_module_name nsids !shared_index in
7070+ let shared_file = Naming.shared_file_name nsids !shared_index in
7171+ let code = Codegen.gen_shared_module sorted_docs in
7272+ let full_path = Filename.concat output_dir shared_file in
7373+ let oc = open_out full_path in
7474+ output_string oc code ;
7575+ close_out oc ;
7676+ Printf.printf " Generated shared: %s\n" shared_file ;
7777+ (* generate re-export modules for each nsid *)
7878+ List.iter
7979+ (fun doc ->
8080+ let stub =
8181+ Codegen.gen_reexport_module ~shared_module_name
8282+ ~all_merged_docs:sorted_docs doc
8383+ in
8484+ let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in
8585+ let full_path = Filename.concat output_dir rel_path in
8686+ let oc = open_out full_path in
8787+ output_string oc stub ;
8888+ close_out oc ;
8989+ Printf.printf " Generated: %s -> %s\n" rel_path shared_module_name )
9090+ docs )
9191+ sccs ;
6392 (* generate index file *)
6493 let index_path =
6594 Filename.concat output_dir (String.lowercase_ascii module_name ^ ".ml")
···85114 Printf.printf "Generated dune file\n" ;
86115 Printf.printf "Done! Generated %d modules\n" (List.length lexicons)
871168888-let input_dir =
8989- let doc = "directory containing lexicon JSON files" in
9090- Cmdliner.Arg.(
9191- required & opt (some dir) None & info ["i"; "input"] ~docv:"DIR" ~doc )
117117+let inputs =
118118+ let doc =
119119+ "lexicon files or directories to search recursively for JSON"
120120+ in
121121+ Cmdliner.Arg.(non_empty & pos_all file [] & info [] ~docv:"INPUT" ~doc)
9212293123let output_dir =
94124 let doc = "output directory for generated code" in
···105135let generate_cmd =
106136 let doc = "generate ocaml types from atproto lexicons" in
107137 let info = Cmdliner.Cmd.info "generate" ~doc in
108108- let generate' input_dir output_dir module_name =
109109- generate ~input_dir ~output_dir ~module_name
138138+ let generate' inputs output_dir module_name =
139139+ generate ~inputs ~output_dir ~module_name
110140 in
111141 Cmdliner.Cmd.v info
112112- Cmdliner.Term.(const generate' $ input_dir $ output_dir $ module_name)
142142+ Cmdliner.Term.(const generate' $ inputs $ output_dir $ module_name)
113143114144let main_cmd =
115145 let doc = "hermes - atproto lexicon code generator" in
+2601-216
hermes-cli/lib/codegen.ml
···11open Lexicon_types
2233-type output =
44- { mutable imports: string list
55- ; mutable generated_unions: string list
66- ; mutable union_names: (string list * string) list (* refs -> context name *)
77- ; buf: Buffer.t }
33+(* use Emitter module for output buffer management *)
44+type output = Emitter.t
8599-let make_output () =
1010- {imports= []; generated_unions= []; union_names= []; buf= Buffer.create 4096}
66+let make_output = Emitter.make
1171212-let add_import out module_name =
1313- if not (List.mem module_name out.imports) then
1414- out.imports <- module_name :: out.imports
88+let add_import = Emitter.add_import
1591616-let mark_union_generated out union_name =
1717- if not (List.mem union_name out.generated_unions) then
1818- out.generated_unions <- union_name :: out.generated_unions
1010+let mark_union_generated = Emitter.mark_union_generated
19112020-let is_union_generated out union_name = List.mem union_name out.generated_unions
1212+let is_union_generated = Emitter.is_union_generated
21132222-(* register a context-based name for a union based on its refs *)
2323-let register_union_name out refs context_name =
2424- let sorted_refs = List.sort String.compare refs in
2525- if not (List.exists (fun (r, _) -> r = sorted_refs) out.union_names) then
2626- out.union_names <- (sorted_refs, context_name) :: out.union_names
1414+let register_union_name = Emitter.register_union_name
27152828-(* look up a union's context-based name, or return None *)
2929-let lookup_union_name out refs =
3030- let sorted_refs = List.sort String.compare refs in
3131- List.assoc_opt sorted_refs out.union_names
1616+let lookup_union_name = Emitter.lookup_union_name
32173333-let emit out s = Buffer.add_string out.buf s
1818+let emit = Emitter.emit
34193535-let emitln out s =
3636- Buffer.add_string out.buf s ;
3737- Buffer.add_char out.buf '\n'
2020+let emitln = Emitter.emitln
38213939-let emit_newline out = Buffer.add_char out.buf '\n'
2222+let emit_newline = Emitter.emit_newline
40234124(* generate ocaml type for a primitive type *)
4225let rec gen_type_ref nsid out (type_def : type_def) : string =
···8164 "unit (* primary type *)"
82658366(* generate reference to another type *)
8484-and gen_ref_type _nsid out ref_str : string =
6767+and gen_ref_type nsid out ref_str : string =
8568 if String.length ref_str > 0 && ref_str.[0] = '#' then begin
8669 (* local ref: #someDef -> someDef *)
8770 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
···9174 (* external ref: com.example.defs#someDef *)
9275 match String.split_on_char '#' ref_str with
9376 | [ext_nsid; def_name] ->
9494- (* use flat module names for include_subdirs unqualified *)
9595- let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
9696- add_import out flat_module ;
9797- flat_module ^ "." ^ Naming.type_name def_name
7777+ if ext_nsid = nsid then
7878+ (* ref to same nsid, treat as local *)
7979+ Naming.type_name def_name
8080+ else begin
8181+ (* use flat module names for include_subdirs unqualified *)
8282+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
8383+ add_import out flat_module ;
8484+ flat_module ^ "." ^ Naming.type_name def_name
8585+ end
9886 | [ext_nsid] ->
9999- (* just nsid, refers to main def *)
100100- let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
101101- add_import out flat_module ; flat_module ^ ".main"
8787+ if ext_nsid = nsid then Naming.type_name "main"
8888+ else begin
8989+ (* just nsid, refers to main def *)
9090+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
9191+ add_import out flat_module ; flat_module ^ ".main"
9292+ end
10293 | _ ->
10394 "invalid_ref"
10495 end
···199190 inline_unions
200191201192(* generate object type definition *)
202202-let gen_object_type nsid out name (spec : object_spec) =
193193+(* ~first: use "type" if true, "and" if false *)
194194+(* ~last: add [@@deriving yojson] if true *)
195195+let gen_object_type ?(first = true) ?(last = true) nsid out name
196196+ (spec : object_spec) =
203197 let required = Option.value spec.required ~default:[] in
204198 let nullable = Option.value spec.nullable ~default:[] in
199199+ let keyword = if first then "type" else "and" in
205200 (* handle empty objects as unit *)
206201 if spec.properties = [] then begin
207207- emitln out (Printf.sprintf "type %s = unit" (Naming.type_name name)) ;
208208- emitln out
209209- (Printf.sprintf "let %s_of_yojson _ = Ok ()" (Naming.type_name name)) ;
210210- emitln out
211211- (Printf.sprintf "let %s_to_yojson () = `Assoc []" (Naming.type_name name)) ;
212212- emit_newline out
202202+ emitln out (Printf.sprintf "%s %s = unit" keyword (Naming.type_name name)) ;
203203+ if last then begin
204204+ emitln out
205205+ (Printf.sprintf "let %s_of_yojson _ = Ok ()" (Naming.type_name name)) ;
206206+ emitln out
207207+ (Printf.sprintf "let %s_to_yojson () = `Assoc []"
208208+ (Naming.type_name name) ) ;
209209+ emit_newline out
210210+ end
213211 end
214212 else begin
215215- (* generate inline union types first *)
216216- gen_inline_unions nsid out spec.properties ;
217217- emitln out (Printf.sprintf "type %s =" (Naming.type_name name)) ;
213213+ (* generate inline union types first, but only if this is the first type *)
214214+ if first then gen_inline_unions nsid out spec.properties ;
215215+ emitln out (Printf.sprintf "%s %s =" keyword (Naming.type_name name)) ;
218216 emitln out " {" ;
219217 List.iter
220218 (fun (prop_name, (prop : property)) ->
···235233 default_attr ) )
236234 spec.properties ;
237235 emitln out " }" ;
238238- emitln out "[@@deriving yojson {strict= false}]" ;
239239- emit_newline out
236236+ if last then begin
237237+ emitln out "[@@deriving yojson {strict= false}]" ;
238238+ emit_newline out
239239+ end
240240 end
241241242242(* generate union type definition *)
···395395 emit_newline out ) ;
396396 (* generate output type *)
397397 ( if output_is_bytes then begin
398398- emitln out " (** Raw bytes output with content type *)" ;
398398+ emitln out " (** raw bytes output with content type *)" ;
399399 emitln out " type output = string * string" ;
400400 emit_newline out
401401 end
···493493 emit out " " ;
494494 ( match body.schema with
495495 | Some (Object spec) ->
496496- (* generate inline union types first *)
497497- gen_inline_unions nsid out spec.properties ;
498498- let required = Option.value spec.required ~default:[] in
499499- emitln out "type input =" ;
500500- emitln out " {" ;
501501- List.iter
502502- (fun (prop_name, (prop : property)) ->
503503- let ocaml_name = Naming.field_name prop_name in
504504- let base_type = gen_type_ref nsid out prop.type_def in
505505- let is_required = List.mem prop_name required in
506506- let type_str =
507507- if is_required then base_type else base_type ^ " option"
508508- in
509509- let key_attr = Naming.key_annotation prop_name ocaml_name in
510510- let default_attr =
511511- if is_required then "" else " [@default None]"
512512- in
513513- emitln out
514514- (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
515515- key_attr default_attr ) )
516516- spec.properties ;
517517- emitln out " }" ;
518518- emitln out " [@@deriving yojson {strict= false}]"
496496+ if spec.properties = [] then begin
497497+ (* empty object input *)
498498+ emitln out "type input = unit" ;
499499+ emitln out " let input_of_yojson _ = Ok ()" ;
500500+ emitln out " let input_to_yojson () = `Assoc []"
501501+ end
502502+ else begin
503503+ (* generate inline union types first *)
504504+ gen_inline_unions nsid out spec.properties ;
505505+ let required = Option.value spec.required ~default:[] in
506506+ emitln out "type input =" ;
507507+ emitln out " {" ;
508508+ List.iter
509509+ (fun (prop_name, (prop : property)) ->
510510+ let ocaml_name = Naming.field_name prop_name in
511511+ let base_type = gen_type_ref nsid out prop.type_def in
512512+ let is_required = List.mem prop_name required in
513513+ let type_str =
514514+ if is_required then base_type else base_type ^ " option"
515515+ in
516516+ let key_attr = Naming.key_annotation prop_name ocaml_name in
517517+ let default_attr =
518518+ if is_required then "" else " [@default None]"
519519+ in
520520+ emitln out
521521+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
522522+ key_attr default_attr ) )
523523+ spec.properties ;
524524+ emitln out " }" ;
525525+ emitln out " [@@deriving yojson {strict= false}]"
526526+ end
519527 | Some other_type ->
520528 emitln out
521529 (Printf.sprintf "type input = %s"
···528536 () ) ;
529537 (* generate output type *)
530538 ( if output_is_bytes then begin
531531- emitln out " (** Raw bytes output with content type *)" ;
539539+ emitln out " (** raw bytes output with content type *)" ;
532540 emitln out " type output = (string * string) option" ;
533541 emit_newline out
534542 end
···629637 | Some body -> (
630638 match body.schema with
631639 | Some (Object obj_spec) ->
632632- emit out " let input = Some ({" ;
633633- let fields =
634634- List.map
635635- (fun (prop_name, _) -> Naming.field_name prop_name)
636636- obj_spec.properties
637637- in
638638- emit out (String.concat "; " fields) ;
639639- emitln out "} |> input_to_yojson) in"
640640+ if obj_spec.properties = [] then
641641+ (* empty object uses unit *)
642642+ emitln out " let input = Some (input_to_yojson ()) in"
643643+ else begin
644644+ emit out " let input = Some ({" ;
645645+ let fields =
646646+ List.map
647647+ (fun (prop_name, _) -> Naming.field_name prop_name)
648648+ obj_spec.properties
649649+ in
650650+ emit out (String.concat "; " fields) ;
651651+ emitln out "} |> input_to_yojson) in"
652652+ end
640653 | Some _ ->
641654 emitln out " let input = Some (input_to_yojson input) in"
642655 | None ->
···659672 emit_newline out
660673661674(* generate string type alias (for strings with knownValues) *)
662662-let gen_string_type _nsid out name (spec : string_spec) =
675675+let gen_string_type out name (spec : string_spec) =
663676 let type_name = Naming.type_name name in
664677 emitln out
665665- (Printf.sprintf "(** String type with known values%s *)"
678678+ (Printf.sprintf "(** string type with known values%s *)"
666679 (match spec.description with Some d -> ": " ^ d | None -> "") ) ;
667680 emitln out (Printf.sprintf "type %s = string" type_name) ;
668681 emitln out (Printf.sprintf "let %s_of_yojson = function" type_name) ;
···671684 emitln out (Printf.sprintf "let %s_to_yojson s = `String s" type_name) ;
672685 emit_newline out
673686674674-(* collect local refs from a type definition *)
675675-let rec collect_local_refs acc = function
676676- | Array {items; _} ->
677677- collect_local_refs acc items
678678- | Ref {ref_; _} ->
679679- if String.length ref_ > 0 && ref_.[0] = '#' then
680680- let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
681681- def_name :: acc
682682- else acc
683683- | Union {refs; _} ->
684684- List.fold_left
685685- (fun a r ->
686686- if String.length r > 0 && r.[0] = '#' then
687687- let def_name = String.sub r 1 (String.length r - 1) in
688688- def_name :: a
689689- else a )
690690- acc refs
691691- | Object {properties; _} ->
692692- List.fold_left
693693- (fun a (_, (prop : property)) -> collect_local_refs a prop.type_def)
694694- acc properties
695695- | Record {record; _} ->
696696- List.fold_left
697697- (fun a (_, (prop : property)) -> collect_local_refs a prop.type_def)
698698- acc record.properties
699699- | Query {parameters; output; _} -> (
700700- let acc =
701701- match parameters with
702702- | Some params ->
703703- List.fold_left
704704- (fun a (_, (prop : property)) ->
705705- collect_local_refs a prop.type_def )
706706- acc params.properties
707707- | None ->
708708- acc
709709- in
710710- match output with
711711- | Some body ->
712712- Option.fold ~none:acc ~some:(collect_local_refs acc) body.schema
713713- | None ->
714714- acc )
715715- | Procedure {parameters; input; output; _} -> (
716716- let acc =
717717- match parameters with
718718- | Some params ->
719719- List.fold_left
720720- (fun a (_, (prop : property)) ->
721721- collect_local_refs a prop.type_def )
722722- acc params.properties
723723- | None ->
724724- acc
725725- in
726726- let acc =
727727- match input with
728728- | Some body ->
729729- Option.fold ~none:acc ~some:(collect_local_refs acc) body.schema
730730- | None ->
731731- acc
732732- in
733733- match output with
734734- | Some body ->
735735- Option.fold ~none:acc ~some:(collect_local_refs acc) body.schema
736736- | None ->
737737- acc )
687687+let find_sccs = Scc.find_def_sccs
688688+689689+(* helper to check if a def generates a type (vs token/query/procedure) *)
690690+let is_type_def def =
691691+ match def.type_def with
692692+ | Object _ | Union _ | Record _ ->
693693+ true
694694+ | String spec when spec.known_values <> None ->
695695+ true
738696 | _ ->
739739- acc
697697+ false
740698741741-(* sort definitions so dependencies come first *)
742742-let sort_definitions (defs : def_entry list) : def_entry list =
743743- (* build dependency map: name -> list of dependencies *)
744744- let deps =
745745- List.map (fun def -> (def.name, collect_local_refs [] def.type_def)) defs
746746- in
747747- (* create name -> def map *)
748748- let def_map = List.fold_left (fun m def -> (def.name, def) :: m) [] defs in
749749- (* topological sort *)
750750- let rec visit visited sorted name =
751751- if List.mem name visited then (visited, sorted)
752752- else
753753- let visited = name :: visited in
754754- let dep_names = try List.assoc name deps with Not_found -> [] in
755755- let visited, sorted =
756756- List.fold_left (fun (v, s) d -> visit v s d) (visited, sorted) dep_names
757757- in
758758- let sorted =
759759- match List.assoc_opt name def_map with
760760- | Some def ->
761761- def :: sorted
762762- | None ->
763763- sorted
764764- in
765765- (visited, sorted)
766766- in
767767- let _, sorted =
768768- List.fold_left (fun (v, s) def -> visit v s def.name) ([], []) defs
769769- in
770770- (* sorted is in reverse order, reverse it *)
771771- List.rev sorted
699699+(* helper to check if a def is an object type (can use [@@deriving yojson]) *)
700700+let is_object_def def =
701701+ match def.type_def with Object _ | Record _ -> true | _ -> false
702702+703703+(* generate a single definition *)
704704+let gen_single_def ?(first = true) ?(last = true) nsid out def =
705705+ match def.type_def with
706706+ | Object spec ->
707707+ gen_object_type ~first ~last nsid out def.name spec
708708+ | Union spec ->
709709+ (* unions always generate their own converters, so they're always "complete" *)
710710+ gen_union_type nsid out def.name spec
711711+ | Token spec ->
712712+ gen_token nsid out def.name spec
713713+ | Query spec ->
714714+ gen_query nsid out def.name spec
715715+ | Procedure spec ->
716716+ gen_procedure nsid out def.name spec
717717+ | Record spec ->
718718+ gen_object_type ~first ~last nsid out def.name spec.record
719719+ | String spec when spec.known_values <> None ->
720720+ gen_string_type out def.name spec
721721+ | String _
722722+ | Integer _
723723+ | Boolean _
724724+ | Bytes _
725725+ | Blob _
726726+ | CidLink _
727727+ | Array _
728728+ | Ref _
729729+ | Unknown _
730730+ | Subscription _ ->
731731+ ()
732732+733733+(* generate a group of mutually recursive definitions (SCC) *)
734734+let gen_scc nsid out scc =
735735+ match scc with
736736+ | [] ->
737737+ ()
738738+ | [def] ->
739739+ (* single definition, no cycle *)
740740+ gen_single_def nsid out def
741741+ | defs ->
742742+ (* multiple definitions forming a cycle *)
743743+ (* first, collect and generate all inline unions from all objects in the SCC *)
744744+ List.iter
745745+ (fun def ->
746746+ match def.type_def with
747747+ | Object spec ->
748748+ gen_inline_unions nsid out spec.properties
749749+ | Record spec ->
750750+ gen_inline_unions nsid out spec.record.properties
751751+ | _ ->
752752+ () )
753753+ defs ;
754754+ (* separate object-like types from others *)
755755+ let obj_defs = List.filter is_object_def defs in
756756+ let other_defs = List.filter (fun d -> not (is_object_def d)) defs in
757757+ (* generate other types first (unions, etc.) - they define their own converters *)
758758+ List.iter (fun def -> gen_single_def nsid out def) other_defs ;
759759+ (* generate object types as mutually recursive *)
760760+ let n = List.length obj_defs in
761761+ List.iteri
762762+ (fun i def ->
763763+ let first = i = 0 in
764764+ let last = i = n - 1 in
765765+ match def.type_def with
766766+ | Object spec ->
767767+ (* skip inline unions since we already generated them above *)
768768+ let required = Option.value spec.required ~default:[] in
769769+ let nullable = Option.value spec.nullable ~default:[] in
770770+ let keyword = if first then "type" else "and" in
771771+ if spec.properties = [] then begin
772772+ emitln out
773773+ (Printf.sprintf "%s %s = unit" keyword
774774+ (Naming.type_name def.name) ) ;
775775+ if last then begin
776776+ (* for empty objects in a recursive group, we still need deriving *)
777777+ emitln out "[@@deriving yojson {strict= false}]" ;
778778+ emit_newline out
779779+ end
780780+ end
781781+ else begin
782782+ emitln out
783783+ (Printf.sprintf "%s %s =" keyword (Naming.type_name def.name)) ;
784784+ emitln out " {" ;
785785+ List.iter
786786+ (fun (prop_name, (prop : property)) ->
787787+ let ocaml_name = Naming.field_name prop_name in
788788+ let base_type = gen_type_ref nsid out prop.type_def in
789789+ let is_required = List.mem prop_name required in
790790+ let is_nullable = List.mem prop_name nullable in
791791+ let type_str =
792792+ if is_required && not is_nullable then base_type
793793+ else base_type ^ " option"
794794+ in
795795+ let key_attr = Naming.key_annotation prop_name ocaml_name in
796796+ let default_attr =
797797+ if is_required && not is_nullable then ""
798798+ else " [@default None]"
799799+ in
800800+ emitln out
801801+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
802802+ key_attr default_attr ) )
803803+ spec.properties ;
804804+ emitln out " }" ;
805805+ if last then begin
806806+ emitln out "[@@deriving yojson {strict= false}]" ;
807807+ emit_newline out
808808+ end
809809+ end
810810+ | Record spec ->
811811+ let obj_spec = spec.record in
812812+ let required = Option.value obj_spec.required ~default:[] in
813813+ let nullable = Option.value obj_spec.nullable ~default:[] in
814814+ let keyword = if first then "type" else "and" in
815815+ if obj_spec.properties = [] then begin
816816+ emitln out
817817+ (Printf.sprintf "%s %s = unit" keyword
818818+ (Naming.type_name def.name) ) ;
819819+ if last then begin
820820+ emitln out "[@@deriving yojson {strict= false}]" ;
821821+ emit_newline out
822822+ end
823823+ end
824824+ else begin
825825+ emitln out
826826+ (Printf.sprintf "%s %s =" keyword (Naming.type_name def.name)) ;
827827+ emitln out " {" ;
828828+ List.iter
829829+ (fun (prop_name, (prop : property)) ->
830830+ let ocaml_name = Naming.field_name prop_name in
831831+ let base_type = gen_type_ref nsid out prop.type_def in
832832+ let is_required = List.mem prop_name required in
833833+ let is_nullable = List.mem prop_name nullable in
834834+ let type_str =
835835+ if is_required && not is_nullable then base_type
836836+ else base_type ^ " option"
837837+ in
838838+ let key_attr = Naming.key_annotation prop_name ocaml_name in
839839+ let default_attr =
840840+ if is_required && not is_nullable then ""
841841+ else " [@default None]"
842842+ in
843843+ emitln out
844844+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
845845+ key_attr default_attr ) )
846846+ obj_spec.properties ;
847847+ emitln out " }" ;
848848+ if last then begin
849849+ emitln out "[@@deriving yojson {strict= false}]" ;
850850+ emit_newline out
851851+ end
852852+ end
853853+ | _ ->
854854+ () )
855855+ obj_defs
772856773857(* generate complete lexicon module *)
774858let gen_lexicon_module (doc : lexicon_doc) : string =
···777861 (* header *)
778862 emitln out (Printf.sprintf "(* generated from %s *)" nsid) ;
779863 emit_newline out ;
780780- (* sort definitions by dependencies *)
781781- let sorted_defs = sort_definitions doc.defs in
782782- (* generate each definition *)
783783- List.iter
784784- (fun def ->
785785- match def.type_def with
786786- | Object spec ->
787787- gen_object_type nsid out def.name spec
788788- | Union spec ->
789789- gen_union_type nsid out def.name spec
790790- | Token spec ->
791791- gen_token nsid out def.name spec
792792- | Query spec ->
793793- gen_query nsid out def.name spec
794794- | Procedure spec ->
795795- gen_procedure nsid out def.name spec
796796- | Record spec ->
797797- (* generate record as object type *)
798798- gen_object_type nsid out def.name spec.record
799799- | String spec when spec.known_values <> None ->
800800- (* generate type alias for strings with known values *)
801801- gen_string_type nsid out def.name spec
802802- | String _
803803- | Integer _
804804- | Boolean _
805805- | Bytes _
806806- | Blob _
807807- | CidLink _
808808- | Array _
809809- | Ref _
810810- | Unknown _
811811- | Subscription _ ->
812812- (* these are typically not standalone definitions *)
813813- () )
814814- sorted_defs ;
815815- Buffer.contents out.buf
864864+ (* find strongly connected components *)
865865+ let sccs = find_sccs nsid doc.defs in
866866+ (* generate each SCC *)
867867+ List.iter (gen_scc nsid out) sccs ;
868868+ Emitter.contents out
816869817870(* get all imports needed for a lexicon *)
818871let get_imports (doc : lexicon_doc) : string list =
···860913 ()
861914 in
862915 List.iter (fun def -> collect_from_type def.type_def) doc.defs ;
863863- out.imports
916916+ Emitter.get_imports out
917917+918918+(* get external nsid dependencies - delegated to Scc module *)
919919+let get_external_nsids = Scc.get_external_nsids
920920+921921+(* generate a merged lexicon module from multiple lexicons *)
922922+let gen_merged_lexicon_module (docs : lexicon_doc list) : string =
923923+ let out = make_output () in
924924+ (* collect all nsids in this merged group for local ref detection *)
925925+ let merged_nsids = List.map (fun d -> d.id) docs in
926926+ (* header *)
927927+ emitln out
928928+ (Printf.sprintf "(* generated from lexicons: %s *)"
929929+ (String.concat ", " merged_nsids) ) ;
930930+ emit_newline out ;
931931+ (* collect all defs from all docs *)
932932+ let all_defs =
933933+ List.concat_map
934934+ (fun doc -> List.map (fun def -> (doc.id, def)) doc.defs)
935935+ docs
936936+ in
937937+ (* collect all inline unions as pseudo-defs for proper ordering *)
938938+ let rec collect_inline_unions_from_type nsid context acc type_def =
939939+ match type_def with
940940+ | Union spec ->
941941+ (* found an inline union - create pseudo-def entry *)
942942+ let union_name = Naming.type_name context in
943943+ (nsid, union_name, spec.refs, spec) :: acc
944944+ | Array {items; _} ->
945945+ collect_inline_unions_from_type nsid (context ^ "_item") acc items
946946+ | Object {properties; _} ->
947947+ List.fold_left
948948+ (fun a (prop_name, (prop : property)) ->
949949+ collect_inline_unions_from_type nsid prop_name a prop.type_def )
950950+ acc properties
951951+ | _ ->
952952+ acc
953953+ in
954954+ let all_inline_unions =
955955+ List.concat_map
956956+ (fun (nsid, def) ->
957957+ match def.type_def with
958958+ | Object spec ->
959959+ List.fold_left
960960+ (fun acc (prop_name, (prop : property)) ->
961961+ collect_inline_unions_from_type nsid prop_name acc prop.type_def )
962962+ [] spec.properties
963963+ | Record spec ->
964964+ List.fold_left
965965+ (fun acc (prop_name, (prop : property)) ->
966966+ collect_inline_unions_from_type nsid prop_name acc prop.type_def )
967967+ [] spec.record.properties
968968+ | _ ->
969969+ [] )
970970+ all_defs
971971+ in
972972+ (* create a lookup for inline unions by their name *)
973973+ let inline_union_map = Hashtbl.create 64 in
974974+ List.iter
975975+ (fun (nsid, name, refs, spec) ->
976976+ Hashtbl.add inline_union_map
977977+ (nsid ^ "#__inline__" ^ name)
978978+ (nsid, name, refs, spec) )
979979+ all_inline_unions ;
980980+ (* detect inline union name collisions - same name but different refs *)
981981+ let inline_union_name_map = Hashtbl.create 64 in
982982+ List.iter
983983+ (fun (nsid, name, refs, _spec) ->
984984+ let sorted_refs = List.sort String.compare refs in
985985+ let existing = Hashtbl.find_opt inline_union_name_map name in
986986+ match existing with
987987+ | None ->
988988+ Hashtbl.add inline_union_name_map name [(nsid, sorted_refs)]
989989+ | Some entries ->
990990+ (* check if this is a different union (different refs) *)
991991+ if not (List.exists (fun (_, r) -> r = sorted_refs) entries) then
992992+ Hashtbl.replace inline_union_name_map name
993993+ ((nsid, sorted_refs) :: entries) )
994994+ all_inline_unions ;
995995+ let colliding_inline_union_names =
996996+ Hashtbl.fold
997997+ (fun name entries acc ->
998998+ if List.length entries > 1 then name :: acc else acc )
999999+ inline_union_name_map []
10001000+ in
10011001+ (* the "host" nsid is the first one - types from here keep short names *)
10021002+ let host_nsid = List.hd merged_nsids in
10031003+ (* function to get unique inline union name *)
10041004+ (* only prefix names from "visiting" nsids, not the host *)
10051005+ let get_unique_inline_union_name nsid name =
10061006+ if List.mem name colliding_inline_union_names && nsid <> host_nsid then
10071007+ Naming.flat_name_of_nsid nsid ^ "_" ^ name
10081008+ else name
10091009+ in
10101010+ (* detect name collisions - names that appear in multiple nsids *)
10111011+ let name_counts = Hashtbl.create 64 in
10121012+ List.iter
10131013+ (fun (nsid, def) ->
10141014+ let existing = Hashtbl.find_opt name_counts def.name in
10151015+ match existing with
10161016+ | None ->
10171017+ Hashtbl.add name_counts def.name [nsid]
10181018+ | Some nsids when not (List.mem nsid nsids) ->
10191019+ Hashtbl.replace name_counts def.name (nsid :: nsids)
10201020+ | _ ->
10211021+ () )
10221022+ all_defs ;
10231023+ let colliding_names =
10241024+ Hashtbl.fold
10251025+ (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc)
10261026+ name_counts []
10271027+ in
10281028+ (* function to get unique type name, adding nsid prefix for collisions *)
10291029+ (* only prefix names from "visiting" nsids, not the host *)
10301030+ let get_unique_type_name nsid def_name =
10311031+ if List.mem def_name colliding_names && nsid <> host_nsid then
10321032+ (* use full nsid as prefix to guarantee uniqueness *)
10331033+ (* app.bsky.feed.defs#viewerState -> app_bsky_feed_defs_viewer_state *)
10341034+ let prefix = Naming.flat_name_of_nsid nsid ^ "_" in
10351035+ Naming.type_name (prefix ^ def_name)
10361036+ else Naming.type_name def_name
10371037+ in
10381038+ (* for merged modules, we need to handle refs differently:
10391039+ - refs to other nsids in the merged group become local refs
10401040+ - refs within same nsid stay as local refs *)
10411041+ (* custom ref type generator that treats merged nsids as local *)
10421042+ let rec gen_merged_type_ref current_nsid type_def =
10431043+ match type_def with
10441044+ | String _ ->
10451045+ "string"
10461046+ | Integer {maximum; _} -> (
10471047+ match maximum with Some m when m > 1073741823 -> "int64" | _ -> "int" )
10481048+ | Boolean _ ->
10491049+ "bool"
10501050+ | Bytes _ ->
10511051+ "bytes"
10521052+ | Blob _ ->
10531053+ "Hermes.blob"
10541054+ | CidLink _ ->
10551055+ "Cid.t"
10561056+ | Array {items; _} ->
10571057+ let item_type = gen_merged_type_ref current_nsid items in
10581058+ item_type ^ " list"
10591059+ | Object _ ->
10601060+ "object_todo"
10611061+ | Ref {ref_; _} ->
10621062+ gen_merged_ref_type current_nsid ref_
10631063+ | Union {refs; _} -> (
10641064+ match lookup_union_name out refs with
10651065+ | Some name ->
10661066+ name
10671067+ | None ->
10681068+ gen_union_type_name refs )
10691069+ | Token _ ->
10701070+ "string"
10711071+ | Unknown _ ->
10721072+ "Yojson.Safe.t"
10731073+ | Query _ | Procedure _ | Subscription _ | Record _ ->
10741074+ "unit (* primary type *)"
10751075+ and gen_merged_ref_type current_nsid ref_str =
10761076+ if String.length ref_str > 0 && ref_str.[0] = '#' then begin
10771077+ (* local ref within same nsid *)
10781078+ let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
10791079+ get_unique_type_name current_nsid def_name
10801080+ end
10811081+ else begin
10821082+ match String.split_on_char '#' ref_str with
10831083+ | [ext_nsid; def_name] ->
10841084+ if List.mem ext_nsid merged_nsids then
10851085+ (* ref to another nsid in the merged group - use unique name *)
10861086+ get_unique_type_name ext_nsid def_name
10871087+ else begin
10881088+ (* truly external ref *)
10891089+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
10901090+ add_import out flat_module ;
10911091+ flat_module ^ "." ^ Naming.type_name def_name
10921092+ end
10931093+ | [ext_nsid] ->
10941094+ if List.mem ext_nsid merged_nsids then
10951095+ get_unique_type_name ext_nsid "main"
10961096+ else begin
10971097+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
10981098+ add_import out flat_module ; flat_module ^ ".main"
10991099+ end
11001100+ | _ ->
11011101+ "invalid_ref"
11021102+ end
11031103+ in
11041104+ (* generate converter expression for reading a type from json *)
11051105+ (* returns (converter_expr, needs_result_unwrap) - if needs_result_unwrap is true, caller should apply Result.get_ok *)
11061106+ let gen_of_yojson_expr current_nsid type_def =
11071107+ match type_def with
11081108+ | String _ | Token _ ->
11091109+ ("to_string", false)
11101110+ | Integer {maximum; _} -> (
11111111+ match maximum with
11121112+ | Some m when m > 1073741823 ->
11131113+ ("(fun j -> Int64.of_int (to_int j))", false)
11141114+ | _ ->
11151115+ ("to_int", false) )
11161116+ | Boolean _ ->
11171117+ ("to_bool", false)
11181118+ | Bytes _ ->
11191119+ ("(fun j -> Bytes.of_string (to_string j))", false)
11201120+ | Blob _ ->
11211121+ ("Hermes.blob_of_yojson", true)
11221122+ | CidLink _ ->
11231123+ ("Cid.of_yojson", true)
11241124+ | Array {items; _} ->
11251125+ let item_type = gen_merged_type_ref current_nsid items in
11261126+ ( Printf.sprintf
11271127+ "(fun j -> to_list j |> List.filter_map (fun x -> match \
11281128+ %s_of_yojson x with Ok v -> Some v | _ -> None))"
11291129+ item_type
11301130+ , false )
11311131+ | Ref {ref_; _} ->
11321132+ let type_name = gen_merged_ref_type current_nsid ref_ in
11331133+ (type_name ^ "_of_yojson", true)
11341134+ | Union {refs; _} ->
11351135+ let type_name =
11361136+ match lookup_union_name out refs with
11371137+ | Some n ->
11381138+ n
11391139+ | None ->
11401140+ gen_union_type_name refs
11411141+ in
11421142+ (type_name ^ "_of_yojson", true)
11431143+ | Unknown _ ->
11441144+ ("(fun j -> j)", false)
11451145+ | _ ->
11461146+ ("(fun _ -> failwith \"unsupported type\")", false)
11471147+ in
11481148+ (* generate converter expression for writing a type to json *)
11491149+ let gen_to_yojson_expr current_nsid type_def =
11501150+ match type_def with
11511151+ | String _ | Token _ ->
11521152+ "(fun s -> `String s)"
11531153+ | Integer {maximum; _} -> (
11541154+ match maximum with
11551155+ | Some m when m > 1073741823 ->
11561156+ "(fun i -> `Int (Int64.to_int i))"
11571157+ | _ ->
11581158+ "(fun i -> `Int i)" )
11591159+ | Boolean _ ->
11601160+ "(fun b -> `Bool b)"
11611161+ | Bytes _ ->
11621162+ "(fun b -> `String (Bytes.to_string b))"
11631163+ | Blob _ ->
11641164+ "Hermes.blob_to_yojson"
11651165+ | CidLink _ ->
11661166+ "Cid.to_yojson"
11671167+ | Array {items; _} ->
11681168+ let item_type = gen_merged_type_ref current_nsid items in
11691169+ Printf.sprintf "(fun l -> `List (List.map %s_to_yojson l))" item_type
11701170+ | Ref {ref_; _} ->
11711171+ let type_name = gen_merged_ref_type current_nsid ref_ in
11721172+ type_name ^ "_to_yojson"
11731173+ | Union {refs; _} ->
11741174+ let type_name =
11751175+ match lookup_union_name out refs with
11761176+ | Some n ->
11771177+ n
11781178+ | None ->
11791179+ gen_union_type_name refs
11801180+ in
11811181+ type_name ^ "_to_yojson"
11821182+ | Unknown _ ->
11831183+ "(fun j -> j)"
11841184+ | _ ->
11851185+ "(fun _ -> `Null)"
11861186+ in
11871187+ (* generate type uri for merged context *)
11881188+ let gen_merged_type_uri current_nsid ref_str =
11891189+ if String.length ref_str > 0 && ref_str.[0] = '#' then
11901190+ current_nsid ^ ref_str
11911191+ else ref_str
11921192+ in
11931193+ (* register inline union names without generating code *)
11941194+ let register_merged_inline_unions nsid properties =
11951195+ let rec collect_inline_unions_with_context context acc type_def =
11961196+ match type_def with
11971197+ | Union spec ->
11981198+ (context, spec.refs, spec) :: acc
11991199+ | Array {items; _} ->
12001200+ collect_inline_unions_with_context (context ^ "_item") acc items
12011201+ | _ ->
12021202+ acc
12031203+ in
12041204+ let inline_unions =
12051205+ List.fold_left
12061206+ (fun acc (prop_name, (prop : property)) ->
12071207+ collect_inline_unions_with_context prop_name acc prop.type_def )
12081208+ [] properties
12091209+ in
12101210+ List.iter
12111211+ (fun (context, refs, _spec) ->
12121212+ let base_name = Naming.type_name context in
12131213+ let unique_name = get_unique_inline_union_name nsid base_name in
12141214+ register_union_name out refs unique_name )
12151215+ inline_unions
12161216+ in
12171217+ (* generate object type for merged context *)
12181218+ let gen_merged_object_type ?(first = true) ?(last = true) current_nsid name
12191219+ (spec : object_spec) =
12201220+ let required = Option.value spec.required ~default:[] in
12211221+ let nullable = Option.value spec.nullable ~default:[] in
12221222+ let keyword = if first then "type" else "and" in
12231223+ let type_name = get_unique_type_name current_nsid name in
12241224+ if spec.properties = [] then begin
12251225+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ;
12261226+ if last then begin
12271227+ emitln out (Printf.sprintf "let %s_of_yojson _ = Ok ()" type_name) ;
12281228+ emitln out (Printf.sprintf "let %s_to_yojson () = `Assoc []" type_name) ;
12291229+ emit_newline out
12301230+ end
12311231+ end
12321232+ else begin
12331233+ if first then register_merged_inline_unions current_nsid spec.properties ;
12341234+ emitln out (Printf.sprintf "%s %s =" keyword type_name) ;
12351235+ emitln out " {" ;
12361236+ List.iter
12371237+ (fun (prop_name, (prop : property)) ->
12381238+ let ocaml_name = Naming.field_name prop_name in
12391239+ let base_type = gen_merged_type_ref current_nsid prop.type_def in
12401240+ let is_required = List.mem prop_name required in
12411241+ let is_nullable = List.mem prop_name nullable in
12421242+ let type_str =
12431243+ if is_required && not is_nullable then base_type
12441244+ else base_type ^ " option"
12451245+ in
12461246+ let key_attr = Naming.key_annotation prop_name ocaml_name in
12471247+ let default_attr =
12481248+ if is_required && not is_nullable then "" else " [@default None]"
12491249+ in
12501250+ emitln out
12511251+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr
12521252+ default_attr ) )
12531253+ spec.properties ;
12541254+ emitln out " }" ;
12551255+ if last then begin
12561256+ emitln out "[@@deriving yojson {strict= false}]" ;
12571257+ emit_newline out
12581258+ end
12591259+ end
12601260+ in
12611261+ (* generate union type for merged context *)
12621262+ let gen_merged_union_type current_nsid name (spec : union_spec) =
12631263+ let type_name = get_unique_type_name current_nsid name in
12641264+ let is_closed = Option.value spec.closed ~default:false in
12651265+ emitln out (Printf.sprintf "type %s =" type_name) ;
12661266+ List.iter
12671267+ (fun ref_str ->
12681268+ let variant_name = Naming.variant_name_of_ref ref_str in
12691269+ let payload_type = gen_merged_ref_type current_nsid ref_str in
12701270+ emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) )
12711271+ spec.refs ;
12721272+ if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ;
12731273+ emit_newline out ;
12741274+ emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ;
12751275+ emitln out " let open Yojson.Safe.Util in" ;
12761276+ emitln out " try" ;
12771277+ emitln out " match json |> member \"$type\" |> to_string with" ;
12781278+ List.iter
12791279+ (fun ref_str ->
12801280+ let variant_name = Naming.variant_name_of_ref ref_str in
12811281+ let full_type_uri = gen_merged_type_uri current_nsid ref_str in
12821282+ let payload_type = gen_merged_ref_type current_nsid ref_str in
12831283+ emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ;
12841284+ emitln out
12851285+ (Printf.sprintf " (match %s_of_yojson json with" payload_type) ;
12861286+ emitln out (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ;
12871287+ emitln out " | Error e -> Error e)" )
12881288+ spec.refs ;
12891289+ if is_closed then
12901290+ emitln out " | t -> Error (\"unknown union type: \" ^ t)"
12911291+ else emitln out " | _ -> Ok (Unknown json)" ;
12921292+ emitln out " with _ -> Error \"failed to parse union\"" ;
12931293+ emit_newline out ;
12941294+ emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ;
12951295+ List.iter
12961296+ (fun ref_str ->
12971297+ let variant_name = Naming.variant_name_of_ref ref_str in
12981298+ let full_type_uri = gen_merged_type_uri current_nsid ref_str in
12991299+ let payload_type = gen_merged_ref_type current_nsid ref_str in
13001300+ emitln out (Printf.sprintf " | %s v ->" variant_name) ;
13011301+ emitln out
13021302+ (Printf.sprintf " (match %s_to_yojson v with" payload_type) ;
13031303+ emitln out
13041304+ (Printf.sprintf
13051305+ " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") :: \
13061306+ fields)"
13071307+ full_type_uri ) ;
13081308+ emitln out " | other -> other)" )
13091309+ spec.refs ;
13101310+ if not is_closed then emitln out " | Unknown j -> j" ;
13111311+ emit_newline out
13121312+ in
13131313+ (* collect refs for merged SCC detection, using compound keys (nsid#name) *)
13141314+ let collect_merged_local_refs current_nsid acc type_def =
13151315+ let rec aux acc = function
13161316+ | Array {items; _} ->
13171317+ aux acc items
13181318+ | Ref {ref_; _} ->
13191319+ if String.length ref_ > 0 && ref_.[0] = '#' then
13201320+ (* local ref: #foo -> current_nsid#foo *)
13211321+ let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
13221322+ (current_nsid ^ "#" ^ def_name) :: acc
13231323+ else begin
13241324+ match String.split_on_char '#' ref_ with
13251325+ | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids ->
13261326+ (* cross-nsid ref within merged group *)
13271327+ (ext_nsid ^ "#" ^ def_name) :: acc
13281328+ | _ ->
13291329+ acc
13301330+ end
13311331+ | Union {refs; _} ->
13321332+ List.fold_left
13331333+ (fun a r ->
13341334+ if String.length r > 0 && r.[0] = '#' then
13351335+ let def_name = String.sub r 1 (String.length r - 1) in
13361336+ (current_nsid ^ "#" ^ def_name) :: a
13371337+ else
13381338+ match String.split_on_char '#' r with
13391339+ | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids ->
13401340+ (ext_nsid ^ "#" ^ def_name) :: a
13411341+ | _ ->
13421342+ a )
13431343+ acc refs
13441344+ | Object {properties; _} ->
13451345+ List.fold_left
13461346+ (fun a (_, (prop : property)) -> aux a prop.type_def)
13471347+ acc properties
13481348+ | Record {record; _} ->
13491349+ List.fold_left
13501350+ (fun a (_, (prop : property)) -> aux a prop.type_def)
13511351+ acc record.properties
13521352+ | Query {parameters; output; _} -> (
13531353+ let acc =
13541354+ match parameters with
13551355+ | Some params ->
13561356+ List.fold_left
13571357+ (fun a (_, (prop : property)) -> aux a prop.type_def)
13581358+ acc params.properties
13591359+ | None ->
13601360+ acc
13611361+ in
13621362+ match output with
13631363+ | Some body ->
13641364+ Option.fold ~none:acc ~some:(aux acc) body.schema
13651365+ | None ->
13661366+ acc )
13671367+ | Procedure {parameters; input; output; _} -> (
13681368+ let acc =
13691369+ match parameters with
13701370+ | Some params ->
13711371+ List.fold_left
13721372+ (fun a (_, (prop : property)) -> aux a prop.type_def)
13731373+ acc params.properties
13741374+ | None ->
13751375+ acc
13761376+ in
13771377+ let acc =
13781378+ match input with
13791379+ | Some body ->
13801380+ Option.fold ~none:acc ~some:(aux acc) body.schema
13811381+ | None ->
13821382+ acc
13831383+ in
13841384+ match output with
13851385+ | Some body ->
13861386+ Option.fold ~none:acc ~some:(aux acc) body.schema
13871387+ | None ->
13881388+ acc )
13891389+ | _ ->
13901390+ acc
13911391+ in
13921392+ aux acc type_def
13931393+ in
13941394+ (* generate merged SCC *)
13951395+ let gen_merged_scc scc =
13961396+ match scc with
13971397+ | [] ->
13981398+ ()
13991399+ | [(nsid, def)] -> (
14001400+ match def.type_def with
14011401+ | Object spec ->
14021402+ gen_merged_object_type nsid def.name spec
14031403+ | Union spec ->
14041404+ gen_merged_union_type nsid def.name spec
14051405+ | Token spec ->
14061406+ gen_token nsid out def.name spec
14071407+ | Query spec ->
14081408+ gen_query nsid out def.name spec
14091409+ | Procedure spec ->
14101410+ gen_procedure nsid out def.name spec
14111411+ | Record spec ->
14121412+ gen_merged_object_type nsid def.name spec.record
14131413+ | String spec when spec.known_values <> None ->
14141414+ gen_string_type out def.name spec
14151415+ | Array {items; _} ->
14161416+ (* generate inline union for array items if needed *)
14171417+ ( match items with
14181418+ | Union spec ->
14191419+ let item_type_name = Naming.type_name (def.name ^ "_item") in
14201420+ register_union_name out spec.refs item_type_name ;
14211421+ gen_merged_union_type nsid (def.name ^ "_item") spec
14221422+ | _ ->
14231423+ () ) ;
14241424+ (* generate type alias for array *)
14251425+ let type_name = get_unique_type_name nsid def.name in
14261426+ let item_type = gen_merged_type_ref nsid items in
14271427+ emitln out (Printf.sprintf "type %s = %s list" type_name item_type) ;
14281428+ emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ;
14291429+ emitln out " let open Yojson.Safe.Util in" ;
14301430+ emitln out
14311431+ (Printf.sprintf
14321432+ " Ok (to_list json |> List.filter_map (fun x -> match \
14331433+ %s_of_yojson x with Ok v -> Some v | _ -> None))"
14341434+ item_type ) ;
14351435+ emitln out
14361436+ (Printf.sprintf
14371437+ "let %s_to_yojson l = `List (List.map %s_to_yojson l)" type_name
14381438+ item_type ) ;
14391439+ emit_newline out
14401440+ | _ ->
14411441+ () )
14421442+ | defs ->
14431443+ (* multi-def SCC - register inline union names first *)
14441444+ List.iter
14451445+ (fun (nsid, def) ->
14461446+ match def.type_def with
14471447+ | Object spec ->
14481448+ register_merged_inline_unions nsid spec.properties
14491449+ | Record spec ->
14501450+ register_merged_inline_unions nsid spec.record.properties
14511451+ | _ ->
14521452+ () )
14531453+ defs ;
14541454+ let obj_defs =
14551455+ List.filter
14561456+ (fun (_, def) ->
14571457+ match def.type_def with Object _ | Record _ -> true | _ -> false )
14581458+ defs
14591459+ in
14601460+ let other_defs =
14611461+ List.filter
14621462+ (fun (_, def) ->
14631463+ match def.type_def with Object _ | Record _ -> false | _ -> true )
14641464+ defs
14651465+ in
14661466+ List.iter
14671467+ (fun (nsid, def) ->
14681468+ match def.type_def with
14691469+ | Union spec ->
14701470+ gen_merged_union_type nsid def.name spec
14711471+ | Token spec ->
14721472+ gen_token nsid out def.name spec
14731473+ | Query spec ->
14741474+ gen_query nsid out def.name spec
14751475+ | Procedure spec ->
14761476+ gen_procedure nsid out def.name spec
14771477+ | String spec when spec.known_values <> None ->
14781478+ gen_string_type out def.name spec
14791479+ | _ ->
14801480+ () )
14811481+ other_defs ;
14821482+ let n = List.length obj_defs in
14831483+ List.iteri
14841484+ (fun i (nsid, def) ->
14851485+ let first = i = 0 in
14861486+ let last = i = n - 1 in
14871487+ match def.type_def with
14881488+ | Object spec ->
14891489+ let required = Option.value spec.required ~default:[] in
14901490+ let nullable = Option.value spec.nullable ~default:[] in
14911491+ let keyword = if first then "type" else "and" in
14921492+ let type_name = get_unique_type_name nsid def.name in
14931493+ if spec.properties = [] then begin
14941494+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ;
14951495+ if last then begin
14961496+ emitln out "[@@deriving yojson {strict= false}]" ;
14971497+ emit_newline out
14981498+ end
14991499+ end
15001500+ else begin
15011501+ emitln out (Printf.sprintf "%s %s =" keyword type_name) ;
15021502+ emitln out " {" ;
15031503+ List.iter
15041504+ (fun (prop_name, (prop : property)) ->
15051505+ let ocaml_name = Naming.field_name prop_name in
15061506+ let base_type = gen_merged_type_ref nsid prop.type_def in
15071507+ let is_required = List.mem prop_name required in
15081508+ let is_nullable = List.mem prop_name nullable in
15091509+ let type_str =
15101510+ if is_required && not is_nullable then base_type
15111511+ else base_type ^ " option"
15121512+ in
15131513+ let key_attr =
15141514+ Naming.key_annotation prop_name ocaml_name
15151515+ in
15161516+ let default_attr =
15171517+ if is_required && not is_nullable then ""
15181518+ else " [@default None]"
15191519+ in
15201520+ emitln out
15211521+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
15221522+ key_attr default_attr ) )
15231523+ spec.properties ;
15241524+ emitln out " }" ;
15251525+ if last then begin
15261526+ emitln out "[@@deriving yojson {strict= false}]" ;
15271527+ emit_newline out
15281528+ end
15291529+ end
15301530+ | Record spec ->
15311531+ let obj_spec = spec.record in
15321532+ let required = Option.value obj_spec.required ~default:[] in
15331533+ let nullable = Option.value obj_spec.nullable ~default:[] in
15341534+ let keyword = if first then "type" else "and" in
15351535+ let type_name = get_unique_type_name nsid def.name in
15361536+ if obj_spec.properties = [] then begin
15371537+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ;
15381538+ if last then begin
15391539+ emitln out "[@@deriving yojson {strict= false}]" ;
15401540+ emit_newline out
15411541+ end
15421542+ end
15431543+ else begin
15441544+ emitln out (Printf.sprintf "%s %s =" keyword type_name) ;
15451545+ emitln out " {" ;
15461546+ List.iter
15471547+ (fun (prop_name, (prop : property)) ->
15481548+ let ocaml_name = Naming.field_name prop_name in
15491549+ let base_type = gen_merged_type_ref nsid prop.type_def in
15501550+ let is_required = List.mem prop_name required in
15511551+ let is_nullable = List.mem prop_name nullable in
15521552+ let type_str =
15531553+ if is_required && not is_nullable then base_type
15541554+ else base_type ^ " option"
15551555+ in
15561556+ let key_attr =
15571557+ Naming.key_annotation prop_name ocaml_name
15581558+ in
15591559+ let default_attr =
15601560+ if is_required && not is_nullable then ""
15611561+ else " [@default None]"
15621562+ in
15631563+ emitln out
15641564+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str
15651565+ key_attr default_attr ) )
15661566+ obj_spec.properties ;
15671567+ emitln out " }" ;
15681568+ if last then begin
15691569+ emitln out "[@@deriving yojson {strict= false}]" ;
15701570+ emit_newline out
15711571+ end
15721572+ end
15731573+ | _ ->
15741574+ () )
15751575+ obj_defs
15761576+ in
15771577+ (* create extended defs that include inline unions as pseudo-entries *)
15781578+ (* inline union key format: nsid#__inline__name *)
15791579+ let inline_union_defs =
15801580+ List.map
15811581+ (fun (nsid, name, refs, spec) ->
15821582+ let key = nsid ^ "#__inline__" ^ name in
15831583+ (* inline unions depend on the types they reference *)
15841584+ let deps =
15851585+ List.filter_map
15861586+ (fun r ->
15871587+ if String.length r > 0 && r.[0] = '#' then
15881588+ let def_name = String.sub r 1 (String.length r - 1) in
15891589+ Some (nsid ^ "#" ^ def_name)
15901590+ else
15911591+ match String.split_on_char '#' r with
15921592+ | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids ->
15931593+ Some (ext_nsid ^ "#" ^ def_name)
15941594+ | _ ->
15951595+ None )
15961596+ refs
15971597+ in
15981598+ (key, deps, `InlineUnion (nsid, name, refs, spec)) )
15991599+ all_inline_unions
16001600+ in
16011601+ (* create regular def entries *)
16021602+ let regular_def_entries =
16031603+ List.map
16041604+ (fun (nsid, def) ->
16051605+ let key = nsid ^ "#" ^ def.name in
16061606+ let base_deps = collect_merged_local_refs nsid [] def.type_def in
16071607+ (* add dependencies on inline unions used by this def *)
16081608+ let inline_deps =
16091609+ match def.type_def with
16101610+ | Object spec | Record {record= spec; _} ->
16111611+ let rec collect_inline_union_deps acc type_def =
16121612+ match type_def with
16131613+ | Union _ -> (
16141614+ (* this property uses an inline union - find its name *)
16151615+ match lookup_union_name out [] with
16161616+ | _ ->
16171617+ acc (* we'll handle this differently *) )
16181618+ | Array {items; _} ->
16191619+ collect_inline_union_deps acc items
16201620+ | _ ->
16211621+ acc
16221622+ in
16231623+ List.fold_left
16241624+ (fun acc (prop_name, (prop : property)) ->
16251625+ match prop.type_def with
16261626+ | Union _ ->
16271627+ let union_name = Naming.type_name prop_name in
16281628+ (nsid ^ "#__inline__" ^ union_name) :: acc
16291629+ | Array {items= Union _; _} ->
16301630+ let union_name = Naming.type_name (prop_name ^ "_item") in
16311631+ (nsid ^ "#__inline__" ^ union_name) :: acc
16321632+ | _ ->
16331633+ collect_inline_union_deps acc prop.type_def )
16341634+ [] spec.properties
16351635+ | _ ->
16361636+ []
16371637+ in
16381638+ (key, base_deps @ inline_deps, `RegularDef (nsid, def)) )
16391639+ all_defs
16401640+ in
16411641+ (* combine all entries *)
16421642+ let all_entries = regular_def_entries @ inline_union_defs in
16431643+ (* build dependency map *)
16441644+ let deps_map = List.map (fun (k, deps, _) -> (k, deps)) all_entries in
16451645+ let entry_map = List.map (fun (k, _, entry) -> (k, entry)) all_entries in
16461646+ let all_keys = List.map (fun (k, _, _) -> k) all_entries in
16471647+ (* run Tarjan's algorithm on combined entries *)
16481648+ let index_counter = ref 0 in
16491649+ let indices = Hashtbl.create 64 in
16501650+ let lowlinks = Hashtbl.create 64 in
16511651+ let on_stack = Hashtbl.create 64 in
16521652+ let stack = ref [] in
16531653+ let sccs = ref [] in
16541654+ let rec strongconnect key =
16551655+ let index = !index_counter in
16561656+ incr index_counter ;
16571657+ Hashtbl.add indices key index ;
16581658+ Hashtbl.add lowlinks key index ;
16591659+ Hashtbl.add on_stack key true ;
16601660+ stack := key :: !stack ;
16611661+ let successors =
16621662+ try List.assoc key deps_map |> List.filter (fun k -> List.mem k all_keys)
16631663+ with Not_found -> []
16641664+ in
16651665+ List.iter
16661666+ (fun succ ->
16671667+ if not (Hashtbl.mem indices succ) then begin
16681668+ strongconnect succ ;
16691669+ Hashtbl.replace lowlinks key
16701670+ (min (Hashtbl.find lowlinks key) (Hashtbl.find lowlinks succ))
16711671+ end
16721672+ else if Hashtbl.find_opt on_stack succ = Some true then
16731673+ Hashtbl.replace lowlinks key
16741674+ (min (Hashtbl.find lowlinks key) (Hashtbl.find indices succ)) )
16751675+ successors ;
16761676+ if Hashtbl.find lowlinks key = Hashtbl.find indices key then begin
16771677+ let rec pop_scc acc =
16781678+ match !stack with
16791679+ | [] ->
16801680+ acc
16811681+ | top :: rest ->
16821682+ stack := rest ;
16831683+ Hashtbl.replace on_stack top false ;
16841684+ if top = key then top :: acc else pop_scc (top :: acc)
16851685+ in
16861686+ let scc_keys = pop_scc [] in
16871687+ let scc_entries =
16881688+ List.filter_map (fun k -> List.assoc_opt k entry_map) scc_keys
16891689+ in
16901690+ if scc_entries <> [] then sccs := scc_entries :: !sccs
16911691+ end
16921692+ in
16931693+ List.iter
16941694+ (fun key -> if not (Hashtbl.mem indices key) then strongconnect key)
16951695+ all_keys ;
16961696+ let ordered_sccs = List.rev !sccs in
16971697+ (* helper to generate object type definition only (no converters) *)
16981698+ let gen_object_type_only ?(keyword = "type") nsid name (spec : object_spec) =
16991699+ let required = Option.value spec.required ~default:[] in
17001700+ let nullable = Option.value spec.nullable ~default:[] in
17011701+ let type_name = get_unique_type_name nsid name in
17021702+ if spec.properties = [] then
17031703+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name)
17041704+ else begin
17051705+ emitln out (Printf.sprintf "%s %s = {" keyword type_name) ;
17061706+ List.iter
17071707+ (fun (prop_name, (prop : property)) ->
17081708+ let ocaml_name = Naming.field_name prop_name in
17091709+ let base_type = gen_merged_type_ref nsid prop.type_def in
17101710+ let is_required = List.mem prop_name required in
17111711+ let is_nullable = List.mem prop_name nullable in
17121712+ let type_str =
17131713+ if is_required && not is_nullable then base_type
17141714+ else base_type ^ " option"
17151715+ in
17161716+ let key_attr = Naming.key_annotation prop_name ocaml_name in
17171717+ let default_attr =
17181718+ if is_required && not is_nullable then "" else " [@default None]"
17191719+ in
17201720+ emitln out
17211721+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr
17221722+ default_attr ) )
17231723+ spec.properties ;
17241724+ emitln out "}"
17251725+ end
17261726+ in
17271727+ (* helper to generate inline union type definition only (no converters) *)
17281728+ let gen_inline_union_type_only ?(keyword = "type") nsid name refs spec =
17291729+ let is_closed = Option.value spec.closed ~default:false in
17301730+ emitln out (Printf.sprintf "%s %s =" keyword name) ;
17311731+ List.iter
17321732+ (fun ref_str ->
17331733+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
17341734+ let payload_type = gen_merged_ref_type nsid ref_str in
17351735+ emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) )
17361736+ refs ;
17371737+ if not is_closed then emitln out " | Unknown of Yojson.Safe.t"
17381738+ in
17391739+ (* helper to generate object converters *)
17401740+ let gen_object_converters ?(of_keyword = "let") ?(to_keyword = "let") nsid
17411741+ name (spec : object_spec) =
17421742+ let required = Option.value spec.required ~default:[] in
17431743+ let nullable = Option.value spec.nullable ~default:[] in
17441744+ let type_name = get_unique_type_name nsid name in
17451745+ if spec.properties = [] then begin
17461746+ if of_keyword <> "SKIP" then
17471747+ emitln out
17481748+ (Printf.sprintf "%s %s_of_yojson _ = Ok ()" of_keyword type_name) ;
17491749+ if to_keyword <> "SKIP" then
17501750+ emitln out
17511751+ (Printf.sprintf "%s %s_to_yojson () = `Assoc []" to_keyword type_name)
17521752+ end
17531753+ else begin
17541754+ (* of_yojson *)
17551755+ if of_keyword <> "SKIP" then begin
17561756+ emitln out
17571757+ (Printf.sprintf "%s %s_of_yojson json =" of_keyword type_name) ;
17581758+ emitln out " let open Yojson.Safe.Util in" ;
17591759+ emitln out " try" ;
17601760+ List.iter
17611761+ (fun (prop_name, (prop : property)) ->
17621762+ let ocaml_name = Naming.field_name prop_name in
17631763+ let conv_expr, needs_unwrap =
17641764+ gen_of_yojson_expr nsid prop.type_def
17651765+ in
17661766+ let is_required = List.mem prop_name required in
17671767+ let is_nullable = List.mem prop_name nullable in
17681768+ let is_optional = (not is_required) || is_nullable in
17691769+ if is_optional then begin
17701770+ if needs_unwrap then
17711771+ emitln out
17721772+ (Printf.sprintf
17731773+ " let %s = json |> member \"%s\" |> to_option (fun x \
17741774+ -> match %s x with Ok v -> Some v | _ -> None) |> \
17751775+ Option.join in"
17761776+ ocaml_name prop_name conv_expr )
17771777+ else
17781778+ emitln out
17791779+ (Printf.sprintf
17801780+ " let %s = json |> member \"%s\" |> to_option %s in"
17811781+ ocaml_name prop_name conv_expr )
17821782+ end
17831783+ else begin
17841784+ if needs_unwrap then
17851785+ emitln out
17861786+ (Printf.sprintf
17871787+ " let %s = json |> member \"%s\" |> %s |> \
17881788+ Result.get_ok in"
17891789+ ocaml_name prop_name conv_expr )
17901790+ else
17911791+ emitln out
17921792+ (Printf.sprintf " let %s = json |> member \"%s\" |> %s in"
17931793+ ocaml_name prop_name conv_expr )
17941794+ end )
17951795+ spec.properties ;
17961796+ emit out " Ok { " ;
17971797+ emit out
17981798+ (String.concat "; "
17991799+ (List.map (fun (pn, _) -> Naming.field_name pn) spec.properties) ) ;
18001800+ emitln out " }" ;
18011801+ emitln out " with e -> Error (Printexc.to_string e)" ;
18021802+ emit_newline out
18031803+ end ;
18041804+ (* to_yojson *)
18051805+ if to_keyword <> "SKIP" then begin
18061806+ emitln out
18071807+ (Printf.sprintf "%s %s_to_yojson (r : %s) =" to_keyword type_name
18081808+ type_name ) ;
18091809+ emitln out " `Assoc [" ;
18101810+ List.iteri
18111811+ (fun i (prop_name, (prop : property)) ->
18121812+ let ocaml_name = Naming.field_name prop_name in
18131813+ let conv_expr = gen_to_yojson_expr nsid prop.type_def in
18141814+ let is_required = List.mem prop_name required in
18151815+ let is_nullable = List.mem prop_name nullable in
18161816+ let is_optional = (not is_required) || is_nullable in
18171817+ let comma =
18181818+ if i < List.length spec.properties - 1 then ";" else ""
18191819+ in
18201820+ if is_optional then
18211821+ emitln out
18221822+ (Printf.sprintf
18231823+ " (\"%s\", match r.%s with Some v -> %s v | None -> \
18241824+ `Null)%s"
18251825+ prop_name ocaml_name conv_expr comma )
18261826+ else
18271827+ emitln out
18281828+ (Printf.sprintf " (\"%s\", %s r.%s)%s" prop_name conv_expr
18291829+ ocaml_name comma ) )
18301830+ spec.properties ;
18311831+ emitln out " ]" ;
18321832+ emit_newline out
18331833+ end
18341834+ end
18351835+ in
18361836+ (* helper to generate inline union converters *)
18371837+ let gen_inline_union_converters ?(of_keyword = "let") ?(to_keyword = "let")
18381838+ nsid name refs spec =
18391839+ let is_closed = Option.value spec.closed ~default:false in
18401840+ (* of_yojson *)
18411841+ if of_keyword <> "SKIP" then begin
18421842+ emitln out (Printf.sprintf "%s %s_of_yojson json =" of_keyword name) ;
18431843+ emitln out " let open Yojson.Safe.Util in" ;
18441844+ emitln out " try" ;
18451845+ emitln out " match json |> member \"$type\" |> to_string with" ;
18461846+ List.iter
18471847+ (fun ref_str ->
18481848+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
18491849+ let full_type_uri = gen_merged_type_uri nsid ref_str in
18501850+ let payload_type = gen_merged_ref_type nsid ref_str in
18511851+ emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ;
18521852+ emitln out
18531853+ (Printf.sprintf " (match %s_of_yojson json with" payload_type) ;
18541854+ emitln out
18551855+ (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ;
18561856+ emitln out " | Error e -> Error e)" )
18571857+ refs ;
18581858+ if is_closed then
18591859+ emitln out " | t -> Error (\"unknown union type: \" ^ t)"
18601860+ else emitln out " | _ -> Ok (Unknown json)" ;
18611861+ emitln out " with _ -> Error \"failed to parse union\"" ;
18621862+ emit_newline out
18631863+ end ;
18641864+ (* to_yojson *)
18651865+ if to_keyword <> "SKIP" then begin
18661866+ emitln out (Printf.sprintf "%s %s_to_yojson = function" to_keyword name) ;
18671867+ List.iter
18681868+ (fun ref_str ->
18691869+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
18701870+ let full_type_uri = gen_merged_type_uri nsid ref_str in
18711871+ let payload_type = gen_merged_ref_type nsid ref_str in
18721872+ emitln out (Printf.sprintf " | %s v ->" variant_name) ;
18731873+ emitln out
18741874+ (Printf.sprintf " (match %s_to_yojson v with" payload_type) ;
18751875+ emitln out
18761876+ (Printf.sprintf
18771877+ " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") \
18781878+ :: fields)"
18791879+ full_type_uri ) ;
18801880+ emitln out " | other -> other)" )
18811881+ refs ;
18821882+ if not is_closed then emitln out " | Unknown j -> j" ;
18831883+ emit_newline out
18841884+ end
18851885+ in
18861886+ (* generate each SCC *)
18871887+ List.iter
18881888+ (fun scc ->
18891889+ (* separate inline unions from regular defs *)
18901890+ let inline_unions_in_scc =
18911891+ List.filter_map (function `InlineUnion x -> Some x | _ -> None) scc
18921892+ in
18931893+ let regular_defs_in_scc =
18941894+ List.filter_map (function `RegularDef x -> Some x | _ -> None) scc
18951895+ in
18961896+ if inline_unions_in_scc = [] then begin
18971897+ (* no inline unions - use standard generation with [@@deriving yojson] *)
18981898+ if regular_defs_in_scc <> [] then gen_merged_scc regular_defs_in_scc
18991899+ end
19001900+ else begin
19011901+ (* has inline unions - generate all types first, then all converters *)
19021902+ (* register inline union names *)
19031903+ List.iter
19041904+ (fun (nsid, name, refs, _spec) ->
19051905+ let unique_name = get_unique_inline_union_name nsid name in
19061906+ register_union_name out refs unique_name ;
19071907+ mark_union_generated out unique_name )
19081908+ inline_unions_in_scc ;
19091909+ (* collect all items to generate *)
19101910+ let all_items =
19111911+ List.map (fun x -> `Inline x) inline_unions_in_scc
19121912+ @ List.map (fun x -> `Regular x) regular_defs_in_scc
19131913+ in
19141914+ let n = List.length all_items in
19151915+ if n = 1 then begin
19161916+ (* single item - generate normally *)
19171917+ match List.hd all_items with
19181918+ | `Inline (nsid, name, refs, spec) ->
19191919+ let unique_name = get_unique_inline_union_name nsid name in
19201920+ gen_inline_union_type_only nsid unique_name refs spec ;
19211921+ emit_newline out ;
19221922+ gen_inline_union_converters nsid unique_name refs spec
19231923+ | `Regular (nsid, def) -> (
19241924+ match def.type_def with
19251925+ | Object spec ->
19261926+ register_merged_inline_unions nsid spec.properties ;
19271927+ gen_object_type_only nsid def.name spec ;
19281928+ emit_newline out ;
19291929+ gen_object_converters nsid def.name spec
19301930+ | Record rspec ->
19311931+ register_merged_inline_unions nsid rspec.record.properties ;
19321932+ gen_object_type_only nsid def.name rspec.record ;
19331933+ emit_newline out ;
19341934+ gen_object_converters nsid def.name rspec.record
19351935+ | _ ->
19361936+ gen_merged_scc [(nsid, def)] )
19371937+ end
19381938+ else begin
19391939+ (* multiple items - generate as mutually recursive types *)
19401940+ (* first pass: register inline unions from objects *)
19411941+ List.iter
19421942+ (function
19431943+ | `Regular (nsid, def) -> (
19441944+ match def.type_def with
19451945+ | Object spec ->
19461946+ register_merged_inline_unions nsid spec.properties
19471947+ | Record rspec ->
19481948+ register_merged_inline_unions nsid rspec.record.properties
19491949+ | _ ->
19501950+ () )
19511951+ | `Inline _ ->
19521952+ () )
19531953+ all_items ;
19541954+ (* second pass: generate all type definitions *)
19551955+ List.iteri
19561956+ (fun i item ->
19571957+ let keyword = if i = 0 then "type" else "and" in
19581958+ match item with
19591959+ | `Inline (nsid, name, refs, spec) ->
19601960+ let unique_name = get_unique_inline_union_name nsid name in
19611961+ gen_inline_union_type_only ~keyword nsid unique_name refs spec
19621962+ | `Regular (nsid, def) -> (
19631963+ match def.type_def with
19641964+ | Object spec ->
19651965+ gen_object_type_only ~keyword nsid def.name spec
19661966+ | Record rspec ->
19671967+ gen_object_type_only ~keyword nsid def.name rspec.record
19681968+ | _ ->
19691969+ () ) )
19701970+ all_items ;
19711971+ emit_newline out ;
19721972+ (* third pass: generate all _of_yojson converters as mutually recursive *)
19731973+ List.iteri
19741974+ (fun i item ->
19751975+ let of_keyword = if i = 0 then "let rec" else "and" in
19761976+ match item with
19771977+ | `Inline (nsid, name, refs, spec) ->
19781978+ let unique_name = get_unique_inline_union_name nsid name in
19791979+ gen_inline_union_converters ~of_keyword ~to_keyword:"SKIP"
19801980+ nsid unique_name refs spec
19811981+ | `Regular (nsid, def) -> (
19821982+ match def.type_def with
19831983+ | Object spec ->
19841984+ gen_object_converters ~of_keyword ~to_keyword:"SKIP" nsid
19851985+ def.name spec
19861986+ | Record rspec ->
19871987+ gen_object_converters ~of_keyword ~to_keyword:"SKIP" nsid
19881988+ def.name rspec.record
19891989+ | _ ->
19901990+ () ) )
19911991+ all_items ;
19921992+ (* fourth pass: generate all _to_yojson converters as mutually recursive *)
19931993+ List.iteri
19941994+ (fun i item ->
19951995+ let to_keyword = if i = 0 then "and" else "and" in
19961996+ match item with
19971997+ | `Inline (nsid, name, refs, spec) ->
19981998+ let unique_name = get_unique_inline_union_name nsid name in
19991999+ gen_inline_union_converters ~of_keyword:"SKIP" ~to_keyword
20002000+ nsid unique_name refs spec
20012001+ | `Regular (nsid, def) -> (
20022002+ match def.type_def with
20032003+ | Object spec ->
20042004+ gen_object_converters ~of_keyword:"SKIP" ~to_keyword nsid
20052005+ def.name spec
20062006+ | Record rspec ->
20072007+ gen_object_converters ~of_keyword:"SKIP" ~to_keyword nsid
20082008+ def.name rspec.record
20092009+ | _ ->
20102010+ () ) )
20112011+ all_items
20122012+ end
20132013+ end )
20142014+ ordered_sccs ;
20152015+ Emitter.contents out
20162016+20172017+(* generate a re-export stub that selectively exports types from a merged module *)
20182018+let gen_reexport_stub ~merged_module_name ~all_merged_docs (doc : lexicon_doc) :
20192019+ string =
20202020+ let buf = Buffer.create 1024 in
20212021+ let emit s = Buffer.add_string buf s in
20222022+ let emitln s = Buffer.add_string buf s ; Buffer.add_char buf '\n' in
20232023+ (* detect collisions across all merged docs *)
20242024+ let all_defs =
20252025+ List.concat_map
20262026+ (fun d -> List.map (fun def -> (d.id, def)) d.defs)
20272027+ all_merged_docs
20282028+ in
20292029+ let name_counts = Hashtbl.create 64 in
20302030+ List.iter
20312031+ (fun (nsid, def) ->
20322032+ let existing = Hashtbl.find_opt name_counts def.name in
20332033+ match existing with
20342034+ | None ->
20352035+ Hashtbl.add name_counts def.name [nsid]
20362036+ | Some nsids when not (List.mem nsid nsids) ->
20372037+ Hashtbl.replace name_counts def.name (nsid :: nsids)
20382038+ | _ ->
20392039+ () )
20402040+ all_defs ;
20412041+ let colliding_names =
20422042+ Hashtbl.fold
20432043+ (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc)
20442044+ name_counts []
20452045+ in
20462046+ (* the "host" nsid is the first one - types from here keep short names *)
20472047+ let host_nsid = (List.hd all_merged_docs).id in
20482048+ let get_unique_type_name nsid def_name =
20492049+ if List.mem def_name colliding_names && nsid <> host_nsid then
20502050+ let prefix = Naming.flat_name_of_nsid nsid ^ "_" in
20512051+ Naming.type_name (prefix ^ def_name)
20522052+ else Naming.type_name def_name
20532053+ in
20542054+ emitln (Printf.sprintf "(* re-exported from %s *)" merged_module_name) ;
20552055+ emitln "" ;
20562056+ List.iter
20572057+ (fun def ->
20582058+ let local_type_name = Naming.type_name def.name in
20592059+ let merged_type_name = get_unique_type_name doc.id def.name in
20602060+ match def.type_def with
20612061+ | Object _ | Record _ | Union _ ->
20622062+ (* type alias and converter aliases *)
20632063+ emitln
20642064+ (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name
20652065+ merged_type_name ) ;
20662066+ emitln
20672067+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
20682068+ merged_module_name merged_type_name ) ;
20692069+ emitln
20702070+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
20712071+ merged_module_name merged_type_name ) ;
20722072+ emit "\n"
20732073+ | String spec when spec.known_values <> None ->
20742074+ emitln
20752075+ (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name
20762076+ merged_type_name ) ;
20772077+ emitln
20782078+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
20792079+ merged_module_name merged_type_name ) ;
20802080+ emitln
20812081+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
20822082+ merged_module_name merged_type_name ) ;
20832083+ emit "\n"
20842084+ | Array _ ->
20852085+ (* re-export array type alias and converters *)
20862086+ emitln
20872087+ (Printf.sprintf "type %s = %s.%s" local_type_name merged_module_name
20882088+ merged_type_name ) ;
20892089+ emitln
20902090+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
20912091+ merged_module_name merged_type_name ) ;
20922092+ emitln
20932093+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
20942094+ merged_module_name merged_type_name ) ;
20952095+ emit "\n"
20962096+ | Token _ ->
20972097+ emitln
20982098+ (Printf.sprintf "let %s = %s.%s" local_type_name merged_module_name
20992099+ merged_type_name ) ;
21002100+ emit "\n"
21012101+ | Query _ | Procedure _ ->
21022102+ let mod_name = Naming.def_module_name def.name in
21032103+ emitln
21042104+ (Printf.sprintf "module %s = %s.%s" mod_name merged_module_name
21052105+ mod_name ) ;
21062106+ emit "\n"
21072107+ | _ ->
21082108+ () )
21092109+ doc.defs ;
21102110+ Buffer.contents buf
21112111+21122112+(* generate a shared module for mutually recursive lexicons *)
21132113+(* uses Naming.shared_type_name for context-based naming instead of full nsid prefix *)
21142114+let gen_shared_module (docs : lexicon_doc list) : string =
21152115+ let out = make_output () in
21162116+ (* collect all nsids in this shared group *)
21172117+ let shared_nsids = List.map (fun d -> d.id) docs in
21182118+ (* header *)
21192119+ emitln out
21202120+ (Printf.sprintf "(* shared module for lexicons: %s *)"
21212121+ (String.concat ", " shared_nsids) ) ;
21222122+ emit_newline out ;
21232123+ (* collect all defs from all docs *)
21242124+ let all_defs =
21252125+ List.concat_map
21262126+ (fun doc -> List.map (fun def -> (doc.id, def)) doc.defs)
21272127+ docs
21282128+ in
21292129+ (* detect name collisions - names that appear in multiple nsids *)
21302130+ let name_counts = Hashtbl.create 64 in
21312131+ List.iter
21322132+ (fun (nsid, def) ->
21332133+ let existing = Hashtbl.find_opt name_counts def.name in
21342134+ match existing with
21352135+ | None ->
21362136+ Hashtbl.add name_counts def.name [nsid]
21372137+ | Some nsids when not (List.mem nsid nsids) ->
21382138+ Hashtbl.replace name_counts def.name (nsid :: nsids)
21392139+ | _ ->
21402140+ () )
21412141+ all_defs ;
21422142+ let colliding_names =
21432143+ Hashtbl.fold
21442144+ (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc)
21452145+ name_counts []
21462146+ in
21472147+ (* also detect inline union name collisions *)
21482148+ let rec collect_inline_union_contexts nsid context acc type_def =
21492149+ match type_def with
21502150+ | Union spec ->
21512151+ (nsid, context, spec.refs) :: acc
21522152+ | Array {items; _} ->
21532153+ collect_inline_union_contexts nsid (context ^ "_item") acc items
21542154+ | Object {properties; _} ->
21552155+ List.fold_left
21562156+ (fun a (prop_name, (prop : property)) ->
21572157+ collect_inline_union_contexts nsid prop_name a prop.type_def )
21582158+ acc properties
21592159+ | _ ->
21602160+ acc
21612161+ in
21622162+ let all_inline_union_contexts =
21632163+ List.concat_map
21642164+ (fun (nsid, def) ->
21652165+ match def.type_def with
21662166+ | Object spec ->
21672167+ List.fold_left
21682168+ (fun acc (prop_name, (prop : property)) ->
21692169+ collect_inline_union_contexts nsid prop_name acc prop.type_def )
21702170+ [] spec.properties
21712171+ | Record rspec ->
21722172+ List.fold_left
21732173+ (fun acc (prop_name, (prop : property)) ->
21742174+ collect_inline_union_contexts nsid prop_name acc prop.type_def )
21752175+ [] rspec.record.properties
21762176+ | _ ->
21772177+ [] )
21782178+ all_defs
21792179+ in
21802180+ (* group inline unions by context name *)
21812181+ let inline_union_by_context = Hashtbl.create 64 in
21822182+ List.iter
21832183+ (fun (nsid, context, refs) ->
21842184+ let key = Naming.type_name context in
21852185+ let sorted_refs = List.sort String.compare refs in
21862186+ let existing = Hashtbl.find_opt inline_union_by_context key in
21872187+ match existing with
21882188+ | None ->
21892189+ Hashtbl.add inline_union_by_context key [(nsid, sorted_refs)]
21902190+ | Some entries ->
21912191+ (* collision if different nsid OR different refs *)
21922192+ if
21932193+ not
21942194+ (List.exists (fun (n, r) -> n = nsid && r = sorted_refs) entries)
21952195+ then
21962196+ Hashtbl.replace inline_union_by_context key
21972197+ ((nsid, sorted_refs) :: entries) )
21982198+ all_inline_union_contexts ;
21992199+ (* add inline union collisions to colliding_names *)
22002200+ let colliding_names =
22012201+ Hashtbl.fold
22022202+ (fun name entries acc ->
22032203+ (* collision if more than one entry (different nsid or different refs) *)
22042204+ if List.length entries > 1 then name :: acc else acc )
22052205+ inline_union_by_context colliding_names
22062206+ in
22072207+ (* function to get unique type name using shared_type_name for collisions *)
22082208+ let get_shared_type_name nsid def_name =
22092209+ if List.mem def_name colliding_names then
22102210+ (* use context-based name: e.g., feed_viewer_state *)
22112211+ Naming.shared_type_name nsid def_name
22122212+ else
22132213+ (* no collision, use simple name *)
22142214+ Naming.type_name def_name
22152215+ in
22162216+ (* custom ref type generator that treats shared nsids as local *)
22172217+ let rec gen_shared_type_ref current_nsid type_def =
22182218+ match type_def with
22192219+ | String _ ->
22202220+ "string"
22212221+ | Integer {maximum; _} -> (
22222222+ match maximum with Some m when m > 1073741823 -> "int64" | _ -> "int" )
22232223+ | Boolean _ ->
22242224+ "bool"
22252225+ | Bytes _ ->
22262226+ "bytes"
22272227+ | Blob _ ->
22282228+ "Hermes.blob"
22292229+ | CidLink _ ->
22302230+ "Cid.t"
22312231+ | Array {items; _} ->
22322232+ let item_type = gen_shared_type_ref current_nsid items in
22332233+ item_type ^ " list"
22342234+ | Object _ ->
22352235+ "object_todo"
22362236+ | Ref {ref_; _} ->
22372237+ gen_shared_ref_type current_nsid ref_
22382238+ | Union {refs; _} -> (
22392239+ match lookup_union_name out refs with
22402240+ | Some name ->
22412241+ name
22422242+ | None ->
22432243+ gen_union_type_name refs )
22442244+ | Token _ ->
22452245+ "string"
22462246+ | Unknown _ ->
22472247+ "Yojson.Safe.t"
22482248+ | Query _ | Procedure _ | Subscription _ | Record _ ->
22492249+ "unit (* primary type *)"
22502250+ and gen_shared_ref_type current_nsid ref_str =
22512251+ if String.length ref_str > 0 && ref_str.[0] = '#' then begin
22522252+ (* local ref within same nsid *)
22532253+ let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
22542254+ get_shared_type_name current_nsid def_name
22552255+ end
22562256+ else begin
22572257+ match String.split_on_char '#' ref_str with
22582258+ | [ext_nsid; def_name] ->
22592259+ if List.mem ext_nsid shared_nsids then
22602260+ (* ref to another nsid in the shared group *)
22612261+ get_shared_type_name ext_nsid def_name
22622262+ else begin
22632263+ (* truly external ref *)
22642264+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
22652265+ add_import out flat_module ;
22662266+ flat_module ^ "." ^ Naming.type_name def_name
22672267+ end
22682268+ | [ext_nsid] ->
22692269+ if List.mem ext_nsid shared_nsids then
22702270+ get_shared_type_name ext_nsid "main"
22712271+ else begin
22722272+ let flat_module = Naming.flat_module_name_of_nsid ext_nsid in
22732273+ add_import out flat_module ; flat_module ^ ".main"
22742274+ end
22752275+ | _ ->
22762276+ "invalid_ref"
22772277+ end
22782278+ in
22792279+ (* generate type uri for shared context *)
22802280+ let gen_shared_type_uri current_nsid ref_str =
22812281+ if String.length ref_str > 0 && ref_str.[0] = '#' then
22822282+ current_nsid ^ ref_str
22832283+ else ref_str
22842284+ in
22852285+ (* generate converter expression for reading a type from json *)
22862286+ let gen_shared_of_yojson_expr current_nsid type_def =
22872287+ match type_def with
22882288+ | String _ | Token _ ->
22892289+ ("to_string", false)
22902290+ | Integer {maximum; _} -> (
22912291+ match maximum with
22922292+ | Some m when m > 1073741823 ->
22932293+ ("(fun j -> Int64.of_int (to_int j))", false)
22942294+ | _ ->
22952295+ ("to_int", false) )
22962296+ | Boolean _ ->
22972297+ ("to_bool", false)
22982298+ | Bytes _ ->
22992299+ ("(fun j -> Bytes.of_string (to_string j))", false)
23002300+ | Blob _ ->
23012301+ ("Hermes.blob_of_yojson", true)
23022302+ | CidLink _ ->
23032303+ ("Cid.of_yojson", true)
23042304+ | Array {items; _} ->
23052305+ let item_type = gen_shared_type_ref current_nsid items in
23062306+ ( Printf.sprintf
23072307+ "(fun j -> to_list j |> List.filter_map (fun x -> match \
23082308+ %s_of_yojson x with Ok v -> Some v | _ -> None))"
23092309+ item_type
23102310+ , false )
23112311+ | Ref {ref_; _} ->
23122312+ let type_name = gen_shared_ref_type current_nsid ref_ in
23132313+ (type_name ^ "_of_yojson", true)
23142314+ | Union {refs; _} ->
23152315+ let type_name =
23162316+ match lookup_union_name out refs with
23172317+ | Some n ->
23182318+ n
23192319+ | None ->
23202320+ gen_union_type_name refs
23212321+ in
23222322+ (type_name ^ "_of_yojson", true)
23232323+ | Unknown _ ->
23242324+ ("(fun j -> j)", false)
23252325+ | _ ->
23262326+ ("(fun _ -> failwith \"unsupported type\")", false)
23272327+ in
23282328+ (* generate converter expression for writing a type to json *)
23292329+ let gen_shared_to_yojson_expr current_nsid type_def =
23302330+ match type_def with
23312331+ | String _ | Token _ ->
23322332+ "(fun s -> `String s)"
23332333+ | Integer {maximum; _} -> (
23342334+ match maximum with
23352335+ | Some m when m > 1073741823 ->
23362336+ "(fun i -> `Int (Int64.to_int i))"
23372337+ | _ ->
23382338+ "(fun i -> `Int i)" )
23392339+ | Boolean _ ->
23402340+ "(fun b -> `Bool b)"
23412341+ | Bytes _ ->
23422342+ "(fun b -> `String (Bytes.to_string b))"
23432343+ | Blob _ ->
23442344+ "Hermes.blob_to_yojson"
23452345+ | CidLink _ ->
23462346+ "Cid.to_yojson"
23472347+ | Array {items; _} ->
23482348+ let item_type = gen_shared_type_ref current_nsid items in
23492349+ Printf.sprintf "(fun l -> `List (List.map %s_to_yojson l))" item_type
23502350+ | Ref {ref_; _} ->
23512351+ let type_name = gen_shared_ref_type current_nsid ref_ in
23522352+ type_name ^ "_to_yojson"
23532353+ | Union {refs; _} ->
23542354+ let type_name =
23552355+ match lookup_union_name out refs with
23562356+ | Some n ->
23572357+ n
23582358+ | None ->
23592359+ gen_union_type_name refs
23602360+ in
23612361+ type_name ^ "_to_yojson"
23622362+ | Unknown _ ->
23632363+ "(fun j -> j)"
23642364+ | _ ->
23652365+ "(fun _ -> `Null)"
23662366+ in
23672367+ (* collect inline unions with context-based naming *)
23682368+ let get_shared_inline_union_name nsid context =
23692369+ let base_name = Naming.type_name context in
23702370+ (* check if there's a collision with this inline union name *)
23712371+ if List.mem base_name colliding_names then
23722372+ Naming.shared_type_name nsid context
23732373+ else base_name
23742374+ in
23752375+ let register_shared_inline_unions nsid properties =
23762376+ let rec collect_inline_unions_with_context context acc type_def =
23772377+ match type_def with
23782378+ | Union spec ->
23792379+ (context, spec.refs, spec) :: acc
23802380+ | Array {items; _} ->
23812381+ collect_inline_unions_with_context (context ^ "_item") acc items
23822382+ | _ ->
23832383+ acc
23842384+ in
23852385+ let inline_unions =
23862386+ List.fold_left
23872387+ (fun acc (prop_name, (prop : property)) ->
23882388+ collect_inline_unions_with_context prop_name acc prop.type_def )
23892389+ [] properties
23902390+ in
23912391+ List.iter
23922392+ (fun (context, refs, _spec) ->
23932393+ let unique_name = get_shared_inline_union_name nsid context in
23942394+ register_union_name out refs unique_name )
23952395+ inline_unions
23962396+ in
23972397+ (* generate object type for shared context *)
23982398+ let gen_shared_object_type ?(first = true) ?(last = true) current_nsid name
23992399+ (spec : object_spec) =
24002400+ let required = Option.value spec.required ~default:[] in
24012401+ let nullable = Option.value spec.nullable ~default:[] in
24022402+ let keyword = if first then "type" else "and" in
24032403+ let type_name = get_shared_type_name current_nsid name in
24042404+ if spec.properties = [] then begin
24052405+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name) ;
24062406+ if last then begin
24072407+ emitln out (Printf.sprintf "let %s_of_yojson _ = Ok ()" type_name) ;
24082408+ emitln out (Printf.sprintf "let %s_to_yojson () = `Assoc []" type_name) ;
24092409+ emit_newline out
24102410+ end
24112411+ end
24122412+ else begin
24132413+ if first then register_shared_inline_unions current_nsid spec.properties ;
24142414+ emitln out (Printf.sprintf "%s %s =" keyword type_name) ;
24152415+ emitln out " {" ;
24162416+ List.iter
24172417+ (fun (prop_name, (prop : property)) ->
24182418+ let ocaml_name = Naming.field_name prop_name in
24192419+ let base_type = gen_shared_type_ref current_nsid prop.type_def in
24202420+ let is_required = List.mem prop_name required in
24212421+ let is_nullable = List.mem prop_name nullable in
24222422+ let type_str =
24232423+ if is_required && not is_nullable then base_type
24242424+ else base_type ^ " option"
24252425+ in
24262426+ let key_attr = Naming.key_annotation prop_name ocaml_name in
24272427+ let default_attr =
24282428+ if is_required && not is_nullable then "" else " [@default None]"
24292429+ in
24302430+ emitln out
24312431+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr
24322432+ default_attr ) )
24332433+ spec.properties ;
24342434+ emitln out " }" ;
24352435+ if last then begin
24362436+ emitln out "[@@deriving yojson {strict= false}]" ;
24372437+ emit_newline out
24382438+ end
24392439+ end
24402440+ in
24412441+ (* generate union type for shared context *)
24422442+ let gen_shared_union_type current_nsid name (spec : union_spec) =
24432443+ let type_name = get_shared_type_name current_nsid name in
24442444+ let is_closed = Option.value spec.closed ~default:false in
24452445+ emitln out (Printf.sprintf "type %s =" type_name) ;
24462446+ List.iter
24472447+ (fun ref_str ->
24482448+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
24492449+ let payload_type = gen_shared_ref_type current_nsid ref_str in
24502450+ emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) )
24512451+ spec.refs ;
24522452+ if not is_closed then emitln out " | Unknown of Yojson.Safe.t" ;
24532453+ emit_newline out ;
24542454+ emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ;
24552455+ emitln out " let open Yojson.Safe.Util in" ;
24562456+ emitln out " try" ;
24572457+ emitln out " match json |> member \"$type\" |> to_string with" ;
24582458+ List.iter
24592459+ (fun ref_str ->
24602460+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
24612461+ let full_type_uri = gen_shared_type_uri current_nsid ref_str in
24622462+ let payload_type = gen_shared_ref_type current_nsid ref_str in
24632463+ emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ;
24642464+ emitln out
24652465+ (Printf.sprintf " (match %s_of_yojson json with" payload_type) ;
24662466+ emitln out (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ;
24672467+ emitln out " | Error e -> Error e)" )
24682468+ spec.refs ;
24692469+ if is_closed then
24702470+ emitln out " | t -> Error (\"unknown union type: \" ^ t)"
24712471+ else emitln out " | _ -> Ok (Unknown json)" ;
24722472+ emitln out " with _ -> Error \"failed to parse union\"" ;
24732473+ emit_newline out ;
24742474+ emitln out (Printf.sprintf "let %s_to_yojson = function" type_name) ;
24752475+ List.iter
24762476+ (fun ref_str ->
24772477+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
24782478+ let full_type_uri = gen_shared_type_uri current_nsid ref_str in
24792479+ let payload_type = gen_shared_ref_type current_nsid ref_str in
24802480+ emitln out (Printf.sprintf " | %s v ->" variant_name) ;
24812481+ emitln out
24822482+ (Printf.sprintf " (match %s_to_yojson v with" payload_type) ;
24832483+ emitln out
24842484+ (Printf.sprintf
24852485+ " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") :: \
24862486+ fields)"
24872487+ full_type_uri ) ;
24882488+ emitln out " | other -> other)" )
24892489+ spec.refs ;
24902490+ if not is_closed then emitln out " | Unknown j -> j" ;
24912491+ emit_newline out
24922492+ in
24932493+ (* collect refs for shared SCC detection, using compound keys (nsid#name) *)
24942494+ let collect_shared_local_refs current_nsid acc type_def =
24952495+ let rec aux acc = function
24962496+ | Array {items; _} ->
24972497+ aux acc items
24982498+ | Ref {ref_; _} ->
24992499+ if String.length ref_ > 0 && ref_.[0] = '#' then
25002500+ (* local ref: #foo -> current_nsid#foo *)
25012501+ let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
25022502+ (current_nsid ^ "#" ^ def_name) :: acc
25032503+ else begin
25042504+ match String.split_on_char '#' ref_ with
25052505+ | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids ->
25062506+ (* cross-nsid ref within shared group *)
25072507+ (ext_nsid ^ "#" ^ def_name) :: acc
25082508+ | _ ->
25092509+ acc
25102510+ end
25112511+ | Union {refs; _} ->
25122512+ List.fold_left
25132513+ (fun a r ->
25142514+ if String.length r > 0 && r.[0] = '#' then
25152515+ let def_name = String.sub r 1 (String.length r - 1) in
25162516+ (current_nsid ^ "#" ^ def_name) :: a
25172517+ else
25182518+ match String.split_on_char '#' r with
25192519+ | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids ->
25202520+ (ext_nsid ^ "#" ^ def_name) :: a
25212521+ | _ ->
25222522+ a )
25232523+ acc refs
25242524+ | Object {properties; _} ->
25252525+ List.fold_left
25262526+ (fun a (_, (prop : property)) -> aux a prop.type_def)
25272527+ acc properties
25282528+ | Record {record; _} ->
25292529+ List.fold_left
25302530+ (fun a (_, (prop : property)) -> aux a prop.type_def)
25312531+ acc record.properties
25322532+ | Query {parameters; output; _} -> (
25332533+ let acc =
25342534+ match parameters with
25352535+ | Some params ->
25362536+ List.fold_left
25372537+ (fun a (_, (prop : property)) -> aux a prop.type_def)
25382538+ acc params.properties
25392539+ | None ->
25402540+ acc
25412541+ in
25422542+ match output with
25432543+ | Some body ->
25442544+ Option.fold ~none:acc ~some:(aux acc) body.schema
25452545+ | None ->
25462546+ acc )
25472547+ | Procedure {parameters; input; output; _} -> (
25482548+ let acc =
25492549+ match parameters with
25502550+ | Some params ->
25512551+ List.fold_left
25522552+ (fun a (_, (prop : property)) -> aux a prop.type_def)
25532553+ acc params.properties
25542554+ | None ->
25552555+ acc
25562556+ in
25572557+ let acc =
25582558+ match input with
25592559+ | Some body ->
25602560+ Option.fold ~none:acc ~some:(aux acc) body.schema
25612561+ | None ->
25622562+ acc
25632563+ in
25642564+ match output with
25652565+ | Some body ->
25662566+ Option.fold ~none:acc ~some:(aux acc) body.schema
25672567+ | None ->
25682568+ acc )
25692569+ | _ ->
25702570+ acc
25712571+ in
25722572+ aux acc type_def
25732573+ in
25742574+ (* generate single shared def *)
25752575+ let gen_shared_single_def (nsid, def) =
25762576+ match def.type_def with
25772577+ | Object spec ->
25782578+ gen_shared_object_type nsid def.name spec
25792579+ | Union spec ->
25802580+ gen_shared_union_type nsid def.name spec
25812581+ | Token spec ->
25822582+ gen_token nsid out def.name spec
25832583+ | Query spec ->
25842584+ gen_query nsid out def.name spec
25852585+ | Procedure spec ->
25862586+ gen_procedure nsid out def.name spec
25872587+ | Record spec ->
25882588+ gen_shared_object_type nsid def.name spec.record
25892589+ | String spec when spec.known_values <> None ->
25902590+ gen_string_type out def.name spec
25912591+ | Array {items; _} ->
25922592+ (* generate inline union for array items if needed *)
25932593+ ( match items with
25942594+ | Union spec ->
25952595+ let item_type_name = Naming.type_name (def.name ^ "_item") in
25962596+ register_union_name out spec.refs item_type_name ;
25972597+ gen_shared_union_type nsid (def.name ^ "_item") spec
25982598+ | _ ->
25992599+ () ) ;
26002600+ (* generate type alias for array *)
26012601+ let type_name = get_shared_type_name nsid def.name in
26022602+ let item_type = gen_shared_type_ref nsid items in
26032603+ emitln out (Printf.sprintf "type %s = %s list" type_name item_type) ;
26042604+ emitln out (Printf.sprintf "let %s_of_yojson json =" type_name) ;
26052605+ emitln out " let open Yojson.Safe.Util in" ;
26062606+ emitln out
26072607+ (Printf.sprintf
26082608+ " Ok (to_list json |> List.filter_map (fun x -> match \
26092609+ %s_of_yojson x with Ok v -> Some v | _ -> None))"
26102610+ item_type ) ;
26112611+ emitln out
26122612+ (Printf.sprintf "let %s_to_yojson l = `List (List.map %s_to_yojson l)"
26132613+ type_name item_type ) ;
26142614+ emit_newline out
26152615+ | _ ->
26162616+ ()
26172617+ in
26182618+ (* helper to generate object type definition only (no converters) *)
26192619+ let gen_shared_object_type_only ?(keyword = "type") nsid name
26202620+ (spec : object_spec) =
26212621+ let required = Option.value spec.required ~default:[] in
26222622+ let nullable = Option.value spec.nullable ~default:[] in
26232623+ let type_name = get_shared_type_name nsid name in
26242624+ if spec.properties = [] then
26252625+ emitln out (Printf.sprintf "%s %s = unit" keyword type_name)
26262626+ else begin
26272627+ emitln out (Printf.sprintf "%s %s = {" keyword type_name) ;
26282628+ List.iter
26292629+ (fun (prop_name, (prop : property)) ->
26302630+ let ocaml_name = Naming.field_name prop_name in
26312631+ let base_type = gen_shared_type_ref nsid prop.type_def in
26322632+ let is_required = List.mem prop_name required in
26332633+ let is_nullable = List.mem prop_name nullable in
26342634+ let type_str =
26352635+ if is_required && not is_nullable then base_type
26362636+ else base_type ^ " option"
26372637+ in
26382638+ let key_attr = Naming.key_annotation prop_name ocaml_name in
26392639+ let default_attr =
26402640+ if is_required && not is_nullable then "" else " [@default None]"
26412641+ in
26422642+ emitln out
26432643+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr
26442644+ default_attr ) )
26452645+ spec.properties ;
26462646+ emitln out "}"
26472647+ end
26482648+ in
26492649+ (* helper to generate inline union type definition only *)
26502650+ let gen_shared_inline_union_type_only ?(keyword = "type") nsid name refs spec
26512651+ =
26522652+ let is_closed = Option.value spec.closed ~default:false in
26532653+ emitln out (Printf.sprintf "%s %s =" keyword name) ;
26542654+ List.iter
26552655+ (fun ref_str ->
26562656+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
26572657+ let payload_type = gen_shared_ref_type nsid ref_str in
26582658+ emitln out (Printf.sprintf " | %s of %s" variant_name payload_type) )
26592659+ refs ;
26602660+ if not is_closed then emitln out " | Unknown of Yojson.Safe.t"
26612661+ in
26622662+ (* helper to generate object converters *)
26632663+ let gen_shared_object_converters ?(of_keyword = "let") ?(to_keyword = "let")
26642664+ nsid name (spec : object_spec) =
26652665+ let required = Option.value spec.required ~default:[] in
26662666+ let nullable = Option.value spec.nullable ~default:[] in
26672667+ let type_name = get_shared_type_name nsid name in
26682668+ if spec.properties = [] then begin
26692669+ if of_keyword <> "SKIP" then
26702670+ emitln out
26712671+ (Printf.sprintf "%s %s_of_yojson _ = Ok ()" of_keyword type_name) ;
26722672+ if to_keyword <> "SKIP" then
26732673+ emitln out
26742674+ (Printf.sprintf "%s %s_to_yojson () = `Assoc []" to_keyword type_name)
26752675+ end
26762676+ else begin
26772677+ (* of_yojson *)
26782678+ if of_keyword <> "SKIP" then begin
26792679+ emitln out
26802680+ (Printf.sprintf "%s %s_of_yojson json =" of_keyword type_name) ;
26812681+ emitln out " let open Yojson.Safe.Util in" ;
26822682+ emitln out " try" ;
26832683+ List.iter
26842684+ (fun (prop_name, (prop : property)) ->
26852685+ let ocaml_name = Naming.field_name prop_name in
26862686+ let conv_expr, needs_unwrap =
26872687+ gen_shared_of_yojson_expr nsid prop.type_def
26882688+ in
26892689+ let is_required = List.mem prop_name required in
26902690+ let is_nullable = List.mem prop_name nullable in
26912691+ let is_optional = (not is_required) || is_nullable in
26922692+ if is_optional then begin
26932693+ if needs_unwrap then
26942694+ emitln out
26952695+ (Printf.sprintf
26962696+ " let %s = json |> member \"%s\" |> to_option (fun x \
26972697+ -> match %s x with Ok v -> Some v | _ -> None) |> \
26982698+ Option.join in"
26992699+ ocaml_name prop_name conv_expr )
27002700+ else
27012701+ emitln out
27022702+ (Printf.sprintf
27032703+ " let %s = json |> member \"%s\" |> to_option %s in"
27042704+ ocaml_name prop_name conv_expr )
27052705+ end
27062706+ else begin
27072707+ if needs_unwrap then
27082708+ emitln out
27092709+ (Printf.sprintf
27102710+ " let %s = json |> member \"%s\" |> %s |> \
27112711+ Result.get_ok in"
27122712+ ocaml_name prop_name conv_expr )
27132713+ else
27142714+ emitln out
27152715+ (Printf.sprintf " let %s = json |> member \"%s\" |> %s in"
27162716+ ocaml_name prop_name conv_expr )
27172717+ end )
27182718+ spec.properties ;
27192719+ emit out " Ok { " ;
27202720+ emit out
27212721+ (String.concat "; "
27222722+ (List.map (fun (pn, _) -> Naming.field_name pn) spec.properties) ) ;
27232723+ emitln out " }" ;
27242724+ emitln out " with e -> Error (Printexc.to_string e)" ;
27252725+ emit_newline out
27262726+ end ;
27272727+ (* to_yojson *)
27282728+ if to_keyword <> "SKIP" then begin
27292729+ emitln out
27302730+ (Printf.sprintf "%s %s_to_yojson (r : %s) =" to_keyword type_name
27312731+ type_name ) ;
27322732+ emitln out " `Assoc [" ;
27332733+ List.iteri
27342734+ (fun i (prop_name, (prop : property)) ->
27352735+ let ocaml_name = Naming.field_name prop_name in
27362736+ let conv_expr = gen_shared_to_yojson_expr nsid prop.type_def in
27372737+ let is_required = List.mem prop_name required in
27382738+ let is_nullable = List.mem prop_name nullable in
27392739+ let is_optional = (not is_required) || is_nullable in
27402740+ let comma =
27412741+ if i < List.length spec.properties - 1 then ";" else ""
27422742+ in
27432743+ if is_optional then
27442744+ emitln out
27452745+ (Printf.sprintf
27462746+ " (\"%s\", match r.%s with Some v -> %s v | None -> \
27472747+ `Null)%s"
27482748+ prop_name ocaml_name conv_expr comma )
27492749+ else
27502750+ emitln out
27512751+ (Printf.sprintf " (\"%s\", %s r.%s)%s" prop_name conv_expr
27522752+ ocaml_name comma ) )
27532753+ spec.properties ;
27542754+ emitln out " ]" ;
27552755+ emit_newline out
27562756+ end
27572757+ end
27582758+ in
27592759+ (* helper to generate inline union converters *)
27602760+ let gen_shared_inline_union_converters ?(of_keyword = "let")
27612761+ ?(to_keyword = "let") nsid name refs spec =
27622762+ let is_closed = Option.value spec.closed ~default:false in
27632763+ (* of_yojson *)
27642764+ if of_keyword <> "SKIP" then begin
27652765+ emitln out (Printf.sprintf "%s %s_of_yojson json =" of_keyword name) ;
27662766+ emitln out " let open Yojson.Safe.Util in" ;
27672767+ emitln out " try" ;
27682768+ emitln out " match json |> member \"$type\" |> to_string with" ;
27692769+ List.iter
27702770+ (fun ref_str ->
27712771+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
27722772+ let full_type_uri = gen_shared_type_uri nsid ref_str in
27732773+ let payload_type = gen_shared_ref_type nsid ref_str in
27742774+ emitln out (Printf.sprintf " | \"%s\" ->" full_type_uri) ;
27752775+ emitln out
27762776+ (Printf.sprintf " (match %s_of_yojson json with" payload_type) ;
27772777+ emitln out
27782778+ (Printf.sprintf " | Ok v -> Ok (%s v)" variant_name) ;
27792779+ emitln out " | Error e -> Error e)" )
27802780+ refs ;
27812781+ if is_closed then
27822782+ emitln out " | t -> Error (\"unknown union type: \" ^ t)"
27832783+ else emitln out " | _ -> Ok (Unknown json)" ;
27842784+ emitln out " with _ -> Error \"failed to parse union\"" ;
27852785+ emit_newline out
27862786+ end ;
27872787+ (* to_yojson *)
27882788+ if to_keyword <> "SKIP" then begin
27892789+ emitln out (Printf.sprintf "%s %s_to_yojson = function" to_keyword name) ;
27902790+ List.iter
27912791+ (fun ref_str ->
27922792+ let variant_name = Naming.qualified_variant_name_of_ref ref_str in
27932793+ let full_type_uri = gen_shared_type_uri nsid ref_str in
27942794+ let payload_type = gen_shared_ref_type nsid ref_str in
27952795+ emitln out (Printf.sprintf " | %s v ->" variant_name) ;
27962796+ emitln out
27972797+ (Printf.sprintf " (match %s_to_yojson v with" payload_type) ;
27982798+ emitln out
27992799+ (Printf.sprintf
28002800+ " | `Assoc fields -> `Assoc ((\"$type\", `String \"%s\") \
28012801+ :: fields)"
28022802+ full_type_uri ) ;
28032803+ emitln out " | other -> other)" )
28042804+ refs ;
28052805+ if not is_closed then emitln out " | Unknown j -> j" ;
28062806+ emit_newline out
28072807+ end
28082808+ in
28092809+ (* collect all inline unions as pseudo-defs for proper ordering *)
28102810+ let rec collect_inline_unions_from_type nsid context acc type_def =
28112811+ match type_def with
28122812+ | Union spec ->
28132813+ let union_name = get_shared_inline_union_name nsid context in
28142814+ (nsid, union_name, spec.refs, spec) :: acc
28152815+ | Array {items; _} ->
28162816+ collect_inline_unions_from_type nsid (context ^ "_item") acc items
28172817+ | Object {properties; _} ->
28182818+ List.fold_left
28192819+ (fun a (prop_name, (prop : property)) ->
28202820+ collect_inline_unions_from_type nsid prop_name a prop.type_def )
28212821+ acc properties
28222822+ | _ ->
28232823+ acc
28242824+ in
28252825+ let all_inline_unions =
28262826+ List.concat_map
28272827+ (fun (nsid, def) ->
28282828+ match def.type_def with
28292829+ | Object spec ->
28302830+ List.fold_left
28312831+ (fun acc (prop_name, (prop : property)) ->
28322832+ collect_inline_unions_from_type nsid prop_name acc prop.type_def )
28332833+ [] spec.properties
28342834+ | Record spec ->
28352835+ List.fold_left
28362836+ (fun acc (prop_name, (prop : property)) ->
28372837+ collect_inline_unions_from_type nsid prop_name acc prop.type_def )
28382838+ [] spec.record.properties
28392839+ | _ ->
28402840+ [] )
28412841+ all_defs
28422842+ in
28432843+ (* create inline union entries *)
28442844+ let inline_union_defs =
28452845+ List.map
28462846+ (fun (nsid, name, refs, spec) ->
28472847+ let key = nsid ^ "#__inline__" ^ name in
28482848+ let deps =
28492849+ List.filter_map
28502850+ (fun r ->
28512851+ if String.length r > 0 && r.[0] = '#' then
28522852+ let def_name = String.sub r 1 (String.length r - 1) in
28532853+ Some (nsid ^ "#" ^ def_name)
28542854+ else
28552855+ match String.split_on_char '#' r with
28562856+ | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids ->
28572857+ Some (ext_nsid ^ "#" ^ def_name)
28582858+ | _ ->
28592859+ None )
28602860+ refs
28612861+ in
28622862+ (key, deps, `InlineUnion (nsid, name, refs, spec)) )
28632863+ all_inline_unions
28642864+ in
28652865+ (* create regular def entries *)
28662866+ let regular_def_entries =
28672867+ List.map
28682868+ (fun (nsid, def) ->
28692869+ let key = nsid ^ "#" ^ def.name in
28702870+ let base_deps = collect_shared_local_refs nsid [] def.type_def in
28712871+ let inline_deps =
28722872+ match def.type_def with
28732873+ | Object spec | Record {record= spec; _} ->
28742874+ List.fold_left
28752875+ (fun acc (prop_name, (prop : property)) ->
28762876+ match prop.type_def with
28772877+ | Union _ ->
28782878+ let union_name =
28792879+ get_shared_inline_union_name nsid prop_name
28802880+ in
28812881+ (nsid ^ "#__inline__" ^ union_name) :: acc
28822882+ | Array {items= Union _; _} ->
28832883+ let union_name =
28842884+ get_shared_inline_union_name nsid (prop_name ^ "_item")
28852885+ in
28862886+ (nsid ^ "#__inline__" ^ union_name) :: acc
28872887+ | _ ->
28882888+ acc )
28892889+ [] spec.properties
28902890+ | _ ->
28912891+ []
28922892+ in
28932893+ (key, base_deps @ inline_deps, `RegularDef (nsid, def)) )
28942894+ all_defs
28952895+ in
28962896+ (* combine all entries *)
28972897+ let all_entries = regular_def_entries @ inline_union_defs in
28982898+ let deps_map = List.map (fun (k, deps, _) -> (k, deps)) all_entries in
28992899+ let entry_map = List.map (fun (k, _, entry) -> (k, entry)) all_entries in
29002900+ let all_keys = List.map (fun (k, _, _) -> k) all_entries in
29012901+ (* run Tarjan's algorithm *)
29022902+ let index_counter = ref 0 in
29032903+ let indices = Hashtbl.create 64 in
29042904+ let lowlinks = Hashtbl.create 64 in
29052905+ let on_stack = Hashtbl.create 64 in
29062906+ let stack = ref [] in
29072907+ let sccs = ref [] in
29082908+ let rec strongconnect key =
29092909+ let index = !index_counter in
29102910+ incr index_counter ;
29112911+ Hashtbl.add indices key index ;
29122912+ Hashtbl.add lowlinks key index ;
29132913+ Hashtbl.add on_stack key true ;
29142914+ stack := key :: !stack ;
29152915+ let successors =
29162916+ try List.assoc key deps_map |> List.filter (fun k -> List.mem k all_keys)
29172917+ with Not_found -> []
29182918+ in
29192919+ List.iter
29202920+ (fun succ ->
29212921+ if not (Hashtbl.mem indices succ) then begin
29222922+ strongconnect succ ;
29232923+ Hashtbl.replace lowlinks key
29242924+ (min (Hashtbl.find lowlinks key) (Hashtbl.find lowlinks succ))
29252925+ end
29262926+ else if Hashtbl.find_opt on_stack succ = Some true then
29272927+ Hashtbl.replace lowlinks key
29282928+ (min (Hashtbl.find lowlinks key) (Hashtbl.find indices succ)) )
29292929+ successors ;
29302930+ if Hashtbl.find lowlinks key = Hashtbl.find indices key then begin
29312931+ let rec pop_scc acc =
29322932+ match !stack with
29332933+ | [] ->
29342934+ acc
29352935+ | top :: rest ->
29362936+ stack := rest ;
29372937+ Hashtbl.replace on_stack top false ;
29382938+ if top = key then top :: acc else pop_scc (top :: acc)
29392939+ in
29402940+ let scc_keys = pop_scc [] in
29412941+ let scc_entries =
29422942+ List.filter_map (fun k -> List.assoc_opt k entry_map) scc_keys
29432943+ in
29442944+ if scc_entries <> [] then sccs := scc_entries :: !sccs
29452945+ end
29462946+ in
29472947+ List.iter
29482948+ (fun key -> if not (Hashtbl.mem indices key) then strongconnect key)
29492949+ all_keys ;
29502950+ let ordered_sccs = List.rev !sccs in
29512951+ (* generate each SCC *)
29522952+ List.iter
29532953+ (fun scc ->
29542954+ let inline_unions_in_scc =
29552955+ List.filter_map (function `InlineUnion x -> Some x | _ -> None) scc
29562956+ in
29572957+ let regular_defs_in_scc =
29582958+ List.filter_map (function `RegularDef x -> Some x | _ -> None) scc
29592959+ in
29602960+ if inline_unions_in_scc = [] then begin
29612961+ (* no inline unions - check if we still need mutual recursion *)
29622962+ match regular_defs_in_scc with
29632963+ | [] ->
29642964+ ()
29652965+ | [(nsid, def)] ->
29662966+ (* single def, generate normally *)
29672967+ gen_shared_single_def (nsid, def)
29682968+ | defs ->
29692969+ (* multiple defs in SCC - need mutual recursion *)
29702970+ (* filter to only object-like types that can be mutually recursive *)
29712971+ let obj_defs =
29722972+ List.filter
29732973+ (fun (_, def) ->
29742974+ match def.type_def with
29752975+ | Object _ | Record _ ->
29762976+ true
29772977+ | _ ->
29782978+ false )
29792979+ defs
29802980+ in
29812981+ let other_defs =
29822982+ List.filter
29832983+ (fun (_, def) ->
29842984+ match def.type_def with
29852985+ | Object _ | Record _ ->
29862986+ false
29872987+ | _ ->
29882988+ true )
29892989+ defs
29902990+ in
29912991+ (* generate non-object types first (they have their own converters) *)
29922992+ List.iter gen_shared_single_def other_defs ;
29932993+ (* generate object types as mutually recursive *)
29942994+ if obj_defs <> [] then begin
29952995+ (* register inline unions from all objects first *)
29962996+ List.iter
29972997+ (fun (nsid, def) ->
29982998+ match def.type_def with
29992999+ | Object spec ->
30003000+ register_shared_inline_unions nsid spec.properties
30013001+ | Record rspec ->
30023002+ register_shared_inline_unions nsid rspec.record.properties
30033003+ | _ ->
30043004+ () )
30053005+ obj_defs ;
30063006+ (* generate all type definitions *)
30073007+ List.iteri
30083008+ (fun i (nsid, def) ->
30093009+ let keyword = if i = 0 then "type" else "and" in
30103010+ match def.type_def with
30113011+ | Object spec ->
30123012+ gen_shared_object_type_only ~keyword nsid def.name spec
30133013+ | Record rspec ->
30143014+ gen_shared_object_type_only ~keyword nsid def.name
30153015+ rspec.record
30163016+ | _ ->
30173017+ () )
30183018+ obj_defs ;
30193019+ emit_newline out ;
30203020+ (* generate all _of_yojson converters as mutually recursive *)
30213021+ List.iteri
30223022+ (fun i (nsid, def) ->
30233023+ let of_keyword = if i = 0 then "let rec" else "and" in
30243024+ match def.type_def with
30253025+ | Object spec ->
30263026+ gen_shared_object_converters ~of_keyword
30273027+ ~to_keyword:"SKIP" nsid def.name spec
30283028+ | Record rspec ->
30293029+ gen_shared_object_converters ~of_keyword
30303030+ ~to_keyword:"SKIP" nsid def.name rspec.record
30313031+ | _ ->
30323032+ () )
30333033+ obj_defs ;
30343034+ (* generate all _to_yojson converters *)
30353035+ List.iter
30363036+ (fun (nsid, def) ->
30373037+ match def.type_def with
30383038+ | Object spec ->
30393039+ gen_shared_object_converters ~of_keyword:"SKIP"
30403040+ ~to_keyword:"and" nsid def.name spec
30413041+ | Record rspec ->
30423042+ gen_shared_object_converters ~of_keyword:"SKIP"
30433043+ ~to_keyword:"and" nsid def.name rspec.record
30443044+ | _ ->
30453045+ () )
30463046+ obj_defs
30473047+ end
30483048+ end
30493049+ else begin
30503050+ (* has inline unions - generate all types first, then all converters *)
30513051+ List.iter
30523052+ (fun (_nsid, name, refs, _spec) ->
30533053+ register_union_name out refs name ;
30543054+ mark_union_generated out name )
30553055+ inline_unions_in_scc ;
30563056+ let all_items =
30573057+ List.map (fun x -> `Inline x) inline_unions_in_scc
30583058+ @ List.map (fun x -> `Regular x) regular_defs_in_scc
30593059+ in
30603060+ let n = List.length all_items in
30613061+ if n = 1 then begin
30623062+ match List.hd all_items with
30633063+ | `Inline (nsid, name, refs, spec) ->
30643064+ gen_shared_inline_union_type_only nsid name refs spec ;
30653065+ emit_newline out ;
30663066+ gen_shared_inline_union_converters nsid name refs spec
30673067+ | `Regular (nsid, def) -> (
30683068+ match def.type_def with
30693069+ | Object spec ->
30703070+ register_shared_inline_unions nsid spec.properties ;
30713071+ gen_shared_object_type_only nsid def.name spec ;
30723072+ emit_newline out ;
30733073+ gen_shared_object_converters nsid def.name spec
30743074+ | Record rspec ->
30753075+ register_shared_inline_unions nsid rspec.record.properties ;
30763076+ gen_shared_object_type_only nsid def.name rspec.record ;
30773077+ emit_newline out ;
30783078+ gen_shared_object_converters nsid def.name rspec.record
30793079+ | _ ->
30803080+ gen_shared_single_def (nsid, def) )
30813081+ end
30823082+ else begin
30833083+ (* multiple items - generate as mutually recursive types *)
30843084+ List.iter
30853085+ (function
30863086+ | `Regular (nsid, def) -> (
30873087+ match def.type_def with
30883088+ | Object spec ->
30893089+ register_shared_inline_unions nsid spec.properties
30903090+ | Record rspec ->
30913091+ register_shared_inline_unions nsid rspec.record.properties
30923092+ | _ ->
30933093+ () )
30943094+ | `Inline _ ->
30953095+ () )
30963096+ all_items ;
30973097+ (* generate all type definitions *)
30983098+ List.iteri
30993099+ (fun i item ->
31003100+ let keyword = if i = 0 then "type" else "and" in
31013101+ match item with
31023102+ | `Inline (nsid, name, refs, spec) ->
31033103+ gen_shared_inline_union_type_only ~keyword nsid name refs spec
31043104+ | `Regular (nsid, def) -> (
31053105+ match def.type_def with
31063106+ | Object spec ->
31073107+ gen_shared_object_type_only ~keyword nsid def.name spec
31083108+ | Record rspec ->
31093109+ gen_shared_object_type_only ~keyword nsid def.name
31103110+ rspec.record
31113111+ | _ ->
31123112+ () ) )
31133113+ all_items ;
31143114+ emit_newline out ;
31153115+ (* generate all _of_yojson converters *)
31163116+ List.iteri
31173117+ (fun i item ->
31183118+ let of_keyword = if i = 0 then "let rec" else "and" in
31193119+ match item with
31203120+ | `Inline (nsid, name, refs, spec) ->
31213121+ gen_shared_inline_union_converters ~of_keyword
31223122+ ~to_keyword:"SKIP" nsid name refs spec
31233123+ | `Regular (nsid, def) -> (
31243124+ match def.type_def with
31253125+ | Object spec ->
31263126+ gen_shared_object_converters ~of_keyword ~to_keyword:"SKIP"
31273127+ nsid def.name spec
31283128+ | Record rspec ->
31293129+ gen_shared_object_converters ~of_keyword ~to_keyword:"SKIP"
31303130+ nsid def.name rspec.record
31313131+ | _ ->
31323132+ () ) )
31333133+ all_items ;
31343134+ (* generate all _to_yojson converters *)
31353135+ List.iteri
31363136+ (fun i item ->
31373137+ let to_keyword = "and" in
31383138+ ignore i ;
31393139+ match item with
31403140+ | `Inline (nsid, name, refs, spec) ->
31413141+ gen_shared_inline_union_converters ~of_keyword:"SKIP"
31423142+ ~to_keyword nsid name refs spec
31433143+ | `Regular (nsid, def) -> (
31443144+ match def.type_def with
31453145+ | Object spec ->
31463146+ gen_shared_object_converters ~of_keyword:"SKIP" ~to_keyword
31473147+ nsid def.name spec
31483148+ | Record rspec ->
31493149+ gen_shared_object_converters ~of_keyword:"SKIP" ~to_keyword
31503150+ nsid def.name rspec.record
31513151+ | _ ->
31523152+ () ) )
31533153+ all_items
31543154+ end
31553155+ end )
31563156+ ordered_sccs ;
31573157+ Emitter.contents out
31583158+31593159+(* generate a re-export module that maps local names to shared module types *)
31603160+let gen_reexport_module ~shared_module_name ~all_merged_docs (doc : lexicon_doc)
31613161+ : string =
31623162+ let buf = Buffer.create 1024 in
31633163+ let emit s = Buffer.add_string buf s in
31643164+ let emitln s = Buffer.add_string buf s ; Buffer.add_char buf '\n' in
31653165+ (* detect collisions across all merged docs *)
31663166+ let all_defs =
31673167+ List.concat_map
31683168+ (fun d -> List.map (fun def -> (d.id, def)) d.defs)
31693169+ all_merged_docs
31703170+ in
31713171+ let name_counts = Hashtbl.create 64 in
31723172+ List.iter
31733173+ (fun (nsid, def) ->
31743174+ let existing = Hashtbl.find_opt name_counts def.name in
31753175+ match existing with
31763176+ | None ->
31773177+ Hashtbl.add name_counts def.name [nsid]
31783178+ | Some nsids when not (List.mem nsid nsids) ->
31793179+ Hashtbl.replace name_counts def.name (nsid :: nsids)
31803180+ | _ ->
31813181+ () )
31823182+ all_defs ;
31833183+ let colliding_names =
31843184+ Hashtbl.fold
31853185+ (fun name nsids acc -> if List.length nsids > 1 then name :: acc else acc)
31863186+ name_counts []
31873187+ in
31883188+ (* function to get shared type name (context-based for collisions) *)
31893189+ let get_shared_type_name nsid def_name =
31903190+ if List.mem def_name colliding_names then
31913191+ Naming.shared_type_name nsid def_name
31923192+ else Naming.type_name def_name
31933193+ in
31943194+ emitln (Printf.sprintf "(* re-exported from %s *)" shared_module_name) ;
31953195+ emitln "" ;
31963196+ List.iter
31973197+ (fun def ->
31983198+ let local_type_name = Naming.type_name def.name in
31993199+ let shared_type_name = get_shared_type_name doc.id def.name in
32003200+ match def.type_def with
32013201+ | Object _ | Record _ | Union _ ->
32023202+ emitln
32033203+ (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name
32043204+ shared_type_name ) ;
32053205+ emitln
32063206+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
32073207+ shared_module_name shared_type_name ) ;
32083208+ emitln
32093209+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
32103210+ shared_module_name shared_type_name ) ;
32113211+ emit "\n"
32123212+ | String spec when spec.known_values <> None ->
32133213+ emitln
32143214+ (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name
32153215+ shared_type_name ) ;
32163216+ emitln
32173217+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
32183218+ shared_module_name shared_type_name ) ;
32193219+ emitln
32203220+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
32213221+ shared_module_name shared_type_name ) ;
32223222+ emit "\n"
32233223+ | Array _ ->
32243224+ emitln
32253225+ (Printf.sprintf "type %s = %s.%s" local_type_name shared_module_name
32263226+ shared_type_name ) ;
32273227+ emitln
32283228+ (Printf.sprintf "let %s_of_yojson = %s.%s_of_yojson" local_type_name
32293229+ shared_module_name shared_type_name ) ;
32303230+ emitln
32313231+ (Printf.sprintf "let %s_to_yojson = %s.%s_to_yojson" local_type_name
32323232+ shared_module_name shared_type_name ) ;
32333233+ emit "\n"
32343234+ | Token _ ->
32353235+ emitln
32363236+ (Printf.sprintf "let %s = %s.%s" local_type_name shared_module_name
32373237+ shared_type_name ) ;
32383238+ emit "\n"
32393239+ | Query _ | Procedure _ ->
32403240+ let mod_name = Naming.def_module_name def.name in
32413241+ emitln
32423242+ (Printf.sprintf "module %s = %s.%s" mod_name shared_module_name
32433243+ mod_name ) ;
32443244+ emit "\n"
32453245+ | _ ->
32463246+ () )
32473247+ doc.defs ;
32483248+ Buffer.contents buf
+88
hermes-cli/lib/naming.ml
···113113 in
114114 String.capitalize_ascii name
115115116116+(* generate qualified variant name including last nsid segment to avoid conflicts *)
117117+(* "app.bsky.embed.images#view" -> "ImagesView" *)
118118+(* "app.bsky.embed.images" (no #) -> "Images" (refers to main) *)
119119+(* "#localDef" -> "LocalDef" (no qualifier for local refs) *)
120120+let qualified_variant_name_of_ref ref_str =
121121+ match String.split_on_char '#' ref_str with
122122+ | [nsid; def] ->
123123+ (* external ref with def: use last segment of nsid as qualifier *)
124124+ let segments = String.split_on_char '.' nsid in
125125+ let qualifier =
126126+ match List.rev segments with
127127+ | last :: _ ->
128128+ String.capitalize_ascii last
129129+ | [] ->
130130+ ""
131131+ in
132132+ qualifier ^ String.capitalize_ascii def
133133+ | [nsid] when not (String.contains nsid '#') -> (
134134+ (* just nsid, no # - refers to main def, use last segment *)
135135+ let segments = String.split_on_char '.' nsid in
136136+ match List.rev segments with
137137+ | last :: _ ->
138138+ String.capitalize_ascii last
139139+ | [] ->
140140+ "Unknown" )
141141+ | _ ->
142142+ (* local ref like "#foo" *)
143143+ if String.length ref_str > 0 && ref_str.[0] = '#' then
144144+ String.capitalize_ascii
145145+ (String.sub ref_str 1 (String.length ref_str - 1))
146146+ else String.capitalize_ascii ref_str
147147+116148let union_type_name refs =
117149 match refs with
118150 | [] ->
···145177 if needs_key_annotation original_name ocaml_name then
146178 Printf.sprintf " [@key \"%s\"]" original_name
147179 else ""
180180+181181+(** find common prefix segments from a list of NSIDs
182182+ e.g. ["app.bsky.actor.defs"; "app.bsky.feed.defs"; "app.bsky.graph.defs"]
183183+ -> ["app"; "bsky"] *)
184184+let common_prefix_of_nsids nsids =
185185+ match nsids with
186186+ | [] ->
187187+ []
188188+ | first :: rest ->
189189+ let first_segments = String.split_on_char '.' first in
190190+ List.fold_left
191191+ (fun prefix nsid ->
192192+ let segments = String.split_on_char '.' nsid in
193193+ let rec common acc l1 l2 =
194194+ match (l1, l2) with
195195+ | h1 :: t1, h2 :: t2 when h1 = h2 ->
196196+ common (h1 :: acc) t1 t2
197197+ | _ ->
198198+ List.rev acc
199199+ in
200200+ common [] prefix segments )
201201+ first_segments rest
202202+203203+(** generate shared module file name from NSIDs
204204+ e.g. ["app.bsky.actor.defs"; "app.bsky.feed.defs"] with index 1
205205+ -> "app_bsky_shared_1.ml" *)
206206+let shared_file_name nsids index =
207207+ let prefix = common_prefix_of_nsids nsids in
208208+ let prefix_str = String.concat "_" prefix in
209209+ prefix_str ^ "_shared_" ^ string_of_int index ^ ".ml"
210210+211211+(** generate shared module name from NSIDs
212212+ e.g. ["app.bsky.actor.defs"; "app.bsky.feed.defs"] with index 1
213213+ -> "App_bsky_shared_1" *)
214214+let shared_module_name nsids index =
215215+ let prefix = common_prefix_of_nsids nsids in
216216+ let prefix_str = String.concat "_" prefix in
217217+ String.capitalize_ascii (prefix_str ^ "_shared_" ^ string_of_int index)
218218+219219+(** generate a short type name for use in shared modules
220220+ uses the last segment of the nsid as context
221221+ e.g. nsid="app.bsky.actor.defs", def_name="viewerState"
222222+ -> "actor_viewer_state" *)
223223+let shared_type_name nsid def_name =
224224+ let segments = String.split_on_char '.' nsid in
225225+ let context =
226226+ match List.rev segments with
227227+ (* use second-last segment if last is "defs" *)
228228+ | "defs" :: second :: _ ->
229229+ second
230230+ | last :: _ ->
231231+ last
232232+ | [] ->
233233+ "unknown"
234234+ in
235235+ type_name (context ^ "_" ^ def_name)
+199-4
hermes-cli/test/test_codegen.ml
···257257 check bool "contains full URI" true
258258 (contains code "com.example.tokens#myToken")
259259260260+(* test generating inline union (union as property type) *)
261261+let test_gen_inline_union () =
262262+ let union_type =
263263+ Lexicon_types.Union
264264+ { refs= ["#typeA"; "#typeB"]
265265+ ; closed= Some false
266266+ ; description= None }
267267+ in
268268+ let obj_spec =
269269+ make_object_spec
270270+ [("status", make_property union_type)]
271271+ ["status"]
272272+ in
273273+ let doc =
274274+ make_lexicon "com.example.inline"
275275+ [make_def "main" (Lexicon_types.Object obj_spec)]
276276+ in
277277+ let code = Codegen.gen_lexicon_module doc in
278278+ (* inline union should get its own type named after the property *)
279279+ check bool "contains type status" true (contains code "type status =") ;
280280+ check bool "contains TypeA variant" true (contains code "| TypeA of") ;
281281+ check bool "contains TypeB variant" true (contains code "| TypeB of") ;
282282+ (* main type should reference the inline union *)
283283+ check bool "main uses status type" true (contains code "status: status")
284284+285285+(* test generating inline union in array (field_item context) *)
286286+let test_gen_inline_union_in_array () =
287287+ let union_type =
288288+ Lexicon_types.Union
289289+ { refs= ["#typeA"; "#typeB"]
290290+ ; closed= Some true
291291+ ; description= None }
292292+ in
293293+ let array_type =
294294+ Lexicon_types.Array
295295+ { items= union_type
296296+ ; min_length= None
297297+ ; max_length= None
298298+ ; description= None }
299299+ in
300300+ let obj_spec =
301301+ make_object_spec
302302+ [("items", make_property array_type)]
303303+ ["items"]
304304+ in
305305+ let doc =
306306+ make_lexicon "com.example.arrayunion"
307307+ [make_def "main" (Lexicon_types.Object obj_spec)]
308308+ in
309309+ let code = Codegen.gen_lexicon_module doc in
310310+ (* inline union in array should be named field_item *)
311311+ check bool "contains type items_item" true (contains code "type items_item =") ;
312312+ check bool "items is items_item list" true (contains code "items_item list")
313313+314314+(* test generating empty object as unit *)
315315+let test_gen_empty_object () =
316316+ let empty_spec =
317317+ { Lexicon_types.properties= []
318318+ ; required= None
319319+ ; nullable= None
320320+ ; description= None }
321321+ in
322322+ let doc =
323323+ make_lexicon "com.example.empty"
324324+ [make_def "main" (Lexicon_types.Object empty_spec)]
325325+ in
326326+ let code = Codegen.gen_lexicon_module doc in
327327+ check bool "contains type main = unit" true (contains code "type main = unit") ;
328328+ check bool "contains main_of_yojson _ = Ok ()" true
329329+ (contains code "main_of_yojson _ = Ok ()")
330330+331331+(* test generating nullable fields (different from optional) *)
332332+let test_gen_nullable_fields () =
333333+ let obj_spec =
334334+ { Lexicon_types.properties=
335335+ [ ("required_nullable", make_property string_type)
336336+ ; ("required_not_nullable", make_property string_type) ]
337337+ ; required= Some ["required_nullable"; "required_not_nullable"]
338338+ ; nullable= Some ["required_nullable"]
339339+ ; description= None }
340340+ in
341341+ let doc =
342342+ make_lexicon "com.example.nullable"
343343+ [make_def "main" (Lexicon_types.Object obj_spec)]
344344+ in
345345+ let code = Codegen.gen_lexicon_module doc in
346346+ (* required + nullable = option *)
347347+ check bool "nullable is option" true
348348+ (contains code "required_nullable: string option") ;
349349+ (* required + not nullable = not option *)
350350+ check bool "not nullable is not option" true
351351+ (contains code "required_not_nullable: string;")
352352+353353+(* test generating mutually recursive types *)
354354+let test_gen_mutually_recursive () =
355355+ (* typeA has a field of typeB, typeB has a field of typeA *)
356356+ let type_a_spec =
357357+ make_object_spec
358358+ [ ("name", make_property string_type)
359359+ ; ("b", make_property (Lexicon_types.Ref {ref_= "#typeB"; description= None}))
360360+ ]
361361+ ["name"]
362362+ in
363363+ let type_b_spec =
364364+ make_object_spec
365365+ [ ("value", make_property int_type)
366366+ ; ("a", make_property (Lexicon_types.Ref {ref_= "#typeA"; description= None}))
367367+ ]
368368+ ["value"]
369369+ in
370370+ let doc =
371371+ make_lexicon "com.example.recursive"
372372+ [ make_def "typeA" (Lexicon_types.Object type_a_spec)
373373+ ; make_def "typeB" (Lexicon_types.Object type_b_spec) ]
374374+ in
375375+ let code = Codegen.gen_lexicon_module doc in
376376+ (* should use "type ... and ..." syntax *)
377377+ check bool "has type keyword" true (contains code "type type_a =") ;
378378+ check bool "has and keyword" true (contains code "and type_b =") ;
379379+ (* deriving should appear after the last type in the group *)
380380+ check bool "has deriving after and block" true
381381+ (contains code "[@@deriving yojson")
382382+383383+(* test generating record type *)
384384+let test_gen_record () =
385385+ let record_spec : Lexicon_types.record_spec =
386386+ { key= "tid"
387387+ ; record=
388388+ make_object_spec
389389+ [("text", make_property string_type)]
390390+ ["text"]
391391+ ; description= Some "A simple record" }
392392+ in
393393+ let doc =
394394+ make_lexicon "com.example.record"
395395+ [make_def "main" (Lexicon_types.Record record_spec)]
396396+ in
397397+ let code = Codegen.gen_lexicon_module doc in
398398+ check bool "contains type main" true (contains code "type main =") ;
399399+ check bool "contains text field" true (contains code "text: string")
400400+401401+(* test generating external ref *)
402402+let test_gen_external_ref () =
403403+ let obj_spec =
404404+ make_object_spec
405405+ [ ( "user"
406406+ , make_property
407407+ (Lexicon_types.Ref {ref_= "com.other.defs#user"; description= None})
408408+ ) ]
409409+ ["user"]
410410+ in
411411+ let doc =
412412+ make_lexicon "com.example.extref"
413413+ [make_def "main" (Lexicon_types.Object obj_spec)]
414414+ in
415415+ let code = Codegen.gen_lexicon_module doc in
416416+ (* should generate qualified module reference *)
417417+ check bool "contains qualified ref" true
418418+ (contains code "Com_other_defs.user")
419419+420420+(* test generating string type with known values *)
421421+let test_gen_string_known_values () =
422422+ let string_spec : Lexicon_types.string_spec =
423423+ { format= None
424424+ ; min_length= None
425425+ ; max_length= None
426426+ ; min_graphemes= None
427427+ ; max_graphemes= None
428428+ ; known_values= Some ["pending"; "active"; "completed"]
429429+ ; enum= None
430430+ ; const= None
431431+ ; default= None
432432+ ; description= Some "Status values" }
433433+ in
434434+ let doc =
435435+ make_lexicon "com.example.status"
436436+ [make_def "status" (Lexicon_types.String string_spec)]
437437+ in
438438+ let code = Codegen.gen_lexicon_module doc in
439439+ check bool "contains type status = string" true
440440+ (contains code "type status = string") ;
441441+ check bool "contains status_of_yojson" true
442442+ (contains code "status_of_yojson")
443443+260444(* test generating query with bytes output (like getBlob) *)
261445let test_gen_query_bytes_output () =
262446 let params_spec =
···317501let object_tests =
318502 [ ("simple object", `Quick, test_gen_simple_object)
319503 ; ("optional fields", `Quick, test_gen_optional_fields)
320320- ; ("key annotation", `Quick, test_gen_key_annotation) ]
504504+ ; ("key annotation", `Quick, test_gen_key_annotation)
505505+ ; ("empty object", `Quick, test_gen_empty_object)
506506+ ; ("nullable fields", `Quick, test_gen_nullable_fields)
507507+ ; ("external ref", `Quick, test_gen_external_ref)
508508+ ; ("record type", `Quick, test_gen_record) ]
321509322510let union_tests =
323511 [ ("open union", `Quick, test_gen_union_type)
324324- ; ("closed union", `Quick, test_gen_closed_union) ]
512512+ ; ("closed union", `Quick, test_gen_closed_union)
513513+ ; ("inline union", `Quick, test_gen_inline_union)
514514+ ; ("inline union in array", `Quick, test_gen_inline_union_in_array) ]
325515326516let xrpc_tests =
327517 [ ("query module", `Quick, test_gen_query_module)
···329519 ; ("query with bytes output", `Quick, test_gen_query_bytes_output)
330520 ; ("procedure with bytes input", `Quick, test_gen_procedure_bytes_input) ]
331521332332-let ordering_tests = [("type ordering", `Quick, test_type_ordering)]
522522+let ordering_tests =
523523+ [ ("type ordering", `Quick, test_type_ordering)
524524+ ; ("mutually recursive", `Quick, test_gen_mutually_recursive) ]
333525334526let token_tests = [("token generation", `Quick, test_gen_token)]
335527528528+let string_tests = [("string with known values", `Quick, test_gen_string_known_values)]
529529+336530let () =
337531 run "Codegen"
338532 [ ("objects", object_tests)
339533 ; ("unions", union_tests)
340534 ; ("xrpc", xrpc_tests)
341535 ; ("ordering", ordering_tests)
342342- ; ("tokens", token_tests) ]
536536+ ; ("tokens", token_tests)
537537+ ; ("strings", string_tests) ]