Skip to main content

upstream_rs/application/operations/
metadata_operation.rs

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