Skip to main content

bnto_core/metadata/
install_hints.rs

1// Per-platform install hints for external dependencies.
2//
3// Package managers and package IDs differ by OS (`brew install yt-dlp` vs
4// `winget install yt-dlp.yt-dlp`), so a single hint string can't serve
5// every platform. Unset platforms fall back to `Dependency::install_hint`,
6// then to `GENERIC_INSTALL_HINT`.
7
8use serde::{Deserialize, Serialize};
9#[cfg(feature = "ts")]
10use ts_rs::TS;
11
12/// Operating-system family an install hint targets.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HostPlatform {
15    MacOs,
16    Linux,
17    Windows,
18}
19
20impl HostPlatform {
21    /// The platform this binary was compiled for. `None` on targets with
22    /// no host OS (WASM) or OSes without hints (BSDs), which use the fallback.
23    pub const fn current() -> Option<Self> {
24        if cfg!(target_os = "macos") {
25            Some(Self::MacOs)
26        } else if cfg!(target_os = "linux") {
27            Some(Self::Linux)
28        } else if cfg!(windows) {
29            Some(Self::Windows)
30        } else {
31            None
32        }
33    }
34}
35
36/// Install commands keyed by platform. Empty entries mean "no specific
37/// hint" and defer to the dependency's generic `installHint`.
38#[cfg_attr(feature = "ts", derive(TS))]
39#[cfg_attr(
40    feature = "ts",
41    ts(
42        export,
43        export_to = "../../../../packages/@bnto/nodes/src/generated/definitionTypes/"
44    )
45)]
46#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
47pub struct InstallHints {
48    /// Install command for macOS (e.g. `brew install yt-dlp`).
49    #[serde(default, skip_serializing_if = "String::is_empty")]
50    pub macos: String,
51    /// Install command for Linux (e.g. `sudo apt install yt-dlp`).
52    #[serde(default, skip_serializing_if = "String::is_empty")]
53    pub linux: String,
54    /// Install command for Windows (e.g. `winget install yt-dlp.yt-dlp`).
55    #[serde(default, skip_serializing_if = "String::is_empty")]
56    pub windows: String,
57}
58
59impl InstallHints {
60    /// The hint for `platform`, or `None` if that platform has none set.
61    pub fn for_platform(&self, platform: HostPlatform) -> Option<&str> {
62        let hint = match platform {
63            HostPlatform::MacOs => &self.macos,
64            HostPlatform::Linux => &self.linux,
65            HostPlatform::Windows => &self.windows,
66        };
67        (!hint.is_empty()).then_some(hint.as_str())
68    }
69
70    /// True when no platform has a hint — lets serde omit the field.
71    pub fn is_empty(&self) -> bool {
72        self.macos.is_empty() && self.linux.is_empty() && self.windows.is_empty()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn hints() -> InstallHints {
81        InstallHints {
82            macos: "brew install yt-dlp".to_string(),
83            linux: String::new(),
84            windows: "winget install yt-dlp.yt-dlp".to_string(),
85        }
86    }
87
88    #[test]
89    fn for_platform_returns_matching_hint() {
90        let h = hints();
91        assert_eq!(
92            h.for_platform(HostPlatform::MacOs),
93            Some("brew install yt-dlp")
94        );
95        assert_eq!(
96            h.for_platform(HostPlatform::Windows),
97            Some("winget install yt-dlp.yt-dlp")
98        );
99    }
100
101    #[test]
102    fn for_platform_returns_none_when_unset() {
103        assert_eq!(hints().for_platform(HostPlatform::Linux), None);
104    }
105
106    #[test]
107    fn is_empty_only_when_no_platform_set() {
108        assert!(InstallHints::default().is_empty());
109        assert!(!hints().is_empty());
110    }
111
112    #[test]
113    fn deserializes_partial_object() {
114        let h: InstallHints = serde_json::from_str(r#"{ "windows": "winget install x" }"#).unwrap();
115        assert_eq!(h.windows, "winget install x");
116        assert!(h.macos.is_empty() && h.linux.is_empty());
117    }
118
119    #[test]
120    fn serializes_without_empty_platforms() {
121        let json = serde_json::to_string(&hints()).unwrap();
122        assert!(!json.contains("linux"), "empty platforms omitted: {json}");
123    }
124
125    #[test]
126    fn current_matches_compile_target() {
127        let expected = if cfg!(target_os = "macos") {
128            Some(HostPlatform::MacOs)
129        } else if cfg!(target_os = "linux") {
130            Some(HostPlatform::Linux)
131        } else if cfg!(windows) {
132            Some(HostPlatform::Windows)
133        } else {
134            None
135        };
136        assert_eq!(HostPlatform::current(), expected);
137    }
138}