Skip to main content

seshat_core/
edge.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::ParseEnumError;
4use crate::ids::{BranchId, EdgeId, NodeId};
5
6/// A typed edge between two knowledge nodes in the graph.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub struct Edge {
10    pub id: EdgeId,
11    pub source_id: NodeId,
12    pub target_id: NodeId,
13    pub edge_type: EdgeType,
14    pub branch_id: BranchId,
15    pub weight: f64,
16    /// JSON-encoded edge-specific metadata.
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub metadata: Option<serde_json::Value>,
19}
20
21/// The type of relationship an edge represents.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum EdgeType {
25    /// General topical relationship.
26    RelatedTo,
27    /// Source updates/supersedes target.
28    Updates,
29    /// Source contradicts target (code vs documentation).
30    Contradicts,
31    /// Source is a component of target.
32    PartOf,
33    /// Source depends on target.
34    DependsOn,
35    /// Source implements target (e.g., function implements interface).
36    Implements,
37}
38
39impl EdgeType {
40    /// Return the canonical snake_case representation.
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            Self::RelatedTo => "related_to",
44            Self::Updates => "updates",
45            Self::Contradicts => "contradicts",
46            Self::PartOf => "part_of",
47            Self::DependsOn => "depends_on",
48            Self::Implements => "implements",
49        }
50    }
51}
52
53impl std::fmt::Display for EdgeType {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::RelatedTo => write!(f, "RelatedTo"),
57            Self::Updates => write!(f, "Updates"),
58            Self::Contradicts => write!(f, "Contradicts"),
59            Self::PartOf => write!(f, "PartOf"),
60            Self::DependsOn => write!(f, "DependsOn"),
61            Self::Implements => write!(f, "Implements"),
62        }
63    }
64}
65
66impl std::str::FromStr for EdgeType {
67    type Err = ParseEnumError;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        match s {
71            "related_to" => Ok(Self::RelatedTo),
72            "updates" => Ok(Self::Updates),
73            "contradicts" => Ok(Self::Contradicts),
74            "part_of" => Ok(Self::PartOf),
75            "depends_on" => Ok(Self::DependsOn),
76            "implements" => Ok(Self::Implements),
77            _ => Err(ParseEnumError {
78                type_name: "EdgeType",
79                value: s.to_owned(),
80            }),
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::ids::{BranchId, EdgeId, NodeId};
89
90    #[test]
91    fn edge_serialization_roundtrip() {
92        let edge = Edge {
93            id: EdgeId(1),
94            source_id: NodeId(10),
95            target_id: NodeId(20),
96            edge_type: EdgeType::DependsOn,
97            branch_id: BranchId::from("main"),
98            weight: 1.0,
99            metadata: None,
100        };
101
102        let json = serde_json::to_string(&edge).expect("serialize");
103        assert!(
104            !json.contains("metadata"),
105            "None metadata should be skipped"
106        );
107
108        let deserialized: Edge = serde_json::from_str(&json).expect("deserialize");
109        assert_eq!(deserialized.edge_type, EdgeType::DependsOn);
110        assert_eq!(deserialized.source_id, NodeId(10));
111        assert_eq!(deserialized.target_id, NodeId(20));
112    }
113
114    #[test]
115    fn all_edge_type_variants() {
116        let types = [
117            EdgeType::RelatedTo,
118            EdgeType::Updates,
119            EdgeType::Contradicts,
120            EdgeType::PartOf,
121            EdgeType::DependsOn,
122            EdgeType::Implements,
123        ];
124        assert_eq!(types.len(), 6);
125    }
126
127    #[test]
128    fn edge_type_display() {
129        assert_eq!(EdgeType::DependsOn.to_string(), "DependsOn");
130        assert_eq!(EdgeType::Contradicts.to_string(), "Contradicts");
131    }
132
133    #[test]
134    fn edge_type_roundtrip_str() {
135        let types = [
136            EdgeType::RelatedTo,
137            EdgeType::Updates,
138            EdgeType::Contradicts,
139            EdgeType::PartOf,
140            EdgeType::DependsOn,
141            EdgeType::Implements,
142        ];
143        for et in types {
144            let s = et.as_str();
145            let parsed: EdgeType = s.parse().unwrap();
146            assert_eq!(parsed, et);
147        }
148    }
149
150    #[test]
151    fn edge_type_parse_unknown() {
152        assert!("bogus".parse::<EdgeType>().is_err());
153    }
154}