use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::{defaults, profile, syntax};
pub const SPEC_FILES: &[&str] = &[SPEC_FILE, "isoloom.yaml"];
pub const SPEC_FILE: &str = "isoloom.yml";
pub fn parse(yaml: &str) -> Result<syntax::Spec> {
parse_file(yaml, SPEC_FILE)
}
fn parse_file(yaml: &str, file: &str) -> Result<syntax::Spec> {
syntax::parse(yaml).map_err(|problems| Error::Invalid {
file: file.to_owned(),
problems,
})
}
pub fn find(dir: &Path) -> Result<PathBuf> {
let found: Vec<PathBuf> = SPEC_FILES.iter().map(|f| dir.join(f)).filter(|p| p.is_file()).collect();
match found.as_slice() {
[one] => Ok(one.clone()),
[] => Err(Error::NotFound(dir.to_path_buf())),
_ => Err(Error::Ambiguous(dir.to_path_buf())),
}
}
pub fn load(dir: &Path) -> Result<syntax::Spec> {
load_with(dir, &[])
}
pub fn load_with(dir: &Path, sets: &[String]) -> Result<syntax::Spec> {
load_profiled(dir, None, sets)
}
pub fn load_profiled(dir: &Path, profile: Option<&profile::Profile>, sets: &[String]) -> Result<syntax::Spec> {
let path = find(dir)?;
let file = path.file_name().map_or_else(|| SPEC_FILE.to_owned(), |f| f.to_string_lossy().into_owned());
let text = std::fs::read_to_string(&path).map_err(|source| Error::Read { path, source })?;
let text = match profile {
Some(p) => {
let doc: serde_yaml_ng::Value = serde_yaml_ng::from_str(&text)?;
let problems = p.check(&doc);
if !problems.is_empty() {
return Err(Error::ProfileMismatch { problems });
}
defaults::override_spec_with(&text, Some(&p.spec), sets)
}
None => defaults::override_spec(&text, sets),
}?;
parse_file(&text, &file)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
const SPEC: &str = "version: 1\nname: shop\nnetworks: { lab: { cidr: 10.1.0.0/24 } }\nmachines:\n web: { os: debian@12, networks: { lab: 10 } }\n";
fn folder(name: &str, files: &[(&str, &str)]) -> PathBuf {
let dir = crate::testing::folder(&format!("load-{name}")).unwrap();
for (file, text) in files {
std::fs::write(dir.join(file), text).unwrap();
}
dir
}
#[test]
fn text_reads_into_the_spec_or_its_problems_under_the_spec_files_name() {
assert_eq!(parse(SPEC).unwrap().name, "shop");
match parse("version: 1\nname: shop\nmachines:\n web: { docker: { image: x } }\n") {
Err(Error::Invalid { file, problems }) => assert_eq!((file.as_str(), problems.len()), ("isoloom.yml", 1)),
other => panic!("{other:?}"),
}
}
#[test]
fn a_folder_has_one_spec_file() {
let none = folder("none", &[]);
assert!(matches!(find(&none), Err(Error::NotFound(d)) if d == none));
let yaml = folder("yaml", &[("isoloom.yaml", SPEC)]);
assert_eq!(find(&yaml).unwrap(), yaml.join("isoloom.yaml"));
let both = folder("both", &[("isoloom.yml", SPEC), ("isoloom.yaml", SPEC)]);
assert!(matches!(find(&both), Err(Error::Ambiguous(d)) if d == both));
}
#[test]
fn a_folder_reads_into_its_spec_and_overrides_apply_first() {
let dir = folder("overrides", &[("isoloom.yml", SPEC)]);
assert_eq!(load(&dir).unwrap().machines["web"].os.map(|os| os.to_string()), Some("debian@12".to_owned()));
let set = load_with(&dir, &["machines.web.os=alpine".to_owned(), "defaults.cloud.aws.region=x".to_owned()]).unwrap();
assert_eq!(set.machines["web"].os.map(|os| os.to_string()), Some("alpine@3.19".to_owned()));
assert!(matches!(load_with(&dir, &["nonsense".to_owned()]), Err(Error::SetShape { .. })));
}
#[test]
fn problems_name_the_file_they_are_in() {
let dir = folder("named", &[("isoloom.yaml", "version: 1\nname: shop\nmachines:\n web: { vm: {} }\n")]);
match load(&dir) {
Err(Error::Invalid { file, .. }) => assert_eq!(file, "isoloom.yaml"),
other => panic!("{other:?}"),
}
}
}