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            session_id: None,
84            parent_node_key: None,
85            page_token: None,
86            page_size: None,
87            all: false,
88            max_results: None,
89            refresh: false,
90        });
91        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
92    }
93
94    #[tokio::test]
95    async fn test_run_with_cli_config_load_failure_is_exit_failure() {
96        // An explicit --config path that doesn't exist is a hard error
97        // (missing_is_error = true), exercising the pre-config-load branch.
98        let mut cli = base_cli(Commands::Servers);
99        cli.config = Some(PathBuf::from("/nonexistent/opcda-bridge-client.toml"));
100        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
101    }
102
103    #[tokio::test]
104    async fn test_run_with_cli_config_load_failure_uses_cli_only_format() {
105        // Same as above but with --json set, to exercise the branch that
106        // formats the config-load error using the CLI-only format rather
107        // than the (unreachable, since config never loaded) full-precedence
108        // format.
109        let mut cli = base_cli(Commands::Servers);
110        cli.config = Some(PathBuf::from("/nonexistent/opcda-bridge-client.toml"));
111        cli.json = true;
112        assert_eq!(run_with_cli(cli).await, ExitCode::FAILURE);
113    }
114
115    #[test]
116    fn test_fail_returns_exit_failure() {
117        let err = anyhow::anyhow!("boom");
118        assert_eq!(fail(&err, output::OutputFormat::Table), ExitCode::FAILURE);
119        assert_eq!(fail(&err, output::OutputFormat::Json), ExitCode::FAILURE);
120    }
121}