Skip to main content

yosh_plugin_manager/
resolve.rs

1//! Asset filename resolution for the plugin manager.
2//!
3//! With the v0.2.0 component model migration, plugin artefacts are
4//! platform-independent `.wasm` files — no per-OS / per-arch suffix.
5//! The default template is `{name}.wasm` and the legacy `{os}` /
6//! `{arch}` / `{ext}` tokens are rejected with a migration message
7//! (the plugin author must republish a single `.wasm` asset).
8//!
9//! See `docs/superpowers/specs/2026-04-27-wasm-plugin-runtime-design.md`
10//! §7 for the rationale.
11
12/// Default asset filename template. v0.2.0+: a single platform-independent
13/// `.wasm` file per plugin.
14pub const DEFAULT_TEMPLATE: &str = "{name}.wasm";
15
16/// Convert plugin name to a form suitable for library names: hyphens
17/// become underscores. Kept for backwards-compatibility with existing
18/// templates; new `.wasm` artefacts typically use the plugin name as-is.
19pub fn normalize_name(name: &str) -> String {
20    name.replace('-', "_")
21}
22
23/// Tokens that were valid before v0.2.0 but are now rejected. Kept as a
24/// constant so the validator and the error message stay in sync.
25const FORBIDDEN_TOKENS: &[&str] = &["{os}", "{arch}", "{ext}"];
26
27/// Reject templates that reference platform-specific tokens. Plugins are
28/// distributed as single platform-independent `.wasm` files in v0.2.0+.
29pub fn check_asset_template(template: &str) -> Result<(), String> {
30    for forbidden in FORBIDDEN_TOKENS {
31        if template.contains(forbidden) {
32            return Err(format!(
33                "asset template token '{}' is no longer supported in v0.2.0; \
34                 plugins are distributed as single platform-independent .wasm files. \
35                 Update the plugin's release to ship `<name>.wasm` and remove \
36                 the `asset = \"...\"` line (or set it to `\"{{name}}.wasm\"`).",
37                forbidden
38            ));
39        }
40    }
41    Ok(())
42}
43
44/// Resolve an asset template by replacing `{name}`. Other historical
45/// tokens (`{os}`, `{arch}`, `{ext}`) are no longer supported and the
46/// caller should have rejected them via `check_asset_template`; for
47/// safety we still treat them as plain text here.
48pub fn resolve_template(template: &str, plugin_name: &str) -> String {
49    template.replace("{name}", &normalize_name(plugin_name))
50}
51
52/// Get the resolved asset filename for a plugin, using custom or default
53/// template. The custom template, if provided, must already have passed
54/// `check_asset_template`.
55pub fn asset_filename(plugin_name: &str, custom_template: Option<&str>) -> String {
56    let template = custom_template.unwrap_or(DEFAULT_TEMPLATE);
57    resolve_template(template, plugin_name)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn normalize_name_replaces_hyphens() {
66        assert_eq!(normalize_name("git-status"), "git_status");
67    }
68
69    #[test]
70    fn normalize_name_no_hyphens() {
71        assert_eq!(normalize_name("simple"), "simple");
72    }
73
74    #[test]
75    fn resolve_default_template() {
76        let result = resolve_template(DEFAULT_TEMPLATE, "git-status");
77        assert_eq!(result, "git_status.wasm");
78    }
79
80    #[test]
81    fn resolve_custom_template() {
82        let result = resolve_template("yosh_{name}.wasm", "auto-env");
83        assert_eq!(result, "yosh_auto_env.wasm");
84    }
85
86    #[test]
87    fn asset_filename_uses_default() {
88        let result = asset_filename("my-plugin", None);
89        assert_eq!(result, "my_plugin.wasm");
90    }
91
92    #[test]
93    fn asset_filename_uses_custom() {
94        let result = asset_filename("my-plugin", Some("custom_{name}.wasm"));
95        assert_eq!(result, "custom_my_plugin.wasm");
96    }
97
98    #[test]
99    fn check_template_accepts_default() {
100        assert!(check_asset_template(DEFAULT_TEMPLATE).is_ok());
101    }
102
103    #[test]
104    fn check_template_accepts_custom_wasm() {
105        assert!(check_asset_template("plugin-{name}.wasm").is_ok());
106    }
107
108    #[test]
109    fn check_template_rejects_os_token() {
110        let err = check_asset_template("lib{name}-{os}-{arch}.{ext}").unwrap_err();
111        assert!(err.contains("{os}"));
112        assert!(err.contains("v0.2.0"));
113    }
114
115    #[test]
116    fn check_template_rejects_arch_token() {
117        let err = check_asset_template("plugin-{arch}.wasm").unwrap_err();
118        assert!(err.contains("{arch}"));
119    }
120
121    #[test]
122    fn check_template_rejects_ext_token() {
123        let err = check_asset_template("plugin.{ext}").unwrap_err();
124        assert!(err.contains("{ext}"));
125    }
126}