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