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