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 clap::ArgMatches;
12use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
13use reqwest::Method;
14use serde_json::Value;
15use std::collections::HashMap;
16use std::str::FromStr;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum AuthScheme {
21 Bearer,
22 Basic,
23 Token,
24 DSN,
25 ApiKey,
26 Custom(String),
27}
28
29impl From<&str> for AuthScheme {
30 fn from(s: &str) -> Self {
31 match s.to_lowercase().as_str() {
32 constants::AUTH_SCHEME_BEARER => Self::Bearer,
33 constants::AUTH_SCHEME_BASIC => Self::Basic,
34 "token" => Self::Token,
35 "dsn" => Self::DSN,
36 constants::AUTH_SCHEME_APIKEY => Self::ApiKey,
37 _ => Self::Custom(s.to_string()),
38 }
39 }
40}
41
42const MAX_TABLE_ROWS: usize = 1000;
44
45fn extract_server_var_args(matches: &ArgMatches) -> Vec<String> {
49 matches
50 .try_get_many::<String>("server-var")
51 .ok()
52 .flatten()
53 .map(|values| values.cloned().collect())
54 .unwrap_or_default()
55}
56
57fn build_http_client() -> Result<reqwest::Client, Error> {
59 reqwest::Client::builder()
60 .timeout(std::time::Duration::from_secs(30))
61 .build()
62 .map_err(|e| {
63 Error::request_failed(
64 reqwest::StatusCode::INTERNAL_SERVER_ERROR,
65 format!("Failed to create HTTP client: {e}"),
66 )
67 })
68}
69
70fn extract_request_body(
72 operation: &CachedCommand,
73 matches: &ArgMatches,
74) -> Result<Option<String>, Error> {
75 if operation.request_body.is_none() {
76 return Ok(None);
77 }
78
79 let mut current_matches = matches;
81 while let Some((_name, sub_matches)) = current_matches.subcommand() {
82 current_matches = sub_matches;
83 }
84
85 if let Some(body_value) = current_matches.get_one::<String>("body") {
86 let _json_body: Value = serde_json::from_str(body_value)
88 .map_err(|e| Error::invalid_json_body(e.to_string()))?;
89 Ok(Some(body_value.clone()))
90 } else {
91 Ok(None)
92 }
93}
94
95fn handle_dry_run(
97 dry_run: bool,
98 method: &reqwest::Method,
99 url: &str,
100 headers: &reqwest::header::HeaderMap,
101 body: Option<&str>,
102 operation: &CachedCommand,
103 capture_output: bool,
104) -> Result<Option<String>, Error> {
105 if !dry_run {
106 return Ok(None);
107 }
108
109 let headers_map: HashMap<String, String> = headers
110 .iter()
111 .map(|(k, v)| {
112 let value = if is_sensitive_header(k.as_str()) {
113 "<REDACTED>".to_string()
114 } else {
115 v.to_str().unwrap_or("<binary>").to_string()
116 };
117 (k.as_str().to_string(), value)
118 })
119 .collect();
120
121 let dry_run_info = serde_json::json!({
122 "dry_run": true,
123 "method": method.to_string(),
124 "url": url,
125 "headers": headers_map,
126 "body": body,
127 "operation_id": operation.operation_id
128 });
129
130 let output = serde_json::to_string_pretty(&dry_run_info).map_err(|e| {
131 Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
132 })?;
133
134 if capture_output {
135 Ok(Some(output))
136 } else {
137 println!("{output}");
138 Ok(None)
139 }
140}
141
142async fn send_request(
144 request: reqwest::RequestBuilder,
145) -> Result<(reqwest::StatusCode, HashMap<String, String>, String), Error> {
146 let response = request
147 .send()
148 .await
149 .map_err(|e| Error::network_request_failed(e.to_string()))?;
150
151 let status = response.status();
152 let response_headers: HashMap<String, String> = response
153 .headers()
154 .iter()
155 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
156 .collect();
157
158 let response_text = response
159 .text()
160 .await
161 .map_err(|e| Error::response_read_error(e.to_string()))?;
162
163 Ok((status, response_headers, response_text))
164}
165
166fn handle_http_error(
168 status: reqwest::StatusCode,
169 response_text: String,
170 spec: &CachedSpec,
171 operation: &CachedCommand,
172) -> Error {
173 let api_name = spec.name.clone();
174 let operation_id = Some(operation.operation_id.clone());
175
176 let security_schemes: Vec<String> = operation
177 .security_requirements
178 .iter()
179 .filter_map(|scheme_name| {
180 spec.security_schemes
181 .get(scheme_name)
182 .and_then(|scheme| scheme.aperture_secret.as_ref())
183 .map(|aperture_secret| aperture_secret.name.clone())
184 })
185 .collect();
186
187 Error::http_error_with_context(
188 status.as_u16(),
189 if response_text.is_empty() {
190 constants::EMPTY_RESPONSE.to_string()
191 } else {
192 response_text
193 },
194 api_name,
195 operation_id,
196 &security_schemes,
197 )
198}
199
200fn prepare_cache_context(
202 cache_config: Option<&CacheConfig>,
203 spec_name: &str,
204 operation_id: &str,
205 method: &reqwest::Method,
206 url: &str,
207 headers: &reqwest::header::HeaderMap,
208 body: Option<&str>,
209) -> Result<Option<(CacheKey, ResponseCache)>, Error> {
210 if let Some(cache_cfg) = cache_config {
211 if cache_cfg.enabled {
212 let header_map: HashMap<String, String> = headers
213 .iter()
214 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
215 .collect();
216
217 let cache_key = CacheKey::from_request(
218 spec_name,
219 operation_id,
220 method.as_ref(),
221 url,
222 &header_map,
223 body,
224 )?;
225
226 let response_cache = ResponseCache::new(cache_cfg.clone())?;
227 Ok(Some((cache_key, response_cache)))
228 } else {
229 Ok(None)
230 }
231 } else {
232 Ok(None)
233 }
234}
235
236async fn check_cache(
238 cache_context: Option<&(CacheKey, ResponseCache)>,
239) -> Result<Option<CachedResponse>, Error> {
240 if let Some((cache_key, response_cache)) = cache_context {
241 response_cache.get(cache_key).await
242 } else {
243 Ok(None)
244 }
245}
246
247#[allow(clippy::too_many_arguments)]
249async fn store_in_cache(
250 cache_context: Option<(CacheKey, ResponseCache)>,
251 response_text: &str,
252 status: reqwest::StatusCode,
253 response_headers: &HashMap<String, String>,
254 method: reqwest::Method,
255 url: String,
256 headers: &reqwest::header::HeaderMap,
257 body: Option<&str>,
258 cache_config: Option<&CacheConfig>,
259) -> Result<(), Error> {
260 if let Some((cache_key, response_cache)) = cache_context {
261 let cached_request_info = CachedRequestInfo {
262 method: method.to_string(),
263 url,
264 headers: headers
265 .iter()
266 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
267 .collect(),
268 body_hash: body.map(|b| {
269 use sha2::{Digest, Sha256};
270 let mut hasher = Sha256::new();
271 hasher.update(b.as_bytes());
272 format!("{:x}", hasher.finalize())
273 }),
274 };
275
276 let cache_ttl = cache_config.and_then(|cfg| {
277 if cfg.default_ttl.as_secs() > 0 {
278 Some(cfg.default_ttl)
279 } else {
280 None
281 }
282 });
283
284 response_cache
285 .store(
286 &cache_key,
287 response_text,
288 status.as_u16(),
289 response_headers,
290 cached_request_info,
291 cache_ttl,
292 )
293 .await?;
294 }
295 Ok(())
296}
297
298#[allow(clippy::too_many_lines)]
325#[allow(clippy::too_many_arguments)]
326#[allow(clippy::missing_panics_doc)]
327#[allow(clippy::missing_errors_doc)]
328pub async fn execute_request(
329 spec: &CachedSpec,
330 matches: &ArgMatches,
331 base_url: Option<&str>,
332 dry_run: bool,
333 idempotency_key: Option<&str>,
334 global_config: Option<&GlobalConfig>,
335 output_format: &OutputFormat,
336 jq_filter: Option<&str>,
337 cache_config: Option<&CacheConfig>,
338 capture_output: bool,
339) -> Result<Option<String>, Error> {
340 let operation = find_operation(spec, matches)?;
342
343 let server_var_args = extract_server_var_args(matches);
345
346 let resolver = BaseUrlResolver::new(spec);
348 let resolver = if let Some(config) = global_config {
349 resolver.with_global_config(config)
350 } else {
351 resolver
352 };
353 let base_url = resolver.resolve_with_variables(base_url, &server_var_args)?;
354
355 let url = build_url(&base_url, &operation.path, operation, matches)?;
357
358 let client = build_http_client()?;
360
361 let mut headers = build_headers(spec, operation, matches, &spec.name, global_config)?;
363
364 if let Some(key) = idempotency_key {
366 headers.insert(
367 HeaderName::from_static("idempotency-key"),
368 HeaderValue::from_str(key).map_err(|_| Error::invalid_idempotency_key())?,
369 );
370 }
371
372 let method = Method::from_str(&operation.method)
374 .map_err(|_| Error::invalid_http_method(&operation.method))?;
375
376 let headers_clone = headers.clone(); let mut request = client.request(method.clone(), &url).headers(headers);
378
379 let request_body = extract_request_body(operation, matches)?;
381 if let Some(ref body) = request_body {
382 let json_body: Value = serde_json::from_str(body)
383 .expect("JSON body was validated in extract_request_body, parsing should succeed");
384 request = request.json(&json_body);
385 }
386
387 let cache_context = prepare_cache_context(
389 cache_config,
390 &spec.name,
391 &operation.operation_id,
392 &method,
393 &url,
394 &headers_clone,
395 request_body.as_deref(),
396 )?;
397
398 if let Some(cached_response) = check_cache(cache_context.as_ref()).await? {
400 let output = print_formatted_response(
401 &cached_response.body,
402 output_format,
403 jq_filter,
404 capture_output,
405 )?;
406 return Ok(output);
407 }
408
409 if let Some(output) = handle_dry_run(
411 dry_run,
412 &method,
413 &url,
414 &headers_clone,
415 request_body.as_deref(),
416 operation,
417 capture_output,
418 )? {
419 return Ok(Some(output));
420 }
421 if dry_run {
422 return Ok(None);
423 }
424
425 let (status, response_headers, response_text) = send_request(request).await?;
427
428 if !status.is_success() {
430 return Err(handle_http_error(status, response_text, spec, operation));
431 }
432
433 store_in_cache(
435 cache_context,
436 &response_text,
437 status,
438 &response_headers,
439 method,
440 url,
441 &headers_clone,
442 request_body.as_deref(),
443 cache_config,
444 )
445 .await?;
446
447 if response_text.is_empty() {
449 Ok(None)
450 } else {
451 print_formatted_response(&response_text, output_format, jq_filter, capture_output)
452 }
453}
454
455fn find_operation<'a>(
457 spec: &'a CachedSpec,
458 matches: &ArgMatches,
459) -> Result<&'a CachedCommand, Error> {
460 let mut current_matches = matches;
462 let mut subcommand_path = Vec::new();
463
464 while let Some((name, sub_matches)) = current_matches.subcommand() {
465 subcommand_path.push(name);
466 current_matches = sub_matches;
467 }
468
469 if let Some(operation_name) = subcommand_path.last() {
472 for command in &spec.commands {
473 let kebab_id = to_kebab_case(&command.operation_id);
475 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
476 return Ok(command);
477 }
478 }
479 }
480
481 let operation_name = subcommand_path
482 .last()
483 .map_or("unknown".to_string(), ToString::to_string);
484 Err(Error::operation_not_found(operation_name))
485}
486
487fn build_url(
492 base_url: &str,
493 path_template: &str,
494 operation: &CachedCommand,
495 matches: &ArgMatches,
496) -> Result<String, Error> {
497 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
498
499 let mut current_matches = matches;
501 while let Some((_name, sub_matches)) = current_matches.subcommand() {
502 current_matches = sub_matches;
503 }
504
505 let mut start = 0;
508 while let Some(open) = url[start..].find('{') {
509 let open_pos = start + open;
510 if let Some(close) = url[open_pos..].find('}') {
511 let close_pos = open_pos + close;
512 let param_name = &url[open_pos + 1..close_pos];
513
514 if let Some(value) = current_matches
515 .try_get_one::<String>(param_name)
516 .ok()
517 .flatten()
518 {
519 url.replace_range(open_pos..=close_pos, value);
520 start = open_pos + value.len();
521 } else {
522 return Err(Error::missing_path_parameter(param_name));
523 }
524 } else {
525 break;
526 }
527 }
528
529 let mut query_params = Vec::new();
531 for arg in current_matches.ids() {
532 let arg_str = arg.as_str();
533 let is_query_param = operation
535 .parameters
536 .iter()
537 .any(|p| p.name == arg_str && p.location == "query");
538 if is_query_param {
539 if let Some(value) = current_matches.get_one::<String>(arg_str) {
540 query_params.push(format!("{}={}", arg_str, urlencoding::encode(value)));
541 }
542 }
543 }
544
545 if !query_params.is_empty() {
546 url.push('?');
547 url.push_str(&query_params.join("&"));
548 }
549
550 Ok(url)
551}
552
553fn build_headers(
555 spec: &CachedSpec,
556 operation: &CachedCommand,
557 matches: &ArgMatches,
558 api_name: &str,
559 global_config: Option<&GlobalConfig>,
560) -> Result<HeaderMap, Error> {
561 let mut headers = HeaderMap::new();
562
563 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
565 headers.insert(
566 constants::HEADER_ACCEPT,
567 HeaderValue::from_static(constants::CONTENT_TYPE_JSON),
568 );
569
570 let mut current_matches = matches;
572 while let Some((_name, sub_matches)) = current_matches.subcommand() {
573 current_matches = sub_matches;
574 }
575
576 for param in &operation.parameters {
578 if param.location == "header" {
579 if let Some(value) = current_matches.get_one::<String>(¶m.name) {
580 let header_name = HeaderName::from_str(¶m.name)
581 .map_err(|e| Error::invalid_header_name(¶m.name, e.to_string()))?;
582 let header_value = HeaderValue::from_str(value)
583 .map_err(|e| Error::invalid_header_value(¶m.name, e.to_string()))?;
584 headers.insert(header_name, header_value);
585 }
586 }
587 }
588
589 for security_scheme_name in &operation.security_requirements {
591 if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
592 add_authentication_header(&mut headers, security_scheme, api_name, global_config)?;
593 }
594 }
595
596 if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
599 for header_str in custom_headers {
600 let (name, value) = parse_custom_header(header_str)?;
601 let header_name = HeaderName::from_str(&name)
602 .map_err(|e| Error::invalid_header_name(&name, e.to_string()))?;
603 let header_value = HeaderValue::from_str(&value)
604 .map_err(|e| Error::invalid_header_value(&name, e.to_string()))?;
605 headers.insert(header_name, header_value);
606 }
607 }
608
609 Ok(headers)
610}
611
612fn validate_header_value(name: &str, value: &str) -> Result<(), Error> {
614 if value.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
615 return Err(Error::invalid_header_value(
616 name,
617 "Header value contains invalid control characters (newline, carriage return, or null)",
618 ));
619 }
620 Ok(())
621}
622
623fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
625 let colon_pos = header_str
627 .find(':')
628 .ok_or_else(|| Error::invalid_header_format(header_str))?;
629
630 let name = header_str[..colon_pos].trim();
631 let value = header_str[colon_pos + 1..].trim();
632
633 if name.is_empty() {
634 return Err(Error::empty_header_name());
635 }
636
637 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
639 let var_name = &value[2..value.len() - 1];
641 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
642 } else {
643 value.to_string()
644 };
645
646 validate_header_value(name, &expanded_value)?;
648
649 Ok((name.to_string(), expanded_value))
650}
651
652fn is_sensitive_header(header_name: &str) -> bool {
654 let name_lower = header_name.to_lowercase();
655 matches!(
656 name_lower.as_str(),
657 "authorization" | "proxy-authorization" | "x-api-key" | "x-api-token" | "x-auth-token"
658 )
659}
660
661#[allow(clippy::too_many_lines)]
663fn add_authentication_header(
664 headers: &mut HeaderMap,
665 security_scheme: &CachedSecurityScheme,
666 api_name: &str,
667 global_config: Option<&GlobalConfig>,
668) -> Result<(), Error> {
669 if std::env::var("RUST_LOG").is_ok() {
671 eprintln!(
672 "[DEBUG] Adding authentication header for scheme: {} (type: {})",
673 security_scheme.name, security_scheme.scheme_type
674 );
675 }
676
677 let secret_config = global_config
679 .and_then(|config| config.api_configs.get(api_name))
680 .and_then(|api_config| api_config.secrets.get(&security_scheme.name));
681
682 let (secret_value, env_var_name) = if let Some(config_secret) = secret_config {
683 let secret_value = std::env::var(&config_secret.name)
685 .map_err(|_| Error::secret_not_set(&security_scheme.name, &config_secret.name))?;
686 (secret_value, config_secret.name.clone())
687 } else if let Some(aperture_secret) = &security_scheme.aperture_secret {
688 let secret_value = std::env::var(&aperture_secret.name)
690 .map_err(|_| Error::secret_not_set(&security_scheme.name, &aperture_secret.name))?;
691 (secret_value, aperture_secret.name.clone())
692 } else {
693 return Ok(());
695 };
696
697 if std::env::var("RUST_LOG").is_ok() {
699 let source = if secret_config.is_some() {
700 "config"
701 } else {
702 "x-aperture-secret"
703 };
704 eprintln!(
705 "[DEBUG] Using secret from {source} for scheme '{}': env var '{env_var_name}'",
706 security_scheme.name
707 );
708 }
709
710 validate_header_value(constants::HEADER_AUTHORIZATION, &secret_value)?;
712
713 match security_scheme.scheme_type.as_str() {
715 constants::AUTH_SCHEME_APIKEY => {
716 let (Some(location), Some(param_name)) =
717 (&security_scheme.location, &security_scheme.parameter_name)
718 else {
719 return Ok(());
720 };
721
722 if location == "header" {
723 let header_name = HeaderName::from_str(param_name)
724 .map_err(|e| Error::invalid_header_name(param_name, e.to_string()))?;
725 let header_value = HeaderValue::from_str(&secret_value)
726 .map_err(|e| Error::invalid_header_value(param_name, e.to_string()))?;
727 headers.insert(header_name, header_value);
728 }
729 }
731 "http" => {
732 if let Some(scheme_str) = &security_scheme.scheme {
733 let auth_scheme: AuthScheme = scheme_str.as_str().into();
734 let auth_value = match &auth_scheme {
735 AuthScheme::Bearer => {
736 format!("Bearer {secret_value}")
737 }
738 AuthScheme::Basic => {
739 use base64::{engine::general_purpose, Engine as _};
743 let encoded = general_purpose::STANDARD.encode(&secret_value);
744 format!("Basic {encoded}")
745 }
746 AuthScheme::Token
747 | AuthScheme::DSN
748 | AuthScheme::ApiKey
749 | AuthScheme::Custom(_) => {
750 format!("{scheme_str} {secret_value}")
754 }
755 };
756
757 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
758 Error::invalid_header_value(constants::HEADER_AUTHORIZATION, e.to_string())
759 })?;
760 headers.insert(constants::HEADER_AUTHORIZATION, header_value);
761
762 if std::env::var("RUST_LOG").is_ok() {
764 match &auth_scheme {
765 AuthScheme::Bearer => {
766 eprintln!("[DEBUG] Added Bearer authentication header");
767 }
768 AuthScheme::Basic => {
769 eprintln!("[DEBUG] Added Basic authentication header (base64 encoded)");
770 }
771 _ => {
772 eprintln!(
773 "[DEBUG] Added custom HTTP auth header with scheme: {scheme_str}"
774 );
775 }
776 }
777 }
778 }
779 }
780 _ => {
781 return Err(Error::unsupported_security_scheme(
782 &security_scheme.scheme_type,
783 ));
784 }
785 }
786
787 Ok(())
788}
789
790fn print_formatted_response(
792 response_text: &str,
793 output_format: &OutputFormat,
794 jq_filter: Option<&str>,
795 capture_output: bool,
796) -> Result<Option<String>, Error> {
797 let processed_text = if let Some(filter) = jq_filter {
799 apply_jq_filter(response_text, filter)?
800 } else {
801 response_text.to_string()
802 };
803
804 match output_format {
805 OutputFormat::Json => {
806 let output = serde_json::from_str::<Value>(&processed_text)
808 .ok()
809 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
810 .unwrap_or_else(|| processed_text.clone());
811
812 if capture_output {
813 return Ok(Some(output));
814 }
815 println!("{output}");
816 }
817 OutputFormat::Yaml => {
818 let output = serde_json::from_str::<Value>(&processed_text)
820 .ok()
821 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
822 .unwrap_or_else(|| processed_text.clone());
823
824 if capture_output {
825 return Ok(Some(output));
826 }
827 println!("{output}");
828 }
829 OutputFormat::Table => {
830 if let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) {
832 let table_output = print_as_table(&json_value, capture_output)?;
833 if capture_output {
834 return Ok(table_output);
835 }
836 } else {
837 if capture_output {
839 return Ok(Some(processed_text));
840 }
841 println!("{processed_text}");
842 }
843 }
844 }
845
846 Ok(None)
847}
848
849#[derive(tabled::Tabled)]
851struct TableRow {
852 #[tabled(rename = "Key")]
853 key: String,
854 #[tabled(rename = "Value")]
855 value: String,
856}
857
858#[derive(tabled::Tabled)]
859struct KeyValue {
860 #[tabled(rename = "Key")]
861 key: String,
862 #[tabled(rename = "Value")]
863 value: String,
864}
865
866#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
868fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
869 use std::collections::BTreeMap;
870 use tabled::Table;
871
872 match json_value {
873 Value::Array(items) => {
874 if items.is_empty() {
875 if capture_output {
876 return Ok(Some(constants::EMPTY_ARRAY.to_string()));
877 }
878 println!("{}", constants::EMPTY_ARRAY);
879 return Ok(None);
880 }
881
882 if items.len() > MAX_TABLE_ROWS {
884 let msg1 = format!(
885 "Array too large: {} items (max {} for table display)",
886 items.len(),
887 MAX_TABLE_ROWS
888 );
889 let msg2 = "Use --format json or --jq to process the full data";
890
891 if capture_output {
892 return Ok(Some(format!("{msg1}\n{msg2}")));
893 }
894 println!("{msg1}");
895 println!("{msg2}");
896 return Ok(None);
897 }
898
899 if let Some(Value::Object(_)) = items.first() {
901 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
903
904 for item in items {
905 if let Value::Object(obj) = item {
906 let mut row = BTreeMap::new();
907 for (key, value) in obj {
908 row.insert(key.clone(), format_value_for_table(value));
909 }
910 table_data.push(row);
911 }
912 }
913
914 if !table_data.is_empty() {
915 let mut rows = Vec::new();
918 for (i, row) in table_data.iter().enumerate() {
919 if i > 0 {
920 rows.push(TableRow {
921 key: "---".to_string(),
922 value: "---".to_string(),
923 });
924 }
925 for (key, value) in row {
926 rows.push(TableRow {
927 key: key.clone(),
928 value: value.clone(),
929 });
930 }
931 }
932
933 let table = Table::new(&rows);
934 if capture_output {
935 return Ok(Some(table.to_string()));
936 }
937 println!("{table}");
938 return Ok(None);
939 }
940 }
941
942 if capture_output {
944 let mut output = String::new();
945 for (i, item) in items.iter().enumerate() {
946 use std::fmt::Write;
947 writeln!(&mut output, "{}: {}", i, format_value_for_table(item)).unwrap();
948 }
949 return Ok(Some(output.trim_end().to_string()));
950 }
951 for (i, item) in items.iter().enumerate() {
952 println!("{}: {}", i, format_value_for_table(item));
953 }
954 }
955 Value::Object(obj) => {
956 if obj.len() > MAX_TABLE_ROWS {
958 let msg1 = format!(
959 "Object too large: {} fields (max {} for table display)",
960 obj.len(),
961 MAX_TABLE_ROWS
962 );
963 let msg2 = "Use --format json or --jq to process the full data";
964
965 if capture_output {
966 return Ok(Some(format!("{msg1}\n{msg2}")));
967 }
968 println!("{msg1}");
969 println!("{msg2}");
970 return Ok(None);
971 }
972
973 let rows: Vec<KeyValue> = obj
975 .iter()
976 .map(|(key, value)| KeyValue {
977 key: key.clone(),
978 value: format_value_for_table(value),
979 })
980 .collect();
981
982 let table = Table::new(&rows);
983 if capture_output {
984 return Ok(Some(table.to_string()));
985 }
986 println!("{table}");
987 }
988 _ => {
989 let formatted = format_value_for_table(json_value);
991 if capture_output {
992 return Ok(Some(formatted));
993 }
994 println!("{formatted}");
995 }
996 }
997
998 Ok(None)
999}
1000
1001fn format_value_for_table(value: &Value) -> String {
1003 match value {
1004 Value::Null => constants::NULL_VALUE.to_string(),
1005 Value::Bool(b) => b.to_string(),
1006 Value::Number(n) => n.to_string(),
1007 Value::String(s) => s.clone(),
1008 Value::Array(arr) => {
1009 if arr.len() <= 3 {
1010 format!(
1011 "[{}]",
1012 arr.iter()
1013 .map(format_value_for_table)
1014 .collect::<Vec<_>>()
1015 .join(", ")
1016 )
1017 } else {
1018 format!("[{} items]", arr.len())
1019 }
1020 }
1021 Value::Object(obj) => {
1022 if obj.len() <= 2 {
1023 format!(
1024 "{{{}}}",
1025 obj.iter()
1026 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
1027 .collect::<Vec<_>>()
1028 .join(", ")
1029 )
1030 } else {
1031 format!("{{object with {} fields}}", obj.len())
1032 }
1033 }
1034 }
1035}
1036
1037pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
1046 let json_value: Value = serde_json::from_str(response_text)
1048 .map_err(|e| Error::jq_filter_error(filter, format!("Response is not valid JSON: {e}")))?;
1049
1050 #[cfg(feature = "jq")]
1051 {
1052 use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
1054 use jaq_parse::parse;
1055 use jaq_std::std;
1056
1057 let (expr, errs) = parse(filter, jaq_parse::main());
1059 if !errs.is_empty() {
1060 return Err(Error::jq_filter_error(
1061 filter,
1062 format!("Parse error in jq expression: {}", errs[0]),
1063 ));
1064 }
1065
1066 let mut ctx = ParseCtx::new(Vec::new());
1068 ctx.insert_defs(std());
1069 let filter =
1070 ctx.compile(expr.expect("JQ expression was already validated, should be Some"));
1071
1072 let jaq_value = serde_json_to_jaq_val(&json_value);
1074
1075 let inputs = RcIter::new(core::iter::empty());
1077 let ctx = Ctx::new([], &inputs);
1078 let results: Result<Vec<Val>, _> = filter.run((ctx, jaq_value.into())).collect();
1079
1080 match results {
1081 Ok(vals) => {
1082 if vals.is_empty() {
1083 Ok(constants::NULL_VALUE.to_string())
1084 } else if vals.len() == 1 {
1085 let json_val = jaq_val_to_serde_json(&vals[0]);
1087 serde_json::to_string_pretty(&json_val).map_err(|e| {
1088 Error::serialization_error(format!("Failed to serialize result: {e}"))
1089 })
1090 } else {
1091 let json_vals: Vec<Value> = vals.iter().map(jaq_val_to_serde_json).collect();
1093 let array = Value::Array(json_vals);
1094 serde_json::to_string_pretty(&array).map_err(|e| {
1095 Error::serialization_error(format!("Failed to serialize results: {e}"))
1096 })
1097 }
1098 }
1099 Err(e) => Err(Error::jq_filter_error(
1100 filter,
1101 format!("Filter execution error: {e}"),
1102 )),
1103 }
1104 }
1105
1106 #[cfg(not(feature = "jq"))]
1107 {
1108 apply_basic_jq_filter(&json_value, filter)
1110 }
1111}
1112
1113#[cfg(not(feature = "jq"))]
1114fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
1116 let uses_advanced_features = filter.contains('[')
1118 || filter.contains(']')
1119 || filter.contains('|')
1120 || filter.contains('(')
1121 || filter.contains(')')
1122 || filter.contains("select")
1123 || filter.contains("map")
1124 || filter.contains("length");
1125
1126 if uses_advanced_features {
1127 eprintln!(
1128 "{} Advanced JQ features require building with --features jq",
1129 crate::constants::MSG_WARNING_PREFIX
1130 );
1131 eprintln!(" Currently only basic field access is supported (e.g., '.field', '.nested.field')");
1132 eprintln!(" To enable full JQ support: cargo install aperture-cli --features jq");
1133 }
1134
1135 let result = match filter {
1136 "." => json_value.clone(),
1137 ".[]" => {
1138 match json_value {
1140 Value::Array(arr) => {
1141 Value::Array(arr.clone())
1143 }
1144 Value::Object(obj) => {
1145 Value::Array(obj.values().cloned().collect())
1147 }
1148 _ => Value::Null,
1149 }
1150 }
1151 ".length" => {
1152 match json_value {
1154 Value::Array(arr) => Value::Number(arr.len().into()),
1155 Value::Object(obj) => Value::Number(obj.len().into()),
1156 Value::String(s) => Value::Number(s.len().into()),
1157 _ => Value::Null,
1158 }
1159 }
1160 filter if filter.starts_with(".[].") => {
1161 let field_path = &filter[4..]; match json_value {
1164 Value::Array(arr) => {
1165 let mapped: Vec<Value> = arr
1166 .iter()
1167 .map(|item| get_nested_field(item, field_path))
1168 .collect();
1169 Value::Array(mapped)
1170 }
1171 _ => Value::Null,
1172 }
1173 }
1174 filter if filter.starts_with('.') => {
1175 let field_path = &filter[1..]; get_nested_field(json_value, field_path)
1178 }
1179 _ => {
1180 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."));
1181 }
1182 };
1183
1184 serde_json::to_string_pretty(&result).map_err(|e| {
1185 Error::serialization_error(format!("Failed to serialize filtered result: {e}"))
1186 })
1187}
1188
1189#[cfg(not(feature = "jq"))]
1190fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
1192 let parts: Vec<&str> = field_path.split('.').collect();
1193 let mut current = json_value;
1194
1195 for part in parts {
1196 if part.is_empty() {
1197 continue;
1198 }
1199
1200 if part.starts_with('[') && part.ends_with(']') {
1202 let index_str = &part[1..part.len() - 1];
1203 if let Ok(index) = index_str.parse::<usize>() {
1204 match current {
1205 Value::Array(arr) => {
1206 if let Some(item) = arr.get(index) {
1207 current = item;
1208 } else {
1209 return Value::Null;
1210 }
1211 }
1212 _ => return Value::Null,
1213 }
1214 } else {
1215 return Value::Null;
1216 }
1217 continue;
1218 }
1219
1220 match current {
1221 Value::Object(obj) => {
1222 if let Some(field) = obj.get(part) {
1223 current = field;
1224 } else {
1225 return Value::Null;
1226 }
1227 }
1228 Value::Array(arr) => {
1229 if let Ok(index) = part.parse::<usize>() {
1231 if let Some(item) = arr.get(index) {
1232 current = item;
1233 } else {
1234 return Value::Null;
1235 }
1236 } else {
1237 return Value::Null;
1238 }
1239 }
1240 _ => return Value::Null,
1241 }
1242 }
1243
1244 current.clone()
1245}
1246
1247#[cfg(feature = "jq")]
1248fn serde_json_to_jaq_val(value: &Value) -> jaq_interpret::Val {
1250 use jaq_interpret::Val;
1251 use std::rc::Rc;
1252
1253 match value {
1254 Value::Null => Val::Null,
1255 Value::Bool(b) => Val::Bool(*b),
1256 Value::Number(n) => {
1257 if let Some(i) = n.as_i64() {
1258 if let Ok(isize_val) = isize::try_from(i) {
1260 Val::Int(isize_val)
1261 } else {
1262 Val::Float(i as f64)
1264 }
1265 } else if let Some(f) = n.as_f64() {
1266 Val::Float(f)
1267 } else {
1268 Val::Null
1269 }
1270 }
1271 Value::String(s) => Val::Str(s.clone().into()),
1272 Value::Array(arr) => {
1273 let jaq_arr: Vec<Val> = arr.iter().map(serde_json_to_jaq_val).collect();
1274 Val::Arr(Rc::new(jaq_arr))
1275 }
1276 Value::Object(obj) => {
1277 let mut jaq_obj = indexmap::IndexMap::with_hasher(ahash::RandomState::new());
1278 for (k, v) in obj {
1279 jaq_obj.insert(Rc::new(k.clone()), serde_json_to_jaq_val(v));
1280 }
1281 Val::Obj(Rc::new(jaq_obj))
1282 }
1283 }
1284}
1285
1286#[cfg(feature = "jq")]
1287fn jaq_val_to_serde_json(val: &jaq_interpret::Val) -> Value {
1289 use jaq_interpret::Val;
1290
1291 match val {
1292 Val::Null => Value::Null,
1293 Val::Bool(b) => Value::Bool(*b),
1294 Val::Int(i) => {
1295 Value::Number((*i as i64).into())
1297 }
1298 Val::Float(f) => {
1299 if let Some(num) = serde_json::Number::from_f64(*f) {
1300 Value::Number(num)
1301 } else {
1302 Value::Null
1303 }
1304 }
1305 Val::Str(s) => Value::String(s.to_string()),
1306 Val::Arr(arr) => {
1307 let json_arr: Vec<Value> = arr.iter().map(jaq_val_to_serde_json).collect();
1308 Value::Array(json_arr)
1309 }
1310 Val::Obj(obj) => {
1311 let mut json_obj = serde_json::Map::new();
1312 for (k, v) in obj.iter() {
1313 json_obj.insert(k.to_string(), jaq_val_to_serde_json(v));
1314 }
1315 Value::Object(json_obj)
1316 }
1317 _ => Value::Null, }
1319}