Skip to main content

cargo_spec/
build.rs

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/// The different specification format that cargo-spec can output
13#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum)]
14pub enum OutputFormat {
15    /// Markdown (the default)
16    Markdown,
17
18    /// Respec
19    Respec,
20}
21
22/// Builds the specification and returns a number of files to watch
23pub 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    //~ 1. parse the specification file with the [toml_parser](#toml-parser)
31    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    //~ 2. retrieve the template file
38    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    //~ 3. extract the spec comments from all the files listed using [comment_parser](#comment-parser)
47    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            // TODO: this will panic if we just wrote @ and not @/
56            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    //~ 4. render the template
69    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    //~ 5. build the spec. We currently support two different formats:
86    use OutputFormat::*;
87    match output_format {
88        //~     - [markdown](https://daringfireball.net/projects/markdown/)
89        Markdown => formats::markdown::build(&rendered, output_file),
90        //~     - [respec](https://github.com/w3c/respec/)
91        Respec => {
92            formats::respec::build(&specification, &rendered, output_file);
93        }
94    };
95
96    // return a number of files to watch (useful for the [watch] function)
97    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    // Create a channel to receive the events.
106    let (tx, rx) = channel();
107
108    // Create a watcher object, delivering debounced events.
109    // The notification back-end is selected based on the platform.
110    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        // build and get files to watch
124        match build(toml_spec.clone(), output_file.clone(), output_format) {
125            Err(e) => println!("error: {}", e),
126            Ok(new_files_to_watch) => {
127                // watch any new files contained in the specification
128                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                // unwatch files that are not in the specification
137                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}