pub use fabro_config::cli::*; #[cfg(feature = "server")] use tracing::debug; #[cfg(feature = "server")] #[derive(Debug, PartialEq)] pub struct ResolvedMode { pub mode: ExecutionMode, pub server_base_url: String, pub tls: Option, } #[cfg(feature = "server")] const DEFAULT_SERVER_URL: &str = "http://localhost:3000"; #[cfg(feature = "server")] pub fn resolve_mode( cli_mode: Option, cli_server_url: Option<&str>, config: &CliConfig, ) -> ResolvedMode { let mode = cli_mode.or_else(|| config.mode.clone()).unwrap_or_default(); let server_defaults = config.server.as_ref(); let server_base_url = cli_server_url .map(String::from) .or_else(|| server_defaults.and_then(|s| s.base_url.clone())) .unwrap_or_else(|| DEFAULT_SERVER_URL.to_string()); let tls = server_defaults.and_then(|s| s.tls.clone()); debug!(mode = ?mode, base_url = %server_base_url, tls = tls.is_some(), "CLI mode resolved"); ResolvedMode { mode, server_base_url, tls, } } #[cfg(feature = "server")] pub fn build_server_client(tls: Option<&ClientTlsConfig>) -> anyhow::Result { let Some(tls) = tls else { return Ok(reqwest::Client::new()); }; let cert_path = fabro_config::expand_tilde(&tls.cert); let key_path = fabro_config::expand_tilde(&tls.key); let ca_path = fabro_config::expand_tilde(&tls.ca); let cert_pem = std::fs::read(&cert_path)?; let key_pem = std::fs::read(&key_path)?; let ca_pem = std::fs::read(&ca_path)?; let mut identity_pem = cert_pem; identity_pem.push(b'\n'); identity_pem.extend_from_slice(&key_pem); let identity = reqwest::Identity::from_pem(&identity_pem)?; let ca_cert = reqwest::Certificate::from_pem(&ca_pem)?; let client = reqwest::Client::builder() .use_rustls_tls() .identity(identity) .add_root_certificate(ca_cert) .build()?; Ok(client) } #[cfg(all(test, feature = "server"))] mod tests { use std::path::PathBuf; use super::*; // --- resolve_mode precedence --- #[test] fn resolve_mode_defaults_to_standalone() { let config = CliConfig::default(); let resolved = resolve_mode(None, None, &config); assert_eq!(resolved.mode, ExecutionMode::Standalone); assert_eq!(resolved.server_base_url, DEFAULT_SERVER_URL); assert_eq!(resolved.tls, None); } #[test] fn resolve_mode_config_overrides_default() { let config = CliConfig { mode: Some(ExecutionMode::Server), server: Some(ServerDefaults { base_url: Some("https://config.example.com".to_string()), tls: None, }), ..CliConfig::default() }; let resolved = resolve_mode(None, None, &config); assert_eq!(resolved.mode, ExecutionMode::Server); assert_eq!(resolved.server_base_url, "https://config.example.com"); } #[test] fn resolve_mode_cli_overrides_config() { let config = CliConfig { mode: Some(ExecutionMode::Standalone), server: Some(ServerDefaults { base_url: Some("https://config.example.com".to_string()), tls: None, }), ..CliConfig::default() }; let resolved = resolve_mode( Some(ExecutionMode::Server), Some("https://cli.example.com"), &config, ); assert_eq!(resolved.mode, ExecutionMode::Server); assert_eq!(resolved.server_base_url, "https://cli.example.com"); } #[test] fn resolve_mode_cli_url_overrides_config_url() { let config = CliConfig { server: Some(ServerDefaults { base_url: Some("https://config.example.com".to_string()), tls: None, }), ..CliConfig::default() }; let resolved = resolve_mode(None, Some("https://cli.example.com"), &config); assert_eq!(resolved.server_base_url, "https://cli.example.com"); } #[test] fn resolve_mode_tls_from_config() { let tls = ClientTlsConfig { cert: PathBuf::from("cert.pem"), key: PathBuf::from("key.pem"), ca: PathBuf::from("ca.pem"), }; let config = CliConfig { server: Some(ServerDefaults { base_url: None, tls: Some(tls.clone()), }), ..CliConfig::default() }; let resolved = resolve_mode(None, None, &config); assert_eq!(resolved.tls, Some(tls)); } }