use crate::models::AuthRequest;
use crate::tls::TlsConfig;
use std::fmt;
use std::time::Duration;
#[derive(Clone)]
pub struct ConnectionConfig {
pub url: String,
pub auth: AuthRequest,
pub connect_timeout: Duration,
pub read_timeout: Duration,
pub tls: TlsConfig,
pub message_buffer: usize,
pub event_buffer: usize,
}
pub const DEFAULT_MESSAGE_BUFFER: usize = 4096;
pub const DEFAULT_EVENT_BUFFER: usize = 1024;
const SENSITIVE_QUERY_KEYS: &[&str] =
&["token", "key", "apikey", "api_key", "secret", "password"];
fn redact_url_query(url: &str) -> String {
let mut parsed = match url::Url::parse(url) {
Ok(u) => u,
Err(_) => return url.to_string(),
};
let original_pairs: Vec<(String, String)> = parsed
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
if original_pairs.is_empty() {
return parsed.to_string();
}
let mut serializer = parsed.query_pairs_mut();
serializer.clear();
for (k, v) in &original_pairs {
let redacted = SENSITIVE_QUERY_KEYS
.iter()
.any(|s| k.eq_ignore_ascii_case(s));
if redacted {
serializer.append_pair(k, "***");
} else {
serializer.append_pair(k, v);
}
}
drop(serializer);
parsed.to_string()
}
impl fmt::Debug for ConnectionConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConnectionConfig")
.field("url", &redact_url_query(&self.url))
.field("auth", &"<redacted>")
.field("connect_timeout", &self.connect_timeout)
.field("read_timeout", &self.read_timeout)
.field("tls", &self.tls)
.field("message_buffer", &self.message_buffer)
.field("event_buffer", &self.event_buffer)
.finish()
}
}
impl ConnectionConfig {
pub fn new(url: impl Into<String>, auth: AuthRequest) -> Self {
Self {
url: url.into(),
auth,
connect_timeout: Duration::from_secs(30),
read_timeout: Duration::from_secs(30),
tls: TlsConfig::default(),
message_buffer: DEFAULT_MESSAGE_BUFFER,
event_buffer: DEFAULT_EVENT_BUFFER,
}
}
pub fn builder(url: impl Into<String>, auth: AuthRequest) -> ConnectionConfigBuilder {
ConnectionConfigBuilder {
url: url.into(),
auth,
connect_timeout: Duration::from_secs(30),
read_timeout: Duration::from_secs(30),
tls: TlsConfig::default(),
message_buffer: DEFAULT_MESSAGE_BUFFER,
event_buffer: DEFAULT_EVENT_BUFFER,
}
}
pub fn fugle_stock(auth: AuthRequest) -> Self {
Self::new(crate::urls::STOCK_WS, auth)
}
pub fn fugle_futopt(auth: AuthRequest) -> Self {
Self::new(crate::urls::FUTOPT_WS, auth)
}
}
pub struct ConnectionConfigBuilder {
url: String,
auth: AuthRequest,
connect_timeout: Duration,
read_timeout: Duration,
tls: TlsConfig,
message_buffer: usize,
event_buffer: usize,
}
impl ConnectionConfigBuilder {
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
pub fn tls(mut self, tls: TlsConfig) -> Self {
self.tls = tls;
self
}
pub fn message_buffer(mut self, cap: usize) -> Self {
assert!(cap > 0, "message_buffer must be greater than zero");
self.message_buffer = cap;
self
}
pub fn event_buffer(mut self, cap: usize) -> Self {
assert!(cap > 0, "event_buffer must be greater than zero");
self.event_buffer = cap;
self
}
pub fn build(self) -> ConnectionConfig {
ConnectionConfig {
url: self.url,
auth: self.auth,
connect_timeout: self.connect_timeout,
read_timeout: self.read_timeout,
tls: self.tls,
message_buffer: self.message_buffer,
event_buffer: self.event_buffer,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_config_new() {
let auth = AuthRequest::with_api_key("test-key");
let config = ConnectionConfig::new("wss://example.com", auth);
assert_eq!(config.url, "wss://example.com");
assert_eq!(config.connect_timeout, Duration::from_secs(30));
assert_eq!(config.read_timeout, Duration::from_secs(30));
}
#[test]
fn test_connection_config_builder() {
let auth = AuthRequest::with_api_key("test-key");
let config = ConnectionConfig::builder("wss://example.com", auth)
.connect_timeout(Duration::from_secs(10))
.read_timeout(Duration::from_secs(20))
.build();
assert_eq!(config.url, "wss://example.com");
assert_eq!(config.connect_timeout, Duration::from_secs(10));
assert_eq!(config.read_timeout, Duration::from_secs(20));
}
#[test]
fn test_fugle_stock_config() {
let auth = AuthRequest::with_api_key("test-key");
let config = ConnectionConfig::fugle_stock(auth);
assert_eq!(config.url, "wss://api.fugle.tw/marketdata/v1.0/stock/streaming");
}
#[test]
fn test_fugle_futopt_config() {
let auth = AuthRequest::with_api_key("test-key");
let config = ConnectionConfig::fugle_futopt(auth);
assert_eq!(config.url, "wss://api.fugle.tw/marketdata/v1.0/futopt/streaming");
}
#[test]
fn test_debug_redacts_auth() {
let auth = AuthRequest::with_api_key("super-secret-api-key");
let config = ConnectionConfig::fugle_stock(auth);
let rendered = format!("{:?}", config);
assert!(rendered.contains("auth: \"<redacted>\""));
assert!(!rendered.contains("super-secret-api-key"));
}
#[test]
fn test_debug_redacts_url_query_token() {
let auth = AuthRequest::with_api_key("k");
let config =
ConnectionConfig::new("wss://example.com/stream?token=secret&v=1", auth);
let rendered = format!("{:?}", config);
assert!(rendered.contains("token=***"));
assert!(rendered.contains("v=1"));
assert!(!rendered.contains("token=secret"));
}
#[test]
fn test_debug_redacts_multiple_sensitive_keys() {
let auth = AuthRequest::with_api_key("k");
let config = ConnectionConfig::new(
"wss://example.com/stream?api_key=AAA&secret=BBB&safe=CCC",
auth,
);
let rendered = format!("{:?}", config);
assert!(rendered.contains("api_key=***"));
assert!(rendered.contains("secret=***"));
assert!(rendered.contains("safe=CCC"));
assert!(!rendered.contains("AAA"));
assert!(!rendered.contains("BBB"));
}
#[test]
fn test_debug_handles_unparseable_url() {
let auth = AuthRequest::with_api_key("k");
let config = ConnectionConfig::new("not a real url", auth);
let rendered = format!("{:?}", config);
assert!(rendered.contains("not a real url"));
}
}