Skip to main content

libgrite_core/
config.rs

1use std::path::Path;
2use serde::{Deserialize, Serialize};
3use crate::error::GriteError;
4use crate::lock::LockPolicy;
5use crate::signing::VerificationPolicy;
6use crate::types::actor::ActorConfig;
7
8/// Repo-level configuration stored in .git/grite/config.toml
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct RepoConfig {
11    /// Default actor ID (hex string)
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub default_actor: Option<String>,
14    /// Lock policy: "off", "warn", or "require"
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub lock_policy: Option<String>,
17    /// Signature verification policy: "off", "warn", or "require"
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub verify_signatures: Option<String>,
20    /// Snapshot configuration
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub snapshot: Option<SnapshotConfig>,
23}
24
25/// Snapshot policy configuration
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SnapshotConfig {
28    /// Create snapshot when events since last snapshot exceed this
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub max_events: Option<u32>,
31    /// Create snapshot when last snapshot is older than this many days
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_age_days: Option<u32>,
34}
35
36impl Default for SnapshotConfig {
37    fn default() -> Self {
38        Self {
39            max_events: Some(10000),
40            max_age_days: Some(7),
41        }
42    }
43}
44
45impl RepoConfig {
46    /// Get the lock policy, defaulting to Warn if not set
47    pub fn get_lock_policy(&self) -> LockPolicy {
48        self.lock_policy
49            .as_ref()
50            .and_then(|s| LockPolicy::from_str(s))
51            .unwrap_or(LockPolicy::Warn)
52    }
53
54    /// Get the verification policy, defaulting to Off if not set
55    pub fn get_verification_policy(&self) -> VerificationPolicy {
56        self.verify_signatures
57            .as_ref()
58            .and_then(|s| VerificationPolicy::from_str(s))
59            .unwrap_or(VerificationPolicy::Off)
60    }
61}
62
63/// Load repo config from .git/grite/config.toml
64pub fn load_repo_config(git_dir: &Path) -> Result<Option<RepoConfig>, GriteError> {
65    let config_path = git_dir.join("grite").join("config.toml");
66    if !config_path.exists() {
67        return Ok(None);
68    }
69    let content = std::fs::read_to_string(&config_path)?;
70    let config: RepoConfig = toml::from_str(&content)?;
71    Ok(Some(config))
72}
73
74/// Save repo config to .git/grite/config.toml
75pub fn save_repo_config(git_dir: &Path, config: &RepoConfig) -> Result<(), GriteError> {
76    let grit_dir = git_dir.join("grite");
77    std::fs::create_dir_all(&grit_dir)?;
78    let config_path = grit_dir.join("config.toml");
79    let content = toml::to_string_pretty(config)?;
80    std::fs::write(&config_path, content)?;
81    Ok(())
82}
83
84/// Load actor config from .git/grite/actors/<actor_id>/config.toml
85pub fn load_actor_config(actor_dir: &Path) -> Result<ActorConfig, GriteError> {
86    let config_path = actor_dir.join("config.toml");
87    if !config_path.exists() {
88        return Err(GriteError::NotFound(format!(
89            "Actor config not found: {}",
90            config_path.display()
91        )));
92    }
93    let content = std::fs::read_to_string(&config_path)?;
94    let config: ActorConfig = toml::from_str(&content)?;
95    Ok(config)
96}
97
98/// Save actor config to .git/grite/actors/<actor_id>/config.toml
99pub fn save_actor_config(actor_dir: &Path, config: &ActorConfig) -> Result<(), GriteError> {
100    std::fs::create_dir_all(actor_dir)?;
101    let config_path = actor_dir.join("config.toml");
102    let content = toml::to_string_pretty(config)?;
103    std::fs::write(&config_path, content)?;
104    Ok(())
105}
106
107/// List all actors in .git/grite/actors/
108pub fn list_actors(git_dir: &Path) -> Result<Vec<ActorConfig>, GriteError> {
109    let actors_dir = git_dir.join("grite").join("actors");
110    if !actors_dir.exists() {
111        return Ok(Vec::new());
112    }
113
114    let mut actors = Vec::new();
115    for entry in std::fs::read_dir(&actors_dir)? {
116        let entry = entry?;
117        if entry.file_type()?.is_dir() {
118            let actor_dir = entry.path();
119            match load_actor_config(&actor_dir) {
120                Ok(config) => actors.push(config),
121                Err(_) => continue, // Skip invalid actor directories
122            }
123        }
124    }
125
126    // Sort by actor_id for deterministic output
127    actors.sort_by(|a, b| a.actor_id.cmp(&b.actor_id));
128    Ok(actors)
129}
130
131/// Get the actors directory path
132pub fn actors_dir(git_dir: &Path) -> std::path::PathBuf {
133    git_dir.join("grite").join("actors")
134}
135
136/// Get the actor directory path for a specific actor
137pub fn actor_dir(git_dir: &Path, actor_id: &str) -> std::path::PathBuf {
138    actors_dir(git_dir).join(actor_id)
139}
140
141/// Get the sled database path for an actor
142pub fn actor_sled_path(git_dir: &Path, actor_id: &str) -> std::path::PathBuf {
143    actor_dir(git_dir, actor_id).join("sled")
144}
145
146/// Get the signing key path for an actor
147pub fn actor_signing_key_path(git_dir: &Path, actor_id: &str) -> std::path::PathBuf {
148    actor_dir(git_dir, actor_id).join("signing_key")
149}
150
151/// Load signing key for an actor (if present)
152pub fn load_signing_key(git_dir: &Path, actor_id: &str) -> Option<String> {
153    let key_path = actor_signing_key_path(git_dir, actor_id);
154    std::fs::read_to_string(key_path).ok()
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use tempfile::tempdir;
161
162    #[test]
163    fn test_repo_config_roundtrip() {
164        let dir = tempdir().unwrap();
165        let git_dir = dir.path();
166
167        let config = RepoConfig {
168            default_actor: Some("00112233445566778899aabbccddeeff".to_string()),
169            lock_policy: Some("warn".to_string()),
170            verify_signatures: Some("warn".to_string()),
171            snapshot: Some(SnapshotConfig {
172                max_events: Some(5000),
173                max_age_days: Some(3),
174            }),
175        };
176
177        save_repo_config(git_dir, &config).unwrap();
178        let loaded = load_repo_config(git_dir).unwrap().unwrap();
179
180        assert_eq!(loaded.default_actor, config.default_actor);
181        assert_eq!(loaded.lock_policy, config.lock_policy);
182    }
183
184    #[test]
185    fn test_actor_config_roundtrip() {
186        let dir = tempdir().unwrap();
187        let actor_dir = dir.path().join("test_actor");
188
189        let config = ActorConfig {
190            actor_id: "00112233445566778899aabbccddeeff".to_string(),
191            label: Some("test-device".to_string()),
192            created_ts: Some(1700000000000),
193            public_key: None,
194            key_scheme: None,
195        };
196
197        save_actor_config(&actor_dir, &config).unwrap();
198        let loaded = load_actor_config(&actor_dir).unwrap();
199
200        assert_eq!(loaded.actor_id, config.actor_id);
201        assert_eq!(loaded.label, config.label);
202    }
203
204    #[test]
205    fn test_list_actors() {
206        let dir = tempdir().unwrap();
207        let git_dir = dir.path();
208
209        // Create actors directory
210        let actors = actors_dir(git_dir);
211        std::fs::create_dir_all(&actors).unwrap();
212
213        // Create two actors
214        for i in 0..2 {
215            let actor_id = format!("{:032x}", i);
216            let actor_path = actors.join(&actor_id);
217            let config = ActorConfig {
218                actor_id: actor_id.clone(),
219                label: Some(format!("actor-{}", i)),
220                created_ts: Some(1700000000000 + i),
221                public_key: None,
222                key_scheme: None,
223            };
224            save_actor_config(&actor_path, &config).unwrap();
225        }
226
227        let found = list_actors(git_dir).unwrap();
228        assert_eq!(found.len(), 2);
229    }
230}