1use std::error::Error as StdError;
2use std::fmt::{self, Write};
3use std::io::Error as IOError;
4use std::string::FromUtf8Error;
5
6use serde_json::error::Error as SerdeError;
7use thiserror::Error;
8
9#[cfg(feature = "dir_source")]
10use walkdir::Error as WalkdirError;
11
12#[cfg(feature = "script_helper")]
13use rhai::{EvalAltResult, ParseError};
14
15#[derive(Debug)]
17pub struct RenderError {
18 pub template_name: Option<String>,
19 pub line_no: Option<usize>,
20 pub column_no: Option<usize>,
21 reason: Box<RenderErrorReason>,
22 unimplemented: bool,
23 }
25
26impl fmt::Display for RenderError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
28 let desc = self.reason.to_string();
29
30 match (self.line_no, self.column_no) {
31 (Some(line), Some(col)) => write!(
32 f,
33 "Error rendering \"{}\" line {}, col {}: {}",
34 self.template_name.as_deref().unwrap_or("Unnamed template"),
35 line,
36 col,
37 desc
38 ),
39 _ => write!(f, "{}", desc),
40 }
41 }
42}
43
44impl From<IOError> for RenderError {
45 fn from(e: IOError) -> RenderError {
46 RenderErrorReason::IOError(e).into()
47 }
48}
49
50impl From<FromUtf8Error> for RenderError {
51 fn from(e: FromUtf8Error) -> Self {
52 RenderErrorReason::Utf8Error(e).into()
53 }
54}
55
56impl From<TemplateError> for RenderError {
57 fn from(e: TemplateError) -> Self {
58 RenderErrorReason::TemplateError(e).into()
59 }
60}
61
62#[derive(Debug, Error)]
64pub enum RenderErrorReason {
65 #[error("Template not found {0}")]
66 TemplateNotFound(String),
67 #[error("Failed to parse template {0}")]
68 TemplateError(
69 #[from]
70 #[source]
71 TemplateError,
72 ),
73 #[error("Failed to access variable in strict mode {0:?}")]
74 MissingVariable(Option<String>),
75 #[error("Partial not found {0}")]
76 PartialNotFound(String),
77 #[error("Helper not found {0}")]
78 HelperNotFound(String),
79 #[error("Helper/Decorator {0} param at index {1} required but not found")]
80 ParamNotFoundForIndex(&'static str, usize),
81 #[error("Helper/Decorator {0} param with name {1} required but not found")]
82 ParamNotFoundForName(&'static str, String),
83 #[error("Helper/Decorator {0} param with name {1} type mismatch for {2}")]
84 ParamTypeMismatchForName(&'static str, String, String),
85 #[error("Helper/Decorator {0} hash with name {1} type mismatch for {2}")]
86 HashTypeMismatchForName(&'static str, String, String),
87 #[error("Decorator not found {0}")]
88 DecoratorNotFound(String),
89 #[error("Can not include current template in partial")]
90 CannotIncludeSelf,
91 #[error("Invalid logging level: {0}")]
92 InvalidLoggingLevel(String),
93 #[error("Invalid param type, {0} expected")]
94 InvalidParamType(&'static str),
95 #[error("Block content required")]
96 BlockContentRequired,
97 #[error("Invalid json path {0}")]
98 InvalidJsonPath(String),
99 #[error("Cannot access array/vector with string index, {0}")]
100 InvalidJsonIndex(String),
101 #[error("Failed to access JSON data: {0}")]
102 SerdeError(
103 #[from]
104 #[source]
105 SerdeError,
106 ),
107 #[error("IO Error: {0}")]
108 IOError(
109 #[from]
110 #[source]
111 IOError,
112 ),
113 #[error("FromUtf8Error: {0}")]
114 Utf8Error(
115 #[from]
116 #[source]
117 FromUtf8Error,
118 ),
119 #[error("Nested error: {0}")]
120 NestedError(#[source] Box<dyn StdError + Send + Sync + 'static>),
121 #[cfg(feature = "script_helper")]
122 #[error("Cannot convert data to Rhai dynamic: {0}")]
123 ScriptValueError(
124 #[from]
125 #[source]
126 Box<EvalAltResult>,
127 ),
128 #[cfg(feature = "script_helper")]
129 #[error("Failed to load rhai script: {0}")]
130 ScriptLoadError(
131 #[from]
132 #[source]
133 ScriptError,
134 ),
135 #[error("Unimplemented")]
136 Unimplemented,
137 #[error("{0}")]
138 Other(String),
139}
140
141impl From<RenderErrorReason> for RenderError {
142 fn from(e: RenderErrorReason) -> RenderError {
143 RenderError {
144 template_name: None,
145 line_no: None,
146 column_no: None,
147 reason: Box::new(e),
148 unimplemented: false,
149 }
150 }
151}
152
153impl RenderError {
154 #[deprecated(since = "5.0.0", note = "Use RenderErrorReason instead")]
155 pub fn new<T: AsRef<str>>(desc: T) -> RenderError {
156 RenderErrorReason::Other(desc.as_ref().to_string()).into()
157 }
158
159 pub fn strict_error(path: Option<&String>) -> RenderError {
160 RenderErrorReason::MissingVariable(path.map(|p| p.to_owned())).into()
161 }
162
163 #[deprecated(since = "5.0.0", note = "Use RenderErrorReason::NestedError instead")]
164 pub fn from_error<E>(_error_info: &str, cause: E) -> RenderError
165 where
166 E: StdError + Send + Sync + 'static,
167 {
168 RenderErrorReason::NestedError(Box::new(cause)).into()
169 }
170
171 #[inline]
172 pub(crate) fn is_unimplemented(&self) -> bool {
173 matches!(*self.reason, RenderErrorReason::Unimplemented)
174 }
175
176 pub fn reason(&self) -> &RenderErrorReason {
178 self.reason.as_ref()
179 }
180}
181
182impl StdError for RenderError {
183 fn source(&self) -> Option<&(dyn StdError + 'static)> {
184 Some(self.reason())
185 }
186}
187
188#[derive(Debug, Error)]
190pub enum TemplateErrorReason {
191 #[error("helper {0:?} was opened, but {1:?} is closing")]
192 MismatchingClosedHelper(String, String),
193 #[error("decorator {0:?} was opened, but {1:?} is closing")]
194 MismatchingClosedDecorator(String, String),
195 #[error("invalid handlebars syntax.")]
196 InvalidSyntax,
197 #[error("invalid parameter {0:?}")]
198 InvalidParam(String),
199 #[error("nested subexpression is not supported")]
200 NestedSubexpression,
201 #[error("Template \"{1}\": {0}")]
202 IoError(IOError, String),
203 #[cfg(feature = "dir_source")]
204 #[error("Walk dir error: {err}")]
205 WalkdirError {
206 #[from]
207 err: WalkdirError,
208 },
209}
210
211#[derive(Debug, Error)]
213pub struct TemplateError {
214 reason: Box<TemplateErrorReason>,
215 template_name: Option<String>,
216 line_no: Option<usize>,
217 column_no: Option<usize>,
218 segment: Option<String>,
219}
220
221impl TemplateError {
222 #[allow(deprecated)]
223 pub fn of(e: TemplateErrorReason) -> TemplateError {
224 TemplateError {
225 reason: Box::new(e),
226 template_name: None,
227 line_no: None,
228 column_no: None,
229 segment: None,
230 }
231 }
232
233 pub fn at(mut self, template_str: &str, line_no: usize, column_no: usize) -> TemplateError {
234 self.line_no = Some(line_no);
235 self.column_no = Some(column_no);
236 self.segment = Some(template_segment(template_str, line_no, column_no));
237 self
238 }
239
240 pub fn in_template(mut self, name: String) -> TemplateError {
241 self.template_name = Some(name);
242 self
243 }
244
245 pub fn reason(&self) -> &TemplateErrorReason {
247 &self.reason
248 }
249
250 pub fn pos(&self) -> Option<(usize, usize)> {
252 match (self.line_no, self.column_no) {
253 (Some(line_no), Some(column_no)) => Some((line_no, column_no)),
254 _ => None,
255 }
256 }
257
258 pub fn name(&self) -> Option<&String> {
261 self.template_name.as_ref()
262 }
263}
264
265impl From<(IOError, String)> for TemplateError {
266 fn from(err_info: (IOError, String)) -> TemplateError {
267 let (e, name) = err_info;
268 TemplateError::of(TemplateErrorReason::IoError(e, name))
269 }
270}
271
272#[cfg(feature = "dir_source")]
273impl From<WalkdirError> for TemplateError {
274 fn from(e: WalkdirError) -> TemplateError {
275 TemplateError::of(TemplateErrorReason::from(e))
276 }
277}
278
279fn template_segment(template_str: &str, line: usize, col: usize) -> String {
280 let range = 3;
281 let line_start = if line >= range { line - range } else { 0 };
282 let line_end = line + range;
283
284 let mut buf = String::new();
285 for (line_count, line_content) in template_str.lines().enumerate() {
286 if line_count >= line_start && line_count <= line_end {
287 let _ = writeln!(&mut buf, "{line_count:4} | {line_content}");
288 if line_count == line - 1 {
289 buf.push_str(" |");
290 for c in 0..line_content.len() {
291 if c != col {
292 buf.push('-');
293 } else {
294 buf.push('^');
295 }
296 }
297 buf.push('\n');
298 }
299 }
300 }
301
302 buf
303}
304
305impl fmt::Display for TemplateError {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
307 match (self.line_no, self.column_no, &self.segment) {
308 (Some(line), Some(col), Some(seg)) => writeln!(
309 f,
310 "Template error: {}\n --> Template error in \"{}\":{}:{}\n |\n{} |\n = reason: {}",
311 self.reason(),
312 self.template_name
313 .as_ref()
314 .unwrap_or(&"Unnamed template".to_owned()),
315 line,
316 col,
317 seg,
318 self.reason()
319 ),
320 _ => write!(f, "{}", self.reason()),
321 }
322 }
323}
324
325#[cfg(feature = "script_helper")]
326#[derive(Debug, Error)]
327pub enum ScriptError {
328 #[error(transparent)]
329 IoError(#[from] IOError),
330
331 #[error(transparent)]
332 ParseError(#[from] ParseError),
333}
334
335#[cfg(test)]
336mod test {
337 use super::*;
338
339 #[test]
340 fn test_source_error() {
341 let reason = RenderErrorReason::TemplateNotFound("unnamed".to_owned());
342 let render_error = RenderError::from(reason);
343
344 let reason2 = render_error.source().unwrap();
345 assert!(matches!(
346 reason2.downcast_ref::<RenderErrorReason>().unwrap(),
347 RenderErrorReason::TemplateNotFound(_)
348 ));
349 }
350}