use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use crate::templates;
pub const DIR: &str = "runtime";
pub fn is_present(root: &Path) -> bool {
root.join(DIR).join("Cargo.toml").is_file()
}
pub fn require(root: &Path) -> Result<()> {
if !is_present(root) {
bail!(
"{DIR}/ not found: this project was created before slave 0.9; copy runtime/ from a \
new project (`slave new`) and add your apps to runtime/src/apps.rs"
);
}
Ok(())
}
pub fn add_app(root: &Path, name: &str, crate_name: &str) -> Result<()> {
let manifest_path = root.join(DIR).join("Cargo.toml");
let manifest = fs::read_to_string(&manifest_path)
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
let apps_path = root.join(DIR).join("src/apps.rs");
let apps = fs::read_to_string(&apps_path)
.with_context(|| format!("failed to read {}", apps_path.display()))?;
let apps = mount_line(&apps, name, crate_name)?;
let manifest = format!(
"{}\n{name} = {{ path = \"../apps/{name}\" }}\n",
manifest.trim_end()
);
fs::write(&apps_path, apps).with_context(|| format!("failed to update {DIR}/src/apps.rs"))?;
fs::write(&manifest_path, manifest)
.with_context(|| format!("failed to update {DIR}/Cargo.toml"))?;
println!(" mounted {name} in the runtime ({DIR}/src/apps.rs)");
Ok(())
}
pub fn remove_app(root: &Path, name: &str) -> Result<()> {
let manifest_path = root.join(DIR).join("Cargo.toml");
let manifest = fs::read_to_string(&manifest_path)
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
let apps_path = root.join(DIR).join("src/apps.rs");
let apps = fs::read_to_string(&apps_path)
.with_context(|| format!("failed to read {}", apps_path.display()))?;
let dependency = format!("{name} = {{ path = \"../apps/{name}\" }}");
let mount = format!("apps.app(\"{name}\",");
let keep = |text: &str, drop: &dyn Fn(&str) -> bool| {
text.lines()
.filter(|line| !drop(line.trim()))
.collect::<Vec<_>>()
.join("\n")
+ "\n"
};
fs::write(&manifest_path, keep(&manifest, &|line| line == dependency))
.with_context(|| format!("failed to update {DIR}/Cargo.toml"))?;
fs::write(&apps_path, keep(&apps, &|line| line.starts_with(&mount)))
.with_context(|| format!("failed to update {DIR}/src/apps.rs"))?;
println!(" removed {name} from the runtime ({DIR}/src/apps.rs)");
Ok(())
}
pub fn has_app(root: &Path, name: &str) -> bool {
fs::read_to_string(root.join(DIR).join("src/apps.rs"))
.is_ok_and(|apps| apps.contains(&format!("apps.app(\"{name}\",")))
}
fn mount_line(apps: &str, name: &str, crate_name: &str) -> Result<String> {
let Some(marker) = apps.find(templates::APPS_MARKER) else {
bail!(
"{DIR}/src/apps.rs has no `{}` line; add `apps.app(\"{name}\", {crate_name}::embedded)?;` yourself",
templates::APPS_MARKER
);
};
let line_start = apps[..marker].rfind('\n').map_or(0, |i| i + 1);
let indent = &apps[line_start..marker];
let line = format!("{indent}apps.app(\"{name}\", {crate_name}::embedded)?;\n");
Ok(format!(
"{}{line}{}",
&apps[..line_start],
&apps[line_start..]
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mounts_apps_above_the_marker() {
let apps =
"pub fn mount(apps: &mut Mount) -> Result<()> {\n // slave:apps\n Ok(())\n}\n";
let apps = mount_line(apps, "shop-front", "shop_front").unwrap();
assert!(
apps.contains(
" apps.app(\"shop-front\", shop_front::embedded)?;\n // slave:apps\n"
),
"{apps}"
);
assert!(mount_line("fn mount() {}", "a", "a").is_err());
}
}