1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{Error, IoResultExt, Result};
6use crate::namespace::NsConfig;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct Config {
11 pub namespace: NsConfig,
13 #[serde(default, skip_serializing_if = "Vec::is_empty")]
15 pub remotes: Vec<Remote>,
16}
17
18impl Config {
19 pub fn new(namespace: NsConfig) -> Self {
21 Self {
22 namespace,
23 remotes: vec![],
24 }
25 }
26
27 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 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 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 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 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#[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 assert!(config.add_remote("origin", "ssh://other").is_err());
134
135 let r = config.get_remote("origin").unwrap();
137 assert_eq!(r.url, "ssh://foo/bar");
138
139 config.remove_remote("origin").unwrap();
141 assert!(config.remotes.is_empty());
142
143 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}