Skip to main content

bhtune_runtime/config/
resolvers.rs

1#![allow(rustdoc::broken_intra_doc_links)]
2
3use std::path::PathBuf;
4
5use super::model::{BhtuneConfig, DEFAULT_BIND_ADDR, DEFAULT_BRIDGE_HOST};
6use super::paths::default_db_path_from;
7
8/// Resolve the database path with `CLI flag > env var > config file > platform default`
9/// precedence. The env var is already folded into `cli_db` by clap's `env` attribute on
10/// `Cli::db`; the platform-default tier takes its own raw environment values (rather than
11/// reading `std::env` internally, like [`default_db_path_from`]) so this stays fully
12/// unit-testable without touching real process environment variables.
13#[allow(clippy::too_many_arguments)]
14pub fn resolve_db_path(
15    cli_db: Option<PathBuf>,
16    config: &BhtuneConfig,
17    xdg_data_home: Option<&str>,
18    home: Option<&str>,
19    appdata: Option<&str>,
20    is_windows: bool,
21) -> PathBuf {
22    cli_db
23        .or_else(|| config.db.clone())
24        .unwrap_or_else(|| default_db_path_from(xdg_data_home, home, appdata, is_windows))
25}
26
27/// Resolve the opcda-bridge gateway address with `CLI flag > env var > config file >
28/// default` precedence. The env var is already folded into `cli_host` by clap's `env`
29/// attribute on `TuneArgs::bridge_host`/`OpcCommand`'s per-variant `bridge_host`.
30pub fn resolve_bridge_host(cli_host: Option<String>, config: &BhtuneConfig) -> String {
31    cli_host
32        .or_else(|| config.bridge_host.clone())
33        .unwrap_or_else(|| DEFAULT_BRIDGE_HOST.to_string())
34}
35
36/// Resolve the history retention policy with `CLI flag > env var >
37/// config file > default` precedence, matching [`resolve_bridge_host`]'s shape. The env var
38/// is already folded into `cli_days` by clap's `env` attribute on `Cli::retention_days`.
39/// `None` means retain forever -- there is no built-in default number of days; see
40/// [`BhtuneConfig::retention_days`] for why.
41pub fn resolve_retention_days(cli_days: Option<u32>, config: &BhtuneConfig) -> Option<u32> {
42    cli_days.or(config.retention_days)
43}
44
45/// Resolve `bhtune-server`'s bind address with `BHTUNE_BIND > config file > default`
46/// precedence. The server adapter reads the environment and passes it here, keeping
47/// process-environment access outside this resolver.
48pub fn resolve_bind_addr(bind_override: Option<String>, config: &BhtuneConfig) -> String {
49    bind_override
50        .or_else(|| config.bind.clone())
51        .unwrap_or_else(|| DEFAULT_BIND_ADDR.to_string())
52}
53
54/// Resolve the OPC DA server ProgID with `CLI flag > config file` precedence, erroring if
55/// neither is set -- there's no sensible default for which OPC server to talk to.
56pub fn resolve_server(cli_server: Option<String>, config: &BhtuneConfig) -> anyhow::Result<String> {
57    cli_server.or_else(|| config.server.clone()).ok_or_else(|| {
58        anyhow::anyhow!(
59            "no OPC server specified: pass --server or set `server` in the bhtune config file"
60        )
61    })
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    #[test]
68    fn resolve_db_path_cli_wins() {
69        let config = BhtuneConfig {
70            db: Some(PathBuf::from("/config/bhtune.db")),
71            ..Default::default()
72        };
73        let resolved = resolve_db_path(
74            Some(PathBuf::from("/cli/bhtune.db")),
75            &config,
76            Some("/xdg-data"),
77            Some("/home/me"),
78            None,
79            false,
80        );
81        assert_eq!(resolved, PathBuf::from("/cli/bhtune.db"));
82    }
83
84    #[test]
85    fn resolve_db_path_config_wins_over_platform_default() {
86        let config = BhtuneConfig {
87            db: Some(PathBuf::from("/config/bhtune.db")),
88            ..Default::default()
89        };
90        let resolved = resolve_db_path(None, &config, Some("/xdg-data"), None, None, false);
91        assert_eq!(resolved, PathBuf::from("/config/bhtune.db"));
92    }
93
94    #[test]
95    fn resolve_db_path_falls_back_to_platform_default() {
96        let resolved = resolve_db_path(
97            None,
98            &BhtuneConfig::default(),
99            Some("/xdg-data"),
100            Some("/home/me"),
101            None,
102            false,
103        );
104        assert_eq!(resolved, PathBuf::from("/xdg-data/bhtune/bhtune.db"));
105    }
106
107    #[test]
108    fn resolve_bridge_host_cli_wins() {
109        let config = BhtuneConfig {
110            bridge_host: Some("configured:1".into()),
111            ..Default::default()
112        };
113        assert_eq!(
114            resolve_bridge_host(Some("cli:2".to_string()), &config),
115            "cli:2".to_string()
116        );
117    }
118
119    #[test]
120    fn resolve_bridge_host_config_wins_over_default() {
121        let config = BhtuneConfig {
122            bridge_host: Some("configured:1".into()),
123            ..Default::default()
124        };
125        assert_eq!(
126            resolve_bridge_host(None, &config),
127            "configured:1".to_string()
128        );
129    }
130
131    #[test]
132    fn resolve_bridge_host_default() {
133        assert_eq!(
134            resolve_bridge_host(None, &BhtuneConfig::default()),
135            DEFAULT_BRIDGE_HOST.to_string()
136        );
137    }
138
139    #[test]
140    fn resolve_bind_addr_override_wins() {
141        let config = BhtuneConfig {
142            bind: Some("0.0.0.0:9999".into()),
143            ..Default::default()
144        };
145        assert_eq!(
146            resolve_bind_addr(Some("127.0.0.1:1234".to_string()), &config),
147            "127.0.0.1:1234".to_string()
148        );
149    }
150
151    #[test]
152    fn resolve_bind_addr_config_wins_over_default() {
153        let config = BhtuneConfig {
154            bind: Some("0.0.0.0:9999".into()),
155            ..Default::default()
156        };
157        assert_eq!(resolve_bind_addr(None, &config), "0.0.0.0:9999".to_string());
158    }
159
160    #[test]
161    fn resolve_bind_addr_default() {
162        assert_eq!(
163            resolve_bind_addr(None, &BhtuneConfig::default()),
164            DEFAULT_BIND_ADDR.to_string()
165        );
166    }
167
168    #[test]
169    fn resolve_retention_days_cli_wins() {
170        let config = BhtuneConfig {
171            retention_days: Some(90),
172            ..Default::default()
173        };
174        assert_eq!(resolve_retention_days(Some(30), &config), Some(30));
175    }
176
177    #[test]
178    fn resolve_retention_days_config_wins_over_default() {
179        let config = BhtuneConfig {
180            retention_days: Some(90),
181            ..Default::default()
182        };
183        assert_eq!(resolve_retention_days(None, &config), Some(90));
184    }
185
186    #[test]
187    fn resolve_retention_days_default_is_retain_forever() {
188        // No CLI flag, env var, or config key at all -- retention is disabled by default,
189        // rather than merely lacking a hardcoded number.
190        assert_eq!(resolve_retention_days(None, &BhtuneConfig::default()), None);
191    }
192
193    #[test]
194    fn resolve_server_cli_wins() {
195        let config = BhtuneConfig {
196            server: Some("ConfigServer".into()),
197            ..Default::default()
198        };
199        assert_eq!(
200            resolve_server(Some("CliServer".to_string()), &config).unwrap(),
201            "CliServer"
202        );
203    }
204
205    #[test]
206    fn resolve_server_config_fallback() {
207        let config = BhtuneConfig {
208            server: Some("ConfigServer".into()),
209            ..Default::default()
210        };
211        assert_eq!(resolve_server(None, &config).unwrap(), "ConfigServer");
212    }
213
214    #[test]
215    fn resolve_server_neither_set_errors() {
216        let err = resolve_server(None, &BhtuneConfig::default()).unwrap_err();
217        assert!(err.to_string().contains("no OPC server specified"));
218    }
219}