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 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}