1use ariadne::{Color, Config, Label, Report, ReportKind, Source};
4use facet_format::{DeserializeError, DeserializeErrorKind};
5
6fn ariadne_config() -> Config {
8 let no_color = std::env::var("NO_COLOR").is_ok();
9 if no_color {
10 Config::default().with_color(false)
11 } else {
12 Config::default()
13 }
14}
15
16fn reflect_span_to_range(span: &facet_reflect::Span) -> std::ops::Range<usize> {
18 let start = span.offset as usize;
19 let end = start + span.len as usize;
20 start..end
21}
22
23pub trait RenderError {
25 fn render(&self, filename: &str, source: &str) -> String;
29
30 fn write_report<W: std::io::Write>(&self, filename: &str, source: &str, writer: W);
32}
33
34impl RenderError for DeserializeError {
39 fn render(&self, filename: &str, source: &str) -> String {
40 let mut output = Vec::new();
41 self.write_report(filename, source, &mut output);
42 String::from_utf8(output).unwrap_or_else(|_| format!("{}", self))
43 }
44
45 fn write_report<W: std::io::Write>(&self, filename: &str, source: &str, writer: W) {
46 let report = build_deserialize_error_report(self, filename, source, ariadne_config());
49 let _ = report
50 .finish()
51 .write((filename, Source::from(source)), writer);
52 }
53}
54
55fn build_deserialize_error_report<'a>(
56 err: &DeserializeError,
57 filename: &'a str,
58 source: &str,
59 config: Config,
60) -> ariadne::ReportBuilder<'static, (&'a str, std::ops::Range<usize>)> {
61 let range = err
63 .span
64 .as_ref()
65 .map(reflect_span_to_range)
66 .unwrap_or(0..source.len().max(1));
67
68 match &err.kind {
69 DeserializeErrorKind::MissingField {
71 field,
72 container_shape,
73 } => Report::build(ReportKind::Error, (filename, range.clone()))
74 .with_config(config)
75 .with_message(format!("missing required field '{}'", field))
76 .with_label(
77 Label::new((filename, range))
78 .with_message(format!("in {}", container_shape))
79 .with_color(Color::Red),
80 )
81 .with_help(format!("{} <value>", field)),
82
83 DeserializeErrorKind::UnknownField { field, suggestion } => {
85 let mut report = Report::build(ReportKind::Error, (filename, range.clone()))
86 .with_config(config)
87 .with_message(format!("unknown field '{}'", field))
88 .with_label(
89 Label::new((filename, range))
90 .with_message("unknown field")
91 .with_color(Color::Red),
92 );
93 if let Some(s) = suggestion {
94 report = report.with_help(format!("did you mean '{}'?", s));
95 }
96 report
97 }
98
99 DeserializeErrorKind::TypeMismatch { expected, got } => {
101 Report::build(ReportKind::Error, (filename, range.clone()))
102 .with_config(config)
103 .with_message(format!("type mismatch: expected {}", expected))
104 .with_label(
105 Label::new((filename, range))
106 .with_message(format!("got {}", got))
107 .with_color(Color::Red),
108 )
109 }
110
111 DeserializeErrorKind::Reflect { kind, context } => {
113 let mut report = Report::build(ReportKind::Error, (filename, range.clone()))
114 .with_config(config)
115 .with_message(format!("{}", kind))
116 .with_label(
117 Label::new((filename, range))
118 .with_message("error here")
119 .with_color(Color::Red),
120 );
121 if !context.is_empty() {
122 report = report.with_note(format!("while {}", context));
123 }
124 report
125 }
126
127 DeserializeErrorKind::UnexpectedEof { expected } => {
129 let eof_range = source.len().saturating_sub(1)..source.len().max(1);
130 Report::build(ReportKind::Error, (filename, eof_range.clone()))
131 .with_config(config)
132 .with_message("unexpected end of input")
133 .with_label(
134 Label::new((filename, eof_range))
135 .with_message(format!("expected {}", expected))
136 .with_color(Color::Red),
137 )
138 }
139
140 DeserializeErrorKind::Unsupported { message } => {
142 Report::build(ReportKind::Error, (filename, 0..1))
143 .with_config(config)
144 .with_message(format!("unsupported: {}", message))
145 }
146
147 DeserializeErrorKind::CannotBorrow { reason } => {
149 Report::build(ReportKind::Error, (filename, 0..1))
150 .with_config(config)
151 .with_message(reason)
152 }
153
154 DeserializeErrorKind::UnexpectedToken { got, expected } => {
156 Report::build(ReportKind::Error, (filename, range.clone()))
157 .with_config(config)
158 .with_message(format!("unexpected token '{}'", got))
159 .with_label(
160 Label::new((filename, range))
161 .with_message(format!("expected {}", expected))
162 .with_color(Color::Red),
163 )
164 }
165
166 DeserializeErrorKind::InvalidValue { message } => {
168 Report::build(ReportKind::Error, (filename, range.clone()))
169 .with_config(config)
170 .with_message(format!("invalid value: {}", message))
171 .with_label(
172 Label::new((filename, range))
173 .with_message("here")
174 .with_color(Color::Red),
175 )
176 }
177
178 _ => Report::build(ReportKind::Error, (filename, range.clone()))
180 .with_config(config)
181 .with_message(format!("{}", err.kind))
182 .with_label(
183 Label::new((filename, range))
184 .with_message("error here")
185 .with_color(Color::Red),
186 ),
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use facet::Facet;
194
195 #[test]
196 fn test_ariadne_no_color() {
197 let config = Config::default().with_color(false);
199
200 let source = "test input";
201 let report =
202 Report::<(&str, std::ops::Range<usize>)>::build(ReportKind::Error, ("test.styx", 0..4))
203 .with_config(config)
204 .with_message("test error")
205 .with_label(
206 Label::new(("test.styx", 0..4))
207 .with_message("here")
208 .with_color(Color::Red),
209 )
210 .finish();
211
212 let mut output = Vec::new();
213 report
214 .write(("test.styx", Source::from(source)), &mut output)
215 .unwrap();
216 let s = String::from_utf8(output).unwrap();
217
218 assert!(
220 !s.contains("\x1b["),
221 "Output should not contain ANSI escape codes when color is disabled:\n{:?}",
222 s
223 );
224 }
225
226 #[test]
227 fn test_ariadne_config_respects_no_color_env() {
228 let no_color = std::env::var("NO_COLOR").is_ok();
230 eprintln!("NO_COLOR is set: {}", no_color);
231
232 let config = ariadne_config();
233
234 let source = "test input";
235 let report =
236 Report::<(&str, std::ops::Range<usize>)>::build(ReportKind::Error, ("test.styx", 0..4))
237 .with_config(config)
238 .with_message("test error")
239 .with_label(
240 Label::new(("test.styx", 0..4))
241 .with_message("here")
242 .with_color(Color::Red),
243 )
244 .finish();
245
246 let mut output = Vec::new();
247 report
248 .write(("test.styx", Source::from(source)), &mut output)
249 .unwrap();
250 let s = String::from_utf8(output).unwrap();
251 eprintln!("Output: {:?}", s);
252
253 assert!(no_color, "NO_COLOR should be set by nextest setup script");
255 assert!(
256 !s.contains("\x1b["),
257 "With NO_COLOR set, output should not contain ANSI escape codes:\n{:?}",
258 s
259 );
260 }
261
262 #[derive(Facet, Debug)]
263 struct Person {
264 name: String,
265 age: u32,
266 }
267
268 #[test]
269 fn test_missing_field_diagnostic() {
270 let source = "name Alice";
271 let result: Result<Person, _> = crate::from_str(source);
272 let err = result.unwrap_err();
273
274 crate::assert_snapshot_stripped!(RenderError::render(&err, "test.styx", source));
275 }
276
277 #[test]
278 fn test_invalid_scalar_diagnostic() {
279 let source = "name Alice\nage notanumber";
280 let result: Result<Person, _> = crate::from_str(source);
281 let err = result.unwrap_err();
282
283 crate::assert_snapshot_stripped!(err.render("test.styx", source));
284 }
285
286 #[test]
287 fn test_unknown_field_diagnostic() {
288 #[derive(Facet, Debug)]
289 #[facet(deny_unknown_fields)]
290 struct Strict {
291 name: String,
292 }
293
294 let source = "name Alice\nunknown_field value";
295 let result: Result<Strict, _> = crate::from_str(source);
296 let err = result.unwrap_err();
297
298 crate::assert_snapshot_stripped!(err.render("test.styx", source));
299 }
300}