nacos_sdk/api/
config.rs

1use crate::api::{error, plugin, props};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5/// Api [`ConfigService`].
6///
7/// # Examples
8///
9/// ```ignore
10///  let mut config_service = nacos_sdk::api::config::ConfigServiceBuilder::new(
11///        nacos_sdk::api::props::ClientProps::new()
12///           .server_addr("127.0.0.1:8848")
13///           // Attention! "public" is "", it is recommended to customize the namespace with clear meaning.
14///           .namespace("")
15///           .app_name("todo-your-app-name"),
16///   )
17///   .build()?;
18/// ```
19#[doc(alias("config", "sdk", "api"))]
20#[derive(Clone, Debug)]
21pub struct ConfigService {
22    inner: Arc<crate::config::NacosConfigService>,
23}
24
25impl ConfigService {
26    /// Get config, return the content.
27    ///
28    /// Attention to [`error::Error::ConfigNotFound`], [`error::Error::ConfigQueryConflict`]
29    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    /// Publish config, return true/false.
40    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    /// Cas publish config with cas_md5 (prev content's md5), return true/false.
56    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    /// Beta publish config, return true/false.
74    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    /// Publish config with params (see keys [`constants::*`]), return true/false.
92    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    /// Remove config, return true/false.
110    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    /// Listen the config change.
117    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    /// Remove a Listener.
129    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
141/// The ConfigChangeListener receive notify of [`ConfigResponse`].
142pub trait ConfigChangeListener: Send + Sync {
143    fn notify(&self, config_resp: ConfigResponse);
144}
145
146/// ConfigResponse for api.
147#[derive(Debug, Clone)]
148pub struct ConfigResponse {
149    /// Namespace/Tenant
150    namespace: String,
151    /// DataId
152    data_id: String,
153    /// Group
154    group: String,
155    /// Content
156    content: String,
157    /// Content's Type; e.g. json,properties,xml,html,text,yaml
158    content_type: String,
159    /// Content's md5
160    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    /// param type, use for [`crate::api::config::ConfigService::publish_config_param`]
223    pub const KEY_PARAM_CONTENT_TYPE: &str = "type";
224
225    /// param betaIps, use for [`crate::api::config::ConfigService::publish_config_param`]
226    pub const KEY_PARAM_BETA_IPS: &str = "betaIps";
227
228    /// param appName, use for [`crate::api::config::ConfigService::publish_config_param`]
229    pub const KEY_PARAM_APP_NAME: &str = "appName";
230
231    /// param tag, use for [`crate::api::config::ConfigService::publish_config_param`]
232    pub const KEY_PARAM_TAG: &str = "tag";
233
234    /// param encryptedDataKey, use inner.
235    pub(crate) const KEY_PARAM_ENCRYPTED_DATA_KEY: &str = "encryptedDataKey";
236}
237
238/// Builder of api [`ConfigService`].
239///
240/// # Examples
241///
242/// ```ignore
243///  let mut config_service = nacos_sdk::api::config::ConfigServiceBuilder::new(
244///        nacos_sdk::api::props::ClientProps::new()
245///           .server_addr("127.0.0.1:8848")
246///           // Attention! "public" is "", it is recommended to customize the namespace with clear meaning.
247///           .namespace("")
248///           .app_name("todo-your-app-name"),
249///   )
250///   .build()?;
251/// ```
252#[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    /// Set [`plugin::AuthPlugin`]
289    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    /// Add [`plugin::EncryptionPlugin`], they will wrapper with [`plugin::ConfigEncryptionFilter`] into [`config_filters`]
308    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    /// Builds a new [`ConfigService`].
318    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        // publish a config
361        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 for config sync in server
371        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        // publish a config for listener
394        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        // example get a config not exit
405        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        // example add a listener with config not exit
414        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 for listener
427        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        // remove a config not exit
440        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        // publish a config
459        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        // publish a config with param
487        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        // publish a config with beta
512        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        // publish a config
538        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 for config sync in server
550        sleep(Duration::from_millis(111)).await;
551
552        // get a config
553        let config_resp = config_service
554            .get_config(data_id.clone(), group.clone())
555            .await
556            .unwrap();
557
558        // publish a config with cas
559        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        // publish a config with cas md5 not right
575        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}