Skip to main content

zub/
config.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{Error, IoResultExt, Result};
6use crate::namespace::NsConfig;
7
8/// repository configuration stored in config.toml
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct Config {
11    /// namespace mapping for this repository
12    pub namespace: NsConfig,
13    /// configured remotes
14    #[serde(default, skip_serializing_if = "Vec::is_empty")]
15    pub remotes: Vec<Remote>,
16}
17
18impl Config {
19    /// create a new config with given namespace
20    pub fn new(namespace: NsConfig) -> Self {
21        Self {
22            namespace,
23            remotes: vec![],
24        }
25    }
26
27    /// load config from file
28    pub fn load(path: &Path) -> Result<Self> {
29        let content = std::fs::read_to_string(path).with_path(path)?;
30        let config: Config = toml::from_str(&content)?;
31        Ok(config)
32    }
33
34    /// save config to file
35    pub fn save(&self, path: &Path) -> Result<()> {
36        let content = toml::to_string_pretty(self)?;
37        std::fs::write(path, content).with_path(path)?;
38        Ok(())
39    }
40
41    /// add a remote
42    pub fn add_remote(&mut self, name: impl Into<String>, url: impl Into<String>) -> Result<()> {
43        let name = name.into();
44        if self.remotes.iter().any(|r| r.name == name) {
45            return Err(Error::RemoteNotFound(format!(
46                "remote '{}' already exists",
47                name
48            )));
49        }
50        self.remotes.push(Remote {
51            name,
52            url: url.into(),
53        });
54        Ok(())
55    }
56
57    /// remove a remote
58    pub fn remove_remote(&mut self, name: &str) -> Result<()> {
59        let pos = self
60            .remotes
61            .iter()
62            .position(|r| r.name == name)
63            .ok_or_else(|| Error::RemoteNotFound(name.to_string()))?;
64        self.remotes.remove(pos);
65        Ok(())
66    }
67
68    /// get remote by name
69    pub fn get_remote(&self, name: &str) -> Option<&Remote> {
70        self.remotes.iter().find(|r| r.name == name)
71    }
72}
73
74impl Default for Config {
75    fn default() -> Self {
76        Self {
77            namespace: NsConfig::default(),
78            remotes: vec![],
79        }
80    }
81}
82
83/// a configured remote repository
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct Remote {
86    pub name: String,
87    pub url: String,
88}
89
90impl Remote {
91    pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
92        Self {
93            name: name.into(),
94            url: url.into(),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::namespace::MapEntry;
103
104    #[test]
105    fn test_config_toml_roundtrip() {
106        let config = Config {
107            namespace: NsConfig {
108                uid_map: vec![MapEntry::new(0, 1000, 1), MapEntry::new(1, 100000, 65536)],
109                gid_map: vec![MapEntry::new(0, 1000, 1), MapEntry::new(1, 100000, 65536)],
110            },
111            remotes: vec![
112                Remote::new("origin", "ssh://server/var/zub"),
113                Remote::new("backup", "/mnt/backup/zub"),
114            ],
115        };
116
117        let toml_str = toml::to_string_pretty(&config).unwrap();
118        let parsed: Config = toml::from_str(&toml_str).unwrap();
119
120        assert_eq!(config.namespace.uid_map, parsed.namespace.uid_map);
121        assert_eq!(config.namespace.gid_map, parsed.namespace.gid_map);
122        assert_eq!(config.remotes, parsed.remotes);
123    }
124
125    #[test]
126    fn test_config_add_remove_remote() {
127        let mut config = Config::default();
128
129        config.add_remote("origin", "ssh://foo/bar").unwrap();
130        assert_eq!(config.remotes.len(), 1);
131
132        // duplicate should fail
133        assert!(config.add_remote("origin", "ssh://other").is_err());
134
135        // get remote
136        let r = config.get_remote("origin").unwrap();
137        assert_eq!(r.url, "ssh://foo/bar");
138
139        // remove
140        config.remove_remote("origin").unwrap();
141        assert!(config.remotes.is_empty());
142
143        // remove non-existent should fail
144        assert!(config.remove_remote("origin").is_err());
145    }
146
147    #[test]
148    fn test_config_minimal_toml() {
149        let toml_str = r#"
150[namespace]
151uid_map = []
152gid_map = []
153"#;
154        let config: Config = toml::from_str(toml_str).unwrap();
155        assert!(config.namespace.uid_map.is_empty());
156        assert!(config.remotes.is_empty());
157    }
158}