aperture_cli/config/
manager.rs1use crate::config::models::{ApiConfig, GlobalConfig};
2use crate::config::url_resolver::BaseUrlResolver;
3use crate::engine::loader;
4use crate::error::Error;
5use crate::fs::{FileSystem, OsFileSystem};
6use crate::spec::{SpecTransformer, SpecValidator};
7use openapiv3::OpenAPI;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12pub struct ConfigManager<F: FileSystem> {
13 fs: F,
14 config_dir: PathBuf,
15}
16
17impl ConfigManager<OsFileSystem> {
18 pub fn new() -> Result<Self, Error> {
24 let config_dir = get_config_dir()?;
25 Ok(Self {
26 fs: OsFileSystem,
27 config_dir,
28 })
29 }
30}
31
32impl<F: FileSystem> ConfigManager<F> {
33 pub const fn with_fs(fs: F, config_dir: PathBuf) -> Self {
34 Self { fs, config_dir }
35 }
36
37 pub fn add_spec(&self, name: &str, file_path: &Path, force: bool) -> Result<(), Error> {
51 let spec_path = self.config_dir.join("specs").join(format!("{name}.yaml"));
52 let cache_path = self.config_dir.join(".cache").join(format!("{name}.bin"));
53
54 if self.fs.exists(&spec_path) && !force {
55 return Err(Error::SpecAlreadyExists {
56 name: name.to_string(),
57 });
58 }
59
60 let content = self.fs.read_to_string(file_path)?;
61 let openapi_spec: OpenAPI = serde_yaml::from_str(&content)?;
62
63 let validator = SpecValidator::new();
65 validator.validate(&openapi_spec)?;
66
67 let transformer = SpecTransformer::new();
69 let cached_spec = transformer.transform(name, &openapi_spec);
70
71 self.fs.create_dir_all(spec_path.parent().unwrap())?;
73 self.fs.create_dir_all(cache_path.parent().unwrap())?;
74
75 self.fs.write_all(&spec_path, content.as_bytes())?;
77
78 let cached_data =
80 bincode::serialize(&cached_spec).map_err(|e| Error::SerializationError {
81 reason: e.to_string(),
82 })?;
83 self.fs.write_all(&cache_path, &cached_data)?;
84
85 Ok(())
86 }
87
88 pub fn list_specs(&self) -> Result<Vec<String>, Error> {
94 let specs_dir = self.config_dir.join("specs");
95 if !self.fs.exists(&specs_dir) {
96 return Ok(Vec::new());
97 }
98
99 let mut specs = Vec::new();
100 for entry in self.fs.read_dir(&specs_dir)? {
101 if self.fs.is_file(&entry) {
102 if let Some(file_name) = entry.file_name().and_then(|s| s.to_str()) {
103 if std::path::Path::new(file_name)
104 .extension()
105 .is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
106 {
107 specs.push(file_name.trim_end_matches(".yaml").to_string());
108 }
109 }
110 }
111 }
112 Ok(specs)
113 }
114
115 pub fn remove_spec(&self, name: &str) -> Result<(), Error> {
121 let spec_path = self.config_dir.join("specs").join(format!("{name}.yaml"));
122 let cache_path = self.config_dir.join(".cache").join(format!("{name}.bin"));
123
124 if !self.fs.exists(&spec_path) {
125 return Err(Error::SpecNotFound {
126 name: name.to_string(),
127 });
128 }
129
130 self.fs.remove_file(&spec_path)?;
131 if self.fs.exists(&cache_path) {
132 self.fs.remove_file(&cache_path)?;
133 }
134
135 Ok(())
136 }
137
138 pub fn edit_spec(&self, name: &str) -> Result<(), Error> {
147 let spec_path = self.config_dir.join("specs").join(format!("{name}.yaml"));
148
149 if !self.fs.exists(&spec_path) {
150 return Err(Error::SpecNotFound {
151 name: name.to_string(),
152 });
153 }
154
155 let editor = std::env::var("EDITOR").map_err(|_| Error::EditorNotSet)?;
156
157 Command::new(editor)
158 .arg(&spec_path)
159 .status()
160 .map_err(Error::Io)?
161 .success()
162 .then_some(()) .ok_or_else(|| Error::EditorFailed {
164 name: name.to_string(),
165 })
166 }
167
168 pub fn load_global_config(&self) -> Result<GlobalConfig, Error> {
174 let config_path = self.config_dir.join("config.toml");
175 if self.fs.exists(&config_path) {
176 let content = self.fs.read_to_string(&config_path)?;
177 toml::from_str(&content).map_err(|e| Error::InvalidConfig {
178 reason: e.to_string(),
179 })
180 } else {
181 Ok(GlobalConfig::default())
182 }
183 }
184
185 pub fn save_global_config(&self, config: &GlobalConfig) -> Result<(), Error> {
191 let config_path = self.config_dir.join("config.toml");
192
193 self.fs.create_dir_all(&self.config_dir)?;
195
196 let content = toml::to_string_pretty(config).map_err(|e| Error::SerializationError {
197 reason: format!("Failed to serialize config: {e}"),
198 })?;
199
200 self.fs.write_all(&config_path, content.as_bytes())?;
201 Ok(())
202 }
203
204 pub fn set_url(
215 &self,
216 api_name: &str,
217 url: &str,
218 environment: Option<&str>,
219 ) -> Result<(), Error> {
220 let spec_path = self
222 .config_dir
223 .join("specs")
224 .join(format!("{api_name}.yaml"));
225 if !self.fs.exists(&spec_path) {
226 return Err(Error::SpecNotFound {
227 name: api_name.to_string(),
228 });
229 }
230
231 let mut config = self.load_global_config()?;
233
234 let api_config = config
236 .api_configs
237 .entry(api_name.to_string())
238 .or_insert_with(|| ApiConfig {
239 base_url_override: None,
240 environment_urls: HashMap::new(),
241 });
242
243 if let Some(env) = environment {
245 api_config
246 .environment_urls
247 .insert(env.to_string(), url.to_string());
248 } else {
249 api_config.base_url_override = Some(url.to_string());
250 }
251
252 self.save_global_config(&config)?;
254 Ok(())
255 }
256
257 #[allow(clippy::type_complexity)]
269 pub fn get_url(
270 &self,
271 api_name: &str,
272 ) -> Result<(Option<String>, HashMap<String, String>, String), Error> {
273 let spec_path = self
275 .config_dir
276 .join("specs")
277 .join(format!("{api_name}.yaml"));
278 if !self.fs.exists(&spec_path) {
279 return Err(Error::SpecNotFound {
280 name: api_name.to_string(),
281 });
282 }
283
284 let cache_dir = self.config_dir.join(".cache");
286 let cached_spec = loader::load_cached_spec(&cache_dir, api_name).ok();
287
288 let config = self.load_global_config()?;
290
291 let api_config = config.api_configs.get(api_name);
293
294 let base_url_override = api_config.and_then(|c| c.base_url_override.clone());
295 let environment_urls = api_config
296 .map(|c| c.environment_urls.clone())
297 .unwrap_or_default();
298
299 let resolved_url = cached_spec.map_or_else(
301 || "https://api.example.com".to_string(),
302 |spec| {
303 let resolver = BaseUrlResolver::new(&spec);
304 let resolver = if api_config.is_some() {
305 resolver.with_global_config(&config)
306 } else {
307 resolver
308 };
309 resolver.resolve(None)
310 },
311 );
312
313 Ok((base_url_override, environment_urls, resolved_url))
314 }
315
316 #[allow(clippy::type_complexity)]
325 pub fn list_urls(
326 &self,
327 ) -> Result<HashMap<String, (Option<String>, HashMap<String, String>)>, Error> {
328 let config = self.load_global_config()?;
329
330 let mut result = HashMap::new();
331 for (api_name, api_config) in config.api_configs {
332 result.insert(
333 api_name,
334 (api_config.base_url_override, api_config.environment_urls),
335 );
336 }
337
338 Ok(result)
339 }
340}
341
342pub fn get_config_dir() -> Result<PathBuf, Error> {
348 let home_dir = dirs::home_dir().ok_or_else(|| Error::HomeDirectoryNotFound)?;
349 let config_dir = home_dir.join(".config").join("aperture");
350 Ok(config_dir)
351}