1use crate::PingapConfig;
16use crate::convert_pingap_config;
17use crate::etcd_storage::EtcdStorage;
18use crate::file_storage::FileStorage;
19use crate::storage::{History, Storage};
20use crate::{Category, Error, Observer};
21use arc_swap::ArcSwap;
22use pingap_util::resolve_path;
23use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
24use std::path::Path;
25use std::sync::Arc;
26use toml::{Value, map::Map};
27
28type Result<T, E = Error> = std::result::Result<T, E>;
29
30pub fn format_category(category: &Category) -> &str {
31 match category {
32 Category::Basic => "basic",
33 Category::Server => "servers",
34 Category::Location => "locations",
35 Category::Upstream => "upstreams",
36 Category::Plugin => "plugins",
37 Category::Certificate => "certificates",
38 Category::Storage => "storages",
39 }
40}
41
42fn to_string_pretty<T>(value: &T) -> Result<String>
43where
44 T: serde::ser::Serialize + ?Sized,
45{
46 toml::to_string_pretty(value).map_err(|e| Error::Ser { source: e })
47}
48
49#[derive(Deserialize, Debug, Serialize)]
50pub struct PingapTomlConfig {
51 pub basic: Option<Value>,
52 pub servers: Option<Map<String, Value>>,
53 pub upstreams: Option<Map<String, Value>>,
54 pub locations: Option<Map<String, Value>>,
55 pub plugins: Option<Map<String, Value>>,
56 pub certificates: Option<Map<String, Value>>,
57 pub storages: Option<Map<String, Value>>,
58}
59
60fn format_item_toml_config(
61 value: Option<Value>,
62 category: &Category,
63 name: &str,
64) -> Result<String> {
65 if let Some(value) = value {
66 let mut wrapper = Map::new();
67 if name.is_empty() {
68 wrapper.insert(format_category(category).to_string(), value);
69 } else {
70 let mut inner = Map::new();
71 inner.insert(name.to_string(), value);
72 wrapper.insert(
73 format_category(category).to_string(),
74 Value::Table(inner),
75 );
76 }
77
78 to_string_pretty(&wrapper)
79 } else {
80 Ok("".to_string())
81 }
82}
83
84impl PingapTomlConfig {
85 pub fn to_toml(&self) -> Result<String> {
86 to_string_pretty(self)
87 }
88 pub fn to_pingap_config(
89 &self,
90 replace_include: bool,
91 ) -> Result<PingapConfig> {
92 let value = self.to_toml()?;
93 convert_pingap_config(value.as_bytes(), replace_include)
94 }
95
96 fn get_toml(&self, category: &Category, name: &str) -> Result<String> {
97 let value = self.get(category, name);
98 format_item_toml_config(value, category, name)
99 }
100 fn get_category_toml(&self, category: &Category) -> Result<String> {
101 let wrapper = |value: Option<Value>| {
102 if let Some(value) = value {
103 let mut wrapper = Map::new();
104 wrapper.insert(format_category(category).to_string(), value);
105 to_string_pretty(&wrapper)
106 } else {
107 Ok("".to_string())
108 }
109 };
110 match category {
111 Category::Basic => wrapper(self.basic.clone()),
112 Category::Server => wrapper(self.servers.clone().map(Value::Table)),
113 Category::Location => {
114 wrapper(self.locations.clone().map(Value::Table))
115 },
116 Category::Upstream => {
117 wrapper(self.upstreams.clone().map(Value::Table))
118 },
119 Category::Plugin => wrapper(self.plugins.clone().map(Value::Table)),
120 Category::Certificate => {
121 wrapper(self.certificates.clone().map(Value::Table))
122 },
123 Category::Storage => {
124 wrapper(self.storages.clone().map(Value::Table))
125 },
126 }
127 }
128 fn update(&mut self, category: &Category, name: &str, value: Value) {
129 let name = name.to_string();
130 match category {
131 Category::Basic => {
132 self.basic = Some(value);
133 },
134 Category::Server => {
135 self.servers.get_or_insert_default().insert(name, value);
136 },
137 Category::Location => {
138 self.locations.get_or_insert_default().insert(name, value);
139 },
140 Category::Upstream => {
141 self.upstreams.get_or_insert_default().insert(name, value);
142 },
143 Category::Plugin => {
144 self.plugins.get_or_insert_default().insert(name, value);
145 },
146 Category::Certificate => {
147 self.certificates
148 .get_or_insert_default()
149 .insert(name, value);
150 },
151 Category::Storage => {
152 self.storages.get_or_insert_default().insert(name, value);
153 },
154 };
155 }
156 fn get(&self, category: &Category, name: &str) -> Option<Value> {
157 match category {
158 Category::Basic => self.basic.clone(),
159 Category::Server => self
160 .servers
161 .as_ref()
162 .and_then(|servers| servers.get(name).cloned()),
163 Category::Location => self
164 .locations
165 .as_ref()
166 .and_then(|locations| locations.get(name).cloned()),
167 Category::Upstream => self
168 .upstreams
169 .as_ref()
170 .and_then(|upstreams| upstreams.get(name).cloned()),
171 Category::Plugin => self
172 .plugins
173 .as_ref()
174 .and_then(|plugins| plugins.get(name).cloned()),
175 Category::Certificate => self
176 .certificates
177 .as_ref()
178 .and_then(|certificates| certificates.get(name).cloned()),
179 Category::Storage => self
180 .storages
181 .as_ref()
182 .and_then(|storages| storages.get(name).cloned()),
183 }
184 }
185 fn delete(&mut self, category: &Category, name: &str) {
186 match category {
187 Category::Basic => self.basic = None,
188 Category::Server => {
189 self.servers.get_or_insert_default().remove(name);
190 },
191 Category::Location => {
192 self.locations.get_or_insert_default().remove(name);
193 },
194 Category::Upstream => {
195 self.upstreams.get_or_insert_default().remove(name);
196 },
197 Category::Plugin => {
198 self.plugins.get_or_insert_default().remove(name);
199 },
200 Category::Certificate => {
201 self.certificates.get_or_insert_default().remove(name);
202 },
203 Category::Storage => {
204 self.storages.get_or_insert_default().remove(name);
205 },
206 };
207 }
208}
209
210#[derive(PartialEq, Clone, Debug)]
211pub enum ConfigMode {
212 Single,
214 MultiByType,
216 MultiByItem,
218}
219
220static SINGLE_KEY: &str = "pingap.toml";
221
222fn bool_from_str<'de, D>(deserializer: D) -> Result<bool, D::Error>
223where
224 D: Deserializer<'de>,
225{
226 let s: &str = Deserialize::deserialize(deserializer)?;
227 match s {
229 "false" => Ok(false),
230 _ => Ok(true),
231 }
232}
233
234#[derive(Deserialize, Default, Debug)]
235struct ConfigManagerParams {
236 #[serde(default, deserialize_with = "bool_from_str")]
237 separation: bool,
238 #[serde(default)]
239 enable_history: bool,
240}
241
242pub fn new_file_config_manager(path: &str) -> Result<ConfigManager> {
243 let (file, query) = path.split_once('?').unwrap_or((path, ""));
244 let file = resolve_path(file);
245 let filepath = Path::new(&file);
246 let (mode, enable_history) = if filepath.is_dir() {
247 let params: ConfigManagerParams =
248 serde_qs::from_str(query).map_err(|e| Error::Invalid {
249 message: e.to_string(),
250 })?;
251 if params.separation {
252 (ConfigMode::MultiByItem, params.enable_history)
253 } else {
254 (ConfigMode::MultiByType, false)
255 }
256 } else {
257 (ConfigMode::Single, false)
258 };
259
260 let mut storage = FileStorage::new(&file)?;
261 if enable_history {
262 storage.with_history_path(&format!("{file}-history"))?;
263 }
264 Ok(ConfigManager::new(Arc::new(storage), mode))
265}
266
267pub fn new_etcd_config_manager(path: &str) -> Result<ConfigManager> {
268 let storage = EtcdStorage::new(path)?;
269 Ok(ConfigManager::new(
270 Arc::new(storage),
271 ConfigMode::MultiByItem,
272 ))
273}
274
275pub struct ConfigManager {
276 storage: Arc<dyn Storage>,
277 mode: ConfigMode,
278 current_config: ArcSwap<PingapConfig>,
279}
280
281impl ConfigManager {
282 pub fn new(storage: Arc<dyn Storage>, mode: ConfigMode) -> Self {
283 Self {
284 storage,
285 mode,
286 current_config: ArcSwap::from_pointee(PingapConfig::default()),
287 }
288 }
289 pub fn support_observer(&self) -> bool {
290 self.storage.support_observer()
291 }
292 pub async fn observe(&self) -> Result<Observer> {
293 self.storage.observe().await
294 }
295
296 pub fn get_current_config(&self) -> Arc<PingapConfig> {
297 self.current_config.load().clone()
298 }
299 pub fn set_current_config(&self, config: PingapConfig) {
300 self.current_config.store(Arc::new(config));
301 }
302
303 fn get_key(&self, category: &Category, name: &str) -> String {
305 match self.mode {
306 ConfigMode::Single => SINGLE_KEY.to_string(),
307 ConfigMode::MultiByType => {
308 format!("{}.toml", format_category(category))
309 },
310 ConfigMode::MultiByItem => {
311 if *category == Category::Basic {
312 format!("{}.toml", format_category(category))
313 } else {
314 format!("{}/{}.toml", format_category(category), name)
315 }
316 },
317 }
318 }
319
320 pub async fn load_all(&self) -> Result<PingapTomlConfig> {
321 let data = self.storage.fetch("").await?;
322 toml::from_str(&data).map_err(|e| Error::De { source: e })
323 }
324 pub async fn save_all(&self, config: &PingapTomlConfig) -> Result<()> {
325 match self.mode {
326 ConfigMode::Single => {
327 self.storage
328 .save(
329 &self.get_key(&Category::Basic, ""),
330 &to_string_pretty(config)?,
331 )
332 .await?;
333 },
334 ConfigMode::MultiByType => {
335 for category in [
336 Category::Basic,
337 Category::Server,
338 Category::Location,
339 Category::Upstream,
340 Category::Plugin,
341 Category::Certificate,
342 Category::Storage,
343 ]
344 .iter()
345 {
346 let value = config.get_category_toml(category)?;
347 self.storage
348 .save(&self.get_key(category, ""), &value)
349 .await?;
350 }
351 },
352 ConfigMode::MultiByItem => {
353 let basic_config = config.get_toml(&Category::Basic, "")?;
354 self.storage
355 .save(&self.get_key(&Category::Basic, ""), &basic_config)
356 .await?;
357
358 for (category, value) in [
359 (Category::Server, config.servers.clone()),
360 (Category::Location, config.locations.clone()),
361 (Category::Upstream, config.upstreams.clone()),
362 (Category::Plugin, config.plugins.clone()),
363 (Category::Certificate, config.certificates.clone()),
364 (Category::Storage, config.storages.clone()),
365 ] {
366 let Some(value) = value else {
367 continue;
368 };
369 for name in value.keys() {
370 let value = config.get_toml(&category, name)?;
371 self.storage
372 .save(&self.get_key(&category, name), &value)
373 .await?;
374 }
375 }
376 },
377 }
378
379 Ok(())
380 }
381 pub async fn update<T: Serialize + Send + Sync>(
382 &self,
383 category: Category,
384 name: &str,
385 value: &T,
386 ) -> Result<()> {
387 let key = self.get_key(&category, name);
388 let value = toml::to_string_pretty(value)
389 .map_err(|e| Error::Ser { source: e })?;
390 if self.mode == ConfigMode::MultiByItem {
392 let value: Value =
393 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
394 let value = format_item_toml_config(Some(value), &category, name)?;
395 return self.storage.save(&key, &value).await;
396 }
397 let mut config = self.load_all().await?;
399 let value: Value =
400 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
401 config.update(&category, name, value);
402 let value = if self.mode == ConfigMode::MultiByType {
404 config.get_category_toml(&category)?
405 } else {
406 to_string_pretty(&config)?
407 };
408
409 self.storage.save(&key, &value).await?;
410 Ok(())
411 }
412 pub async fn get<T: DeserializeOwned + Send>(
413 &self,
414 category: Category,
415 name: &str,
416 ) -> Result<Option<T>> {
417 let key = self.get_key(&category, name);
418 let data = self.storage.fetch(&key).await?;
419 let config: PingapTomlConfig =
420 toml::from_str(&data).map_err(|e| Error::De { source: e })?;
421
422 if let Some(value) = config.get(&category, name) {
423 let value = to_string_pretty(&value)?;
424 let value =
425 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
426 Ok(Some(value))
427 } else {
428 Ok(None)
429 }
430 }
431 pub async fn delete(&self, category: Category, name: &str) -> Result<()> {
432 let key = self.get_key(&category, name);
433
434 let mut current_config = (*self.get_current_config()).clone();
435 current_config.remove(category.to_string().as_str(), name)?;
436
437 if self.mode == ConfigMode::MultiByItem {
438 return self.storage.delete(&key).await;
439 }
440 let mut config = self.load_all().await?;
441 config.delete(&category, name);
442 let value = if self.mode == ConfigMode::MultiByType {
443 config.get_category_toml(&category)?
444 } else {
445 to_string_pretty(&config)?
446 };
447 self.storage.save(&key, &value).await
448 }
449 pub fn support_history(&self) -> bool {
450 self.storage.support_history()
451 }
452 pub async fn history(
453 &self,
454 category: Category,
455 name: &str,
456 ) -> Result<Option<Vec<History>>> {
457 if !self.storage.support_history() {
458 return Ok(None);
459 }
460 let key = self.get_key(&category, name);
461 self.storage.fetch_history(&key).await
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use nanoid::nanoid;
469 use pretty_assertions::assert_eq;
470
471 fn new_pingap_config() -> PingapTomlConfig {
472 let basic_config = r#"auto_restart_check_interval = "1s"
473 name = "pingap"
474 pid_file = "/tmp/pingap.pid"
475 "#;
476
477 let server_config = r#"[server1]
478 addr = "127.0.0.1:8080"
479 locations = ["location1"]
480 threads = 1
481
482 [server2]
483 addr = "127.0.0.1:8081"
484 locations = ["location2"]
485 threads = 2
486 "#;
487
488 let upstream_config = r#"[upstream1]
489 addrs = ["127.0.0.1:7080"]
490
491 [upstream2]
492 addrs = ["127.0.0.1:7081"]
493 "#;
494
495 let location_config = r#"[location1]
496 upstream = "upstream1"
497
498 [location2]
499 upstream = "upstream2"
500 "#;
501
502 let plugin_config = r#"[plugin1]
503 value = "/plugin1"
504 category = "plugin1"
505
506 [plugin2]
507 value = "/plugin2"
508 category = "plugin2"
509 "#;
510
511 let certificate_config = r#"[certificate1]
512 cert = "/certificate1"
513 key = "/key1"
514
515 [certificate2]
516 cert = "/certificate2"
517 key = "/key2"
518 "#;
519 let storage_config = r#"[storage1]
520 value = "/storage1"
521 category = "storage1"
522
523 [storage2]
524 value = "/storage2"
525 category = "storage2"
526 "#;
527
528 PingapTomlConfig {
529 basic: Some(toml::from_str(basic_config).unwrap()),
530 servers: Some(toml::from_str(server_config).unwrap()),
531 upstreams: Some(toml::from_str(upstream_config).unwrap()),
532 locations: Some(toml::from_str(location_config).unwrap()),
533 plugins: Some(toml::from_str(plugin_config).unwrap()),
534 certificates: Some(toml::from_str(certificate_config).unwrap()),
535 storages: Some(toml::from_str(storage_config).unwrap()),
536 }
537 }
538
539 async fn test_config_manger(manager: ConfigManager, mode: ConfigMode) {
540 assert_eq!(true, mode == manager.mode);
541
542 let config = new_pingap_config();
543
544 manager.save_all(&config).await.unwrap();
545
546 let data = manager.storage.fetch("").await.unwrap();
548 let new_config = toml::from_str::<PingapTomlConfig>(&data).unwrap();
549
550 assert_eq!(toml::to_string(&config), toml::to_string(&new_config));
551
552 let current_config = manager.load_all().await.unwrap();
553 assert_eq!(
554 toml::to_string(&config).unwrap(),
555 toml::to_string(¤t_config).unwrap()
556 );
557
558 let value: Value =
561 manager.get(Category::Basic, "").await.unwrap().unwrap();
562 assert_eq!(
563 r#"auto_restart_check_interval = "1s"
564name = "pingap"
565pid_file = "/tmp/pingap.pid"
566"#,
567 toml::to_string(&value).unwrap()
568 );
569 let new_basic_config: Value = toml::from_str(
571 r#"auto_restart_check_interval = "2s"
572name = "pingap2"
573pid_file = "/tmp/pingap2.pid"
574"#,
575 )
576 .unwrap();
577 manager
578 .update(Category::Basic, "", &new_basic_config)
579 .await
580 .unwrap();
581 let value: Value =
583 manager.get(Category::Basic, "").await.unwrap().unwrap();
584 assert_eq!(
585 toml::to_string(&new_basic_config).unwrap(),
586 toml::to_string(&value).unwrap()
587 );
588 let value: Value = manager
593 .get(Category::Server, "server1")
594 .await
595 .unwrap()
596 .unwrap();
597 assert_eq!(
598 r#"addr = "127.0.0.1:8080"
599locations = ["location1"]
600threads = 1
601"#,
602 toml::to_string(&value).unwrap()
603 );
604 let new_server_config: Value = toml::from_str(
606 r#"addr = "192.186.1.1:8080"
607locations = ["location1"]
608threads = 1
609"#,
610 )
611 .unwrap();
612 manager
613 .update(Category::Server, "server2", &new_server_config)
614 .await
615 .unwrap();
616 let value: Value = manager
618 .get(Category::Server, "server2")
619 .await
620 .unwrap()
621 .unwrap();
622 assert_eq!(
623 toml::to_string(&new_server_config).unwrap(),
624 toml::to_string(&value).unwrap()
625 );
626 let value: Value = manager
631 .get(Category::Upstream, "upstream2")
632 .await
633 .unwrap()
634 .unwrap();
635 assert_eq!(
636 r#"addrs = ["127.0.0.1:7081"]
637"#,
638 toml::to_string(&value).unwrap()
639 );
640 let new_upstream_config: Value = toml::from_str(
642 r#"addrs = ["192.168.1.1:7081"]
643"#,
644 )
645 .unwrap();
646 manager
647 .update(Category::Upstream, "upstream2", &new_upstream_config)
648 .await
649 .unwrap();
650
651 let value: Value = manager
653 .get(Category::Upstream, "upstream2")
654 .await
655 .unwrap()
656 .unwrap();
657 assert_eq!(
658 toml::to_string(&new_upstream_config).unwrap(),
659 toml::to_string(&value).unwrap()
660 );
661
662 let value: Value = manager
667 .get(Category::Location, "location2")
668 .await
669 .unwrap()
670 .unwrap();
671 assert_eq!(
672 r#"upstream = "upstream2"
673"#,
674 toml::to_string(&value).unwrap()
675 );
676
677 let new_location_config: Value = toml::from_str(
679 r#"upstream = "upstream22"
680"#,
681 )
682 .unwrap();
683 manager
684 .update(Category::Location, "location2", &new_location_config)
685 .await
686 .unwrap();
687
688 let value: Value = manager
690 .get(Category::Location, "location2")
691 .await
692 .unwrap()
693 .unwrap();
694 assert_eq!(
695 toml::to_string(&new_location_config).unwrap(),
696 toml::to_string(&value).unwrap()
697 );
698
699 let value: Value = manager
704 .get(Category::Plugin, "plugin2")
705 .await
706 .unwrap()
707 .unwrap();
708 assert_eq!(
709 r#"category = "plugin2"
710value = "/plugin2"
711"#,
712 toml::to_string(&value).unwrap()
713 );
714
715 let new_plugin_config: Value = toml::from_str(
717 r#"category = "plugin22"
718value = "/plugin22"
719"#,
720 )
721 .unwrap();
722 manager
723 .update(Category::Plugin, "plugin2", &new_plugin_config)
724 .await
725 .unwrap();
726 let value: Value = manager
728 .get(Category::Plugin, "plugin2")
729 .await
730 .unwrap()
731 .unwrap();
732 assert_eq!(
733 toml::to_string(&new_plugin_config).unwrap(),
734 toml::to_string(&value).unwrap()
735 );
736
737 let value: Value = manager
742 .get(Category::Certificate, "certificate2")
743 .await
744 .unwrap()
745 .unwrap();
746 assert_eq!(
747 r#"cert = "/certificate2"
748key = "/key2"
749"#,
750 toml::to_string(&value).unwrap()
751 );
752
753 let new_certificate_config: Value = toml::from_str(
755 r#"cert = "/certificate22"
756key = "/key22"
757"#,
758 )
759 .unwrap();
760 manager
761 .update(
762 Category::Certificate,
763 "certificate2",
764 &new_certificate_config,
765 )
766 .await
767 .unwrap();
768 let value: Value = manager
770 .get(Category::Certificate, "certificate2")
771 .await
772 .unwrap()
773 .unwrap();
774 assert_eq!(
775 toml::to_string(&new_certificate_config).unwrap(),
776 toml::to_string(&value).unwrap()
777 );
778 let value: Value = manager
783 .get(Category::Storage, "storage2")
784 .await
785 .unwrap()
786 .unwrap();
787 assert_eq!(
788 r#"category = "storage2"
789value = "/storage2"
790"#,
791 toml::to_string(&value).unwrap()
792 );
793 let new_storage_config: Value = toml::from_str(
795 r#"category = "storage22"
796value = "/storage22"
797"#,
798 )
799 .unwrap();
800 manager
801 .update(Category::Storage, "storage2", &new_storage_config)
802 .await
803 .unwrap();
804 let value: Value = manager
806 .get(Category::Storage, "storage2")
807 .await
808 .unwrap()
809 .unwrap();
810 assert_eq!(
811 toml::to_string(&new_storage_config).unwrap(),
812 toml::to_string(&value).unwrap()
813 );
814 manager.delete(Category::Basic, "").await.unwrap();
820 let basic_config: Option<Value> =
821 manager.get(Category::Basic, "").await.unwrap();
822 assert_eq!(None, basic_config);
823
824 manager.delete(Category::Server, "server1").await.unwrap();
826 let server_config: Option<Value> =
827 manager.get(Category::Server, "server1").await.unwrap();
828 assert_eq!(None, server_config);
829
830 manager
832 .delete(Category::Location, "location1")
833 .await
834 .unwrap();
835 let location_config: Option<Value> =
836 manager.get(Category::Location, "location1").await.unwrap();
837 assert_eq!(None, location_config);
838
839 manager
841 .delete(Category::Upstream, "upstream1")
842 .await
843 .unwrap();
844 let upstream_config: Option<Value> =
845 manager.get(Category::Upstream, "upstream1").await.unwrap();
846 assert_eq!(None, upstream_config);
847
848 manager.delete(Category::Plugin, "plugin1").await.unwrap();
850 let plugin_config: Option<Value> =
851 manager.get(Category::Plugin, "plugin1").await.unwrap();
852 assert_eq!(None, plugin_config);
853
854 manager
856 .delete(Category::Certificate, "certificate1")
857 .await
858 .unwrap();
859 let certificate_config: Option<Value> = manager
860 .get(Category::Certificate, "certificate1")
861 .await
862 .unwrap();
863 assert_eq!(None, certificate_config);
864
865 manager.delete(Category::Storage, "storage1").await.unwrap();
867 let storage_config: Option<Value> =
868 manager.get(Category::Storage, "storage1").await.unwrap();
869 assert_eq!(None, storage_config);
870
871 let current_config = manager.load_all().await.unwrap();
872 assert_eq!(
873 r#"[servers.server2]
874addr = "192.186.1.1:8080"
875locations = ["location1"]
876threads = 1
877
878[upstreams.upstream2]
879addrs = ["192.168.1.1:7081"]
880
881[locations.location2]
882upstream = "upstream22"
883
884[plugins.plugin2]
885category = "plugin22"
886value = "/plugin22"
887
888[certificates.certificate2]
889cert = "/certificate22"
890key = "/key22"
891
892[storages.storage2]
893category = "storage22"
894value = "/storage22"
895"#,
896 toml::to_string(¤t_config).unwrap()
897 );
898
899 }
901
902 #[tokio::test]
903 async fn test_single_config_manger() {
904 let file = tempfile::NamedTempFile::with_suffix(".toml").unwrap();
905
906 let manager =
907 new_file_config_manager(&file.path().to_string_lossy()).unwrap();
908 test_config_manger(manager, ConfigMode::Single).await;
909 }
910
911 #[tokio::test]
912 async fn test_multi_by_type_config_manger() {
913 let file = tempfile::TempDir::new().unwrap();
914
915 let manager =
916 new_file_config_manager(&file.path().to_string_lossy()).unwrap();
917 test_config_manger(manager, ConfigMode::MultiByType).await;
918 }
919
920 #[tokio::test]
921 async fn test_multi_by_item_config_manger() {
922 let file = tempfile::TempDir::new().unwrap();
923
924 let manager = new_file_config_manager(&format!(
925 "{}?separation",
926 file.path().to_string_lossy()
927 ))
928 .unwrap();
929 test_config_manger(manager, ConfigMode::MultiByItem).await;
930 }
931
932 #[tokio::test]
933 async fn test_etcd_config_manger() {
934 let url = format!(
935 "etcd://127.0.0.1:2379/{}?timeout=10s&connect_timeout=5s",
936 nanoid!(16)
937 );
938 let manager = new_etcd_config_manager(&url).unwrap();
939 test_config_manger(manager, ConfigMode::MultiByItem).await;
940 }
941}