muster/adapter/config/
yaml.rs1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use directories::ProjectDirs;
7use getset::Getters;
8use serde::Serialize;
9use typed_builder::TypedBuilder;
10
11use crate::domain::{
12 config::{ConfigError, WorkspaceConfig},
13 port::ConfigSource,
14};
15
16const APP_DIR: &str = "muster";
18
19pub(crate) fn config_dir_path(filename: &str) -> Option<PathBuf> {
23 ProjectDirs::from("", "", APP_DIR).map(|dirs| dirs.config_dir().join(filename))
24}
25
26pub(crate) fn load_workspace(path: &Path) -> Result<WorkspaceConfig, ConfigError> {
32 let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {
33 path: path.to_path_buf(),
34 source,
35 })?;
36 let config: WorkspaceConfig = serde_yaml_ng::from_str(&raw)?;
37 config.validate()?;
38 Ok(config)
39}
40
41pub(crate) fn write_config<T: Serialize>(path: &Path, value: &T) -> Result<(), ConfigError> {
53 let dest = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
54 if let Some(parent) = dest.parent()
55 && !parent.as_os_str().is_empty()
56 {
57 fs::create_dir_all(parent).map_err(|source| ConfigError::Write {
58 path: parent.to_path_buf(),
59 source,
60 })?;
61 }
62 let raw = serde_yaml_ng::to_string(value)?;
63 let temp = temp_path(&dest);
64 fs::write(&temp, raw).map_err(|source| ConfigError::Write {
65 path: temp.clone(),
66 source,
67 })?;
68 fs::rename(&temp, &dest).map_err(|source| {
69 let _ = fs::remove_file(&temp);
70 ConfigError::Write {
71 path: dest.clone(),
72 source,
73 }
74 })
75}
76
77fn temp_path(path: &Path) -> PathBuf {
80 let mut name = path.file_name().unwrap_or_default().to_os_string();
81 name.push(format!(".{}.tmp", std::process::id()));
82 path.with_file_name(name)
83}
84
85#[derive(Clone, Debug, Getters, TypedBuilder)]
87#[getset(get = "pub")]
88pub struct YamlConfigSource {
89 path: PathBuf,
91}
92
93impl ConfigSource for YamlConfigSource {
94 fn load(&self) -> Result<WorkspaceConfig, ConfigError> {
95 load_workspace(&self.path)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn example_config() -> PathBuf {
104 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/muster.yml")
105 }
106
107 #[test]
108 fn loads_the_example_config() {
109 let source = YamlConfigSource::builder().path(example_config()).build();
110 let config = source.load().unwrap();
111 assert!(!config.to_processes().is_empty());
112 }
113
114 #[test]
115 fn missing_file_is_a_read_error() {
116 let source = YamlConfigSource::builder()
117 .path(PathBuf::from("/nonexistent/muster.yml"))
118 .build();
119 assert!(matches!(source.load(), Err(ConfigError::Read { .. })));
120 }
121}