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