Skip to main content

weavatrix_rust/model/
snapshot.rs

1use blazingly_json::{Map, Value};
2use serde::{Deserialize, Serialize};
3use weavatrix_graph::{AttributeValue, Edge, Node, NodeKind, SourceSpan};
4
5pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Capability {
9    pub id: String,
10    pub state: CapabilityState,
11    pub detail: String,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CapabilityState {
17    Complete,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct Diagnostic {
22    pub code: String,
23    pub message: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub span: Option<SourceSpan>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct Snapshot {
30    pub schema_version: u32,
31    pub generator: String,
32    pub repository: String,
33    pub revision: String,
34    pub capabilities: Vec<Capability>,
35    pub nodes: Vec<Node>,
36    pub edges: Vec<Edge>,
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub diagnostics: Vec<Diagnostic>,
39}
40
41impl Snapshot {
42    #[must_use]
43    pub fn legacy_value(&self) -> Value {
44        let nodes = self.nodes.iter().map(legacy_node).collect::<Vec<_>>();
45        let links = self.edges.iter().map(legacy_edge).collect::<Vec<_>>();
46        Value::Object(Map::from_iter([
47            ("nodes".to_owned(), Value::Array(nodes)),
48            ("links".to_owned(), Value::Array(links)),
49            (
50                "schemaVersion".to_owned(),
51                Value::String("weavatrix.rust.legacy.v1".into()),
52            ),
53            ("edgeTypesV".to_owned(), Value::from(2)),
54            ("edgeProvenanceV".to_owned(), Value::from(1)),
55            (
56                "generator".to_owned(),
57                Value::String(self.generator.clone()),
58            ),
59            (
60                "repository".to_owned(),
61                Value::String(self.repository.clone()),
62            ),
63            ("revision".to_owned(), Value::String(self.revision.clone())),
64        ]))
65    }
66
67    /// Serializes a JavaScript Weavatrix-compatible `{ nodes, links }` graph.
68    ///
69    /// # Errors
70    ///
71    /// Returns any JSON serialization error.
72    pub fn legacy_json(&self, pretty: bool) -> blazingly_json::Result<String> {
73        if pretty {
74            blazingly_json::to_string_pretty(&self.legacy_value())
75        } else {
76            blazingly_json::to_string(&self.legacy_value())
77        }
78    }
79}
80
81fn legacy_node(node: &Node) -> Value {
82    let mut out = legacy_object([
83        ("id", Value::String(node.id.to_string())),
84        ("label", Value::String(node.label.clone())),
85        ("kind", Value::String(node.kind.as_str().to_owned())),
86        ("file_type", Value::String("code".into())),
87    ]);
88    if node.kind == NodeKind::File {
89        out.insert("source_file".into(), Value::String(node.label.clone()));
90    }
91    if let Some(language) = &node.language {
92        out.insert("language".into(), Value::String(language.clone()));
93    }
94    legacy_record(out, node.span.as_ref(), &node.attributes, |out, span| {
95        out.insert(
96            "source_location".into(),
97            Value::String(format!("L{}", span.start.line)),
98        );
99        out.insert(
100            "source_end".into(),
101            Value::String(format!("L{}", span.end.line)),
102        );
103    })
104}
105
106fn legacy_edge(edge: &Edge) -> Value {
107    let mut out = legacy_object([
108        ("source", Value::String(edge.source.to_string())),
109        ("target", Value::String(edge.target.to_string())),
110        ("relation", Value::String(edge.kind.as_str().to_owned())),
111        (
112            "provenance",
113            Value::String(edge.provenance.evidence.as_str().to_owned()),
114        ),
115        (
116            "confidence",
117            Value::String(format!("{:?}", edge.provenance.confidence).to_ascii_lowercase()),
118        ),
119        (
120            "extractor",
121            Value::String(edge.provenance.extractor.clone()),
122        ),
123    ]);
124    if let Some(detail) = &edge.provenance.detail {
125        out.insert("detail".into(), Value::String(detail.clone()));
126    }
127    legacy_record(
128        out,
129        edge.provenance.span.as_ref(),
130        &edge.attributes,
131        |out, span| {
132            out.insert("line".into(), Value::from(span.start.line));
133            out.insert(
134                "character".into(),
135                Value::from(span.start.column.saturating_sub(1)),
136            );
137        },
138    )
139}
140
141fn legacy_object(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Map<String, Value> {
142    let mut out = Map::new();
143    for (key, value) in entries {
144        out.insert(key.to_owned(), value);
145    }
146    out
147}
148
149fn legacy_record(
150    mut out: Map<String, Value>,
151    span: Option<&SourceSpan>,
152    attributes: &std::collections::BTreeMap<String, AttributeValue>,
153    decorate_span: impl FnOnce(&mut Map<String, Value>, &SourceSpan),
154) -> Value {
155    if let Some(span) = span {
156        out.insert("source_file".into(), Value::String(span.file.clone()));
157        out.insert("source_range".into(), legacy_range(span));
158        decorate_span(&mut out, span);
159    }
160    insert_attributes(&mut out, attributes);
161    Value::Object(out)
162}
163
164fn legacy_range(span: &SourceSpan) -> Value {
165    Value::Object(Map::from_iter([
166        (
167            "start".to_owned(),
168            legacy_position(span.start.line, span.start.column),
169        ),
170        (
171            "end".to_owned(),
172            legacy_position(span.end.line, span.end.column),
173        ),
174    ]))
175}
176
177fn legacy_position(line: u32, column: u32) -> Value {
178    Value::Object(Map::from_iter([
179        ("line".to_owned(), Value::from(line)),
180        (
181            "character".to_owned(),
182            Value::from(column.saturating_sub(1)),
183        ),
184    ]))
185}
186
187fn insert_attributes(
188    out: &mut Map<String, Value>,
189    attributes: &std::collections::BTreeMap<String, AttributeValue>,
190) {
191    for (key, value) in attributes {
192        if !out.contains_key(key) {
193            out.insert(
194                key.clone(),
195                blazingly_json::to_value(value).unwrap_or(Value::Null),
196            );
197        }
198    }
199}