objective categorical abstract machine language personal data server
at main 242 lines 8.3 kB view raw
1open Hermes_cli 2 3let dune_file = 4 Printf.sprintf 5 {|(library 6 (name %s) 7 (libraries hermes yojson lwt) 8 (preprocess (pps ppx_deriving_yojson)))|} 9 10let util_file = 11 Printf.sprintf 12 {|let query_string_list_of_yojson = function 13 | `List l -> 14 Ok (List.filter_map (function `String s -> Some s | _ -> None) l) 15 | `String s -> 16 Ok [s] 17 | `Null -> 18 Ok [] 19 | _ -> 20 Error "expected string or string list" 21 22let query_string_list_to_yojson l = `List (List.map (fun s -> `String s) l) 23 24let query_int_list_of_yojson = function 25 | `List l -> 26 Ok (List.filter_map (function `Int i -> Some i | _ -> None) l) 27 | `Int i -> 28 Ok [i] 29 | `Null -> 30 Ok [] 31 | _ -> 32 Error "expected int or int list" 33 34let query_int_list_to_yojson l = `List (List.map (fun i -> `Int i) l) 35 36let query_string_list_option_of_yojson = function 37 | `List l -> 38 Ok (Some (List.filter_map (function `String s -> Some s | _ -> None) l)) 39 | `String s -> 40 Ok (Some [s]) 41 | `Null -> 42 Ok None 43 | _ -> 44 Error "expected string or string list" 45 46let query_string_list_option_to_yojson = function 47 | Some l -> 48 `List (List.map (fun s -> `String s) l) 49 | None -> 50 `Null 51 52let query_int_list_option_of_yojson = function 53 | `List l -> 54 Ok (Some (List.filter_map (function `Int i -> Some i | _ -> None) l)) 55 | `Int i -> 56 Ok (Some [i]) 57 | `Null -> 58 Ok None 59 | _ -> 60 Error "expected int or int list" 61 62let query_int_list_option_to_yojson = function 63 | Some l -> 64 `List (List.map (fun i -> `Int i) l) 65 | None -> 66 `Null|} 67 68(* recursively find all json files in a path (file or directory) *) 69let find_json_files path = 70 let rec aux acc p = 71 if Sys.is_directory p then 72 Sys.readdir p |> Array.to_list 73 |> List.map (Filename.concat p) 74 |> List.fold_left aux acc 75 else if Filename.check_suffix p ".json" then p :: acc 76 else acc 77 in 78 aux [] path 79 80let generate_index lexicons = 81 let nsids = List.map (fun lexicon -> lexicon.Lexicon_types.id) lexicons in 82 let trie = Naming.group_nsids_by_prefix nsids in 83 let rec build_index (trie : Naming.trie) index indent = 84 match trie with 85 | Node children -> 86 List.fold_left 87 (fun acc (key, child) -> 88 match (child : Naming.trie) with 89 | Module nsid -> 90 let module_name = Naming.flat_module_name_of_nsid nsid in 91 acc ^ indent 92 ^ Printf.sprintf "module %s = %s\n" 93 (String.capitalize_ascii key) 94 module_name 95 | Node _ -> 96 acc ^ indent 97 ^ Printf.sprintf "module %s = struct\n" 98 (String.capitalize_ascii key) 99 ^ build_index child index (indent ^ " ") 100 ^ indent ^ "end\n" ) 101 index children 102 | _ -> 103 failwith "build_index called with invalid trie" 104 in 105 build_index (Node trie) "" "" 106 107(* generate module structure from lexicons *) 108let generate ~inputs ~output_dir ~module_name = 109 (* create output directory *) 110 if not (Sys.file_exists output_dir) then Sys.mkdir output_dir 0o755 ; 111 (* find all lexicon files from all inputs *) 112 let files = List.concat_map find_json_files inputs in 113 Printf.printf "Found %d lexicon files\n" (List.length files) ; 114 (* parse all files *) 115 let lexicons = 116 List.filter_map 117 (fun path -> 118 match Parser.parse_file path with 119 | Ok doc -> 120 Printf.printf " Parsed: %s\n" doc.Lexicon_types.id ; 121 Some doc 122 | Error e -> 123 Printf.eprintf " Error parsing %s: %s\n" path e ; 124 None ) 125 files 126 in 127 Printf.printf "Successfully parsed %d lexicons\n" (List.length lexicons) ; 128 (* find file-level SCCs to detect cross-file cycles *) 129 let sccs = Scc.find_file_sccs lexicons in 130 Printf.printf "Found %d file-level SCCs\n" (List.length sccs) ; 131 (* track shared module index for unique naming *) 132 let shared_index = ref 0 in 133 (* generate each SCC *) 134 List.iter 135 (fun scc -> 136 match scc with 137 | [] -> 138 () 139 | [doc] -> 140 (* single file, no cycle - generate normally *) 141 let code = Codegen.gen_lexicon_module doc in 142 let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in 143 let full_path = Filename.concat output_dir rel_path in 144 let oc = open_out full_path in 145 output_string oc code ; 146 close_out oc ; 147 Printf.printf " Generated: %s\n" rel_path 148 | docs -> 149 (* multiple files forming a cycle - use shared module strategy *) 150 incr shared_index ; 151 let nsids = List.map (fun d -> d.Lexicon_types.id) docs in 152 Printf.printf " Cyclic lexicons: %s\n" (String.concat ", " nsids) ; 153 (* sort for consistent ordering *) 154 let sorted_docs = 155 List.sort 156 (fun a b -> String.compare a.Lexicon_types.id b.Lexicon_types.id) 157 docs 158 in 159 (* generate shared module with all types *) 160 let shared_module_name = 161 Naming.shared_module_name nsids !shared_index 162 in 163 let shared_file = Naming.shared_file_name nsids !shared_index in 164 let code = Codegen.gen_shared_module sorted_docs in 165 let full_path = Filename.concat output_dir shared_file in 166 let oc = open_out full_path in 167 output_string oc code ; 168 close_out oc ; 169 Printf.printf " Generated shared: %s\n" shared_file ; 170 (* generate re-export modules for each nsid *) 171 List.iter 172 (fun doc -> 173 let stub = 174 Codegen.gen_reexport_module ~shared_module_name 175 ~all_merged_docs:sorted_docs doc 176 in 177 let rel_path = Naming.file_path_of_nsid doc.Lexicon_types.id in 178 let full_path = Filename.concat output_dir rel_path in 179 let oc = open_out full_path in 180 output_string oc stub ; 181 close_out oc ; 182 Printf.printf " Generated: %s -> %s\n" rel_path 183 shared_module_name ) 184 docs ) 185 sccs ; 186 (* generate index file *) 187 let index_path = 188 Filename.concat output_dir (String.lowercase_ascii module_name ^ ".ml") 189 in 190 let oc = open_out index_path in 191 Printf.fprintf oc "(* %s - generated from atproto lexicons *)\n\n" module_name ; 192 (* export each lexicon as a module alias *) 193 Out_channel.output_string oc (generate_index lexicons) ; 194 close_out oc ; 195 Printf.printf "Generated index: %s\n" index_path ; 196 (* generate dune file *) 197 let dune_path = Filename.concat output_dir "dune" in 198 let oc = open_out dune_path in 199 Out_channel.output_string oc (dune_file (String.lowercase_ascii module_name)) ; 200 close_out oc ; 201 Printf.printf "Generated dune file\n" ; 202 (* generate util file *) 203 let util_path = Filename.concat output_dir "hermes_util.ml" in 204 let oc = open_out util_path in 205 Printf.fprintf oc "(* %s - generated from atproto lexicons *)\n\n" module_name ; 206 (* export each lexicon as a module alias *) 207 Out_channel.output_string oc util_file ; 208 close_out oc ; 209 Printf.printf "Generated util file: %s\n" util_path ; 210 Printf.printf "Done! Generated %d modules\n" (List.length lexicons) 211 212let inputs = 213 let doc = "lexicon files or directories to search recursively for JSON" in 214 Cmdliner.Arg.(non_empty & pos_all file [] & info [] ~docv:"INPUT" ~doc) 215 216let output_dir = 217 let doc = "output directory for generated code" in 218 Cmdliner.Arg.( 219 required & opt (some string) None & info ["o"; "output"] ~docv:"DIR" ~doc ) 220 221let module_name = 222 let doc = "name of the generated module" in 223 Cmdliner.Arg.( 224 value 225 & opt string "Hermes_lexicons" 226 & info ["m"; "module-name"] ~docv:"NAME" ~doc ) 227 228let generate_cmd = 229 let doc = "generate ocaml types from atproto lexicons" in 230 let info = Cmdliner.Cmd.info "generate" ~doc in 231 let generate' inputs output_dir module_name = 232 generate ~inputs ~output_dir ~module_name 233 in 234 Cmdliner.Cmd.v info 235 Cmdliner.Term.(const generate' $ inputs $ output_dir $ module_name) 236 237let main_cmd = 238 let doc = "hermes - atproto lexicon code generator" in 239 let info = Cmdliner.Cmd.info "hermes-cli" ~version:"0.1.0" ~doc in 240 Cmdliner.Cmd.group info [generate_cmd] 241 242let () = exit (Cmdliner.Cmd.eval main_cmd)