1use crate::error::{ConfigError, Result, ScopeError};
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50use std::path::{Path, PathBuf};
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
68#[serde(default)]
69pub struct Config {
70 pub chains: ChainsConfig,
72
73 pub output: OutputConfig,
75
76 #[serde(alias = "portfolio")]
78 pub address_book: AddressBookConfig,
79
80 pub monitor: MonitorConfig,
82
83 pub ghola: GholaConfig,
85
86 pub web: WebConfig,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
94#[serde(default)]
95pub struct ChainsConfig {
96 pub ethereum_rpc: Option<String>,
103
104 pub bsc_rpc: Option<String>,
108
109 #[doc(hidden)]
111 pub aegis_rpc: Option<String>,
112
113 pub solana_rpc: Option<String>,
120
121 pub tron_api: Option<String>,
125
126 pub api_keys: HashMap<String, String>,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(default)]
139pub struct OutputConfig {
140 pub format: OutputFormat,
142
143 pub color: bool,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
149#[serde(default)]
150pub struct AddressBookConfig {
151 pub data_dir: Option<PathBuf>,
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(default)]
167pub struct GholaConfig {
168 pub enabled: bool,
171
172 pub stealth: bool,
175
176 pub buffer_size: u32,
180}
181
182impl Default for GholaConfig {
183 fn default() -> Self {
184 Self {
185 enabled: false,
186 stealth: false,
187 buffer_size: 4096,
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
194#[serde(default)]
195pub struct WebConfig {
196 pub auth_token: Option<String>,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
204#[serde(rename_all = "lowercase")]
205pub enum OutputFormat {
206 #[default]
208 Table,
209
210 Json,
212
213 Csv,
215
216 #[value(name = "markdown")]
218 Markdown,
219}
220
221impl Default for OutputConfig {
222 fn default() -> Self {
223 Self {
224 format: OutputFormat::Table,
225 color: true,
226 }
227 }
228}
229
230impl Config {
231 pub fn load(path: Option<&Path>) -> Result<Self> {
260 let config_path = path
262 .map(PathBuf::from)
263 .or_else(|| std::env::var("SCOPE_CONFIG").ok().map(PathBuf::from))
264 .unwrap_or_else(Self::default_path);
265
266 if !config_path.exists() {
269 tracing::debug!(
270 path = %config_path.display(),
271 "No config file found, using defaults"
272 );
273 return Ok(Self::default());
274 }
275
276 tracing::debug!(path = %config_path.display(), "Loading configuration");
277
278 let contents = std::fs::read_to_string(&config_path).map_err(|e| {
279 ScopeError::Config(ConfigError::Read {
280 path: config_path.clone(),
281 source: e,
282 })
283 })?;
284
285 let config: Config =
286 serde_yaml::from_str(&contents).map_err(|e| ConfigError::Parse { source: e })?;
287
288 Ok(config)
289 }
290
291 pub fn default_path() -> PathBuf {
299 if let Some(home) = dirs::home_dir() {
301 let xdg_path = home.join(".config").join("scope").join("config.yaml");
302 if xdg_path.exists() {
303 return xdg_path;
304 }
305 }
306
307 if let Some(config_dir) = dirs::config_dir() {
309 let platform_path = config_dir.join("scope").join("config.yaml");
310 if platform_path.exists() {
311 return platform_path;
312 }
313 }
314
315 dirs::home_dir()
317 .map(|h| h.join(".config").join("scope").join("config.yaml"))
318 .unwrap_or_else(|| PathBuf::from(".").join("scope").join("config.yaml"))
319 }
320
321 pub fn config_path() -> Option<PathBuf> {
327 dirs::home_dir().map(|h| h.join(".config").join("scope").join("config.yaml"))
328 }
329
330 pub fn default_data_dir() -> PathBuf {
334 dirs::home_dir()
335 .map(|h| h.join(".config").join("scope").join("data"))
336 .unwrap_or_else(|| PathBuf::from(".config").join("scope").join("data"))
337 }
338
339 pub fn data_dir(&self) -> PathBuf {
341 self.address_book
342 .data_dir
343 .clone()
344 .unwrap_or_else(Self::default_data_dir)
345 }
346}
347
348impl std::fmt::Display for OutputFormat {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 match self {
351 OutputFormat::Table => write!(f, "table"),
352 OutputFormat::Json => write!(f, "json"),
353 OutputFormat::Csv => write!(f, "csv"),
354 OutputFormat::Markdown => write!(f, "markdown"),
355 }
356 }
357}
358
359pub const DEFAULT_MONITOR_REFRESH_SECS: u64 = 5;
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
375#[serde(rename_all = "kebab-case")]
376pub enum LayoutPreset {
377 #[default]
379 Dashboard,
380 ChartFocus,
382 Feed,
384 Compact,
386 Exchange,
388}
389
390impl LayoutPreset {
391 pub fn next(&self) -> Self {
393 match self {
394 LayoutPreset::Dashboard => LayoutPreset::ChartFocus,
395 LayoutPreset::ChartFocus => LayoutPreset::Feed,
396 LayoutPreset::Feed => LayoutPreset::Compact,
397 LayoutPreset::Compact => LayoutPreset::Exchange,
398 LayoutPreset::Exchange => LayoutPreset::Dashboard,
399 }
400 }
401
402 pub fn prev(&self) -> Self {
404 match self {
405 LayoutPreset::Dashboard => LayoutPreset::Exchange,
406 LayoutPreset::ChartFocus => LayoutPreset::Dashboard,
407 LayoutPreset::Feed => LayoutPreset::ChartFocus,
408 LayoutPreset::Compact => LayoutPreset::Feed,
409 LayoutPreset::Exchange => LayoutPreset::Compact,
410 }
411 }
412
413 pub fn label(&self) -> &'static str {
415 match self {
416 LayoutPreset::Dashboard => "Dashboard",
417 LayoutPreset::ChartFocus => "Chart",
418 LayoutPreset::Feed => "Feed",
419 LayoutPreset::Compact => "Compact",
420 LayoutPreset::Exchange => "Exchange",
421 }
422 }
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
430#[serde(default)]
431pub struct WidgetVisibility {
432 pub price_chart: bool,
434 pub volume_chart: bool,
436 pub buy_sell_pressure: bool,
438 pub metrics_panel: bool,
440 pub activity_log: bool,
442 pub holder_count: bool,
444 pub liquidity_depth: bool,
446}
447
448impl Default for WidgetVisibility {
449 fn default() -> Self {
450 Self {
451 price_chart: true,
452 volume_chart: true,
453 buy_sell_pressure: true,
454 metrics_panel: true,
455 activity_log: true,
456 holder_count: true,
457 liquidity_depth: true,
458 }
459 }
460}
461
462impl WidgetVisibility {
463 pub fn visible_count(&self) -> usize {
465 [
466 self.price_chart,
467 self.volume_chart,
468 self.buy_sell_pressure,
469 self.metrics_panel,
470 self.activity_log,
471 ]
472 .iter()
473 .filter(|&&v| v)
474 .count()
475 }
476
477 pub fn toggle_by_index(&mut self, index: usize) {
479 match index {
480 1 => self.price_chart = !self.price_chart,
481 2 => self.volume_chart = !self.volume_chart,
482 3 => self.buy_sell_pressure = !self.buy_sell_pressure,
483 4 => self.metrics_panel = !self.metrics_panel,
484 5 => self.activity_log = !self.activity_log,
485 _ => {}
486 }
487 }
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
492#[serde(rename_all = "kebab-case")]
493pub enum ScaleMode {
494 #[default]
496 Linear,
497 Log,
499}
500
501impl ScaleMode {
502 pub fn toggle(&self) -> Self {
504 match self {
505 ScaleMode::Linear => ScaleMode::Log,
506 ScaleMode::Log => ScaleMode::Linear,
507 }
508 }
509
510 pub fn label(&self) -> &'static str {
512 match self {
513 ScaleMode::Linear => "Lin",
514 ScaleMode::Log => "Log",
515 }
516 }
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
521#[serde(rename_all = "kebab-case")]
522pub enum ColorScheme {
523 #[default]
525 GreenRed,
526 BlueOrange,
528 Monochrome,
530}
531
532impl ColorScheme {
533 pub fn next(&self) -> Self {
535 match self {
536 ColorScheme::GreenRed => ColorScheme::BlueOrange,
537 ColorScheme::BlueOrange => ColorScheme::Monochrome,
538 ColorScheme::Monochrome => ColorScheme::GreenRed,
539 }
540 }
541
542 pub fn label(&self) -> &'static str {
544 match self {
545 ColorScheme::GreenRed => "G/R",
546 ColorScheme::BlueOrange => "B/O",
547 ColorScheme::Monochrome => "Mono",
548 }
549 }
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
554#[serde(default)]
555pub struct AlertConfig {
556 pub price_min: Option<f64>,
558 pub price_max: Option<f64>,
560 pub whale_min_usd: Option<f64>,
562 pub volume_spike_threshold_pct: Option<f64>,
564}
565
566impl Default for AlertConfig {
567 #[allow(clippy::derivable_impls)]
568 fn default() -> Self {
569 Self {
570 price_min: None,
571 price_max: None,
572 whale_min_usd: None,
573 volume_spike_threshold_pct: None,
574 }
575 }
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
580#[serde(default)]
581pub struct ExportConfig {
582 pub path: Option<String>,
584}
585
586impl Default for ExportConfig {
587 #[allow(clippy::derivable_impls)]
588 fn default() -> Self {
589 Self { path: None }
590 }
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
598#[serde(default)]
599pub struct MonitorConfig {
600 pub layout: LayoutPreset,
602 pub refresh_seconds: u64,
604 pub widgets: WidgetVisibility,
606 pub scale: ScaleMode,
608 pub color_scheme: ColorScheme,
610 pub alerts: AlertConfig,
612 pub export: ExportConfig,
614 pub auto_pause_on_input: bool,
616 #[serde(default)]
618 pub venue: Option<String>,
619}
620
621impl Default for MonitorConfig {
622 fn default() -> Self {
623 Self {
624 layout: LayoutPreset::Dashboard,
625 refresh_seconds: DEFAULT_MONITOR_REFRESH_SECS,
626 widgets: WidgetVisibility::default(),
627 scale: ScaleMode::Linear,
628 color_scheme: ColorScheme::GreenRed,
629 alerts: AlertConfig::default(),
630 export: ExportConfig::default(),
631 auto_pause_on_input: false,
632 venue: None,
633 }
634 }
635}
636
637#[cfg(test)]
642mod tests {
643 use super::*;
644 use std::io::Write;
645 use tempfile::NamedTempFile;
646
647 #[test]
648 fn test_default_config() {
649 let config = Config::default();
650
651 assert!(config.chains.api_keys.is_empty());
652 assert!(config.chains.ethereum_rpc.is_none());
653 assert!(config.chains.bsc_rpc.is_none());
654 assert!(config.chains.aegis_rpc.is_none());
655 assert!(config.chains.solana_rpc.is_none());
656 assert!(config.chains.tron_api.is_none());
657 assert_eq!(config.output.format, OutputFormat::Table);
658 assert!(config.output.color);
659 assert!(config.address_book.data_dir.is_none());
660 }
661
662 #[test]
663 fn test_load_from_yaml_full() {
664 let yaml = r#"
665chains:
666 ethereum_rpc: "https://example.com/rpc"
667 bsc_rpc: "https://bsc-dataseed.binance.org"
668 solana_rpc: "https://api.mainnet-beta.solana.com"
669 tron_api: "https://api.trongrid.io"
670 api_keys:
671 etherscan: "test-api-key"
672 polygonscan: "another-key"
673 bscscan: "bsc-key"
674 solscan: "sol-key"
675 tronscan: "tron-key"
676
677output:
678 format: json
679 color: false
680
681address_book:
682 data_dir: "/custom/data"
683"#;
684
685 let mut file = NamedTempFile::new().unwrap();
686 file.write_all(yaml.as_bytes()).unwrap();
687
688 let config = Config::load(Some(file.path())).unwrap();
689
690 assert_eq!(
692 config.chains.ethereum_rpc,
693 Some("https://example.com/rpc".into())
694 );
695 assert_eq!(
696 config.chains.bsc_rpc,
697 Some("https://bsc-dataseed.binance.org".into())
698 );
699
700 assert_eq!(
702 config.chains.solana_rpc,
703 Some("https://api.mainnet-beta.solana.com".into())
704 );
705 assert_eq!(
706 config.chains.tron_api,
707 Some("https://api.trongrid.io".into())
708 );
709
710 assert_eq!(
712 config.chains.api_keys.get("etherscan"),
713 Some(&"test-api-key".into())
714 );
715 assert_eq!(
716 config.chains.api_keys.get("polygonscan"),
717 Some(&"another-key".into())
718 );
719 assert_eq!(
720 config.chains.api_keys.get("bscscan"),
721 Some(&"bsc-key".into())
722 );
723 assert_eq!(
724 config.chains.api_keys.get("solscan"),
725 Some(&"sol-key".into())
726 );
727 assert_eq!(
728 config.chains.api_keys.get("tronscan"),
729 Some(&"tron-key".into())
730 );
731
732 assert_eq!(config.output.format, OutputFormat::Json);
733 assert!(!config.output.color);
734 assert_eq!(
735 config.address_book.data_dir,
736 Some(PathBuf::from("/custom/data"))
737 );
738 }
739
740 #[test]
741 fn test_load_partial_yaml_uses_defaults() {
742 let yaml = r#"
743chains:
744 ethereum_rpc: "https://partial.example.com"
745"#;
746
747 let mut file = NamedTempFile::new().unwrap();
748 file.write_all(yaml.as_bytes()).unwrap();
749
750 let config = Config::load(Some(file.path())).unwrap();
751
752 assert_eq!(
754 config.chains.ethereum_rpc,
755 Some("https://partial.example.com".into())
756 );
757
758 assert!(config.chains.api_keys.is_empty());
760 assert_eq!(config.output.format, OutputFormat::Table);
761 assert!(config.output.color);
762 }
763
764 #[test]
765 fn test_load_missing_file_returns_defaults() {
766 let config = Config::load(Some(Path::new("/nonexistent/path/config.yaml"))).unwrap();
767 assert_eq!(config, Config::default());
768 }
769
770 #[test]
771 fn test_load_invalid_yaml_returns_error() {
772 let mut file = NamedTempFile::new().unwrap();
773 file.write_all(b"invalid: yaml: : content: [").unwrap();
774
775 let result = Config::load(Some(file.path()));
776 assert!(result.is_err());
777 assert!(matches!(result.unwrap_err(), ScopeError::Config(_)));
778 }
779
780 #[test]
781 fn test_load_empty_file_returns_defaults() {
782 let file = NamedTempFile::new().unwrap();
783 let config = Config::load(Some(file.path())).unwrap();
786 assert_eq!(config, Config::default());
787 }
788
789 #[test]
790 fn test_output_format_serialization() {
791 let json_format = OutputFormat::Json;
792 let serialized = serde_yaml::to_string(&json_format).unwrap();
793 assert!(serialized.contains("json"));
794
795 let deserialized: OutputFormat = serde_yaml::from_str("csv").unwrap();
796 assert_eq!(deserialized, OutputFormat::Csv);
797 }
798
799 #[test]
800 fn test_output_format_display() {
801 assert_eq!(OutputFormat::Table.to_string(), "table");
802 assert_eq!(OutputFormat::Json.to_string(), "json");
803 assert_eq!(OutputFormat::Csv.to_string(), "csv");
804 assert_eq!(OutputFormat::Markdown.to_string(), "markdown");
805 }
806
807 #[test]
808 fn test_default_path_is_absolute_or_relative() {
809 let path = Config::default_path();
810 assert!(path.ends_with("scope/config.yaml") || path.ends_with("scope\\config.yaml"));
812 }
813
814 #[test]
815 fn test_default_data_dir() {
816 let data_dir = Config::default_data_dir();
817 assert!(data_dir.ends_with("scope") || data_dir.to_string_lossy().contains("scope"));
818 }
819
820 #[test]
821 fn test_data_dir_uses_config_value() {
822 let config = Config {
823 address_book: AddressBookConfig {
824 data_dir: Some(PathBuf::from("/custom/path")),
825 },
826 ..Default::default()
827 };
828
829 assert_eq!(config.data_dir(), PathBuf::from("/custom/path"));
830 }
831
832 #[test]
833 fn test_data_dir_falls_back_to_default() {
834 let config = Config::default();
835 assert_eq!(config.data_dir(), Config::default_data_dir());
836 }
837
838 #[test]
839 fn test_config_clone_and_eq() {
840 let config1 = Config::default();
841 let config2 = config1.clone();
842 assert_eq!(config1, config2);
843 }
844
845 #[test]
846 fn test_config_path_returns_some() {
847 let path = Config::config_path();
848 assert!(path.is_some());
850 assert!(path.unwrap().to_string_lossy().contains("scope"));
851 }
852
853 #[test]
854 fn test_config_debug() {
855 let config = Config::default();
856 let debug = format!("{:?}", config);
857 assert!(debug.contains("Config"));
858 assert!(debug.contains("ChainsConfig"));
859 }
860
861 #[test]
862 fn test_output_config_default() {
863 let output = OutputConfig::default();
864 assert_eq!(output.format, OutputFormat::Table);
865 assert!(output.color);
866 }
867
868 #[test]
869 fn test_config_serialization_roundtrip() {
870 let mut config = Config::default();
871 config
872 .chains
873 .api_keys
874 .insert("etherscan".to_string(), "test_key".to_string());
875 config.output.format = OutputFormat::Json;
876 config.output.color = false;
877 config.address_book.data_dir = Some(PathBuf::from("/custom"));
878
879 let yaml = serde_yaml::to_string(&config).unwrap();
880 let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
881 assert_eq!(config, deserialized);
882 }
883
884 #[test]
885 fn test_chains_config_with_multiple_api_keys() {
886 let mut api_keys = HashMap::new();
887 api_keys.insert("etherscan".into(), "key1".into());
888 api_keys.insert("polygonscan".into(), "key2".into());
889 api_keys.insert("bscscan".into(), "key3".into());
890
891 let chains = ChainsConfig {
892 ethereum_rpc: Some("https://rpc.example.com".into()),
893 api_keys,
894 ..Default::default()
895 };
896
897 assert_eq!(chains.api_keys.len(), 3);
898 assert!(chains.api_keys.contains_key("etherscan"));
899 }
900
901 #[test]
902 fn test_load_via_scope_config_env_var() {
903 let yaml = r#"
904chains:
905 ethereum_rpc: "https://env-test.example.com"
906output:
907 format: csv
908"#;
909 let mut file = NamedTempFile::new().unwrap();
910 file.write_all(yaml.as_bytes()).unwrap();
911
912 let path_str = file.path().to_string_lossy().to_string();
913 unsafe { std::env::set_var("SCOPE_CONFIG", &path_str) };
914
915 let config = Config::load(None).unwrap();
917 assert_eq!(
918 config.chains.ethereum_rpc,
919 Some("https://env-test.example.com".into())
920 );
921 assert_eq!(config.output.format, OutputFormat::Csv);
922
923 unsafe { std::env::remove_var("SCOPE_CONFIG") };
924 }
925
926 #[test]
927 fn test_output_format_default() {
928 let fmt = OutputFormat::default();
929 assert_eq!(fmt, OutputFormat::Table);
930 }
931
932 #[test]
933 fn test_address_book_config_default() {
934 let port = AddressBookConfig::default();
935 assert!(port.data_dir.is_none());
936 }
937
938 #[test]
939 fn test_chains_config_default() {
940 let chains = ChainsConfig::default();
941 assert!(chains.ethereum_rpc.is_none());
942 assert!(chains.bsc_rpc.is_none());
943 assert!(chains.aegis_rpc.is_none());
944 assert!(chains.solana_rpc.is_none());
945 assert!(chains.tron_api.is_none());
946 assert!(chains.api_keys.is_empty());
947 }
948
949 #[test]
950 fn test_load_unreadable_file_returns_error() {
951 let dir = tempfile::tempdir().unwrap();
953 let config_path = dir.path().join("config.yaml");
954 std::fs::create_dir_all(&config_path).unwrap();
956 let result = Config::load(Some(&config_path));
957 assert!(result.is_err());
958 }
959
960 #[test]
961 fn test_default_path_returns_valid_path() {
962 let path = Config::default_path();
963 assert!(path.to_str().unwrap().contains("scope"));
965 }
966
967 #[test]
972 fn test_ghola_config_default() {
973 let ghola = GholaConfig::default();
974 assert!(!ghola.enabled);
975 assert!(!ghola.stealth);
976 assert_eq!(ghola.buffer_size, 4096);
977 }
978
979 #[test]
980 fn test_config_default_ghola_disabled() {
981 let config = Config::default();
982 assert!(!config.ghola.enabled);
983 assert!(!config.ghola.stealth);
984 assert_eq!(config.ghola.buffer_size, 4096);
985 }
986
987 #[test]
988 fn test_load_ghola_enabled() {
989 let yaml = r#"
990ghola:
991 enabled: true
992 stealth: true
993 buffer_size: 8192
994"#;
995 let mut file = NamedTempFile::new().unwrap();
996 file.write_all(yaml.as_bytes()).unwrap();
997
998 let config = Config::load(Some(file.path())).unwrap();
999 assert!(config.ghola.enabled);
1000 assert!(config.ghola.stealth);
1001 assert_eq!(config.ghola.buffer_size, 8192);
1002 }
1003
1004 #[test]
1005 fn test_load_ghola_partial() {
1006 let yaml = r#"
1007ghola:
1008 enabled: true
1009"#;
1010 let mut file = NamedTempFile::new().unwrap();
1011 file.write_all(yaml.as_bytes()).unwrap();
1012
1013 let config = Config::load(Some(file.path())).unwrap();
1014 assert!(config.ghola.enabled);
1015 assert!(!config.ghola.stealth); assert_eq!(config.ghola.buffer_size, 4096); }
1018
1019 #[test]
1020 fn test_load_ghola_absent_uses_defaults() {
1021 let yaml = r#"
1022chains:
1023 ethereum_rpc: "https://example.com"
1024"#;
1025 let mut file = NamedTempFile::new().unwrap();
1026 file.write_all(yaml.as_bytes()).unwrap();
1027
1028 let config = Config::load(Some(file.path())).unwrap();
1029 assert!(!config.ghola.enabled);
1030 assert!(!config.ghola.stealth);
1031 assert_eq!(config.ghola.buffer_size, 4096);
1032 }
1033
1034 #[test]
1035 fn test_load_ghola_custom_buffer_size() {
1036 let yaml = r#"
1037ghola:
1038 enabled: false
1039 buffer_size: 16384
1040"#;
1041 let mut file = NamedTempFile::new().unwrap();
1042 file.write_all(yaml.as_bytes()).unwrap();
1043
1044 let config = Config::load(Some(file.path())).unwrap();
1045 assert!(!config.ghola.enabled);
1046 assert_eq!(config.ghola.buffer_size, 16384);
1047 }
1048
1049 #[test]
1050 fn test_ghola_config_serialization_roundtrip() {
1051 let mut config = Config::default();
1052 config.ghola.enabled = true;
1053 config.ghola.stealth = true;
1054 config.ghola.buffer_size = 8192;
1055
1056 let yaml = serde_yaml::to_string(&config).unwrap();
1057 let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
1058 assert_eq!(config.ghola, deserialized.ghola);
1059 }
1060}