OpenAPI generator for OCaml with Requests/Eio/Jsont
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)