Skip to main content

cc_switch/config/
types.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4/// Type alias for configuration map
5type ConfigMap = BTreeMap<String, Configuration>;
6/// Type alias for environment variable map
7type EnvMap = BTreeMap<String, String>;
8/// Type alias for JSON value map
9type JsonMap = BTreeMap<String, serde_json::Value>;
10
11/// Storage mode for how configuration should be written to settings.json
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
13pub enum StorageMode {
14    /// Write to env field with uppercase environment variable names (default)
15    #[serde(rename = "env")]
16    #[default]
17    Env,
18    /// Write to root level with camelCase field names
19    #[serde(rename = "config")]
20    Config,
21}
22
23/// Represents a Claude API configuration
24///
25/// Contains the components needed to configure Claude API access:
26/// - alias_name: User-friendly identifier for the configuration
27/// - token: API authentication token
28/// - url: Base URL for the API endpoint
29/// - model: Optional custom model name
30/// - small_fast_model: Optional Haiku-class model for background tasks
31#[derive(Serialize, Deserialize, Default, Clone)]
32pub struct Configuration {
33    /// User-friendly alias name for this configuration
34    pub alias_name: String,
35    /// ANTHROPIC_AUTH_TOKEN value (API authentication token)
36    pub token: String,
37    /// ANTHROPIC_BASE_URL value (API endpoint URL)
38    pub url: String,
39    /// ANTHROPIC_MODEL value (custom model name)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub model: Option<String>,
42    /// ANTHROPIC_SMALL_FAST_MODEL value (Haiku-class model for background tasks)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub small_fast_model: Option<String>,
45    /// ANTHROPIC_MAX_THINKING_TOKENS value (Maximum thinking tokens limit)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub max_thinking_tokens: Option<u32>,
48    /// API timeout in milliseconds
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub api_timeout_ms: Option<u32>,
51    /// Disable non-essential traffic flag
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub claude_code_disable_nonessential_traffic: Option<u32>,
54    /// Default Sonnet model name
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub anthropic_default_sonnet_model: Option<String>,
57    /// Default Opus model name
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub anthropic_default_opus_model: Option<String>,
60    /// Default Haiku model name
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub anthropic_default_haiku_model: Option<String>,
63}
64
65impl Configuration {
66    /// Get all environment variable names that this configuration can set
67    ///
68    /// Returns a vector of all UPPERCASE environment variable names
69    /// that can be set by this configuration, used for conflict detection
70    /// in env mode.
71    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        // Verify all expected fields are present
96        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        // Verify all fields are uppercase
124        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        // Add all possible environment variables
139        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        // Add some other env variables that shouldn't be removed
147        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        // Remove Anthropic environment variables
155        settings.remove_anthropic_env();
156
157        // Verify all Anthropic fields are removed
158        for field in &env_fields {
159            assert!(
160                !settings.env.contains_key(*field),
161                "Field {} should be removed",
162                field
163            );
164        }
165
166        // Verify other fields are preserved
167        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        // Add all possible environment variables
187        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        // Create a test configuration
195        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        // Switch to new configuration
210        settings.switch_to_config(&config);
211
212        // Verify the required fields are set correctly
213        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        // Verify fields not set in the config are removed (not just left with old values)
231        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/// Storage manager for Claude API configurations
245///
246/// Handles persistence and retrieval of multiple API configurations
247/// stored in `~/.cc_auto_switch/configurations.json`
248#[derive(Serialize, Deserialize, Default)]
249pub struct ConfigStorage {
250    /// Map of alias names to configuration objects
251    pub configurations: ConfigMap,
252    /// Custom directory for Claude settings (optional)
253    pub claude_settings_dir: Option<String>,
254    /// Default storage mode for writing configurations (None = use env mode)
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub default_storage_mode: Option<StorageMode>,
257}
258
259/// Claude settings manager for API configuration
260///
261/// Manages the Claude settings.json file to control Claude's API configuration
262/// Handles environment variables and preserves other settings
263#[derive(Default, Clone)]
264#[allow(dead_code)]
265pub struct ClaudeSettings {
266    /// Environment variables map (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, ANTHROPIC_SMALL_FAST_MODEL)
267    pub env: EnvMap,
268    /// Other settings to preserve when modifying API configuration
269    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        // Serialize env field only if it has content
284        if !self.env.is_empty() {
285            map.serialize_entry("env", &self.env)?;
286        }
287
288        // Serialize other fields
289        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/// Parameters for adding a new configuration
319#[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}