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");
99 std::process::exit(1);
100 };
101 let spec_name = validate_api_name(&spec_name)?;
102 reinit_spec(manager, &spec_name, output)?;
103 }
104 crate::cli::ConfigCommands::ClearCache { api_name, all } => {
105 if let Some(ref name) = api_name {
106 validate_api_name(name)?;
107 }
108 clear_response_cache(manager, api_name.as_deref(), all, output).await?;
109 }
110 crate::cli::ConfigCommands::CacheStats { api_name } => {
111 if let Some(ref name) = api_name {
112 validate_api_name(name)?;
113 }
114 show_cache_stats(manager, api_name.as_deref(), output).await?;
115 }
116 crate::cli::ConfigCommands::SetSecret {
117 api_name,
118 scheme_name,
119 env,
120 interactive,
121 } => {
122 let api_name = validate_api_name(&api_name)?;
123 if interactive {
124 manager.set_secret_interactive(&api_name)?;
125 return Ok(());
126 }
127 let (Some(scheme), Some(env_var)) = (scheme_name, env) else {
128 return Err(Error::invalid_config(
129 "Either provide --scheme and --env, or use --interactive",
130 ));
131 };
132 manager.set_secret(&api_name, &scheme, &env_var)?;
133 output.success(format!(
134 "Set secret for scheme '{scheme}' in API '{api_name}' to use environment variable '{env_var}'"
135 ));
136 }
137 crate::cli::ConfigCommands::ListSecrets { api_name } => {
138 let api_name = validate_api_name(&api_name)?;
139 let secrets = manager.list_secrets(&api_name)?;
140 if secrets.is_empty() {
141 output.info(format!("No secrets configured for API '{api_name}'"));
142 } else {
143 print_secrets_list(&api_name, secrets, output);
144 }
145 }
146 crate::cli::ConfigCommands::RemoveSecret {
147 api_name,
148 scheme_name,
149 } => {
150 let api_name = validate_api_name(&api_name)?;
151 manager.remove_secret(&api_name, &scheme_name)?;
152 output.success(format!(
153 "Removed secret configuration for scheme '{scheme_name}' from API '{api_name}'"
154 ));
155 }
156 crate::cli::ConfigCommands::ClearSecrets { api_name, force } => {
157 let api_name = validate_api_name(&api_name)?;
158 let secrets = manager.list_secrets(&api_name)?;
159 if secrets.is_empty() {
160 output.info(format!("No secrets configured for API '{api_name}'"));
161 return Ok(());
162 }
163 if force {
164 manager.clear_secrets(&api_name)?;
165 output.success(format!(
166 "Cleared all secret configurations for API '{api_name}'"
167 ));
168 return Ok(());
169 }
170 output.info(format!(
171 "This will remove all {} secret configuration(s) for API '{api_name}':",
172 secrets.len()
173 ));
174 for scheme_name in secrets.keys() {
175 output.info(format!(" - {scheme_name}"));
176 }
177 if !crate::interactive::confirm("Are you sure you want to continue?")? {
178 output.info("Operation cancelled");
179 return Ok(());
180 }
181 manager.clear_secrets(&api_name)?;
182 output.success(format!(
183 "Cleared all secret configurations for API '{api_name}'"
184 ));
185 }
186 crate::cli::ConfigCommands::Set { key, value } => {
187 use crate::config::settings::{SettingKey, SettingValue};
188 let setting_key: SettingKey = key.parse()?;
189 let setting_value = SettingValue::parse_for_key(setting_key, &value)?;
190 manager.set_setting(&setting_key, &setting_value)?;
191 output.success(format!("Set {key} = {value}"));
192 }
193 crate::cli::ConfigCommands::Get { key, json } => {
194 use crate::config::settings::SettingKey;
195 let setting_key: SettingKey = key.parse()?;
196 let value = manager.get_setting(&setting_key)?;
197 if json {
198 println!(
200 "{}",
201 serde_json::json!({ "key": key, "value": value.to_string() })
202 );
203 } else {
204 println!("{value}");
206 }
207 }
208 crate::cli::ConfigCommands::Settings { json } => {
209 let settings = manager.list_settings()?;
210 print_settings_list(settings, json, output)?;
211 }
212 crate::cli::ConfigCommands::SetMapping {
213 api_name,
214 group,
215 operation,
216 name,
217 op_group,
218 alias,
219 remove_alias,
220 hidden,
221 visible,
222 } => {
223 let api_name = validate_api_name(&api_name)?;
224 handle_set_mapping(
225 manager,
226 &api_name,
227 group.as_deref(),
228 operation.as_deref(),
229 name.as_deref(),
230 op_group.as_deref(),
231 alias.as_deref(),
232 remove_alias.as_deref(),
233 hidden,
234 visible,
235 output,
236 )?;
237 }
238 crate::cli::ConfigCommands::ListMappings { api_name } => {
239 let api_name = validate_api_name(&api_name)?;
240 handle_list_mappings(manager, &api_name, output)?;
241 }
242 crate::cli::ConfigCommands::RemoveMapping {
243 api_name,
244 group,
245 operation,
246 } => {
247 let api_name = validate_api_name(&api_name)?;
248 handle_remove_mapping(manager, &api_name, group, operation, output)?;
249 }
250 }
251
252 Ok(())
253}
254
255pub fn print_secrets_list(
257 api_name: &str,
258 secrets: std::collections::HashMap<String, crate::config::models::ApertureSecret>,
259 output: &Output,
260) {
261 output.info(format!("Configured secrets for API '{api_name}':"));
262 for (scheme_name, secret) in secrets {
263 match secret.source {
264 SecretSource::Env => {
265 println!(" {scheme_name}: environment variable '{}'", secret.name);
267 }
268 }
269 }
270}
271
272pub fn print_api_url_entry(
274 api_name: &str,
275 base_override: Option<&str>,
276 env_urls: &std::collections::HashMap<String, String>,
277 output: &Output,
278) {
279 println!("\n{api_name}:");
281 if let Some(base) = base_override {
282 println!(" Base override: {base}");
284 }
285 if !env_urls.is_empty() {
286 output.info(" Environment URLs:");
287 for (env, url) in env_urls {
288 println!(" {env}: {url}");
290 }
291 }
292}
293
294pub fn print_url_configuration(
296 name: &str,
297 base_override: Option<&str>,
298 env_urls: &std::collections::HashMap<String, String>,
299 resolved: &str,
300 output: &Output,
301) {
302 output.info(format!("Base URL configuration for '{name}':"));
303 if let Some(base) = base_override {
304 println!(" Base override: {base}");
306 } else {
307 println!(" Base override: (none)");
309 }
310 if !env_urls.is_empty() {
311 println!(" Environment URLs:");
313 for (env, url) in env_urls {
314 println!(" {env}: {url}");
316 }
317 }
318 println!("\nResolved URL (current): {resolved}");
320 if let Ok(current_env) = std::env::var(constants::ENV_APERTURE_ENV) {
321 output.info(format!("(Using APERTURE_ENV={current_env})"));
322 }
323}
324
325pub fn reinit_spec(
326 manager: &ConfigManager<OsFileSystem>,
327 spec_name: &ApiContextName,
328 output: &Output,
329) -> Result<(), Error> {
330 output.info(format!("Reinitializing cached specification: {spec_name}"));
331 let specs = manager.list_specs()?;
332 if !specs.contains(&spec_name.to_string()) {
333 return Err(Error::spec_not_found(spec_name.as_str()));
334 }
335 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
336 PathBuf::from(dir)
337 } else {
338 get_config_dir()?
339 };
340 let specs_dir = config_dir.join(constants::DIR_SPECS);
341 let spec_path = specs_dir.join(format!("{spec_name}.yaml"));
342 let strict = manager.get_strict_preference(spec_name).unwrap_or(false);
343 manager.add_spec(spec_name, &spec_path, true, strict)?;
344 output.success(format!(
345 "Successfully reinitialized cache for '{spec_name}'"
346 ));
347 Ok(())
348}
349
350pub fn reinit_all_specs(
351 manager: &ConfigManager<OsFileSystem>,
352 output: &Output,
353) -> Result<(), Error> {
354 let specs = manager.list_specs()?;
355 if specs.is_empty() {
356 output.info("No API specifications found to reinitialize.");
357 return Ok(());
358 }
359 output.info(format!(
360 "Reinitializing {} cached specification(s)...",
361 specs.len()
362 ));
363 for spec_name in &specs {
364 let validated = match validate_api_name(spec_name) {
365 Ok(v) => v,
366 Err(e) => {
367 eprintln!(" {spec_name}: {e}");
370 continue;
371 }
372 };
373 match reinit_spec(manager, &validated, output) {
374 Ok(()) => output.info(format!(" {spec_name}")),
375 Err(e) => eprintln!(" {spec_name}: {e}"),
378 }
379 }
380 output.success("Reinitialization complete.");
381 Ok(())
382}
383
384pub fn list_specs_with_details(
385 manager: &ConfigManager<OsFileSystem>,
386 specs: Vec<String>,
387 verbose: bool,
388 output: &Output,
389) {
390 let cache_dir = manager.config_dir().join(constants::DIR_CACHE);
391 for spec_name in specs {
392 if !verbose {
393 println!("- {spec_name}");
395 continue;
396 }
397 let Ok(cached_spec) = crate::engine::loader::load_cached_spec(&cache_dir, &spec_name)
398 else {
399 println!("- {spec_name}");
401 continue;
402 };
403 println!("- {spec_name}:");
405 output.info(format!(" Version: {}", cached_spec.version));
406 let available = cached_spec.commands.len();
407 let skipped = cached_spec.skipped_endpoints.len();
408 let total = available + skipped;
409 if skipped > 0 {
410 output.info(format!(
411 " Endpoints: {available} of {total} available ({skipped} skipped)"
412 ));
413 display_skipped_endpoints_info(&cached_spec, output);
414 } else {
415 output.info(format!(" Endpoints: {available} available"));
416 }
417 }
418}
419
420fn display_skipped_endpoints_info(cached_spec: &crate::cache::models::CachedSpec, output: &Output) {
421 output.info(" Skipped endpoints:");
422 for endpoint in &cached_spec.skipped_endpoints {
423 output.info(format!(
424 " - {} {} - {} not supported",
425 endpoint.method, endpoint.path, endpoint.content_type
426 ));
427 }
428}
429
430pub fn print_settings_list(
431 settings: Vec<crate::config::settings::SettingInfo>,
432 json: bool,
433 output: &Output,
434) -> Result<(), Error> {
435 if json {
436 println!("{}", serde_json::to_string_pretty(&settings)?);
438 return Ok(());
439 }
440 output.info("Available configuration settings:");
441 println!();
443 for setting in settings {
444 println!(" {} = {}", setting.key, setting.value);
446 println!(
448 " Type: {} Default: {}",
449 setting.type_name, setting.default
450 );
451 println!(" {}", setting.description);
453 println!();
455 }
456 Ok(())
457}
458
459pub async fn clear_response_cache(
461 _manager: &ConfigManager<OsFileSystem>,
462 api_name: Option<&str>,
463 all: bool,
464 output: &Output,
465) -> Result<(), Error> {
466 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
467 PathBuf::from(dir)
468 } else {
469 get_config_dir()?
470 };
471 let cache_config = CacheConfig {
472 cache_dir: config_dir
473 .join(constants::DIR_CACHE)
474 .join(constants::DIR_RESPONSES),
475 ..Default::default()
476 };
477 let cache = ResponseCache::new(cache_config)?;
478 let cleared_count = if all {
479 cache.clear_all().await?
480 } else {
481 let Some(api) = api_name else {
482 eprintln!("Error: Either specify an API name or use --all flag");
485 std::process::exit(1);
486 };
487 cache.clear_api_cache(api).await?
488 };
489 if all {
490 output.success(format!(
491 "Cleared {cleared_count} cached responses for all APIs"
492 ));
493 } else {
494 let Some(api) = api_name else {
495 unreachable!("API name must be Some if not all");
496 };
497 output.success(format!(
498 "Cleared {cleared_count} cached responses for API '{api}'"
499 ));
500 }
501 Ok(())
502}
503
504pub async fn show_cache_stats(
506 _manager: &ConfigManager<OsFileSystem>,
507 api_name: Option<&str>,
508 output: &Output,
509) -> Result<(), Error> {
510 let config_dir = if let Ok(dir) = std::env::var(constants::ENV_APERTURE_CONFIG_DIR) {
511 PathBuf::from(dir)
512 } else {
513 get_config_dir()?
514 };
515 let cache_config = CacheConfig {
516 cache_dir: config_dir
517 .join(constants::DIR_CACHE)
518 .join(constants::DIR_RESPONSES),
519 ..Default::default()
520 };
521 let cache = ResponseCache::new(cache_config)?;
522 let stats = cache.get_stats(api_name).await?;
523 if let Some(api) = api_name {
524 output.info(format!("Cache statistics for API '{api}':"));
525 } else {
526 output.info("Cache statistics for all APIs:");
527 }
528 println!(" Total entries: {}", stats.total_entries);
530 println!(" Valid entries: {}", stats.valid_entries);
532 println!(" Expired entries: {}", stats.expired_entries);
534 #[allow(clippy::cast_precision_loss)]
535 let size_mb = stats.total_size_bytes as f64 / 1024.0 / 1024.0;
536 println!(" Total size: {size_mb:.2} MB");
538 if stats.total_entries != 0 {
539 #[allow(clippy::cast_precision_loss)]
540 let hit_rate = stats.valid_entries as f64 / stats.total_entries as f64 * 100.0;
541 println!(" Hit rate: {hit_rate:.1}%");
543 }
544 Ok(())
545}
546
547#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
551pub fn handle_set_mapping(
552 manager: &ConfigManager<OsFileSystem>,
553 api_name: &crate::config::context_name::ApiContextName,
554 group: Option<&[String]>,
555 operation: Option<&str>,
556 name: Option<&str>,
557 op_group: Option<&str>,
558 alias: Option<&str>,
559 remove_alias: Option<&str>,
560 hidden: bool,
561 visible: bool,
562 output: &Output,
563) -> Result<(), Error> {
564 if let Some([original, new_name, ..]) = group {
566 manager.set_group_mapping(api_name, original, new_name)?;
567 output.success(format!(
568 "Set group mapping for '{api_name}': '{original}' → '{new_name}'"
569 ));
570 output.info("Run 'aperture config reinit' to apply changes.");
571 return Ok(());
572 }
573
574 let Some(op_id) = operation else {
576 return Err(Error::invalid_config(
577 "Either --group or --operation must be specified",
578 ));
579 };
580
581 let hidden_flag = match (hidden, visible) {
582 (true, _) => Some(true),
583 (_, true) => Some(false),
584 _ => None,
585 };
586
587 manager.set_operation_mapping(api_name, op_id, name, op_group, alias, hidden_flag)?;
588
589 if let Some(alias_to_remove) = remove_alias {
591 manager.remove_alias(api_name, op_id, alias_to_remove)?;
592 }
593
594 let mut changes = Vec::new();
596 if let Some(n) = name {
597 changes.push(format!("name='{n}'"));
598 }
599 if let Some(g) = op_group {
600 changes.push(format!("group='{g}'"));
601 }
602 if let Some(a) = alias {
603 changes.push(format!("alias+='{a}'"));
604 }
605 if let Some(a) = remove_alias {
606 changes.push(format!("alias-='{a}'"));
607 }
608 if hidden {
609 changes.push("hidden=true".to_string());
610 }
611 if visible {
612 changes.push("hidden=false".to_string());
613 }
614
615 let change_desc = if changes.is_empty() {
616 "(no changes)".to_string()
617 } else {
618 changes.join(", ")
619 };
620
621 output.success(format!(
622 "Set operation mapping for '{api_name}': '{op_id}' → {change_desc}"
623 ));
624 output.info("Run 'aperture config reinit' to apply changes.");
625 Ok(())
626}
627
628pub fn handle_list_mappings(
630 manager: &ConfigManager<OsFileSystem>,
631 api_name: &crate::config::context_name::ApiContextName,
632 output: &Output,
633) -> Result<(), Error> {
634 let mapping = manager.get_command_mapping(api_name)?;
635 let Some(mapping) = mapping else {
636 output.info(format!(
637 "No command mappings configured for API '{api_name}'"
638 ));
639 return Ok(());
640 };
641
642 if mapping.groups.is_empty() && mapping.operations.is_empty() {
643 output.info(format!(
644 "No command mappings configured for API '{api_name}'"
645 ));
646 return Ok(());
647 }
648
649 output.info(format!("Command mappings for API '{api_name}':"));
650
651 if !mapping.groups.is_empty() {
652 println!("\n Group renames:");
654 for (original, new_name) in &mapping.groups {
655 println!(" '{original}' → '{new_name}'");
657 }
658 }
659
660 if !mapping.operations.is_empty() {
661 println!("\n Operation mappings:");
663 for (op_id, op_mapping) in &mapping.operations {
664 print_operation_mapping(op_id, op_mapping);
665 }
666 }
667
668 Ok(())
669}
670
671pub fn handle_remove_mapping(
673 manager: &ConfigManager<OsFileSystem>,
674 api_name: &crate::config::context_name::ApiContextName,
675 group: Option<String>,
676 operation: Option<String>,
677 output: &Output,
678) -> Result<(), Error> {
679 match (group, operation) {
680 (Some(ref original), None) => {
681 manager.remove_group_mapping(api_name, original)?;
682 output.success(format!(
683 "Removed group mapping for tag '{original}' from API '{api_name}'"
684 ));
685 }
686 (None, Some(ref op_id)) => {
687 manager.remove_operation_mapping(api_name, op_id)?;
688 output.success(format!(
689 "Removed operation mapping for '{op_id}' from API '{api_name}'"
690 ));
691 }
692 (Some(_), Some(_)) => {
693 return Err(Error::invalid_config(
694 "Specify either --group or --operation, not both",
695 ));
696 }
697 (None, None) => {
698 return Err(Error::invalid_config(
699 "Either --group or --operation must be specified",
700 ));
701 }
702 }
703 output.info("Run 'aperture config reinit' to apply changes.");
704 Ok(())
705}
706
707fn print_operation_mapping(op_id: &str, op_mapping: &crate::config::models::OperationMapping) {
709 println!(" {op_id}:");
711 if let Some(ref name) = op_mapping.name {
712 println!(" name: {name}");
714 }
715 if let Some(ref group) = op_mapping.group {
716 println!(" group: {group}");
718 }
719 if !op_mapping.aliases.is_empty() {
720 println!(" aliases: {}", op_mapping.aliases.join(", "));
722 }
723 if op_mapping.hidden {
724 println!(" hidden: true");
726 }
727}