Skip to main content

cc2report/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Application configuration
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Config {
7    pub api: ApiConfig,
8    pub cache: CacheConfig,
9    pub output: OutputConfig,
10    pub processing: ProcessingConfig,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiConfig {
15    pub openai_api_key: Option<String>,
16    pub model: String,
17    pub temperature: f32,
18    pub max_retries: u32,
19    pub retry_delay_ms: u64,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CacheConfig {
24    pub enabled: bool,
25    pub cache_dir: PathBuf,
26    pub ttl_hours: u64,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct OutputConfig {
31    pub format: OutputFormat,
32    pub language: String,
33    pub show_token_usage: bool,
34    pub quiet: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum OutputFormat {
40    Markdown,
41    Json,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ProcessingConfig {
46    pub parallel_requests: usize,
47    pub log_dir: PathBuf,
48}
49
50impl Default for Config {
51    fn default() -> Self {
52        Self {
53            api: ApiConfig {
54                openai_api_key: None,
55                model: "gpt-4o".to_string(),
56                temperature: 0.3,
57                max_retries: 3,
58                retry_delay_ms: 1000,
59            },
60            cache: CacheConfig {
61                enabled: true,
62                cache_dir: default_cache_dir(),
63                ttl_hours: 24,
64            },
65            output: OutputConfig {
66                format: OutputFormat::Markdown,
67                language: detect_system_language(),
68                show_token_usage: false,
69                quiet: false,
70            },
71            processing: ProcessingConfig {
72                parallel_requests: 1,
73                log_dir: default_log_dir(),
74            },
75        }
76    }
77}
78
79impl Config {
80    /// Create config from command line arguments
81    pub fn from_args(matches: &clap::ArgMatches) -> Result<Self, Box<dyn std::error::Error>> {
82        let mut config = Self::default();
83
84        // API configuration
85        if let Some(key) = matches.get_one::<String>("api-key") {
86            config.api.openai_api_key = Some(key.clone());
87        } else if let Ok(key) = std::env::var("OPENAI_API_KEY") {
88            config.api.openai_api_key = Some(key);
89        }
90
91        if let Some(model) = matches.get_one::<String>("model") {
92            config.api.model = model.clone();
93        }
94
95        // Cache configuration is always enabled by default
96        // Users can clear cache with --clear-cache if needed
97
98        // Output configuration
99        if let Some(format) = matches.get_one::<String>("format") {
100            config.output.format = match format.as_str() {
101                "json" => OutputFormat::Json,
102                _ => OutputFormat::Markdown,
103            };
104        }
105
106        if let Some(lang) = matches.get_one::<String>("lang") {
107            config.output.language = lang.clone();
108        }
109
110        config.output.show_token_usage = matches.get_flag("show-token-usage");
111        config.output.quiet = matches.get_flag("quiet");
112
113        // Processing configuration
114        if let Some(parallel) = matches.get_one::<usize>("parallel") {
115            config.processing.parallel_requests = (*parallel).min(10);
116        }
117
118        if let Some(log_dir) = matches.get_one::<String>("log-dir") {
119            config.processing.log_dir = PathBuf::from(log_dir);
120        }
121
122        Ok(config)
123    }
124
125    /// Validate configuration
126    pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
127        if self.api.openai_api_key.is_none() {
128            return Err("OpenAI API key is required. Set OPENAI_API_KEY environment variable or use --api-key option.".into());
129        }
130
131        if !self.processing.log_dir.exists() {
132            return Err(format!(
133                "Log directory does not exist: {}",
134                self.processing.log_dir.display()
135            )
136            .into());
137        }
138
139        Ok(())
140    }
141}
142
143fn default_cache_dir() -> PathBuf {
144    if let Ok(home) = std::env::var("HOME") {
145        PathBuf::from(home).join(".cache").join("cc2report")
146    } else {
147        PathBuf::from(".cc2report-cache")
148    }
149}
150
151fn default_log_dir() -> PathBuf {
152    if let Ok(home) = std::env::var("HOME") {
153        PathBuf::from(home).join(".claude").join("projects")
154    } else {
155        PathBuf::from(".")
156    }
157}
158
159fn detect_system_language() -> String {
160    if let Ok(lang) = std::env::var("LANG") {
161        let lang_code = lang.split('.').next().unwrap_or(&lang);
162        let primary_lang = lang_code.split('_').next().unwrap_or(lang_code);
163
164        match primary_lang {
165            "ja" => "ja",
166            "zh" => "zh",
167            "ko" => "ko",
168            "es" => "es",
169            "fr" => "fr",
170            "de" => "de",
171            "pt" => "pt",
172            "ru" => "ru",
173            "it" => "it",
174            "nl" => "nl",
175            "pl" => "pl",
176            "tr" => "tr",
177            "ar" => "ar",
178            "hi" => "hi",
179            "th" => "th",
180            "vi" => "vi",
181            "id" => "id",
182            "ms" => "ms",
183            _ => "en",
184        }
185        .to_string()
186    } else {
187        "en".to_string()
188    }
189}