carp_cli/config/
settings.rs1use crate::utils::error::{CarpError, CarpResult};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9 pub registry_url: String,
11 pub api_token: Option<String>,
13 pub timeout: u64,
15 pub verify_ssl: bool,
17 pub default_output_dir: Option<String>,
19}
20
21impl Default for Config {
22 fn default() -> Self {
23 Self {
24 registry_url: "https://api.carp.refcell.org".to_string(),
25 api_token: None,
26 timeout: 30,
27 verify_ssl: true,
28 default_output_dir: None,
29 }
30 }
31}
32
33pub struct ConfigManager;
35
36impl ConfigManager {
37 pub fn config_path() -> CarpResult<PathBuf> {
39 let config_dir = dirs::config_dir()
40 .ok_or_else(|| CarpError::Config("Unable to find config directory".to_string()))?;
41
42 let carp_dir = config_dir.join("carp");
43 if !carp_dir.exists() {
44 fs::create_dir_all(&carp_dir)?;
45 }
46
47 Ok(carp_dir.join("config.toml"))
48 }
49
50 pub fn load() -> CarpResult<Config> {
52 let config_path = Self::config_path()?;
53
54 if !config_path.exists() {
55 let default_config = Config::default();
56 Self::save(&default_config)?;
57 return Ok(default_config);
58 }
59
60 let contents = fs::read_to_string(&config_path)
61 .map_err(|e| CarpError::Config(format!("Failed to read config file: {}", e)))?;
62
63 let config: Config = toml::from_str(&contents)?;
64
65 Self::validate_registry_url(&config.registry_url)?;
67
68 if !config.registry_url.starts_with("https://") {
70 eprintln!("Warning: Registry URL is not using HTTPS. This is insecure.");
71 }
72
73 Ok(config)
74 }
75
76 fn validate_registry_url(url: &str) -> CarpResult<()> {
78 if url.is_empty() {
80 return Err(CarpError::Config("Registry URL cannot be empty".to_string()));
81 }
82
83 if !url.starts_with("http://") && !url.starts_with("https://") {
84 return Err(CarpError::Config(
85 "Registry URL must start with http:// or https://".to_string()
86 ));
87 }
88
89 if let Err(_) = url.parse::<reqwest::Url>() {
91 return Err(CarpError::Config(
92 "Invalid registry URL format".to_string()
93 ));
94 }
95
96 Ok(())
97 }
98
99 pub fn save(config: &Config) -> CarpResult<()> {
101 let config_path = Self::config_path()?;
102 let contents = toml::to_string_pretty(config)
103 .map_err(|e| CarpError::Config(format!("Failed to serialize config: {}", e)))?;
104
105 fs::write(&config_path, contents)
106 .map_err(|e| CarpError::Config(format!("Failed to write config file: {}", e)))?;
107
108 #[cfg(unix)]
110 {
111 use std::os::unix::fs::PermissionsExt;
112 let mut perms = fs::metadata(&config_path)?.permissions();
113 perms.set_mode(0o600);
114 fs::set_permissions(&config_path, perms)?;
115 }
116
117 Ok(())
118 }
119
120 pub fn set_api_token(token: String) -> CarpResult<()> {
122 let mut config = Self::load()?;
123 config.api_token = Some(token);
124 Self::save(&config)
125 }
126
127 pub fn clear_api_token() -> CarpResult<()> {
129 let mut config = Self::load()?;
130 config.api_token = None;
131 Self::save(&config)
132 }
133
134 pub fn cache_dir() -> CarpResult<PathBuf> {
136 let cache_dir = dirs::cache_dir()
137 .ok_or_else(|| CarpError::Config("Unable to find cache directory".to_string()))?;
138
139 let carp_cache = cache_dir.join("carp");
140 if !carp_cache.exists() {
141 fs::create_dir_all(&carp_cache)?;
142 }
143
144 Ok(carp_cache)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use tempfile::TempDir;
152
153 #[test]
154 fn test_default_config() {
155 let config = Config::default();
156 assert_eq!(config.registry_url, "https://api.carp.refcell.org");
157 assert!(config.api_token.is_none());
158 assert_eq!(config.timeout, 30);
159 assert!(config.verify_ssl);
160 }
161
162 #[test]
163 fn test_config_serialization() {
164 let config = Config::default();
165 let toml_str = toml::to_string(&config).unwrap();
166 let deserialized: Config = toml::from_str(&toml_str).unwrap();
167
168 assert_eq!(config.registry_url, deserialized.registry_url);
169 assert_eq!(config.timeout, deserialized.timeout);
170 }
171}