1use serde::{Deserialize, Serialize};
10
11use crate::{FactSet, Store, StoreError};
12
13pub const ARTIFACT_SCHEMA: &str = "roteiro.graph/v1";
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct GraphArtifact {
19 pub schema: String,
21 pub tree: Option<String>,
23 pub facts: FactSet,
25}
26
27impl GraphArtifact {
28 pub fn from_store(store: &Store) -> Result<Self, StoreError> {
33 Ok(Self {
34 schema: ARTIFACT_SCHEMA.to_owned(),
35 tree: store.sync_state()?,
36 facts: store.export_factset()?,
37 })
38 }
39
40 pub fn to_json(&self) -> Result<String, StoreError> {
45 Ok(serde_json::to_string_pretty(self)?)
46 }
47
48 pub fn from_json(json: &str) -> Result<Self, StoreError> {
54 let artifact: Self = serde_json::from_str(json)?;
55 if artifact.schema != ARTIFACT_SCHEMA {
56 return Err(StoreError::Corrupt(format!(
57 "unsupported graph artifact schema: {} (expected {ARTIFACT_SCHEMA})",
58 artifact.schema
59 )));
60 }
61 Ok(artifact)
62 }
63
64 pub fn load_into(&self, store: &mut Store) -> Result<(), StoreError> {
72 store.rebuild(&self.facts, self.tree.as_deref())
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::{ARTIFACT_SCHEMA, GraphArtifact};
79 use crate::{Edge, EdgeKind, FactSet, Node, NodeKind, Store};
80
81 fn sample_nodes() -> Vec<Node> {
84 vec![
85 Node::new("adr:0001", NodeKind::Adr, "Build Roteiro"),
86 Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main"),
87 Node::new("sym:rust:a.rs#helper", NodeKind::Fn, "helper"),
88 ]
89 }
90
91 fn sample_edges() -> Vec<Edge> {
92 vec![
93 Edge::authored("adr:0001", "sym:rust:a.rs#main", EdgeKind::References),
94 Edge::derived(
95 "sym:rust:a.rs#main",
96 "sym:rust:a.rs#helper",
97 EdgeKind::Calls,
98 ),
99 ]
100 }
101
102 fn seeded_ordered(reversed: bool, tree: Option<&str>) -> Store {
105 let mut store = Store::open_in_memory().expect("store");
106 let mut nodes = sample_nodes();
107 let mut edges = sample_edges();
108 if reversed {
109 nodes.reverse();
110 edges.reverse();
111 }
112 let mut facts = FactSet::new();
113 facts.nodes = nodes;
114 facts.edges = edges;
115 store.rebuild(&facts, tree).expect("rebuild");
116 store
117 }
118
119 fn seeded() -> Store {
120 seeded_ordered(false, Some("treeabc"))
121 }
122
123 #[test]
124 fn round_trips_through_json_and_a_fresh_store() {
125 let store = seeded();
126 let artifact = GraphArtifact::from_store(&store).expect("capture");
127 assert_eq!(artifact.schema, ARTIFACT_SCHEMA);
128 assert_eq!(artifact.tree.as_deref(), Some("treeabc"));
129 assert_eq!(artifact.facts.nodes.len(), 3);
130 assert_eq!(artifact.facts.edges.len(), 2);
131
132 let json = artifact.to_json().expect("json");
134 let parsed = GraphArtifact::from_json(&json).expect("parse");
135 assert_eq!(parsed, artifact);
136
137 let mut fresh = Store::open_in_memory().expect("fresh");
139 parsed.load_into(&mut fresh).expect("load");
140 assert_eq!(fresh.node_count().expect("nc"), 3);
141 assert_eq!(fresh.edge_count().expect("ec"), 2);
142 assert_eq!(
143 fresh.sync_state().expect("state").as_deref(),
144 Some("treeabc")
145 );
146 let inbound = fresh.edges_to("sym:rust:a.rs#main").expect("edges");
148 assert!(
149 inbound
150 .iter()
151 .any(|e| e.provenance == crate::Provenance::Authored)
152 );
153 }
154
155 #[test]
156 fn export_is_deterministic_regardless_of_insertion_order() {
157 let a = GraphArtifact::from_store(&seeded_ordered(false, Some("treeabc")))
161 .expect("a")
162 .to_json()
163 .expect("ja");
164 let b = GraphArtifact::from_store(&seeded_ordered(true, Some("treeabc")))
165 .expect("b")
166 .to_json()
167 .expect("jb");
168 assert_eq!(a, b);
169 }
170
171 #[test]
172 fn artifact_without_tree_records_no_sync_state() {
173 let store = seeded_ordered(false, None);
176 let artifact = GraphArtifact::from_store(&store).expect("capture");
177 assert_eq!(artifact.tree, None);
178
179 let mut fresh = Store::open_in_memory().expect("fresh");
180 artifact.load_into(&mut fresh).expect("load");
181 assert_eq!(fresh.node_count().expect("nc"), 3);
182 assert_eq!(fresh.sync_state().expect("state"), None);
183 }
184
185 #[test]
186 fn rejects_unknown_schema() {
187 let json = r#"{"schema":"roteiro.graph/v999","tree":null,"facts":{"nodes":[],"edges":[]}}"#;
188 assert!(matches!(
189 GraphArtifact::from_json(json),
190 Err(crate::StoreError::Corrupt(_))
191 ));
192 }
193}