use std::path::PathBuf;
use std::time::Duration;
use crate::client::Client;
use crate::error::Error;
#[derive(Clone, Debug)]
pub struct ClientOptions {
pub app_id: String,
pub secret: String,
pub nodes: Vec<String>,
pub env: String,
pub name: Option<String>,
pub tag: Option<String>,
pub http_timeout: Duration,
pub reconnect_interval: Duration,
pub heartbeat_interval: Duration,
pub cache: CacheOptions,
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
app_id: String::new(),
secret: String::new(),
nodes: Vec::new(),
env: String::new(),
name: None,
tag: None,
http_timeout: Duration::from_secs(100),
reconnect_interval: Duration::from_secs(5),
heartbeat_interval: Duration::from_secs(30),
cache: CacheOptions::default(),
}
}
}
impl ClientOptions {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub(crate) fn normalized(mut self) -> Result<Self, Error> {
self.app_id = self.app_id.trim().to_string();
if self.app_id.is_empty() {
return Err(Error::EmptyAppId);
}
self.nodes = normalize_nodes(&self.nodes);
if self.nodes.is_empty() {
return Err(Error::EmptyNodes);
}
self.env = self.env.trim().to_ascii_uppercase();
self.secret = self.secret.trim().to_string();
self.name = trim_optional(self.name);
self.tag = trim_optional(self.tag);
if self.http_timeout.is_zero() {
self.http_timeout = Duration::from_secs(30);
}
if self.reconnect_interval.is_zero() {
self.reconnect_interval = Duration::from_secs(5);
}
if self.heartbeat_interval.is_zero() {
self.heartbeat_interval = Duration::from_secs(30);
}
#[cfg(not(feature = "cache-encrypt"))]
if self.cache.encrypt {
return Err(Error::CacheEncryptDisabled);
}
Ok(self)
}
}
#[derive(Clone, Debug)]
pub struct CacheOptions {
pub enabled: bool,
pub directory: PathBuf,
pub encrypt: bool,
}
impl Default for CacheOptions {
fn default() -> Self {
Self {
enabled: true,
directory: PathBuf::new(),
encrypt: false,
}
}
}
#[derive(Clone, Debug, Default)]
#[must_use]
pub struct ClientBuilder {
options: ClientOptions,
}
impl ClientBuilder {
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.options.app_id = app_id.into();
self
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.options.secret = secret.into();
self
}
pub fn nodes<I, S>(mut self, nodes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.nodes = nodes.into_iter().map(Into::into).collect();
self
}
pub fn env(mut self, env: impl Into<String>) -> Self {
self.options.env = env.into();
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.options.name = Some(name.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.options.tag = Some(tag.into());
self
}
pub fn http_timeout(mut self, timeout: Duration) -> Self {
self.options.http_timeout = timeout;
self
}
pub fn reconnect_interval(mut self, interval: Duration) -> Self {
self.options.reconnect_interval = interval;
self
}
pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
self.options.heartbeat_interval = interval;
self
}
pub fn cache(mut self, cache: CacheOptions) -> Self {
self.options.cache = cache;
self
}
pub fn build(self) -> Result<Client, Error> {
Client::new(self.options)
}
pub fn build_options(self) -> Result<ClientOptions, Error> {
self.options.normalized()
}
}
pub(crate) fn normalize_nodes(nodes: &[String]) -> Vec<String> {
nodes
.iter()
.flat_map(|node| node.split(','))
.map(str::trim)
.filter(|node| !node.is_empty())
.map(|node| node.trim_end_matches('/').to_string())
.collect()
}
fn trim_optional(value: Option<String>) -> Option<String> {
value.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
})
}
#[cfg(test)]
mod tests {
use super::{ClientOptions, normalize_nodes};
#[test]
fn normalize_nodes_splits_commas_and_strips_slashes() {
let nodes = vec![
" http://localhost:5000/ ".into(),
"http://n2:1,http://n3:2/".into(),
];
assert_eq!(
normalize_nodes(&nodes),
vec![
"http://localhost:5000".to_string(),
"http://n2:1".to_string(),
"http://n3:2".to_string(),
]
);
}
#[test]
fn normalized_rejects_empty_app_id() {
let error = ClientOptions {
nodes: vec!["http://localhost:5000".into()],
..ClientOptions::default()
}
.normalized()
.unwrap_err();
assert_eq!(error.to_string(), "app_id must not be empty");
}
#[test]
fn normalized_uppercases_env() {
let options = ClientOptions {
app_id: "app".into(),
nodes: vec!["http://localhost:5000".into()],
env: " dev ".into(),
..ClientOptions::default()
}
.normalized()
.unwrap();
assert_eq!(options.env, "DEV");
}
#[cfg(not(feature = "cache-encrypt"))]
#[test]
fn normalized_rejects_encrypt_without_feature() {
use super::CacheOptions;
use crate::Error;
let error = ClientOptions {
app_id: "app".into(),
nodes: vec!["http://localhost:5000".into()],
cache: CacheOptions {
encrypt: true,
..CacheOptions::default()
},
..ClientOptions::default()
}
.normalized()
.unwrap_err();
assert!(matches!(error, Error::CacheEncryptDisabled));
}
#[cfg(feature = "cache-encrypt")]
#[test]
fn normalized_allows_encrypt_with_feature() {
use super::CacheOptions;
ClientOptions {
app_id: "app".into(),
nodes: vec!["http://localhost:5000".into()],
cache: CacheOptions {
encrypt: true,
..CacheOptions::default()
},
..ClientOptions::default()
}
.normalized()
.unwrap();
}
}