objective categorical abstract machine language personal data server
at main 149 lines 5.7 kB view raw
1open Hermes_cli 2 3(* recursively find all json files in a path (file or directory) *) 4let find_json_files path = 5 let rec aux acc p = 6 if Sys.is_directory p then 7 Sys.readdir p |> Array.to_list 8 |> List.map (Filename.concat p) 9 |> List.fold_left aux acc 10 else if Filename.check_suffix p ".json" then p :: acc 11 else acc 12 in 13 aux [] path 14 15(* generate module structure from lexicons *) 16let generate ~inputs ~output_dir ~module_name = 17 (* create output directory *) 18 if not (Sys.file_exists output_dir) then Sys.mkdir output_dir 0o755 ; 19 (* find all lexicon files from all inputs *) 20 let files = List.concat_map find_json_files inputs in 21 Printf.printf "Found %d lexicon files\n" (List.length files) ; 22 (* parse all files *) 23 let lexicons = 24 List.filter_map 25 (fun path -> 26 match Parser.parse_file path with 27 | Ok doc -> 28 Printf.printf " Parsed: %s\n" doc.Lexicon_types.id ; 29 Some doc 30 | Error e -> 31 Printf.eprintf " Error parsing %s: %s\n" path e ; 32 None ) 33 files 34 in 35 Printf.printf "Successfully parsed %d lexicons\n" (List.length lexicons) ; 36 (* find file-level SCCs to detect cross-file cycles *) 37 let sccs = Scc.find_file_sccs lexicons in 38 Printf.printf "Found %d file-level SCCs\n" (List.length sccs) ; 39 (* track shared module index for unique naming *) 40 let shared_index = ref 0 in 41 (* generate each SCC *) 42 List.iter 43 (fun scc -> 44 match scc with 45 | [] -> 46 () 47 | [doc] -> 48 (* single file, no cycle - generate normally *) 49 let code = Codegen.gen_lexicon_module doc in 50 let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in 51 let full_path = Filename.concat output_dir rel_path in 52 let oc = open_out full_path in 53 output_string oc code ; 54 close_out oc ; 55 Printf.printf " Generated: %s\n" rel_path 56 | docs -> 57 (* multiple files forming a cycle - use shared module strategy *) 58 incr shared_index ; 59 let nsids = List.map (fun d -> d.Lexicon_types.id) docs in 60 Printf.printf " Cyclic lexicons: %s\n" (String.concat ", " nsids) ; 61 (* sort for consistent ordering *) 62 let sorted_docs = 63 List.sort 64 (fun a b -> String.compare a.Lexicon_types.id b.Lexicon_types.id) 65 docs 66 in 67 (* generate shared module with all types *) 68 let shared_module_name = 69 Naming.shared_module_name nsids !shared_index 70 in 71 let shared_file = Naming.shared_file_name nsids !shared_index in 72 let code = Codegen.gen_shared_module sorted_docs in 73 let full_path = Filename.concat output_dir shared_file in 74 let oc = open_out full_path in 75 output_string oc code ; 76 close_out oc ; 77 Printf.printf " Generated shared: %s\n" shared_file ; 78 (* generate re-export modules for each nsid *) 79 List.iter 80 (fun doc -> 81 let stub = 82 Codegen.gen_reexport_module ~shared_module_name 83 ~all_merged_docs:sorted_docs doc 84 in 85 let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in 86 let full_path = Filename.concat output_dir rel_path in 87 let oc = open_out full_path in 88 output_string oc stub ; 89 close_out oc ; 90 Printf.printf " Generated: %s -> %s\n" rel_path 91 shared_module_name ) 92 docs ) 93 sccs ; 94 (* generate index file *) 95 let index_path = 96 Filename.concat output_dir (String.lowercase_ascii module_name ^ ".ml") 97 in 98 let oc = open_out index_path in 99 Printf.fprintf oc "(* %s - generated from atproto lexicons *)\n\n" module_name ; 100 (* export each lexicon as a module alias *) 101 List.iter 102 (fun doc -> 103 let flat_module = Naming.flat_module_name_of_nsid doc.Lexicon_types.id in 104 Printf.fprintf oc "module %s = %s\n" flat_module flat_module ) 105 lexicons ; 106 close_out oc ; 107 Printf.printf "Generated index: %s\n" index_path ; 108 (* generate dune file *) 109 let dune_path = Filename.concat output_dir "dune" in 110 let oc = open_out dune_path in 111 Printf.fprintf oc "(library\n" ; 112 Printf.fprintf oc " (name %s)\n" (String.lowercase_ascii module_name) ; 113 Printf.fprintf oc " (libraries hermes yojson lwt)\n" ; 114 Printf.fprintf oc " (preprocess (pps ppx_deriving_yojson)))\n" ; 115 close_out oc ; 116 Printf.printf "Generated dune file\n" ; 117 Printf.printf "Done! Generated %d modules\n" (List.length lexicons) 118 119let inputs = 120 let doc = "lexicon files or directories to search recursively for JSON" in 121 Cmdliner.Arg.(non_empty & pos_all file [] & info [] ~docv:"INPUT" ~doc) 122 123let output_dir = 124 let doc = "output directory for generated code" in 125 Cmdliner.Arg.( 126 required & opt (some string) None & info ["o"; "output"] ~docv:"DIR" ~doc ) 127 128let module_name = 129 let doc = "name of the generated module" in 130 Cmdliner.Arg.( 131 value 132 & opt string "Hermes_lexicons" 133 & info ["m"; "module-name"] ~docv:"NAME" ~doc ) 134 135let generate_cmd = 136 let doc = "generate ocaml types from atproto lexicons" in 137 let info = Cmdliner.Cmd.info "generate" ~doc in 138 let generate' inputs output_dir module_name = 139 generate ~inputs ~output_dir ~module_name 140 in 141 Cmdliner.Cmd.v info 142 Cmdliner.Term.(const generate' $ inputs $ output_dir $ module_name) 143 144let main_cmd = 145 let doc = "hermes - atproto lexicon code generator" in 146 let info = Cmdliner.Cmd.info "hermes-cli" ~version:"0.1.0" ~doc in 147 Cmdliner.Cmd.group info [generate_cmd] 148 149let () = exit (Cmdliner.Cmd.eval main_cmd)