Skip to main content

nodex_core/model/
graph.rs

1use indexmap::IndexMap;
2use serde::ser::SerializeStruct;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use super::edge::Edge;
7use super::node::Node;
8
9/// Immutable document graph with pre-built adjacency indices.
10/// Indices are automatically rebuilt on deserialization.
11pub struct Graph {
12    nodes: IndexMap<String, Node>,
13    edges: Vec<Edge>,
14    incoming: BTreeMap<String, Vec<usize>>,
15    outgoing: BTreeMap<String, Vec<usize>>,
16}
17
18impl Graph {
19    /// Build a graph from nodes and edges. Constructs adjacency indices.
20    pub fn new(nodes: IndexMap<String, Node>, edges: Vec<Edge>) -> Self {
21        let (incoming, outgoing) = build_indices(&edges);
22        Self {
23            nodes,
24            edges,
25            incoming,
26            outgoing,
27        }
28    }
29
30    pub fn node(&self, id: &str) -> Option<&Node> {
31        self.nodes.get(id)
32    }
33
34    pub fn nodes(&self) -> &IndexMap<String, Node> {
35        &self.nodes
36    }
37
38    pub fn edges(&self) -> &[Edge] {
39        &self.edges
40    }
41
42    /// Edge indices where `target == id`.
43    pub fn incoming_indices(&self, id: &str) -> &[usize] {
44        self.incoming
45            .get(id)
46            .map(|v| v.as_slice())
47            .unwrap_or_default()
48    }
49
50    fn outgoing_indices(&self, id: &str) -> &[usize] {
51        self.outgoing
52            .get(id)
53            .map(|v| v.as_slice())
54            .unwrap_or_default()
55    }
56
57    /// Edges pointing to `id`.
58    pub fn incoming_edges(&self, id: &str) -> Vec<&Edge> {
59        self.incoming_indices(id)
60            .iter()
61            .filter_map(|&idx| self.edges.get(idx))
62            .collect()
63    }
64
65    /// Edges originating from `id`.
66    pub fn outgoing_edges(&self, id: &str) -> Vec<&Edge> {
67        self.outgoing_indices(id)
68            .iter()
69            .filter_map(|&idx| self.edges.get(idx))
70            .collect()
71    }
72
73    pub fn node_count(&self) -> usize {
74        self.nodes.len()
75    }
76
77    pub fn edge_count(&self) -> usize {
78        self.edges.len()
79    }
80}
81
82fn build_indices(edges: &[Edge]) -> (BTreeMap<String, Vec<usize>>, BTreeMap<String, Vec<usize>>) {
83    let mut incoming: BTreeMap<String, Vec<usize>> = BTreeMap::new();
84    let mut outgoing: BTreeMap<String, Vec<usize>> = BTreeMap::new();
85
86    for (idx, edge) in edges.iter().enumerate() {
87        outgoing.entry(edge.source.clone()).or_default().push(idx);
88        if let Some(target_id) = edge.target.id() {
89            incoming.entry(target_id.to_string()).or_default().push(idx);
90        }
91    }
92
93    (incoming, outgoing)
94}
95
96/// Serialized schema revision. Bumped on any breaking change to the
97/// on-disk shape of `graph.json` (fields of `Node`, `Edge`, or the
98/// top-level envelope). Readers compare against `SCHEMA_VERSION` and
99/// refuse to load files produced by a newer version than they
100/// understand — `nodex build --full` is the escape hatch.
101pub const SCHEMA_VERSION: u32 = 1;
102
103/// Serialize nodes + edges with a schema-version envelope. Indices
104/// are derived state and intentionally omitted.
105impl Serialize for Graph {
106    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
107        let mut s = serializer.serialize_struct("Graph", 3)?;
108        s.serialize_field("schema_version", &SCHEMA_VERSION)?;
109        s.serialize_field("nodes", &self.nodes)?;
110        s.serialize_field("edges", &self.edges)?;
111        s.end()
112    }
113}
114
115/// Deserialize nodes + edges, then automatically rebuild indices.
116///
117/// Older `graph.json` files without a `schema_version` field are
118/// treated as version 0, which the reader can still handle because
119/// the on-disk shape through v1 was backward-compatible (pure field
120/// additions). Any newer version surfaces a Deserialize error that
121/// propagates up as `PARSE_ERROR` — the user is instructed to
122/// `nodex build --full` to regenerate.
123impl<'de> Deserialize<'de> for Graph {
124    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
125        #[derive(Deserialize)]
126        struct Raw {
127            #[serde(default)]
128            schema_version: u32,
129            nodes: IndexMap<String, Node>,
130            edges: Vec<Edge>,
131        }
132
133        let raw = Raw::deserialize(deserializer)?;
134        if raw.schema_version > SCHEMA_VERSION {
135            return Err(serde::de::Error::custom(format!(
136                "graph.json schema_version {} is newer than this binary supports ({}); \
137                 run `nodex build --full` to regenerate",
138                raw.schema_version, SCHEMA_VERSION
139            )));
140        }
141        Ok(Graph::new(raw.nodes, raw.edges))
142    }
143}
144
145impl std::fmt::Debug for Graph {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("Graph")
148            .field("nodes", &self.nodes.len())
149            .field("edges", &self.edges.len())
150            .finish()
151    }
152}