use crate::services::storage::{
metadata_storage::MetadataStorage, package_storage::PackageStorage,
};
use anyhow::{Context, Result};
pub struct MetadataManager<'a> {
package_storage: &'a mut PackageStorage,
metadata_storage: &'a mut MetadataStorage,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataSetResult {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataBulkSetResult {
pub applied: Vec<MetadataSetResult>,
pub failures: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataBulkGetResult {
pub values: Vec<(String, String)>,
pub failures: Vec<(String, String)>,
}
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(&mut self, name: &str, reason: Option<String>) -> Result<()> {
let package = self
.package_storage
.get_mut_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
if package.is_pinned {
return Ok(());
}
package.is_pinned = true;
self.package_storage.save_packages()?;
if let Some(reason) = reason {
self.metadata_storage.set_pin_reason(name, reason)?;
}
Ok(())
}
pub fn unpin_package(&mut self, name: &str) -> Result<()> {
let package = self
.package_storage
.get_mut_package_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Package '{}' not found", name))?;
if !package.is_pinned {
return Ok(());
}
package.is_pinned = false;
self.package_storage.save_packages()?;
self.metadata_storage.clear_pin_reason(name)?;
Ok(())
}
pub fn remove_package(&mut self, name: &str) -> Result<()> {
if !self.package_storage.remove_package_by_name(name)? {
return Err(anyhow::anyhow!("Package '{}' not found", name));
}
self.metadata_storage.remove_package(name)?;
Ok(())
}
pub fn rename_package(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
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 {
return Ok(false);
}
if self.package_storage.get_package_by_name(new_name).is_some() {
return Err(anyhow::anyhow!("Package '{}' already exists", 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)?;
Ok(true)
}
pub fn set_key(&mut self, name: &str, set_key: &str) -> Result<MetadataSetResult> {
let (key_path, value) = Self::parse_set_key(set_key)?;
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)?;
Ok(MetadataSetResult {
key: key_path,
value,
})
}
pub fn get_key(&self, name: &str, get_key: &str) -> Result<String> {
let key_path = get_key.trim();
if key_path.is_empty() {
return Err(anyhow::anyhow!("Key path cannot be empty"));
}
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);
Ok(value_str)
}
pub fn set_bulk(&mut self, name: &str, set_keys: &[String]) -> MetadataBulkSetResult {
let mut applied = Vec::new();
let mut failures = Vec::new();
for set_key in set_keys {
match self.set_key(name, set_key) {
Ok(result) => applied.push(result),
Err(err) => failures.push((set_key.clone(), err.to_string())),
}
}
MetadataBulkSetResult { applied, failures }
}
pub fn get_bulk(&self, name: &str, get_keys: &[String]) -> MetadataBulkGetResult {
let mut values = Vec::new();
let mut failures = Vec::new();
for get_key in get_keys {
match self.get_key(name, get_key) {
Ok(value) => {
values.push((get_key.clone(), value));
}
Err(err) => failures.push((get_key.clone(), err.to_string())),
}
}
MetadataBulkGetResult { values, failures }
}
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);
manager.pin_package("fd", None).expect("pin package");
assert!(
manager
.package_storage
.get_package_by_name("fd")
.expect("package")
.is_pinned
);
manager.unpin_package("fd").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);
manager
.set_key("rg", "is_pinned=true")
.expect("set bool key");
manager
.set_key("rg", "version.major=12")
.expect("set nested numeric key");
assert_eq!(
manager.get_key("rg", "is_pinned").expect("get bool"),
"true"
);
assert_eq!(
manager.get_key("rg", "version.major").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);
assert!(manager.rename_package("old", "taken").is_err());
manager
.rename_package("old", "new")
.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);
manager
.remove_package("fd")
.expect("remove package metadata");
assert!(manager.package_storage.get_package_by_name("fd").is_none());
let err = manager
.remove_package("fd")
.expect_err("missing package should error");
assert!(err.to_string().contains("Package 'fd' not found"));
cleanup(&path).expect("cleanup");
let _ = cleanup(&metadata_path);
}
}