nms_copilot/mcp/
config.rs1use std::path::Path;
8
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize, Default)]
16#[serde(default)]
17pub struct McpConfig {
18 pub logging: LoggingConfig,
20}
21
22#[derive(Debug, Deserialize)]
24#[serde(default)]
25pub struct LoggingConfig {
26 pub level: String,
28 pub coloured: bool,
30 pub output: String,
32 pub report_caller: bool,
34}
35
36impl Default for LoggingConfig {
37 fn default() -> Self {
38 Self {
39 level: "info".into(),
40 coloured: true,
41 output: "stderr".into(),
42 report_caller: false,
43 }
44 }
45}
46
47impl LoggingConfig {
48 pub fn to_twyg_opts(&self) -> twyg::Opts {
50 let output = match self.output.as_str() {
51 "stdout" => twyg::Output::Stdout,
52 _ => twyg::Output::Stderr,
53 };
54 let level = match self.level.as_str() {
55 "trace" => twyg::LogLevel::Trace,
56 "debug" => twyg::LogLevel::Debug,
57 "warn" => twyg::LogLevel::Warn,
58 "error" => twyg::LogLevel::Error,
59 _ => twyg::LogLevel::Info,
60 };
61 let colors = twyg::Colors {
62 timestamp: Some(twyg::Color::hi_black()),
63 ..Default::default()
64 };
65 twyg::OptsBuilder::new()
66 .coloured(self.coloured)
67 .output(output)
68 .level(level)
69 .report_caller(self.report_caller)
70 .timestamp_format(twyg::TSFormat::Simple)
71 .colors(colors)
72 .build()
73 .unwrap_or_default()
74 }
75}
76
77impl McpConfig {
78 pub fn load() -> Self {
82 let path = dirs::home_dir()
83 .map(|h| h.join(".nms-copilot/config.toml"))
84 .unwrap_or_default();
85 Self::load_from(&path)
86 }
87
88 pub fn load_from(path: &Path) -> Self {
90 if !path.exists() {
91 return Self::default();
92 }
93 match std::fs::read_to_string(path) {
94 Ok(content) => toml::from_str(&content).unwrap_or_default(),
95 Err(_) => Self::default(),
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_default_config() {
106 let config = McpConfig::default();
107 assert_eq!(config.logging.level, "info");
108 assert!(config.logging.coloured);
109 assert_eq!(config.logging.output, "stderr");
110 assert!(!config.logging.report_caller);
111 }
112
113 #[test]
114 fn test_parse_logging_config() {
115 let toml = r#"
116 [logging]
117 level = "debug"
118 coloured = false
119 output = "stdout"
120 report_caller = true
121 "#;
122 let config: McpConfig = toml::from_str(toml).unwrap();
123 assert_eq!(config.logging.level, "debug");
124 assert!(!config.logging.coloured);
125 assert_eq!(config.logging.output, "stdout");
126 assert!(config.logging.report_caller);
127 }
128
129 #[test]
130 fn test_parse_empty_config() {
131 let config: McpConfig = toml::from_str("").unwrap();
132 assert_eq!(config.logging.level, "info");
133 }
134
135 #[test]
136 fn test_to_twyg_opts() {
137 let config = LoggingConfig::default();
138 let opts = config.to_twyg_opts();
139 assert!(!format!("{:?}", opts).is_empty());
141 }
142
143 #[test]
144 fn test_load_nonexistent_returns_default() {
145 let config = McpConfig::load_from(std::path::Path::new("/nonexistent/config.toml"));
146 assert_eq!(config.logging.level, "info");
147 }
148
149 #[test]
150 fn test_unknown_sections_ignored() {
151 let toml = r#"
152 [save]
153 path = "/tmp/save"
154
155 [display]
156 emoji_glyphs = false
157
158 [logging]
159 level = "warn"
160 "#;
161 let config: McpConfig = toml::from_str(toml).unwrap();
162 assert_eq!(config.logging.level, "warn");
163 }
164}