Skip to main content

upstream_rs/application/operations/
metadata_operation.rs

1use crate::services::storage::{
2    metadata_storage::MetadataStorage, package_storage::PackageStorage,
3};
4use anyhow::{Context, Result};
5
6pub struct MetadataManager<'a> {
7    package_storage: &'a mut PackageStorage,
8    metadata_storage: &'a mut MetadataStorage,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct MetadataSetResult {
13    pub key: String,
14    pub value: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct MetadataBulkSetResult {
19    pub applied: Vec<MetadataSetResult>,
20    pub failures: Vec<(String, String)>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct MetadataBulkGetResult {
25    pub values: Vec<(String, String)>,
26    pub failures: Vec<(String, String)>,
27}
28
29impl<'a> MetadataManager<'a> {
30    pub fn new(
31        package_storage: &'a mut PackageStorage,
32        metadata_storage: &'a mut MetadataStorage,
33    ) -> Self {
34        Self {
35            package_storage,
36            metadata_storage,
37        }
38    }
39
40    /// Pins a package to its current version, preventing automatic updates.
41    pub fn pin_package(&mut self, name: &str, reason: Option<String>) -> Result<()> {
42        let package = self
43            .package_storage
44            .get_mut_package_by_name(name)
45            .ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
46
47        if package.is_pinned {
48            return Ok(());
49        }
50
51        package.is_pinned = true;
52        self.package_storage.save_packages()?;
53        if let Some(reason) = reason {
54            self.metadata_storage.set_pin_reason(name, reason)?;
55        }
56
57        Ok(())
58    }
59
60    /// Unpins a package, allowing it to receive automatic updates.
61    pub fn unpin_package(&mut self, name: &str) -> Result<()> {
62        let package = self
63            .package_storage
64            .get_mut_package_by_name(name)
65            .ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
66
67        if !package.is_pinned {
68            return Ok(());
69        }
70
71        package.is_pinned = false;
72        self.package_storage.save_packages()?;
73        self.metadata_storage.clear_pin_reason(name)?;
74
75        Ok(())
76    }
77
78    /// Removes package metadata for a package without touching runtime artifacts.
79    pub fn remove_package(&mut self, name: &str) -> Result<()> {
80        if !self.package_storage.remove_package_by_name(name)? {
81            return Err(anyhow::anyhow!("Package '{}' not found", name));
82        }
83        self.metadata_storage.remove_package(name)?;
84
85        Ok(())
86    }
87
88    /// Renames a package alias without changing provider/repo/version metadata.
89    pub fn rename_package(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
90        let old_name = old_name.trim();
91        let new_name = new_name.trim();
92
93        if old_name.is_empty() || new_name.is_empty() {
94            return Err(anyhow::anyhow!("Package names cannot be empty"));
95        }
96
97        if old_name == new_name {
98            return Ok(false);
99        }
100
101        if self.package_storage.get_package_by_name(new_name).is_some() {
102            return Err(anyhow::anyhow!("Package '{}' already exists", new_name));
103        }
104
105        let package = self
106            .package_storage
107            .get_mut_package_by_name(old_name)
108            .ok_or_else(|| anyhow::anyhow!("Package '{}' not found", old_name))?;
109
110        package.name = new_name.to_string();
111        self.package_storage.save_packages()?;
112        self.metadata_storage.rename_package(old_name, new_name)?;
113
114        Ok(true)
115    }
116
117    /// Sets a package metadata field using dot-notation key path.
118    /// Example: "is_pinned=true" or "pattern=.*x86_64.*"
119    pub fn set_key(&mut self, name: &str, set_key: &str) -> Result<MetadataSetResult> {
120        let (key_path, value) = Self::parse_set_key(set_key)?;
121
122        // Get the package
123        let package = self
124            .package_storage
125            .get_package_by_name(name)
126            .ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
127
128        // Serialize to JSON for manipulation
129        let mut json_value = serde_json::to_value(package)?;
130
131        // Navigate to the field and set it
132        Self::set_nested_value(&mut json_value, &key_path, &value)?;
133
134        // Deserialize back to Package
135        let updated_package: crate::models::upstream::Package =
136            serde_json::from_value(json_value).context("Failed to deserialize updated package")?;
137
138        // Update in storage
139        self.package_storage
140            .add_or_update_package(updated_package)?;
141
142        Ok(MetadataSetResult {
143            key: key_path,
144            value,
145        })
146    }
147
148    /// Gets a package metadata field using dot-notation key path.
149    /// Example: "is_pinned" or "version"
150    pub fn get_key(&self, name: &str, get_key: &str) -> Result<String> {
151        let key_path = get_key.trim();
152        if key_path.is_empty() {
153            return Err(anyhow::anyhow!("Key path cannot be empty"));
154        }
155
156        // Get the package
157        let package = self
158            .package_storage
159            .get_package_by_name(name)
160            .ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
161
162        // Serialize to JSON for field access
163        let json_value = serde_json::to_value(package)?;
164
165        // Navigate to the field
166        let value = Self::get_nested_value(&json_value, key_path)?;
167
168        let value_str = Self::format_value(&value);
169
170        Ok(value_str)
171    }
172
173    /// Sets multiple package metadata fields in bulk.
174    pub fn set_bulk(&mut self, name: &str, set_keys: &[String]) -> MetadataBulkSetResult {
175        let mut applied = Vec::new();
176        let mut failures = Vec::new();
177
178        for set_key in set_keys {
179            match self.set_key(name, set_key) {
180                Ok(result) => applied.push(result),
181                Err(err) => failures.push((set_key.clone(), err.to_string())),
182            }
183        }
184
185        MetadataBulkSetResult { applied, failures }
186    }
187
188    /// Gets multiple package metadata fields in bulk.
189    pub fn get_bulk(&self, name: &str, get_keys: &[String]) -> MetadataBulkGetResult {
190        let mut values = Vec::new();
191        let mut failures = Vec::new();
192
193        for get_key in get_keys {
194            match self.get_key(name, get_key) {
195                Ok(value) => {
196                    values.push((get_key.clone(), value));
197                }
198                Err(err) => failures.push((get_key.clone(), err.to_string())),
199            }
200        }
201
202        MetadataBulkGetResult { values, failures }
203    }
204
205    /// Parses a set_key string in the format "key=value" into (key_path, value).
206    fn parse_set_key(set_key: &str) -> Result<(String, String)> {
207        let parts: Vec<&str> = set_key.splitn(2, '=').collect();
208        if parts.len() != 2 {
209            return Err(anyhow::anyhow!(
210                "Invalid set_key format. Expected 'key=value', got '{}'",
211                set_key
212            ));
213        }
214
215        let key_path = parts[0].trim();
216        let value = parts[1].trim();
217
218        if key_path.is_empty() {
219            return Err(anyhow::anyhow!("Key path cannot be empty"));
220        }
221
222        Ok((key_path.to_string(), value.to_string()))
223    }
224
225    /// Gets a nested value from JSON using dot notation.
226    fn get_nested_value(json: &serde_json::Value, path: &str) -> Result<serde_json::Value> {
227        let keys: Vec<&str> = path.split('.').collect();
228        let mut current = json;
229
230        for key in keys {
231            current = current
232                .get(key)
233                .ok_or_else(|| anyhow::anyhow!("Field '{}' not found", key))?;
234        }
235
236        Ok(current.clone())
237    }
238
239    /// Sets a nested value in JSON using dot notation.
240    fn set_nested_value(json: &mut serde_json::Value, path: &str, value: &str) -> Result<()> {
241        let keys: Vec<&str> = path.split('.').collect();
242
243        if keys.is_empty() {
244            return Err(anyhow::anyhow!("Empty path"));
245        }
246
247        let mut current = json;
248
249        // Navigate to the parent of the target field
250        for key in &keys[..keys.len() - 1] {
251            current = current
252                .get_mut(key)
253                .ok_or_else(|| anyhow::anyhow!("Field '{}' not found", key))?;
254        }
255
256        // Set the final field
257        let final_key = keys[keys.len() - 1];
258        let target = current
259            .get_mut(final_key)
260            .ok_or_else(|| anyhow::anyhow!("Field '{}' not found", final_key))?;
261
262        // Parse the value based on the target type
263        *target = Self::parse_value_for_type(target, value)?;
264
265        Ok(())
266    }
267
268    /// Parses a string value into the appropriate JSON type based on the existing field type.
269    fn parse_value_for_type(
270        existing: &serde_json::Value,
271        value_str: &str,
272    ) -> Result<serde_json::Value> {
273        match existing {
274            serde_json::Value::Bool(_) => {
275                let bool_val = value_str
276                    .parse::<bool>()
277                    .with_context(|| format!("Expected boolean value, got '{}'", value_str))?;
278                Ok(serde_json::Value::Bool(bool_val))
279            }
280            serde_json::Value::Number(_) => {
281                if let Ok(int_val) = value_str.parse::<i64>() {
282                    Ok(serde_json::json!(int_val))
283                } else if let Ok(float_val) = value_str.parse::<f64>() {
284                    Ok(serde_json::json!(float_val))
285                } else {
286                    Err(anyhow::anyhow!(
287                        "Expected numeric value, got '{}'",
288                        value_str
289                    ))
290                }
291            }
292            serde_json::Value::String(_) => Ok(serde_json::Value::String(value_str.to_string())),
293            serde_json::Value::Null => {
294                if value_str == "null" {
295                    Ok(serde_json::Value::Null)
296                } else {
297                    // Try to infer type
298                    if let Ok(bool_val) = value_str.parse::<bool>() {
299                        Ok(serde_json::Value::Bool(bool_val))
300                    } else if let Ok(int_val) = value_str.parse::<i64>() {
301                        Ok(serde_json::json!(int_val))
302                    } else {
303                        Ok(serde_json::Value::String(value_str.to_string()))
304                    }
305                }
306            }
307            _ => {
308                // For objects/arrays, try to parse as JSON
309                serde_json::from_str(value_str).with_context(|| {
310                    format!(
311                        "Cannot set complex type from string. Expected JSON, got '{}'",
312                        value_str
313                    )
314                })
315            }
316        }
317    }
318
319    /// Formats a JSON value as a string for display.
320    fn format_value(value: &serde_json::Value) -> String {
321        match value {
322            serde_json::Value::String(s) => s.clone(),
323            serde_json::Value::Null => "null".to_string(),
324            serde_json::Value::Bool(b) => b.to_string(),
325            serde_json::Value::Number(n) => n.to_string(),
326            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
327                serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
328            }
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::MetadataManager;
336    use crate::models::common::enums::{Channel, Filetype, Provider};
337    use crate::models::upstream::Package;
338    use crate::services::storage::{
339        metadata_storage::MetadataStorage, package_storage::PackageStorage,
340    };
341    use std::path::{Path, PathBuf};
342    use std::time::{SystemTime, UNIX_EPOCH};
343    use std::{fs, io};
344
345    fn temp_packages_file(name: &str) -> PathBuf {
346        let nanos = SystemTime::now()
347            .duration_since(UNIX_EPOCH)
348            .map(|d| d.as_nanos())
349            .unwrap_or(0);
350        std::env::temp_dir()
351            .join(format!("upstream-metadata-test-{name}-{nanos}"))
352            .join("packages.json")
353    }
354
355    fn temp_metadata_file(name: &str) -> PathBuf {
356        let nanos = SystemTime::now()
357            .duration_since(UNIX_EPOCH)
358            .map(|d| d.as_nanos())
359            .unwrap_or(0);
360        std::env::temp_dir()
361            .join(format!("upstream-metadata-test-{name}-{nanos}"))
362            .join("metadata.json")
363    }
364
365    fn test_package(name: &str) -> Package {
366        Package::with_defaults(
367            name.to_string(),
368            format!("owner/{name}"),
369            Filetype::Archive,
370            None,
371            None,
372            Channel::Stable,
373            Provider::Github,
374            None,
375        )
376    }
377
378    fn cleanup(path: &Path) -> io::Result<()> {
379        if let Some(parent) = path.parent() {
380            fs::remove_dir_all(parent)?;
381        }
382        Ok(())
383    }
384
385    #[test]
386    fn parse_set_key_requires_key_value_pair() {
387        assert!(MetadataManager::parse_set_key("is_pinned=true").is_ok());
388        assert!(MetadataManager::parse_set_key("invalid").is_err());
389        assert!(MetadataManager::parse_set_key("=value").is_err());
390    }
391
392    #[test]
393    fn pin_and_unpin_update_package_state() {
394        let path = temp_packages_file("pin");
395        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
396        let metadata_path = temp_metadata_file("pin");
397        let mut storage = PackageStorage::new(&path).expect("create storage");
398        let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
399        storage
400            .add_or_update_package(test_package("fd"))
401            .expect("store package");
402        let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
403
404        manager.pin_package("fd", None).expect("pin package");
405        assert!(
406            manager
407                .package_storage
408                .get_package_by_name("fd")
409                .expect("package")
410                .is_pinned
411        );
412
413        manager.unpin_package("fd").expect("unpin package");
414        assert!(
415            !manager
416                .package_storage
417                .get_package_by_name("fd")
418                .expect("package")
419                .is_pinned
420        );
421
422        cleanup(&path).expect("cleanup");
423        let _ = cleanup(&metadata_path);
424    }
425
426    #[test]
427    fn set_key_and_get_key_support_nested_and_typed_values() {
428        let path = temp_packages_file("set-get");
429        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
430        let metadata_path = temp_metadata_file("set-get");
431        let mut storage = PackageStorage::new(&path).expect("create storage");
432        let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
433        storage
434            .add_or_update_package(test_package("rg"))
435            .expect("store package");
436        let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
437
438        manager
439            .set_key("rg", "is_pinned=true")
440            .expect("set bool key");
441        manager
442            .set_key("rg", "version.major=12")
443            .expect("set nested numeric key");
444
445        assert_eq!(
446            manager.get_key("rg", "is_pinned").expect("get bool"),
447            "true"
448        );
449        assert_eq!(
450            manager.get_key("rg", "version.major").expect("get nested"),
451            "12"
452        );
453
454        cleanup(&path).expect("cleanup");
455        let _ = cleanup(&metadata_path);
456    }
457
458    #[test]
459    fn rename_package_rejects_duplicates_and_updates_alias() {
460        let path = temp_packages_file("rename");
461        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
462        let metadata_path = temp_metadata_file("rename");
463        let mut storage = PackageStorage::new(&path).expect("create storage");
464        let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
465        storage
466            .add_or_update_package(test_package("old"))
467            .expect("store old");
468        storage
469            .add_or_update_package(test_package("taken"))
470            .expect("store taken");
471        let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
472
473        assert!(manager.rename_package("old", "taken").is_err());
474        manager
475            .rename_package("old", "new")
476            .expect("rename package");
477        assert!(manager.package_storage.get_package_by_name("new").is_some());
478        assert!(manager.package_storage.get_package_by_name("old").is_none());
479
480        cleanup(&path).expect("cleanup");
481        let _ = cleanup(&metadata_path);
482    }
483
484    #[test]
485    fn remove_package_deletes_metadata_and_errors_when_missing() {
486        let path = temp_packages_file("remove");
487        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
488        let metadata_path = temp_metadata_file("remove");
489        let mut storage = PackageStorage::new(&path).expect("create storage");
490        let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
491        storage
492            .add_or_update_package(test_package("fd"))
493            .expect("store package");
494        let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
495
496        manager
497            .remove_package("fd")
498            .expect("remove package metadata");
499        assert!(manager.package_storage.get_package_by_name("fd").is_none());
500
501        let err = manager
502            .remove_package("fd")
503            .expect_err("missing package should error");
504        assert!(err.to_string().contains("Package 'fd' not found"));
505
506        cleanup(&path).expect("cleanup");
507        let _ = cleanup(&metadata_path);
508    }
509}