cargo_witgen/
app.rs

1use anyhow::{bail, Context, Result};
2use clap::{Args, Parser, Subcommand};
3use clap_cargo_extra::ClapCargo;
4use heck::ToKebabCase;
5use regex::Regex;
6use std::{
7    collections::HashMap,
8    // fmt::Write,
9    fs::{read, OpenOptions},
10    io::Write,
11    path::{Path, PathBuf},
12};
13use syn::File;
14use witgen_macro_helper::{parse_crate_as_file, Resolver, Wit};
15
16#[derive(Parser, Debug)]
17#[clap(
18    author = "Benjamin Coenen <benjamin.coenen@hotmail.com>, Willem Wyndham <willem@ahalabs.dev>"
19)]
20pub struct App {
21    #[clap(subcommand)]
22    pub command: Command,
23}
24
25#[derive(Debug, Subcommand)]
26pub enum Command {
27    /// Generate wit files
28    #[clap(alias = "gen")]
29    Generate(Witgen),
30}
31
32#[derive(Debug, Args)]
33pub struct Witgen {
34    /// Specify input file to generate wit definitions from
35    #[clap(long, short = 'i')]
36    pub input: Option<PathBuf>,
37
38    /// Specify input directory to generate wit definitions from
39    ///
40    ///
41    /// Will expect library: `<input-dir>/src/lib.rs`
42    #[clap(long, short = 'd', default_value = ".")]
43    pub input_dir: PathBuf,
44
45    /// Specify output file to generate wit definitions
46    #[clap(long, short = 'o', default_value = "index.wit")]
47    pub output: PathBuf,
48
49    /// Specify prefix file to copy into top of the generated wit file
50    #[clap(long, short = 'b')]
51    pub prefix_file: Vec<PathBuf>,
52
53    /// Specify prefix string to copy into top of the generated wit file
54    /// `--prefix-string 'use * from "string.wit"'`
55    #[clap(long, short = 'a')]
56    pub prefix_string: Vec<String>,
57
58    /// Print results to stdout instead file
59    #[clap(long)]
60    pub stdout: bool,
61
62    /// Do not resolve the `use` references in generated wit file to combine into one
63    #[clap(long)]
64    pub skip_resolve: bool,
65
66    /// Skip adding prologue to file
67    #[clap(long)]
68    pub skip_prologue: bool,
69
70    #[clap(flatten)]
71    pub cargo: ClapCargo,
72}
73
74impl Witgen {
75    pub fn from_path(path: &Path) -> Self {
76        Self {
77            input: None,
78            input_dir: path.to_path_buf(),
79            output: PathBuf::from("index.wit"),
80            prefix_file: vec![],
81            prefix_string: vec![],
82            stdout: false,
83            cargo: ClapCargo::default(),
84            skip_resolve: false,
85            skip_prologue: true,
86        }
87    }
88
89    pub fn gen_from_path(path: &Path) -> Result<String> {
90        let witgen = Witgen::from_path(path);
91        witgen.generate_str(witgen.read_input()?)
92    }
93
94    // Part of extra API but current results in unused warning
95    #[allow(dead_code)]
96    pub fn gen_static_from_path(path: &Path) -> Result<String> {
97        let witgen = Witgen::from_path(path);
98        witgen.resolve(&witgen.generate_str(witgen.read_input()?)?)
99    }
100
101    pub fn read_input(&self) -> Result<File> {
102        // TODO: figure out how to avoid the clone()
103        let input = self
104            .input
105            .as_ref()
106            .map_or_else(|| self.input_dir.join("src/lib.rs"), |i| i.clone());
107
108        if !input.exists() {
109            bail!("input {:?} doesn't exist", input);
110        }
111        parse_crate_as_file(&input)
112    }
113
114    pub fn generate_str(&self, file: File) -> Result<String> {
115        let wit: Wit = file.into();
116        let mut wit_str = if self.skip_prologue {
117            String::new()
118        } else {
119            format!("// auto-generated file by witgen (https://github.com/bnjjj/witgen), please do not edit yourself, you can generate a new one thanks to cargo witgen generate command. (cargo-witgen v{}) \n\n", env!("CARGO_PKG_VERSION"))
120        };
121        if !self.prefix_string.is_empty() {
122            wit_str.push_str(&self.prefix_string.join("\n"));
123            wit_str.push('\n');
124        }
125        for path in &self.prefix_file {
126            let prefix_file = String::from_utf8(read(path)?)?;
127            wit_str.push_str(&prefix_file);
128            wit_str.push('\n');
129        }
130        wit_str.push_str(&wit.to_string());
131        Ok(wit_str)
132    }
133
134    pub fn write_output(&self, wit_str: &str) -> Result<()> {
135        if self.stdout {
136            println!("{wit_str}");
137        } else {
138            write_file(&self.output, wit_str)?;
139        }
140        Ok(())
141    }
142
143    pub fn resolve_wit(&self, wit_str: &str) -> Result<HashMap<String, String>> {
144        let mut resolver = WitResolver::new(&self.cargo);
145        let _ = resolver.parse_wit_interface(
146            self.output.to_str().expect("failed to decode output"),
147            wit_str,
148        )?;
149        Ok(resolver.wit_generated)
150    }
151
152    pub fn run(&self) -> Result<()> {
153        let input = self.read_input()?;
154        let mut wit_str = self.generate_str(input)?;
155        if !self.skip_resolve {
156            wit_str = self.resolve(&wit_str)?;
157        }
158        self.write_output(&wit_str)
159    }
160
161    pub fn resolve(&self, wit_str: &str) -> Result<String> {
162        let dep_wit = self
163            .resolve_wit(wit_str)?
164            .into_values()
165            .collect::<Vec<String>>()
166            .join("\n");
167
168        // remove `use` from file since combining
169        let re = Regex::new(r"^use .+\n").unwrap();
170        let mut res = re.replace_all(wit_str, "").to_string();
171        res.push_str(&dep_wit);
172        Ok(res)
173    }
174}
175
176struct WitResolver<'a> {
177    cargo: &'a ClapCargo,
178    wit_generated: HashMap<String, String>,
179}
180
181impl<'a> WitResolver<'a> {
182    fn new(cargo: &'a ClapCargo) -> Self {
183        Self {
184            cargo,
185            wit_generated: Default::default(),
186        }
187    }
188}
189
190impl Resolver for WitResolver<'_> {
191    fn resolve_name(&mut self, name: &str) -> Result<String> {
192        let package = self
193            .cargo
194            .find_package(name)?
195            .or_else(|| {
196                self.cargo
197                    .find_package(&name.to_kebab_case())
198                    .unwrap_or(None)
199            })
200            .map_or_else(|| bail!("Failed to find {name}"), Ok)?;
201
202        let manifest_dir = package.manifest_path.as_std_path().parent().map_or_else(
203            || bail!("failed to find parent of {}", package.manifest_path),
204            Ok,
205        )?;
206
207        let res = Witgen::gen_from_path(manifest_dir)?;
208        self.wit_generated.insert(name.to_string(), res.clone());
209        Ok(res)
210    }
211}
212
213impl Command {
214    pub fn run(&self) -> Result<()> {
215        match self {
216            Command::Generate(witgen) => witgen.run()?,
217        };
218        Ok(())
219    }
220}
221
222impl App {
223    #[allow(dead_code)]
224    pub fn run(&self) -> Result<()> {
225        self.command.run()
226    }
227}
228
229fn write_file(path: &Path, contents: &str) -> Result<()> {
230    let mut file = OpenOptions::new()
231        .write(true)
232        .truncate(true)
233        .create(true)
234        .open(path)
235        .expect("cannot create file to generate wit");
236    file.write_all(contents.as_bytes())
237        .context("cannot write to file")?;
238    file.flush().context("cannot flush file")?;
239    Ok(())
240}