Skip to main content

upstream_rs/application/operations/
config_operation.rs

1use 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    /// Sets a configuration value using dot-notation key path.
33    /// Example: "parent.child=value" or "github.api_token=abc123"
34    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    /// Gets a configuration value using dot-notation key path.
46    /// Example: "parent.child" or "github.api_token"
47    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    /// Sets multiple configuration values in bulk.
60    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    /// Gets multiple configuration values in bulk.
75    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    /// Parses a set_key string in the format "key.path=value" into (key_path, value).
92    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    /// Formats a JSON value as a string for display.
113    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}