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