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