use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub project_root: PathBuf,
pub docs_directory: PathBuf,
pub state_file: PathBuf,
pub dustbin_directory: PathBuf,
pub preserve_dustbin_structure: bool,
pub auto_stage_git: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
project_root: PathBuf::from("."),
docs_directory: PathBuf::from("./design/docs"),
state_file: PathBuf::from("./design/docs/.odm/state.json"),
dustbin_directory: PathBuf::from("./design/docs/.dustbin"),
preserve_dustbin_structure: true,
auto_stage_git: true,
}
}
}
impl Config {
pub fn load(docs_dir: Option<&str>) -> Result<Self> {
let mut config = Config::default();
if let Some(dir) = docs_dir {
let path = PathBuf::from(dir);
config.docs_directory = path.clone();
config.state_file = path.join(".odm/state.json");
config.dustbin_directory = path.join(".dustbin");
}
if let Some(file_config) = Self::load_from_file(&config.docs_directory)? {
config.merge(file_config);
}
Ok(config)
}
fn load_from_file(docs_dir: &Path) -> Result<Option<PartialConfig>> {
let config_path = docs_dir.join(".odm/config.toml");
if !config_path.exists() {
return Ok(None);
}
let contents =
std::fs::read_to_string(&config_path).context("Failed to read .odm/config.toml")?;
let config: PartialConfig =
toml::from_str(&contents).context("Failed to parse .odm/config.toml")?;
Ok(Some(config))
}
fn merge(&mut self, other: PartialConfig) {
if let Some(val) = other.project_root {
self.project_root = val;
}
if let Some(val) = other.dustbin_directory {
self.dustbin_directory = val;
}
if let Some(val) = other.preserve_dustbin_structure {
self.preserve_dustbin_structure = val;
}
if let Some(val) = other.auto_stage_git {
self.auto_stage_git = val;
}
}
pub fn dustbin_dir_for_state(&self, state_dir: &str) -> PathBuf {
if self.preserve_dustbin_structure {
self.dustbin_directory.join(state_dir)
} else {
self.dustbin_directory.clone()
}
}
}
#[derive(Debug, Deserialize)]
struct PartialConfig {
project_root: Option<PathBuf>,
dustbin_directory: Option<PathBuf>,
preserve_dustbin_structure: Option<bool>,
auto_stage_git: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.docs_directory, PathBuf::from("./design/docs"));
assert_eq!(config.state_file, PathBuf::from("./design/docs/.odm/state.json"));
assert!(config.preserve_dustbin_structure);
assert!(config.auto_stage_git);
}
#[test]
fn test_load_with_docs_dir() {
let config = Config::load(Some("/custom/docs")).unwrap();
assert_eq!(config.docs_directory, PathBuf::from("/custom/docs"));
assert_eq!(config.state_file, PathBuf::from("/custom/docs/.odm/state.json"));
assert_eq!(config.dustbin_directory, PathBuf::from("/custom/docs/.dustbin"));
}
#[test]
fn test_load_from_file() {
let temp = TempDir::new().unwrap();
let docs_dir = temp.path();
fs::create_dir_all(docs_dir.join(".odm")).unwrap();
fs::write(
docs_dir.join(".odm/config.toml"),
r#"
preserve_dustbin_structure = false
auto_stage_git = false
"#,
)
.unwrap();
let config = Config::load(Some(docs_dir.to_str().unwrap())).unwrap();
assert!(!config.preserve_dustbin_structure);
assert!(!config.auto_stage_git);
}
#[test]
fn test_dustbin_dir_for_state_preserved() {
let config = Config {
dustbin_directory: PathBuf::from("/dustbin"),
preserve_dustbin_structure: true,
..Default::default()
};
let result = config.dustbin_dir_for_state("01-draft");
assert_eq!(result, PathBuf::from("/dustbin/01-draft"));
}
#[test]
fn test_dustbin_dir_for_state_flat() {
let config = Config {
dustbin_directory: PathBuf::from("/dustbin"),
preserve_dustbin_structure: false,
..Default::default()
};
let result = config.dustbin_dir_for_state("01-draft");
assert_eq!(result, PathBuf::from("/dustbin"));
}
}