1use std::fmt;
4
5use ariadne::{Color, Config, Label, Report, ReportKind, Source};
6use facet_format::DeserializeError;
7use styx_parse::Span;
8
9fn ariadne_config() -> Config {
11 let no_color = std::env::var("NO_COLOR").is_ok();
12 if no_color {
13 Config::default().with_color(false)
14 } else {
15 Config::default()
16 }
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct StyxError {
22 pub kind: StyxErrorKind,
23 pub span: Option<Span>,
24}
25
26impl StyxError {
27 pub fn new(kind: StyxErrorKind, span: Option<Span>) -> Self {
28 Self { kind, span }
29 }
30
31 pub fn render(&self, filename: &str, source: &str) -> String {
35 let mut output = Vec::new();
36 self.write_report(filename, source, &mut output);
37 String::from_utf8(output).unwrap_or_else(|_| format!("{}", self))
38 }
39
40 pub fn write_report<W: std::io::Write>(&self, filename: &str, source: &str, writer: W) {
42 let report = self.build_report(filename, source);
43 let _ = report
44 .with_config(ariadne_config())
45 .finish()
46 .write((filename, Source::from(source)), writer);
47 }
48
49 pub fn build_report<'a>(
51 &self,
52 filename: &'a str,
53 _source: &str,
54 ) -> ariadne::ReportBuilder<'static, (&'a str, std::ops::Range<usize>)> {
55 let range = self
56 .span
57 .map(|s| s.start as usize..s.end as usize)
58 .unwrap_or(0..1);
59
60 match &self.kind {
61 StyxErrorKind::InvalidScalar { value, expected } => {
63 Report::build(ReportKind::Error, (filename, range.clone()))
64 .with_message(format!("invalid value '{}'", value))
65 .with_label(
66 Label::new((filename, range))
67 .with_message(format!("expected {}", expected))
68 .with_color(Color::Red),
69 )
70 }
71
72 StyxErrorKind::MissingField { name } => {
74 Report::build(ReportKind::Error, (filename, range.clone()))
75 .with_message(format!("missing required field '{}'", name))
76 .with_label(
77 Label::new((filename, range))
78 .with_message("in this object")
79 .with_color(Color::Red),
80 )
81 .with_help(format!("add the required field: {} <value>", name))
82 }
83
84 StyxErrorKind::UnknownField { name } => {
86 Report::build(ReportKind::Error, (filename, range.clone()))
87 .with_message(format!("unknown field '{}'", name))
88 .with_label(
89 Label::new((filename, range))
90 .with_message("unknown field")
91 .with_color(Color::Red),
92 )
93 }
94
95 StyxErrorKind::UnexpectedToken { got, expected } => {
96 Report::build(ReportKind::Error, (filename, range.clone()))
97 .with_message(format!("unexpected token '{}'", got))
98 .with_label(
99 Label::new((filename, range))
100 .with_message(format!("expected {}", expected))
101 .with_color(Color::Red),
102 )
103 }
104
105 StyxErrorKind::UnexpectedEof { expected } => {
106 Report::build(ReportKind::Error, (filename, range.clone()))
107 .with_message("unexpected end of input")
108 .with_label(
109 Label::new((filename, range))
110 .with_message(format!("expected {}", expected))
111 .with_color(Color::Red),
112 )
113 }
114
115 StyxErrorKind::InvalidEscape { sequence } => {
116 Report::build(ReportKind::Error, (filename, range.clone()))
117 .with_message(format!("invalid escape sequence '{}'", sequence))
118 .with_label(
119 Label::new((filename, range))
120 .with_message("invalid escape")
121 .with_color(Color::Red),
122 )
123 .with_help("valid escapes are: \\\\, \\\", \\n, \\r, \\t, \\uXXXX, \\u{X...}")
124 }
125 }
126 }
127}
128
129impl fmt::Display for StyxError {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}", self.kind)?;
132 if let Some(span) = &self.span {
133 write!(f, " at offset {}", span.start)?;
134 }
135 Ok(())
136 }
137}
138
139impl std::error::Error for StyxError {}
140
141#[derive(Debug, Clone, PartialEq)]
143pub enum StyxErrorKind {
144 UnexpectedToken { got: String, expected: &'static str },
146 UnexpectedEof { expected: &'static str },
148 InvalidScalar {
150 value: String,
151 expected: &'static str,
152 },
153 MissingField { name: String },
155 UnknownField { name: String },
157 InvalidEscape { sequence: String },
159}
160
161impl fmt::Display for StyxErrorKind {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 StyxErrorKind::UnexpectedToken { got, expected } => {
165 write!(f, "unexpected token '{}', expected {}", got, expected)
166 }
167 StyxErrorKind::UnexpectedEof { expected } => {
168 write!(f, "unexpected end of input, expected {}", expected)
169 }
170 StyxErrorKind::InvalidScalar { value, expected } => {
171 write!(f, "invalid value '{}', expected {}", value, expected)
172 }
173 StyxErrorKind::MissingField { name } => {
174 write!(f, "missing required field '{}'", name)
175 }
176 StyxErrorKind::UnknownField { name } => {
177 write!(f, "unknown field '{}'", name)
178 }
179 StyxErrorKind::InvalidEscape { sequence } => {
180 write!(f, "invalid escape sequence '{}'", sequence)
181 }
182 }
183 }
184}
185
186#[allow(dead_code)]
188fn reflect_span_to_range(span: &facet_reflect::Span) -> std::ops::Range<usize> {
189 let start = span.offset;
190 let end = start + span.len;
191 start..end
192}
193
194#[allow(dead_code)]
196pub trait RenderError {
197 fn render(&self, filename: &str, source: &str) -> String;
201
202 fn write_report<W: std::io::Write>(&self, filename: &str, source: &str, writer: W);
204}
205
206impl RenderError for DeserializeError<StyxError> {
211 fn render(&self, filename: &str, source: &str) -> String {
212 let mut output = Vec::new();
213 self.write_report(filename, source, &mut output);
214 String::from_utf8(output).unwrap_or_else(|_| format!("{}", self))
215 }
216
217 fn write_report<W: std::io::Write>(&self, filename: &str, source: &str, writer: W) {
218 let report = build_deserialize_error_report(self, filename, source, ariadne_config());
221 let _ = report
222 .finish()
223 .write((filename, Source::from(source)), writer);
224 }
225}
226
227#[allow(dead_code)]
228fn build_deserialize_error_report<'a>(
229 err: &DeserializeError<StyxError>,
230 filename: &'a str,
231 source: &str,
232 config: Config,
233) -> ariadne::ReportBuilder<'static, (&'a str, std::ops::Range<usize>)> {
234 match err {
235 DeserializeError::Parser(styx_err) => {
238 styx_err.build_report(filename, source).with_config(config)
239 }
240
241 DeserializeError::MissingField {
243 field,
244 type_name,
245 span,
246 ..
247 } => {
248 let range = span
249 .as_ref()
250 .map(reflect_span_to_range)
251 .unwrap_or(0..source.len().max(1));
252 Report::build(ReportKind::Error, (filename, range.clone()))
253 .with_config(config)
254 .with_message(format!("missing required field '{}'", field))
255 .with_label(
256 Label::new((filename, range))
257 .with_message(format!("in {}", type_name))
258 .with_color(Color::Red),
259 )
260 .with_help(format!("add the required field: {} <value>", field))
261 }
262
263 DeserializeError::UnknownField { field, span, .. } => {
265 let range = span.as_ref().map(reflect_span_to_range).unwrap_or(0..1);
266 Report::build(ReportKind::Error, (filename, range.clone()))
267 .with_config(config)
268 .with_message(format!("unknown field '{}'", field))
269 .with_label(
270 Label::new((filename, range))
271 .with_message("unknown field")
272 .with_color(Color::Red),
273 )
274 }
275
276 DeserializeError::TypeMismatch {
278 expected,
279 got,
280 span,
281 ..
282 } => {
283 let range = span.as_ref().map(reflect_span_to_range).unwrap_or(0..1);
284 Report::build(ReportKind::Error, (filename, range.clone()))
285 .with_config(config)
286 .with_message(format!("type mismatch: expected {}", expected))
287 .with_label(
288 Label::new((filename, range))
289 .with_message(format!("got {}", got))
290 .with_color(Color::Red),
291 )
292 }
293
294 DeserializeError::Reflect { error, span, .. } => {
296 let range = span.as_ref().map(reflect_span_to_range).unwrap_or(0..1);
297 Report::build(ReportKind::Error, (filename, range.clone()))
298 .with_config(config)
299 .with_message(format!("{}", error))
300 .with_label(
301 Label::new((filename, range))
302 .with_message("error here")
303 .with_color(Color::Red),
304 )
305 }
306
307 DeserializeError::UnexpectedEof { expected } => {
309 let range = source.len().saturating_sub(1)..source.len().max(1);
310 Report::build(ReportKind::Error, (filename, range.clone()))
311 .with_config(config)
312 .with_message("unexpected end of input")
313 .with_label(
314 Label::new((filename, range))
315 .with_message(format!("expected {}", expected))
316 .with_color(Color::Red),
317 )
318 }
319
320 DeserializeError::Unsupported(msg) => Report::build(ReportKind::Error, (filename, 0..1))
322 .with_config(config)
323 .with_message(format!("unsupported: {}", msg)),
324
325 DeserializeError::CannotBorrow { message } => {
327 Report::build(ReportKind::Error, (filename, 0..1))
328 .with_config(config)
329 .with_message(message.clone())
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use facet::Facet;
338
339 #[test]
340 fn test_ariadne_no_color() {
341 let config = Config::default().with_color(false);
343
344 let source = "test input";
345 let report =
346 Report::<(&str, std::ops::Range<usize>)>::build(ReportKind::Error, ("test.styx", 0..4))
347 .with_config(config)
348 .with_message("test error")
349 .with_label(
350 Label::new(("test.styx", 0..4))
351 .with_message("here")
352 .with_color(Color::Red),
353 )
354 .finish();
355
356 let mut output = Vec::new();
357 report
358 .write(("test.styx", Source::from(source)), &mut output)
359 .unwrap();
360 let s = String::from_utf8(output).unwrap();
361
362 assert!(
364 !s.contains("\x1b["),
365 "Output should not contain ANSI escape codes when color is disabled:\n{:?}",
366 s
367 );
368 }
369
370 #[test]
371 fn test_ariadne_config_respects_no_color_env() {
372 let no_color = std::env::var("NO_COLOR").is_ok();
374 eprintln!("NO_COLOR is set: {}", no_color);
375
376 let config = ariadne_config();
377
378 let source = "test input";
379 let report =
380 Report::<(&str, std::ops::Range<usize>)>::build(ReportKind::Error, ("test.styx", 0..4))
381 .with_config(config)
382 .with_message("test error")
383 .with_label(
384 Label::new(("test.styx", 0..4))
385 .with_message("here")
386 .with_color(Color::Red),
387 )
388 .finish();
389
390 let mut output = Vec::new();
391 report
392 .write(("test.styx", Source::from(source)), &mut output)
393 .unwrap();
394 let s = String::from_utf8(output).unwrap();
395 eprintln!("Output: {:?}", s);
396
397 assert!(no_color, "NO_COLOR should be set by nextest setup script");
399 assert!(
400 !s.contains("\x1b["),
401 "With NO_COLOR set, output should not contain ANSI escape codes:\n{:?}",
402 s
403 );
404 }
405
406 #[derive(Facet, Debug)]
407 struct Person {
408 name: String,
409 age: u32,
410 }
411
412 #[test]
413 fn test_missing_field_diagnostic() {
414 let source = "name Alice";
415 let result: Result<Person, _> = crate::from_str(source);
416 let err = result.unwrap_err();
417
418 let rendered = RenderError::render(&err, "test.styx", source);
420
421 let no_color = std::env::var("NO_COLOR").is_ok();
423 if no_color {
424 assert!(
425 !rendered.contains("\x1b["),
426 "Output should not contain ANSI escape codes:\n{:?}",
427 rendered
428 );
429 }
430
431 insta::assert_snapshot!(rendered);
432 }
433
434 #[test]
435 fn test_invalid_scalar_diagnostic() {
436 let source = "name Alice\nage notanumber";
437 let result: Result<Person, _> = crate::from_str(source);
438 let err = result.unwrap_err();
439
440 let rendered = err.render("test.styx", source);
441 insta::assert_snapshot!(rendered);
442 }
443
444 #[test]
445 fn test_unknown_field_diagnostic() {
446 #[derive(Facet, Debug)]
447 #[facet(deny_unknown_fields)]
448 struct Strict {
449 name: String,
450 }
451
452 let source = "name Alice\nunknown_field value";
453 let result: Result<Strict, _> = crate::from_str(source);
454 let err = result.unwrap_err();
455
456 let rendered = err.render("test.styx", source);
457 insta::assert_snapshot!(rendered);
458 }
459}