Skip to main content

standard_plugin_cli/
project.rs

1//! Finding plugins on disk: a plugin directory holds a crate and its
2//! manifest (`bundle/standard-plugin.json`, or `standard-plugin.json` beside
3//! `Cargo.toml`); `bundle/` receives what `build` makes.
4
5use std::path::{Path, PathBuf};
6
7use standard_plugin_manifest::{MANIFEST_MAX_BYTES, Manifest};
8
9/// One plugin crate.
10#[derive(Clone, Debug)]
11pub struct Plugin {
12    pub dir: PathBuf,
13    /// Where the manifest source lives.
14    pub manifest_path: PathBuf,
15    /// Where the bundle is assembled.
16    pub bundle: PathBuf,
17}
18
19impl Plugin {
20    fn at(dir: &Path) -> Option<Self> {
21        let bundle = dir.join("bundle");
22        [
23            bundle.join("standard-plugin.json"),
24            dir.join("standard-plugin.json"),
25        ]
26        .into_iter()
27        .find(|path| path.is_file())
28        .map(|manifest_path| Self {
29            dir: dir.to_path_buf(),
30            manifest_path,
31            bundle,
32        })
33    }
34
35    pub fn manifest_bytes(&self) -> Result<Vec<u8>, String> {
36        let bytes = std::fs::read(&self.manifest_path)
37            .map_err(|error| format!("{}: {error}", self.manifest_path.display()))?;
38        if bytes.len() as u64 > MANIFEST_MAX_BYTES {
39            return Err(format!(
40                "{} is larger than {MANIFEST_MAX_BYTES} bytes",
41                self.manifest_path.display()
42            ));
43        }
44        Ok(bytes)
45    }
46
47    pub fn manifest(&self) -> Result<Manifest, String> {
48        Manifest::parse(&self.manifest_bytes()?)
49            .map_err(|error| format!("{}: {error}", self.manifest_path.display()))
50    }
51
52    /// Copies a manifest kept beside `Cargo.toml` into the bundle.
53    pub fn stage_manifest(&self) -> std::io::Result<()> {
54        std::fs::create_dir_all(&self.bundle)?;
55        let staged = self.bundle.join("standard-plugin.json");
56        if self.manifest_path != staged {
57            std::fs::copy(&self.manifest_path, staged)?;
58        }
59        Ok(())
60    }
61}
62
63/// The plugins in `dir`: `dir` itself, or else its immediate
64/// subdirectories that are plugins (a companion pair's `ui/` and
65/// `companion/`).
66pub fn plugins(dir: &Path) -> Result<Vec<Plugin>, String> {
67    if let Some(plugin) = Plugin::at(dir) {
68        return Ok(vec![plugin]);
69    }
70    let mut found: Vec<Plugin> = std::fs::read_dir(dir)
71        .map_err(|error| format!("{}: {error}", dir.display()))?
72        .filter_map(Result::ok)
73        .map(|entry| entry.path())
74        .filter(|path| path.is_dir())
75        .filter_map(|path| Plugin::at(&path))
76        .collect();
77    found.sort_by(|a, b| a.dir.cmp(&b.dir));
78    if found.is_empty() {
79        return Err(format!(
80            "no plugin in {}: expected bundle/standard-plugin.json or standard-plugin.json",
81            dir.display()
82        ));
83    }
84    Ok(found)
85}