use std::{
fs,
path::{Path, PathBuf},
};
use directories::ProjectDirs;
use getset::Getters;
use serde::Serialize;
use typed_builder::TypedBuilder;
use crate::domain::{
config::{ConfigError, WorkspaceConfig},
port::ConfigSource,
};
const APP_DIR: &str = "muster";
pub(crate) fn config_dir_path(filename: &str) -> Option<PathBuf> {
ProjectDirs::from("", "", APP_DIR).map(|dirs| dirs.config_dir().join(filename))
}
pub(crate) fn load_workspace(path: &Path) -> Result<WorkspaceConfig, ConfigError> {
let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {
path: path.to_path_buf(),
source,
})?;
let config: WorkspaceConfig = serde_yaml_ng::from_str(&raw)?;
config.validate()?;
Ok(config)
}
pub(crate) fn write_config<T: Serialize>(path: &Path, value: &T) -> Result<(), ConfigError> {
let dest = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
if let Some(parent) = dest.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent).map_err(|source| ConfigError::Write {
path: parent.to_path_buf(),
source,
})?;
}
let raw = serde_yaml_ng::to_string(value)?;
let temp = temp_path(&dest);
fs::write(&temp, raw).map_err(|source| ConfigError::Write {
path: temp.clone(),
source,
})?;
fs::rename(&temp, &dest).map_err(|source| {
let _ = fs::remove_file(&temp);
ConfigError::Write {
path: dest.clone(),
source,
}
})
}
fn temp_path(path: &Path) -> PathBuf {
let mut name = path.file_name().unwrap_or_default().to_os_string();
name.push(format!(".{}.tmp", std::process::id()));
path.with_file_name(name)
}
#[derive(Clone, Debug, Getters, TypedBuilder)]
#[getset(get = "pub")]
pub struct YamlConfigSource {
path: PathBuf,
}
impl ConfigSource for YamlConfigSource {
fn load(&self) -> Result<WorkspaceConfig, ConfigError> {
load_workspace(&self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn example_config() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/muster.yml")
}
#[test]
fn loads_the_example_config() {
let source = YamlConfigSource::builder().path(example_config()).build();
let config = source.load().unwrap();
assert!(!config.to_processes().is_empty());
}
#[test]
fn missing_file_is_a_read_error() {
let source = YamlConfigSource::builder()
.path(PathBuf::from("/nonexistent/muster.yml"))
.build();
assert!(matches!(source.load(), Err(ConfigError::Read { .. })));
}
}