upstream_rs/application/operations/
config_operation.rs1use crate::services::storage::config_storage::ConfigStorage;
2use anyhow::Result;
3use toml;
4
5pub struct ConfigUpdater<'a> {
6 config_storage: &'a mut ConfigStorage,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ConfigSetResult {
11 pub key: String,
12 pub display_value: String,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ConfigBulkSetResult {
17 pub applied: Vec<ConfigSetResult>,
18 pub failures: Vec<(String, String)>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ConfigBulkGetResult {
23 pub values: Vec<(String, String)>,
24 pub failures: Vec<(String, String)>,
25}
26
27impl<'a> ConfigUpdater<'a> {
28 pub fn new(config_storage: &'a mut ConfigStorage) -> Self {
29 Self { config_storage }
30 }
31
32 pub fn set_key(&mut self, set_key: &str) -> Result<ConfigSetResult> {
35 let (key_path, value) = Self::parse_set_key(set_key)?;
36
37 self.config_storage.try_set_value(&key_path, &value)?;
38
39 Ok(ConfigSetResult {
40 key: key_path,
41 display_value: value,
42 })
43 }
44
45 pub fn get_key(&self, get_key: &str) -> Result<String> {
48 let key_path = get_key.trim();
49
50 if key_path.is_empty() {
51 return Err(anyhow::anyhow!("Key path cannot be empty"));
52 }
53
54 let value: toml::Value = self.config_storage.try_get_value(key_path)?;
55
56 Ok(Self::format_value(&value))
57 }
58
59 pub fn set_bulk(&mut self, set_keys: &[String]) -> ConfigBulkSetResult {
61 let mut applied = Vec::new();
62 let mut failures = Vec::new();
63
64 for set_key in set_keys {
65 match self.set_key(set_key) {
66 Ok(result) => applied.push(result),
67 Err(err) => failures.push((set_key.clone(), err.to_string())),
68 }
69 }
70
71 ConfigBulkSetResult { applied, failures }
72 }
73
74 pub fn get_bulk(&self, get_keys: &[String]) -> ConfigBulkGetResult {
76 let mut values = Vec::new();
77 let mut failures = Vec::new();
78
79 for get_key in get_keys {
80 match self.get_key(get_key) {
81 Ok(value) => {
82 values.push((get_key.clone(), value));
83 }
84 Err(err) => failures.push((get_key.clone(), err.to_string())),
85 }
86 }
87
88 ConfigBulkGetResult { values, failures }
89 }
90
91 fn parse_set_key(set_key: &str) -> Result<(String, String)> {
93 let parts: Vec<&str> = set_key.splitn(2, '=').collect();
94
95 if parts.len() != 2 {
96 return Err(anyhow::anyhow!(
97 "Invalid set_key format. Expected 'key.path=value', got '{}'",
98 set_key
99 ));
100 }
101
102 let key_path = parts[0].trim();
103 let value = parts[1].trim();
104
105 if key_path.is_empty() {
106 return Err(anyhow::anyhow!("Key path cannot be empty"));
107 }
108
109 Ok((key_path.to_string(), value.to_string()))
110 }
111
112 fn format_value(value: &toml::Value) -> String {
114 match value {
115 toml::Value::String(s) => s.clone(),
116 toml::Value::Integer(i) => i.to_string(),
117 toml::Value::Float(f) => f.to_string(),
118 toml::Value::Boolean(b) => b.to_string(),
119 toml::Value::Table(_) | toml::Value::Array(_) => {
120 toml::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
121 }
122 toml::Value::Datetime(dt) => dt.to_string(),
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::ConfigUpdater;
130 use crate::services::storage::config_storage::ConfigStorage;
131 use std::path::{Path, PathBuf};
132 use std::time::{SystemTime, UNIX_EPOCH};
133 use std::{fs, io};
134
135 fn temp_config_file(name: &str) -> PathBuf {
136 let nanos = SystemTime::now()
137 .duration_since(UNIX_EPOCH)
138 .map(|d| d.as_nanos())
139 .unwrap_or(0);
140 std::env::temp_dir()
141 .join(format!("upstream-config-updater-test-{name}-{nanos}"))
142 .join("config.toml")
143 }
144
145 fn cleanup(path: &Path) -> io::Result<()> {
146 if let Some(parent) = path.parent() {
147 fs::remove_dir_all(parent)?;
148 }
149 Ok(())
150 }
151
152 #[test]
153 fn parse_set_key_requires_key_value_format() {
154 assert!(ConfigUpdater::parse_set_key("github.api_token=ghp_abc").is_ok());
155 assert!(ConfigUpdater::parse_set_key("missing-separator").is_err());
156 assert!(ConfigUpdater::parse_set_key(" =x").is_err());
157 }
158
159 #[test]
160 fn set_key_and_get_key_round_trip_value() {
161 let config_file = temp_config_file("roundtrip");
162 fs::create_dir_all(config_file.parent().expect("config parent")).expect("create parent");
163 let mut storage = ConfigStorage::new(&config_file).expect("create storage");
164 let mut updater = ConfigUpdater::new(&mut storage);
165
166 updater
167 .set_key("github.api_token=ghp_abc")
168 .expect("set key");
169 let value = updater.get_key("github.api_token").expect("get key");
170 assert_eq!(value, "ghp_abc");
171
172 cleanup(&config_file).expect("cleanup");
173 }
174
175 #[test]
176 fn set_bulk_continues_after_failures_and_applies_valid_keys() {
177 let config_file = temp_config_file("bulk");
178 fs::create_dir_all(config_file.parent().expect("config parent")).expect("create parent");
179 let mut storage = ConfigStorage::new(&config_file).expect("create storage");
180 let mut updater = ConfigUpdater::new(&mut storage);
181 let keys = vec![
182 "github.api_token=ghp_abc".to_string(),
183 "badformat".to_string(),
184 "gitlab.api_token=glpat_abc".to_string(),
185 ];
186
187 let result = updater.set_bulk(&keys);
188 assert_eq!(result.applied.len(), 2);
189 assert_eq!(result.failures.len(), 1);
190 let github = updater.get_key("github.api_token").expect("github key");
191 let gitlab = updater.get_key("gitlab.api_token").expect("gitlab key");
192 assert_eq!(github, "ghp_abc");
193 assert_eq!(gitlab, "glpat_abc");
194
195 cleanup(&config_file).expect("cleanup");
196 }
197}