1use crate::{
2 AttributeValue, Confidence, Edge, EdgeKind, EvidenceKind, Graph, GraphBuilder, Node, NodeId,
3 NodeKind, Provenance, Result, SourcePosition, SourceSpan,
4};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct LegacyGraph {
12 #[serde(default)]
13 pub nodes: Vec<LegacyNode>,
14 #[serde(default)]
15 pub links: Vec<LegacyLink>,
16 #[serde(flatten)]
17 pub metadata: BTreeMap<String, AttributeValue>,
18}
19
20impl LegacyGraph {
21 pub fn into_graph(self, extractor: impl Into<String>) -> Result<Graph> {
28 let extractor = extractor.into();
29 let mut builder = GraphBuilder::new();
30 for node in self.nodes {
31 builder.add_node(node.into_node()?)?;
32 }
33 for link in self.links {
34 builder.add_edge(link.into_edge(extractor.clone())?)?;
35 }
36 builder.build()
37 }
38}
39
40impl TryFrom<LegacyGraph> for Graph {
41 type Error = crate::GraphError;
42
43 fn try_from(value: LegacyGraph) -> Result<Self> {
44 value.into_graph("weavatrix.legacy")
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct LegacyNode {
51 pub id: String,
52 #[serde(default)]
53 pub label: Option<String>,
54 #[serde(default)]
55 pub kind: Option<String>,
56 #[serde(default, rename = "type")]
57 pub node_type: Option<String>,
58 #[serde(default)]
59 pub language: Option<String>,
60 #[serde(default)]
61 pub source_file: Option<String>,
62 #[serde(default)]
63 pub source_range: Option<LegacyRange>,
64 #[serde(default)]
65 pub selection_start: Option<LegacyPoint>,
66 #[serde(default)]
67 pub selection_end: Option<LegacyPoint>,
68 #[serde(flatten)]
69 pub attributes: BTreeMap<String, AttributeValue>,
70}
71
72impl LegacyNode {
73 pub fn into_node(mut self) -> Result<Node> {
79 let inferred_label = infer_label(&self.id);
80 let kind = parse_node_kind(self.kind.as_deref().or(self.node_type.as_deref()), &self.id);
81 let mut node = Node::new(self.id, self.label.unwrap_or(inferred_label), kind)?;
82 node.language = self.language.take();
83 if let Some(span) = self.source_range.take().and_then(|range| {
84 self.source_file
85 .as_ref()
86 .map(|file| range.into_span(file.clone()))
87 }) {
88 node.span = Some(span);
89 }
90 if let Some(source_file) = self.source_file {
91 node.attributes
92 .insert("source_file".into(), source_file.into());
93 }
94 if let Some(selection_start) = self.selection_start {
95 node.attributes
96 .insert("selection_start".into(), selection_start.into());
97 }
98 if let Some(selection_end) = self.selection_end {
99 node.attributes
100 .insert("selection_end".into(), selection_end.into());
101 }
102 node.attributes.extend(self.attributes);
103 Ok(node)
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct LegacyLink {
110 pub source: String,
111 pub target: String,
112 #[serde(default)]
113 pub relation: Option<String>,
114 #[serde(default)]
115 pub kind: Option<String>,
116 #[serde(default, rename = "type")]
117 pub edge_type: Option<String>,
118 #[serde(default)]
119 pub confidence: Option<String>,
120 #[serde(default)]
121 pub provenance: Option<String>,
122 #[serde(default)]
123 pub line: Option<u32>,
124 #[serde(default)]
125 pub character: Option<u32>,
126 #[serde(default, rename = "compileOnly")]
127 pub compile_only: Option<bool>,
128 #[serde(default, rename = "typeOnly")]
129 pub type_only: Option<bool>,
130 #[serde(default)]
131 pub specifier: Option<String>,
132 #[serde(default)]
133 pub usage: Option<String>,
134 #[serde(flatten)]
135 pub attributes: BTreeMap<String, AttributeValue>,
136}
137
138impl LegacyLink {
139 pub fn into_edge(mut self, extractor: impl Into<String>) -> Result<Edge> {
145 let kind_value = self
146 .relation
147 .as_deref()
148 .or(self.kind.as_deref())
149 .or(self.edge_type.as_deref())
150 .unwrap_or("references");
151 let kind = EdgeKind::from_str(kind_value)?;
152 let evidence = parse_evidence(self.provenance.as_deref().or(self.confidence.as_deref()));
153 let confidence = parse_confidence(self.confidence.as_deref(), &evidence);
154 let mut provenance = Provenance::new(extractor, evidence, confidence)?;
155 if let Some(line) = self.line {
156 let column = self.character.unwrap_or(0).saturating_add(1);
157 provenance.span = Some(SourceSpan::new(
158 infer_edge_file(&self.source),
159 SourcePosition::new(line, column),
160 SourcePosition::new(line, column.saturating_add(1)),
161 ));
162 self.attributes
163 .insert("line".into(), i64::from(line).into());
164 }
165 if let Some(character) = self.character {
166 self.attributes
167 .insert("character".into(), i64::from(character).into());
168 }
169 insert_optional(&mut self.attributes, "compileOnly", self.compile_only);
170 insert_optional(&mut self.attributes, "typeOnly", self.type_only);
171 if let Some(specifier) = self.specifier {
172 self.attributes.insert("specifier".into(), specifier.into());
173 }
174 if let Some(usage) = self.usage {
175 self.attributes.insert("usage".into(), usage.into());
176 }
177 Ok(Edge {
178 source: NodeId::new(self.source)?,
179 target: NodeId::new(self.target)?,
180 kind,
181 provenance,
182 attributes: self.attributes,
183 })
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct LegacyPoint {
189 pub line: u32,
190 pub character: u32,
191}
192
193impl From<LegacyPoint> for AttributeValue {
194 fn from(value: LegacyPoint) -> Self {
195 let mut object = BTreeMap::new();
196 object.insert("line".into(), i64::from(value.line).into());
197 object.insert("character".into(), i64::from(value.character).into());
198 Self::Object(object)
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct LegacyRange {
204 pub start: LegacyPoint,
205 pub end: LegacyPoint,
206}
207
208impl LegacyRange {
209 #[must_use]
210 pub fn into_span(self, file: String) -> SourceSpan {
211 SourceSpan::new(
212 file,
213 SourcePosition::new(
214 self.start.line.saturating_add(1),
215 self.start.character.saturating_add(1),
216 ),
217 SourcePosition::new(
218 self.end.line.saturating_add(1),
219 self.end.character.saturating_add(1),
220 ),
221 )
222 }
223}
224
225fn infer_label(id: &str) -> String {
226 id.rsplit(['/', '#']).next().unwrap_or(id).to_owned()
227}
228
229fn infer_edge_file(source: &str) -> String {
230 source.split('#').next().unwrap_or(source).to_owned()
231}
232
233fn parse_node_kind(value: Option<&str>, id: &str) -> NodeKind {
234 if let Some(value) = value.and_then(|value| NodeKind::from_str(value).ok()) {
235 return value;
236 }
237 if id.contains('#') {
238 NodeKind::Function
239 } else {
240 NodeKind::File
241 }
242}
243
244fn parse_evidence(value: Option<&str>) -> EvidenceKind {
245 value
246 .and_then(|value| EvidenceKind::from_str(value).ok())
247 .unwrap_or(EvidenceKind::Extracted)
248}
249
250fn parse_confidence(value: Option<&str>, evidence: &EvidenceKind) -> Confidence {
251 match value
252 .unwrap_or_default()
253 .trim()
254 .to_ascii_lowercase()
255 .as_str()
256 {
257 "exact" | "exact_lsp" => Confidence::Exact,
258 "high" | "extracted" | "resolved" => Confidence::High,
259 "medium" => Confidence::Medium,
260 "low" | "inferred" | "conflict" => Confidence::Low,
261 _ => match evidence {
262 EvidenceKind::ExactLsp => Confidence::Exact,
263 EvidenceKind::Inferred | EvidenceKind::Conflict => Confidence::Low,
264 _ => Confidence::High,
265 },
266 }
267}
268
269fn insert_optional(
270 attributes: &mut BTreeMap<String, AttributeValue>,
271 key: &'static str,
272 value: Option<bool>,
273) {
274 if let Some(value) = value {
275 attributes.insert(key.into(), value.into());
276 }
277}