1use crate::cache::models::{CachedCommand, CachedSecurityScheme, CachedSpec};
2use crate::cli::OutputFormat;
3use crate::config::models::GlobalConfig;
4use crate::config::url_resolver::BaseUrlResolver;
5use crate::constants;
6use crate::error::Error;
7use crate::response_cache::{
8 CacheConfig, CacheKey, CachedRequestInfo, CachedResponse, ResponseCache,
9};
10use crate::utils::to_kebab_case;
11use base64::{engine::general_purpose, Engine as _};
12use clap::ArgMatches;
13use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
14use reqwest::Method;
15use serde_json::Value;
16use sha2::{Digest, Sha256};
17use std::collections::{BTreeMap, HashMap};
18use std::fmt::Write;
19use std::str::FromStr;
20use tabled::Table;
21
22#[cfg(feature = "jq")]
23use jaq_core::{Ctx, RcIter};
24#[cfg(feature = "jq")]
25use jaq_json::Val;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum AuthScheme {
30 Bearer,
31 Basic,
32 Token,
33 DSN,
34 ApiKey,
35 Custom(String),
36}
37
38impl From<&str> for AuthScheme {
39 fn from(s: &str) -> Self {
40 match s.to_lowercase().as_str() {
41 constants::AUTH_SCHEME_BEARER => Self::Bearer,
42 constants::AUTH_SCHEME_BASIC => Self::Basic,
43 "token" => Self::Token,
44 "dsn" => Self::DSN,
45 constants::AUTH_SCHEME_APIKEY => Self::ApiKey,
46 _ => Self::Custom(s.to_string()),
47 }
48 }
49}
50
51const MAX_TABLE_ROWS: usize = 1000;
53
54fn extract_server_var_args(matches: &ArgMatches) -> Vec<String> {
58 matches
59 .try_get_many::<String>("server-var")
60 .ok()
61 .flatten()
62 .map(|values| values.cloned().collect())
63 .unwrap_or_default()
64}
65
66fn build_http_client() -> Result<reqwest::Client, Error> {
68 reqwest::Client::builder()
69 .timeout(std::time::Duration::from_secs(30))
70 .build()
71 .map_err(|e| {
72 Error::request_failed(
73 reqwest::StatusCode::INTERNAL_SERVER_ERROR,
74 format!("Failed to create HTTP client: {e}"),
75 )
76 })
77}
78
79fn extract_request_body(
81 operation: &CachedCommand,
82 matches: &ArgMatches,
83) -> Result<Option<String>, Error> {
84 if operation.request_body.is_none() {
85 return Ok(None);
86 }
87
88 let mut current_matches = matches;
90 while let Some((_name, sub_matches)) = current_matches.subcommand() {
91 current_matches = sub_matches;
92 }
93
94 if let Some(body_value) = current_matches.get_one::<String>("body") {
95 let _json_body: Value = serde_json::from_str(body_value)
97 .map_err(|e| Error::invalid_json_body(e.to_string()))?;
98 Ok(Some(body_value.clone()))
99 } else {
100 Ok(None)
101 }
102}
103
104fn handle_dry_run(
106 dry_run: bool,
107 method: &reqwest::Method,
108 url: &str,
109 headers: &reqwest::header::HeaderMap,
110 body: Option<&str>,
111 operation: &CachedCommand,
112 capture_output: bool,
113) -> Result<Option<String>, Error> {
114 if !dry_run {
115 return Ok(None);
116 }
117
118 let headers_map: HashMap<String, String> = headers
119 .iter()
120 .map(|(k, v)| {
121 let value = if is_sensitive_header(k.as_str()) {
122 "<REDACTED>".to_string()
123 } else {
124 v.to_str().unwrap_or("<binary>").to_string()
125 };
126 (k.as_str().to_string(), value)
127 })
128 .collect();
129
130 let dry_run_info = serde_json::json!({
131 "dry_run": true,
132 "method": method.to_string(),
133 "url": url,
134 "headers": headers_map,
135 "body": body,
136 "operation_id": operation.operation_id
137 });
138
139 let output = serde_json::to_string_pretty(&dry_run_info).map_err(|e| {
140 Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
141 })?;
142
143 if capture_output {
144 Ok(Some(output))
145 } else {
146 println!("{output}");
147 Ok(None)
148 }
149}
150
151async fn send_request(
153 request: reqwest::RequestBuilder,
154) -> Result<(reqwest::StatusCode, HashMap<String, String>, String), Error> {
155 let response = request
156 .send()
157 .await
158 .map_err(|e| Error::network_request_failed(e.to_string()))?;
159
160 let status = response.status();
161 let response_headers: HashMap<String, String> = response
162 .headers()
163 .iter()
164 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
165 .collect();
166
167 let response_text = response
168 .text()
169 .await
170 .map_err(|e| Error::response_read_error(e.to_string()))?;
171
172 Ok((status, response_headers, response_text))
173}
174
175fn handle_http_error(
177 status: reqwest::StatusCode,
178 response_text: String,
179 spec: &CachedSpec,
180 operation: &CachedCommand,
181) -> Error {
182 let api_name = spec.name.clone();
183 let operation_id = Some(operation.operation_id.clone());
184
185 let security_schemes: Vec<String> = operation
186 .security_requirements
187 .iter()
188 .filter_map(|scheme_name| {
189 spec.security_schemes
190 .get(scheme_name)
191 .and_then(|scheme| scheme.aperture_secret.as_ref())
192 .map(|aperture_secret| aperture_secret.name.clone())
193 })
194 .collect();
195
196 Error::http_error_with_context(
197 status.as_u16(),
198 if response_text.is_empty() {
199 constants::EMPTY_RESPONSE.to_string()
200 } else {
201 response_text
202 },
203 api_name,
204 operation_id,
205 &security_schemes,
206 )
207}
208
209fn prepare_cache_context(
211 cache_config: Option<&CacheConfig>,
212 spec_name: &str,
213 operation_id: &str,
214 method: &reqwest::Method,
215 url: &str,
216 headers: &reqwest::header::HeaderMap,
217 body: Option<&str>,
218) -> Result<Option<(CacheKey, ResponseCache)>, Error> {
219 if let Some(cache_cfg) = cache_config {
220 if cache_cfg.enabled {
221 let header_map: HashMap<String, String> = headers
222 .iter()
223 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
224 .collect();
225
226 let cache_key = CacheKey::from_request(
227 spec_name,
228 operation_id,
229 method.as_ref(),
230 url,
231 &header_map,
232 body,
233 )?;
234
235 let response_cache = ResponseCache::new(cache_cfg.clone())?;
236 Ok(Some((cache_key, response_cache)))
237 } else {
238 Ok(None)
239 }
240 } else {
241 Ok(None)
242 }
243}
244
245async fn check_cache(
247 cache_context: Option<&(CacheKey, ResponseCache)>,
248) -> Result<Option<CachedResponse>, Error> {
249 if let Some((cache_key, response_cache)) = cache_context {
250 response_cache.get(cache_key).await
251 } else {
252 Ok(None)
253 }
254}
255
256#[allow(clippy::too_many_arguments)]
258async fn store_in_cache(
259 cache_context: Option<(CacheKey, ResponseCache)>,
260 response_text: &str,
261 status: reqwest::StatusCode,
262 response_headers: &HashMap<String, String>,
263 method: reqwest::Method,
264 url: String,
265 headers: &reqwest::header::HeaderMap,
266 body: Option<&str>,
267 cache_config: Option<&CacheConfig>,
268) -> Result<(), Error> {
269 if let Some((cache_key, response_cache)) = cache_context {
270 let cached_request_info = CachedRequestInfo {
271 method: method.to_string(),
272 url,
273 headers: headers
274 .iter()
275 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
276 .collect(),
277 body_hash: body.map(|b| {
278 let mut hasher = Sha256::new();
279 hasher.update(b.as_bytes());
280 format!("{:x}", hasher.finalize())
281 }),
282 };
283
284 let cache_ttl = cache_config.and_then(|cfg| {
285 if cfg.default_ttl.as_secs() > 0 {
286 Some(cfg.default_ttl)
287 } else {
288 None
289 }
290 });
291
292 response_cache
293 .store(
294 &cache_key,
295 response_text,
296 status.as_u16(),
297 response_headers,
298 cached_request_info,
299 cache_ttl,
300 )
301 .await?;
302 }
303 Ok(())
304}
305
306#[allow(clippy::too_many_lines)]
333#[allow(clippy::too_many_arguments)]
334#[allow(clippy::missing_panics_doc)]
335#[allow(clippy::missing_errors_doc)]
336pub async fn execute_request(
337 spec: &CachedSpec,
338 matches: &ArgMatches,
339 base_url: Option<&str>,
340 dry_run: bool,
341 idempotency_key: Option<&str>,
342 global_config: Option<&GlobalConfig>,
343 output_format: &OutputFormat,
344 jq_filter: Option<&str>,
345 cache_config: Option<&CacheConfig>,
346 capture_output: bool,
347) -> Result<Option<String>, Error> {
348 let (operation, operation_matches) = find_operation_with_matches(spec, matches)?;
350
351 if operation_matches
354 .try_contains_id("show-examples")
355 .unwrap_or(false)
356 && operation_matches.get_flag("show-examples")
357 {
358 print_extended_examples(operation);
359 return Ok(None);
360 }
361
362 let server_var_args = extract_server_var_args(matches);
364
365 let resolver = BaseUrlResolver::new(spec);
367 let resolver = if let Some(config) = global_config {
368 resolver.with_global_config(config)
369 } else {
370 resolver
371 };
372 let base_url = resolver.resolve_with_variables(base_url, &server_var_args)?;
373
374 let url = build_url(&base_url, &operation.path, operation, operation_matches)?;
376
377 let client = build_http_client()?;
379
380 let mut headers = build_headers(
382 spec,
383 operation,
384 operation_matches,
385 &spec.name,
386 global_config,
387 )?;
388
389 if let Some(key) = idempotency_key {
391 headers.insert(
392 HeaderName::from_static("idempotency-key"),
393 HeaderValue::from_str(key).map_err(|_| Error::invalid_idempotency_key())?,
394 );
395 }
396
397 let method = Method::from_str(&operation.method)
399 .map_err(|_| Error::invalid_http_method(&operation.method))?;
400
401 let headers_clone = headers.clone(); let mut request = client.request(method.clone(), &url).headers(headers);
403
404 let request_body = extract_request_body(operation, operation_matches)?;
406 if let Some(ref body) = request_body {
407 let json_body: Value = serde_json::from_str(body)
408 .expect("JSON body was validated in extract_request_body, parsing should succeed");
409 request = request.json(&json_body);
410 }
411
412 let cache_context = prepare_cache_context(
414 cache_config,
415 &spec.name,
416 &operation.operation_id,
417 &method,
418 &url,
419 &headers_clone,
420 request_body.as_deref(),
421 )?;
422
423 if let Some(cached_response) = check_cache(cache_context.as_ref()).await? {
425 let output = print_formatted_response(
426 &cached_response.body,
427 output_format,
428 jq_filter,
429 capture_output,
430 )?;
431 return Ok(output);
432 }
433
434 if let Some(output) = handle_dry_run(
436 dry_run,
437 &method,
438 &url,
439 &headers_clone,
440 request_body.as_deref(),
441 operation,
442 capture_output,
443 )? {
444 return Ok(Some(output));
445 }
446 if dry_run {
447 return Ok(None);
448 }
449
450 let (status, response_headers, response_text) = send_request(request).await?;
452
453 if !status.is_success() {
455 return Err(handle_http_error(status, response_text, spec, operation));
456 }
457
458 store_in_cache(
460 cache_context,
461 &response_text,
462 status,
463 &response_headers,
464 method,
465 url,
466 &headers_clone,
467 request_body.as_deref(),
468 cache_config,
469 )
470 .await?;
471
472 if response_text.is_empty() {
474 Ok(None)
475 } else {
476 print_formatted_response(&response_text, output_format, jq_filter, capture_output)
477 }
478}
479
480fn print_extended_examples(operation: &CachedCommand) {
483 println!("Command: {}\n", to_kebab_case(&operation.operation_id));
484
485 if let Some(ref summary) = operation.summary {
486 println!("Description: {summary}\n");
487 }
488
489 println!("Method: {} {}\n", operation.method, operation.path);
490
491 if operation.examples.is_empty() {
492 println!("No examples available for this command.");
493 return;
494 }
495
496 println!("Examples:\n");
497 for (i, example) in operation.examples.iter().enumerate() {
498 println!("{}. {}", i + 1, example.description);
499 println!(" {}", example.command_line);
500 if let Some(ref explanation) = example.explanation {
501 println!(" {explanation}");
502 }
503 println!();
504 }
505
506 if !operation.parameters.is_empty() {
508 println!("Parameters:");
509 for param in &operation.parameters {
510 let required = if param.required { " (required)" } else { "" };
511 let param_type = param.schema_type.as_deref().unwrap_or("string");
512 println!(" --{}{} [{}]", param.name, required, param_type);
513 if let Some(ref desc) = param.description {
514 println!(" {desc}");
515 }
516 }
517 println!();
518 }
519
520 if operation.request_body.is_some() {
521 println!("Request Body:");
522 println!(" --body JSON (required)");
523 println!(" JSON data to send in the request body");
524 }
525}
526
527#[allow(dead_code)]
528fn find_operation<'a>(
529 spec: &'a CachedSpec,
530 matches: &ArgMatches,
531) -> Result<&'a CachedCommand, Error> {
532 let mut current_matches = matches;
534 let mut subcommand_path = Vec::new();
535
536 while let Some((name, sub_matches)) = current_matches.subcommand() {
537 subcommand_path.push(name);
538 current_matches = sub_matches;
539 }
540
541 if let Some(operation_name) = subcommand_path.last() {
544 for command in &spec.commands {
545 let kebab_id = to_kebab_case(&command.operation_id);
547 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
548 return Ok(command);
549 }
550 }
551 }
552
553 let operation_name = subcommand_path
554 .last()
555 .map_or("unknown".to_string(), ToString::to_string);
556
557 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
559
560 Err(Error::operation_not_found_with_suggestions(
561 operation_name,
562 &suggestions,
563 ))
564}
565
566fn find_operation_with_matches<'a>(
567 spec: &'a CachedSpec,
568 matches: &'a ArgMatches,
569) -> Result<(&'a CachedCommand, &'a ArgMatches), Error> {
570 let mut current_matches = matches;
572 let mut subcommand_path = Vec::new();
573
574 while let Some((name, sub_matches)) = current_matches.subcommand() {
575 subcommand_path.push(name);
576 current_matches = sub_matches;
577 }
578
579 if let Some(operation_name) = subcommand_path.last() {
582 for command in &spec.commands {
583 let kebab_id = to_kebab_case(&command.operation_id);
585 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
586 return Ok((command, current_matches));
588 }
589 }
590 }
591
592 let operation_name = subcommand_path
593 .last()
594 .map_or("unknown".to_string(), ToString::to_string);
595
596 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
598
599 Err(Error::operation_not_found_with_suggestions(
600 operation_name,
601 &suggestions,
602 ))
603}
604
605fn build_url(
610 base_url: &str,
611 path_template: &str,
612 operation: &CachedCommand,
613 matches: &ArgMatches,
614) -> Result<String, Error> {
615 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
616
617 let mut current_matches = matches;
619 while let Some((_name, sub_matches)) = current_matches.subcommand() {
620 current_matches = sub_matches;
621 }
622
623 let mut start = 0;
626 while let Some(open) = url[start..].find('{') {
627 let open_pos = start + open;
628 if let Some(close) = url[open_pos..].find('}') {
629 let close_pos = open_pos + close;
630 let param_name = &url[open_pos + 1..close_pos];
631
632 let param = operation.parameters.iter().find(|p| p.name == param_name);
634 let is_boolean = param
635 .and_then(|p| p.schema_type.as_ref())
636 .is_some_and(|t| t == "boolean");
637
638 let value = if is_boolean {
639 if current_matches.get_flag(param_name) {
641 "true".to_string()
642 } else {
643 "false".to_string()
644 }
645 } else if let Some(string_value) = current_matches
646 .try_get_one::<String>(param_name)
647 .ok()
648 .flatten()
649 {
650 string_value.clone()
651 } else {
652 return Err(Error::missing_path_parameter(param_name));
653 };
654
655 url.replace_range(open_pos..=close_pos, &value);
656 start = open_pos + value.len();
657 } else {
658 break;
659 }
660 }
661
662 let mut query_params = Vec::new();
664 for arg in current_matches.ids() {
665 let arg_str = arg.as_str();
666 let param = operation
668 .parameters
669 .iter()
670 .find(|p| p.name == arg_str && p.location == "query");
671
672 if let Some(param) = param {
673 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
675
676 if is_boolean {
677 if current_matches.get_flag(arg_str) {
679 query_params.push(format!("{arg_str}=true"));
680 }
681 } else if let Some(value) = current_matches.get_one::<String>(arg_str) {
682 query_params.push(format!("{arg_str}={}", urlencoding::encode(value)));
684 }
685 }
686 }
687
688 if !query_params.is_empty() {
689 url.push('?');
690 url.push_str(&query_params.join("&"));
691 }
692
693 Ok(url)
694}
695
696fn build_headers(
698 spec: &CachedSpec,
699 operation: &CachedCommand,
700 matches: &ArgMatches,
701 api_name: &str,
702 global_config: Option<&GlobalConfig>,
703) -> Result<HeaderMap, Error> {
704 let mut headers = HeaderMap::new();
705
706 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
708 headers.insert(
709 constants::HEADER_ACCEPT,
710 HeaderValue::from_static(constants::CONTENT_TYPE_JSON),
711 );
712
713 let mut current_matches = matches;
715 while let Some((_name, sub_matches)) = current_matches.subcommand() {
716 current_matches = sub_matches;
717 }
718
719 for param in &operation.parameters {
721 if param.location != "header" {
723 continue;
724 }
725
726 let header_name = HeaderName::from_str(¶m.name)
727 .map_err(|e| Error::invalid_header_name(¶m.name, e.to_string()))?;
728
729 let is_boolean = matches!(param.schema_type.as_deref(), Some("boolean"));
731
732 let header_value = if is_boolean {
733 if current_matches.get_flag(¶m.name) {
736 HeaderValue::from_static("true")
737 } else {
738 continue;
740 }
741 } else if let Some(value) = current_matches.get_one::<String>(¶m.name) {
742 HeaderValue::from_str(value)
744 .map_err(|e| Error::invalid_header_value(¶m.name, e.to_string()))?
745 } else {
746 continue;
748 };
749
750 headers.insert(header_name, header_value);
751 }
752
753 for security_scheme_name in &operation.security_requirements {
755 if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
756 add_authentication_header(&mut headers, security_scheme, api_name, global_config)?;
757 }
758 }
759
760 if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
763 for header_str in custom_headers {
764 let (name, value) = parse_custom_header(header_str)?;
765 let header_name = HeaderName::from_str(&name)
766 .map_err(|e| Error::invalid_header_name(&name, e.to_string()))?;
767 let header_value = HeaderValue::from_str(&value)
768 .map_err(|e| Error::invalid_header_value(&name, e.to_string()))?;
769 headers.insert(header_name, header_value);
770 }
771 }
772
773 Ok(headers)
774}
775
776fn validate_header_value(name: &str, value: &str) -> Result<(), Error> {
778 if value.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
779 return Err(Error::invalid_header_value(
780 name,
781 "Header value contains invalid control characters (newline, carriage return, or null)",
782 ));
783 }
784 Ok(())
785}
786
787fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
789 let colon_pos = header_str
791 .find(':')
792 .ok_or_else(|| Error::invalid_header_format(header_str))?;
793
794 let name = header_str[..colon_pos].trim();
795 let value = header_str[colon_pos + 1..].trim();
796
797 if name.is_empty() {
798 return Err(Error::empty_header_name());
799 }
800
801 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
803 let var_name = &value[2..value.len() - 1];
805 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
806 } else {
807 value.to_string()
808 };
809
810 validate_header_value(name, &expanded_value)?;
812
813 Ok((name.to_string(), expanded_value))
814}
815
816fn is_sensitive_header(header_name: &str) -> bool {
818 let name_lower = header_name.to_lowercase();
819 matches!(
820 name_lower.as_str(),
821 "authorization" | "proxy-authorization" | "x-api-key" | "x-api-token" | "x-auth-token"
822 )
823}
824
825#[allow(clippy::too_many_lines)]
827fn add_authentication_header(
828 headers: &mut HeaderMap,
829 security_scheme: &CachedSecurityScheme,
830 api_name: &str,
831 global_config: Option<&GlobalConfig>,
832) -> Result<(), Error> {
833 if std::env::var("RUST_LOG").is_ok() {
835 eprintln!(
836 "[DEBUG] Adding authentication header for scheme: {} (type: {})",
837 security_scheme.name, security_scheme.scheme_type
838 );
839 }
840
841 let secret_config = global_config
843 .and_then(|config| config.api_configs.get(api_name))
844 .and_then(|api_config| api_config.secrets.get(&security_scheme.name));
845
846 let (secret_value, env_var_name) = if let Some(config_secret) = secret_config {
847 let secret_value = std::env::var(&config_secret.name)
849 .map_err(|_| Error::secret_not_set(&security_scheme.name, &config_secret.name))?;
850 (secret_value, config_secret.name.clone())
851 } else if let Some(aperture_secret) = &security_scheme.aperture_secret {
852 let secret_value = std::env::var(&aperture_secret.name)
854 .map_err(|_| Error::secret_not_set(&security_scheme.name, &aperture_secret.name))?;
855 (secret_value, aperture_secret.name.clone())
856 } else {
857 return Ok(());
859 };
860
861 if std::env::var("RUST_LOG").is_ok() {
863 let source = if secret_config.is_some() {
864 "config"
865 } else {
866 "x-aperture-secret"
867 };
868 eprintln!(
869 "[DEBUG] Using secret from {source} for scheme '{}': env var '{env_var_name}'",
870 security_scheme.name
871 );
872 }
873
874 validate_header_value(constants::HEADER_AUTHORIZATION, &secret_value)?;
876
877 match security_scheme.scheme_type.as_str() {
879 constants::AUTH_SCHEME_APIKEY => {
880 let (Some(location), Some(param_name)) =
881 (&security_scheme.location, &security_scheme.parameter_name)
882 else {
883 return Ok(());
884 };
885
886 if location == "header" {
887 let header_name = HeaderName::from_str(param_name)
888 .map_err(|e| Error::invalid_header_name(param_name, e.to_string()))?;
889 let header_value = HeaderValue::from_str(&secret_value)
890 .map_err(|e| Error::invalid_header_value(param_name, e.to_string()))?;
891 headers.insert(header_name, header_value);
892 }
893 }
895 "http" => {
896 if let Some(scheme_str) = &security_scheme.scheme {
897 let auth_scheme: AuthScheme = scheme_str.as_str().into();
898 let auth_value = match &auth_scheme {
899 AuthScheme::Bearer => {
900 format!("Bearer {secret_value}")
901 }
902 AuthScheme::Basic => {
903 let encoded = general_purpose::STANDARD.encode(&secret_value);
907 format!("Basic {encoded}")
908 }
909 AuthScheme::Token
910 | AuthScheme::DSN
911 | AuthScheme::ApiKey
912 | AuthScheme::Custom(_) => {
913 format!("{scheme_str} {secret_value}")
917 }
918 };
919
920 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
921 Error::invalid_header_value(constants::HEADER_AUTHORIZATION, e.to_string())
922 })?;
923 headers.insert(constants::HEADER_AUTHORIZATION, header_value);
924
925 if std::env::var("RUST_LOG").is_ok() {
927 match &auth_scheme {
928 AuthScheme::Bearer => {
929 eprintln!("[DEBUG] Added Bearer authentication header");
930 }
931 AuthScheme::Basic => {
932 eprintln!("[DEBUG] Added Basic authentication header (base64 encoded)");
933 }
934 _ => {
935 eprintln!(
936 "[DEBUG] Added custom HTTP auth header with scheme: {scheme_str}"
937 );
938 }
939 }
940 }
941 }
942 }
943 _ => {
944 return Err(Error::unsupported_security_scheme(
945 &security_scheme.scheme_type,
946 ));
947 }
948 }
949
950 Ok(())
951}
952
953fn print_formatted_response(
955 response_text: &str,
956 output_format: &OutputFormat,
957 jq_filter: Option<&str>,
958 capture_output: bool,
959) -> Result<Option<String>, Error> {
960 let processed_text = if let Some(filter) = jq_filter {
962 apply_jq_filter(response_text, filter)?
963 } else {
964 response_text.to_string()
965 };
966
967 match output_format {
968 OutputFormat::Json => {
969 let output = serde_json::from_str::<Value>(&processed_text)
971 .ok()
972 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
973 .unwrap_or_else(|| processed_text.clone());
974
975 if capture_output {
976 return Ok(Some(output));
977 }
978 println!("{output}");
979 }
980 OutputFormat::Yaml => {
981 let output = serde_json::from_str::<Value>(&processed_text)
983 .ok()
984 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
985 .unwrap_or_else(|| processed_text.clone());
986
987 if capture_output {
988 return Ok(Some(output));
989 }
990 println!("{output}");
991 }
992 OutputFormat::Table => {
993 if let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) {
995 let table_output = print_as_table(&json_value, capture_output)?;
996 if capture_output {
997 return Ok(table_output);
998 }
999 } else {
1000 if capture_output {
1002 return Ok(Some(processed_text));
1003 }
1004 println!("{processed_text}");
1005 }
1006 }
1007 }
1008
1009 Ok(None)
1010}
1011
1012#[derive(tabled::Tabled)]
1014struct TableRow {
1015 #[tabled(rename = "Key")]
1016 key: String,
1017 #[tabled(rename = "Value")]
1018 value: String,
1019}
1020
1021#[derive(tabled::Tabled)]
1022struct KeyValue {
1023 #[tabled(rename = "Key")]
1024 key: String,
1025 #[tabled(rename = "Value")]
1026 value: String,
1027}
1028
1029#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1031fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
1032 match json_value {
1033 Value::Array(items) => {
1034 if items.is_empty() {
1035 if capture_output {
1036 return Ok(Some(constants::EMPTY_ARRAY.to_string()));
1037 }
1038 println!("{}", constants::EMPTY_ARRAY);
1039 return Ok(None);
1040 }
1041
1042 if items.len() > MAX_TABLE_ROWS {
1044 let msg1 = format!(
1045 "Array too large: {} items (max {} for table display)",
1046 items.len(),
1047 MAX_TABLE_ROWS
1048 );
1049 let msg2 = "Use --format json or --jq to process the full data";
1050
1051 if capture_output {
1052 return Ok(Some(format!("{msg1}\n{msg2}")));
1053 }
1054 println!("{msg1}");
1055 println!("{msg2}");
1056 return Ok(None);
1057 }
1058
1059 if let Some(Value::Object(_)) = items.first() {
1061 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
1063
1064 for item in items {
1065 if let Value::Object(obj) = item {
1066 let mut row = BTreeMap::new();
1067 for (key, value) in obj {
1068 row.insert(key.clone(), format_value_for_table(value));
1069 }
1070 table_data.push(row);
1071 }
1072 }
1073
1074 if !table_data.is_empty() {
1075 let mut rows = Vec::new();
1078 for (i, row) in table_data.iter().enumerate() {
1079 if i > 0 {
1080 rows.push(TableRow {
1081 key: "---".to_string(),
1082 value: "---".to_string(),
1083 });
1084 }
1085 for (key, value) in row {
1086 rows.push(TableRow {
1087 key: key.clone(),
1088 value: value.clone(),
1089 });
1090 }
1091 }
1092
1093 let table = Table::new(&rows);
1094 if capture_output {
1095 return Ok(Some(table.to_string()));
1096 }
1097 println!("{table}");
1098 return Ok(None);
1099 }
1100 }
1101
1102 if capture_output {
1104 let mut output = String::new();
1105 for (i, item) in items.iter().enumerate() {
1106 writeln!(&mut output, "{}: {}", i, format_value_for_table(item)).unwrap();
1107 }
1108 return Ok(Some(output.trim_end().to_string()));
1109 }
1110 for (i, item) in items.iter().enumerate() {
1111 println!("{}: {}", i, format_value_for_table(item));
1112 }
1113 }
1114 Value::Object(obj) => {
1115 if obj.len() > MAX_TABLE_ROWS {
1117 let msg1 = format!(
1118 "Object too large: {} fields (max {} for table display)",
1119 obj.len(),
1120 MAX_TABLE_ROWS
1121 );
1122 let msg2 = "Use --format json or --jq to process the full data";
1123
1124 if capture_output {
1125 return Ok(Some(format!("{msg1}\n{msg2}")));
1126 }
1127 println!("{msg1}");
1128 println!("{msg2}");
1129 return Ok(None);
1130 }
1131
1132 let rows: Vec<KeyValue> = obj
1134 .iter()
1135 .map(|(key, value)| KeyValue {
1136 key: key.clone(),
1137 value: format_value_for_table(value),
1138 })
1139 .collect();
1140
1141 let table = Table::new(&rows);
1142 if capture_output {
1143 return Ok(Some(table.to_string()));
1144 }
1145 println!("{table}");
1146 }
1147 _ => {
1148 let formatted = format_value_for_table(json_value);
1150 if capture_output {
1151 return Ok(Some(formatted));
1152 }
1153 println!("{formatted}");
1154 }
1155 }
1156
1157 Ok(None)
1158}
1159
1160fn format_value_for_table(value: &Value) -> String {
1162 match value {
1163 Value::Null => constants::NULL_VALUE.to_string(),
1164 Value::Bool(b) => b.to_string(),
1165 Value::Number(n) => n.to_string(),
1166 Value::String(s) => s.clone(),
1167 Value::Array(arr) => {
1168 if arr.len() <= 3 {
1169 format!(
1170 "[{}]",
1171 arr.iter()
1172 .map(format_value_for_table)
1173 .collect::<Vec<_>>()
1174 .join(", ")
1175 )
1176 } else {
1177 format!("[{} items]", arr.len())
1178 }
1179 }
1180 Value::Object(obj) => {
1181 if obj.len() <= 2 {
1182 format!(
1183 "{{{}}}",
1184 obj.iter()
1185 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
1186 .collect::<Vec<_>>()
1187 .join(", ")
1188 )
1189 } else {
1190 format!("{{object with {} fields}}", obj.len())
1191 }
1192 }
1193 }
1194}
1195
1196pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
1205 let json_value: Value = serde_json::from_str(response_text)
1207 .map_err(|e| Error::jq_filter_error(filter, format!("Response is not valid JSON: {e}")))?;
1208
1209 #[cfg(feature = "jq")]
1210 {
1211 use jaq_core::load::{Arena, File, Loader};
1213 use jaq_core::Compiler;
1214
1215 let program = File {
1217 code: filter,
1218 path: (),
1219 };
1220
1221 let defs: Vec<_> = jaq_std::defs().chain(jaq_json::defs()).collect();
1224 let funs: Vec<_> = jaq_std::funs().chain(jaq_json::funs()).collect();
1225
1226 let loader = Loader::new(defs);
1228 let arena = Arena::default();
1229
1230 let modules = match loader.load(&arena, program) {
1232 Ok(modules) => modules,
1233 Err(errs) => {
1234 return Err(Error::jq_filter_error(
1235 filter,
1236 format!("Parse error: {:?}", errs),
1237 ));
1238 }
1239 };
1240
1241 let filter_fn = match Compiler::default().with_funs(funs).compile(modules) {
1243 Ok(filter) => filter,
1244 Err(errs) => {
1245 return Err(Error::jq_filter_error(
1246 filter,
1247 format!("Compilation error: {:?}", errs),
1248 ));
1249 }
1250 };
1251
1252 let jaq_value = Val::from(json_value);
1254
1255 let inputs = RcIter::new(core::iter::empty());
1257 let ctx = Ctx::new([], &inputs);
1258
1259 let output = filter_fn.run((ctx, jaq_value));
1261
1262 let results: Result<Vec<Val>, _> = output.collect();
1264
1265 match results {
1266 Ok(vals) => {
1267 if vals.is_empty() {
1268 Ok(constants::NULL_VALUE.to_string())
1269 } else if vals.len() == 1 {
1270 let json_val = serde_json::Value::from(vals[0].clone());
1272 serde_json::to_string_pretty(&json_val).map_err(|e| {
1273 Error::serialization_error(format!("Failed to serialize result: {e}"))
1274 })
1275 } else {
1276 let json_vals: Vec<Value> =
1278 vals.into_iter().map(serde_json::Value::from).collect();
1279 let array = Value::Array(json_vals);
1280 serde_json::to_string_pretty(&array).map_err(|e| {
1281 Error::serialization_error(format!("Failed to serialize results: {e}"))
1282 })
1283 }
1284 }
1285 Err(e) => Err(Error::jq_filter_error(
1286 format!("{:?}", filter),
1287 format!("Filter execution error: {e}"),
1288 )),
1289 }
1290 }
1291
1292 #[cfg(not(feature = "jq"))]
1293 {
1294 apply_basic_jq_filter(&json_value, filter)
1296 }
1297}
1298
1299#[cfg(not(feature = "jq"))]
1300fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
1302 let uses_advanced_features = filter.contains('[')
1304 || filter.contains(']')
1305 || filter.contains('|')
1306 || filter.contains('(')
1307 || filter.contains(')')
1308 || filter.contains("select")
1309 || filter.contains("map")
1310 || filter.contains("length");
1311
1312 if uses_advanced_features {
1313 eprintln!(
1314 "{} Advanced JQ features require building with --features jq",
1315 crate::constants::MSG_WARNING_PREFIX
1316 );
1317 eprintln!(" Currently only basic field access is supported (e.g., '.field', '.nested.field')");
1318 eprintln!(" To enable full JQ support: cargo install aperture-cli --features jq");
1319 }
1320
1321 let result = match filter {
1322 "." => json_value.clone(),
1323 ".[]" => {
1324 match json_value {
1326 Value::Array(arr) => {
1327 Value::Array(arr.clone())
1329 }
1330 Value::Object(obj) => {
1331 Value::Array(obj.values().cloned().collect())
1333 }
1334 _ => Value::Null,
1335 }
1336 }
1337 ".length" => {
1338 match json_value {
1340 Value::Array(arr) => Value::Number(arr.len().into()),
1341 Value::Object(obj) => Value::Number(obj.len().into()),
1342 Value::String(s) => Value::Number(s.len().into()),
1343 _ => Value::Null,
1344 }
1345 }
1346 filter if filter.starts_with(".[].") => {
1347 let field_path = &filter[4..]; match json_value {
1350 Value::Array(arr) => {
1351 let mapped: Vec<Value> = arr
1352 .iter()
1353 .map(|item| get_nested_field(item, field_path))
1354 .collect();
1355 Value::Array(mapped)
1356 }
1357 _ => Value::Null,
1358 }
1359 }
1360 filter if filter.starts_with('.') => {
1361 let field_path = &filter[1..]; get_nested_field(json_value, field_path)
1364 }
1365 _ => {
1366 return Err(Error::jq_filter_error(filter, "Unsupported JQ filter. Only basic field access like '.name' or '.metadata.role' is supported without the full jq library."));
1367 }
1368 };
1369
1370 serde_json::to_string_pretty(&result).map_err(|e| {
1371 Error::serialization_error(format!("Failed to serialize filtered result: {e}"))
1372 })
1373}
1374
1375#[cfg(not(feature = "jq"))]
1376fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
1378 let parts: Vec<&str> = field_path.split('.').collect();
1379 let mut current = json_value;
1380
1381 for part in parts {
1382 if part.is_empty() {
1383 continue;
1384 }
1385
1386 if part.starts_with('[') && part.ends_with(']') {
1388 let index_str = &part[1..part.len() - 1];
1389 if let Ok(index) = index_str.parse::<usize>() {
1390 match current {
1391 Value::Array(arr) => {
1392 if let Some(item) = arr.get(index) {
1393 current = item;
1394 } else {
1395 return Value::Null;
1396 }
1397 }
1398 _ => return Value::Null,
1399 }
1400 } else {
1401 return Value::Null;
1402 }
1403 continue;
1404 }
1405
1406 match current {
1407 Value::Object(obj) => {
1408 if let Some(field) = obj.get(part) {
1409 current = field;
1410 } else {
1411 return Value::Null;
1412 }
1413 }
1414 Value::Array(arr) => {
1415 if let Ok(index) = part.parse::<usize>() {
1417 if let Some(item) = arr.get(index) {
1418 current = item;
1419 } else {
1420 return Value::Null;
1421 }
1422 } else {
1423 return Value::Null;
1424 }
1425 }
1426 _ => return Value::Null,
1427 }
1428 }
1429
1430 current.clone()
1431}
1432
1433#[cfg(test)]
1434mod tests {
1435 use super::*;
1436
1437 #[test]
1438 fn test_apply_jq_filter_simple_field_access() {
1439 let json = r#"{"name": "Alice", "age": 30}"#;
1440 let result = apply_jq_filter(json, ".name").unwrap();
1441 let parsed: Value = serde_json::from_str(&result).unwrap();
1442 assert_eq!(parsed, serde_json::json!("Alice"));
1443 }
1444
1445 #[test]
1446 fn test_apply_jq_filter_nested_field_access() {
1447 let json = r#"{"user": {"name": "Bob", "id": 123}}"#;
1448 let result = apply_jq_filter(json, ".user.name").unwrap();
1449 let parsed: Value = serde_json::from_str(&result).unwrap();
1450 assert_eq!(parsed, serde_json::json!("Bob"));
1451 }
1452
1453 #[cfg(feature = "jq")]
1454 #[test]
1455 fn test_apply_jq_filter_array_index() {
1456 let json = r#"{"items": ["first", "second", "third"]}"#;
1457 let result = apply_jq_filter(json, ".items[1]").unwrap();
1458 let parsed: Value = serde_json::from_str(&result).unwrap();
1459 assert_eq!(parsed, serde_json::json!("second"));
1460 }
1461
1462 #[cfg(feature = "jq")]
1463 #[test]
1464 fn test_apply_jq_filter_array_iteration() {
1465 let json = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
1466 let result = apply_jq_filter(json, ".[].id").unwrap();
1467 let parsed: Value = serde_json::from_str(&result).unwrap();
1468 assert_eq!(parsed, serde_json::json!([1, 2, 3]));
1470 }
1471
1472 #[cfg(feature = "jq")]
1473 #[test]
1474 fn test_apply_jq_filter_complex_expression() {
1475 let json = r#"{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}"#;
1476 let result = apply_jq_filter(json, ".users | map(.name)").unwrap();
1477 let parsed: Value = serde_json::from_str(&result).unwrap();
1478 assert_eq!(parsed, serde_json::json!(["Alice", "Bob"]));
1479 }
1480
1481 #[cfg(feature = "jq")]
1482 #[test]
1483 fn test_apply_jq_filter_select() {
1484 let json =
1485 r#"[{"id": 1, "active": true}, {"id": 2, "active": false}, {"id": 3, "active": true}]"#;
1486 let result = apply_jq_filter(json, "[.[] | select(.active)]").unwrap();
1487 let parsed: Value = serde_json::from_str(&result).unwrap();
1488 assert_eq!(
1489 parsed,
1490 serde_json::json!([{"id": 1, "active": true}, {"id": 3, "active": true}])
1491 );
1492 }
1493
1494 #[test]
1495 fn test_apply_jq_filter_invalid_json() {
1496 let json = "not valid json";
1497 let result = apply_jq_filter(json, ".field");
1498 assert!(result.is_err());
1499 if let Err(err) = result {
1500 let error_msg = err.to_string();
1501 assert!(error_msg.contains("JQ filter error"));
1502 assert!(error_msg.contains(".field"));
1503 assert!(error_msg.contains("Response is not valid JSON"));
1504 } else {
1505 panic!("Expected error");
1506 }
1507 }
1508
1509 #[cfg(feature = "jq")]
1510 #[test]
1511 fn test_apply_jq_filter_invalid_expression() {
1512 let json = r#"{"name": "test"}"#;
1513 let result = apply_jq_filter(json, "invalid..expression");
1514 assert!(result.is_err());
1515 if let Err(err) = result {
1516 let error_msg = err.to_string();
1517 assert!(error_msg.contains("JQ filter error") || error_msg.contains("Parse error"));
1518 assert!(error_msg.contains("invalid..expression"));
1519 } else {
1520 panic!("Expected error");
1521 }
1522 }
1523
1524 #[test]
1525 fn test_apply_jq_filter_null_result() {
1526 let json = r#"{"name": "test"}"#;
1527 let result = apply_jq_filter(json, ".missing_field").unwrap();
1528 let parsed: Value = serde_json::from_str(&result).unwrap();
1529 assert_eq!(parsed, serde_json::json!(null));
1530 }
1531
1532 #[cfg(feature = "jq")]
1533 #[test]
1534 fn test_apply_jq_filter_arithmetic() {
1535 let json = r#"{"x": 10, "y": 20}"#;
1536 let result = apply_jq_filter(json, ".x + .y").unwrap();
1537 let parsed: Value = serde_json::from_str(&result).unwrap();
1538 assert_eq!(parsed, serde_json::json!(30));
1539 }
1540
1541 #[cfg(feature = "jq")]
1542 #[test]
1543 fn test_apply_jq_filter_string_concatenation() {
1544 let json = r#"{"first": "Hello", "second": "World"}"#;
1545 let result = apply_jq_filter(json, r#".first + " " + .second"#).unwrap();
1546 let parsed: Value = serde_json::from_str(&result).unwrap();
1547 assert_eq!(parsed, serde_json::json!("Hello World"));
1548 }
1549
1550 #[cfg(feature = "jq")]
1551 #[test]
1552 fn test_apply_jq_filter_length() {
1553 let json = r#"{"items": [1, 2, 3, 4, 5]}"#;
1554 let result = apply_jq_filter(json, ".items | length").unwrap();
1555 let parsed: Value = serde_json::from_str(&result).unwrap();
1556 assert_eq!(parsed, serde_json::json!(5));
1557 }
1558}