Skip to main content

reifydb_value/error/
render.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::fmt::Write;
5
6use super::Diagnostic;
7use crate::fragment::Fragment;
8
9pub trait DiagnosticRenderer {
10	fn render(&self, diagnostic: &Diagnostic) -> String;
11}
12
13pub struct DefaultRenderer;
14
15pub fn get_line(source: &str, line: u32) -> &str {
16	source.lines().nth((line - 1) as usize).unwrap_or("")
17}
18
19impl DiagnosticRenderer for DefaultRenderer {
20	fn render(&self, diagnostic: &Diagnostic) -> String {
21		let mut output = String::new();
22
23		if diagnostic.cause.is_none() {
24			self.render_flat(&mut output, diagnostic);
25		} else {
26			self.render_nested(&mut output, diagnostic, 0);
27		}
28
29		output
30	}
31}
32
33impl DefaultRenderer {
34	fn render_flat(&self, output: &mut String, diagnostic: &Diagnostic) {
35		self.render_flat_header(output, diagnostic);
36		self.render_flat_location(output, diagnostic);
37		self.render_flat_operator_chain(output, diagnostic);
38		self.render_flat_trailing(output, diagnostic);
39	}
40
41	#[inline]
42	fn render_flat_header(&self, output: &mut String, diagnostic: &Diagnostic) {
43		let _ = writeln!(output, "Error {}", diagnostic.code);
44		let _ = writeln!(output, "  {}", diagnostic.message);
45		let _ = writeln!(output);
46	}
47
48	#[inline]
49	fn render_flat_location(&self, output: &mut String, diagnostic: &Diagnostic) {
50		if let Fragment::Statement {
51			line,
52			column,
53			text,
54			..
55		} = &diagnostic.fragment
56		{
57			let fragment = text;
58			let line = line.0;
59			let col = column.0;
60			let rql = diagnostic.rql.as_deref().unwrap_or("");
61
62			let _ = writeln!(output, "LOCATION");
63			let _ = writeln!(output, "  line {}, column {}", line, col);
64			let _ = writeln!(output);
65
66			let line_content = get_line(rql, line);
67
68			let _ = writeln!(output, "RQL");
69			let _ = writeln!(output, "  {} │ {}", line, line_content);
70			let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
71			let _ = writeln!(output, "    │ {}{}", " ".repeat(fragment_start), "~".repeat(fragment.len()));
72			let _ = writeln!(output, "    │");
73
74			let label_text = diagnostic.label.as_deref().unwrap_or("");
75			let fragment_center = fragment_start + fragment.len() / 2;
76			let label_center_offset = if label_text.len() / 2 > fragment_center {
77				0
78			} else {
79				fragment_center - label_text.len() / 2
80			};
81
82			let _ = writeln!(output, "    │ {}{}", " ".repeat(label_center_offset), label_text);
83			let _ = writeln!(output);
84		}
85	}
86
87	#[inline]
88	fn render_flat_operator_chain(&self, output: &mut String, diagnostic: &Diagnostic) {
89		if let Some(chain) = &diagnostic.operator_chain
90			&& !chain.is_empty()
91		{
92			let _ = writeln!(output, "OPERATOR CHAIN");
93			for (i, entry) in chain.iter().enumerate() {
94				let _ = writeln!(
95					output,
96					"  {}. {} (node_id={}, version={})",
97					i + 1,
98					entry.operator_name,
99					entry.node_id,
100					entry.operator_version
101				);
102			}
103			let _ = writeln!(output);
104		}
105	}
106
107	#[inline]
108	fn render_flat_trailing(&self, output: &mut String, diagnostic: &Diagnostic) {
109		if let Some(help) = &diagnostic.help {
110			let _ = writeln!(output, "HELP");
111			let _ = writeln!(output, "  {}", help);
112			let _ = writeln!(output);
113		}
114
115		if let Some(col) = &diagnostic.column {
116			let _ = writeln!(output, "COLUMN");
117			let _ = writeln!(output, "  column `{}` is of type `{}`", col.name, col.r#type);
118			let _ = writeln!(output);
119		}
120
121		if !diagnostic.notes.is_empty() {
122			let _ = writeln!(output, "NOTES");
123			for note in &diagnostic.notes {
124				let _ = writeln!(output, "  • {}", note);
125			}
126		}
127	}
128
129	fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
130		let indent = if depth == 0 {
131			""
132		} else {
133			"  "
134		};
135		let prefix = if depth == 0 {
136			""
137		} else {
138			"↳ "
139		};
140
141		self.render_nested_header(output, diagnostic, indent, prefix);
142		self.render_nested_location(output, diagnostic, indent);
143
144		if let Some(cause) = &diagnostic.cause {
145			self.render_nested(output, cause, depth + 1);
146		}
147
148		self.render_nested_trailing(output, diagnostic, indent, depth);
149	}
150
151	#[inline]
152	fn render_nested_header(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str, prefix: &str) {
153		let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
154	}
155
156	#[inline]
157	fn render_nested_location(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str) {
158		if let Fragment::Statement {
159			line,
160			column,
161			text,
162			..
163		} = &diagnostic.fragment
164		{
165			let fragment = text;
166			let line = line.0;
167			let col = column.0;
168			let rql = diagnostic.rql.as_deref().unwrap_or("");
169
170			let _ = writeln!(
171				output,
172				"{}  at {} (line {}, column {})",
173				indent,
174				if rql.is_empty() {
175					"unknown".to_string()
176				} else {
177					format!("\"{}\"", fragment)
178				},
179				line,
180				col
181			);
182			let _ = writeln!(output);
183
184			let line_content = get_line(rql, line);
185
186			let _ = writeln!(output, "{}  {} │ {}", indent, line, line_content);
187			let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
188			let _ = writeln!(
189				output,
190				"{}    │ {}{}",
191				indent,
192				" ".repeat(fragment_start),
193				"~".repeat(fragment.len())
194			);
195
196			let label_text = diagnostic.label.as_deref().unwrap_or("");
197			if !label_text.is_empty() {
198				let fragment_center = fragment_start + fragment.len() / 2;
199				let label_center_offset = if label_text.len() / 2 > fragment_center {
200					0
201				} else {
202					fragment_center - label_text.len() / 2
203				};
204
205				let _ = writeln!(
206					output,
207					"{}    │ {}{}",
208					indent,
209					" ".repeat(label_center_offset),
210					label_text
211				);
212			}
213			let _ = writeln!(output);
214		}
215	}
216
217	#[inline]
218	fn render_nested_trailing(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str, depth: usize) {
219		if let Some(help) = &diagnostic.help {
220			let _ = writeln!(output, "{}  help: {}", indent, help);
221		}
222
223		if let Some(col) = &diagnostic.column {
224			let _ = writeln!(output, "{}  column `{}` is of type `{}`", indent, col.name, col.r#type);
225		}
226
227		if !diagnostic.notes.is_empty() {
228			for note in &diagnostic.notes {
229				let _ = writeln!(output, "{}  note: {}", indent, note);
230			}
231		}
232
233		if depth > 0 {
234			let _ = writeln!(output);
235		}
236	}
237}
238
239impl DefaultRenderer {
240	pub fn render_string(diagnostic: &Diagnostic) -> String {
241		DefaultRenderer.render(diagnostic)
242	}
243}