use std::{fs, io, mem};
use camino::Utf8PathBuf;
use thiserror::Error as ThisError;
use crate::{
facts::Facts,
preflight::{self, RawMain},
};
const MAIN_TOML_FILE: &str = "main.toml";
#[derive(Debug, ThisError)]
pub(crate) enum Error {
#[error("valid config file not found")]
ConfigNotFound,
#[error("expected an absolute path, received a relative path '{0}'")]
RelativePath(Utf8PathBuf),
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
#[error(transparent)]
Preflight {
#[from]
source: preflight::Error,
},
}
type Result<T> = std::result::Result<T, Error>;
pub(crate) fn find_config_file(facts: &Facts) -> Result<Utf8PathBuf> {
let config_paths = [
facts
.config_dir
.join(env!("CARGO_PKG_NAME"))
.join(MAIN_TOML_FILE),
facts
.home_dir
.join(".dotfiles")
.join(env!("CARGO_PKG_NAME"))
.join(MAIN_TOML_FILE),
];
for config_path in config_paths {
if config_path.exists() {
return Ok(config_path);
}
}
Err(Error::ConfigNotFound)
}
fn read_config_toml(src: Utf8PathBuf, visited: &mut Vec<Utf8PathBuf>) -> Result<RawMain> {
if !src.is_absolute() {
return Err(Error::RelativePath(src));
}
let src = src.canonicalize_utf8()?;
visited.push(src.clone());
let text = fs::read_to_string(&src)?;
let mut config = RawMain::try_from(text.as_str())?;
let cwd = src
.parent()
.expect("cannot determine parent directory for {src}");
let mut includes = vec![];
mem::swap(&mut includes, &mut config.includes);
for include in includes {
let next_src = if include.src.is_absolute() {
include.src.clone()
} else {
cwd.join(include.src)
};
if visited.contains(&next_src) {
break;
}
let mut next_cfg = read_config_toml(next_src, visited)?;
config.jobs.append(&mut next_cfg.jobs);
}
Ok(config)
}
pub(crate) fn read_main_toml(src: Utf8PathBuf) -> Result<RawMain> {
let src = if src.is_absolute() {
src
} else {
crate::env::current_dir().join(src)
};
let mut visited = Vec::new();
read_config_toml(src, &mut visited)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use toml::value::Value;
use crate::preflight::RawJob;
use super::*;
#[test]
fn read_main_toml_with_no_includes() -> Result<()> {
let src = crate::env::current_dir()
.join("tests")
.join("fixtures")
.join("config_no_includes.toml");
let want = RawMain {
jobs: vec![RawJob {
name: String::from("config_no_includes"),
needs: vec![],
spec: HashMap::from([(String::from("type"), Value::from("fake"))]),
}],
..Default::default()
};
let got = read_main_toml(src)?;
assert_eq!(got, want);
Ok(())
}
#[test]
fn read_main_toml_with_includes_not_found() {
let src = crate::env::current_dir()
.join("tests")
.join("fixtures")
.join("config_not_found.toml");
let got = read_main_toml(src);
assert!(got.is_err());
assert!(matches!(got.unwrap_err(), Error::Io { .. }));
}
#[test]
fn read_main_toml_with_deep_includes() -> Result<()> {
let src = crate::env::current_dir()
.join("tests")
.join("fixtures")
.join("config_deep_includes.toml");
let want = RawMain {
jobs: vec![
RawJob {
name: String::from("config_deep_includes"),
needs: vec![],
spec: HashMap::from([(String::from("type"), Value::from("fake"))]),
},
RawJob {
name: String::from("config_includes"),
needs: vec![],
spec: HashMap::from([(String::from("type"), Value::from("fake"))]),
},
RawJob {
name: String::from("config_no_includes"),
needs: vec![],
spec: HashMap::from([(String::from("type"), Value::from("fake"))]),
},
],
..Default::default()
};
let got = read_main_toml(src)?;
assert_eq!(got, want);
Ok(())
}
#[test]
fn read_main_toml_with_recursive_includes() -> Result<()> {
let src = crate::env::current_dir()
.join("tests")
.join("fixtures")
.join("config_recursive_includes.toml");
let want = RawMain {
jobs: vec![RawJob {
name: String::from("config_recursive_includes"),
needs: vec![],
spec: HashMap::from([(String::from("type"), Value::from("fake"))]),
}],
..Default::default()
};
let got = read_main_toml(src)?;
assert_eq!(got, want);
Ok(())
}
}