use serde::{Deserialize, Serialize};
use crate::{FactSet, Store, StoreError};
pub const ARTIFACT_SCHEMA: &str = "roteiro.graph/v1";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphArtifact {
pub schema: String,
pub tree: Option<String>,
pub facts: FactSet,
}
impl GraphArtifact {
pub fn from_store(store: &Store) -> Result<Self, StoreError> {
Ok(Self {
schema: ARTIFACT_SCHEMA.to_owned(),
tree: store.sync_state()?,
facts: store.export_factset()?,
})
}
pub fn to_json(&self) -> Result<String, StoreError> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn from_json(json: &str) -> Result<Self, StoreError> {
let artifact: Self = serde_json::from_str(json)?;
if artifact.schema != ARTIFACT_SCHEMA {
return Err(StoreError::Corrupt(format!(
"unsupported graph artifact schema: {} (expected {ARTIFACT_SCHEMA})",
artifact.schema
)));
}
Ok(artifact)
}
pub fn load_into(&self, store: &mut Store) -> Result<(), StoreError> {
store.rebuild(&self.facts, self.tree.as_deref())
}
}
#[cfg(test)]
mod tests {
use super::{ARTIFACT_SCHEMA, GraphArtifact};
use crate::{Edge, EdgeKind, FactSet, Node, NodeKind, Store};
fn sample_nodes() -> Vec<Node> {
vec![
Node::new("adr:0001", NodeKind::Adr, "Build Roteiro"),
Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main"),
Node::new("sym:rust:a.rs#helper", NodeKind::Fn, "helper"),
]
}
fn sample_edges() -> Vec<Edge> {
vec![
Edge::authored("adr:0001", "sym:rust:a.rs#main", EdgeKind::References),
Edge::derived(
"sym:rust:a.rs#main",
"sym:rust:a.rs#helper",
EdgeKind::Calls,
),
]
}
fn seeded_ordered(reversed: bool, tree: Option<&str>) -> Store {
let mut store = Store::open_in_memory().expect("store");
let mut nodes = sample_nodes();
let mut edges = sample_edges();
if reversed {
nodes.reverse();
edges.reverse();
}
let mut facts = FactSet::new();
facts.nodes = nodes;
facts.edges = edges;
store.rebuild(&facts, tree).expect("rebuild");
store
}
fn seeded() -> Store {
seeded_ordered(false, Some("treeabc"))
}
#[test]
fn round_trips_through_json_and_a_fresh_store() {
let store = seeded();
let artifact = GraphArtifact::from_store(&store).expect("capture");
assert_eq!(artifact.schema, ARTIFACT_SCHEMA);
assert_eq!(artifact.tree.as_deref(), Some("treeabc"));
assert_eq!(artifact.facts.nodes.len(), 3);
assert_eq!(artifact.facts.edges.len(), 2);
let json = artifact.to_json().expect("json");
let parsed = GraphArtifact::from_json(&json).expect("parse");
assert_eq!(parsed, artifact);
let mut fresh = Store::open_in_memory().expect("fresh");
parsed.load_into(&mut fresh).expect("load");
assert_eq!(fresh.node_count().expect("nc"), 3);
assert_eq!(fresh.edge_count().expect("ec"), 2);
assert_eq!(
fresh.sync_state().expect("state").as_deref(),
Some("treeabc")
);
let inbound = fresh.edges_to("sym:rust:a.rs#main").expect("edges");
assert!(
inbound
.iter()
.any(|e| e.provenance == crate::Provenance::Authored)
);
}
#[test]
fn export_is_deterministic_regardless_of_insertion_order() {
let a = GraphArtifact::from_store(&seeded_ordered(false, Some("treeabc")))
.expect("a")
.to_json()
.expect("ja");
let b = GraphArtifact::from_store(&seeded_ordered(true, Some("treeabc")))
.expect("b")
.to_json()
.expect("jb");
assert_eq!(a, b);
}
#[test]
fn artifact_without_tree_records_no_sync_state() {
let store = seeded_ordered(false, None);
let artifact = GraphArtifact::from_store(&store).expect("capture");
assert_eq!(artifact.tree, None);
let mut fresh = Store::open_in_memory().expect("fresh");
artifact.load_into(&mut fresh).expect("load");
assert_eq!(fresh.node_count().expect("nc"), 3);
assert_eq!(fresh.sync_state().expect("state"), None);
}
#[test]
fn rejects_unknown_schema() {
let json = r#"{"schema":"roteiro.graph/v999","tree":null,"facts":{"nodes":[],"edges":[]}}"#;
assert!(matches!(
GraphArtifact::from_json(json),
Err(crate::StoreError::Corrupt(_))
));
}
}