use std::io;
use std::path::Path;
#[derive(Debug, Default)]
pub struct InitReport {
pub config_written: bool,
pub samples_written: Vec<String>,
pub samples_skipped: Vec<String>,
}
const SAMPLES: &[(&str, &str)] = &[
("examples/notes/ownership.md", include_str!("../examples/notes/ownership.md")),
("examples/notes/borrowing.md", include_str!("../examples/notes/borrowing.md")),
("examples/notes/async.md", include_str!("../examples/notes/async.md")),
("examples/notes/error-handling.md", include_str!("../examples/notes/error-handling.md")),
];
const CONFIG_TEMPLATE: &str = include_str!("../kibble.toml.example");
pub fn run_init(root: &Path, force: bool) -> io::Result<InitReport> {
let mut report = InitReport::default();
let cfg = root.join("kibble.toml");
if cfg.exists() && !force {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"kibble.toml already exists (use --force to overwrite)",
));
}
std::fs::write(&cfg, CONFIG_TEMPLATE)?;
report.config_written = true;
for (rel, contents) in SAMPLES {
let dest = root.join(rel);
if dest.exists() {
report.samples_skipped.push((*rel).to_string());
continue;
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, contents)?;
report.samples_written.push((*rel).to_string());
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("kibble_init_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn init_writes_config_and_samples() {
let d = tmp("fresh");
let r = run_init(&d, false).unwrap();
assert!(r.config_written);
assert_eq!(r.samples_written.len(), 4);
assert!(d.join("kibble.toml").is_file());
assert!(d.join("examples/notes/ownership.md").is_file());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn init_refuses_existing_config_without_force() {
let d = tmp("exists");
std::fs::write(d.join("kibble.toml"), "keep me").unwrap();
let err = run_init(&d, false).unwrap_err();
assert!(err.to_string().contains("already exists"), "got: {err}");
assert_eq!(std::fs::read_to_string(d.join("kibble.toml")).unwrap(), "keep me");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn init_force_overwrites_config() {
let d = tmp("force");
std::fs::write(d.join("kibble.toml"), "old").unwrap();
let r = run_init(&d, true).unwrap();
assert!(r.config_written);
assert_ne!(std::fs::read_to_string(d.join("kibble.toml")).unwrap(), "old");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn init_skips_existing_sample_files() {
let d = tmp("skip");
std::fs::create_dir_all(d.join("examples/notes")).unwrap();
std::fs::write(d.join("examples/notes/ownership.md"), "mine").unwrap();
let r = run_init(&d, false).unwrap();
assert!(r.samples_skipped.iter().any(|s| s.contains("ownership.md")));
assert_eq!(std::fs::read_to_string(d.join("examples/notes/ownership.md")).unwrap(), "mine");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn embedded_samples_are_nonempty_and_on_topic() {
for (rel, embedded) in SAMPLES {
assert!(embedded.len() > 40, "{rel} sample is suspiciously short");
let topic = std::path::Path::new(rel).file_stem().unwrap().to_str().unwrap();
let keyword = topic.split('-').next().unwrap(); assert!(
embedded.to_lowercase().contains(keyword),
"{rel} should mention its topic {keyword:?} so search finds it"
);
}
}
}