1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
10pub enum EdgeType {
11 DelegatesTo,
13 Calls,
15 Reads,
17 Writes,
19 Approves,
21 Messages,
23}
24
25impl EdgeType {
26 pub fn as_str(&self) -> &'static str {
28 match self {
29 EdgeType::DelegatesTo => "delegates_to",
30 EdgeType::Calls => "calls",
31 EdgeType::Reads => "reads",
32 EdgeType::Writes => "writes",
33 EdgeType::Approves => "approves",
34 EdgeType::Messages => "messages",
35 }
36 }
37
38 pub const ALL: &'static [EdgeType] = &[
40 EdgeType::DelegatesTo,
41 EdgeType::Calls,
42 EdgeType::Reads,
43 EdgeType::Writes,
44 EdgeType::Approves,
45 EdgeType::Messages,
46 ];
47}
48
49impl core::fmt::Display for EdgeType {
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[cfg(feature = "alloc")]
57#[derive(Debug)]
58pub struct UnknownEdgeType(pub alloc::string::String);
59
60#[cfg(feature = "alloc")]
61impl core::fmt::Display for UnknownEdgeType {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 write!(f, "unknown edge type: {:?}", self.0)
64 }
65}
66
67#[cfg(feature = "alloc")]
68impl core::convert::TryFrom<&str> for EdgeType {
69 type Error = UnknownEdgeType;
70
71 fn try_from(s: &str) -> Result<Self, Self::Error> {
72 match s {
73 "delegates_to" => Ok(EdgeType::DelegatesTo),
74 "calls" => Ok(EdgeType::Calls),
75 "reads" => Ok(EdgeType::Reads),
76 "writes" => Ok(EdgeType::Writes),
77 "approves" => Ok(EdgeType::Approves),
78 "messages" => Ok(EdgeType::Messages),
79 other => Err(UnknownEdgeType(alloc::string::String::from(other))),
80 }
81 }
82}
83
84#[cfg(feature = "std")]
85impl std::str::FromStr for EdgeType {
86 type Err = UnknownEdgeType;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 EdgeType::try_from(s)
90 }
91}
92
93#[cfg(feature = "std")]
95#[derive(Debug, Clone)]
96pub struct NewEdge {
97 pub source: crate::identity::AgentId,
99 pub target: crate::identity::AgentId,
101 pub edge_type: EdgeType,
103 pub metadata: Option<serde_json::Value>,
105}
106
107#[cfg(feature = "std")]
109#[derive(Debug, Clone)]
110pub struct Edge {
111 pub id: i64,
113 pub source: crate::identity::AgentId,
115 pub target: crate::identity::AgentId,
117 pub edge_type: EdgeType,
119 pub created_at: chrono::DateTime<chrono::Utc>,
121 pub metadata: Option<serde_json::Value>,
123}
124
125#[cfg(feature = "std")]
127#[derive(Debug, thiserror::Error)]
128pub enum EdgeRepoError {
129 #[error("edge store error: {0}")]
131 Store(String),
132}
133
134#[cfg(feature = "std")]
141#[async_trait::async_trait]
142pub trait EdgeRepo: Send + Sync {
143 async fn insert(&self, edge: NewEdge) -> Result<i64, EdgeRepoError>;
145
146 async fn list_outgoing(
151 &self,
152 source: crate::identity::AgentId,
153 edge_type: Option<EdgeType>,
154 limit: u32,
155 ) -> Result<Vec<Edge>, EdgeRepoError>;
156
157 async fn list_incoming(
162 &self,
163 target: crate::identity::AgentId,
164 edge_type: Option<EdgeType>,
165 limit: u32,
166 ) -> Result<Vec<Edge>, EdgeRepoError>;
167
168 async fn list_by_type(
172 &self,
173 edge_type: EdgeType,
174 since: chrono::DateTime<chrono::Utc>,
175 limit: u32,
176 ) -> Result<Vec<Edge>, EdgeRepoError>;
177}
178
179#[cfg(all(feature = "std", feature = "test-utils"))]
185pub struct MockEdgeRepo {
186 inner: std::sync::Mutex<Vec<Edge>>,
187 next_id: std::sync::atomic::AtomicI64,
188}
189
190#[cfg(all(feature = "std", feature = "test-utils"))]
191impl MockEdgeRepo {
192 pub fn new() -> Self {
194 Self {
195 inner: std::sync::Mutex::new(Vec::new()),
196 next_id: std::sync::atomic::AtomicI64::new(1),
197 }
198 }
199
200 pub fn snapshot(&self) -> Vec<Edge> {
202 self.inner.lock().expect("mock lock poisoned").clone()
203 }
204}
205
206#[cfg(all(feature = "std", feature = "test-utils"))]
207impl Default for MockEdgeRepo {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(all(feature = "std", feature = "test-utils"))]
214#[async_trait::async_trait]
215impl EdgeRepo for MockEdgeRepo {
216 async fn insert(&self, edge: NewEdge) -> Result<i64, EdgeRepoError> {
217 let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
218 let record = Edge {
219 id,
220 source: edge.source,
221 target: edge.target,
222 edge_type: edge.edge_type,
223 created_at: chrono::Utc::now(),
224 metadata: edge.metadata,
225 };
226 self.inner.lock().expect("mock lock poisoned").push(record);
227 Ok(id)
228 }
229
230 async fn list_outgoing(
231 &self,
232 source: crate::identity::AgentId,
233 edge_type: Option<EdgeType>,
234 limit: u32,
235 ) -> Result<Vec<Edge>, EdgeRepoError> {
236 let data = self.inner.lock().expect("mock lock poisoned");
237 Ok(data
238 .iter()
239 .filter(|e| e.source == source && edge_type.map_or(true, |et| e.edge_type == et))
240 .rev()
241 .take((limit as usize).min(1000))
242 .cloned()
243 .collect())
244 }
245
246 async fn list_incoming(
247 &self,
248 target: crate::identity::AgentId,
249 edge_type: Option<EdgeType>,
250 limit: u32,
251 ) -> Result<Vec<Edge>, EdgeRepoError> {
252 let data = self.inner.lock().expect("mock lock poisoned");
253 Ok(data
254 .iter()
255 .filter(|e| e.target == target && edge_type.map_or(true, |et| e.edge_type == et))
256 .rev()
257 .take((limit as usize).min(1000))
258 .cloned()
259 .collect())
260 }
261
262 async fn list_by_type(
263 &self,
264 edge_type: EdgeType,
265 since: chrono::DateTime<chrono::Utc>,
266 limit: u32,
267 ) -> Result<Vec<Edge>, EdgeRepoError> {
268 let data = self.inner.lock().expect("mock lock poisoned");
269 Ok(data
270 .iter()
271 .filter(|e| e.edge_type == edge_type && e.created_at >= since)
272 .rev()
273 .take((limit as usize).min(1000))
274 .cloned()
275 .collect())
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use core::convert::TryFrom;
283
284 #[test]
285 fn all_six_variants_parse_from_wire_strings() {
286 let cases = [
287 ("delegates_to", EdgeType::DelegatesTo),
288 ("calls", EdgeType::Calls),
289 ("reads", EdgeType::Reads),
290 ("writes", EdgeType::Writes),
291 ("approves", EdgeType::Approves),
292 ("messages", EdgeType::Messages),
293 ];
294 for (s, expected) in cases {
295 assert_eq!(EdgeType::try_from(s).unwrap(), expected, "parsing {s:?}");
296 }
297 }
298
299 #[test]
300 fn unknown_string_returns_error() {
301 assert!(EdgeType::try_from("follows").is_err());
302 assert!(EdgeType::try_from("").is_err());
303 }
304
305 #[test]
306 fn as_str_round_trips() {
307 for &variant in EdgeType::ALL {
308 assert_eq!(EdgeType::try_from(variant.as_str()).unwrap(), variant);
309 }
310 }
311
312 #[test]
313 fn from_str_parses_all_six_variants() {
314 use std::str::FromStr;
315 for &variant in EdgeType::ALL {
316 assert_eq!(EdgeType::from_str(variant.as_str()).unwrap(), variant);
317 }
318 }
319
320 #[test]
321 fn str_parse_parses_all_six_variants() {
322 for &variant in EdgeType::ALL {
323 assert_eq!(variant.as_str().parse::<EdgeType>().unwrap(), variant);
324 }
325 }
326
327 #[test]
328 fn display_matches_as_str() {
329 for &variant in EdgeType::ALL {
330 assert_eq!(format!("{variant}"), variant.as_str());
331 }
332 }
333
334 #[test]
335 fn all_contains_all_six_variants() {
336 assert_eq!(EdgeType::ALL.len(), 6);
337 }
338}