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: Option<&str> = Deserialize::deserialize(deserializer)?;
227 match s {
228 Some("false") => Ok(false),
229 _ => Ok(true),
230 }
231}
232
233#[derive(Deserialize, Default, Debug)]
234struct ConfigManagerParams {
235 #[serde(default, deserialize_with = "bool_from_str")]
236 separation: bool,
237 #[serde(default)]
238 enable_history: bool,
239}
240
241pub fn new_file_config_manager(path: &str) -> Result<ConfigManager> {
242 let (file, query) = path.split_once('?').unwrap_or((path, ""));
243 let file = resolve_path(file);
244 let filepath = Path::new(&file);
245 let (mode, enable_history) = if filepath.is_dir() {
246 let params: ConfigManagerParams =
247 serde_qs::from_str(query).map_err(|e| Error::Invalid {
248 message: e.to_string(),
249 })?;
250 if params.separation {
251 (ConfigMode::MultiByItem, params.enable_history)
252 } else {
253 (ConfigMode::MultiByType, false)
254 }
255 } else {
256 (ConfigMode::Single, false)
257 };
258
259 let mut storage = FileStorage::new(&file)?;
260 if enable_history {
261 storage.with_history_path(&format!("{file}-history"))?;
262 }
263 Ok(ConfigManager::new(Arc::new(storage), mode))
264}
265
266pub fn new_etcd_config_manager(path: &str) -> Result<ConfigManager> {
267 let storage = EtcdStorage::new(path)?;
268 Ok(ConfigManager::new(
269 Arc::new(storage),
270 ConfigMode::MultiByItem,
271 ))
272}
273
274pub struct ConfigManager {
275 storage: Arc<dyn Storage>,
276 mode: ConfigMode,
277 current_config: ArcSwap<PingapConfig>,
278}
279
280impl ConfigManager {
281 pub fn new(storage: Arc<dyn Storage>, mode: ConfigMode) -> Self {
282 Self {
283 storage,
284 mode,
285 current_config: ArcSwap::from_pointee(PingapConfig::default()),
286 }
287 }
288 pub fn support_observer(&self) -> bool {
289 self.storage.support_observer()
290 }
291 pub async fn observe(&self) -> Result<Observer> {
292 self.storage.observe().await
293 }
294
295 pub fn get_current_config(&self) -> Arc<PingapConfig> {
296 self.current_config.load().clone()
297 }
298 pub fn set_current_config(&self, config: PingapConfig) {
299 self.current_config.store(Arc::new(config));
300 }
301
302 fn get_key(&self, category: &Category, name: &str) -> String {
304 match self.mode {
305 ConfigMode::Single => SINGLE_KEY.to_string(),
306 ConfigMode::MultiByType => {
307 format!("{}.toml", format_category(category))
308 },
309 ConfigMode::MultiByItem => {
310 if *category == Category::Basic {
311 format!("{}.toml", format_category(category))
312 } else {
313 format!("{}/{}.toml", format_category(category), name)
314 }
315 },
316 }
317 }
318
319 pub async fn load_all(&self) -> Result<PingapTomlConfig> {
320 let data = self.storage.fetch("").await?;
321 toml::from_str(&data).map_err(|e| Error::De { source: e })
322 }
323 pub async fn save_all(&self, config: &PingapTomlConfig) -> Result<()> {
324 match self.mode {
325 ConfigMode::Single => {
326 self.storage
327 .save(
328 &self.get_key(&Category::Basic, ""),
329 &to_string_pretty(config)?,
330 )
331 .await?;
332 },
333 ConfigMode::MultiByType => {
334 for category in [
335 Category::Basic,
336 Category::Server,
337 Category::Location,
338 Category::Upstream,
339 Category::Plugin,
340 Category::Certificate,
341 Category::Storage,
342 ]
343 .iter()
344 {
345 let value = config.get_category_toml(category)?;
346 self.storage
347 .save(&self.get_key(category, ""), &value)
348 .await?;
349 }
350 },
351 ConfigMode::MultiByItem => {
352 let basic_config = config.get_toml(&Category::Basic, "")?;
353 self.storage
354 .save(&self.get_key(&Category::Basic, ""), &basic_config)
355 .await?;
356
357 for (category, value) in [
358 (Category::Server, config.servers.clone()),
359 (Category::Location, config.locations.clone()),
360 (Category::Upstream, config.upstreams.clone()),
361 (Category::Plugin, config.plugins.clone()),
362 (Category::Certificate, config.certificates.clone()),
363 (Category::Storage, config.storages.clone()),
364 ] {
365 let Some(value) = value else {
366 continue;
367 };
368 for name in value.keys() {
369 let value = config.get_toml(&category, name)?;
370 self.storage
371 .save(&self.get_key(&category, name), &value)
372 .await?;
373 }
374 }
375 },
376 }
377
378 Ok(())
379 }
380 pub async fn update<T: Serialize + Send + Sync>(
381 &self,
382 category: Category,
383 name: &str,
384 value: &T,
385 ) -> Result<()> {
386 let key = self.get_key(&category, name);
387 let value = toml::to_string_pretty(value)
388 .map_err(|e| Error::Ser { source: e })?;
389 if self.mode == ConfigMode::MultiByItem {
391 let value: Value =
392 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
393 let value = format_item_toml_config(Some(value), &category, name)?;
394 return self.storage.save(&key, &value).await;
395 }
396 let mut config = self.load_all().await?;
398 let value: Value =
399 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
400 config.update(&category, name, value);
401 let value = if self.mode == ConfigMode::MultiByType {
403 config.get_category_toml(&category)?
404 } else {
405 to_string_pretty(&config)?
406 };
407
408 self.storage.save(&key, &value).await?;
409 Ok(())
410 }
411 pub async fn get<T: DeserializeOwned + Send>(
412 &self,
413 category: Category,
414 name: &str,
415 ) -> Result<Option<T>> {
416 let key = self.get_key(&category, name);
417 let data = self.storage.fetch(&key).await?;
418 let config: PingapTomlConfig =
419 toml::from_str(&data).map_err(|e| Error::De { source: e })?;
420
421 if let Some(value) = config.get(&category, name) {
422 let value = to_string_pretty(&value)?;
423 let value =
424 toml::from_str(&value).map_err(|e| Error::De { source: e })?;
425 Ok(Some(value))
426 } else {
427 Ok(None)
428 }
429 }
430 pub async fn delete(&self, category: Category, name: &str) -> Result<()> {
431 let key = self.get_key(&category, name);
432
433 let mut current_config = (*self.get_current_config()).clone();
434 current_config.remove(category.to_string().as_str(), name)?;
435
436 if self.mode == ConfigMode::MultiByItem {
437 return self.storage.delete(&key).await;
438 }
439 let mut config = self.load_all().await?;
440 config.delete(&category, name);
441 let value = if self.mode == ConfigMode::MultiByType {
442 config.get_category_toml(&category)?
443 } else {
444 to_string_pretty(&config)?
445 };
446 self.storage.save(&key, &value).await
447 }
448 pub fn support_history(&self) -> bool {
449 self.storage.support_history()
450 }
451 pub async fn history(
452 &self,
453 category: Category,
454 name: &str,
455 ) -> Result<Option<Vec<History>>> {
456 if !self.storage.support_history() {
457 return Ok(None);
458 }
459 let key = self.get_key(&category, name);
460 self.storage.fetch_history(&key).await
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use nanoid::nanoid;
468 use pretty_assertions::assert_eq;
469
470 fn new_pingap_config() -> PingapTomlConfig {
471 let basic_config = r#"auto_restart_check_interval = "1s"
472 name = "pingap"
473 pid_file = "/tmp/pingap.pid"
474 "#;
475
476 let server_config = r#"[server1]
477 addr = "127.0.0.1:8080"
478 locations = ["location1"]
479 threads = 1
480
481 [server2]
482 addr = "127.0.0.1:8081"
483 locations = ["location2"]
484 threads = 2
485 "#;
486
487 let upstream_config = r#"[upstream1]
488 addrs = ["127.0.0.1:7080"]
489
490 [upstream2]
491 addrs = ["127.0.0.1:7081"]
492 "#;
493
494 let location_config = r#"[location1]
495 upstream = "upstream1"
496
497 [location2]
498 upstream = "upstream2"
499 "#;
500
501 let plugin_config = r#"[plugin1]
502 value = "/plugin1"
503 category = "plugin1"
504
505 [plugin2]
506 value = "/plugin2"
507 category = "plugin2"
508 "#;
509
510 let certificate_config = r#"[certificate1]
511 cert = "/certificate1"
512 key = "/key1"
513
514 [certificate2]
515 cert = "/certificate2"
516 key = "/key2"
517 "#;
518 let storage_config = r#"[storage1]
519 value = "/storage1"
520 category = "storage1"
521
522 [storage2]
523 value = "/storage2"
524 category = "storage2"
525 "#;
526
527 PingapTomlConfig {
528 basic: Some(toml::from_str(basic_config).unwrap()),
529 servers: Some(toml::from_str(server_config).unwrap()),
530 upstreams: Some(toml::from_str(upstream_config).unwrap()),
531 locations: Some(toml::from_str(location_config).unwrap()),
532 plugins: Some(toml::from_str(plugin_config).unwrap()),
533 certificates: Some(toml::from_str(certificate_config).unwrap()),
534 storages: Some(toml::from_str(storage_config).unwrap()),
535 }
536 }
537
538 async fn test_config_manger(manager: ConfigManager, mode: ConfigMode) {
539 assert_eq!(true, mode == manager.mode);
540
541 let config = new_pingap_config();
542
543 manager.save_all(&config).await.unwrap();
544
545 let data = manager.storage.fetch("").await.unwrap();
547 let new_config = toml::from_str::<PingapTomlConfig>(&data).unwrap();
548
549 assert_eq!(toml::to_string(&config), toml::to_string(&new_config));
550
551 let current_config = manager.load_all().await.unwrap();
552 assert_eq!(
553 toml::to_string(&config).unwrap(),
554 toml::to_string(¤t_config).unwrap()
555 );
556
557 let value: Value =
560 manager.get(Category::Basic, "").await.unwrap().unwrap();
561 assert_eq!(
562 r#"auto_restart_check_interval = "1s"
563name = "pingap"
564pid_file = "/tmp/pingap.pid"
565"#,
566 toml::to_string(&value).unwrap()
567 );
568 let new_basic_config: Value = toml::from_str(
570 r#"auto_restart_check_interval = "2s"
571name = "pingap2"
572pid_file = "/tmp/pingap2.pid"
573"#,
574 )
575 .unwrap();
576 manager
577 .update(Category::Basic, "", &new_basic_config)
578 .await
579 .unwrap();
580 let value: Value =
582 manager.get(Category::Basic, "").await.unwrap().unwrap();
583 assert_eq!(
584 toml::to_string(&new_basic_config).unwrap(),
585 toml::to_string(&value).unwrap()
586 );
587 let value: Value = manager
592 .get(Category::Server, "server1")
593 .await
594 .unwrap()
595 .unwrap();
596 assert_eq!(
597 r#"addr = "127.0.0.1:8080"
598locations = ["location1"]
599threads = 1
600"#,
601 toml::to_string(&value).unwrap()
602 );
603 let new_server_config: Value = toml::from_str(
605 r#"addr = "192.186.1.1:8080"
606locations = ["location1"]
607threads = 1
608"#,
609 )
610 .unwrap();
611 manager
612 .update(Category::Server, "server2", &new_server_config)
613 .await
614 .unwrap();
615 let value: Value = manager
617 .get(Category::Server, "server2")
618 .await
619 .unwrap()
620 .unwrap();
621 assert_eq!(
622 toml::to_string(&new_server_config).unwrap(),
623 toml::to_string(&value).unwrap()
624 );
625 let value: Value = manager
630 .get(Category::Upstream, "upstream2")
631 .await
632 .unwrap()
633 .unwrap();
634 assert_eq!(
635 r#"addrs = ["127.0.0.1:7081"]
636"#,
637 toml::to_string(&value).unwrap()
638 );
639 let new_upstream_config: Value = toml::from_str(
641 r#"addrs = ["192.168.1.1:7081"]
642"#,
643 )
644 .unwrap();
645 manager
646 .update(Category::Upstream, "upstream2", &new_upstream_config)
647 .await
648 .unwrap();
649
650 let value: Value = manager
652 .get(Category::Upstream, "upstream2")
653 .await
654 .unwrap()
655 .unwrap();
656 assert_eq!(
657 toml::to_string(&new_upstream_config).unwrap(),
658 toml::to_string(&value).unwrap()
659 );
660
661 let value: Value = manager
666 .get(Category::Location, "location2")
667 .await
668 .unwrap()
669 .unwrap();
670 assert_eq!(
671 r#"upstream = "upstream2"
672"#,
673 toml::to_string(&value).unwrap()
674 );
675
676 let new_location_config: Value = toml::from_str(
678 r#"upstream = "upstream22"
679"#,
680 )
681 .unwrap();
682 manager
683 .update(Category::Location, "location2", &new_location_config)
684 .await
685 .unwrap();
686
687 let value: Value = manager
689 .get(Category::Location, "location2")
690 .await
691 .unwrap()
692 .unwrap();
693 assert_eq!(
694 toml::to_string(&new_location_config).unwrap(),
695 toml::to_string(&value).unwrap()
696 );
697
698 let value: Value = manager
703 .get(Category::Plugin, "plugin2")
704 .await
705 .unwrap()
706 .unwrap();
707 assert_eq!(
708 r#"category = "plugin2"
709value = "/plugin2"
710"#,
711 toml::to_string(&value).unwrap()
712 );
713
714 let new_plugin_config: Value = toml::from_str(
716 r#"category = "plugin22"
717value = "/plugin22"
718"#,
719 )
720 .unwrap();
721 manager
722 .update(Category::Plugin, "plugin2", &new_plugin_config)
723 .await
724 .unwrap();
725 let value: Value = manager
727 .get(Category::Plugin, "plugin2")
728 .await
729 .unwrap()
730 .unwrap();
731 assert_eq!(
732 toml::to_string(&new_plugin_config).unwrap(),
733 toml::to_string(&value).unwrap()
734 );
735
736 let value: Value = manager
741 .get(Category::Certificate, "certificate2")
742 .await
743 .unwrap()
744 .unwrap();
745 assert_eq!(
746 r#"cert = "/certificate2"
747key = "/key2"
748"#,
749 toml::to_string(&value).unwrap()
750 );
751
752 let new_certificate_config: Value = toml::from_str(
754 r#"cert = "/certificate22"
755key = "/key22"
756"#,
757 )
758 .unwrap();
759 manager
760 .update(
761 Category::Certificate,
762 "certificate2",
763 &new_certificate_config,
764 )
765 .await
766 .unwrap();
767 let value: Value = manager
769 .get(Category::Certificate, "certificate2")
770 .await
771 .unwrap()
772 .unwrap();
773 assert_eq!(
774 toml::to_string(&new_certificate_config).unwrap(),
775 toml::to_string(&value).unwrap()
776 );
777 let value: Value = manager
782 .get(Category::Storage, "storage2")
783 .await
784 .unwrap()
785 .unwrap();
786 assert_eq!(
787 r#"category = "storage2"
788value = "/storage2"
789"#,
790 toml::to_string(&value).unwrap()
791 );
792 let new_storage_config: Value = toml::from_str(
794 r#"category = "storage22"
795value = "/storage22"
796"#,
797 )
798 .unwrap();
799 manager
800 .update(Category::Storage, "storage2", &new_storage_config)
801 .await
802 .unwrap();
803 let value: Value = manager
805 .get(Category::Storage, "storage2")
806 .await
807 .unwrap()
808 .unwrap();
809 assert_eq!(
810 toml::to_string(&new_storage_config).unwrap(),
811 toml::to_string(&value).unwrap()
812 );
813 manager.delete(Category::Basic, "").await.unwrap();
819 let basic_config: Option<Value> =
820 manager.get(Category::Basic, "").await.unwrap();
821 assert_eq!(None, basic_config);
822
823 manager.delete(Category::Server, "server1").await.unwrap();
825 let server_config: Option<Value> =
826 manager.get(Category::Server, "server1").await.unwrap();
827 assert_eq!(None, server_config);
828
829 manager
831 .delete(Category::Location, "location1")
832 .await
833 .unwrap();
834 let location_config: Option<Value> =
835 manager.get(Category::Location, "location1").await.unwrap();
836 assert_eq!(None, location_config);
837
838 manager
840 .delete(Category::Upstream, "upstream1")
841 .await
842 .unwrap();
843 let upstream_config: Option<Value> =
844 manager.get(Category::Upstream, "upstream1").await.unwrap();
845 assert_eq!(None, upstream_config);
846
847 manager.delete(Category::Plugin, "plugin1").await.unwrap();
849 let plugin_config: Option<Value> =
850 manager.get(Category::Plugin, "plugin1").await.unwrap();
851 assert_eq!(None, plugin_config);
852
853 manager
855 .delete(Category::Certificate, "certificate1")
856 .await
857 .unwrap();
858 let certificate_config: Option<Value> = manager
859 .get(Category::Certificate, "certificate1")
860 .await
861 .unwrap();
862 assert_eq!(None, certificate_config);
863
864 manager.delete(Category::Storage, "storage1").await.unwrap();
866 let storage_config: Option<Value> =
867 manager.get(Category::Storage, "storage1").await.unwrap();
868 assert_eq!(None, storage_config);
869
870 let current_config = manager.load_all().await.unwrap();
871 assert_eq!(
872 r#"[servers.server2]
873addr = "192.186.1.1:8080"
874locations = ["location1"]
875threads = 1
876
877[upstreams.upstream2]
878addrs = ["192.168.1.1:7081"]
879
880[locations.location2]
881upstream = "upstream22"
882
883[plugins.plugin2]
884category = "plugin22"
885value = "/plugin22"
886
887[certificates.certificate2]
888cert = "/certificate22"
889key = "/key22"
890
891[storages.storage2]
892category = "storage22"
893value = "/storage22"
894"#,
895 toml::to_string(¤t_config).unwrap()
896 );
897
898 }
900
901 #[tokio::test]
902 async fn test_single_config_manger() {
903 let file = tempfile::NamedTempFile::with_suffix(".toml").unwrap();
904
905 let manager =
906 new_file_config_manager(&file.path().to_string_lossy()).unwrap();
907 test_config_manger(manager, ConfigMode::Single).await;
908 }
909
910 #[tokio::test]
911 async fn test_multi_by_type_config_manger() {
912 let file = tempfile::TempDir::new().unwrap();
913
914 let manager =
915 new_file_config_manager(&file.path().to_string_lossy()).unwrap();
916 test_config_manger(manager, ConfigMode::MultiByType).await;
917 }
918
919 #[tokio::test]
920 async fn test_multi_by_item_config_manger() {
921 let file = tempfile::TempDir::new().unwrap();
922
923 let manager = new_file_config_manager(&format!(
924 "{}?separation",
925 file.path().to_string_lossy()
926 ))
927 .unwrap();
928 test_config_manger(manager, ConfigMode::MultiByItem).await;
929 }
930
931 #[tokio::test]
932 async fn test_etcd_config_manger() {
933 let url = format!(
934 "etcd://127.0.0.1:2379/{}?timeout=10s&connect_timeout=5s",
935 nanoid!(16)
936 );
937 let manager = new_etcd_config_manager(&url).unwrap();
938 test_config_manger(manager, ConfigMode::MultiByItem).await;
939 }
940}