use anyhow::Result;
use std::collections::HashMap;
use crate::lockfile::ResourceId;
#[derive(Debug, Clone)]
pub struct TransitiveChangeTracker {
changed_resources: HashMap<String, (String, String, String, Option<serde_json::Value>)>,
}
impl TransitiveChangeTracker {
pub fn new() -> Self {
Self {
changed_resources: HashMap::new(),
}
}
pub fn record_change(
&mut self,
resource_id: &str,
old_version: &str,
new_version: &str,
new_sha: &str,
variant_inputs: Option<serde_json::Value>,
) {
self.changed_resources.insert(
resource_id.to_string(),
(old_version.to_string(), new_version.to_string(), new_sha.to_string(), variant_inputs),
);
}
pub fn get_changed_resources(
&self,
) -> &HashMap<String, (String, String, String, Option<serde_json::Value>)> {
&self.changed_resources
}
pub fn clear(&mut self) {
self.changed_resources.clear();
}
}
impl Default for TransitiveChangeTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ResourceParams {
pub resource_id: ResourceId,
pub version: String,
pub sha: String,
pub version_constraint: String,
pub required_by: String,
}
#[derive(Debug, Clone)]
pub struct ResourceEntry {
pub resource_id: ResourceId,
pub version: String,
pub sha: String,
pub version_constraint: String,
pub required_by: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ResourceRegistry {
resources: HashMap<String, ResourceEntry>,
}
impl ResourceRegistry {
pub fn new() -> Self {
Self {
resources: HashMap::new(),
}
}
pub fn add_or_update_resource(&mut self, params: ResourceParams) {
let ResourceParams {
resource_id,
version,
sha,
version_constraint,
required_by,
} = params;
let resource_id_string =
resource_id_to_string(&resource_id).expect("ResourceId should have a valid source");
self.resources
.entry(resource_id_string.clone())
.and_modify(|entry| {
entry.version = version.clone();
entry.sha = sha.clone();
if !entry.required_by.contains(&required_by) {
entry.required_by.push(required_by.clone());
}
})
.or_insert_with(|| ResourceEntry {
resource_id: resource_id.clone(),
version,
sha,
version_constraint,
required_by: vec![required_by],
});
}
pub fn all_resources(&self) -> impl Iterator<Item = &ResourceEntry> {
self.resources.values()
}
pub fn update_version_and_sha(
&mut self,
resource_id: &str,
new_version: String,
new_sha: String,
) {
if let Some(entry) = self.resources.get_mut(resource_id) {
entry.version = new_version;
entry.sha = new_sha;
}
}
}
impl Default for ResourceRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn parse_resource_id_string(resource_id: &str) -> Result<(&str, &str)> {
let parts: Vec<&str> = resource_id.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("Invalid resource_id format: {}", resource_id));
}
Ok((parts[0], parts[1]))
}
pub fn resource_id_to_string(resource_id: &ResourceId) -> Result<String> {
let source = resource_id
.source()
.ok_or_else(|| anyhow::anyhow!("Resource {} has no source", resource_id))?;
Ok(format!("{}:{}", source, resource_id.name()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_resource_id() {
let (source, path) = parse_resource_id_string("community:agents/helper.md").unwrap();
assert_eq!(source, "community");
assert_eq!(path, "agents/helper.md");
}
#[test]
fn test_parse_resource_id_invalid() {
let result = parse_resource_id_string("invalid");
assert!(result.is_err());
}
}