use crate::services::storage::{
metadata_storage::MetadataStorage, package_storage::PackageStorage,
};
use anyhow::{Context, Result};
use console::style;
macro_rules! message {
($cb:expr, $($arg:tt)*) => {{
if let Some(cb) = $cb.as_mut() {
cb(&format!($($arg)*));
}
}};
}
pub struct MetadataManager<'a> {
package_storage: &'a mut PackageStorage,
metadata_storage: &'a mut MetadataStorage,
}
impl<'a> MetadataManager<'a> {
pub fn new(
package_storage: &'a mut PackageStorage,
metadata_storage: &'a mut MetadataStorage,
) -> Self {
Self {
package_storage,
metadata_storage,
}
}
pub fn pin_package<H>(
&mut self,
name: &str,
reason: Option<String>,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
message!(message_callback, "Pinning package '{}'...", name);
let package = self
.package_storage
.get_mut_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
if package.is_pinned {
message!(
message_callback,
"{}",
style(format!("Package '{}' is already pinned", name)).yellow()
);
return Ok(());
}
let version = package.version.clone();
package.is_pinned = true;
self.package_storage.save_packages()?;
if let Some(reason) = reason {
self.metadata_storage.set_pin_reason(name, reason)?;
}
message!(
message_callback,
"{}",
style(format!("Package '{}' pinned at version {}", name, version)).green()
);
Ok(())
}
pub fn unpin_package<H>(&mut self, name: &str, message_callback: &mut Option<H>) -> Result<()>
where
H: FnMut(&str),
{
message!(message_callback, "Unpinning package '{}'...", name);
let package = self
.package_storage
.get_mut_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
if !package.is_pinned {
message!(
message_callback,
"{}",
style(format!("Package '{}' is not pinned", name)).yellow()
);
return Ok(());
}
package.is_pinned = false;
self.package_storage.save_packages()?;
self.metadata_storage.clear_pin_reason(name)?;
message!(
message_callback,
"{}",
style(format!("Package '{}' unpinned", name)).green()
);
Ok(())
}
pub fn remove_package<H>(&mut self, name: &str, message_callback: &mut Option<H>) -> Result<()>
where
H: FnMut(&str),
{
message!(
message_callback,
"Removing package metadata for '{}'...",
name
);
if !self.package_storage.remove_package_by_name(name)? {
return Err(anyhow::anyhow!("Package '{}' not found", name));
}
self.metadata_storage.remove_package(name)?;
message!(
message_callback,
"{}",
style(format!("Package '{}' metadata removed", name)).green()
);
Ok(())
}
pub fn rename_package<H>(
&mut self,
old_name: &str,
new_name: &str,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
let old_name = old_name.trim();
let new_name = new_name.trim();
if old_name.is_empty() || new_name.is_empty() {
return Err(anyhow::anyhow!("Package names cannot be empty"));
}
if old_name == new_name {
message!(
message_callback,
"{}",
style("Old and new package names are identical; no changes made").yellow()
);
return Ok(());
}
if self.package_storage.get_package_by_name(new_name).is_some() {
return Err(anyhow::anyhow!("Package '{}' already exists", new_name));
}
message!(
message_callback,
"Renaming package '{}' -> '{}' ...",
old_name,
new_name
);
let package = self
.package_storage
.get_mut_package_by_name(old_name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", old_name))?;
package.name = new_name.to_string();
self.package_storage.save_packages()?;
self.metadata_storage.rename_package(old_name, new_name)?;
message!(
message_callback,
"{}",
style(format!("Package '{}' renamed to '{}'", old_name, new_name)).green()
);
Ok(())
}
pub fn set_key<H>(
&mut self,
name: &str,
set_key: &str,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
let (key_path, value) = Self::parse_set_key(set_key)?;
message!(
message_callback,
"Setting '{}' for package '{}' = '{}'",
key_path,
name,
value
);
let package = self
.package_storage
.get_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
let mut json_value = serde_json::to_value(package)?;
Self::set_nested_value(&mut json_value, &key_path, &value)?;
let updated_package: crate::models::upstream::Package =
serde_json::from_value(json_value).context("Failed to deserialize updated package")?;
self.package_storage
.add_or_update_package(updated_package)?;
message!(
message_callback,
"{}",
style("Package metadata updated successfully").green()
);
Ok(())
}
pub fn get_key<H>(
&self,
name: &str,
get_key: &str,
message_callback: &mut Option<H>,
) -> Result<String>
where
H: FnMut(&str),
{
let key_path = get_key.trim();
if key_path.is_empty() {
return Err(anyhow::anyhow!("Key path cannot be empty"));
}
message!(
message_callback,
"Getting value for '{}' from package '{}'",
key_path,
name
);
let package = self
.package_storage
.get_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
let json_value = serde_json::to_value(package)?;
let value = Self::get_nested_value(&json_value, key_path)?;
let value_str = Self::format_value(&value);
message!(
message_callback,
"{}.{} = {}",
name,
key_path,
style(&value_str).cyan()
);
Ok(value_str)
}
pub fn set_bulk<H>(
&mut self,
name: &str,
set_keys: &[String],
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
let mut failures = 0;
for set_key in set_keys {
match self.set_key(name, set_key, message_callback) {
Ok(_) => {}
Err(e) => {
message!(message_callback, "Failed to set '{}': {}", set_key, e);
failures += 1;
}
}
}
if failures > 0 {
message!(
message_callback,
"{} {}",
failures,
style("key(s) failed to be set").red()
);
}
Ok(())
}
pub fn get_bulk<H>(
&self,
name: &str,
get_keys: &[String],
message_callback: &mut Option<H>,
) -> Result<Vec<(String, String)>>
where
H: FnMut(&str),
{
let mut results = Vec::new();
for get_key in get_keys {
match self.get_key(name, get_key, message_callback) {
Ok(value) => {
results.push((get_key.clone(), value));
}
Err(e) => {
message!(
message_callback,
"{} '{}': {}",
style("Failed to get").red(),
get_key,
e
);
}
}
}
Ok(results)
}
fn parse_set_key(set_key: &str) -> Result<(String, String)> {
let parts: Vec<&str> = set_key.splitn(2, '=').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!(
"Invalid set_key format. Expected 'key=value', got '{}'",
set_key
));
}
let key_path = parts[0].trim();
let value = parts[1].trim();
if key_path.is_empty() {
return Err(anyhow::anyhow!("Key path cannot be empty"));
}
Ok((key_path.to_string(), value.to_string()))
}
fn get_nested_value(json: &serde_json::Value, path: &str) -> Result<serde_json::Value> {
let keys: Vec<&str> = path.split('.').collect();
let mut current = json;
for key in keys {
current = current
.get(key)
.ok_or_else(|| anyhow::anyhow!("Field '{}' not found", key))?;
}
Ok(current.clone())
}
fn set_nested_value(json: &mut serde_json::Value, path: &str, value: &str) -> Result<()> {
let keys: Vec<&str> = path.split('.').collect();
if keys.is_empty() {
return Err(anyhow::anyhow!("Empty path"));
}
let mut current = json;
for key in &keys[..keys.len() - 1] {
current = current
.get_mut(key)
.ok_or_else(|| anyhow::anyhow!("Field '{}' not found", key))?;
}
let final_key = keys[keys.len() - 1];
let target = current
.get_mut(final_key)
.ok_or_else(|| anyhow::anyhow!("Field '{}' not found", final_key))?;
*target = Self::parse_value_for_type(target, value)?;
Ok(())
}
fn parse_value_for_type(
existing: &serde_json::Value,
value_str: &str,
) -> Result<serde_json::Value> {
match existing {
serde_json::Value::Bool(_) => {
let bool_val = value_str
.parse::<bool>()
.with_context(|| format!("Expected boolean value, got '{}'", value_str))?;
Ok(serde_json::Value::Bool(bool_val))
}
serde_json::Value::Number(_) => {
if let Ok(int_val) = value_str.parse::<i64>() {
Ok(serde_json::json!(int_val))
} else if let Ok(float_val) = value_str.parse::<f64>() {
Ok(serde_json::json!(float_val))
} else {
Err(anyhow::anyhow!(
"Expected numeric value, got '{}'",
value_str
))
}
}
serde_json::Value::String(_) => Ok(serde_json::Value::String(value_str.to_string())),
serde_json::Value::Null => {
if value_str == "null" {
Ok(serde_json::Value::Null)
} else {
if let Ok(bool_val) = value_str.parse::<bool>() {
Ok(serde_json::Value::Bool(bool_val))
} else if let Ok(int_val) = value_str.parse::<i64>() {
Ok(serde_json::json!(int_val))
} else {
Ok(serde_json::Value::String(value_str.to_string()))
}
}
}
_ => {
serde_json::from_str(value_str).with_context(|| {
format!(
"Cannot set complex type from string. Expected JSON, got '{}'",
value_str
)
})
}
}
}
fn format_value(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
}
}
}
}
#[cfg(test)]
mod tests {
use super::MetadataManager;
use crate::models::common::enums::{Channel, Filetype, Provider};
use crate::models::upstream::Package;
use crate::services::storage::{
metadata_storage::MetadataStorage, package_storage::PackageStorage,
};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{fs, io};
fn temp_packages_file(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir()
.join(format!("upstream-metadata-test-{name}-{nanos}"))
.join("packages.json")
}
fn temp_metadata_file(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir()
.join(format!("upstream-metadata-test-{name}-{nanos}"))
.join("metadata.json")
}
fn test_package(name: &str) -> Package {
Package::with_defaults(
name.to_string(),
format!("owner/{name}"),
Filetype::Archive,
None,
None,
Channel::Stable,
Provider::Github,
None,
)
}
fn cleanup(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::remove_dir_all(parent)?;
}
Ok(())
}
#[test]
fn parse_set_key_requires_key_value_pair() {
assert!(MetadataManager::parse_set_key("is_pinned=true").is_ok());
assert!(MetadataManager::parse_set_key("invalid").is_err());
assert!(MetadataManager::parse_set_key("=value").is_err());
}
#[test]
fn pin_and_unpin_update_package_state() {
let path = temp_packages_file("pin");
fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
let metadata_path = temp_metadata_file("pin");
let mut storage = PackageStorage::new(&path).expect("create storage");
let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
storage
.add_or_update_package(test_package("fd"))
.expect("store package");
let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
let mut messages: Option<fn(&str)> = None;
manager
.pin_package("fd", None, &mut messages)
.expect("pin package");
assert!(
manager
.package_storage
.get_package_by_name("fd")
.expect("package")
.is_pinned
);
manager
.unpin_package("fd", &mut messages)
.expect("unpin package");
assert!(
!manager
.package_storage
.get_package_by_name("fd")
.expect("package")
.is_pinned
);
cleanup(&path).expect("cleanup");
let _ = cleanup(&metadata_path);
}
#[test]
fn set_key_and_get_key_support_nested_and_typed_values() {
let path = temp_packages_file("set-get");
fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
let metadata_path = temp_metadata_file("set-get");
let mut storage = PackageStorage::new(&path).expect("create storage");
let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
storage
.add_or_update_package(test_package("rg"))
.expect("store package");
let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
let mut messages: Option<fn(&str)> = None;
manager
.set_key("rg", "is_pinned=true", &mut messages)
.expect("set bool key");
manager
.set_key("rg", "version.major=12", &mut messages)
.expect("set nested numeric key");
assert_eq!(
manager
.get_key("rg", "is_pinned", &mut messages)
.expect("get bool"),
"true"
);
assert_eq!(
manager
.get_key("rg", "version.major", &mut messages)
.expect("get nested"),
"12"
);
cleanup(&path).expect("cleanup");
let _ = cleanup(&metadata_path);
}
#[test]
fn rename_package_rejects_duplicates_and_updates_alias() {
let path = temp_packages_file("rename");
fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
let metadata_path = temp_metadata_file("rename");
let mut storage = PackageStorage::new(&path).expect("create storage");
let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
storage
.add_or_update_package(test_package("old"))
.expect("store old");
storage
.add_or_update_package(test_package("taken"))
.expect("store taken");
let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
let mut messages: Option<fn(&str)> = None;
assert!(
manager
.rename_package("old", "taken", &mut messages)
.is_err()
);
manager
.rename_package("old", "new", &mut messages)
.expect("rename package");
assert!(manager.package_storage.get_package_by_name("new").is_some());
assert!(manager.package_storage.get_package_by_name("old").is_none());
cleanup(&path).expect("cleanup");
let _ = cleanup(&metadata_path);
}
#[test]
fn remove_package_deletes_metadata_and_errors_when_missing() {
let path = temp_packages_file("remove");
fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
let metadata_path = temp_metadata_file("remove");
let mut storage = PackageStorage::new(&path).expect("create storage");
let mut metadata_storage = MetadataStorage::new(&metadata_path).expect("metadata");
storage
.add_or_update_package(test_package("fd"))
.expect("store package");
let mut manager = MetadataManager::new(&mut storage, &mut metadata_storage);
let mut messages: Option<fn(&str)> = None;
manager
.remove_package("fd", &mut messages)
.expect("remove package metadata");
assert!(manager.package_storage.get_package_by_name("fd").is_none());
let err = manager
.remove_package("fd", &mut messages)
.expect_err("missing package should error");
assert!(err.to_string().contains("Package 'fd' not found"));
cleanup(&path).expect("cleanup");
let _ = cleanup(&metadata_path);
}
}