agile_config_client/
options.rs1use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::client::Client;
7use crate::error::Error;
8
9#[derive(Clone, Debug)]
29pub struct ClientOptions {
30 pub app_id: String,
32 pub secret: String,
34 pub nodes: Vec<String>,
38 pub env: String,
40 pub name: Option<String>,
42 pub tag: Option<String>,
44 pub http_timeout: Duration,
46 pub reconnect_interval: Duration,
48 pub heartbeat_interval: Duration,
50 pub cache: CacheOptions,
52}
53
54impl Default for ClientOptions {
55 fn default() -> Self {
56 Self {
57 app_id: String::new(),
58 secret: String::new(),
59 nodes: Vec::new(),
60 env: String::new(),
61 name: None,
62 tag: None,
63 http_timeout: Duration::from_secs(100),
64 reconnect_interval: Duration::from_secs(5),
65 heartbeat_interval: Duration::from_secs(30),
66 cache: CacheOptions::default(),
67 }
68 }
69}
70
71impl ClientOptions {
72 pub fn builder() -> ClientBuilder {
74 ClientBuilder::default()
75 }
76
77 pub(crate) fn normalized(mut self) -> Result<Self, Error> {
78 self.app_id = self.app_id.trim().to_string();
79 if self.app_id.is_empty() {
80 return Err(Error::EmptyAppId);
81 }
82
83 self.nodes = normalize_nodes(&self.nodes);
84 if self.nodes.is_empty() {
85 return Err(Error::EmptyNodes);
86 }
87
88 self.env = self.env.trim().to_ascii_uppercase();
89 self.secret = self.secret.trim().to_string();
90 self.name = trim_optional(self.name);
91 self.tag = trim_optional(self.tag);
92
93 if self.http_timeout.is_zero() {
94 self.http_timeout = Duration::from_secs(30);
95 }
96 if self.reconnect_interval.is_zero() {
97 self.reconnect_interval = Duration::from_secs(5);
98 }
99 if self.heartbeat_interval.is_zero() {
100 self.heartbeat_interval = Duration::from_secs(30);
101 }
102
103 #[cfg(not(feature = "cache-encrypt"))]
104 if self.cache.encrypt {
105 return Err(Error::CacheEncryptDisabled);
106 }
107
108 Ok(self)
109 }
110}
111
112#[derive(Clone, Debug)]
114pub struct CacheOptions {
115 pub enabled: bool,
117 pub directory: PathBuf,
119 pub encrypt: bool,
124}
125
126impl Default for CacheOptions {
127 fn default() -> Self {
128 Self {
129 enabled: true,
130 directory: PathBuf::new(),
131 encrypt: false,
132 }
133 }
134}
135
136#[derive(Clone, Debug, Default)]
138#[must_use]
139pub struct ClientBuilder {
140 options: ClientOptions,
141}
142
143impl ClientBuilder {
144 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
146 self.options.app_id = app_id.into();
147 self
148 }
149
150 pub fn secret(mut self, secret: impl Into<String>) -> Self {
152 self.options.secret = secret.into();
153 self
154 }
155
156 pub fn nodes<I, S>(mut self, nodes: I) -> Self
158 where
159 I: IntoIterator<Item = S>,
160 S: Into<String>,
161 {
162 self.options.nodes = nodes.into_iter().map(Into::into).collect();
163 self
164 }
165
166 pub fn env(mut self, env: impl Into<String>) -> Self {
168 self.options.env = env.into();
169 self
170 }
171
172 pub fn name(mut self, name: impl Into<String>) -> Self {
174 self.options.name = Some(name.into());
175 self
176 }
177
178 pub fn tag(mut self, tag: impl Into<String>) -> Self {
180 self.options.tag = Some(tag.into());
181 self
182 }
183
184 pub fn http_timeout(mut self, timeout: Duration) -> Self {
186 self.options.http_timeout = timeout;
187 self
188 }
189
190 pub fn reconnect_interval(mut self, interval: Duration) -> Self {
192 self.options.reconnect_interval = interval;
193 self
194 }
195
196 pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
198 self.options.heartbeat_interval = interval;
199 self
200 }
201
202 pub fn cache(mut self, cache: CacheOptions) -> Self {
204 self.options.cache = cache;
205 self
206 }
207
208 pub fn build(self) -> Result<Client, Error> {
215 Client::new(self.options)
216 }
217
218 pub fn build_options(self) -> Result<ClientOptions, Error> {
225 self.options.normalized()
226 }
227}
228
229pub(crate) fn normalize_nodes(nodes: &[String]) -> Vec<String> {
230 nodes
231 .iter()
232 .flat_map(|node| node.split(','))
233 .map(str::trim)
234 .filter(|node| !node.is_empty())
235 .map(|node| node.trim_end_matches('/').to_string())
236 .collect()
237}
238
239fn trim_optional(value: Option<String>) -> Option<String> {
240 value.and_then(|raw| {
241 let trimmed = raw.trim();
242 if trimmed.is_empty() {
243 None
244 } else {
245 Some(trimmed.to_string())
246 }
247 })
248}
249
250#[cfg(test)]
251mod tests {
252 use super::{ClientOptions, normalize_nodes};
253
254 #[test]
255 fn normalize_nodes_splits_commas_and_strips_slashes() {
256 let nodes = vec![
257 " http://localhost:5000/ ".into(),
258 "http://n2:1,http://n3:2/".into(),
259 ];
260 assert_eq!(
261 normalize_nodes(&nodes),
262 vec![
263 "http://localhost:5000".to_string(),
264 "http://n2:1".to_string(),
265 "http://n3:2".to_string(),
266 ]
267 );
268 }
269
270 #[test]
271 fn normalized_rejects_empty_app_id() {
272 let error = ClientOptions {
273 nodes: vec!["http://localhost:5000".into()],
274 ..ClientOptions::default()
275 }
276 .normalized()
277 .unwrap_err();
278 assert_eq!(error.to_string(), "app_id must not be empty");
279 }
280
281 #[test]
282 fn normalized_uppercases_env() {
283 let options = ClientOptions {
284 app_id: "app".into(),
285 nodes: vec!["http://localhost:5000".into()],
286 env: " dev ".into(),
287 ..ClientOptions::default()
288 }
289 .normalized()
290 .unwrap();
291 assert_eq!(options.env, "DEV");
292 }
293
294 #[cfg(not(feature = "cache-encrypt"))]
295 #[test]
296 fn normalized_rejects_encrypt_without_feature() {
297 use super::CacheOptions;
298 use crate::Error;
299
300 let error = ClientOptions {
301 app_id: "app".into(),
302 nodes: vec!["http://localhost:5000".into()],
303 cache: CacheOptions {
304 encrypt: true,
305 ..CacheOptions::default()
306 },
307 ..ClientOptions::default()
308 }
309 .normalized()
310 .unwrap_err();
311 assert!(matches!(error, Error::CacheEncryptDisabled));
312 }
313
314 #[cfg(feature = "cache-encrypt")]
315 #[test]
316 fn normalized_allows_encrypt_with_feature() {
317 use super::CacheOptions;
318
319 ClientOptions {
320 app_id: "app".into(),
321 nodes: vec!["http://localhost:5000".into()],
322 cache: CacheOptions {
323 encrypt: true,
324 ..CacheOptions::default()
325 },
326 ..ClientOptions::default()
327 }
328 .normalized()
329 .unwrap();
330 }
331}