Skip to main content

yui/
mount.rs

1//! Resolve `[[mount.entry]]` definitions: render `dst` via Tera, evaluate
2//! `when`, drop disabled entries.
3
4use camino::Utf8PathBuf;
5use tera::Context;
6
7use crate::Result;
8use crate::config::{MountEntry, MountStrategy};
9use crate::paths;
10use crate::template::{self, Engine};
11
12/// A mount entry after Tera rendering of `dst` and `when`-filtering.
13#[derive(Debug, Clone)]
14pub struct ResolvedMount {
15    pub src: Utf8PathBuf,
16    pub dst: Utf8PathBuf,
17    pub strategy: MountStrategy,
18}
19
20pub fn resolve(
21    entries: &[MountEntry],
22    default_strategy: MountStrategy,
23    engine: &mut Engine,
24    ctx: &Context,
25) -> Result<Vec<ResolvedMount>> {
26    let mut out = Vec::with_capacity(entries.len());
27    for e in entries {
28        if let Some(when) = &e.when {
29            // `template::eval_truthy` accepts both bare (`yui.os == 'linux'`)
30            // and pre-wrapped (`{{ … }}`) forms — same convention used by
31            // marker links and render rules. Without it, a bare expression
32            // would be silently filtered out (the literal expression string
33            // doesn't equal "true" / "1" so the row drops). The README and
34            // `init` skeleton recommend bare form, so this MUST agree.
35            if !template::eval_truthy(when, engine, ctx)? {
36                continue;
37            }
38        }
39        let dst_str = engine.render(&e.dst, ctx)?;
40        let dst = paths::expand_tilde(dst_str.trim());
41        out.push(ResolvedMount {
42            src: e.src.clone(),
43            dst,
44            strategy: e.strategy.unwrap_or(default_strategy),
45        });
46    }
47    Ok(out)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::template;
54    use crate::vars::YuiVars;
55
56    fn vars() -> YuiVars {
57        YuiVars {
58            os: "linux".into(),
59            arch: "x86_64".into(),
60            host: "test".into(),
61            user: "u".into(),
62            source: "/dotfiles".into(),
63        }
64    }
65
66    #[test]
67    fn renders_dst_and_filters_when_false() {
68        let entries = vec![
69            MountEntry {
70                src: "home".into(),
71                dst: "/{{ yui.os }}/u".into(),
72                when: None,
73                strategy: None,
74            },
75            MountEntry {
76                src: "appdata".into(),
77                dst: "/appdata".into(),
78                when: Some("{{ yui.os == 'windows' }}".into()),
79                strategy: None,
80            },
81        ];
82        let mut e = Engine::new();
83        let ctx = template::config_context(&vars());
84        let resolved = resolve(&entries, MountStrategy::Marker, &mut e, &ctx).unwrap();
85        assert_eq!(resolved.len(), 1);
86        assert_eq!(resolved[0].src, Utf8PathBuf::from("home"));
87        assert_eq!(resolved[0].dst, Utf8PathBuf::from("/linux/u"));
88        assert_eq!(resolved[0].strategy, MountStrategy::Marker);
89    }
90
91    #[test]
92    fn when_true_keeps_entry() {
93        let entries = vec![MountEntry {
94            src: "home".into(),
95            dst: "/h".into(),
96            when: Some("{{ yui.os == 'linux' }}".into()),
97            strategy: None,
98        }];
99        let mut e = Engine::new();
100        let ctx = template::config_context(&vars());
101        let resolved = resolve(&entries, MountStrategy::Marker, &mut e, &ctx).unwrap();
102        assert_eq!(resolved.len(), 1);
103    }
104
105    #[test]
106    fn bare_when_form_works() {
107        // Regression: README and `init` skeleton recommend bare-form `when`,
108        // so this MUST resolve via `template::eval_truthy`. Earlier this
109        // function used a direct `engine.render(when)` which only handled
110        // the wrapped form — caught in PR #12 review (gemini-code-assist).
111        let entries = vec![MountEntry {
112            src: "home".into(),
113            dst: "/h".into(),
114            when: Some("yui.os == 'linux'".into()),
115            strategy: None,
116        }];
117        let mut e = Engine::new();
118        let ctx = template::config_context(&vars());
119        let resolved = resolve(&entries, MountStrategy::Marker, &mut e, &ctx).unwrap();
120        assert_eq!(resolved.len(), 1);
121    }
122
123    #[test]
124    fn bare_when_form_filters_when_false() {
125        let entries = vec![MountEntry {
126            src: "home".into(),
127            dst: "/h".into(),
128            when: Some("yui.os == 'no-such-os'".into()),
129            strategy: None,
130        }];
131        let mut e = Engine::new();
132        let ctx = template::config_context(&vars());
133        let resolved = resolve(&entries, MountStrategy::Marker, &mut e, &ctx).unwrap();
134        assert!(resolved.is_empty());
135    }
136
137    #[test]
138    fn per_entry_strategy_overrides_default() {
139        let entries = vec![MountEntry {
140            src: "home".into(),
141            dst: "/h".into(),
142            when: None,
143            strategy: Some(MountStrategy::PerFile),
144        }];
145        let mut e = Engine::new();
146        let ctx = template::config_context(&vars());
147        let resolved = resolve(&entries, MountStrategy::Marker, &mut e, &ctx).unwrap();
148        assert_eq!(resolved[0].strategy, MountStrategy::PerFile);
149    }
150}