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