OpenAPI generator for OCaml with Requests/Eio/Jsont
at main 157 lines 5.5 kB view raw
1(** OpenAPI code generator CLI. *) 2 3let setup_logging style_renderer level = 4 Fmt_tty.setup_std_outputs ?style_renderer (); 5 Logs.set_level level; 6 Logs.set_reporter (Logs_fmt.reporter ()) 7 8let read_file path = 9 let ic = open_in path in 10 let n = in_channel_length ic in 11 let s = really_input_string ic n in 12 close_in ic; 13 s 14 15(** Check if a file appears to be YAML based on extension or content *) 16let is_yaml_file path content = 17 let ext = Filename.extension path |> String.lowercase_ascii in 18 ext = ".yaml" || ext = ".yml" || 19 (* Also detect YAML by content if no clear extension *) 20 (ext <> ".json" && String.length content > 0 && 21 (content.[0] = '#' || String.sub content 0 (min 7 (String.length content)) = "openapi")) 22 23(** Parse spec file and run action, handling errors uniformly. 24 Automatically handles both JSON and YAML formats using jsont codecs. *) 25let with_spec spec_path f = 26 let spec_content = read_file spec_path in 27 let result = 28 if is_yaml_file spec_path spec_content then begin 29 Logs.info (fun m -> m "Detected YAML format"); 30 Yamlt.decode_string Openapi.Spec.jsont spec_content 31 end else 32 Openapi.Spec.of_string spec_content 33 in 34 match result with 35 | Error e -> 36 Logs.err (fun m -> m "Failed to parse OpenAPI spec: %s" e); 37 1 38 | Ok spec -> f spec 39 40let generate_cmd spec_path output_dir package_name include_regen_rule = 41 setup_logging None (Some Logs.Info); 42 Logs.info (fun m -> m "Reading OpenAPI spec from %s" spec_path); 43 with_spec spec_path (fun spec -> 44 Logs.info (fun m -> m "Parsed OpenAPI spec: %s v%s" 45 spec.info.title spec.info.version); 46 47 let package_name = Option.value package_name 48 ~default:(Openapi.Codegen.Name.to_snake_case spec.info.title) in 49 50 (* Use spec_path for dune.inc regeneration rule if requested *) 51 let spec_path_for_dune = if include_regen_rule then Some spec_path else None in 52 let config = Openapi.Codegen.{ output_dir; package_name; spec_path = spec_path_for_dune } in 53 let files = Openapi.Codegen.generate ~config spec in 54 55 (try Unix.mkdir output_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 56 Openapi.Codegen.write_files ~output_dir files; 57 58 Logs.info (fun m -> m "Generated %d files in %s" (List.length files) output_dir); 59 List.iter (fun (name, _) -> Logs.info (fun m -> m " - %s" name)) files; 60 0) 61 62let inspect_cmd spec_path = 63 setup_logging None (Some Logs.Info); 64 with_spec spec_path (fun spec -> 65 Fmt.pr "@[<v>"; 66 Fmt.pr "OpenAPI Specification@,"; 67 Fmt.pr "====================@,@,"; 68 Fmt.pr "Title: %s@," spec.info.title; 69 Fmt.pr "Version: %s@," spec.info.version; 70 Option.iter (fun d -> Fmt.pr "Description: %s@," d) spec.info.description; 71 Fmt.pr "@,"; 72 73 Fmt.pr "Servers:@,"; 74 List.iter (fun (s : Openapi.Spec.server) -> 75 Fmt.pr " - %s@," s.url 76 ) spec.servers; 77 Fmt.pr "@,"; 78 79 Fmt.pr "Paths (%d):@," (List.length spec.paths); 80 List.iter (fun (path, _item) -> 81 Fmt.pr " - %s@," path 82 ) spec.paths; 83 Fmt.pr "@,"; 84 85 (match spec.components with 86 | Some c -> 87 Fmt.pr "Schemas (%d):@," (List.length c.schemas); 88 List.iter (fun (name, _) -> 89 Fmt.pr " - %s@," name 90 ) c.schemas 91 | None -> ()); 92 93 Fmt.pr "@]"; 94 0) 95 96(* Cmdliner setup *) 97open Cmdliner 98 99let spec_path = 100 let doc = "Path to the OpenAPI specification file (JSON or YAML)." in 101 Arg.(required & pos 0 (some file) None & info [] ~docv:"SPEC" ~doc) 102 103let output_dir = 104 let doc = "Output directory for generated code." in 105 Arg.(required & opt (some string) None & info ["o"; "output"] ~docv:"DIR" ~doc) 106 107let package_name = 108 let doc = "Package name for generated code (defaults to API title)." in 109 Arg.(value & opt (some string) None & info ["n"; "name"] ~docv:"NAME" ~doc) 110 111let include_regen_rule = 112 let doc = "Include dune.inc regeneration rule with spec path." in 113 Arg.(value & flag & info ["regen"; "include-regen-rule"] ~doc) 114 115let generate_term = 116 Term.(const generate_cmd $ spec_path $ output_dir $ package_name $ include_regen_rule) 117 118let generate_info = 119 let doc = "Generate OCaml code from an OpenAPI specification." in 120 let man = [ 121 `S Manpage.s_description; 122 `P "Generates OCaml types and client code from an OpenAPI 3.x specification."; 123 `P "The generated code uses:"; 124 `I ("$(b,jsont)", "for JSON encoding/decoding"); 125 `I ("$(b,requests)", "for HTTP client (Eio-based)"); 126 `I ("$(b,ptime)", "for date-time handling"); 127 `S Manpage.s_examples; 128 `P "Generate client from local spec:"; 129 `Pre " openapi generate spec.json -o ./client -n my_api"; 130 `P "Generate with regeneration rule for dune:"; 131 `Pre " openapi generate spec.json -o ./client -n my_api --regen"; 132 ] in 133 Cmd.info "generate" ~doc ~man 134 135let inspect_term = 136 Term.(const inspect_cmd $ spec_path) 137 138let inspect_info = 139 let doc = "Inspect an OpenAPI specification." in 140 Cmd.info "inspect" ~doc 141 142let main_info = 143 let doc = "OpenAPI code generator for OCaml." in 144 let man = [ 145 `S Manpage.s_description; 146 `P "Generate OCaml API clients from OpenAPI 3.x specifications."; 147 `P "Use $(b,generate) to create client code, or $(b,inspect) to view spec details."; 148 ] in 149 Cmd.info "openapi" ~version:"0.1.0" ~doc ~man 150 151let main_cmd = 152 Cmd.group main_info [ 153 Cmd.v generate_info generate_term; 154 Cmd.v inspect_info inspect_term; 155 ] 156 157let () = exit (Cmd.eval' main_cmd)