1use alloc::{string::String, vec::Vec};
4use core::fmt;
5
6#[derive(Debug, thiserror::Error)]
8pub enum TemplateError {
9 #[cfg(feature = "std")]
11 #[error("failed to load template: {0}")]
12 Io(#[from] std::io::Error),
13
14 #[error("undefined variable: {0}")]
16 UndefinedVariable(String),
17
18 #[error("template syntax error: {0}")]
20 Syntax(SyntaxError),
21
22 #[error("missing required parameters: {}", .0.join(", "))]
24 MissingParams(Vec<String>),
25
26 #[error("type mismatch for '{name}': expected {expected}, got {actual} ({actual_value})")]
28 TypeMismatch {
29 name: String,
31 expected: String,
33 actual: String,
35 actual_value: String,
37 },
38
39 #[error("unknown filter: {0}")]
41 UnknownFilter(String),
42
43 #[error("include not found: {0}")]
45 IncludeNotFound(String),
46
47 #[error(
54 "template parameter declarations were modified at runtime: {details}. \
55 The frontmatter `params:` block is part of the compile-time \
56 contract and must not be changed"
57 )]
58 DeclarationsMutated {
59 details: String,
61 },
62
63 #[error("extra undeclared parameters: {}", .0.join(", "))]
69 ExtraParams(Vec<String>),
70
71 #[error("template panic: {0}")]
73 Panic(String),
74}
75
76impl TemplateError {
77 pub(crate) fn panic(msg: impl Into<String>) -> Self {
79 Self::Panic(msg.into())
80 }
81
82 pub(crate) fn syntax(msg: impl Into<String>) -> Self {
87 Self::Syntax(SyntaxError::new(msg))
88 }
89
90 #[must_use]
97 pub fn kind(&self) -> ErrorKind {
98 match self {
99 #[cfg(feature = "std")]
100 Self::Io(_) => ErrorKind::Io,
101 Self::UndefinedVariable(_) => ErrorKind::UndefinedVariable,
102 Self::Syntax(_) => ErrorKind::Syntax,
103 Self::MissingParams(_) => ErrorKind::MissingParams,
104 Self::TypeMismatch { .. } => ErrorKind::TypeMismatch,
105 Self::UnknownFilter(_) => ErrorKind::UnknownFilter,
106 Self::IncludeNotFound(_) => ErrorKind::IncludeNotFound,
107 Self::DeclarationsMutated { .. } => ErrorKind::DeclarationsMutated,
108 Self::ExtraParams(_) => ErrorKind::ExtraParams,
109 Self::Panic(_) => ErrorKind::Panic,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[non_exhaustive]
122pub enum ErrorKind {
123 Io,
125 UndefinedVariable,
127 Syntax,
129 MissingParams,
131 TypeMismatch,
133 UnknownFilter,
135 IncludeNotFound,
137 DeclarationsMutated,
139 ExtraParams,
141 Panic,
143}
144
145impl ErrorKind {
146 #[must_use]
151 pub const fn as_str(self) -> &'static str {
152 match self {
153 Self::Io => "io",
154 Self::UndefinedVariable => "undefined_variable",
155 Self::Syntax => "syntax",
156 Self::MissingParams => "missing_params",
157 Self::TypeMismatch => "type_mismatch",
158 Self::UnknownFilter => "unknown_filter",
159 Self::IncludeNotFound => "include_not_found",
160 Self::DeclarationsMutated => "declarations_mutated",
161 Self::ExtraParams => "extra_params",
162 Self::Panic => "panic",
163 }
164 }
165}
166
167impl fmt::Display for ErrorKind {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str(self.as_str())
170 }
171}
172
173#[derive(Debug, Clone)]
178pub struct SyntaxError {
179 pub message: String,
181 pub line: Option<usize>,
183 pub snippet: Option<String>,
185}
186
187impl SyntaxError {
188 #[must_use]
190 pub fn new(message: impl Into<String>) -> Self {
191 Self {
192 message: message.into(),
193 line: None,
194 snippet: None,
195 }
196 }
197
198 #[must_use]
200 pub fn at_line(mut self, line: usize, snippet: impl Into<String>) -> Self {
201 self.line = Some(line);
202 self.snippet = Some(snippet.into());
203 self
204 }
205}
206
207impl fmt::Display for SyntaxError {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 match (self.line, self.snippet.as_deref()) {
210 (Some(line), Some(snippet)) => {
211 write!(f, "line {line}: {}\n --> {snippet}", self.message)
212 }
213 (Some(line), None) => write!(f, "line {line}: {}", self.message),
214 _ => f.write_str(&self.message),
215 }
216 }
217}
218
219impl From<String> for SyntaxError {
221 fn from(message: String) -> Self {
222 Self::new(message)
223 }
224}
225
226pub(crate) fn levenshtein_distance(a: &str, b: &str) -> usize {
231 let a_len = a.len();
232 let b_len = b.len();
233 if a_len == 0 {
234 return b_len;
235 }
236 if b_len == 0 {
237 return a_len;
238 }
239 let mut prev: Vec<usize> = (0..=b_len).collect();
240 let mut curr = vec![0; b_len + 1];
241 for (i, ca) in a.chars().enumerate() {
242 curr[0] = i + 1;
243 for (j, cb) in b.chars().enumerate() {
244 let cost = usize::from(ca != cb);
245 curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
246 }
247 core::mem::swap(&mut prev, &mut curr);
248 }
249 prev[b_len]
250}
251
252#[cfg(test)]
253mod tests {
254 use alloc::string::ToString;
255
256 use super::*;
257
258 #[test]
261 fn syntax_error_new_sets_message() {
262 let err = SyntaxError::new("unexpected token");
263 assert_eq!(err.message, "unexpected token");
264 }
265
266 #[test]
267 fn syntax_error_new_defaults_line_to_none() {
268 let err = SyntaxError::new("oops");
269 assert!(err.line.is_none());
270 }
271
272 #[test]
273 fn syntax_error_new_defaults_snippet_to_none() {
274 let err = SyntaxError::new("oops");
275 assert!(err.snippet.is_none());
276 }
277
278 #[test]
281 fn syntax_error_at_line_sets_line_and_snippet() {
282 let err = SyntaxError::new("bad token").at_line(42, "{{ bad }}");
283 assert_eq!(err.line, Some(42));
284 assert_eq!(err.snippet.as_deref(), Some("{{ bad }}"));
285 assert_eq!(err.message, "bad token");
286 }
287
288 #[test]
289 fn syntax_error_at_line_line_one() {
290 let err = SyntaxError::new("err").at_line(1, "line1");
291 assert_eq!(err.line, Some(1));
292 }
293
294 #[test]
297 fn syntax_error_display_with_line_and_snippet() {
298 let err = SyntaxError::new("unexpected end").at_line(7, "{{ if");
299 let formatted = err.to_string();
300 assert_eq!(formatted, "line 7: unexpected end\n --> {{ if");
301 }
302
303 #[test]
304 fn syntax_error_display_with_line_only() {
305 let mut err = SyntaxError::new("missing bracket");
306 err.line = Some(3);
307 let formatted = err.to_string();
309 assert_eq!(formatted, "line 3: missing bracket");
310 }
311
312 #[test]
313 fn syntax_error_display_message_only() {
314 let err = SyntaxError::new("generic problem");
315 assert_eq!(err.to_string(), "generic problem");
316 }
317
318 #[test]
319 fn syntax_error_display_message_only_when_snippet_without_line() {
320 let mut err = SyntaxError::new("edge case");
322 err.snippet = Some("some snippet".into());
323 assert_eq!(err.to_string(), "edge case");
325 }
326
327 #[test]
330 fn syntax_error_from_string() {
331 let s = String::from("converted message");
332 let err: SyntaxError = s.into();
333 assert_eq!(err.message, "converted message");
334 assert!(err.line.is_none());
335 assert!(err.snippet.is_none());
336 }
337
338 #[test]
341 fn template_error_syntax_constructor() {
342 let err = TemplateError::syntax("bad template");
343 match &err {
344 TemplateError::Syntax(inner) => {
345 assert_eq!(inner.message, "bad template");
346 assert!(inner.line.is_none());
347 }
348 other => panic!("expected Syntax variant, got: {other}"),
349 }
350 }
351
352 #[test]
353 fn template_error_syntax_accepts_string() {
354 let err = TemplateError::syntax(String::from("owned msg"));
355 assert!(matches!(err, TemplateError::Syntax(_)));
356 }
357
358 #[test]
361 fn template_error_display_undefined_variable() {
362 let err = TemplateError::UndefinedVariable("user_name".into());
363 assert_eq!(err.to_string(), "undefined variable: user_name");
364 }
365
366 #[test]
367 fn template_error_display_syntax() {
368 let err = TemplateError::Syntax(SyntaxError::new("unexpected end of input"));
369 assert_eq!(
370 err.to_string(),
371 "template syntax error: unexpected end of input"
372 );
373 }
374
375 #[test]
376 fn template_error_display_missing_params() {
377 let err = TemplateError::MissingParams(vec!["alpha".into(), "beta".into()]);
378 assert_eq!(err.to_string(), "missing required parameters: alpha, beta");
379 }
380
381 #[test]
382 fn template_error_display_missing_params_single() {
383 let err = TemplateError::MissingParams(vec!["only".into()]);
384 assert_eq!(err.to_string(), "missing required parameters: only");
385 }
386
387 #[test]
388 fn template_error_display_type_mismatch() {
389 let err = TemplateError::TypeMismatch {
390 name: "count".into(),
391 expected: "int".into(),
392 actual: "string".into(),
393 actual_value: "\"hello\"".into(),
394 };
395 assert_eq!(
396 err.to_string(),
397 "type mismatch for 'count': expected int, got string (\"hello\")"
398 );
399 }
400
401 #[test]
402 fn template_error_display_unknown_filter() {
403 let err = TemplateError::UnknownFilter("capitalize".into());
404 assert_eq!(err.to_string(), "unknown filter: capitalize");
405 }
406
407 #[test]
408 fn template_error_display_include_not_found() {
409 let err = TemplateError::IncludeNotFound("header.tmpl".into());
410 assert_eq!(err.to_string(), "include not found: header.tmpl");
411 }
412
413 #[test]
414 fn template_error_display_declarations_mutated() {
415 let err = TemplateError::DeclarationsMutated {
416 details: "added parameter 'foo'".into(),
417 };
418 let msg = err.to_string();
419 assert!(msg.contains("declarations were modified at runtime"));
420 assert!(msg.contains("added parameter 'foo'"));
421 }
422
423 #[test]
424 fn template_error_display_extra_params() {
425 let err = TemplateError::ExtraParams(vec!["x".into(), "y".into()]);
426 assert_eq!(err.to_string(), "extra undeclared parameters: x, y");
427 }
428
429 #[test]
430 fn template_error_display_panic() {
431 let err = TemplateError::panic("custom panic message");
432 assert_eq!(err.to_string(), "template panic: custom panic message");
433 }
434
435 #[test]
438 fn error_kind_maps_every_variant() {
439 assert_eq!(
440 TemplateError::UndefinedVariable("x".into()).kind(),
441 ErrorKind::UndefinedVariable
442 );
443 assert_eq!(TemplateError::syntax("bad").kind(), ErrorKind::Syntax);
444 assert_eq!(
445 TemplateError::MissingParams(vec!["a".into()]).kind(),
446 ErrorKind::MissingParams
447 );
448 assert_eq!(
449 TemplateError::TypeMismatch {
450 name: "n".into(),
451 expected: "int".into(),
452 actual: "str".into(),
453 actual_value: "\"x\"".into(),
454 }
455 .kind(),
456 ErrorKind::TypeMismatch
457 );
458 assert_eq!(
459 TemplateError::UnknownFilter("f".into()).kind(),
460 ErrorKind::UnknownFilter
461 );
462 assert_eq!(
463 TemplateError::IncludeNotFound("i".into()).kind(),
464 ErrorKind::IncludeNotFound
465 );
466 assert_eq!(
467 TemplateError::DeclarationsMutated {
468 details: "d".into()
469 }
470 .kind(),
471 ErrorKind::DeclarationsMutated
472 );
473 assert_eq!(
474 TemplateError::ExtraParams(vec!["e".into()]).kind(),
475 ErrorKind::ExtraParams
476 );
477 assert_eq!(TemplateError::panic("p").kind(), ErrorKind::Panic);
478 }
479
480 #[test]
481 fn error_kind_as_str_is_stable() {
482 assert_eq!(ErrorKind::Io.as_str(), "io");
485 assert_eq!(ErrorKind::UndefinedVariable.as_str(), "undefined_variable");
486 assert_eq!(ErrorKind::Syntax.as_str(), "syntax");
487 assert_eq!(ErrorKind::MissingParams.as_str(), "missing_params");
488 assert_eq!(ErrorKind::TypeMismatch.as_str(), "type_mismatch");
489 assert_eq!(ErrorKind::UnknownFilter.as_str(), "unknown_filter");
490 assert_eq!(ErrorKind::IncludeNotFound.as_str(), "include_not_found");
491 assert_eq!(
492 ErrorKind::DeclarationsMutated.as_str(),
493 "declarations_mutated"
494 );
495 assert_eq!(ErrorKind::ExtraParams.as_str(), "extra_params");
496 assert_eq!(ErrorKind::Panic.as_str(), "panic");
497 }
498
499 #[test]
500 fn error_kind_display_matches_as_str() {
501 assert_eq!(ErrorKind::MissingParams.to_string(), "missing_params");
502 }
503
504 #[test]
507 fn levenshtein_identical_strings() {
508 assert_eq!(levenshtein_distance("hello", "hello"), 0);
509 }
510
511 #[test]
512 fn levenshtein_empty_strings() {
513 assert_eq!(levenshtein_distance("", ""), 0);
514 }
515
516 #[test]
517 fn levenshtein_one_empty() {
518 assert_eq!(levenshtein_distance("abc", ""), 3);
519 assert_eq!(levenshtein_distance("", "xyz"), 3);
520 }
521
522 #[test]
523 fn levenshtein_single_char_diff() {
524 assert_eq!(levenshtein_distance("cat", "bat"), 1);
525 }
526
527 #[test]
528 fn levenshtein_kitten_sitting() {
529 assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
530 }
531
532 #[test]
533 fn levenshtein_completely_different() {
534 assert_eq!(levenshtein_distance("abc", "xyz"), 3);
535 }
536
537 #[test]
538 fn levenshtein_prefix() {
539 assert_eq!(levenshtein_distance("abc", "abcdef"), 3);
540 }
541
542 #[test]
543 fn levenshtein_single_insertion() {
544 assert_eq!(levenshtein_distance("ac", "abc"), 1);
545 }
546
547 #[test]
548 fn levenshtein_single_deletion() {
549 assert_eq!(levenshtein_distance("abc", "ac"), 1);
550 }
551
552 #[test]
553 fn levenshtein_symmetric() {
554 assert_eq!(
555 levenshtein_distance("foo", "bar"),
556 levenshtein_distance("bar", "foo")
557 );
558 }
559}