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
91#[derive(Debug, Clone)]
96pub struct SyntaxError {
97 pub message: String,
99 pub line: Option<usize>,
101 pub snippet: Option<String>,
103}
104
105impl SyntaxError {
106 #[must_use]
108 pub fn new(message: impl Into<String>) -> Self {
109 Self {
110 message: message.into(),
111 line: None,
112 snippet: None,
113 }
114 }
115
116 #[must_use]
118 pub fn at_line(mut self, line: usize, snippet: impl Into<String>) -> Self {
119 self.line = Some(line);
120 self.snippet = Some(snippet.into());
121 self
122 }
123}
124
125impl fmt::Display for SyntaxError {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match (self.line, self.snippet.as_deref()) {
128 (Some(line), Some(snippet)) => {
129 write!(f, "line {line}: {}\n --> {snippet}", self.message)
130 }
131 (Some(line), None) => write!(f, "line {line}: {}", self.message),
132 _ => f.write_str(&self.message),
133 }
134 }
135}
136
137impl From<String> for SyntaxError {
139 fn from(message: String) -> Self {
140 Self::new(message)
141 }
142}
143
144pub(crate) fn levenshtein_distance(a: &str, b: &str) -> usize {
149 let a_len = a.len();
150 let b_len = b.len();
151 if a_len == 0 {
152 return b_len;
153 }
154 if b_len == 0 {
155 return a_len;
156 }
157 let mut prev: Vec<usize> = (0..=b_len).collect();
158 let mut curr = vec![0; b_len + 1];
159 for (i, ca) in a.chars().enumerate() {
160 curr[0] = i + 1;
161 for (j, cb) in b.chars().enumerate() {
162 let cost = usize::from(ca != cb);
163 curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
164 }
165 core::mem::swap(&mut prev, &mut curr);
166 }
167 prev[b_len]
168}
169
170#[cfg(test)]
171mod tests {
172 use alloc::string::ToString;
173
174 use super::*;
175
176 #[test]
179 fn syntax_error_new_sets_message() {
180 let err = SyntaxError::new("unexpected token");
181 assert_eq!(err.message, "unexpected token");
182 }
183
184 #[test]
185 fn syntax_error_new_defaults_line_to_none() {
186 let err = SyntaxError::new("oops");
187 assert!(err.line.is_none());
188 }
189
190 #[test]
191 fn syntax_error_new_defaults_snippet_to_none() {
192 let err = SyntaxError::new("oops");
193 assert!(err.snippet.is_none());
194 }
195
196 #[test]
199 fn syntax_error_at_line_sets_line_and_snippet() {
200 let err = SyntaxError::new("bad token").at_line(42, "{{ bad }}");
201 assert_eq!(err.line, Some(42));
202 assert_eq!(err.snippet.as_deref(), Some("{{ bad }}"));
203 assert_eq!(err.message, "bad token");
204 }
205
206 #[test]
207 fn syntax_error_at_line_line_one() {
208 let err = SyntaxError::new("err").at_line(1, "line1");
209 assert_eq!(err.line, Some(1));
210 }
211
212 #[test]
215 fn syntax_error_display_with_line_and_snippet() {
216 let err = SyntaxError::new("unexpected end").at_line(7, "{{ if");
217 let formatted = err.to_string();
218 assert_eq!(formatted, "line 7: unexpected end\n --> {{ if");
219 }
220
221 #[test]
222 fn syntax_error_display_with_line_only() {
223 let mut err = SyntaxError::new("missing bracket");
224 err.line = Some(3);
225 let formatted = err.to_string();
227 assert_eq!(formatted, "line 3: missing bracket");
228 }
229
230 #[test]
231 fn syntax_error_display_message_only() {
232 let err = SyntaxError::new("generic problem");
233 assert_eq!(err.to_string(), "generic problem");
234 }
235
236 #[test]
237 fn syntax_error_display_message_only_when_snippet_without_line() {
238 let mut err = SyntaxError::new("edge case");
240 err.snippet = Some("some snippet".into());
241 assert_eq!(err.to_string(), "edge case");
243 }
244
245 #[test]
248 fn syntax_error_from_string() {
249 let s = String::from("converted message");
250 let err: SyntaxError = s.into();
251 assert_eq!(err.message, "converted message");
252 assert!(err.line.is_none());
253 assert!(err.snippet.is_none());
254 }
255
256 #[test]
259 fn template_error_syntax_constructor() {
260 let err = TemplateError::syntax("bad template");
261 match &err {
262 TemplateError::Syntax(inner) => {
263 assert_eq!(inner.message, "bad template");
264 assert!(inner.line.is_none());
265 }
266 other => panic!("expected Syntax variant, got: {other}"),
267 }
268 }
269
270 #[test]
271 fn template_error_syntax_accepts_string() {
272 let err = TemplateError::syntax(String::from("owned msg"));
273 assert!(matches!(err, TemplateError::Syntax(_)));
274 }
275
276 #[test]
279 fn template_error_display_undefined_variable() {
280 let err = TemplateError::UndefinedVariable("user_name".into());
281 assert_eq!(err.to_string(), "undefined variable: user_name");
282 }
283
284 #[test]
285 fn template_error_display_syntax() {
286 let err = TemplateError::Syntax(SyntaxError::new("unexpected end of input"));
287 assert_eq!(
288 err.to_string(),
289 "template syntax error: unexpected end of input"
290 );
291 }
292
293 #[test]
294 fn template_error_display_missing_params() {
295 let err = TemplateError::MissingParams(vec!["alpha".into(), "beta".into()]);
296 assert_eq!(err.to_string(), "missing required parameters: alpha, beta");
297 }
298
299 #[test]
300 fn template_error_display_missing_params_single() {
301 let err = TemplateError::MissingParams(vec!["only".into()]);
302 assert_eq!(err.to_string(), "missing required parameters: only");
303 }
304
305 #[test]
306 fn template_error_display_type_mismatch() {
307 let err = TemplateError::TypeMismatch {
308 name: "count".into(),
309 expected: "int".into(),
310 actual: "string".into(),
311 actual_value: "\"hello\"".into(),
312 };
313 assert_eq!(
314 err.to_string(),
315 "type mismatch for 'count': expected int, got string (\"hello\")"
316 );
317 }
318
319 #[test]
320 fn template_error_display_unknown_filter() {
321 let err = TemplateError::UnknownFilter("capitalize".into());
322 assert_eq!(err.to_string(), "unknown filter: capitalize");
323 }
324
325 #[test]
326 fn template_error_display_include_not_found() {
327 let err = TemplateError::IncludeNotFound("header.tmpl".into());
328 assert_eq!(err.to_string(), "include not found: header.tmpl");
329 }
330
331 #[test]
332 fn template_error_display_declarations_mutated() {
333 let err = TemplateError::DeclarationsMutated {
334 details: "added parameter 'foo'".into(),
335 };
336 let msg = err.to_string();
337 assert!(msg.contains("declarations were modified at runtime"));
338 assert!(msg.contains("added parameter 'foo'"));
339 }
340
341 #[test]
342 fn template_error_display_extra_params() {
343 let err = TemplateError::ExtraParams(vec!["x".into(), "y".into()]);
344 assert_eq!(err.to_string(), "extra undeclared parameters: x, y");
345 }
346
347 #[test]
348 fn template_error_display_panic() {
349 let err = TemplateError::panic("custom panic message");
350 assert_eq!(err.to_string(), "template panic: custom panic message");
351 }
352
353 #[test]
356 fn levenshtein_identical_strings() {
357 assert_eq!(levenshtein_distance("hello", "hello"), 0);
358 }
359
360 #[test]
361 fn levenshtein_empty_strings() {
362 assert_eq!(levenshtein_distance("", ""), 0);
363 }
364
365 #[test]
366 fn levenshtein_one_empty() {
367 assert_eq!(levenshtein_distance("abc", ""), 3);
368 assert_eq!(levenshtein_distance("", "xyz"), 3);
369 }
370
371 #[test]
372 fn levenshtein_single_char_diff() {
373 assert_eq!(levenshtein_distance("cat", "bat"), 1);
374 }
375
376 #[test]
377 fn levenshtein_kitten_sitting() {
378 assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
379 }
380
381 #[test]
382 fn levenshtein_completely_different() {
383 assert_eq!(levenshtein_distance("abc", "xyz"), 3);
384 }
385
386 #[test]
387 fn levenshtein_prefix() {
388 assert_eq!(levenshtein_distance("abc", "abcdef"), 3);
389 }
390
391 #[test]
392 fn levenshtein_single_insertion() {
393 assert_eq!(levenshtein_distance("ac", "abc"), 1);
394 }
395
396 #[test]
397 fn levenshtein_single_deletion() {
398 assert_eq!(levenshtein_distance("abc", "ac"), 1);
399 }
400
401 #[test]
402 fn levenshtein_symmetric() {
403 assert_eq!(
404 levenshtein_distance("foo", "bar"),
405 levenshtein_distance("bar", "foo")
406 );
407 }
408}