#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum EdgeType {
DelegatesTo,
Calls,
Reads,
Writes,
Approves,
Messages,
}
impl EdgeType {
pub fn as_str(&self) -> &'static str {
match self {
EdgeType::DelegatesTo => "delegates_to",
EdgeType::Calls => "calls",
EdgeType::Reads => "reads",
EdgeType::Writes => "writes",
EdgeType::Approves => "approves",
EdgeType::Messages => "messages",
}
}
pub const ALL: &'static [EdgeType] = &[
EdgeType::DelegatesTo,
EdgeType::Calls,
EdgeType::Reads,
EdgeType::Writes,
EdgeType::Approves,
EdgeType::Messages,
];
}
impl core::fmt::Display for EdgeType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(feature = "alloc")]
#[derive(Debug)]
pub struct UnknownEdgeType(pub alloc::string::String);
#[cfg(feature = "alloc")]
impl core::fmt::Display for UnknownEdgeType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "unknown edge type: {:?}", self.0)
}
}
#[cfg(feature = "alloc")]
impl core::convert::TryFrom<&str> for EdgeType {
type Error = UnknownEdgeType;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"delegates_to" => Ok(EdgeType::DelegatesTo),
"calls" => Ok(EdgeType::Calls),
"reads" => Ok(EdgeType::Reads),
"writes" => Ok(EdgeType::Writes),
"approves" => Ok(EdgeType::Approves),
"messages" => Ok(EdgeType::Messages),
other => Err(UnknownEdgeType(alloc::string::String::from(other))),
}
}
}
#[cfg(feature = "std")]
impl std::str::FromStr for EdgeType {
type Err = UnknownEdgeType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
EdgeType::try_from(s)
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct NewEdge {
pub source: crate::identity::AgentId,
pub target: crate::identity::AgentId,
pub edge_type: EdgeType,
pub metadata: Option<serde_json::Value>,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct Edge {
pub id: i64,
pub source: crate::identity::AgentId,
pub target: crate::identity::AgentId,
pub edge_type: EdgeType,
pub created_at: chrono::DateTime<chrono::Utc>,
pub metadata: Option<serde_json::Value>,
}
#[cfg(feature = "std")]
#[derive(Debug, thiserror::Error)]
pub enum EdgeRepoError {
#[error("edge store error: {0}")]
Store(String),
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
pub trait EdgeRepo: Send + Sync {
async fn insert(&self, edge: NewEdge) -> Result<i64, EdgeRepoError>;
async fn list_outgoing(
&self,
source: crate::identity::AgentId,
edge_type: Option<EdgeType>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError>;
async fn list_incoming(
&self,
target: crate::identity::AgentId,
edge_type: Option<EdgeType>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError>;
async fn list_by_type(
&self,
edge_type: EdgeType,
since: chrono::DateTime<chrono::Utc>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError>;
}
#[cfg(all(feature = "std", feature = "test-utils"))]
pub struct MockEdgeRepo {
inner: std::sync::Mutex<Vec<Edge>>,
next_id: std::sync::atomic::AtomicI64,
}
#[cfg(all(feature = "std", feature = "test-utils"))]
impl MockEdgeRepo {
pub fn new() -> Self {
Self {
inner: std::sync::Mutex::new(Vec::new()),
next_id: std::sync::atomic::AtomicI64::new(1),
}
}
pub fn snapshot(&self) -> Vec<Edge> {
self.inner.lock().expect("mock lock poisoned").clone()
}
}
#[cfg(all(feature = "std", feature = "test-utils"))]
impl Default for MockEdgeRepo {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(feature = "std", feature = "test-utils"))]
#[async_trait::async_trait]
impl EdgeRepo for MockEdgeRepo {
async fn insert(&self, edge: NewEdge) -> Result<i64, EdgeRepoError> {
let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let record = Edge {
id,
source: edge.source,
target: edge.target,
edge_type: edge.edge_type,
created_at: chrono::Utc::now(),
metadata: edge.metadata,
};
self.inner.lock().expect("mock lock poisoned").push(record);
Ok(id)
}
async fn list_outgoing(
&self,
source: crate::identity::AgentId,
edge_type: Option<EdgeType>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError> {
let data = self.inner.lock().expect("mock lock poisoned");
Ok(data
.iter()
.filter(|e| e.source == source && edge_type.map_or(true, |et| e.edge_type == et))
.rev()
.take((limit as usize).min(1000))
.cloned()
.collect())
}
async fn list_incoming(
&self,
target: crate::identity::AgentId,
edge_type: Option<EdgeType>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError> {
let data = self.inner.lock().expect("mock lock poisoned");
Ok(data
.iter()
.filter(|e| e.target == target && edge_type.map_or(true, |et| e.edge_type == et))
.rev()
.take((limit as usize).min(1000))
.cloned()
.collect())
}
async fn list_by_type(
&self,
edge_type: EdgeType,
since: chrono::DateTime<chrono::Utc>,
limit: u32,
) -> Result<Vec<Edge>, EdgeRepoError> {
let data = self.inner.lock().expect("mock lock poisoned");
Ok(data
.iter()
.filter(|e| e.edge_type == edge_type && e.created_at >= since)
.rev()
.take((limit as usize).min(1000))
.cloned()
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::convert::TryFrom;
#[test]
fn all_six_variants_parse_from_wire_strings() {
let cases = [
("delegates_to", EdgeType::DelegatesTo),
("calls", EdgeType::Calls),
("reads", EdgeType::Reads),
("writes", EdgeType::Writes),
("approves", EdgeType::Approves),
("messages", EdgeType::Messages),
];
for (s, expected) in cases {
assert_eq!(EdgeType::try_from(s).unwrap(), expected, "parsing {s:?}");
}
}
#[test]
fn unknown_string_returns_error() {
assert!(EdgeType::try_from("follows").is_err());
assert!(EdgeType::try_from("").is_err());
}
#[test]
fn as_str_round_trips() {
for &variant in EdgeType::ALL {
assert_eq!(EdgeType::try_from(variant.as_str()).unwrap(), variant);
}
}
#[test]
fn from_str_parses_all_six_variants() {
use std::str::FromStr;
for &variant in EdgeType::ALL {
assert_eq!(EdgeType::from_str(variant.as_str()).unwrap(), variant);
}
}
#[test]
fn str_parse_parses_all_six_variants() {
for &variant in EdgeType::ALL {
assert_eq!(variant.as_str().parse::<EdgeType>().unwrap(), variant);
}
}
#[test]
fn display_matches_as_str() {
for &variant in EdgeType::ALL {
assert_eq!(format!("{variant}"), variant.as_str());
}
}
#[test]
fn all_contains_all_six_variants() {
assert_eq!(EdgeType::ALL.len(), 6);
}
}