Skip to main content

cadvm_core/
config.rs

1//! Repository configuration (`.cadvm/config.json`) and author resolution.
2//!
3//! Config is a flat `key = value` store serialized as a JSON object. The only
4//! keys cadvm interprets today are [`USER_NAME`] and [`USER_EMAIL`], used to
5//! stamp commits, but arbitrary keys may be stored.
6
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{CoreError, Result};
12use crate::model::Author;
13use crate::repo::Repository;
14
15/// Config key for the author name.
16pub const USER_NAME: &str = "user.name";
17/// Config key for the author email.
18pub const USER_EMAIL: &str = "user.email";
19/// Environment variable that overrides the author name.
20pub const ENV_AUTHOR_NAME: &str = "CADVM_AUTHOR_NAME";
21/// Environment variable that overrides the author email.
22pub const ENV_AUTHOR_EMAIL: &str = "CADVM_AUTHOR_EMAIL";
23
24/// Fallback author name when nothing is configured.
25const UNKNOWN_NAME: &str = "unknown";
26
27/// A flat string key/value configuration store.
28#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct Config {
31    entries: BTreeMap<String, String>,
32}
33
34impl Config {
35    /// Load config from the repository, or an empty config if none exists yet.
36    pub fn load(repo: &Repository) -> Result<Config> {
37        let path = repo.config_path();
38        match std::fs::read(&path) {
39            Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
40            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Config::default()),
41            Err(e) => Err(CoreError::io(&path, e)),
42        }
43    }
44
45    /// Persist config to the repository.
46    pub fn save(&self, repo: &Repository) -> Result<()> {
47        let path = repo.config_path();
48        let mut bytes = serde_json::to_vec_pretty(self)?;
49        bytes.push(b'\n');
50        std::fs::write(&path, bytes).map_err(|e| CoreError::io(&path, e))
51    }
52
53    /// Read a key.
54    pub fn get(&self, key: &str) -> Option<&str> {
55        self.entries.get(key).map(String::as_str)
56    }
57
58    /// Set a key.
59    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
60        self.entries.insert(key.into(), value.into());
61    }
62
63    /// All entries, sorted by key.
64    pub fn entries(&self) -> impl Iterator<Item = (&str, &str)> {
65        self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
66    }
67}
68
69/// Resolve the commit author: environment overrides config, which overrides a
70/// neutral fallback. Never fails — a snapshot must always be possible.
71pub fn resolve_author(repo: &Repository) -> Result<Author> {
72    let config = Config::load(repo)?;
73    let name = std::env::var(ENV_AUTHOR_NAME)
74        .ok()
75        .filter(|s| !s.is_empty())
76        .or_else(|| config.get(USER_NAME).map(str::to_string))
77        .unwrap_or_else(|| UNKNOWN_NAME.to_string());
78    let email = std::env::var(ENV_AUTHOR_EMAIL)
79        .ok()
80        .filter(|s| !s.is_empty())
81        .or_else(|| config.get(USER_EMAIL).map(str::to_string))
82        .unwrap_or_default();
83    Ok(Author { name, email })
84}