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