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