1use crate::config::context_name::ApiContextName;
4use crate::config::manager::{get_config_dir, ConfigManager};
5use crate::config::models::SecretSource;
6use crate::constants;
7use crate::error::Error;
8use crate::fs::OsFileSystem;
9use crate::output::Output;
10use crate::response_cache::{CacheConfig, ResponseCache};
11use std::path::PathBuf;
12
13pub fn validate_api_name(name: &str) -> Result<ApiContextName, Error> {
15 ApiContextName::new(name)
16}
17
18#[allow(clippy::too_many_lines)]
20pub async fn execute_config_command(
21 manager: &ConfigManager<OsFileSystem>,
22 command: crate::cli::ConfigCommands,
23 output: &Output,
24) -> Result<(), Error> {
25 match command {
26 crate::cli::ConfigCommands::Add {
27 name,
28 file_or_url,
29 force,
30 strict,
31 } => {
32 let name = validate_api_name(&name)?;
33 manager
34 .add_spec_auto(&name, &file_or_url, force, strict)
35 .await?;
36 output.success(format!("Spec '{name}' added successfully."));
37 }
38 crate::cli::ConfigCommands::List { verbose } => {
39 let specs = manager.list_specs()?;
40 if specs.is_empty() {
41 output.info("No API specifications found.");
42 } else {
43 output.info("Registered API specifications:");
44 list_specs_with_details(manager, specs, verbose, output);
45 }
46 }
47 crate::cli::ConfigCommands::Remove { name } => {
48 let name = validate_api_name(&name)?;
49 manager.remove_spec(&name)?;
50 output.success(format!("Spec '{name}' removed successfully."));
51 }
52 crate::cli::ConfigCommands::Edit { name } => {
53 let name = validate_api_name(&name)?;
54 manager.edit_spec(&name)?;
55 output.success(format!("Opened spec '{name}' in editor."));
56 }
57 crate::cli::ConfigCommands::SetUrl { name, url, env } => {
58 let name = validate_api_name(&name)?;
59 manager.set_url(&name, &url, env.as_deref())?;
60 if let Some(environment) = env {
61 output.success(format!(
62 "Set base URL for '{name}' in environment '{environment}': {url}"
63 ));
64 } else {
65 output.success(format!("Set base URL for '{name}': {url}"));
66 }
67 }
68 crate::cli::ConfigCommands::GetUrl { name } => {
69 let name = validate_api_name(&name)?;
70 let (base_override, env_urls, resolved) = manager.get_url(&name)?;
71 print_url_configuration(
72 &name,
73 base_override.as_deref(),
74 &env_urls,
75 &resolved,
76 output,
77 );
78 }
79 crate::cli::ConfigCommands::ListUrls {} => {
80 let all_urls = manager.list_urls()?;
81 if all_urls.is_empty() {
82 output.info("No base URLs configured.");
83 return Ok(());
84 }
85 output.info("Configured base URLs:");
86 for (api_name, (base_override, env_urls)) in all_urls {
87 print_api_url_entry(&api_name, base_override.as_deref(), &env_urls, output);
88 }
89 }
90 crate::cli::ConfigCommands::Reinit { context, all } => {
91 if all {
92 reinit_all_specs(manager, output)?;
93 return Ok(());
94 }
95 let Some(spec_name) = context else {
96 eprintln!("Error: Either specify a spec name or use --all flag");
97 std::process::exit(1);
98 };
99 let spec_name = validate_api_name(&spec_name)?;
100 reinit_spec(manager, &spec_name, output)?;
101 }
102 crate::cli::ConfigCommands::ClearCache { api_name, all } => {
103 if let Some(ref name) = api_name {
104 validate_api_name(name)?;
105 }
106 clear_response_cache(manager, api_name.as_deref(), all, output).await?;
107 }
108 crate::cli::ConfigCommands::CacheStats { api_name } => {
109 if let Some(ref name) = api_name {
110 validate_api_name(name)?;
111 }
112 show_cache_stats(manager, api_name.as_deref(), output).await?;
113 }
114 crate::cli::ConfigCommands::SetSecret {
115 api_name,
116 scheme_name,
117 env,
118 interactive,
119 } => {
120 let api_name = validate_api_name(&api_name)?;
121 if interactive {
122 manager.set_secret_interactive(&api_name)?;
123 return Ok(());
124 }
125 let (Some(scheme), Some(env_var)) = (scheme_name, env) else {
126 return Err(Error::invalid_config(
127 "Either provide --scheme and --env, or use --interactive",
128 ));
129 };
130 manager.set_secret(&api_name, &scheme, &env_var)?;
131 output.success(format!(
132 "Set secret for scheme '{scheme}' in API '{api_name}' to use environment variable '{env_var}'"
133 ));
134 }
135 crate::cli::ConfigCommands::ListSecrets { api_name } => {
136 let api_name = validate_api_name(&api_name)?;
137 let secrets = manager.list_secrets(&api_name)?;
138 if secrets.is_empty() {
139 output.info(format!("No secrets configured for API '{api_name}'"));
140 } else {
141 print_secrets_list(&api_name, secrets, output);
142 }
143 }
144 crate::cli::ConfigCommands::RemoveSecret {
145 api_name,
146 scheme_name,
147 } => {
148 let api_name = validate_api_name(&api_name)?;
149 manager.remove_secret(&api_name, &scheme_name)?;
150 output.success(format!(
151 "Removed secret configuration for scheme '{scheme_name}' from API '{api_name}'"
152 ));
153 }
154 crate::cli::ConfigCommands::ClearSecrets { api_name, force } => {
155 let api_name = validate_api_name(&api_name)?;
156 let secrets = manager.list_secrets(&api_name)?;
157 if secrets.is_empty() {
158 output.info(format!("No secrets configured for API '{api_name}'"));
159 return Ok(());
160 }
161 if force {
162 manager.clear_secrets(&api_name)?;
163 output.success(format!(
164 "Cleared all secret configurations for API '{api_name}'"
165 ));
166 return Ok(());
167 }
168 output.info(format!(
169 "This will remove all {} secret configuration(s) for API '{api_name}':",
170 secrets.len()
171 ));
172 for scheme_name in secrets.keys() {
173 output.info(format!(" - {scheme_name}"));
174 }
175 if !crate::interactive::confirm("Are you sure you want to continue?")? {
176 output.info("Operation cancelled");
177 return Ok(());
178 }
179 manager.clear_secrets(&api_name)?;
180 output.success(format!(
181 "Cleared all secret configurations for API '{api_name}'"
182 ));
183 }
184 crate::cli::ConfigCommands::Set { key, value } => {
185 use crate::config::settings::{SettingKey, SettingValue};
186 let setting_key: SettingKey = key.parse()?;
187 let setting_value = SettingValue::parse_for_key(setting_key, &value)?;
188 manager.set_setting(&setting_key, &setting_value)?;
189 output.success(format!("Set {key} = {value}"));
190 }
191 crate::cli::ConfigCommands::Get { key, json } => {
192 use crate::config::settings::SettingKey;
193 let setting_key: SettingKey = key.parse()?;
194 let value = manager.get_setting(&setting_key)?;
195 if json {
196 println!(
198 "{}",
199 serde_json::json!({ "key": key, "value": value.to_string() })
200 );
201 } else {
202 println!("{value}");
204 }
205 }
206 crate::cli::ConfigCommands::Settings { json } => {
207 let settings = manager.list_settings()?;
208 print_settings_list(settings, json, output)?;
209 }
210 crate::cli::ConfigCommands::SetMapping {
211 api_name,
212 group,
213 operation,
214 name,
215 op_group,
216 alias,
217 remove_alias,
218 hidden,
219 visible,
220 } => {
221 let api_name = validate_api_name(&api_name)?;
222 handle_set_mapping(
223 manager,
224 &api_name,
225 group.as_deref(),
226 operation.as_deref(),
227 name.as_deref(),
228 op_group.as_deref(),
229 alias.as_deref(),
230 remove_alias.as_deref(),
231 hidden,
232 visible,
233 output,
234 )?;
235 }
236 crate::cli::ConfigCommands::ListMappings { api_name } => {
237 let api_name = validate_api_name(&api_name)?;
238 handle_list_mappings(manager, &api_name, output)?;
239 }
240 crate::cli::ConfigCommands::RemoveMapping {
241 api_name,
242 group,
243 operation,
244 } => {
245 let api_name = validate_api_name(&api_name)?;
246 handle_remove_mapping(manager, &api_name, group, operation, output)?;
247 }
248 }
249
250 Ok(())
251}
252
253pub fn print_secrets_list(
255 api_name: &str,
256 secrets: std::collections::HashMap<String, crate::config::models::ApertureSecret>,
257 output: &Output,
258) {
259 output.info(format!("Configured secrets for API '{api_name}':"));
260 for (scheme_name, secret) in secrets {
261 match secret.source {
262 SecretSource::Env => {
263 println!(" {scheme_name}: environment variable '{}'", secret.name);
265 }
266 }
267 }
268}
269
270pub fn print_api_url_entry(
272 api_name: &str,
273 base_override: Option<&str>,
274 env_urls: &std::collections::HashMap<String, String>,
275 output: &Output,
276) {
277 println!("\n{api_name}:");
279 if let Some(base) = base_override {
280 println!(" Base override: {base}");
282 }
283 if !env_urls.is_empty() {
284 output.info(" Environment URLs:");
285 for (env, url) in env_urls {
286 println!(" {env}: {url}");
288 }
289 }
290}
291
292pub fn print_url_configuration(
294 name: &str,
295 base_override: Option<&str>,
296 env_urls: &std::collections::HashMap<String, String>,
297 resolved: &str,
298 output: &Output,
299) {
300 output.info(format!("Base URL configuration for '{name}':"));
301 if let Some(base) = base_override {
302 println!(" Base override: {base}");
304 } else {
305 println!(" Base override: (none)");
307 }
308 if !env_urls.is_empty() {
309 println!(" Environment URLs:");
311 for (env, url) in env_urls {
312 println!(" {env}: {url}");
314 }
315 }
316 println!("\nResolved URL (current): {resolved}");
318 if let Ok(current_env) = std::env::var(constants::ENV_APERTURE_ENV) {
319 output.info(format!("(Using APERTURE_ENV={current_env})"));
320 }
321}
322
323pub fn reinit_spec(
324 manager: &ConfigManager<OsFileSystem>,
325 spec_name: &ApiContextName,
326 output: &Output,
327) -> Result<(), Error> {
328 output.info(format!("Reinitializing cached specification: {spec_name}"));
329 let specs = manager.list_specs()?;
330 if !specs.contains(&spec_name.to_string()) {
331 return Err(Error::spec_not_found(spec_name.as_str()));
332 }
333 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
334 PathBuf::from(dir)
335 } else {
336 get_config_dir()?
337 };
338 let specs_dir = config_dir.join(constants::DIR_SPECS);
339 let spec_path = specs_dir.join(format!("{spec_name}.yaml"));
340 let strict = manager.get_strict_preference(spec_name).unwrap_or(false);
341 manager.add_spec(spec_name, &spec_path, true, strict)?;
342 output.success(format!(
343 "Successfully reinitialized cache for '{spec_name}'"
344 ));
345 Ok(())
346}
347
348pub fn reinit_all_specs(
349 manager: &ConfigManager<OsFileSystem>,
350 output: &Output,
351) -> Result<(), Error> {
352 let specs = manager.list_specs()?;
353 if specs.is_empty() {
354 output.info("No API specifications found to reinitialize.");
355 return Ok(());
356 }
357 output.info(format!(
358 "Reinitializing {} cached specification(s)...",
359 specs.len()
360 ));
361 for spec_name in &specs {
362 let validated = match validate_api_name(spec_name) {
363 Ok(v) => v,
364 Err(e) => {
365 eprintln!(" {spec_name}: {e}");
366 continue;
367 }
368 };
369 match reinit_spec(manager, &validated, output) {
370 Ok(()) => output.info(format!(" {spec_name}")),
371 Err(e) => eprintln!(" {spec_name}: {e}"),
372 }
373 }
374 output.success("Reinitialization complete.");
375 Ok(())
376}
377
378pub fn list_specs_with_details(
379 manager: &ConfigManager<OsFileSystem>,
380 specs: Vec<String>,
381 verbose: bool,
382 output: &Output,
383) {
384 let cache_dir = manager.config_dir().join(constants::DIR_CACHE);
385 for spec_name in specs {
386 if !verbose {
387 println!("- {spec_name}");
389 continue;
390 }
391 let Ok(cached_spec) = crate::engine::loader::load_cached_spec(&cache_dir, &spec_name)
392 else {
393 println!("- {spec_name}");
395 continue;
396 };
397 println!("- {spec_name}:");
399 output.info(format!(" Version: {}", cached_spec.version));
400 let available = cached_spec.commands.len();
401 let skipped = cached_spec.skipped_endpoints.len();
402 let total = available + skipped;
403 if skipped > 0 {
404 output.info(format!(
405 " Endpoints: {available} of {total} available ({skipped} skipped)"
406 ));
407 display_skipped_endpoints_info(&cached_spec, output);
408 } else {
409 output.info(format!(" Endpoints: {available} available"));
410 }
411 }
412}
413
414fn display_skipped_endpoints_info(cached_spec: &crate::cache::models::CachedSpec, output: &Output) {
415 output.info(" Skipped endpoints:");
416 for endpoint in &cached_spec.skipped_endpoints {
417 output.info(format!(
418 " - {} {} - {} not supported",
419 endpoint.method, endpoint.path, endpoint.content_type
420 ));
421 }
422}
423
424pub fn print_settings_list(
425 settings: Vec<crate::config::settings::SettingInfo>,
426 json: bool,
427 output: &Output,
428) -> Result<(), Error> {
429 if json {
430 println!("{}", serde_json::to_string_pretty(&settings)?);
432 return Ok(());
433 }
434 output.info("Available configuration settings:");
435 println!();
437 for setting in settings {
438 println!(" {} = {}", setting.key, setting.value);
440 println!(
442 " Type: {} Default: {}",
443 setting.type_name, setting.default
444 );
445 println!(" {}", setting.description);
447 println!();
449 }
450 Ok(())
451}
452
453pub async fn clear_response_cache(
455 _manager: &ConfigManager<OsFileSystem>,
456 api_name: Option<&str>,
457 all: bool,
458 output: &Output,
459) -> Result<(), Error> {
460 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
461 PathBuf::from(dir)
462 } else {
463 get_config_dir()?
464 };
465 let cache_config = CacheConfig {
466 cache_dir: config_dir
467 .join(constants::DIR_CACHE)
468 .join(constants::DIR_RESPONSES),
469 ..Default::default()
470 };
471 let cache = ResponseCache::new(cache_config)?;
472 let cleared_count = if all {
473 cache.clear_all().await?
474 } else {
475 let Some(api) = api_name else {
476 eprintln!("Error: Either specify an API name or use --all flag");
477 std::process::exit(1);
478 };
479 cache.clear_api_cache(api).await?
480 };
481 if all {
482 output.success(format!(
483 "Cleared {cleared_count} cached responses for all APIs"
484 ));
485 } else {
486 let Some(api) = api_name else {
487 unreachable!("API name must be Some if not all");
488 };
489 output.success(format!(
490 "Cleared {cleared_count} cached responses for API '{api}'"
491 ));
492 }
493 Ok(())
494}
495
496pub async fn show_cache_stats(
498 _manager: &ConfigManager<OsFileSystem>,
499 api_name: Option<&str>,
500 output: &Output,
501) -> Result<(), Error> {
502 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
503 PathBuf::from(dir)
504 } else {
505 get_config_dir()?
506 };
507 let cache_config = CacheConfig {
508 cache_dir: config_dir
509 .join(constants::DIR_CACHE)
510 .join(constants::DIR_RESPONSES),
511 ..Default::default()
512 };
513 let cache = ResponseCache::new(cache_config)?;
514 let stats = cache.get_stats(api_name).await?;
515 if let Some(api) = api_name {
516 output.info(format!("Cache statistics for API '{api}':"));
517 } else {
518 output.info("Cache statistics for all APIs:");
519 }
520 println!(" Total entries: {}", stats.total_entries);
522 println!(" Valid entries: {}", stats.valid_entries);
524 println!(" Expired entries: {}", stats.expired_entries);
526 #[allow(clippy::cast_precision_loss)]
527 let size_mb = stats.total_size_bytes as f64 / 1024.0 / 1024.0;
528 println!(" Total size: {size_mb:.2} MB");
530 if stats.total_entries != 0 {
531 #[allow(clippy::cast_precision_loss)]
532 let hit_rate = stats.valid_entries as f64 / stats.total_entries as f64 * 100.0;
533 println!(" Hit rate: {hit_rate:.1}%");
535 }
536 Ok(())
537}
538
539#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
543pub fn handle_set_mapping(
544 manager: &ConfigManager<OsFileSystem>,
545 api_name: &crate::config::context_name::ApiContextName,
546 group: Option<&[String]>,
547 operation: Option<&str>,
548 name: Option<&str>,
549 op_group: Option<&str>,
550 alias: Option<&str>,
551 remove_alias: Option<&str>,
552 hidden: bool,
553 visible: bool,
554 output: &Output,
555) -> Result<(), Error> {
556 if let Some([original, new_name, ..]) = group {
558 manager.set_group_mapping(api_name, original, new_name)?;
559 output.success(format!(
560 "Set group mapping for '{api_name}': '{original}' → '{new_name}'"
561 ));
562 output.info("Run 'aperture config reinit' to apply changes.");
563 return Ok(());
564 }
565
566 let Some(op_id) = operation else {
568 return Err(Error::invalid_config(
569 "Either --group or --operation must be specified",
570 ));
571 };
572
573 let hidden_flag = match (hidden, visible) {
574 (true, _) => Some(true),
575 (_, true) => Some(false),
576 _ => None,
577 };
578
579 manager.set_operation_mapping(api_name, op_id, name, op_group, alias, hidden_flag)?;
580
581 if let Some(alias_to_remove) = remove_alias {
583 manager.remove_alias(api_name, op_id, alias_to_remove)?;
584 }
585
586 let mut changes = Vec::new();
588 if let Some(n) = name {
589 changes.push(format!("name='{n}'"));
590 }
591 if let Some(g) = op_group {
592 changes.push(format!("group='{g}'"));
593 }
594 if let Some(a) = alias {
595 changes.push(format!("alias+='{a}'"));
596 }
597 if let Some(a) = remove_alias {
598 changes.push(format!("alias-='{a}'"));
599 }
600 if hidden {
601 changes.push("hidden=true".to_string());
602 }
603 if visible {
604 changes.push("hidden=false".to_string());
605 }
606
607 let change_desc = if changes.is_empty() {
608 "(no changes)".to_string()
609 } else {
610 changes.join(", ")
611 };
612
613 output.success(format!(
614 "Set operation mapping for '{api_name}': '{op_id}' → {change_desc}"
615 ));
616 output.info("Run 'aperture config reinit' to apply changes.");
617 Ok(())
618}
619
620pub fn handle_list_mappings(
622 manager: &ConfigManager<OsFileSystem>,
623 api_name: &crate::config::context_name::ApiContextName,
624 output: &Output,
625) -> Result<(), Error> {
626 let mapping = manager.get_command_mapping(api_name)?;
627 let Some(mapping) = mapping else {
628 output.info(format!(
629 "No command mappings configured for API '{api_name}'"
630 ));
631 return Ok(());
632 };
633
634 if mapping.groups.is_empty() && mapping.operations.is_empty() {
635 output.info(format!(
636 "No command mappings configured for API '{api_name}'"
637 ));
638 return Ok(());
639 }
640
641 output.info(format!("Command mappings for API '{api_name}':"));
642
643 if !mapping.groups.is_empty() {
644 println!("\n Group renames:");
646 for (original, new_name) in &mapping.groups {
647 println!(" '{original}' → '{new_name}'");
649 }
650 }
651
652 if !mapping.operations.is_empty() {
653 println!("\n Operation mappings:");
655 for (op_id, op_mapping) in &mapping.operations {
656 print_operation_mapping(op_id, op_mapping);
657 }
658 }
659
660 Ok(())
661}
662
663pub fn handle_remove_mapping(
665 manager: &ConfigManager<OsFileSystem>,
666 api_name: &crate::config::context_name::ApiContextName,
667 group: Option<String>,
668 operation: Option<String>,
669 output: &Output,
670) -> Result<(), Error> {
671 match (group, operation) {
672 (Some(ref original), None) => {
673 manager.remove_group_mapping(api_name, original)?;
674 output.success(format!(
675 "Removed group mapping for tag '{original}' from API '{api_name}'"
676 ));
677 }
678 (None, Some(ref op_id)) => {
679 manager.remove_operation_mapping(api_name, op_id)?;
680 output.success(format!(
681 "Removed operation mapping for '{op_id}' from API '{api_name}'"
682 ));
683 }
684 (Some(_), Some(_)) => {
685 return Err(Error::invalid_config(
686 "Specify either --group or --operation, not both",
687 ));
688 }
689 (None, None) => {
690 return Err(Error::invalid_config(
691 "Either --group or --operation must be specified",
692 ));
693 }
694 }
695 output.info("Run 'aperture config reinit' to apply changes.");
696 Ok(())
697}
698
699fn print_operation_mapping(op_id: &str, op_mapping: &crate::config::models::OperationMapping) {
701 println!(" {op_id}:");
703 if let Some(ref name) = op_mapping.name {
704 println!(" name: {name}");
706 }
707 if let Some(ref group) = op_mapping.group {
708 println!(" group: {group}");
710 }
711 if !op_mapping.aliases.is_empty() {
712 println!(" aliases: {}", op_mapping.aliases.join(", "));
714 }
715 if op_mapping.hidden {
716 println!(" hidden: true");
718 }
719}