Skip to content

Commit d4cb8ab

Browse files
authored
feat(graindoc): Allow directory input & output (#1263)
chore(docs): Generate or update markdown for entire stdlib
1 parent 4637667 commit d4cb8ab

35 files changed

+4016
-151
lines changed

compiler/graindoc/graindoc.re

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ open Compile;
44
open Grain_typed;
55
open Grain_utils;
66
open Grain_diagnostics;
7-
open Filename;
7+
open Grain_utils.Filepath.Args;
88

99
let () =
1010
Printexc.register_printer(exc =>
@@ -28,13 +28,17 @@ let () =
2828
);
2929

3030
[@deriving cmdliner]
31-
type params = {
31+
type io_params = {
3232
/** Grain source file for which to extract documentation */
3333
[@pos 0] [@docv "FILE"]
34-
input: Filepath.Args.ExistingFile.t,
34+
input: ExistingFileOrDirectory.t,
3535
/** Output filename */
3636
[@name "o"] [@docv "FILE"]
37-
output: option(Filepath.Args.MaybeExistingFile.t),
37+
output: option(MaybeExistingFileOrDirectory.t),
38+
};
39+
40+
[@deriving cmdliner]
41+
type params = {
3842
/**
3943
The version to use as current when generating markdown for `@since` and `@history` attributes.
4044
Any future versions will be replace with `next` in the output.
@@ -43,10 +47,10 @@ type params = {
4347
current_version: option(string),
4448
};
4549

46-
let compile_typed = (opts: params) => {
47-
let input = Filepath.to_string(opts.input);
48-
49-
switch (Compile.compile_file(~hook=stop_after_typed, input)) {
50+
let compile_typed = (input: Fp.t(Fp.absolute)) => {
51+
switch (
52+
Compile.compile_file(~hook=stop_after_typed, Filepath.to_string(input))
53+
) {
5054
| exception exn =>
5155
let bt =
5256
if (Printexc.backtrace_status()) {
@@ -71,7 +75,7 @@ let compile_typed = (opts: params) => {
7175
};
7276

7377
let generate_docs =
74-
({current_version, output}: params, program: Typedtree.typed_program) => {
78+
(~current_version, ~output=?, program: Typedtree.typed_program) => {
7579
let comments = Comments.to_ordered(program.comments);
7680

7781
let env = program.env;
@@ -205,15 +209,12 @@ let generate_docs =
205209

206210
let contents = Buffer.to_bytes(buf);
207211
switch (output) {
208-
| Some(NotExists(outfile)) =>
209-
Fs_access.ensure_parent_directory_exists(Filepath.to_string(outfile))
210-
| _ => ()
211-
};
212-
213-
switch (output) {
214-
| Some(Exists(outfile))
215-
| Some(NotExists(outfile)) =>
216-
let oc = Fs_access.open_file_for_writing(Filepath.to_string(outfile));
212+
| Some(outfile) =>
213+
let outfile = Filepath.to_string(outfile);
214+
// TODO: This crashes if you do something weird like `-o stdout/map.gr/foo`
215+
// because `foo` doesn't exist so it tries to mkdir it and raises
216+
Fs_access.ensure_parent_directory_exists(outfile);
217+
let oc = Fs_access.open_file_for_writing(outfile);
217218
output_bytes(oc, contents);
218219
close_out(oc);
219220
| None => print_bytes(contents)
@@ -222,13 +223,87 @@ let generate_docs =
222223
();
223224
};
224225

225-
let graindoc = opts => {
226-
let program = compile_typed(opts);
227-
try(generate_docs(opts, program)) {
228-
| exn =>
229-
Format.eprintf("@[%s@]@.", Printexc.to_string(exn));
230-
exit(2);
226+
type run = {
227+
input_path: Fp.t(Fp.absolute),
228+
output_path: option(Fp.t(Fp.absolute)),
229+
};
230+
231+
let enumerate_directory = (input_dir_path, output_dir_path) => {
232+
let all_files = Array.to_list(Fs_access.readdir(input_dir_path));
233+
let grain_files =
234+
List.filter(
235+
filepath => Filename.extension(Fp.toString(filepath)) == ".gr",
236+
all_files,
237+
);
238+
List.map(
239+
filepath => {
240+
// We relativize between the input directory and the full filepath
241+
// such that we can reconstruct the directory structure of the input directory
242+
let relative_path =
243+
Fp.relativizeExn(~source=input_dir_path, ~dest=filepath);
244+
let gr_basename = Option.get(Fp.baseName(relative_path));
245+
let md_basename =
246+
Filepath.String.remove_extension(gr_basename) ++ ".md";
247+
let dirname = Fp.dirName(relative_path);
248+
let md_relative_path = Fp.join(dirname, Fp.relativeExn(md_basename));
249+
let output_path = Fp.join(output_dir_path, md_relative_path);
250+
{input_path: filepath, output_path: Some(output_path)};
251+
},
252+
grain_files,
253+
);
254+
};
255+
256+
let enumerate_runs = opts =>
257+
switch (opts.input, opts.output) {
258+
| (File(input_file_path), None) =>
259+
`Ok([{input_path: input_file_path, output_path: None}])
260+
| (File(input_file_path), Some(Exists(File(output_file_path)))) =>
261+
`Ok([
262+
{input_path: input_file_path, output_path: Some(output_file_path)},
263+
])
264+
| (File(input_file_path), Some(NotExists(output_file_path))) =>
265+
`Ok([
266+
{input_path: input_file_path, output_path: Some(output_file_path)},
267+
])
268+
| (Directory(_), None) =>
269+
`Error((
270+
false,
271+
"Directory input must be used with `-o` flag to specify output directory",
272+
))
273+
| (Directory(input_dir_path), Some(Exists(Directory(output_dir_path)))) =>
274+
`Ok(enumerate_directory(input_dir_path, output_dir_path))
275+
| (Directory(input_dir_path), Some(NotExists(output_dir_path))) =>
276+
`Ok(enumerate_directory(input_dir_path, output_dir_path))
277+
| (File(input_file_path), Some(Exists(Directory(output_dir_path)))) =>
278+
`Error((
279+
false,
280+
"Using a file as input cannot be combined with directory output",
281+
))
282+
| (Directory(_), Some(Exists(File(_)))) =>
283+
`Error((
284+
false,
285+
"Using a directory as input cannot be written as a single file output",
286+
))
231287
};
288+
289+
let graindoc = (opts, runs) => {
290+
List.iter(
291+
({input_path, output_path}) => {
292+
let program = compile_typed(input_path);
293+
try(
294+
generate_docs(
295+
~current_version=opts.current_version,
296+
~output=?output_path,
297+
program,
298+
)
299+
) {
300+
| exn =>
301+
Format.eprintf("@[%s@]@.", Printexc.to_string(exn));
302+
exit(2);
303+
};
304+
},
305+
runs,
306+
);
232307
};
233308

234309
let cmd = {
@@ -243,7 +318,9 @@ let cmd = {
243318

244319
Cmd.v(
245320
Cmd.info(Sys.argv[0], ~version, ~doc),
246-
Grain_utils.Config.with_cli_options(graindoc) $ params_cmdliner_term(),
321+
Grain_utils.Config.with_cli_options(graindoc)
322+
$ params_cmdliner_term()
323+
$ ret(const(enumerate_runs) $ io_params_cmdliner_term()),
247324
);
248325
};
249326

compiler/src/utils/filepath.re

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,64 @@ module Args = {
184184
let cmdliner_converter = (prsr, prntr);
185185
};
186186

187+
module ExistingFileOrDirectory = {
188+
type t =
189+
| File(Fp.t(Fp.absolute))
190+
| Directory(Fp.t(Fp.absolute));
191+
192+
type err =
193+
| InvalidPath(string)
194+
| NotExists(Fp.t(Fp.absolute))
195+
| InvalidFileType(Fp.t(Fp.absolute));
196+
197+
let query = fname => {
198+
switch (from_string(fname)) {
199+
| Some(path) =>
200+
let abs_path = derelativize(path);
201+
switch (Fs.query(abs_path)) {
202+
| Some(File(path, _stat)) => Ok(File(path))
203+
| Some(Dir(path, _)) => Ok(Directory(path))
204+
| Some(Other(path, _, _)) => Error(InvalidFileType(path))
205+
| Some(Link(_, realpath, _)) =>
206+
Error(InvalidFileType(derelativize(realpath)))
207+
| None => Error(NotExists(abs_path))
208+
};
209+
| None => Error(InvalidPath(fname))
210+
};
211+
};
212+
213+
let prsr = fname => {
214+
switch (query(fname)) {
215+
| Ok(file) => `Ok(file)
216+
| Error(InvalidFileType(path)) =>
217+
`Error(
218+
Format.sprintf(
219+
"%s exists but is not a file or directory",
220+
to_string(path),
221+
),
222+
)
223+
| Error(NotExists(path)) =>
224+
`Error(Format.sprintf("%s does not exist", to_string(path)))
225+
| Error(InvalidPath(fname)) =>
226+
`Error(Format.sprintf("Invalid path: %s", fname))
227+
};
228+
};
229+
230+
let prntr = (formatter, value) => {
231+
switch (value) {
232+
| File(path) => Format.fprintf(formatter, "File: %s", to_string(path))
233+
| Directory(path) =>
234+
Format.fprintf(formatter, "Directory: %s", to_string(path))
235+
};
236+
};
237+
238+
let cmdliner_converter = (prsr, prntr);
239+
};
240+
187241
module MaybeExistingFile = {
188242
type t =
189-
| Exists(Fp.t(Fp.absolute))
190-
| NotExists(Fp.t(Fp.absolute));
243+
| Exists(ExistingFile.t)
244+
| NotExists(ExistingFile.t);
191245

192246
let prsr = fname => {
193247
switch (ExistingFile.query(fname)) {
@@ -213,4 +267,39 @@ module Args = {
213267

214268
let cmdliner_converter = (prsr, prntr);
215269
};
270+
271+
module MaybeExistingFileOrDirectory = {
272+
type t =
273+
| Exists(ExistingFileOrDirectory.t)
274+
| NotExists(Fp.t(Fp.absolute));
275+
276+
let prsr = fname => {
277+
switch (ExistingFileOrDirectory.query(fname)) {
278+
| Ok(path) => `Ok(Exists(path))
279+
| Error(NotExists(path)) => `Ok(NotExists(path))
280+
| Error(InvalidFileType(path)) =>
281+
`Error(
282+
Format.sprintf(
283+
"%s exists but is not a file or directory",
284+
to_string(path),
285+
),
286+
)
287+
| Error(InvalidPath(fname)) =>
288+
`Error(Format.sprintf("Invalid path: %s", fname))
289+
};
290+
};
291+
292+
let prntr = (formatter, value) => {
293+
switch (value) {
294+
| Exists(File(path)) =>
295+
Format.fprintf(formatter, "File: %s", to_string(path))
296+
| Exists(Directory(path)) =>
297+
Format.fprintf(formatter, "Directory: %s", to_string(path))
298+
| NotExists(path) =>
299+
Format.fprintf(formatter, "Path: %s", to_string(path))
300+
};
301+
};
302+
303+
let cmdliner_converter = (prsr, prntr);
304+
};
216305
};

0 commit comments

Comments
 (0)