1use a2a::*;
4use async_trait::async_trait;
5use std::collections::HashMap;
6use tokio::sync::RwLock;
7
8#[async_trait]
10pub trait PushConfigStore: Send + Sync + 'static {
11 async fn save(
13 &self,
14 task_id: &str,
15 config: PushNotificationConfig,
16 ) -> Result<PushNotificationConfig, A2AError>;
17
18 async fn get(&self, task_id: &str, config_id: &str)
20 -> Result<PushNotificationConfig, A2AError>;
21
22 async fn list(&self, task_id: &str) -> Result<Vec<PushNotificationConfig>, A2AError>;
24
25 async fn delete(&self, task_id: &str, config_id: &str) -> Result<(), A2AError>;
27
28 async fn delete_all(&self, task_id: &str) -> Result<(), A2AError>;
30}
31
32pub struct InMemoryPushConfigStore {
34 configs: RwLock<HashMap<String, HashMap<String, PushNotificationConfig>>>,
35}
36
37impl InMemoryPushConfigStore {
38 pub fn new() -> Self {
39 InMemoryPushConfigStore {
40 configs: RwLock::new(HashMap::new()),
41 }
42 }
43}
44
45impl Default for InMemoryPushConfigStore {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51#[async_trait]
52impl PushConfigStore for InMemoryPushConfigStore {
53 async fn save(
54 &self,
55 task_id: &str,
56 mut config: PushNotificationConfig,
57 ) -> Result<PushNotificationConfig, A2AError> {
58 if config.url.is_empty() {
59 return Err(A2AError::invalid_params("push config URL cannot be empty"));
60 }
61
62 if config.id.is_none() {
63 config.id = Some(uuid::Uuid::now_v7().to_string());
64 }
65
66 let mut store = self.configs.write().await;
67 let task_configs = store.entry(task_id.to_string()).or_default();
68 let config_id = config.id.clone().unwrap();
69 task_configs.insert(config_id, config.clone());
70 Ok(config)
71 }
72
73 async fn get(
74 &self,
75 task_id: &str,
76 config_id: &str,
77 ) -> Result<PushNotificationConfig, A2AError> {
78 let store = self.configs.read().await;
79 store
80 .get(task_id)
81 .and_then(|configs| configs.get(config_id))
82 .cloned()
83 .ok_or_else(A2AError::push_notification_not_supported)
84 }
85
86 async fn list(&self, task_id: &str) -> Result<Vec<PushNotificationConfig>, A2AError> {
87 let store = self.configs.read().await;
88 Ok(store
89 .get(task_id)
90 .map(|configs| configs.values().cloned().collect())
91 .unwrap_or_default())
92 }
93
94 async fn delete(&self, task_id: &str, config_id: &str) -> Result<(), A2AError> {
95 let mut store = self.configs.write().await;
96 if let Some(configs) = store.get_mut(task_id) {
97 configs.remove(config_id);
98 }
99 Ok(())
100 }
101
102 async fn delete_all(&self, task_id: &str) -> Result<(), A2AError> {
103 let mut store = self.configs.write().await;
104 store.remove(task_id);
105 Ok(())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn make_config(url: &str) -> PushNotificationConfig {
114 PushNotificationConfig {
115 url: url.to_string(),
116 id: None,
117 token: None,
118 authentication: None,
119 }
120 }
121
122 #[tokio::test]
123 async fn test_save_and_get() {
124 let store = InMemoryPushConfigStore::new();
125 let config = store
126 .save("t1", make_config("https://example.com/hook"))
127 .await
128 .unwrap();
129 assert!(config.id.is_some());
130
131 let got = store.get("t1", config.id.as_ref().unwrap()).await.unwrap();
132 assert_eq!(got.url, "https://example.com/hook");
133 }
134
135 #[tokio::test]
136 async fn test_save_empty_url() {
137 let store = InMemoryPushConfigStore::new();
138 let result = store.save("t1", make_config("")).await;
139 assert!(result.is_err());
140 }
141
142 #[tokio::test]
143 async fn test_save_with_existing_id() {
144 let store = InMemoryPushConfigStore::new();
145 let mut config = make_config("https://example.com/hook");
146 config.id = Some("my-id".to_string());
147 let saved = store.save("t1", config).await.unwrap();
148 assert_eq!(saved.id.as_deref(), Some("my-id"));
149 }
150
151 #[tokio::test]
152 async fn test_get_not_found() {
153 let store = InMemoryPushConfigStore::new();
154 let result = store.get("t0", "nonexistent").await;
155 assert!(result.is_err());
156 }
157
158 #[tokio::test]
159 async fn test_list() {
160 let store = InMemoryPushConfigStore::new();
161 store
162 .save("t1", make_config("https://a.com"))
163 .await
164 .unwrap();
165 store
166 .save("t1", make_config("https://b.com"))
167 .await
168 .unwrap();
169 store
170 .save("t2", make_config("https://c.com"))
171 .await
172 .unwrap();
173
174 let configs = store.list("t1").await.unwrap();
175 assert_eq!(configs.len(), 2);
176
177 let configs = store.list("t2").await.unwrap();
178 assert_eq!(configs.len(), 1);
179
180 let configs = store.list("t3").await.unwrap();
181 assert_eq!(configs.len(), 0);
182 }
183
184 #[tokio::test]
185 async fn test_delete() {
186 let store = InMemoryPushConfigStore::new();
187 let config = store
188 .save("t1", make_config("https://a.com"))
189 .await
190 .unwrap();
191 let config_id = config.id.unwrap();
192
193 store.delete("t1", &config_id).await.unwrap();
194 let result = store.get("t1", &config_id).await;
195 assert!(result.is_err());
196 }
197
198 #[tokio::test]
199 async fn test_delete_all() {
200 let store = InMemoryPushConfigStore::new();
201 store
202 .save("t1", make_config("https://a.com"))
203 .await
204 .unwrap();
205 store
206 .save("t1", make_config("https://b.com"))
207 .await
208 .unwrap();
209
210 store.delete_all("t1").await.unwrap();
211 let configs = store.list("t1").await.unwrap();
212 assert_eq!(configs.len(), 0);
213 }
214}