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

sunng87 / handlebars-rust / 20657583393

02 Jan 2026 12:10PM UTC coverage: 83.449% (-0.3%) from 83.707%
20657583393

Pull #733

github

web-flow
Merge 2226d6e72 into 718db6bb1
Pull Request #733: fix: block scoped inline

26 of 27 new or added lines in 4 files covered. (96.3%)

10 existing lines in 2 files now uncovered.

1679 of 2012 relevant lines covered (83.45%)

7.34 hits per line

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

87.02
/src/context.rs
1
use std::collections::{HashMap, VecDeque};
2

3
use serde::Serialize;
4
use serde_json::value::{to_value, Value as Json};
5
use serde_json::Map;
6

7
use crate::block::{BlockContext, BlockParamHolder};
8
use crate::error::{RenderError, RenderErrorReason};
9
use crate::grammar::Rule;
10
use crate::json::path::{merge_json_path, PathSeg};
11
use crate::json::value::ScopedJson;
12
use crate::util::extend;
13

14
pub type Object = HashMap<String, Json>;
15

16
/// The context wrap data you render on your templates.
17
///
18
#[derive(Debug, Clone)]
19
pub struct Context {
20
    data: Json,
21
}
22

23
#[derive(Debug)]
24
enum ResolvedPath<'a> {
25
    // FIXME: change to borrowed when possible
26
    // full path
27
    AbsolutePath(Vec<String>),
28
    // relative path and path root
29
    RelativePath(Vec<String>),
30
    // relative path against block param value
31
    BlockParamValue(Vec<String>, &'a Json),
32
    // relative path against derived value,
33
    LocalValue(Vec<String>, &'a Json),
34
}
35

36
fn parse_json_visitor<'a>(
15✔
37
    relative_path: &[PathSeg],
38
    block_contexts: &'a VecDeque<BlockContext<'_>>,
39
    always_for_absolute_path: bool,
40
) -> ResolvedPath<'a> {
41
    let mut path_context_depth: usize = 0;
15✔
42
    let mut with_block_param = None;
15✔
43
    let mut from_root = false;
15✔
44

45
    // peek relative_path for block param, @root and  "../../"
46
    for path_seg in relative_path {
16✔
47
        match path_seg {
14✔
48
            PathSeg::Named(the_path) => {
13✔
49
                if let Some((holder, base_path)) = get_in_block_params(block_contexts, the_path) {
18✔
50
                    with_block_param = Some((holder, base_path));
3✔
51
                }
52
                break;
×
53
            }
54
            PathSeg::Ruled(the_rule) => match the_rule {
5✔
55
                Rule::path_root => {
×
56
                    from_root = true;
4✔
57
                    break;
4✔
58
                }
59
                Rule::path_up => path_context_depth += 1,
4✔
60
                _ => break,
×
61
            },
62
        }
63
    }
64

65
    let mut path_stack = Vec::with_capacity(relative_path.len() + 5);
28✔
66
    match with_block_param {
18✔
67
        Some((BlockParamHolder::Value(ref value), _)) => {
3✔
68
            merge_json_path(&mut path_stack, &relative_path[(path_context_depth + 1)..]);
6✔
69
            ResolvedPath::BlockParamValue(path_stack, value)
3✔
70
        }
71
        Some((BlockParamHolder::Path(ref paths), base_path)) => {
2✔
72
            extend(&mut path_stack, base_path);
3✔
73
            if !paths.is_empty() {
3✔
74
                extend(&mut path_stack, paths);
2✔
75
            }
76
            merge_json_path(&mut path_stack, &relative_path[(path_context_depth + 1)..]);
5✔
77

78
            ResolvedPath::AbsolutePath(path_stack)
2✔
79
        }
80
        None => {
×
81
            if path_context_depth > 0 {
14✔
82
                let blk = block_contexts
2✔
83
                    .get(path_context_depth)
2✔
84
                    .or_else(|| block_contexts.front());
2✔
85

86
                if let Some(base_value) = blk.and_then(|blk| blk.base_value()) {
8✔
UNCOV
87
                    merge_json_path(&mut path_stack, relative_path);
×
UNCOV
88
                    ResolvedPath::LocalValue(path_stack, base_value)
×
89
                } else {
90
                    if let Some(base_path) = blk.map(BlockContext::base_path) {
4✔
91
                        extend(&mut path_stack, base_path);
4✔
92
                    }
93
                    merge_json_path(&mut path_stack, relative_path);
2✔
94
                    ResolvedPath::AbsolutePath(path_stack)
2✔
95
                }
96
            } else if from_root {
19✔
97
                merge_json_path(&mut path_stack, relative_path);
4✔
98
                ResolvedPath::AbsolutePath(path_stack)
4✔
99
            } else if always_for_absolute_path {
14✔
100
                if let Some(base_value) = block_contexts.front().and_then(|blk| blk.base_value()) {
81✔
101
                    merge_json_path(&mut path_stack, relative_path);
6✔
102
                    ResolvedPath::LocalValue(path_stack, base_value)
6✔
103
                } else {
104
                    if let Some(base_path) = block_contexts.front().map(BlockContext::base_path) {
32✔
105
                        extend(&mut path_stack, base_path);
29✔
106
                    }
107
                    merge_json_path(&mut path_stack, relative_path);
16✔
108
                    ResolvedPath::AbsolutePath(path_stack)
15✔
109
                }
110
            } else {
111
                merge_json_path(&mut path_stack, relative_path);
×
112
                ResolvedPath::RelativePath(path_stack)
×
113
            }
114
        }
115
    }
116
}
117

118
fn get_data<'a>(d: Option<&'a Json>, p: &str) -> Result<Option<&'a Json>, RenderError> {
14✔
119
    let result = match d {
13✔
120
        Some(Json::Array(l)) => p
14✔
121
            .parse::<usize>()
122
            .map(|idx_u| l.get(idx_u))
19✔
123
            .map_err(|_| RenderErrorReason::InvalidJsonIndex(p.to_owned()))?,
17✔
124
        Some(Json::Object(m)) => m.get(p),
13✔
125
        Some(_) => None,
2✔
126
        None => None,
×
127
    };
128
    Ok(result)
13✔
129
}
130

131
fn get_in_block_params<'a>(
13✔
132
    block_contexts: &'a VecDeque<BlockContext<'_>>,
133
    p: &str,
134
) -> Option<(&'a BlockParamHolder, &'a Vec<String>)> {
135
    for bc in block_contexts {
28✔
136
        let v = bc.get_block_param(p);
13✔
137
        if v.is_some() {
12✔
138
            return v.map(|v| (v, bc.base_path()));
9✔
139
        }
140
    }
141

142
    None
12✔
143
}
144

145
pub(crate) fn merge_json(base: &Json, addition: &HashMap<&str, &Json>) -> Json {
7✔
146
    // if no addition json provided, just return the original one
147
    if addition.is_empty() {
6✔
148
        return base.clone();
6✔
149
    }
150

151
    let mut base_map = match base {
2✔
152
        Json::Object(ref m) => m.clone(),
1✔
153
        Json::Array(ref a) => {
1✔
154
            let mut base_map = Map::new();
1✔
155
            for (idx, value) in a.iter().enumerate() {
2✔
156
                base_map.insert(idx.to_string(), value.clone());
2✔
157
            }
158
            base_map
1✔
159
        }
160
        Json::String(ref s) => {
1✔
161
            let mut base_map = Map::new();
1✔
162
            for (idx, value) in s.chars().enumerate() {
2✔
163
                base_map.insert(idx.to_string(), Json::String(value.to_string()));
2✔
164
            }
165
            base_map
1✔
166
        }
167
        _ => Map::new(),
2✔
168
    };
169

170
    for (k, v) in addition {
4✔
171
        base_map.insert((*k).to_string(), (*v).clone());
4✔
172
    }
173

174
    Json::Object(base_map)
2✔
175
}
176

177
impl Context {
178
    /// Create a context with null data
179
    pub fn null() -> Context {
1✔
180
        Context { data: Json::Null }
181
    }
182

183
    /// Create a context with given data
184
    pub fn wraps<T: Serialize>(e: T) -> Result<Context, RenderError> {
41✔
185
        to_value(e)
44✔
186
            .map_err(|e| RenderErrorReason::SerdeError(e).into())
43✔
187
            .map(|d| Context { data: d })
125✔
188
    }
189

190
    /// Navigate the context with relative path and block scopes
191
    pub(crate) fn navigate<'rc>(
15✔
192
        &'rc self,
193
        relative_path: &[PathSeg],
194
        block_contexts: &VecDeque<BlockContext<'_>>,
195
        recursive_lookup: bool,
196
    ) -> Result<ScopedJson<'rc>, RenderError> {
197
        // always use absolute at the moment until we get base_value lifetime issue fixed
198
        let resolved_visitor = parse_json_visitor(relative_path, block_contexts, true);
15✔
199

200
        match resolved_visitor {
27✔
201
            ResolvedPath::AbsolutePath(mut paths) => {
19✔
202
                // Only attempt a recursive resolution if it looks like a simple
203
                // named parameter, rather than an explicitly pathed parameter like
204
                // "./foo" or "../foo".
205
                let allow_recursive =
38✔
206
                    recursive_lookup && matches!(relative_path, [PathSeg::Named(_)]);
×
207
                if allow_recursive {
18✔
208
                    // Paths is probably something like: ["children", "0", "name"].
209
                    // This block of code will first try to resolve that set of
210
                    // paths, but if no value is found, it will then successively
211
                    // mutate that list to remove the parent element (the penultimate
212
                    // one) and resolving each in turn:
213
                    // ["children", "name"]
214
                    // ["name"]
215
                    while !paths.is_empty() {
2✔
216
                        let mut ptr = Some(self.data());
2✔
217
                        for p in &paths {
2✔
218
                            ptr = match get_data(ptr, p) {
3✔
219
                                Ok(p) => p,
1✔
220
                                Err(err) => {
1✔
221
                                    use crate::RenderErrorReason::InvalidJsonIndex;
×
222
                                    if matches!(err.reason(), InvalidJsonIndex(_)) {
2✔
223
                                        ptr = None;
1✔
224
                                        break;
×
225
                                    }
226
                                    return Err(err);
×
227
                                }
228
                            };
229
                        }
230

231
                        match ptr {
1✔
232
                            Some(v) => {
1✔
233
                                return Ok(ScopedJson::Context(v, paths));
1✔
234
                            }
235
                            None => {
×
236
                                let paths_len = paths.len();
1✔
237
                                if paths_len == 1 {
1✔
238
                                    return Ok(ScopedJson::Missing);
×
239
                                }
240

241
                                paths.remove(paths_len - 2);
1✔
242
                            }
243
                        }
244
                    }
245
                    Ok(ScopedJson::Missing)
×
246
                } else {
247
                    let mut ptr = Some(self.data());
37✔
248
                    for p in &paths {
30✔
249
                        ptr = get_data(ptr, p)?;
27✔
250
                    }
251
                    Ok(ptr.map_or_else(|| ScopedJson::Missing, |v| ScopedJson::Context(v, paths)))
45✔
252
                }
253
            }
254
            ResolvedPath::RelativePath(_paths) => {
255
                // relative path is disabled for now
256
                unreachable!()
257
                // let mut ptr = block_contexts.front().and_then(|blk| blk.base_value());
258
                // for p in paths.iter() {
259
                //     ptr = get_data(ptr, p)?;
260
                // }
261

262
                // Ok(ptr
263
                //     .map(|v| ScopedJson::Context(v, paths))
264
                //     .unwrap_or_else(|| ScopedJson::Missing))
265
            }
266
            ResolvedPath::BlockParamValue(paths, value)
3✔
267
            | ResolvedPath::LocalValue(paths, value) => {
×
268
                let mut ptr = Some(value);
7✔
269
                for p in &paths {
18✔
270
                    ptr = get_data(ptr, p)?;
8✔
271
                }
272
                Ok(ptr.map_or_else(|| ScopedJson::Missing, |v| ScopedJson::Derived(v.clone())))
19✔
273
            }
274
        }
275
    }
276

277
    /// Return the Json data wrapped in context
278
    pub fn data(&self) -> &Json {
16✔
279
        &self.data
280
    }
281

282
    /// Return the mutable reference to Json data wrapped in context
283
    pub fn data_mut(&mut self) -> &mut Json {
1✔
284
        &mut self.data
285
    }
286
}
287

288
impl From<Json> for Context {
289
    fn from(data: Json) -> Context {
1✔
290
        Context { data }
291
    }
292
}
293

294
#[cfg(test)]
295
mod test {
296
    use crate::json::value;
297
    use crate::{BlockParams, Path};
298

299
    use super::*;
300

301
    fn navigate_from_root<'rc>(
302
        ctx: &'rc Context,
303
        path: &str,
304
    ) -> Result<ScopedJson<'rc>, RenderError> {
305
        let relative_path = Path::parse(path).unwrap();
306
        ctx.navigate(relative_path.segs().unwrap(), &VecDeque::new(), false)
307
    }
308

309
    #[derive(Serialize)]
310
    struct Address {
311
        city: String,
312
        country: String,
313
    }
314

315
    #[derive(Serialize)]
316
    struct Person {
317
        name: String,
318
        age: i16,
319
        addr: Address,
320
        titles: Vec<String>,
321
    }
322

323
    #[test]
324
    fn test_render() {
325
        let v = "hello";
326
        let ctx = Context::wraps(v.to_string()).unwrap();
327
        assert_eq!(
328
            navigate_from_root(&ctx, "this").unwrap().render(),
329
            v.to_string()
330
        );
331
    }
332

333
    #[test]
334
    fn test_navigation() {
335
        let addr = Address {
336
            city: "Beijing".to_string(),
337
            country: "China".to_string(),
338
        };
339

340
        let person = Person {
341
            name: "Ning Sun".to_string(),
342
            age: 27,
343
            addr,
344
            titles: vec!["programmer".to_string(), "cartographer".to_string()],
345
        };
346

347
        let ctx = Context::wraps(person).unwrap();
348
        assert_eq!(
349
            navigate_from_root(&ctx, "./addr/country").unwrap().render(),
350
            "China".to_string()
351
        );
352
        assert_eq!(
353
            navigate_from_root(&ctx, "addr.[country]").unwrap().render(),
354
            "China".to_string()
355
        );
356

357
        let v = true;
358
        let ctx2 = Context::wraps(v).unwrap();
359
        assert_eq!(
360
            navigate_from_root(&ctx2, "this").unwrap().render(),
361
            "true".to_string()
362
        );
363

364
        assert_eq!(
365
            navigate_from_root(&ctx, "titles.[0]").unwrap().render(),
366
            "programmer".to_string()
367
        );
368

369
        assert_eq!(
370
            navigate_from_root(&ctx, "age").unwrap().render(),
371
            "27".to_string()
372
        );
373
    }
374

375
    #[test]
376
    fn test_this() {
377
        let mut map_with_this = Map::new();
378
        map_with_this.insert("this".to_string(), value::to_json("hello"));
379
        map_with_this.insert("age".to_string(), value::to_json(5usize));
380
        let ctx1 = Context::wraps(&map_with_this).unwrap();
381

382
        let mut map_without_this = Map::new();
383
        map_without_this.insert("age".to_string(), value::to_json(4usize));
384
        let ctx2 = Context::wraps(&map_without_this).unwrap();
385

386
        assert_eq!(
387
            navigate_from_root(&ctx1, "this").unwrap().render(),
388
            "[object]".to_owned()
389
        );
390
        assert_eq!(
391
            navigate_from_root(&ctx2, "age").unwrap().render(),
392
            "4".to_owned()
393
        );
394
    }
395

396
    #[test]
397
    fn test_merge_json() {
398
        let map = json!({ "age": 4 });
399
        let s = "hello".to_owned();
400
        let arr = json!(["a", "b"]);
401
        let mut hash = HashMap::new();
402
        let v = value::to_json("h1");
403
        hash.insert("tag", &v);
404

405
        let ctx_a1 = Context::wraps(merge_json(&map, &hash)).unwrap();
406
        assert_eq!(
407
            navigate_from_root(&ctx_a1, "age").unwrap().render(),
408
            "4".to_owned()
409
        );
410
        assert_eq!(
411
            navigate_from_root(&ctx_a1, "tag").unwrap().render(),
412
            "h1".to_owned()
413
        );
414

415
        let ctx_a2 = Context::wraps(merge_json(&value::to_json(&s), &hash)).unwrap();
416
        assert_eq!(
417
            navigate_from_root(&ctx_a2, "this").unwrap().render(),
418
            "[object]".to_owned()
419
        );
420
        assert_eq!(
421
            navigate_from_root(&ctx_a2, "tag").unwrap().render(),
422
            "h1".to_owned()
423
        );
424
        assert_eq!(
425
            navigate_from_root(&ctx_a2, "0").unwrap().render(),
426
            "h".to_owned()
427
        );
428
        assert_eq!(
429
            navigate_from_root(&ctx_a2, "1").unwrap().render(),
430
            "e".to_owned()
431
        );
432

433
        let ctx_a3 = Context::wraps(merge_json(&value::to_json(&arr), &hash)).unwrap();
434
        assert_eq!(
435
            navigate_from_root(&ctx_a3, "tag").unwrap().render(),
436
            "h1".to_owned()
437
        );
438
        assert_eq!(
439
            navigate_from_root(&ctx_a3, "0").unwrap().render(),
440
            "a".to_owned()
441
        );
442
        assert_eq!(
443
            navigate_from_root(&ctx_a3, "1").unwrap().render(),
444
            "b".to_owned()
445
        );
446

447
        let ctx_a4 = Context::wraps(merge_json(&value::to_json(&s), &HashMap::new())).unwrap();
448
        assert_eq!(
449
            navigate_from_root(&ctx_a4, "this").unwrap().render(),
450
            "hello".to_owned()
451
        );
452
    }
453

454
    #[test]
455
    fn test_key_name_with_this() {
456
        let m = json!({
457
            "this_name": "the_value"
458
        });
459
        let ctx = Context::wraps(m).unwrap();
460
        assert_eq!(
461
            navigate_from_root(&ctx, "this_name").unwrap().render(),
462
            "the_value".to_string()
463
        );
464
    }
465

466
    use serde::ser::Error as SerdeError;
467
    use serde::{Serialize, Serializer};
468

469
    struct UnserializableType {}
470

471
    impl Serialize for UnserializableType {
472
        fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
473
        where
474
            S: Serializer,
475
        {
476
            Err(SerdeError::custom("test"))
477
        }
478
    }
479

480
    #[test]
481
    fn test_serialize_error() {
482
        let d = UnserializableType {};
483
        assert!(Context::wraps(d).is_err());
484
    }
485

486
    #[test]
487
    fn test_root() {
488
        let m = json!({
489
            "a" : {
490
                "b" : {
491
                    "c" : {
492
                        "d" : 1
493
                    }
494
                }
495
            },
496
            "b": 2
497
        });
498
        let ctx = Context::wraps(m).unwrap();
499
        let mut block = BlockContext::new();
500
        *block.base_path_mut() = ["a".to_owned(), "b".to_owned()].to_vec();
501

502
        let mut blocks = VecDeque::new();
503
        blocks.push_front(block);
504

505
        assert_eq!(
506
            ctx.navigate(
507
                Path::parse("@root/b").unwrap().segs().unwrap(),
508
                &blocks,
509
                false
510
            )
511
            .unwrap()
512
            .render(),
513
            "2".to_string()
514
        );
515
    }
516

517
    #[test]
518
    fn test_block_params() {
519
        let m = json!([{
520
            "a": [1, 2]
521
        }, {
522
            "b": [2, 3]
523
        }]);
524

525
        let ctx = Context::wraps(m).unwrap();
526
        let mut block_params = BlockParams::new();
527
        block_params
528
            .add_path("z", ["0".to_owned(), "a".to_owned()].to_vec())
529
            .unwrap();
530
        block_params.add_value("t", json!("good")).unwrap();
531

532
        let mut block = BlockContext::new();
533
        block.set_block_params(block_params);
534

535
        let mut blocks = VecDeque::new();
536
        blocks.push_front(block);
537

538
        assert_eq!(
539
            ctx.navigate(
540
                Path::parse("z.[1]").unwrap().segs().unwrap(),
541
                &blocks,
542
                false
543
            )
544
            .unwrap()
545
            .render(),
546
            "2".to_string()
547
        );
548
        assert_eq!(
549
            ctx.navigate(Path::parse("t").unwrap().segs().unwrap(), &blocks, false)
550
                .unwrap()
551
                .render(),
552
            "good".to_string()
553
        );
554
    }
555
}
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