1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4type ConfigMap = BTreeMap<String, Configuration>;
6type EnvMap = BTreeMap<String, String>;
8type JsonMap = BTreeMap<String, serde_json::Value>;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
13pub enum StorageMode {
14 #[serde(rename = "env")]
16 #[default]
17 Env,
18 #[serde(rename = "config")]
20 Config,
21}
22
23#[derive(Serialize, Deserialize, Default, Clone)]
32pub struct Configuration {
33 pub alias_name: String,
35 pub token: String,
37 pub url: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub model: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub small_fast_model: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub max_thinking_tokens: Option<u32>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub api_timeout_ms: Option<u32>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub claude_code_disable_nonessential_traffic: Option<u32>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub anthropic_default_sonnet_model: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub anthropic_default_opus_model: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub anthropic_default_haiku_model: Option<String>,
63}
64
65impl Configuration {
66 pub fn get_env_field_names() -> Vec<&'static str> {
72 vec![
73 "ANTHROPIC_AUTH_TOKEN",
74 "ANTHROPIC_BASE_URL",
75 "ANTHROPIC_MODEL",
76 "ANTHROPIC_SMALL_FAST_MODEL",
77 "ANTHROPIC_MAX_THINKING_TOKENS",
78 "API_TIMEOUT_MS",
79 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
80 "ANTHROPIC_DEFAULT_SONNET_MODEL",
81 "ANTHROPIC_DEFAULT_OPUS_MODEL",
82 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
83 ]
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_get_env_field_names() {
93 let fields = Configuration::get_env_field_names();
94
95 let expected_fields = vec![
97 "ANTHROPIC_AUTH_TOKEN",
98 "ANTHROPIC_BASE_URL",
99 "ANTHROPIC_MODEL",
100 "ANTHROPIC_SMALL_FAST_MODEL",
101 "ANTHROPIC_MAX_THINKING_TOKENS",
102 "API_TIMEOUT_MS",
103 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
104 "ANTHROPIC_DEFAULT_SONNET_MODEL",
105 "ANTHROPIC_DEFAULT_OPUS_MODEL",
106 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
107 ];
108
109 assert_eq!(
110 fields.len(),
111 expected_fields.len(),
112 "Should have exactly 10 fields"
113 );
114
115 for expected_field in expected_fields {
116 assert!(
117 fields.contains(&expected_field),
118 "Missing field: {}",
119 expected_field
120 );
121 }
122
123 for field in &fields {
125 assert_eq!(
126 field,
127 &field.to_uppercase(),
128 "Field {} should be uppercase",
129 field
130 );
131 }
132 }
133
134 #[test]
135 fn test_remove_anthropic_env_uses_dynamic_fields() {
136 let mut settings = ClaudeSettings::default();
137
138 let env_fields = Configuration::get_env_field_names();
140 for field in &env_fields {
141 settings
142 .env
143 .insert(field.to_string(), "test_value".to_string());
144 }
145
146 settings
148 .env
149 .insert("OTHER_VAR".to_string(), "other_value".to_string());
150 settings
151 .env
152 .insert("CLAUDE_THEME".to_string(), "dark".to_string());
153
154 settings.remove_anthropic_env();
156
157 for field in &env_fields {
159 assert!(
160 !settings.env.contains_key(*field),
161 "Field {} should be removed",
162 field
163 );
164 }
165
166 assert!(
168 settings.env.contains_key("OTHER_VAR"),
169 "Other variables should be preserved"
170 );
171 assert!(
172 settings.env.contains_key("CLAUDE_THEME"),
173 "Other variables should be preserved"
174 );
175 assert_eq!(
176 settings.env.get("OTHER_VAR"),
177 Some(&"other_value".to_string())
178 );
179 assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
180 }
181
182 #[test]
183 fn test_switch_to_config_uses_dynamic_fields() {
184 let mut settings = ClaudeSettings::default();
185
186 let env_fields = Configuration::get_env_field_names();
188 for field in &env_fields {
189 settings
190 .env
191 .insert(field.to_string(), "old_value".to_string());
192 }
193
194 let config = Configuration {
196 alias_name: "test".to_string(),
197 token: "new_token".to_string(),
198 url: "https://api.new.com".to_string(),
199 model: Some("new_model".to_string()),
200 small_fast_model: Some("new_fast_model".to_string()),
201 max_thinking_tokens: Some(50000),
202 api_timeout_ms: Some(300000),
203 claude_code_disable_nonessential_traffic: Some(1),
204 anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
205 anthropic_default_opus_model: Some("new_opus".to_string()),
206 anthropic_default_haiku_model: Some("new_haiku".to_string()),
207 };
208
209 settings.switch_to_config(&config);
211
212 assert_eq!(
214 settings.env.get("ANTHROPIC_AUTH_TOKEN"),
215 Some(&"new_token".to_string())
216 );
217 assert_eq!(
218 settings.env.get("ANTHROPIC_BASE_URL"),
219 Some(&"https://api.new.com".to_string())
220 );
221 assert_eq!(
222 settings.env.get("ANTHROPIC_MODEL"),
223 Some(&"new_model".to_string())
224 );
225 assert_eq!(
226 settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
227 Some(&"new_fast_model".to_string())
228 );
229
230 assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
232 assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
233 assert!(
234 !settings
235 .env
236 .contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
237 );
238 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
239 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
240 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
241 }
242}
243
244#[derive(Serialize, Deserialize, Default)]
249pub struct ConfigStorage {
250 pub configurations: ConfigMap,
252 pub claude_settings_dir: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub default_storage_mode: Option<StorageMode>,
257}
258
259#[derive(Default, Clone)]
264#[allow(dead_code)]
265pub struct ClaudeSettings {
266 pub env: EnvMap,
268 pub other: JsonMap,
270}
271
272impl Serialize for ClaudeSettings {
273 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
274 where
275 S: Serializer,
276 {
277 use serde::ser::SerializeMap;
278
279 let mut map = serializer.serialize_map(Some(
280 self.other.len() + if self.env.is_empty() { 0 } else { 1 },
281 ))?;
282
283 if !self.env.is_empty() {
285 map.serialize_entry("env", &self.env)?;
286 }
287
288 for (key, value) in &self.other {
290 map.serialize_entry(key, value)?;
291 }
292
293 map.end()
294 }
295}
296
297impl<'de> Deserialize<'de> for ClaudeSettings {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: Deserializer<'de>,
301 {
302 #[derive(Deserialize)]
303 struct ClaudeSettingsHelper {
304 #[serde(default)]
305 env: EnvMap,
306 #[serde(flatten)]
307 other: JsonMap,
308 }
309
310 let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
311 Ok(ClaudeSettings {
312 env: helper.env,
313 other: helper.other,
314 })
315 }
316}
317
318#[allow(dead_code)]
320pub struct AddCommandParams {
321 pub alias_name: String,
322 pub token: Option<String>,
323 pub url: Option<String>,
324 pub model: Option<String>,
325 pub small_fast_model: Option<String>,
326 pub max_thinking_tokens: Option<u32>,
327 pub api_timeout_ms: Option<u32>,
328 pub claude_code_disable_nonessential_traffic: Option<u32>,
329 pub anthropic_default_sonnet_model: Option<String>,
330 pub anthropic_default_opus_model: Option<String>,
331 pub anthropic_default_haiku_model: Option<String>,
332 pub force: bool,
333 pub interactive: bool,
334 pub token_arg: Option<String>,
335 pub url_arg: Option<String>,
336 pub from_file: Option<String>,
337}