1use crate::api::{error, plugin, props};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5#[doc(alias("config", "sdk", "api"))]
20#[derive(Clone, Debug)]
21pub struct ConfigService {
22 inner: Arc<crate::config::NacosConfigService>,
23}
24
25impl ConfigService {
26 pub async fn get_config(
30 &self,
31 data_id: String,
32 group: String,
33 ) -> error::Result<ConfigResponse> {
34 crate::common::util::check_not_blank(&data_id, "data_id")?;
35 crate::common::util::check_not_blank(&group, "group")?;
36 self.inner.get_config(data_id, group).await
37 }
38
39 pub async fn publish_config(
41 &self,
42 data_id: String,
43 group: String,
44 content: String,
45 content_type: Option<String>,
46 ) -> error::Result<bool> {
47 crate::common::util::check_not_blank(&data_id, "data_id")?;
48 crate::common::util::check_not_blank(&group, "group")?;
49 crate::common::util::check_not_blank(&content, "content")?;
50 self.inner
51 .publish_config(data_id, group, content, content_type)
52 .await
53 }
54
55 pub async fn publish_config_cas(
57 &self,
58 data_id: String,
59 group: String,
60 content: String,
61 content_type: Option<String>,
62 cas_md5: String,
63 ) -> error::Result<bool> {
64 crate::common::util::check_not_blank(&data_id, "data_id")?;
65 crate::common::util::check_not_blank(&group, "group")?;
66 crate::common::util::check_not_blank(&content, "content")?;
67 crate::common::util::check_not_blank(&cas_md5, "cas_md5")?;
68 self.inner
69 .publish_config_cas(data_id, group, content, content_type, cas_md5)
70 .await
71 }
72
73 pub async fn publish_config_beta(
75 &self,
76 data_id: String,
77 group: String,
78 content: String,
79 content_type: Option<String>,
80 beta_ips: String,
81 ) -> error::Result<bool> {
82 crate::common::util::check_not_blank(&data_id, "data_id")?;
83 crate::common::util::check_not_blank(&group, "group")?;
84 crate::common::util::check_not_blank(&content, "content")?;
85 crate::common::util::check_not_blank(&beta_ips, "beta_ips")?;
86 self.inner
87 .publish_config_beta(data_id, group, content, content_type, beta_ips)
88 .await
89 }
90
91 pub async fn publish_config_param(
93 &self,
94 data_id: String,
95 group: String,
96 content: String,
97 content_type: Option<String>,
98 cas_md5: Option<String>,
99 params: HashMap<String, String>,
100 ) -> error::Result<bool> {
101 crate::common::util::check_not_blank(&data_id, "data_id")?;
102 crate::common::util::check_not_blank(&group, "group")?;
103 crate::common::util::check_not_blank(&content, "content")?;
104 self.inner
105 .publish_config_param(data_id, group, content, content_type, cas_md5, params)
106 .await
107 }
108
109 pub async fn remove_config(&self, data_id: String, group: String) -> error::Result<bool> {
111 crate::common::util::check_not_blank(&data_id, "data_id")?;
112 crate::common::util::check_not_blank(&group, "group")?;
113 self.inner.remove_config(data_id, group).await
114 }
115
116 pub async fn add_listener(
118 &self,
119 data_id: String,
120 group: String,
121 listener: Arc<dyn ConfigChangeListener>,
122 ) -> error::Result<()> {
123 crate::common::util::check_not_blank(&data_id, "data_id")?;
124 crate::common::util::check_not_blank(&group, "group")?;
125 self.inner.add_listener(data_id, group, listener).await
126 }
127
128 pub async fn remove_listener(
130 &self,
131 data_id: String,
132 group: String,
133 listener: Arc<dyn ConfigChangeListener>,
134 ) -> error::Result<()> {
135 crate::common::util::check_not_blank(&data_id, "data_id")?;
136 crate::common::util::check_not_blank(&group, "group")?;
137 self.inner.remove_listener(data_id, group, listener).await
138 }
139}
140
141pub trait ConfigChangeListener: Send + Sync {
143 fn notify(&self, config_resp: ConfigResponse);
144}
145
146#[derive(Debug, Clone)]
148pub struct ConfigResponse {
149 namespace: String,
151 data_id: String,
153 group: String,
155 content: String,
157 content_type: String,
159 md5: String,
161}
162
163impl std::fmt::Display for ConfigResponse {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 let mut content = self.content.clone();
166 if content.len() > 30 {
167 content.truncate(30);
168 content.push_str("...");
169 }
170 write!(
171 f,
172 "ConfigResponse(namespace={n},data_id={d},group={g},md5={m},content={c})",
173 n = self.namespace,
174 d = self.data_id,
175 g = self.group,
176 m = self.md5,
177 c = content
178 )
179 }
180}
181
182impl ConfigResponse {
183 pub fn new(
184 data_id: String,
185 group: String,
186 namespace: String,
187 content: String,
188 content_type: String,
189 md5: String,
190 ) -> Self {
191 ConfigResponse {
192 data_id,
193 group,
194 namespace,
195 content,
196 content_type,
197 md5,
198 }
199 }
200
201 pub fn namespace(&self) -> &String {
202 &self.namespace
203 }
204 pub fn data_id(&self) -> &String {
205 &self.data_id
206 }
207 pub fn group(&self) -> &String {
208 &self.group
209 }
210 pub fn content(&self) -> &String {
211 &self.content
212 }
213 pub fn content_type(&self) -> &String {
214 &self.content_type
215 }
216 pub fn md5(&self) -> &String {
217 &self.md5
218 }
219}
220
221pub mod constants {
222 pub const KEY_PARAM_CONTENT_TYPE: &str = "type";
224
225 pub const KEY_PARAM_BETA_IPS: &str = "betaIps";
227
228 pub const KEY_PARAM_APP_NAME: &str = "appName";
230
231 pub const KEY_PARAM_TAG: &str = "tag";
233
234 pub(crate) const KEY_PARAM_ENCRYPTED_DATA_KEY: &str = "encryptedDataKey";
236}
237
238#[doc(alias("config", "builder"))]
253pub struct ConfigServiceBuilder {
254 client_props: props::ClientProps,
255 auth_plugin: Option<Arc<dyn plugin::AuthPlugin>>,
256 config_filters: Vec<Box<dyn plugin::ConfigFilter>>,
257}
258
259impl Default for ConfigServiceBuilder {
260 fn default() -> Self {
261 ConfigServiceBuilder {
262 client_props: props::ClientProps::new(),
263 auth_plugin: None,
264 config_filters: Vec::new(),
265 }
266 }
267}
268
269impl ConfigServiceBuilder {
270 pub fn new(client_props: props::ClientProps) -> Self {
271 ConfigServiceBuilder {
272 client_props,
273 auth_plugin: None,
274 config_filters: Vec::new(),
275 }
276 }
277
278 #[cfg(feature = "auth-by-http")]
279 pub fn enable_auth_plugin_http(self) -> Self {
280 self.with_auth_plugin(Arc::new(plugin::HttpLoginAuthPlugin::default()))
281 }
282
283 #[cfg(feature = "auth-by-aliyun")]
284 pub fn enable_auth_plugin_aliyun(self) -> Self {
285 self.with_auth_plugin(Arc::new(plugin::AliyunRamAuthPlugin::default()))
286 }
287
288 pub fn with_auth_plugin(mut self, auth_plugin: Arc<dyn plugin::AuthPlugin>) -> Self {
290 self.auth_plugin = Some(auth_plugin);
291 self
292 }
293
294 pub fn with_config_filters(
295 mut self,
296 config_filters: Vec<Box<dyn plugin::ConfigFilter>>,
297 ) -> Self {
298 self.config_filters = config_filters;
299 self
300 }
301
302 pub fn add_config_filter(mut self, config_filter: Box<dyn plugin::ConfigFilter>) -> Self {
303 self.config_filters.push(config_filter);
304 self
305 }
306
307 pub fn with_encryption_plugins(
309 self,
310 encryption_plugins: Vec<Box<dyn plugin::EncryptionPlugin>>,
311 ) -> Self {
312 self.add_config_filter(Box::new(plugin::ConfigEncryptionFilter::new(
313 encryption_plugins,
314 )))
315 }
316
317 pub fn build(self) -> error::Result<ConfigService> {
319 let auth_plugin = match self.auth_plugin {
320 None => Arc::new(plugin::NoopAuthPlugin::default()),
321 Some(plugin) => plugin,
322 };
323 let inner = crate::config::NacosConfigService::new(
324 self.client_props,
325 auth_plugin,
326 self.config_filters,
327 )?;
328 let inner = Arc::new(inner);
329 Ok(ConfigService { inner })
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use crate::api::config::ConfigServiceBuilder;
336 use crate::api::config::{ConfigChangeListener, ConfigResponse, ConfigService};
337 use std::collections::HashMap;
338 use std::time::Duration;
339 use tokio::time::sleep;
340
341 struct TestConfigChangeListener;
342
343 impl ConfigChangeListener for TestConfigChangeListener {
344 fn notify(&self, config_resp: ConfigResponse) {
345 tracing::info!("listen the config={}", config_resp);
346 }
347 }
348
349 #[tokio::test]
350 #[ignore]
351 async fn test_api_config_service() {
352 tracing_subscriber::fmt()
353 .with_max_level(tracing::Level::DEBUG)
354 .init();
355
356 let (data_id, group) = ("test_api_config_service".to_string(), "TEST".to_string());
357
358 let config_service = ConfigServiceBuilder::default().build().unwrap();
359
360 let _publish_resp = config_service
362 .publish_config(
363 data_id.clone(),
364 group.clone(),
365 "test_api_config_service".to_string(),
366 Some("text".to_string()),
367 )
368 .await
369 .unwrap();
370 sleep(Duration::from_millis(111)).await;
372
373 let config = config_service
374 .get_config(data_id.clone(), group.clone())
375 .await;
376 match config {
377 Ok(config) => tracing::info!("get the config {}", config),
378 Err(err) => tracing::error!("get the config {:?}", err),
379 }
380
381 let _listen = config_service
382 .add_listener(
383 data_id.clone(),
384 group.clone(),
385 std::sync::Arc::new(TestConfigChangeListener {}),
386 )
387 .await;
388 match _listen {
389 Ok(_) => tracing::info!("listening the config success"),
390 Err(err) => tracing::error!("listen config error {:?}", err),
391 }
392
393 let _publish_resp = config_service
395 .publish_config(
396 data_id.clone(),
397 group.clone(),
398 "test_api_config_service_for_listener".to_string(),
399 Some("text".to_string()),
400 )
401 .await
402 .unwrap();
403
404 let config_resp = config_service
406 .get_config("todo-data-id".to_string(), "todo-group".to_string())
407 .await;
408 match config_resp {
409 Ok(config_resp) => tracing::info!("get the config {}", config_resp),
410 Err(err) => tracing::error!("get the config {:?}", err),
411 }
412
413 let _listen = config_service
415 .add_listener(
416 "todo-data-id".to_string(),
417 "todo-group".to_string(),
418 std::sync::Arc::new(TestConfigChangeListener {}),
419 )
420 .await;
421 match _listen {
422 Ok(_) => tracing::info!("listening the config success"),
423 Err(err) => tracing::error!("listen config error {:?}", err),
424 }
425
426 sleep(Duration::from_millis(111)).await;
428 }
429
430 #[tokio::test]
431 #[ignore]
432 async fn test_api_config_service_remove_config() {
433 tracing_subscriber::fmt()
434 .with_max_level(tracing::Level::DEBUG)
435 .init();
436
437 let config_service = ConfigServiceBuilder::default().build().unwrap();
438
439 let remove_resp = config_service
441 .remove_config("todo-data-id".to_string(), "todo-group".to_string())
442 .await;
443 match remove_resp {
444 Ok(result) => tracing::info!("remove a config not exit: {}", result),
445 Err(err) => tracing::error!("remove a config not exit: {:?}", err),
446 }
447 }
448
449 #[tokio::test]
450 #[ignore]
451 async fn test_api_config_service_publish_config() {
452 tracing_subscriber::fmt()
453 .with_max_level(tracing::Level::DEBUG)
454 .init();
455
456 let config_service = ConfigServiceBuilder::default().build().unwrap();
457
458 let publish_resp = config_service
460 .publish_config(
461 "test_api_config_service_publish_config".to_string(),
462 "TEST".to_string(),
463 "test_api_config_service_publish_config".to_string(),
464 Some("text".to_string()),
465 )
466 .await
467 .unwrap();
468 tracing::info!("publish a config: {}", publish_resp);
469 assert_eq!(true, publish_resp);
470 }
471
472 #[tokio::test]
473 #[ignore]
474 async fn test_api_config_service_publish_config_param() {
475 tracing_subscriber::fmt()
476 .with_max_level(tracing::Level::DEBUG)
477 .init();
478
479 let config_service = ConfigServiceBuilder::default().build().unwrap();
480
481 let mut params = HashMap::new();
482 params.insert(
483 crate::api::config::constants::KEY_PARAM_APP_NAME.into(),
484 "test".into(),
485 );
486 let publish_resp = config_service
488 .publish_config_param(
489 "test_api_config_service_publish_config_param".to_string(),
490 "TEST".to_string(),
491 "test_api_config_service_publish_config_param".to_string(),
492 None,
493 None,
494 params,
495 )
496 .await
497 .unwrap();
498 tracing::info!("publish a config with param: {}", publish_resp);
499 assert_eq!(true, publish_resp);
500 }
501
502 #[tokio::test]
503 #[ignore]
504 async fn test_api_config_service_publish_config_beta() {
505 tracing_subscriber::fmt()
506 .with_max_level(tracing::Level::DEBUG)
507 .init();
508
509 let config_service = ConfigServiceBuilder::default().build().unwrap();
510
511 let publish_resp = config_service
513 .publish_config_beta(
514 "test_api_config_service_publish_config".to_string(),
515 "TEST".to_string(),
516 "test_api_config_service_publish_config_beta".to_string(),
517 None,
518 "127.0.0.1,192.168.0.1".to_string(),
519 )
520 .await
521 .unwrap();
522 tracing::info!("publish a config with beta: {}", publish_resp);
523 assert_eq!(true, publish_resp);
524 }
525
526 #[tokio::test]
527 #[ignore]
528 async fn test_api_config_service_publish_config_cas() {
529 tracing_subscriber::fmt()
530 .with_max_level(tracing::Level::DEBUG)
531 .init();
532
533 let config_service = ConfigServiceBuilder::default().build().unwrap();
534
535 let data_id = "test_api_config_service_publish_config_cas".to_string();
536 let group = "TEST".to_string();
537 let publish_resp = config_service
539 .publish_config(
540 data_id.clone(),
541 group.clone(),
542 "test_api_config_service_publish_config_cas".to_string(),
543 None,
544 )
545 .await
546 .unwrap();
547 assert_eq!(true, publish_resp);
548
549 sleep(Duration::from_millis(111)).await;
551
552 let config_resp = config_service
554 .get_config(data_id.clone(), group.clone())
555 .await
556 .unwrap();
557
558 let content_cas_md5 =
560 "test_api_config_service_publish_config_cas_md5_".to_string() + config_resp.md5();
561 let publish_resp = config_service
562 .publish_config_cas(
563 data_id.clone(),
564 group.clone(),
565 content_cas_md5.clone(),
566 None,
567 config_resp.md5().to_string(),
568 )
569 .await
570 .unwrap();
571 tracing::info!("publish a config with cas: {}", publish_resp);
572 assert_eq!(true, publish_resp);
573
574 let content_cas_md5_not_right = "test_api_config_service_publish_config_cas_md5_not_right";
576 let publish_resp = config_service
577 .publish_config_cas(
578 data_id.clone(),
579 group.clone(),
580 content_cas_md5_not_right.to_string(),
581 None,
582 config_resp.md5().to_string(),
583 )
584 .await;
585 match publish_resp {
586 Ok(result) => tracing::info!("publish a config with cas: {}", result),
587 Err(err) => tracing::error!("publish a config with cas: {:?}", err),
588 }
589 sleep(Duration::from_millis(111)).await;
590
591 let config_resp = config_service
592 .get_config(data_id.clone(), group.clone())
593 .await
594 .unwrap();
595 assert_ne!(content_cas_md5_not_right, config_resp.content().as_str());
596 assert_eq!(content_cas_md5.as_str(), config_resp.content().as_str());
597 }
598}