use crate::{PusherError, Result, Token};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use std::time::Duration;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Clone, Debug)]
pub struct Config {
scheme: String,
host: String,
port: Option<u16>,
app_id: String,
token: Token,
timeout: Duration,
encryption_master_key: Option<EncryptionKey>,
pool_max_idle_per_host: usize,
enable_retry: bool,
max_retries: u32,
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
struct EncryptionKey(Vec<u8>);
impl std::fmt::Debug for EncryptionKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EncryptionKey([REDACTED])")
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
pub fn new(
app_id: impl Into<String>,
key: impl Into<String>,
secret: impl Into<String>,
) -> Self {
ConfigBuilder::default()
.app_id(app_id)
.key(key)
.secret(secret)
.build()
.expect("Basic config should always be valid")
}
pub fn validate(&self) -> Result<()> {
if self.app_id.is_empty() {
return Err(PusherError::Config {
message: "App ID cannot be empty".to_string(),
});
}
if self.token.key.is_empty() {
return Err(PusherError::Config {
message: "App key cannot be empty".to_string(),
});
}
if let Some(ref key) = self.encryption_master_key {
if key.0.len() != 32 {
return Err(PusherError::Config {
message: format!("Encryption key must be 32 bytes, got {}", key.0.len()),
});
}
}
Ok(())
}
pub fn scheme(&self) -> &str {
&self.scheme
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> Option<u16> {
self.port
}
pub fn app_id(&self) -> &str {
&self.app_id
}
pub fn token(&self) -> &Token {
&self.token
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn encryption_master_key(&self) -> Option<&[u8]> {
self.encryption_master_key.as_ref().map(|k| k.0.as_slice())
}
pub fn pool_max_idle_per_host(&self) -> usize {
self.pool_max_idle_per_host
}
pub fn enable_retry(&self) -> bool {
self.enable_retry
}
pub fn max_retries(&self) -> u32 {
self.max_retries
}
pub fn base_url(&self) -> String {
let port = match self.port {
Some(port) => format!(":{}", port),
None => String::new(),
};
format!("{}://{}{}", self.scheme, self.host, port)
}
pub fn prefix_path(&self, sub_path: &str) -> String {
format!("/apps/{}{}", self.app_id, sub_path)
}
}
#[derive(Default)]
pub struct ConfigBuilder {
scheme: Option<String>,
host: Option<String>,
port: Option<u16>,
app_id: Option<String>,
key: Option<String>,
secret: Option<String>,
timeout: Option<Duration>,
encryption_master_key: Option<EncryptionKey>,
pool_max_idle_per_host: Option<usize>,
enable_retry: Option<bool>,
max_retries: Option<u32>,
}
impl ConfigBuilder {
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = Some(app_id.into());
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
pub fn cluster(mut self, cluster: impl AsRef<str>) -> Self {
self.host = Some(format!("api-{}.pusher.com", cluster.as_ref()));
self
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn use_tls(mut self, use_tls: bool) -> Self {
self.scheme = Some(if use_tls { "https" } else { "http" }.to_string());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn encryption_master_key(mut self, key: Vec<u8>) -> Result<Self> {
if key.len() != 32 {
return Err(PusherError::Config {
message: format!("Encryption key must be 32 bytes, got {}", key.len()),
});
}
self.encryption_master_key = Some(EncryptionKey(key));
Ok(self)
}
pub fn encryption_master_key_base64(self, key_base64: impl AsRef<str>) -> Result<Self> {
let decoded = BASE64
.decode(key_base64.as_ref())
.map_err(|e| PusherError::Config {
message: format!("Invalid base64 encryption key: {}", e),
})?;
self.encryption_master_key(decoded)
}
pub fn pool_max_idle_per_host(mut self, max: usize) -> Self {
self.pool_max_idle_per_host = Some(max);
self
}
pub fn enable_retry(mut self, enable: bool) -> Self {
self.enable_retry = Some(enable);
self
}
pub fn max_retries(mut self, max: u32) -> Self {
self.max_retries = Some(max);
self
}
pub fn build(self) -> Result<Config> {
let app_id = self.app_id.ok_or_else(|| PusherError::Config {
message: "App ID is required".to_string(),
})?;
let key = self.key.ok_or_else(|| PusherError::Config {
message: "App key is required".to_string(),
})?;
let secret = self.secret.ok_or_else(|| PusherError::Config {
message: "App secret is required".to_string(),
})?;
let config = Config {
scheme: self.scheme.unwrap_or_else(|| "https".to_string()),
host: self.host.unwrap_or_else(|| "api.pusherapp.com".to_string()),
port: self.port,
app_id,
token: Token::new(key, secret),
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
encryption_master_key: self.encryption_master_key,
pool_max_idle_per_host: self.pool_max_idle_per_host.unwrap_or(10),
enable_retry: self.enable_retry.unwrap_or(true),
max_retries: self.max_retries.unwrap_or(3),
};
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = Config::builder()
.app_id("123")
.key("key")
.secret("secret")
.cluster("eu")
.timeout(Duration::from_secs(10))
.enable_retry(false)
.build()
.unwrap();
assert_eq!(config.app_id(), "123");
assert_eq!(config.host(), "api-eu.pusher.com");
assert_eq!(config.timeout(), Duration::from_secs(10));
assert!(!config.enable_retry());
}
#[test]
fn test_config_validation() {
assert!(Config::builder().build().is_err());
assert!(Config::builder().app_id("123").build().is_err());
assert!(Config::builder()
.app_id("123")
.key("key")
.secret("secret")
.build()
.is_ok());
}
#[test]
fn test_encryption_key_validation() {
let config = Config::builder()
.app_id("123")
.key("key")
.secret("secret")
.encryption_master_key(vec![0u8; 32])
.unwrap()
.build()
.unwrap();
assert!(config.encryption_master_key().is_some());
assert!(Config::builder()
.app_id("123")
.key("key")
.secret("secret")
.encryption_master_key(vec![0u8; 16])
.is_err());
}
}