• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sunng87 / handlebars-rust / 20639969489

01 Jan 2026 02:08PM UTC coverage: 83.707% (+0.03%) from 83.673%
20639969489

Pull #732

github

web-flow
Merge abe1b6ce9 into c349c3955
Pull Request #732: fix: correct partial-block render

39 of 43 new or added lines in 2 files covered. (90.7%)

1 existing line in 1 file now uncovered.

1680 of 2007 relevant lines covered (83.71%)

6.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

44.21
/src/error.rs
1
use std::borrow::ToOwned;
2
use std::error::Error as StdError;
3
use std::fmt::{self, Write};
4
use std::io::Error as IOError;
5
use std::string::FromUtf8Error;
6

7
use serde_json::error::Error as SerdeError;
8
use thiserror::Error;
9

10
#[cfg(feature = "dir_source")]
11
use walkdir::Error as WalkdirError;
12

13
#[cfg(feature = "script_helper")]
14
use rhai::{EvalAltResult, ParseError};
15

16
/// Error when rendering data on template.
17
#[non_exhaustive]
18
#[derive(Debug)]
19
pub 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
    // backtrace: Backtrace,
26
}
27

28
impl fmt::Display for RenderError {
×
29
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
×
30
        let desc = self.reason.to_string();
2✔
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

46
impl From<IOError> for RenderError {
×
47
    fn from(e: IOError) -> RenderError {
×
48
        RenderErrorReason::IOError(e).into()
×
49
    }
50
}
51

52
impl From<FromUtf8Error> for RenderError {
×
53
    fn from(e: FromUtf8Error) -> Self {
×
54
        RenderErrorReason::Utf8Error(e).into()
×
55
    }
56
}
57

58
impl From<TemplateError> for RenderError {
59
    fn from(e: TemplateError) -> Self {
×
60
        RenderErrorReason::TemplateError(e).into()
×
61
    }
62
}
63

64
/// Template rendering error
65
#[non_exhaustive]
66
#[derive(Debug, Error)]
67
pub 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("Partial block cound not be found")]
81
    PartialBlockNotFound,
82
    #[error("Helper not found {0}")]
83
    HelperNotFound(String),
84
    #[error("Helper/Decorator {0} param at index {1} required but not found")]
85
    ParamNotFoundForIndex(&'static str, usize),
86
    #[error("Helper/Decorator {0} param with name {1} required but not found")]
87
    ParamNotFoundForName(&'static str, String),
88
    #[error("Helper/Decorator {0} param with name {1} type mismatch for {2}")]
89
    ParamTypeMismatchForName(&'static str, String, String),
90
    #[error("Helper/Decorator {0} hash with name {1} type mismatch for {2}")]
91
    HashTypeMismatchForName(&'static str, String, String),
92
    #[error("Decorator not found {0}")]
93
    DecoratorNotFound(String),
94
    #[error("Can not include current template in partial")]
95
    CannotIncludeSelf,
96
    #[error("Invalid logging level: {0}")]
97
    InvalidLoggingLevel(String),
98
    #[error("Invalid param type, {0} expected")]
99
    InvalidParamType(&'static str),
100
    #[error("Block content required")]
101
    BlockContentRequired,
102
    #[error("Invalid json path {0}")]
103
    InvalidJsonPath(String),
104
    #[error("Cannot access array/vector with string index, {0}")]
105
    InvalidJsonIndex(String),
106
    #[error("Failed to access JSON data: {0}")]
107
    SerdeError(
108
        #[from]
109
        #[source]
110
        SerdeError,
111
    ),
112
    #[error("IO Error: {0}")]
113
    IOError(
114
        #[from]
115
        #[source]
116
        IOError,
117
    ),
118
    #[error("FromUtf8Error: {0}")]
119
    Utf8Error(
120
        #[from]
121
        #[source]
122
        FromUtf8Error,
123
    ),
124
    #[error("Nested error: {0}")]
125
    NestedError(#[source] Box<dyn StdError + Send + Sync + 'static>),
126
    #[cfg(feature = "script_helper")]
127
    #[error("Cannot convert data to Rhai dynamic: {0}")]
128
    ScriptValueError(
129
        #[from]
130
        #[source]
131
        Box<EvalAltResult>,
132
    ),
133
    #[cfg(feature = "script_helper")]
134
    #[error("Failed to load rhai script: {0}")]
135
    ScriptLoadError(
136
        #[from]
137
        #[source]
138
        ScriptError,
139
    ),
140
    #[error("Unimplemented")]
141
    Unimplemented,
142
    #[error("{0}")]
143
    Other(String),
144
}
145

146
impl From<RenderErrorReason> for RenderError {
×
147
    fn from(e: RenderErrorReason) -> RenderError {
2✔
148
        RenderError {
149
            template_name: None,
150
            line_no: None,
151
            column_no: None,
152
            reason: Box::new(e),
2✔
153
            unimplemented: false,
154
        }
155
    }
156
}
157

158
impl From<RenderError> for RenderErrorReason {
×
159
    fn from(e: RenderError) -> Self {
×
160
        *e.reason
×
161
    }
162
}
163

164
impl RenderError {
×
165
    #[deprecated(since = "5.0.0", note = "Use RenderErrorReason instead")]
×
166
    pub fn new<T: AsRef<str>>(desc: T) -> RenderError {
×
167
        RenderErrorReason::Other(desc.as_ref().to_string()).into()
×
168
    }
169

170
    pub fn strict_error(path: Option<&String>) -> RenderError {
1✔
171
        RenderErrorReason::MissingVariable(path.map(ToOwned::to_owned)).into()
1✔
172
    }
173

174
    #[deprecated(since = "5.0.0", note = "Use RenderErrorReason::NestedError instead")]
×
175
    pub fn from_error<E>(_error_info: &str, cause: E) -> RenderError
×
176
    where
177
        E: StdError + Send + Sync + 'static,
178
    {
179
        RenderErrorReason::NestedError(Box::new(cause)).into()
×
180
    }
181

182
    #[inline]
×
183
    pub(crate) fn is_unimplemented(&self) -> bool {
2✔
184
        matches!(*self.reason, RenderErrorReason::Unimplemented)
2✔
185
    }
186

187
    /// Get `RenderErrorReason` for this error
188
    pub fn reason(&self) -> &RenderErrorReason {
2✔
189
        self.reason.as_ref()
2✔
190
    }
191
}
192

193
impl StdError for RenderError {
×
194
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
1✔
195
        Some(self.reason())
1✔
196
    }
197
}
198

199
/// Template parsing error
200
#[non_exhaustive]
201
#[derive(Debug, Error)]
202
pub enum TemplateErrorReason {
203
    #[error("helper {0:?} was opened, but {1:?} is closing")]
204
    MismatchingClosedHelper(String, String),
205
    #[error("decorator {0:?} was opened, but {1:?} is closing")]
206
    MismatchingClosedDecorator(String, String),
207
    #[error("invalid handlebars syntax: {0}")]
208
    InvalidSyntax(String),
209
    #[error("invalid parameter {0:?}")]
210
    InvalidParam(String),
211
    #[error("nested subexpression is not supported")]
212
    NestedSubexpression,
213
    #[error("Template \"{1}\": {0}")]
214
    IoError(IOError, String),
215
    #[cfg(feature = "dir_source")]
216
    #[error("Walk dir error: {err}")]
217
    WalkdirError {
218
        #[from]
219
        err: WalkdirError,
220
    },
221
}
222

223
/// Error on parsing template.
224
#[derive(Debug, Error)]
225
pub struct TemplateError {
226
    reason: Box<TemplateErrorReason>,
227
    template_name: Option<String>,
228
    line_no: Option<usize>,
229
    column_no: Option<usize>,
230
    segment: Option<String>,
231
}
232

233
impl TemplateError {
×
234
    #[allow(deprecated)]
×
235
    pub fn of(e: TemplateErrorReason) -> TemplateError {
1✔
236
        TemplateError {
237
            reason: Box::new(e),
1✔
238
            template_name: None,
239
            line_no: None,
240
            column_no: None,
241
            segment: None,
242
        }
243
    }
244

245
    pub fn at(mut self, template_str: &str, line_no: usize, column_no: usize) -> TemplateError {
1✔
246
        self.line_no = Some(line_no);
1✔
247
        self.column_no = Some(column_no);
1✔
248
        self.segment = Some(template_segment(template_str, line_no, column_no));
4✔
249
        self
3✔
250
    }
251

252
    pub fn in_template(mut self, name: String) -> TemplateError {
3✔
253
        self.template_name = Some(name);
6✔
254
        self
3✔
255
    }
256

257
    /// Get underlying reason for the error
258
    pub fn reason(&self) -> &TemplateErrorReason {
1✔
259
        &self.reason
3✔
260
    }
261

262
    /// Get the line number and column number of this error
263
    pub fn pos(&self) -> Option<(usize, usize)> {
1✔
264
        match (self.line_no, self.column_no) {
1✔
265
            (Some(line_no), Some(column_no)) => Some((line_no, column_no)),
1✔
266
            _ => None,
×
267
        }
268
    }
269

270
    /// Get template name of this error
271
    /// Returns `None` when the template has no associated name
272
    pub fn name(&self) -> Option<&String> {
×
273
        self.template_name.as_ref()
×
274
    }
275
}
276

277
impl From<(IOError, String)> for TemplateError {
278
    fn from(err_info: (IOError, String)) -> TemplateError {
×
279
        let (e, name) = err_info;
×
280
        TemplateError::of(TemplateErrorReason::IoError(e, name))
×
281
    }
282
}
283

284
#[cfg(feature = "dir_source")]
285
impl From<WalkdirError> for TemplateError {
286
    fn from(e: WalkdirError) -> TemplateError {
×
287
        TemplateError::of(TemplateErrorReason::from(e))
×
288
    }
289
}
290

291
fn template_segment(template_str: &str, line: usize, col: usize) -> String {
1✔
292
    let range = 3;
1✔
293
    let line_start = line.saturating_sub(range);
1✔
294
    let line_end = line + range;
1✔
295

296
    let mut buf = String::new();
1✔
297
    for (line_count, line_content) in template_str.lines().enumerate() {
4✔
298
        if line_count >= line_start && line_count <= line_end {
6✔
299
            let _ = writeln!(&mut buf, "{line_count:4} | {line_content}");
1✔
300
            if line_count == line - 1 {
3✔
301
                buf.push_str("     |");
3✔
302
                for c in 0..line_content.len() {
3✔
303
                    if c != col {
3✔
304
                        buf.push('-');
6✔
305
                    } else {
306
                        buf.push('^');
2✔
307
                    }
308
                }
309
                buf.push('\n');
2✔
310
            }
311
        }
312
    }
313

314
    buf
2✔
315
}
316

317
impl fmt::Display for TemplateError {
×
318
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
×
319
        match (self.line_no, self.column_no, &self.segment) {
×
320
            (Some(line), Some(col), Some(seg)) => writeln!(
×
321
                f,
322
                "Template error: {}\n    --> Template error in \"{}\":{}:{}\n     |\n{}     |\n     = reason: {}",
323
                self.reason(),
×
324
                self.template_name
×
325
                    .as_ref()
×
326
                    .unwrap_or(&"Unnamed template".to_owned()),
×
327
                line,
328
                col,
×
UNCOV
329
                seg,
×
330
                self.reason()
×
331
            ),
332
            _ => write!(f, "{}", self.reason()),
×
333
        }
334
    }
335
}
336

337
#[cfg(feature = "script_helper")]
338
#[non_exhaustive]
339
#[derive(Debug, Error)]
340
pub enum ScriptError {
341
    #[error(transparent)]
342
    IoError(#[from] IOError),
343

344
    #[error(transparent)]
345
    ParseError(#[from] ParseError),
346
}
347

348
#[cfg(test)]
349
mod test {
350
    use super::*;
351

352
    #[test]
353
    fn test_source_error() {
354
        let reason = RenderErrorReason::TemplateNotFound("unnamed".to_owned());
355
        let render_error = RenderError::from(reason);
356

357
        let reason2 = render_error.source().unwrap();
358
        assert!(matches!(
359
            reason2.downcast_ref::<RenderErrorReason>().unwrap(),
360
            RenderErrorReason::TemplateNotFound(_)
361
        ));
362
    }
363
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc