1use crate::cache::models::{CachedCommand, CachedParameter, 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::resilience::{
8 calculate_retry_delay_with_header, is_retryable_status, parse_retry_after_value,
9};
10use crate::response_cache::{
11 CacheConfig, CacheKey, CachedRequestInfo, CachedResponse, ResponseCache,
12};
13use crate::utils::to_kebab_case;
14use base64::{engine::general_purpose, Engine as _};
15use clap::ArgMatches;
16use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
17use reqwest::Method;
18use serde_json::Value;
19use sha2::{Digest, Sha256};
20use std::collections::{BTreeMap, HashMap};
21use std::fmt::Write;
22use std::str::FromStr;
23use tabled::Table;
24use tokio::time::sleep;
25
26#[cfg(feature = "jq")]
27use jaq_core::{Ctx, RcIter};
28#[cfg(feature = "jq")]
29use jaq_json::Val;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum AuthScheme {
34 Bearer,
35 Basic,
36 Token,
37 DSN,
38 ApiKey,
39 Custom(String),
40}
41
42impl From<&str> for AuthScheme {
43 fn from(s: &str) -> Self {
44 match s.to_lowercase().as_str() {
45 constants::AUTH_SCHEME_BEARER => Self::Bearer,
46 constants::AUTH_SCHEME_BASIC => Self::Basic,
47 "token" => Self::Token,
48 "dsn" => Self::DSN,
49 constants::AUTH_SCHEME_APIKEY => Self::ApiKey,
50 _ => Self::Custom(s.to_string()),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct RetryContext {
58 pub max_attempts: u32,
60 pub initial_delay_ms: u64,
62 pub max_delay_ms: u64,
64 pub force_retry: bool,
66 pub method: Option<String>,
68 pub has_idempotency_key: bool,
70}
71
72impl Default for RetryContext {
73 fn default() -> Self {
74 Self {
75 max_attempts: 0, initial_delay_ms: 500,
77 max_delay_ms: 30_000,
78 force_retry: false,
79 method: None,
80 has_idempotency_key: false,
81 }
82 }
83}
84
85impl RetryContext {
86 #[must_use]
88 pub const fn is_enabled(&self) -> bool {
89 self.max_attempts > 0
90 }
91
92 #[must_use]
94 pub fn is_safe_to_retry(&self) -> bool {
95 if self.force_retry || self.has_idempotency_key {
96 return true;
97 }
98
99 self.method.as_ref().is_some_and(|m| {
101 matches!(
102 m.to_uppercase().as_str(),
103 "GET" | "HEAD" | "PUT" | "OPTIONS" | "TRACE"
104 )
105 })
106 }
107}
108
109const MAX_TABLE_ROWS: usize = 1000;
111
112fn extract_server_var_args(matches: &ArgMatches) -> Vec<String> {
116 matches
117 .try_get_many::<String>("server-var")
118 .ok()
119 .flatten()
120 .map(|values| values.cloned().collect())
121 .unwrap_or_default()
122}
123
124fn build_http_client() -> Result<reqwest::Client, Error> {
126 reqwest::Client::builder()
127 .timeout(std::time::Duration::from_secs(30))
128 .build()
129 .map_err(|e| {
130 Error::request_failed(
131 reqwest::StatusCode::INTERNAL_SERVER_ERROR,
132 format!("Failed to create HTTP client: {e}"),
133 )
134 })
135}
136
137fn extract_request_body(
139 operation: &CachedCommand,
140 matches: &ArgMatches,
141) -> Result<Option<String>, Error> {
142 if operation.request_body.is_none() {
143 return Ok(None);
144 }
145
146 let mut current_matches = matches;
148 while let Some((_name, sub_matches)) = current_matches.subcommand() {
149 current_matches = sub_matches;
150 }
151
152 if let Some(body_value) = current_matches.get_one::<String>("body") {
153 let _json_body: Value = serde_json::from_str(body_value)
155 .map_err(|e| Error::invalid_json_body(e.to_string()))?;
156 Ok(Some(body_value.clone()))
157 } else {
158 Ok(None)
159 }
160}
161
162fn handle_dry_run(
164 dry_run: bool,
165 method: &reqwest::Method,
166 url: &str,
167 headers: &reqwest::header::HeaderMap,
168 body: Option<&str>,
169 operation: &CachedCommand,
170 capture_output: bool,
171) -> Result<Option<String>, Error> {
172 if !dry_run {
173 return Ok(None);
174 }
175
176 let headers_map: HashMap<String, String> = headers
177 .iter()
178 .map(|(k, v)| {
179 let value = if is_sensitive_header(k.as_str()) {
180 "<REDACTED>".to_string()
181 } else {
182 v.to_str().unwrap_or("<binary>").to_string()
183 };
184 (k.as_str().to_string(), value)
185 })
186 .collect();
187
188 let dry_run_info = serde_json::json!({
189 "dry_run": true,
190 "method": method.to_string(),
191 "url": url,
192 "headers": headers_map,
193 "body": body,
194 "operation_id": operation.operation_id
195 });
196
197 let output = serde_json::to_string_pretty(&dry_run_info).map_err(|e| {
198 Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
199 })?;
200
201 if capture_output {
202 Ok(Some(output))
203 } else {
204 println!("{output}");
206 Ok(None)
207 }
208}
209
210async fn send_request(
212 request: reqwest::RequestBuilder,
213) -> Result<(reqwest::StatusCode, HashMap<String, String>, String), Error> {
214 let response = request
215 .send()
216 .await
217 .map_err(|e| Error::network_request_failed(e.to_string()))?;
218
219 let status = response.status();
220 let response_headers: HashMap<String, String> = response
221 .headers()
222 .iter()
223 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
224 .collect();
225
226 let response_text = response
227 .text()
228 .await
229 .map_err(|e| Error::response_read_error(e.to_string()))?;
230
231 Ok((status, response_headers, response_text))
232}
233
234#[allow(clippy::too_many_arguments)]
236#[allow(clippy::too_many_lines)]
237async fn send_request_with_retry(
238 client: &reqwest::Client,
239 method: Method,
240 url: &str,
241 headers: HeaderMap,
242 body: Option<String>,
243 retry_context: Option<&RetryContext>,
244 operation: &CachedCommand,
245) -> Result<(reqwest::StatusCode, HashMap<String, String>, String), Error> {
246 use crate::resilience::RetryConfig;
247
248 let Some(ctx) = retry_context else {
250 let request = build_request(client, method, url, headers, body);
251 return send_request(request).await;
252 };
253
254 if !ctx.is_enabled() {
255 let request = build_request(client, method, url, headers, body);
256 return send_request(request).await;
257 }
258
259 if !ctx.is_safe_to_retry() {
261 eprintln!(
263 "Warning: Retries disabled for {} {} - method is not idempotent and no --idempotency-key provided",
264 method,
265 operation.operation_id
266 );
267 eprintln!(
269 " Use --force-retry to enable retries anyway, or provide --idempotency-key"
270 );
271 let request = build_request(client, method.clone(), url, headers, body);
272 return send_request(request).await;
273 }
274
275 let retry_config = RetryConfig {
277 max_attempts: ctx.max_attempts as usize,
278 initial_delay_ms: ctx.initial_delay_ms,
279 max_delay_ms: ctx.max_delay_ms,
280 backoff_multiplier: 2.0,
281 jitter: true,
282 };
283
284 let max_attempts = ctx.max_attempts;
285 let mut attempt: u32 = 0;
286 let mut last_error: Option<Error> = None;
287 let mut last_status: Option<reqwest::StatusCode> = None;
288 let mut last_response_headers: Option<HashMap<String, String>> = None;
289 let mut last_response_text: Option<String> = None;
290
291 while attempt < max_attempts {
292 attempt += 1;
293
294 let request = build_request(client, method.clone(), url, headers.clone(), body.clone());
295 let result = send_request(request).await;
296
297 match result {
298 Ok((status, response_headers, response_text)) => {
299 if status.is_success() {
301 return Ok((status, response_headers, response_text));
302 }
303
304 if !is_retryable_status(status.as_u16()) {
306 return Ok((status, response_headers, response_text));
307 }
308
309 let retry_after = response_headers
311 .get("retry-after")
312 .and_then(|v| parse_retry_after_value(v));
313
314 let delay = calculate_retry_delay_with_header(
316 &retry_config,
317 (attempt - 1) as usize, retry_after,
319 );
320
321 if attempt < max_attempts {
323 eprintln!(
325 "Retry {}/{}: {} {} returned {} - retrying in {}ms",
326 attempt,
327 max_attempts,
328 method,
329 operation.operation_id,
330 status.as_u16(),
331 delay.as_millis()
332 );
333 sleep(delay).await;
334 }
335
336 last_status = Some(status);
338 last_response_headers = Some(response_headers);
339 last_response_text = Some(response_text);
340 }
341 Err(e) => {
342 let should_retry = matches!(&e, Error::Network(_));
344
345 if !should_retry {
346 return Err(e);
347 }
348
349 let delay =
351 calculate_retry_delay_with_header(&retry_config, (attempt - 1) as usize, None);
352
353 if attempt < max_attempts {
354 eprintln!(
356 "Retry {}/{}: {} {} failed - retrying in {}ms: {}",
357 attempt,
358 max_attempts,
359 method,
360 operation.operation_id,
361 delay.as_millis(),
362 e
363 );
364 sleep(delay).await;
365 }
366
367 last_error = Some(e);
368 }
369 }
370 }
371
372 if let (Some(status), Some(headers), Some(text)) =
374 (last_status, last_response_headers, last_response_text)
375 {
376 eprintln!(
378 "Retry exhausted: {} {} failed after {} attempts",
379 method, operation.operation_id, max_attempts
380 );
381 return Ok((status, headers, text));
382 }
383
384 if let Some(e) = last_error {
386 eprintln!(
388 "Retry exhausted: {} {} failed after {} attempts",
389 method, operation.operation_id, max_attempts
390 );
391 return Err(Error::retry_limit_exceeded_detailed(
393 max_attempts,
394 attempt,
395 e.to_string(),
396 ctx.initial_delay_ms,
397 ctx.max_delay_ms,
398 None,
399 &operation.operation_id,
400 ));
401 }
402
403 Err(Error::retry_limit_exceeded_detailed(
405 max_attempts,
406 attempt,
407 "Request failed with no response",
408 ctx.initial_delay_ms,
409 ctx.max_delay_ms,
410 None,
411 &operation.operation_id,
412 ))
413}
414
415fn build_request(
417 client: &reqwest::Client,
418 method: Method,
419 url: &str,
420 headers: HeaderMap,
421 body: Option<String>,
422) -> reqwest::RequestBuilder {
423 let mut request = client.request(method, url).headers(headers);
424 if let Some(json_body) = body.and_then(|s| serde_json::from_str::<Value>(&s).ok()) {
425 request = request.json(&json_body);
426 }
427 request
428}
429
430fn handle_http_error(
432 status: reqwest::StatusCode,
433 response_text: String,
434 spec: &CachedSpec,
435 operation: &CachedCommand,
436) -> Error {
437 let api_name = spec.name.clone();
438 let operation_id = Some(operation.operation_id.clone());
439
440 let security_schemes: Vec<String> = operation
441 .security_requirements
442 .iter()
443 .filter_map(|scheme_name| {
444 spec.security_schemes
445 .get(scheme_name)
446 .and_then(|scheme| scheme.aperture_secret.as_ref())
447 .map(|aperture_secret| aperture_secret.name.clone())
448 })
449 .collect();
450
451 Error::http_error_with_context(
452 status.as_u16(),
453 if response_text.is_empty() {
454 constants::EMPTY_RESPONSE.to_string()
455 } else {
456 response_text
457 },
458 api_name,
459 operation_id,
460 &security_schemes,
461 )
462}
463
464fn prepare_cache_context(
466 cache_config: Option<&CacheConfig>,
467 spec_name: &str,
468 operation_id: &str,
469 method: &reqwest::Method,
470 url: &str,
471 headers: &reqwest::header::HeaderMap,
472 body: Option<&str>,
473) -> Result<Option<(CacheKey, ResponseCache)>, Error> {
474 let Some(cache_cfg) = cache_config else {
475 return Ok(None);
476 };
477
478 if !cache_cfg.enabled {
479 return Ok(None);
480 }
481
482 let header_map: HashMap<String, String> = headers
483 .iter()
484 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
485 .collect();
486
487 let cache_key = CacheKey::from_request(
488 spec_name,
489 operation_id,
490 method.as_ref(),
491 url,
492 &header_map,
493 body,
494 )?;
495
496 let response_cache = ResponseCache::new(cache_cfg.clone())?;
497 Ok(Some((cache_key, response_cache)))
498}
499
500async fn check_cache(
502 cache_context: Option<&(CacheKey, ResponseCache)>,
503) -> Result<Option<CachedResponse>, Error> {
504 if let Some((cache_key, response_cache)) = cache_context {
505 response_cache.get(cache_key).await
506 } else {
507 Ok(None)
508 }
509}
510
511#[allow(clippy::too_many_arguments)]
513async fn store_in_cache(
514 cache_context: Option<(CacheKey, ResponseCache)>,
515 response_text: &str,
516 status: reqwest::StatusCode,
517 response_headers: &HashMap<String, String>,
518 method: reqwest::Method,
519 url: String,
520 headers: &reqwest::header::HeaderMap,
521 body: Option<&str>,
522 cache_config: Option<&CacheConfig>,
523) -> Result<(), Error> {
524 let Some((cache_key, response_cache)) = cache_context else {
525 return Ok(());
526 };
527
528 let cached_request_info = CachedRequestInfo {
529 method: method.to_string(),
530 url,
531 headers: headers
532 .iter()
533 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
534 .collect(),
535 body_hash: body.map(|b| {
536 let mut hasher = Sha256::new();
537 hasher.update(b.as_bytes());
538 format!("{:x}", hasher.finalize())
539 }),
540 };
541
542 let cache_ttl = cache_config.and_then(|cfg| {
543 if cfg.default_ttl.as_secs() > 0 {
544 Some(cfg.default_ttl)
545 } else {
546 None
547 }
548 });
549
550 response_cache
551 .store(
552 &cache_key,
553 response_text,
554 status.as_u16(),
555 response_headers,
556 cached_request_info,
557 cache_ttl,
558 )
559 .await?;
560
561 Ok(())
562}
563
564#[allow(clippy::too_many_lines)]
592#[allow(clippy::too_many_arguments)]
593#[allow(clippy::missing_panics_doc)]
594#[allow(clippy::missing_errors_doc)]
595pub async fn execute_request(
596 spec: &CachedSpec,
597 matches: &ArgMatches,
598 base_url: Option<&str>,
599 dry_run: bool,
600 idempotency_key: Option<&str>,
601 global_config: Option<&GlobalConfig>,
602 output_format: &OutputFormat,
603 jq_filter: Option<&str>,
604 cache_config: Option<&CacheConfig>,
605 capture_output: bool,
606 retry_context: Option<&RetryContext>,
607) -> Result<Option<String>, Error> {
608 let (operation, operation_matches) = find_operation_with_matches(spec, matches)?;
610
611 if operation_matches
614 .try_contains_id("show-examples")
615 .unwrap_or(false)
616 && operation_matches.get_flag("show-examples")
617 {
618 print_extended_examples(operation);
619 return Ok(None);
620 }
621
622 let server_var_args = extract_server_var_args(matches);
624
625 let resolver = BaseUrlResolver::new(spec);
627 let resolver = if let Some(config) = global_config {
628 resolver.with_global_config(config)
629 } else {
630 resolver
631 };
632 let base_url = resolver.resolve_with_variables(base_url, &server_var_args)?;
633
634 let url = build_url(&base_url, &operation.path, operation, operation_matches)?;
636
637 let client = build_http_client()?;
639
640 let mut headers = build_headers(
642 spec,
643 operation,
644 operation_matches,
645 &spec.name,
646 global_config,
647 )?;
648
649 if let Some(key) = idempotency_key {
651 headers.insert(
652 HeaderName::from_static("idempotency-key"),
653 HeaderValue::from_str(key).map_err(|_| Error::invalid_idempotency_key())?,
654 );
655 }
656
657 let method = Method::from_str(&operation.method)
659 .map_err(|_| Error::invalid_http_method(&operation.method))?;
660
661 let headers_clone = headers.clone(); let request_body = extract_request_body(operation, operation_matches)?;
665
666 let cache_context = prepare_cache_context(
668 cache_config,
669 &spec.name,
670 &operation.operation_id,
671 &method,
672 &url,
673 &headers_clone,
674 request_body.as_deref(),
675 )?;
676
677 if let Some(cached_response) = check_cache(cache_context.as_ref()).await? {
679 let output = print_formatted_response(
680 &cached_response.body,
681 output_format,
682 jq_filter,
683 capture_output,
684 )?;
685 return Ok(output);
686 }
687
688 if let Some(output) = handle_dry_run(
690 dry_run,
691 &method,
692 &url,
693 &headers_clone,
694 request_body.as_deref(),
695 operation,
696 capture_output,
697 )? {
698 return Ok(Some(output));
699 }
700 if dry_run {
701 return Ok(None);
702 }
703
704 let retry_ctx = retry_context.map(|ctx| {
706 let mut ctx = ctx.clone();
707 ctx.method = Some(method.to_string());
708 ctx
709 });
710
711 let (status, response_headers, response_text) = send_request_with_retry(
713 &client,
714 method.clone(),
715 &url,
716 headers,
717 request_body.clone(),
718 retry_ctx.as_ref(),
719 operation,
720 )
721 .await?;
722
723 if !status.is_success() {
725 return Err(handle_http_error(status, response_text, spec, operation));
726 }
727
728 store_in_cache(
730 cache_context,
731 &response_text,
732 status,
733 &response_headers,
734 method,
735 url,
736 &headers_clone,
737 request_body.as_deref(),
738 cache_config,
739 )
740 .await?;
741
742 if response_text.is_empty() {
744 Ok(None)
745 } else {
746 print_formatted_response(&response_text, output_format, jq_filter, capture_output)
747 }
748}
749
750fn print_extended_examples(operation: &CachedCommand) {
753 println!("Command: {}\n", to_kebab_case(&operation.operation_id));
755
756 if let Some(ref summary) = operation.summary {
757 println!("Description: {summary}\n");
759 }
760
761 println!("Method: {} {}\n", operation.method, operation.path);
763
764 if operation.examples.is_empty() {
765 println!("No examples available for this command.");
767 return;
768 }
769
770 println!("Examples:\n");
772 for (i, example) in operation.examples.iter().enumerate() {
773 println!("{}. {}", i + 1, example.description);
775 println!(" {}", example.command_line);
777 if let Some(ref explanation) = example.explanation {
778 println!(" {explanation}");
780 }
781 println!();
783 }
784
785 if operation.parameters.is_empty() {
787 return;
788 }
789
790 println!("Parameters:");
792 for param in &operation.parameters {
793 let required = if param.required { " (required)" } else { "" };
794 let param_type = param.schema_type.as_deref().unwrap_or("string");
795 println!(" --{}{} [{}]", param.name, required, param_type);
797
798 let Some(ref desc) = param.description else {
799 continue;
800 };
801 println!(" {desc}");
803 }
804 println!();
806
807 if operation.request_body.is_some() {
808 println!("Request Body:");
810 println!(" --body JSON (required)");
812 println!(" JSON data to send in the request body");
814 }
815}
816
817#[allow(dead_code)]
818fn find_operation<'a>(
819 spec: &'a CachedSpec,
820 matches: &ArgMatches,
821) -> Result<&'a CachedCommand, Error> {
822 let mut current_matches = matches;
824 let mut subcommand_path = Vec::new();
825
826 while let Some((name, sub_matches)) = current_matches.subcommand() {
827 subcommand_path.push(name);
828 current_matches = sub_matches;
829 }
830
831 let Some(operation_name) = subcommand_path.last() else {
834 let operation_name = "unknown".to_string();
835 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
836 return Err(Error::operation_not_found_with_suggestions(
837 operation_name,
838 &suggestions,
839 ));
840 };
841
842 for command in &spec.commands {
843 let kebab_id = to_kebab_case(&command.operation_id);
845 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
846 return Ok(command);
847 }
848 }
849
850 let operation_name = subcommand_path
851 .last()
852 .map_or_else(|| "unknown".to_string(), ToString::to_string);
853
854 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
856
857 Err(Error::operation_not_found_with_suggestions(
858 operation_name,
859 &suggestions,
860 ))
861}
862
863fn find_operation_with_matches<'a>(
864 spec: &'a CachedSpec,
865 matches: &'a ArgMatches,
866) -> Result<(&'a CachedCommand, &'a ArgMatches), Error> {
867 let mut current_matches = matches;
869 let mut subcommand_path = Vec::new();
870
871 while let Some((name, sub_matches)) = current_matches.subcommand() {
872 subcommand_path.push(name);
873 current_matches = sub_matches;
874 }
875
876 let Some(operation_name) = subcommand_path.last() else {
879 let operation_name = "unknown".to_string();
880 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
881 return Err(Error::operation_not_found_with_suggestions(
882 operation_name,
883 &suggestions,
884 ));
885 };
886
887 for command in &spec.commands {
888 let kebab_id = to_kebab_case(&command.operation_id);
890 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
891 return Ok((command, current_matches));
893 }
894 }
895
896 let operation_name = subcommand_path
897 .last()
898 .map_or_else(|| "unknown".to_string(), ToString::to_string);
899
900 let suggestions = crate::suggestions::suggest_similar_operations(spec, &operation_name);
902
903 Err(Error::operation_not_found_with_suggestions(
904 operation_name,
905 &suggestions,
906 ))
907}
908
909fn get_query_param_value(
912 param: &CachedParameter,
913 current_matches: &ArgMatches,
914 arg_str: &str,
915) -> Option<String> {
916 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
917
918 if is_boolean {
919 current_matches
921 .get_flag(arg_str)
922 .then(|| format!("{arg_str}=true"))
923 } else {
924 current_matches
926 .get_one::<String>(arg_str)
927 .map(|value| format!("{arg_str}={}", urlencoding::encode(value)))
928 }
929}
930
931fn build_url(
936 base_url: &str,
937 path_template: &str,
938 operation: &CachedCommand,
939 matches: &ArgMatches,
940) -> Result<String, Error> {
941 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
942
943 let mut current_matches = matches;
945 while let Some((_name, sub_matches)) = current_matches.subcommand() {
946 current_matches = sub_matches;
947 }
948
949 let mut start = 0;
952 while let Some(open) = url[start..].find('{') {
953 let open_pos = start + open;
954 let Some(close) = url[open_pos..].find('}') else {
955 break;
956 };
957
958 let close_pos = open_pos + close;
959 let param_name = &url[open_pos + 1..close_pos];
960
961 let param = operation.parameters.iter().find(|p| p.name == param_name);
963 let is_boolean = param
964 .and_then(|p| p.schema_type.as_ref())
965 .is_some_and(|t| t == "boolean");
966
967 let value = if is_boolean {
968 if current_matches.get_flag(param_name) {
970 "true"
971 } else {
972 "false"
973 }
974 .to_string()
975 } else {
976 match current_matches
977 .try_get_one::<String>(param_name)
978 .ok()
979 .flatten()
980 {
981 Some(string_value) => string_value.clone(),
982 None => return Err(Error::missing_path_parameter(param_name)),
983 }
984 };
985
986 url.replace_range(open_pos..=close_pos, &value);
987 start = open_pos + value.len();
988 }
989
990 let mut query_params = Vec::new();
992 for arg in current_matches.ids() {
993 let arg_str = arg.as_str();
994 let param = operation
996 .parameters
997 .iter()
998 .find(|p| p.name == arg_str && p.location == "query");
999
1000 let Some(param) = param else {
1001 continue;
1002 };
1003
1004 if let Some(value) = get_query_param_value(param, current_matches, arg_str) {
1006 query_params.push(value);
1007 }
1008 }
1009
1010 if !query_params.is_empty() {
1011 url.push('?');
1012 url.push_str(&query_params.join("&"));
1013 }
1014
1015 Ok(url)
1016}
1017
1018fn get_header_param_value(
1021 param: &CachedParameter,
1022 current_matches: &ArgMatches,
1023) -> Result<Option<HeaderValue>, Error> {
1024 let is_boolean = matches!(param.schema_type.as_deref(), Some("boolean"));
1025
1026 if is_boolean {
1029 return Ok(current_matches
1030 .get_flag(¶m.name)
1031 .then_some(HeaderValue::from_static("true")));
1032 }
1033
1034 current_matches
1036 .get_one::<String>(¶m.name)
1037 .map(|value| {
1038 HeaderValue::from_str(value)
1039 .map_err(|e| Error::invalid_header_value(¶m.name, e.to_string()))
1040 })
1041 .transpose()
1042}
1043
1044fn build_headers(
1046 spec: &CachedSpec,
1047 operation: &CachedCommand,
1048 matches: &ArgMatches,
1049 api_name: &str,
1050 global_config: Option<&GlobalConfig>,
1051) -> Result<HeaderMap, Error> {
1052 let mut headers = HeaderMap::new();
1053
1054 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
1056 headers.insert(
1057 constants::HEADER_ACCEPT,
1058 HeaderValue::from_static(constants::CONTENT_TYPE_JSON),
1059 );
1060
1061 let mut current_matches = matches;
1063 while let Some((_name, sub_matches)) = current_matches.subcommand() {
1064 current_matches = sub_matches;
1065 }
1066
1067 for param in &operation.parameters {
1069 if param.location != "header" {
1071 continue;
1072 }
1073
1074 let header_name = HeaderName::from_str(¶m.name)
1075 .map_err(|e| Error::invalid_header_name(¶m.name, e.to_string()))?;
1076
1077 let Some(header_value) = get_header_param_value(param, current_matches)? else {
1079 continue;
1080 };
1081
1082 headers.insert(header_name, header_value);
1083 }
1084
1085 for security_scheme_name in &operation.security_requirements {
1087 let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) else {
1088 continue;
1089 };
1090 add_authentication_header(&mut headers, security_scheme, api_name, global_config)?;
1091 }
1092
1093 let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") else {
1096 return Ok(headers);
1097 };
1098
1099 for header_str in custom_headers {
1100 let (name, value) = parse_custom_header(header_str)?;
1101 let header_name = HeaderName::from_str(&name)
1102 .map_err(|e| Error::invalid_header_name(&name, e.to_string()))?;
1103 let header_value = HeaderValue::from_str(&value)
1104 .map_err(|e| Error::invalid_header_value(&name, e.to_string()))?;
1105 headers.insert(header_name, header_value);
1106 }
1107
1108 Ok(headers)
1109}
1110
1111fn validate_header_value(name: &str, value: &str) -> Result<(), Error> {
1113 if value.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
1114 return Err(Error::invalid_header_value(
1115 name,
1116 "Header value contains invalid control characters (newline, carriage return, or null)",
1117 ));
1118 }
1119 Ok(())
1120}
1121
1122fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
1124 let colon_pos = header_str
1126 .find(':')
1127 .ok_or_else(|| Error::invalid_header_format(header_str))?;
1128
1129 let name = header_str[..colon_pos].trim();
1130 let value = header_str[colon_pos + 1..].trim();
1131
1132 if name.is_empty() {
1133 return Err(Error::empty_header_name());
1134 }
1135
1136 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
1138 let var_name = &value[2..value.len() - 1];
1140 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
1141 } else {
1142 value.to_string()
1143 };
1144
1145 validate_header_value(name, &expanded_value)?;
1147
1148 Ok((name.to_string(), expanded_value))
1149}
1150
1151fn is_sensitive_header(header_name: &str) -> bool {
1153 let name_lower = header_name.to_lowercase();
1154 matches!(
1155 name_lower.as_str(),
1156 "authorization" | "proxy-authorization" | "x-api-key" | "x-api-token" | "x-auth-token"
1157 )
1158}
1159
1160#[allow(clippy::too_many_lines)]
1162fn add_authentication_header(
1163 headers: &mut HeaderMap,
1164 security_scheme: &CachedSecurityScheme,
1165 api_name: &str,
1166 global_config: Option<&GlobalConfig>,
1167) -> Result<(), Error> {
1168 if std::env::var("RUST_LOG").is_ok() {
1170 eprintln!(
1172 "[DEBUG] Adding authentication header for scheme: {} (type: {})",
1173 security_scheme.name, security_scheme.scheme_type
1174 );
1175 }
1176
1177 let secret_config = global_config
1179 .and_then(|config| config.api_configs.get(api_name))
1180 .and_then(|api_config| api_config.secrets.get(&security_scheme.name));
1181
1182 let (secret_value, env_var_name) = match (secret_config, &security_scheme.aperture_secret) {
1183 (Some(config_secret), _) => {
1184 let secret_value = std::env::var(&config_secret.name)
1186 .map_err(|_| Error::secret_not_set(&security_scheme.name, &config_secret.name))?;
1187 (secret_value, config_secret.name.clone())
1188 }
1189 (None, Some(aperture_secret)) => {
1190 let secret_value = std::env::var(&aperture_secret.name)
1192 .map_err(|_| Error::secret_not_set(&security_scheme.name, &aperture_secret.name))?;
1193 (secret_value, aperture_secret.name.clone())
1194 }
1195 (None, None) => {
1196 return Ok(());
1198 }
1199 };
1200
1201 if std::env::var("RUST_LOG").is_ok() {
1203 let source = if secret_config.is_some() {
1204 "config"
1205 } else {
1206 "x-aperture-secret"
1207 };
1208 eprintln!(
1210 "[DEBUG] Using secret from {source} for scheme '{}': env var '{env_var_name}'",
1211 security_scheme.name
1212 );
1213 }
1214
1215 validate_header_value(constants::HEADER_AUTHORIZATION, &secret_value)?;
1217
1218 match security_scheme.scheme_type.as_str() {
1220 constants::AUTH_SCHEME_APIKEY => {
1221 let (Some(location), Some(param_name)) =
1222 (&security_scheme.location, &security_scheme.parameter_name)
1223 else {
1224 return Ok(());
1225 };
1226
1227 if location == "header" {
1228 let header_name = HeaderName::from_str(param_name)
1229 .map_err(|e| Error::invalid_header_name(param_name, e.to_string()))?;
1230 let header_value = HeaderValue::from_str(&secret_value)
1231 .map_err(|e| Error::invalid_header_value(param_name, e.to_string()))?;
1232 headers.insert(header_name, header_value);
1233 }
1234 }
1236 "http" => {
1237 let Some(scheme_str) = &security_scheme.scheme else {
1238 return Ok(());
1239 };
1240
1241 let auth_scheme: AuthScheme = scheme_str.as_str().into();
1242 let auth_value = match &auth_scheme {
1243 AuthScheme::Bearer => {
1244 format!("Bearer {secret_value}")
1245 }
1246 AuthScheme::Basic => {
1247 let encoded = general_purpose::STANDARD.encode(&secret_value);
1251 format!("Basic {encoded}")
1252 }
1253 AuthScheme::Token
1254 | AuthScheme::DSN
1255 | AuthScheme::ApiKey
1256 | AuthScheme::Custom(_) => {
1257 format!("{scheme_str} {secret_value}")
1261 }
1262 };
1263
1264 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
1265 Error::invalid_header_value(constants::HEADER_AUTHORIZATION, e.to_string())
1266 })?;
1267 headers.insert(constants::HEADER_AUTHORIZATION, header_value);
1268
1269 if std::env::var("RUST_LOG").is_ok() {
1271 match &auth_scheme {
1272 AuthScheme::Bearer => {
1273 eprintln!("[DEBUG] Added Bearer authentication header");
1275 }
1276 AuthScheme::Basic => {
1277 eprintln!("[DEBUG] Added Basic authentication header (base64 encoded)");
1279 }
1280 _ => {
1281 eprintln!(
1283 "[DEBUG] Added custom HTTP auth header with scheme: {scheme_str}"
1284 );
1285 }
1286 }
1287 }
1288 }
1289 _ => {
1290 return Err(Error::unsupported_security_scheme(
1291 &security_scheme.scheme_type,
1292 ));
1293 }
1294 }
1295
1296 Ok(())
1297}
1298
1299fn print_formatted_response(
1301 response_text: &str,
1302 output_format: &OutputFormat,
1303 jq_filter: Option<&str>,
1304 capture_output: bool,
1305) -> Result<Option<String>, Error> {
1306 let processed_text = if let Some(filter) = jq_filter {
1308 apply_jq_filter(response_text, filter)?
1309 } else {
1310 response_text.to_string()
1311 };
1312
1313 match output_format {
1314 OutputFormat::Json => {
1315 let output = serde_json::from_str::<Value>(&processed_text)
1317 .ok()
1318 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
1319 .unwrap_or_else(|| processed_text.clone());
1320
1321 if capture_output {
1322 return Ok(Some(output));
1323 }
1324 println!("{output}");
1326 }
1327 OutputFormat::Yaml => {
1328 let output = serde_json::from_str::<Value>(&processed_text)
1330 .ok()
1331 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
1332 .unwrap_or_else(|| processed_text.clone());
1333
1334 if capture_output {
1335 return Ok(Some(output));
1336 }
1337 println!("{output}");
1339 }
1340 OutputFormat::Table => {
1341 let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) else {
1343 if capture_output {
1345 return Ok(Some(processed_text));
1346 }
1347 println!("{processed_text}");
1349 return Ok(None);
1350 };
1351
1352 let table_output = print_as_table(&json_value, capture_output)?;
1353 if capture_output {
1354 return Ok(table_output);
1355 }
1356 }
1357 }
1358
1359 Ok(None)
1360}
1361
1362#[derive(tabled::Tabled)]
1364struct TableRow {
1365 #[tabled(rename = "Key")]
1366 key: String,
1367 #[tabled(rename = "Value")]
1368 value: String,
1369}
1370
1371#[derive(tabled::Tabled)]
1372struct KeyValue {
1373 #[tabled(rename = "Key")]
1374 key: String,
1375 #[tabled(rename = "Value")]
1376 value: String,
1377}
1378
1379fn print_numbered_list(items: &[Value], capture_output: bool) -> Option<String> {
1381 if capture_output {
1382 let mut output = String::new();
1383 for (i, item) in items.iter().enumerate() {
1384 writeln!(&mut output, "{}: {}", i, format_value_for_table(item))
1385 .expect("writing to String cannot fail");
1386 }
1387 return Some(output.trim_end().to_string());
1388 }
1389
1390 for (i, item) in items.iter().enumerate() {
1391 println!("{}: {}", i, format_value_for_table(item));
1393 }
1394 None
1395}
1396
1397fn output_or_capture(message: &str, capture_output: bool) -> Option<String> {
1399 if capture_output {
1400 return Some(message.to_string());
1401 }
1402 println!("{message}");
1404 None
1405}
1406
1407#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1409fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
1410 match json_value {
1411 Value::Array(items) => {
1412 if items.is_empty() {
1413 return Ok(output_or_capture(constants::EMPTY_ARRAY, capture_output));
1414 }
1415
1416 if items.len() > MAX_TABLE_ROWS {
1418 let msg = format!(
1419 "Array too large: {} items (max {} for table display)\nUse --format json or --jq to process the full data",
1420 items.len(),
1421 MAX_TABLE_ROWS
1422 );
1423 return Ok(output_or_capture(&msg, capture_output));
1424 }
1425
1426 let Some(Value::Object(_)) = items.first() else {
1428 return Ok(print_numbered_list(items, capture_output));
1430 };
1431
1432 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
1434
1435 for item in items {
1436 let Value::Object(obj) = item else {
1437 continue;
1438 };
1439 let mut row = BTreeMap::new();
1440 for (key, value) in obj {
1441 row.insert(key.clone(), format_value_for_table(value));
1442 }
1443 table_data.push(row);
1444 }
1445
1446 if table_data.is_empty() {
1447 return Ok(print_numbered_list(items, capture_output));
1449 }
1450
1451 let mut rows = Vec::new();
1454 for (i, row) in table_data.iter().enumerate() {
1455 if i > 0 {
1456 rows.push(TableRow {
1457 key: "---".to_string(),
1458 value: "---".to_string(),
1459 });
1460 }
1461 for (key, value) in row {
1462 rows.push(TableRow {
1463 key: key.clone(),
1464 value: value.clone(),
1465 });
1466 }
1467 }
1468
1469 let table = Table::new(&rows);
1470 Ok(output_or_capture(&table.to_string(), capture_output))
1471 }
1472 Value::Object(obj) => {
1473 if obj.len() > MAX_TABLE_ROWS {
1475 let msg = format!(
1476 "Object too large: {} fields (max {} for table display)\nUse --format json or --jq to process the full data",
1477 obj.len(),
1478 MAX_TABLE_ROWS
1479 );
1480 return Ok(output_or_capture(&msg, capture_output));
1481 }
1482
1483 let rows: Vec<KeyValue> = obj
1485 .iter()
1486 .map(|(key, value)| KeyValue {
1487 key: key.clone(),
1488 value: format_value_for_table(value),
1489 })
1490 .collect();
1491
1492 let table = Table::new(&rows);
1493 Ok(output_or_capture(&table.to_string(), capture_output))
1494 }
1495 _ => {
1496 let formatted = format_value_for_table(json_value);
1498 Ok(output_or_capture(&formatted, capture_output))
1499 }
1500 }
1501}
1502
1503fn format_value_for_table(value: &Value) -> String {
1505 match value {
1506 Value::Null => constants::NULL_VALUE.to_string(),
1507 Value::Bool(b) => b.to_string(),
1508 Value::Number(n) => n.to_string(),
1509 Value::String(s) => s.clone(),
1510 Value::Array(arr) => {
1511 if arr.len() <= 3 {
1512 format!(
1513 "[{}]",
1514 arr.iter()
1515 .map(format_value_for_table)
1516 .collect::<Vec<_>>()
1517 .join(", ")
1518 )
1519 } else {
1520 format!("[{} items]", arr.len())
1521 }
1522 }
1523 Value::Object(obj) => {
1524 if obj.len() <= 2 {
1525 format!(
1526 "{{{}}}",
1527 obj.iter()
1528 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
1529 .collect::<Vec<_>>()
1530 .join(", ")
1531 )
1532 } else {
1533 format!("{{object with {} fields}}", obj.len())
1534 }
1535 }
1536 }
1537}
1538
1539pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
1548 let json_value: Value = serde_json::from_str(response_text)
1550 .map_err(|e| Error::jq_filter_error(filter, format!("Response is not valid JSON: {e}")))?;
1551
1552 #[cfg(feature = "jq")]
1553 {
1554 use jaq_core::load::{Arena, File, Loader};
1556 use jaq_core::Compiler;
1557
1558 let program = File {
1560 code: filter,
1561 path: (),
1562 };
1563
1564 let defs: Vec<_> = jaq_std::defs().chain(jaq_json::defs()).collect();
1567 let funs: Vec<_> = jaq_std::funs().chain(jaq_json::funs()).collect();
1568
1569 let loader = Loader::new(defs);
1571 let arena = Arena::default();
1572
1573 let modules = match loader.load(&arena, program) {
1575 Ok(modules) => modules,
1576 Err(errs) => {
1577 return Err(Error::jq_filter_error(
1578 filter,
1579 format!("Parse error: {:?}", errs),
1580 ));
1581 }
1582 };
1583
1584 let filter_fn = match Compiler::default().with_funs(funs).compile(modules) {
1586 Ok(filter) => filter,
1587 Err(errs) => {
1588 return Err(Error::jq_filter_error(
1589 filter,
1590 format!("Compilation error: {:?}", errs),
1591 ));
1592 }
1593 };
1594
1595 let jaq_value = Val::from(json_value);
1597
1598 let inputs = RcIter::new(core::iter::empty());
1600 let ctx = Ctx::new([], &inputs);
1601
1602 let output = filter_fn.run((ctx, jaq_value));
1604
1605 let results: Result<Vec<Val>, _> = output.collect();
1607
1608 match results {
1609 Ok(vals) => {
1610 if vals.is_empty() {
1611 return Ok(constants::NULL_VALUE.to_string());
1612 }
1613
1614 if vals.len() == 1 {
1615 let json_val = serde_json::Value::from(vals[0].clone());
1617 return serde_json::to_string_pretty(&json_val).map_err(|e| {
1618 Error::serialization_error(format!("Failed to serialize result: {e}"))
1619 });
1620 }
1621
1622 let json_vals: Vec<Value> = vals.into_iter().map(serde_json::Value::from).collect();
1624 let array = Value::Array(json_vals);
1625 serde_json::to_string_pretty(&array).map_err(|e| {
1626 Error::serialization_error(format!("Failed to serialize results: {e}"))
1627 })
1628 }
1629 Err(e) => Err(Error::jq_filter_error(
1630 format!("{:?}", filter),
1631 format!("Filter execution error: {e}"),
1632 )),
1633 }
1634 }
1635
1636 #[cfg(not(feature = "jq"))]
1637 {
1638 apply_basic_jq_filter(&json_value, filter)
1640 }
1641}
1642
1643#[cfg(not(feature = "jq"))]
1644fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
1646 let uses_advanced_features = filter.contains('[')
1648 || filter.contains(']')
1649 || filter.contains('|')
1650 || filter.contains('(')
1651 || filter.contains(')')
1652 || filter.contains("select")
1653 || filter.contains("map")
1654 || filter.contains("length");
1655
1656 if uses_advanced_features {
1657 eprintln!(
1659 "{} Advanced JQ features require building with --features jq",
1660 crate::constants::MSG_WARNING_PREFIX
1661 );
1662 eprintln!(" Currently only basic field access is supported (e.g., '.field', '.nested.field')");
1664 eprintln!(" To enable full JQ support: cargo install aperture-cli --features jq");
1666 }
1667
1668 let result = match filter {
1669 "." => json_value.clone(),
1670 ".[]" => {
1671 match json_value {
1673 Value::Array(arr) => {
1674 Value::Array(arr.clone())
1676 }
1677 Value::Object(obj) => {
1678 Value::Array(obj.values().cloned().collect())
1680 }
1681 _ => Value::Null,
1682 }
1683 }
1684 ".length" => {
1685 match json_value {
1687 Value::Array(arr) => Value::Number(arr.len().into()),
1688 Value::Object(obj) => Value::Number(obj.len().into()),
1689 Value::String(s) => Value::Number(s.len().into()),
1690 _ => Value::Null,
1691 }
1692 }
1693 filter if filter.starts_with(".[].") => {
1694 let field_path = &filter[4..]; match json_value {
1697 Value::Array(arr) => {
1698 let mapped: Vec<Value> = arr
1699 .iter()
1700 .map(|item| get_nested_field(item, field_path))
1701 .collect();
1702 Value::Array(mapped)
1703 }
1704 _ => Value::Null,
1705 }
1706 }
1707 filter if filter.starts_with('.') => {
1708 let field_path = &filter[1..]; get_nested_field(json_value, field_path)
1711 }
1712 _ => {
1713 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."));
1714 }
1715 };
1716
1717 serde_json::to_string_pretty(&result).map_err(|e| {
1718 Error::serialization_error(format!("Failed to serialize filtered result: {e}"))
1719 })
1720}
1721
1722#[cfg(not(feature = "jq"))]
1723fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
1725 let parts: Vec<&str> = field_path.split('.').collect();
1726 let mut current = json_value;
1727
1728 for part in parts {
1729 if part.is_empty() {
1730 continue;
1731 }
1732
1733 if part.starts_with('[') && part.ends_with(']') {
1735 let index_str = &part[1..part.len() - 1];
1736 let Ok(index) = index_str.parse::<usize>() else {
1737 return Value::Null;
1738 };
1739
1740 match current {
1741 Value::Array(arr) => {
1742 let Some(item) = arr.get(index) else {
1743 return Value::Null;
1744 };
1745 current = item;
1746 }
1747 _ => return Value::Null,
1748 }
1749 continue;
1750 }
1751
1752 match current {
1753 Value::Object(obj) => {
1754 if let Some(field) = obj.get(part) {
1755 current = field;
1756 } else {
1757 return Value::Null;
1758 }
1759 }
1760 Value::Array(arr) => {
1761 let Ok(index) = part.parse::<usize>() else {
1763 return Value::Null;
1764 };
1765
1766 let Some(item) = arr.get(index) else {
1767 return Value::Null;
1768 };
1769 current = item;
1770 }
1771 _ => return Value::Null,
1772 }
1773 }
1774
1775 current.clone()
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780 use super::*;
1781
1782 #[test]
1783 fn test_apply_jq_filter_simple_field_access() {
1784 let json = r#"{"name": "Alice", "age": 30}"#;
1785 let result = apply_jq_filter(json, ".name").unwrap();
1786 let parsed: Value = serde_json::from_str(&result).unwrap();
1787 assert_eq!(parsed, serde_json::json!("Alice"));
1788 }
1789
1790 #[test]
1791 fn test_apply_jq_filter_nested_field_access() {
1792 let json = r#"{"user": {"name": "Bob", "id": 123}}"#;
1793 let result = apply_jq_filter(json, ".user.name").unwrap();
1794 let parsed: Value = serde_json::from_str(&result).unwrap();
1795 assert_eq!(parsed, serde_json::json!("Bob"));
1796 }
1797
1798 #[cfg(feature = "jq")]
1799 #[test]
1800 fn test_apply_jq_filter_array_index() {
1801 let json = r#"{"items": ["first", "second", "third"]}"#;
1802 let result = apply_jq_filter(json, ".items[1]").unwrap();
1803 let parsed: Value = serde_json::from_str(&result).unwrap();
1804 assert_eq!(parsed, serde_json::json!("second"));
1805 }
1806
1807 #[cfg(feature = "jq")]
1808 #[test]
1809 fn test_apply_jq_filter_array_iteration() {
1810 let json = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
1811 let result = apply_jq_filter(json, ".[].id").unwrap();
1812 let parsed: Value = serde_json::from_str(&result).unwrap();
1813 assert_eq!(parsed, serde_json::json!([1, 2, 3]));
1815 }
1816
1817 #[cfg(feature = "jq")]
1818 #[test]
1819 fn test_apply_jq_filter_complex_expression() {
1820 let json = r#"{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}"#;
1821 let result = apply_jq_filter(json, ".users | map(.name)").unwrap();
1822 let parsed: Value = serde_json::from_str(&result).unwrap();
1823 assert_eq!(parsed, serde_json::json!(["Alice", "Bob"]));
1824 }
1825
1826 #[cfg(feature = "jq")]
1827 #[test]
1828 fn test_apply_jq_filter_select() {
1829 let json =
1830 r#"[{"id": 1, "active": true}, {"id": 2, "active": false}, {"id": 3, "active": true}]"#;
1831 let result = apply_jq_filter(json, "[.[] | select(.active)]").unwrap();
1832 let parsed: Value = serde_json::from_str(&result).unwrap();
1833 assert_eq!(
1834 parsed,
1835 serde_json::json!([{"id": 1, "active": true}, {"id": 3, "active": true}])
1836 );
1837 }
1838
1839 #[test]
1840 fn test_apply_jq_filter_invalid_json() {
1841 let json = "not valid json";
1842 let result = apply_jq_filter(json, ".field");
1843 assert!(result.is_err());
1844 if let Err(err) = result {
1845 let error_msg = err.to_string();
1846 assert!(error_msg.contains("JQ filter error"));
1847 assert!(error_msg.contains(".field"));
1848 assert!(error_msg.contains("Response is not valid JSON"));
1849 } else {
1850 panic!("Expected error");
1851 }
1852 }
1853
1854 #[cfg(feature = "jq")]
1855 #[test]
1856 fn test_apply_jq_filter_invalid_expression() {
1857 let json = r#"{"name": "test"}"#;
1858 let result = apply_jq_filter(json, "invalid..expression");
1859 assert!(result.is_err());
1860 if let Err(err) = result {
1861 let error_msg = err.to_string();
1862 assert!(error_msg.contains("JQ filter error") || error_msg.contains("Parse error"));
1863 assert!(error_msg.contains("invalid..expression"));
1864 } else {
1865 panic!("Expected error");
1866 }
1867 }
1868
1869 #[test]
1870 fn test_apply_jq_filter_null_result() {
1871 let json = r#"{"name": "test"}"#;
1872 let result = apply_jq_filter(json, ".missing_field").unwrap();
1873 let parsed: Value = serde_json::from_str(&result).unwrap();
1874 assert_eq!(parsed, serde_json::json!(null));
1875 }
1876
1877 #[cfg(feature = "jq")]
1878 #[test]
1879 fn test_apply_jq_filter_arithmetic() {
1880 let json = r#"{"x": 10, "y": 20}"#;
1881 let result = apply_jq_filter(json, ".x + .y").unwrap();
1882 let parsed: Value = serde_json::from_str(&result).unwrap();
1883 assert_eq!(parsed, serde_json::json!(30));
1884 }
1885
1886 #[cfg(feature = "jq")]
1887 #[test]
1888 fn test_apply_jq_filter_string_concatenation() {
1889 let json = r#"{"first": "Hello", "second": "World"}"#;
1890 let result = apply_jq_filter(json, r#".first + " " + .second"#).unwrap();
1891 let parsed: Value = serde_json::from_str(&result).unwrap();
1892 assert_eq!(parsed, serde_json::json!("Hello World"));
1893 }
1894
1895 #[cfg(feature = "jq")]
1896 #[test]
1897 fn test_apply_jq_filter_length() {
1898 let json = r#"{"items": [1, 2, 3, 4, 5]}"#;
1899 let result = apply_jq_filter(json, ".items | length").unwrap();
1900 let parsed: Value = serde_json::from_str(&result).unwrap();
1901 assert_eq!(parsed, serde_json::json!(5));
1902 }
1903}