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