aperture_cli/engine/
executor.rs

1use crate::cache::models::{CachedCommand, CachedSecurityScheme, CachedSpec};
2use crate::cli::OutputFormat;
3use crate::config::models::GlobalConfig;
4use crate::config::url_resolver::BaseUrlResolver;
5use crate::error::Error;
6use crate::response_cache::{CacheConfig, CacheKey, CachedRequestInfo, ResponseCache};
7use crate::utils::to_kebab_case;
8use clap::ArgMatches;
9use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
10use reqwest::Method;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::str::FromStr;
14
15/// Represents supported authentication schemes
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AuthScheme {
18    Bearer,
19    Basic,
20    Token,
21    DSN,
22    ApiKey,
23    Custom(String),
24}
25
26impl From<&str> for AuthScheme {
27    fn from(s: &str) -> Self {
28        match s.to_lowercase().as_str() {
29            "bearer" => Self::Bearer,
30            "basic" => Self::Basic,
31            "token" => Self::Token,
32            "dsn" => Self::DSN,
33            "apikey" => Self::ApiKey,
34            _ => Self::Custom(s.to_string()),
35        }
36    }
37}
38
39/// Maximum number of rows to display in table format to prevent memory exhaustion
40const MAX_TABLE_ROWS: usize = 1000;
41
42/// Executes HTTP requests based on parsed CLI arguments and cached spec data.
43///
44/// This module handles the mapping from CLI arguments back to API operations,
45/// resolves authentication secrets, builds HTTP requests, and validates responses.
46///
47/// # Arguments
48/// * `spec` - The cached specification containing operation details
49/// * `matches` - Parsed CLI arguments from clap
50/// * `base_url` - Optional base URL override. If None, uses `BaseUrlResolver`
51/// * `dry_run` - If true, show request details without executing
52/// * `idempotency_key` - Optional idempotency key for safe retries
53/// * `global_config` - Optional global configuration for URL resolution
54/// * `output_format` - Format for response output (json, yaml, table)
55/// * `jq_filter` - Optional JQ filter expression to apply to response
56/// * `cache_config` - Optional cache configuration for response caching
57/// * `capture_output` - If true, captures output and returns it instead of printing to stdout
58///
59/// # Returns
60/// * `Ok(Option<String>)` - Request executed successfully. Returns Some(output) if `capture_output` is true
61/// * `Err(Error)` - Request failed or validation error
62///
63/// # Errors
64/// Returns errors for authentication failures, network issues, response validation, or JQ filter errors
65///
66/// # Panics
67/// Panics if JSON serialization of dry-run information fails (extremely unlikely)
68#[allow(clippy::too_many_lines)]
69#[allow(clippy::too_many_arguments)]
70pub async fn execute_request(
71    spec: &CachedSpec,
72    matches: &ArgMatches,
73    base_url: Option<&str>,
74    dry_run: bool,
75    idempotency_key: Option<&str>,
76    global_config: Option<&GlobalConfig>,
77    output_format: &OutputFormat,
78    jq_filter: Option<&str>,
79    cache_config: Option<&CacheConfig>,
80    capture_output: bool,
81) -> Result<Option<String>, Error> {
82    // Find the operation from the command hierarchy
83    let operation = find_operation(spec, matches)?;
84
85    // Extract server variable arguments from CLI matches
86    // Note: server-var is a global flag, so it may not be present in test scenarios
87    let server_var_args: Vec<String> = matches
88        .try_get_many::<String>("server-var")
89        .ok()
90        .flatten()
91        .map(|values| values.cloned().collect())
92        .unwrap_or_default();
93
94    // Resolve base URL using the new priority hierarchy with server variable support
95    let resolver = BaseUrlResolver::new(spec);
96    let resolver = if let Some(config) = global_config {
97        resolver.with_global_config(config)
98    } else {
99        resolver
100    };
101    let base_url = resolver.resolve_with_variables(base_url, &server_var_args)?;
102
103    // Build the full URL with path parameters
104    let url = build_url(&base_url, &operation.path, operation, matches)?;
105
106    // Create HTTP client with timeout
107    let client = reqwest::Client::builder()
108        .timeout(std::time::Duration::from_secs(30))
109        .build()
110        .map_err(|e| Error::RequestFailed {
111            reason: format!("Failed to create HTTP client: {e}"),
112        })?;
113
114    // Build headers including authentication and idempotency
115    let mut headers = build_headers(spec, operation, matches, &spec.name, global_config)?;
116
117    // Add idempotency key if provided
118    if let Some(key) = idempotency_key {
119        headers.insert(
120            HeaderName::from_static("idempotency-key"),
121            HeaderValue::from_str(key).map_err(|_| Error::InvalidIdempotencyKey)?,
122        );
123    }
124
125    // Build request
126    let method = Method::from_str(&operation.method).map_err(|_| Error::InvalidHttpMethod {
127        method: operation.method.clone(),
128    })?;
129
130    let headers_clone = headers.clone(); // For dry-run output
131    let mut request = client.request(method.clone(), &url).headers(headers);
132
133    // Add request body if present
134    // Get to the deepest subcommand matches
135    let mut current_matches = matches;
136    while let Some((_name, sub_matches)) = current_matches.subcommand() {
137        current_matches = sub_matches;
138    }
139
140    let request_body = if operation.request_body.is_some() {
141        if let Some(body_value) = current_matches.get_one::<String>("body") {
142            let json_body: Value =
143                serde_json::from_str(body_value).map_err(|e| Error::InvalidJsonBody {
144                    reason: e.to_string(),
145                })?;
146            request = request.json(&json_body);
147            Some(body_value.clone())
148        } else {
149            None
150        }
151    } else {
152        None
153    };
154
155    // Check cache for response if caching is enabled
156    let cache_key = if let Some(cache_cfg) = cache_config {
157        if cache_cfg.enabled {
158            // Create cache key from request details
159            let header_map: HashMap<String, String> = headers_clone
160                .iter()
161                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
162                .collect();
163
164            let cache_key = CacheKey::from_request(
165                &spec.name,
166                &operation.operation_id,
167                method.as_ref(),
168                &url,
169                &header_map,
170                request_body.as_deref(),
171            )?;
172
173            let response_cache = ResponseCache::new(cache_cfg.clone())?;
174
175            // Try to get cached response
176            if let Some(cached_response) = response_cache.get(&cache_key).await? {
177                // Use cached response
178                let output = print_formatted_response(
179                    &cached_response.body,
180                    output_format,
181                    jq_filter,
182                    capture_output,
183                )?;
184                return Ok(output);
185            }
186
187            Some((cache_key, response_cache))
188        } else {
189            None
190        }
191    } else {
192        None
193    };
194
195    // Handle dry-run mode - show request details without executing
196    if dry_run {
197        let dry_run_info = serde_json::json!({
198            "dry_run": true,
199            "method": operation.method,
200            "url": url,
201            "headers": headers_clone.iter().map(|(k, v)| (k.as_str(), v.to_str().unwrap_or("<binary>"))).collect::<std::collections::HashMap<_, _>>(),
202            "operation_id": operation.operation_id
203        });
204        let dry_run_output =
205            serde_json::to_string_pretty(&dry_run_info).map_err(|e| Error::SerializationError {
206                reason: format!("Failed to serialize dry-run info: {e}"),
207            })?;
208
209        if capture_output {
210            return Ok(Some(dry_run_output));
211        }
212        println!("{dry_run_output}");
213        return Ok(None);
214    }
215
216    // Execute request
217    let response = request.send().await.map_err(|e| Error::RequestFailed {
218        reason: e.to_string(),
219    })?;
220
221    let status = response.status();
222    let response_headers: HashMap<String, String> = response
223        .headers()
224        .iter()
225        .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
226        .collect();
227
228    let response_text = response
229        .text()
230        .await
231        .map_err(|e| Error::ResponseReadError {
232            reason: e.to_string(),
233        })?;
234
235    // Check if request was successful
236    if !status.is_success() {
237        // Gather context for enhanced error reporting
238        let api_name = spec.name.clone();
239        let operation_id = Some(operation.operation_id.clone());
240        let security_schemes: Vec<String> = operation
241            .security_requirements
242            .iter()
243            .filter_map(|scheme_name| {
244                spec.security_schemes
245                    .get(scheme_name)
246                    .and_then(|scheme| scheme.aperture_secret.as_ref())
247                    .map(|aperture_secret| aperture_secret.name.clone())
248            })
249            .collect();
250
251        return Err(Error::HttpErrorWithContext {
252            status: status.as_u16(),
253            body: if response_text.is_empty() {
254                "(empty response)".to_string()
255            } else {
256                response_text
257            },
258            api_name,
259            operation_id,
260            security_schemes,
261        });
262    }
263
264    // Store response in cache if caching is enabled
265    if let Some((cache_key, response_cache)) = cache_key {
266        // Create cached request info
267        let cached_request_info = CachedRequestInfo {
268            method: method.to_string(),
269            url: url.clone(),
270            headers: headers_clone
271                .iter()
272                .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
273                .collect(),
274            body_hash: request_body.as_ref().map(|body| {
275                use sha2::{Digest, Sha256};
276                let mut hasher = Sha256::new();
277                hasher.update(body.as_bytes());
278                format!("{:x}", hasher.finalize())
279            }),
280        };
281
282        // Store in cache with custom TTL if specified
283        let cache_ttl = cache_config.and_then(|cfg| {
284            if cfg.default_ttl.as_secs() > 0 {
285                Some(cfg.default_ttl)
286            } else {
287                None
288            }
289        });
290
291        let _ = response_cache
292            .store(
293                &cache_key,
294                &response_text,
295                status.as_u16(),
296                &response_headers,
297                cached_request_info,
298                cache_ttl,
299            )
300            .await;
301    }
302
303    // Print response in the requested format
304    if response_text.is_empty() {
305        Ok(None)
306    } else {
307        print_formatted_response(&response_text, output_format, jq_filter, capture_output)
308    }
309}
310
311/// Finds the operation from the command hierarchy
312fn find_operation<'a>(
313    spec: &'a CachedSpec,
314    matches: &ArgMatches,
315) -> Result<&'a CachedCommand, Error> {
316    // Get the subcommand path from matches
317    let mut current_matches = matches;
318    let mut subcommand_path = Vec::new();
319
320    while let Some((name, sub_matches)) = current_matches.subcommand() {
321        subcommand_path.push(name);
322        current_matches = sub_matches;
323    }
324
325    // For now, just find the first matching operation
326    // In a real implementation, we'd match based on the full path
327    if let Some(operation_name) = subcommand_path.last() {
328        for command in &spec.commands {
329            // Convert operation_id to kebab-case for comparison
330            let kebab_id = to_kebab_case(&command.operation_id);
331            if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
332                return Ok(command);
333            }
334        }
335    }
336
337    Err(Error::OperationNotFound)
338}
339
340/// Builds the full URL with path parameters substituted
341///
342/// Note: Server variable substitution is now handled by `BaseUrlResolver.resolve_with_variables()`
343/// before calling this function, so `base_url` should already have server variables resolved.
344fn build_url(
345    base_url: &str,
346    path_template: &str,
347    operation: &CachedCommand,
348    matches: &ArgMatches,
349) -> Result<String, Error> {
350    let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
351
352    // Get to the deepest subcommand matches
353    let mut current_matches = matches;
354    while let Some((_name, sub_matches)) = current_matches.subcommand() {
355        current_matches = sub_matches;
356    }
357
358    // Substitute path parameters
359    // Look for {param} patterns and replace with values from matches
360    let mut start = 0;
361    while let Some(open) = url[start..].find('{') {
362        let open_pos = start + open;
363        if let Some(close) = url[open_pos..].find('}') {
364            let close_pos = open_pos + close;
365            let param_name = &url[open_pos + 1..close_pos];
366
367            if let Some(value) = current_matches
368                .try_get_one::<String>(param_name)
369                .ok()
370                .flatten()
371            {
372                url.replace_range(open_pos..=close_pos, value);
373                start = open_pos + value.len();
374            } else {
375                return Err(Error::MissingPathParameter {
376                    name: param_name.to_string(),
377                });
378            }
379        } else {
380            break;
381        }
382    }
383
384    // Add query parameters
385    let mut query_params = Vec::new();
386    for arg in current_matches.ids() {
387        let arg_str = arg.as_str();
388        // Skip non-query args - only process query parameters from the operation
389        let is_query_param = operation
390            .parameters
391            .iter()
392            .any(|p| p.name == arg_str && p.location == "query");
393        if is_query_param {
394            if let Some(value) = current_matches.get_one::<String>(arg_str) {
395                query_params.push(format!("{}={}", arg_str, urlencoding::encode(value)));
396            }
397        }
398    }
399
400    if !query_params.is_empty() {
401        url.push('?');
402        url.push_str(&query_params.join("&"));
403    }
404
405    Ok(url)
406}
407
408/// Builds headers including authentication
409fn build_headers(
410    spec: &CachedSpec,
411    operation: &CachedCommand,
412    matches: &ArgMatches,
413    api_name: &str,
414    global_config: Option<&GlobalConfig>,
415) -> Result<HeaderMap, Error> {
416    let mut headers = HeaderMap::new();
417
418    // Add default headers
419    headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
420    headers.insert("Accept", HeaderValue::from_static("application/json"));
421
422    // Get to the deepest subcommand matches
423    let mut current_matches = matches;
424    while let Some((_name, sub_matches)) = current_matches.subcommand() {
425        current_matches = sub_matches;
426    }
427
428    // Add header parameters from matches
429    for param in &operation.parameters {
430        if param.location == "header" {
431            if let Some(value) = current_matches.get_one::<String>(&param.name) {
432                let header_name =
433                    HeaderName::from_str(&param.name).map_err(|e| Error::InvalidHeaderName {
434                        name: param.name.clone(),
435                        reason: e.to_string(),
436                    })?;
437                let header_value =
438                    HeaderValue::from_str(value).map_err(|e| Error::InvalidHeaderValue {
439                        name: param.name.clone(),
440                        reason: e.to_string(),
441                    })?;
442                headers.insert(header_name, header_value);
443            }
444        }
445    }
446
447    // Add authentication headers based on security requirements
448    for security_scheme_name in &operation.security_requirements {
449        if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
450            add_authentication_header(&mut headers, security_scheme, api_name, global_config)?;
451        }
452    }
453
454    // Add custom headers from --header/-H flags
455    // Use try_get_many to avoid panic when header arg doesn't exist
456    if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
457        for header_str in custom_headers {
458            let (name, value) = parse_custom_header(header_str)?;
459            let header_name =
460                HeaderName::from_str(&name).map_err(|e| Error::InvalidHeaderName {
461                    name: name.clone(),
462                    reason: e.to_string(),
463                })?;
464            let header_value =
465                HeaderValue::from_str(&value).map_err(|e| Error::InvalidHeaderValue {
466                    name: name.clone(),
467                    reason: e.to_string(),
468                })?;
469            headers.insert(header_name, header_value);
470        }
471    }
472
473    Ok(headers)
474}
475
476/// Validates that a header value doesn't contain control characters
477fn validate_header_value(name: &str, value: &str) -> Result<(), Error> {
478    if value.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
479        return Err(Error::InvalidHeaderValue {
480            name: name.to_string(),
481            reason: "Header value contains invalid control characters (newline, carriage return, or null)".to_string(),
482        });
483    }
484    Ok(())
485}
486
487/// Parses a custom header string in the format "Name: Value" or "Name:Value"
488fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
489    // Find the colon separator
490    let colon_pos = header_str
491        .find(':')
492        .ok_or_else(|| Error::InvalidHeaderFormat {
493            header: header_str.to_string(),
494        })?;
495
496    let name = header_str[..colon_pos].trim();
497    let value = header_str[colon_pos + 1..].trim();
498
499    if name.is_empty() {
500        return Err(Error::EmptyHeaderName);
501    }
502
503    // Support environment variable expansion in header values
504    let expanded_value = if value.starts_with("${") && value.ends_with('}') {
505        // Extract environment variable name
506        let var_name = &value[2..value.len() - 1];
507        std::env::var(var_name).unwrap_or_else(|_| value.to_string())
508    } else {
509        value.to_string()
510    };
511
512    // Validate the header value
513    validate_header_value(name, &expanded_value)?;
514
515    Ok((name.to_string(), expanded_value))
516}
517
518/// Adds an authentication header based on a security scheme
519#[allow(clippy::too_many_lines)]
520fn add_authentication_header(
521    headers: &mut HeaderMap,
522    security_scheme: &CachedSecurityScheme,
523    api_name: &str,
524    global_config: Option<&GlobalConfig>,
525) -> Result<(), Error> {
526    // Debug logging when RUST_LOG is set
527    if std::env::var("RUST_LOG").is_ok() {
528        eprintln!(
529            "[DEBUG] Adding authentication header for scheme: {} (type: {})",
530            security_scheme.name, security_scheme.scheme_type
531        );
532    }
533
534    // Priority 1: Check config-based secrets first
535    let secret_config = global_config
536        .and_then(|config| config.api_configs.get(api_name))
537        .and_then(|api_config| api_config.secrets.get(&security_scheme.name));
538
539    let (secret_value, env_var_name) = if let Some(config_secret) = secret_config {
540        // Use config-based secret
541        let secret_value = std::env::var(&config_secret.name).map_err(|_| Error::SecretNotSet {
542            scheme_name: security_scheme.name.clone(),
543            env_var: config_secret.name.clone(),
544        })?;
545        (secret_value, config_secret.name.clone())
546    } else if let Some(aperture_secret) = &security_scheme.aperture_secret {
547        // Priority 2: Fall back to x-aperture-secret extension
548        let secret_value =
549            std::env::var(&aperture_secret.name).map_err(|_| Error::SecretNotSet {
550                scheme_name: security_scheme.name.clone(),
551                env_var: aperture_secret.name.clone(),
552            })?;
553        (secret_value, aperture_secret.name.clone())
554    } else {
555        // No authentication configuration found - skip this scheme
556        return Ok(());
557    };
558
559    // Debug logging for resolved secret source
560    if std::env::var("RUST_LOG").is_ok() {
561        let source = if secret_config.is_some() {
562            "config"
563        } else {
564            "x-aperture-secret"
565        };
566        eprintln!(
567            "[DEBUG] Using secret from {source} for scheme '{}': env var '{env_var_name}'",
568            security_scheme.name
569        );
570    }
571
572    // Validate the secret doesn't contain control characters
573    validate_header_value("Authorization", &secret_value)?;
574
575    // Build the appropriate header based on scheme type
576    match security_scheme.scheme_type.as_str() {
577        "apiKey" => {
578            let (Some(location), Some(param_name)) =
579                (&security_scheme.location, &security_scheme.parameter_name)
580            else {
581                return Ok(());
582            };
583
584            if location == "header" {
585                let header_name =
586                    HeaderName::from_str(param_name).map_err(|e| Error::InvalidHeaderName {
587                        name: param_name.clone(),
588                        reason: e.to_string(),
589                    })?;
590                let header_value = HeaderValue::from_str(&secret_value).map_err(|e| {
591                    Error::InvalidHeaderValue {
592                        name: param_name.clone(),
593                        reason: e.to_string(),
594                    }
595                })?;
596                headers.insert(header_name, header_value);
597            }
598            // Note: query and cookie locations are handled differently in request building
599        }
600        "http" => {
601            if let Some(scheme_str) = &security_scheme.scheme {
602                let auth_scheme: AuthScheme = scheme_str.as_str().into();
603                let auth_value = match &auth_scheme {
604                    AuthScheme::Bearer => {
605                        format!("Bearer {secret_value}")
606                    }
607                    AuthScheme::Basic => {
608                        // Basic auth expects "username:password" format in the secret
609                        // The secret should contain the raw "username:password" string
610                        // We'll base64 encode it before adding to the header
611                        use base64::{engine::general_purpose, Engine as _};
612                        let encoded = general_purpose::STANDARD.encode(&secret_value);
613                        format!("Basic {encoded}")
614                    }
615                    AuthScheme::Token
616                    | AuthScheme::DSN
617                    | AuthScheme::ApiKey
618                    | AuthScheme::Custom(_) => {
619                        // Treat any other HTTP scheme as a bearer-like token
620                        // Format: "Authorization: <scheme> <token>"
621                        // This supports Token, ApiKey, DSN, and any custom schemes
622                        format!("{scheme_str} {secret_value}")
623                    }
624                };
625
626                let header_value =
627                    HeaderValue::from_str(&auth_value).map_err(|e| Error::InvalidHeaderValue {
628                        name: "Authorization".to_string(),
629                        reason: e.to_string(),
630                    })?;
631                headers.insert("Authorization", header_value);
632
633                // Debug logging
634                if std::env::var("RUST_LOG").is_ok() {
635                    match &auth_scheme {
636                        AuthScheme::Bearer => {
637                            eprintln!("[DEBUG] Added Bearer authentication header");
638                        }
639                        AuthScheme::Basic => {
640                            eprintln!("[DEBUG] Added Basic authentication header (base64 encoded)");
641                        }
642                        _ => {
643                            eprintln!(
644                                "[DEBUG] Added custom HTTP auth header with scheme: {scheme_str}"
645                            );
646                        }
647                    }
648                }
649            }
650        }
651        _ => {
652            return Err(Error::UnsupportedSecurityScheme {
653                scheme_type: security_scheme.scheme_type.clone(),
654            });
655        }
656    }
657
658    Ok(())
659}
660
661/// Prints the response text in the specified format
662fn print_formatted_response(
663    response_text: &str,
664    output_format: &OutputFormat,
665    jq_filter: Option<&str>,
666    capture_output: bool,
667) -> Result<Option<String>, Error> {
668    // Apply JQ filter if provided
669    let processed_text = if let Some(filter) = jq_filter {
670        apply_jq_filter(response_text, filter)?
671    } else {
672        response_text.to_string()
673    };
674
675    match output_format {
676        OutputFormat::Json => {
677            // Try to pretty-print JSON (default behavior)
678            let output = serde_json::from_str::<Value>(&processed_text)
679                .ok()
680                .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
681                .unwrap_or_else(|| processed_text.clone());
682
683            if capture_output {
684                return Ok(Some(output));
685            }
686            println!("{output}");
687        }
688        OutputFormat::Yaml => {
689            // Convert JSON to YAML
690            let output = serde_json::from_str::<Value>(&processed_text)
691                .ok()
692                .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
693                .unwrap_or_else(|| processed_text.clone());
694
695            if capture_output {
696                return Ok(Some(output));
697            }
698            println!("{output}");
699        }
700        OutputFormat::Table => {
701            // Convert JSON to table format
702            if let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) {
703                let table_output = print_as_table(&json_value, capture_output)?;
704                if capture_output {
705                    return Ok(table_output);
706                }
707            } else {
708                // If not JSON, output as-is
709                if capture_output {
710                    return Ok(Some(processed_text));
711                }
712                println!("{processed_text}");
713            }
714        }
715    }
716
717    Ok(None)
718}
719
720// Define table structures at module level to avoid clippy::items_after_statements
721#[derive(tabled::Tabled)]
722struct TableRow {
723    #[tabled(rename = "Key")]
724    key: String,
725    #[tabled(rename = "Value")]
726    value: String,
727}
728
729#[derive(tabled::Tabled)]
730struct KeyValue {
731    #[tabled(rename = "Key")]
732    key: String,
733    #[tabled(rename = "Value")]
734    value: String,
735}
736
737/// Prints JSON data as a formatted table
738#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
739fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
740    use std::collections::BTreeMap;
741    use tabled::Table;
742
743    match json_value {
744        Value::Array(items) => {
745            if items.is_empty() {
746                if capture_output {
747                    return Ok(Some("(empty array)".to_string()));
748                }
749                println!("(empty array)");
750                return Ok(None);
751            }
752
753            // Check if array is too large
754            if items.len() > MAX_TABLE_ROWS {
755                let msg1 = format!(
756                    "Array too large: {} items (max {} for table display)",
757                    items.len(),
758                    MAX_TABLE_ROWS
759                );
760                let msg2 = "Use --format json or --jq to process the full data";
761
762                if capture_output {
763                    return Ok(Some(format!("{msg1}\n{msg2}")));
764                }
765                println!("{msg1}");
766                println!("{msg2}");
767                return Ok(None);
768            }
769
770            // Try to create a table from array of objects
771            if let Some(Value::Object(_)) = items.first() {
772                // Create table for array of objects
773                let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
774
775                for item in items {
776                    if let Value::Object(obj) = item {
777                        let mut row = BTreeMap::new();
778                        for (key, value) in obj {
779                            row.insert(key.clone(), format_value_for_table(value));
780                        }
781                        table_data.push(row);
782                    }
783                }
784
785                if !table_data.is_empty() {
786                    // For now, use a simple key-value representation
787                    // In the future, we could implement a more sophisticated table structure
788                    let mut rows = Vec::new();
789                    for (i, row) in table_data.iter().enumerate() {
790                        if i > 0 {
791                            rows.push(TableRow {
792                                key: "---".to_string(),
793                                value: "---".to_string(),
794                            });
795                        }
796                        for (key, value) in row {
797                            rows.push(TableRow {
798                                key: key.clone(),
799                                value: value.clone(),
800                            });
801                        }
802                    }
803
804                    let table = Table::new(&rows);
805                    if capture_output {
806                        return Ok(Some(table.to_string()));
807                    }
808                    println!("{table}");
809                    return Ok(None);
810                }
811            }
812
813            // Fallback: print array as numbered list
814            if capture_output {
815                let mut output = String::new();
816                for (i, item) in items.iter().enumerate() {
817                    use std::fmt::Write;
818                    writeln!(&mut output, "{}: {}", i, format_value_for_table(item)).unwrap();
819                }
820                return Ok(Some(output.trim_end().to_string()));
821            }
822            for (i, item) in items.iter().enumerate() {
823                println!("{}: {}", i, format_value_for_table(item));
824            }
825        }
826        Value::Object(obj) => {
827            // Check if object has too many fields
828            if obj.len() > MAX_TABLE_ROWS {
829                let msg1 = format!(
830                    "Object too large: {} fields (max {} for table display)",
831                    obj.len(),
832                    MAX_TABLE_ROWS
833                );
834                let msg2 = "Use --format json or --jq to process the full data";
835
836                if capture_output {
837                    return Ok(Some(format!("{msg1}\n{msg2}")));
838                }
839                println!("{msg1}");
840                println!("{msg2}");
841                return Ok(None);
842            }
843
844            // Create a simple key-value table for objects
845            let rows: Vec<KeyValue> = obj
846                .iter()
847                .map(|(key, value)| KeyValue {
848                    key: key.clone(),
849                    value: format_value_for_table(value),
850                })
851                .collect();
852
853            let table = Table::new(&rows);
854            if capture_output {
855                return Ok(Some(table.to_string()));
856            }
857            println!("{table}");
858        }
859        _ => {
860            // For primitive values, just print them
861            let formatted = format_value_for_table(json_value);
862            if capture_output {
863                return Ok(Some(formatted));
864            }
865            println!("{formatted}");
866        }
867    }
868
869    Ok(None)
870}
871
872/// Formats a JSON value for display in a table cell
873fn format_value_for_table(value: &Value) -> String {
874    match value {
875        Value::Null => "null".to_string(),
876        Value::Bool(b) => b.to_string(),
877        Value::Number(n) => n.to_string(),
878        Value::String(s) => s.clone(),
879        Value::Array(arr) => {
880            if arr.len() <= 3 {
881                format!(
882                    "[{}]",
883                    arr.iter()
884                        .map(format_value_for_table)
885                        .collect::<Vec<_>>()
886                        .join(", ")
887                )
888            } else {
889                format!("[{} items]", arr.len())
890            }
891        }
892        Value::Object(obj) => {
893            if obj.len() <= 2 {
894                format!(
895                    "{{{}}}",
896                    obj.iter()
897                        .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
898                        .collect::<Vec<_>>()
899                        .join(", ")
900                )
901            } else {
902                format!("{{object with {} fields}}", obj.len())
903            }
904        }
905    }
906}
907
908/// Applies a JQ filter to the response text
909///
910/// # Errors
911///
912/// Returns an error if:
913/// - The response text is not valid JSON
914/// - The JQ filter expression is invalid
915/// - The filter execution fails
916pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
917    // Parse the response as JSON
918    let json_value: Value =
919        serde_json::from_str(response_text).map_err(|e| Error::JqFilterError {
920            reason: format!("Response is not valid JSON: {e}"),
921        })?;
922
923    #[cfg(feature = "jq")]
924    {
925        // Use jaq (pure Rust implementation) when available
926        use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
927        use jaq_parse::parse;
928        use jaq_std::std;
929
930        // Parse the filter expression
931        let (expr, errs) = parse(filter, jaq_parse::main());
932        if !errs.is_empty() {
933            return Err(Error::JqFilterError {
934                reason: format!("Parse error in jq expression: {}", errs[0]),
935            });
936        }
937
938        // Create parsing context and compile the filter
939        let mut ctx = ParseCtx::new(Vec::new());
940        ctx.insert_defs(std());
941        let filter = ctx.compile(expr.unwrap());
942
943        // Convert serde_json::Value to jaq Val
944        let jaq_value = serde_json_to_jaq_val(&json_value);
945
946        // Execute the filter
947        let inputs = RcIter::new(core::iter::empty());
948        let ctx = Ctx::new([], &inputs);
949        let results: Result<Vec<Val>, _> = filter.run((ctx, jaq_value.into())).collect();
950
951        match results {
952            Ok(vals) => {
953                if vals.is_empty() {
954                    Ok("null".to_string())
955                } else if vals.len() == 1 {
956                    // Single result - convert back to JSON
957                    let json_val = jaq_val_to_serde_json(&vals[0]);
958                    serde_json::to_string_pretty(&json_val).map_err(|e| Error::JqFilterError {
959                        reason: format!("Failed to serialize result: {e}"),
960                    })
961                } else {
962                    // Multiple results - return as JSON array
963                    let json_vals: Vec<Value> = vals.iter().map(jaq_val_to_serde_json).collect();
964                    let array = Value::Array(json_vals);
965                    serde_json::to_string_pretty(&array).map_err(|e| Error::JqFilterError {
966                        reason: format!("Failed to serialize results: {e}"),
967                    })
968                }
969            }
970            Err(e) => Err(Error::JqFilterError {
971                reason: format!("Filter execution error: {e}"),
972            }),
973        }
974    }
975
976    #[cfg(not(feature = "jq"))]
977    {
978        // Basic JQ-like functionality without full jq library
979        apply_basic_jq_filter(&json_value, filter)
980    }
981}
982
983#[cfg(not(feature = "jq"))]
984/// Basic JQ-like functionality for common cases
985fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
986    // Check if the filter uses advanced features
987    let uses_advanced_features = filter.contains('[')
988        || filter.contains(']')
989        || filter.contains('|')
990        || filter.contains('(')
991        || filter.contains(')')
992        || filter.contains("select")
993        || filter.contains("map")
994        || filter.contains("length");
995
996    if uses_advanced_features {
997        eprintln!("Warning: Advanced JQ features require building with --features jq");
998        eprintln!("         Currently only basic field access is supported (e.g., '.field', '.nested.field')");
999        eprintln!("         To enable full JQ support: cargo install aperture-cli --features jq");
1000    }
1001
1002    let result = match filter {
1003        "." => json_value.clone(),
1004        ".[]" => {
1005            // Handle array iteration
1006            match json_value {
1007                Value::Array(arr) => {
1008                    // Return array elements as a JSON array
1009                    Value::Array(arr.clone())
1010                }
1011                Value::Object(obj) => {
1012                    // Return object values as an array
1013                    Value::Array(obj.values().cloned().collect())
1014                }
1015                _ => Value::Null,
1016            }
1017        }
1018        ".length" => {
1019            // Handle length operation
1020            match json_value {
1021                Value::Array(arr) => Value::Number(arr.len().into()),
1022                Value::Object(obj) => Value::Number(obj.len().into()),
1023                Value::String(s) => Value::Number(s.len().into()),
1024                _ => Value::Null,
1025            }
1026        }
1027        filter if filter.starts_with(".[].") => {
1028            // Handle array map like .[].name
1029            let field_path = &filter[4..]; // Remove ".[].""
1030            match json_value {
1031                Value::Array(arr) => {
1032                    let mapped: Vec<Value> = arr
1033                        .iter()
1034                        .map(|item| get_nested_field(item, field_path))
1035                        .collect();
1036                    Value::Array(mapped)
1037                }
1038                _ => Value::Null,
1039            }
1040        }
1041        filter if filter.starts_with('.') => {
1042            // Handle simple field access like .name, .metadata.role
1043            let field_path = &filter[1..]; // Remove the leading dot
1044            get_nested_field(json_value, field_path)
1045        }
1046        _ => {
1047            return Err(Error::JqFilterError {
1048                reason: format!("Unsupported JQ filter: '{filter}'. Only basic field access like '.name' or '.metadata.role' is supported without the full jq library."),
1049            });
1050        }
1051    };
1052
1053    serde_json::to_string_pretty(&result).map_err(|e| Error::JqFilterError {
1054        reason: format!("Failed to serialize filtered result: {e}"),
1055    })
1056}
1057
1058#[cfg(not(feature = "jq"))]
1059/// Get a nested field from JSON using dot notation
1060fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
1061    let parts: Vec<&str> = field_path.split('.').collect();
1062    let mut current = json_value;
1063
1064    for part in parts {
1065        if part.is_empty() {
1066            continue;
1067        }
1068
1069        // Handle array index notation like [0]
1070        if part.starts_with('[') && part.ends_with(']') {
1071            let index_str = &part[1..part.len() - 1];
1072            if let Ok(index) = index_str.parse::<usize>() {
1073                match current {
1074                    Value::Array(arr) => {
1075                        if let Some(item) = arr.get(index) {
1076                            current = item;
1077                        } else {
1078                            return Value::Null;
1079                        }
1080                    }
1081                    _ => return Value::Null,
1082                }
1083            } else {
1084                return Value::Null;
1085            }
1086            continue;
1087        }
1088
1089        match current {
1090            Value::Object(obj) => {
1091                if let Some(field) = obj.get(part) {
1092                    current = field;
1093                } else {
1094                    return Value::Null;
1095                }
1096            }
1097            Value::Array(arr) => {
1098                // Handle numeric string as array index
1099                if let Ok(index) = part.parse::<usize>() {
1100                    if let Some(item) = arr.get(index) {
1101                        current = item;
1102                    } else {
1103                        return Value::Null;
1104                    }
1105                } else {
1106                    return Value::Null;
1107                }
1108            }
1109            _ => return Value::Null,
1110        }
1111    }
1112
1113    current.clone()
1114}
1115
1116#[cfg(feature = "jq")]
1117/// Convert serde_json::Value to jaq Val
1118fn serde_json_to_jaq_val(value: &Value) -> jaq_interpret::Val {
1119    use jaq_interpret::Val;
1120    use std::rc::Rc;
1121
1122    match value {
1123        Value::Null => Val::Null,
1124        Value::Bool(b) => Val::Bool(*b),
1125        Value::Number(n) => {
1126            if let Some(i) = n.as_i64() {
1127                // Convert i64 to isize safely
1128                if let Ok(isize_val) = isize::try_from(i) {
1129                    Val::Int(isize_val)
1130                } else {
1131                    // Fallback to float for large numbers
1132                    Val::Float(i as f64)
1133                }
1134            } else if let Some(f) = n.as_f64() {
1135                Val::Float(f)
1136            } else {
1137                Val::Null
1138            }
1139        }
1140        Value::String(s) => Val::Str(s.clone().into()),
1141        Value::Array(arr) => {
1142            let jaq_arr: Vec<Val> = arr.iter().map(serde_json_to_jaq_val).collect();
1143            Val::Arr(Rc::new(jaq_arr))
1144        }
1145        Value::Object(obj) => {
1146            let mut jaq_obj = indexmap::IndexMap::with_hasher(ahash::RandomState::new());
1147            for (k, v) in obj {
1148                jaq_obj.insert(Rc::new(k.clone()), serde_json_to_jaq_val(v));
1149            }
1150            Val::Obj(Rc::new(jaq_obj))
1151        }
1152    }
1153}
1154
1155#[cfg(feature = "jq")]
1156/// Convert jaq Val to serde_json::Value
1157fn jaq_val_to_serde_json(val: &jaq_interpret::Val) -> Value {
1158    use jaq_interpret::Val;
1159
1160    match val {
1161        Val::Null => Value::Null,
1162        Val::Bool(b) => Value::Bool(*b),
1163        Val::Int(i) => {
1164            // Convert isize to i64
1165            Value::Number((*i as i64).into())
1166        }
1167        Val::Float(f) => {
1168            if let Some(num) = serde_json::Number::from_f64(*f) {
1169                Value::Number(num)
1170            } else {
1171                Value::Null
1172            }
1173        }
1174        Val::Str(s) => Value::String(s.to_string()),
1175        Val::Arr(arr) => {
1176            let json_arr: Vec<Value> = arr.iter().map(jaq_val_to_serde_json).collect();
1177            Value::Array(json_arr)
1178        }
1179        Val::Obj(obj) => {
1180            let mut json_obj = serde_json::Map::new();
1181            for (k, v) in obj.iter() {
1182                json_obj.insert(k.to_string(), jaq_val_to_serde_json(v));
1183            }
1184            Value::Object(json_obj)
1185        }
1186        _ => Value::Null, // Handle any other Val variants as null
1187    }
1188}