Skip to main content

opcda_bridge_client/
lib.rs

1pub mod cli;
2pub mod commands;
3pub mod config;
4pub mod output;
5
6#[cfg(test)]
7mod test_support;
8
9use std::process::ExitCode;
10
11/// Parse CLI arguments and run, returning a process exit code.
12///
13/// Kept as a thin wrapper around [`run_with_cli`] so `main.rs` stays a
14/// one-line delegator while the actual control flow (config loading, error
15/// formatting) is exercised by tests against an already-parsed `Cli`,
16/// without needing to control `std::env::args()`.
17pub async fn run() -> ExitCode {
18    use clap::Parser;
19    run_with_cli(cli::Cli::parse()).await
20}
21
22/// Resolve config, dispatch the command, and format any error for
23/// display.
24///
25/// CLI-only output format (`--json` / `--output` / `OPC_BRIDGE_OUTPUT`) is
26/// resolved *before* loading the config file, so a config-load failure can
27/// still be reported in the right format even though the config file's own
28/// `output` key could never be known at that point. Once the config loads
29/// successfully, the fully-resolved `CLI > env > config > default` format
30/// applies to the command's own result.
31async fn run_with_cli(cli: cli::Cli) -> ExitCode {
32    let cli_format = output::resolve_from_cli(&cli);
33    match config::load_config(cli.config.as_deref()) {
34        Err(e) => fail(&e, cli_format.unwrap_or(output::OutputFormat::Table)),
35        Ok(config) => {
36            let format = config::resolve_output(cli_format, &config);
37            match cli::run_command(cli, &config, format).await {
38                Ok(()) => ExitCode::SUCCESS,
39                Err(e) => fail(&e, format),
40            }
41        }
42    }
43}
44
45/// Print an error to stderr in the requested format and return a failure
46/// exit code.
47fn fail(err: &anyhow::Error, format: output::OutputFormat) -> ExitCode {
48    eprintln!("{}", output::format_error(err, format));
49    ExitCode::FAILURE
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::cli::{Cli, Commands};
56    use crate::test_support::{MockBridgeService, start_mock_server};
57    use std::path::PathBuf;
58
59    fn base_cli(command: Commands) -> Cli {
60        Cli {
61            host: None,
62            config: None,
63            output: None,
64            json: false,
65            command,
66        }
67    }
68
69    #[tokio::test]
70    async fn test_run_with_cli_success_is_exit_success() {
71        let host = start_mock_server(MockBridgeService::default()).await;
72        let mut cli = base_cli(Commands::Servers);
73        cli.host = Some(host);
74        assert_eq!(run_with_cli(cli).await, ExitCode::SUCCESS);
75    }
76
77    #[tokio::test]
78    async fn test_run_with_cli_command_error_is_exit_failure() {
79        // No --server and no config file `server` key: resolve_server errors
80        // before any network call is made.
81        let cli = base_cli(Commands::Browse {
82            server: None,
83            flat: false,
84            path: String::new(),
85            max_tags: None,
86        });
87        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
88    }
89
90    #[tokio::test]
91    async fn test_run_with_cli_config_load_failure_is_exit_failure() {
92        // An explicit --config path that doesn't exist is a hard error
93        // (missing_is_error = true), exercising the pre-config-load branch.
94        let mut cli = base_cli(Commands::Servers);
95        cli.config = Some(PathBuf::from("/nonexistent/opcda-bridge-client.toml"));
96        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
97    }
98
99    #[tokio::test]
100    async fn test_run_with_cli_config_load_failure_uses_cli_only_format() {
101        // Same as above but with --json set, to exercise the branch that
102        // formats the config-load error using the CLI-only format rather
103        // than the (unreachable, since config never loaded) full-precedence
104        // format.
105        let mut cli = base_cli(Commands::Servers);
106        cli.config = Some(PathBuf::from("/nonexistent/opcda-bridge-client.toml"));
107        cli.json = true;
108        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
109    }
110
111    #[test]
112    fn test_fail_returns_exit_failure() {
113        let err = anyhow::anyhow!("boom");
114        assert_eq!(fail(&err, output::OutputFormat::Table), ExitCode::FAILURE);
115        assert_eq!(fail(&err, output::OutputFormat::Json), ExitCode::FAILURE);
116    }
117}