1use clap::ArgEnum;
2use miette::{IntoDiagnostic, Result, WrapErr};
3use std::{
4 collections::HashSet,
5 fs::{self},
6 path::PathBuf,
7};
8use tinytemplate::TinyTemplate;
9
10use crate::{comment_parser, errors::SpecError, formats, git::get_local_repo_path, toml_parser};
11
12#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum)]
14pub enum OutputFormat {
15 Markdown,
17
18 Respec,
20}
21
22pub fn build(
24 toml_spec: PathBuf,
25 output_file: Option<PathBuf>,
26 output_format: OutputFormat,
27) -> Result<HashSet<PathBuf>> {
28 let mut files_to_watch = HashSet::new();
29
30 let mut specification = toml_parser::parse_toml_spec(toml_spec.as_path())?;
32
33 let mut spec_dir =
34 fs::canonicalize(&toml_spec).expect("couldn't canonicalize the specification path");
35 spec_dir.pop();
36
37 let mut template_path = spec_dir.clone();
39 template_path.push(&specification.config.template);
40 files_to_watch.insert(template_path.clone());
41
42 let template = fs::read_to_string(&template_path)
43 .into_diagnostic()
44 .wrap_err_with(|| format!("could not read template {}", template_path.display(),))?;
45
46 let base = get_local_repo_path();
48 for filename in specification.sections.values_mut() {
49 let path = if matches!(filename.chars().next(), Some('@')) {
50 let base = base
51 .as_ref()
52 .ok_or(SpecError::NotGitRepo(filename.clone()))
53 .into_diagnostic()?;
54 let base = base.trim();
55 let filename = filename.split_at(2).1.to_string();
57 PathBuf::from(base).join(filename)
58 } else {
59 let mut path = spec_dir.clone();
60 path.push(&filename);
61 path
62 };
63 files_to_watch.insert(path.clone());
64
65 *filename = comment_parser::parse_file(&path)?;
66 }
67
68 let mut tt = TinyTemplate::new();
70 tt.set_default_formatter(&tinytemplate::format_unescaped);
71 tt.add_template("specification", &template)
72 .into_diagnostic()
73 .wrap_err_with(|| format!("can't parse template {}", template_path.display(),))?;
74
75 let rendered = tt
76 .render("specification", &specification)
77 .into_diagnostic()
78 .wrap_err_with(|| {
79 format!(
80 "template file can't be rendered: {}",
81 template_path.display()
82 )
83 })?;
84
85 use OutputFormat::*;
87 match output_format {
88 Markdown => formats::markdown::build(&rendered, output_file),
90 Respec => {
92 formats::respec::build(&specification, &rendered, output_file);
93 }
94 };
95
96 Ok(files_to_watch)
98}
99
100pub fn watch(toml_spec: PathBuf, output_format: OutputFormat, output_file: Option<PathBuf>) {
101 use notify::{watcher, RecursiveMode, Watcher};
102 use std::sync::mpsc::channel;
103 use std::time::Duration;
104
105 let (tx, rx) = channel();
107
108 let mut watcher = watcher(tx, Duration::from_secs(10)).unwrap();
111 watcher
112 .watch(toml_spec.clone(), RecursiveMode::NonRecursive)
113 .unwrap_or_else(|_e| {
114 panic!(
115 "could not watch specification file: {}",
116 toml_spec.display()
117 )
118 });
119
120 let mut files_to_watch = HashSet::new();
121
122 loop {
123 match build(toml_spec.clone(), output_file.clone(), output_format) {
125 Err(e) => println!("error: {}", e),
126 Ok(new_files_to_watch) => {
127 for file in new_files_to_watch.difference(&files_to_watch) {
129 watcher
130 .watch(&file, RecursiveMode::NonRecursive)
131 .unwrap_or_else(|_e| {
132 panic!("could not find file to watch {}", file.display())
133 });
134 }
135
136 for file in files_to_watch.difference(&new_files_to_watch) {
138 watcher.unwatch(&file).unwrap_or_else(|_e| {
139 panic!("could not find file to watch {}", file.display())
140 });
141 }
142
143 files_to_watch = new_files_to_watch;
144 }
145 };
146
147 match rx.recv() {
148 Ok(event) => println!("{:?}", event),
149 Err(e) => panic!("watch error: {:?}", e),
150 }
151 }
152}