1use serde::{Deserialize, Serialize};
7use tracing::debug;
8
9use punch_types::{PunchError, PunchResult};
10
11use crate::MemorySubstrate;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ChannelRecord {
16 pub id: String,
18 pub name: String,
20 pub platform: String,
22 pub credentials: String,
24 pub settings: String,
26 pub status: String,
28 pub validated_at: Option<String>,
30 pub created_at: String,
32 pub updated_at: String,
34}
35
36impl MemorySubstrate {
37 pub async fn save_channel(&self, record: &ChannelRecord) -> PunchResult<()> {
39 let conn = self.conn.lock().await;
40 conn.execute(
41 "INSERT INTO channels (id, name, platform, credentials, settings, status, validated_at, created_at, updated_at)
42 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
43 ON CONFLICT(name) DO UPDATE SET
44 platform = excluded.platform,
45 credentials = excluded.credentials,
46 settings = excluded.settings,
47 status = excluded.status,
48 validated_at = excluded.validated_at,
49 updated_at = excluded.updated_at",
50 rusqlite::params![
51 record.id,
52 record.name,
53 record.platform,
54 record.credentials,
55 record.settings,
56 record.status,
57 record.validated_at,
58 record.created_at,
59 record.updated_at,
60 ],
61 )
62 .map_err(|e| PunchError::Memory(format!("failed to save channel: {e}")))?;
63
64 debug!(name = %record.name, platform = %record.platform, "channel saved");
65 Ok(())
66 }
67
68 pub async fn load_channel(&self, name: &str) -> PunchResult<Option<ChannelRecord>> {
70 let conn = self.conn.lock().await;
71 let mut stmt = conn
72 .prepare(
73 "SELECT id, name, platform, credentials, settings, status, validated_at, created_at, updated_at
74 FROM channels WHERE name = ?1",
75 )
76 .map_err(|e| PunchError::Memory(format!("failed to prepare channel query: {e}")))?;
77
78 let result = stmt
79 .query_row([name], |row| {
80 Ok(ChannelRecord {
81 id: row.get(0)?,
82 name: row.get(1)?,
83 platform: row.get(2)?,
84 credentials: row.get(3)?,
85 settings: row.get(4)?,
86 status: row.get(5)?,
87 validated_at: row.get(6)?,
88 created_at: row.get(7)?,
89 updated_at: row.get(8)?,
90 })
91 })
92 .ok();
93
94 Ok(result)
95 }
96
97 pub async fn list_channels(&self) -> PunchResult<Vec<ChannelRecord>> {
99 let conn = self.conn.lock().await;
100 let mut stmt = conn
101 .prepare(
102 "SELECT id, name, platform, credentials, settings, status, validated_at, created_at, updated_at
103 FROM channels ORDER BY created_at DESC",
104 )
105 .map_err(|e| PunchError::Memory(format!("failed to list channels: {e}")))?;
106
107 let rows = stmt
108 .query_map([], |row| {
109 Ok(ChannelRecord {
110 id: row.get(0)?,
111 name: row.get(1)?,
112 platform: row.get(2)?,
113 credentials: row.get(3)?,
114 settings: row.get(4)?,
115 status: row.get(5)?,
116 validated_at: row.get(6)?,
117 created_at: row.get(7)?,
118 updated_at: row.get(8)?,
119 })
120 })
121 .map_err(|e| PunchError::Memory(format!("failed to read channel rows: {e}")))?;
122
123 let mut channels = Vec::new();
124 for row in rows {
125 let record =
126 row.map_err(|e| PunchError::Memory(format!("failed to read channel: {e}")))?;
127 channels.push(record);
128 }
129 Ok(channels)
130 }
131
132 pub async fn delete_channel(&self, name: &str) -> PunchResult<()> {
134 let conn = self.conn.lock().await;
135 conn.execute("DELETE FROM channels WHERE name = ?1", [name])
136 .map_err(|e| PunchError::Memory(format!("failed to delete channel: {e}")))?;
137 debug!(name = name, "channel deleted");
138 Ok(())
139 }
140
141 pub async fn update_channel_status(&self, name: &str, status: &str) -> PunchResult<()> {
143 let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
144 let conn = self.conn.lock().await;
145 conn.execute(
146 "UPDATE channels SET status = ?1, updated_at = ?2 WHERE name = ?3",
147 rusqlite::params![status, now, name],
148 )
149 .map_err(|e| PunchError::Memory(format!("failed to update channel status: {e}")))?;
150 debug!(name = name, status = status, "channel status updated");
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::MemorySubstrate;
159
160 fn make_record(name: &str, platform: &str) -> ChannelRecord {
161 let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
162 ChannelRecord {
163 id: uuid::Uuid::new_v4().to_string(),
164 name: name.to_string(),
165 platform: platform.to_string(),
166 credentials: "{}".to_string(),
167 settings: "{}".to_string(),
168 status: "disconnected".to_string(),
169 validated_at: None,
170 created_at: now.clone(),
171 updated_at: now,
172 }
173 }
174
175 #[tokio::test]
176 async fn test_save_and_load_channel() {
177 let substrate = MemorySubstrate::in_memory().unwrap();
178 let record = make_record("my-telegram", "telegram");
179
180 substrate.save_channel(&record).await.unwrap();
181 let loaded = substrate.load_channel("my-telegram").await.unwrap();
182 assert!(loaded.is_some());
183 let loaded = loaded.unwrap();
184 assert_eq!(loaded.name, "my-telegram");
185 assert_eq!(loaded.platform, "telegram");
186 assert_eq!(loaded.status, "disconnected");
187 }
188
189 #[tokio::test]
190 async fn test_save_channel_upsert() {
191 let substrate = MemorySubstrate::in_memory().unwrap();
192 let mut record = make_record("my-slack", "slack");
193 substrate.save_channel(&record).await.unwrap();
194
195 record.status = "connected".to_string();
196 substrate.save_channel(&record).await.unwrap();
197
198 let loaded = substrate.load_channel("my-slack").await.unwrap().unwrap();
199 assert_eq!(loaded.status, "connected");
200 }
201
202 #[tokio::test]
203 async fn test_list_channels() {
204 let substrate = MemorySubstrate::in_memory().unwrap();
205 substrate
206 .save_channel(&make_record("ch-1", "telegram"))
207 .await
208 .unwrap();
209 substrate
210 .save_channel(&make_record("ch-2", "slack"))
211 .await
212 .unwrap();
213 substrate
214 .save_channel(&make_record("ch-3", "discord"))
215 .await
216 .unwrap();
217
218 let channels = substrate.list_channels().await.unwrap();
219 assert_eq!(channels.len(), 3);
220 }
221
222 #[tokio::test]
223 async fn test_delete_channel() {
224 let substrate = MemorySubstrate::in_memory().unwrap();
225 substrate
226 .save_channel(&make_record("to-delete", "telegram"))
227 .await
228 .unwrap();
229
230 assert!(substrate.load_channel("to-delete").await.unwrap().is_some());
231
232 substrate.delete_channel("to-delete").await.unwrap();
233 assert!(substrate.load_channel("to-delete").await.unwrap().is_none());
234 }
235
236 #[tokio::test]
237 async fn test_update_channel_status() {
238 let substrate = MemorySubstrate::in_memory().unwrap();
239 substrate
240 .save_channel(&make_record("status-test", "discord"))
241 .await
242 .unwrap();
243
244 substrate
245 .update_channel_status("status-test", "connected")
246 .await
247 .unwrap();
248
249 let loaded = substrate
250 .load_channel("status-test")
251 .await
252 .unwrap()
253 .unwrap();
254 assert_eq!(loaded.status, "connected");
255 }
256
257 #[tokio::test]
258 async fn test_load_nonexistent_channel() {
259 let substrate = MemorySubstrate::in_memory().unwrap();
260 let result = substrate.load_channel("nonexistent").await.unwrap();
261 assert!(result.is_none());
262 }
263}