agpm_cli/resolver/backtracking/
registry.rs1use anyhow::Result;
9use std::collections::HashMap;
10
11use crate::lockfile::ResourceId;
12
13#[derive(Debug, Clone)]
19pub struct TransitiveChangeTracker {
20 changed_resources: HashMap<String, (String, String, String, Option<serde_json::Value>)>,
22}
23
24impl TransitiveChangeTracker {
25 pub fn new() -> Self {
26 Self {
27 changed_resources: HashMap::new(),
28 }
29 }
30
31 pub fn record_change(
32 &mut self,
33 resource_id: &str,
34 old_version: &str,
35 new_version: &str,
36 new_sha: &str,
37 variant_inputs: Option<serde_json::Value>,
38 ) {
39 self.changed_resources.insert(
40 resource_id.to_string(),
41 (old_version.to_string(), new_version.to_string(), new_sha.to_string(), variant_inputs),
42 );
43 }
44
45 pub fn get_changed_resources(
46 &self,
47 ) -> &HashMap<String, (String, String, String, Option<serde_json::Value>)> {
48 &self.changed_resources
49 }
50
51 pub fn clear(&mut self) {
52 self.changed_resources.clear();
53 }
54}
55
56impl Default for TransitiveChangeTracker {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct ResourceParams {
65 pub resource_id: ResourceId,
66 pub version: String,
67 pub sha: String,
68 pub version_constraint: String,
69 pub required_by: String,
70}
71
72#[derive(Debug, Clone)]
74pub struct ResourceEntry {
75 pub resource_id: ResourceId,
77
78 pub version: String,
80
81 pub sha: String,
83
84 pub version_constraint: String,
86
87 pub required_by: Vec<String>,
89}
90
91#[derive(Debug, Clone)]
97pub struct ResourceRegistry {
98 resources: HashMap<String, ResourceEntry>,
100}
101
102impl ResourceRegistry {
103 pub fn new() -> Self {
104 Self {
105 resources: HashMap::new(),
106 }
107 }
108
109 pub fn add_or_update_resource(&mut self, params: ResourceParams) {
114 let ResourceParams {
115 resource_id,
116 version,
117 sha,
118 version_constraint,
119 required_by,
120 } = params;
121
122 let resource_id_string =
124 resource_id_to_string(&resource_id).expect("ResourceId should have a valid source");
125
126 self.resources
127 .entry(resource_id_string.clone())
128 .and_modify(|entry| {
129 entry.version = version.clone();
130 entry.sha = sha.clone();
131 if !entry.required_by.contains(&required_by) {
132 entry.required_by.push(required_by.clone());
133 }
134 })
135 .or_insert_with(|| ResourceEntry {
136 resource_id: resource_id.clone(),
137 version,
138 sha,
139 version_constraint,
140 required_by: vec![required_by],
141 });
142 }
143
144 pub fn all_resources(&self) -> impl Iterator<Item = &ResourceEntry> {
146 self.resources.values()
147 }
148
149 pub fn update_version_and_sha(
154 &mut self,
155 resource_id: &str,
156 new_version: String,
157 new_sha: String,
158 ) {
159 if let Some(entry) = self.resources.get_mut(resource_id) {
160 entry.version = new_version;
161 entry.sha = new_sha;
162 }
163 }
164}
165
166impl Default for ResourceRegistry {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172pub fn parse_resource_id_string(resource_id: &str) -> Result<(&str, &str)> {
174 let parts: Vec<&str> = resource_id.splitn(2, ':').collect();
175 if parts.len() != 2 {
176 return Err(anyhow::anyhow!("Invalid resource_id format: {}", resource_id));
177 }
178 Ok((parts[0], parts[1]))
179}
180
181pub fn resource_id_to_string(resource_id: &ResourceId) -> Result<String> {
183 let source = resource_id
184 .source()
185 .ok_or_else(|| anyhow::anyhow!("Resource {} has no source", resource_id))?;
186 Ok(format!("{}:{}", source, resource_id.name()))
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_parse_resource_id() {
195 let (source, path) = parse_resource_id_string("community:agents/helper.md").unwrap();
196 assert_eq!(source, "community");
197 assert_eq!(path, "agents/helper.md");
198 }
199
200 #[test]
201 fn test_parse_resource_id_invalid() {
202 let result = parse_resource_id_string("invalid");
203 assert!(result.is_err());
204 }
205}