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