1use crate::cache::models::{CachedCommand, CachedSecurityScheme, CachedSpec};
2use crate::cli::OutputFormat;
3use crate::config::models::GlobalConfig;
4use crate::config::url_resolver::BaseUrlResolver;
5use crate::error::Error;
6use crate::response_cache::{CacheConfig, CacheKey, CachedRequestInfo, ResponseCache};
7use crate::utils::to_kebab_case;
8use clap::ArgMatches;
9use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
10use reqwest::Method;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::str::FromStr;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AuthScheme {
18 Bearer,
19 Basic,
20 Token,
21 DSN,
22 ApiKey,
23 Custom(String),
24}
25
26impl From<&str> for AuthScheme {
27 fn from(s: &str) -> Self {
28 match s.to_lowercase().as_str() {
29 "bearer" => Self::Bearer,
30 "basic" => Self::Basic,
31 "token" => Self::Token,
32 "dsn" => Self::DSN,
33 "apikey" => Self::ApiKey,
34 _ => Self::Custom(s.to_string()),
35 }
36 }
37}
38
39const MAX_TABLE_ROWS: usize = 1000;
41
42#[allow(clippy::too_many_lines)]
69#[allow(clippy::too_many_arguments)]
70pub async fn execute_request(
71 spec: &CachedSpec,
72 matches: &ArgMatches,
73 base_url: Option<&str>,
74 dry_run: bool,
75 idempotency_key: Option<&str>,
76 global_config: Option<&GlobalConfig>,
77 output_format: &OutputFormat,
78 jq_filter: Option<&str>,
79 cache_config: Option<&CacheConfig>,
80 capture_output: bool,
81) -> Result<Option<String>, Error> {
82 let operation = find_operation(spec, matches)?;
84
85 let server_var_args: Vec<String> = matches
88 .try_get_many::<String>("server-var")
89 .ok()
90 .flatten()
91 .map(|values| values.cloned().collect())
92 .unwrap_or_default();
93
94 let resolver = BaseUrlResolver::new(spec);
96 let resolver = if let Some(config) = global_config {
97 resolver.with_global_config(config)
98 } else {
99 resolver
100 };
101 let base_url = resolver.resolve_with_variables(base_url, &server_var_args)?;
102
103 let url = build_url(&base_url, &operation.path, operation, matches)?;
105
106 let client = reqwest::Client::builder()
108 .timeout(std::time::Duration::from_secs(30))
109 .build()
110 .map_err(|e| Error::RequestFailed {
111 reason: format!("Failed to create HTTP client: {e}"),
112 })?;
113
114 let mut headers = build_headers(spec, operation, matches, &spec.name, global_config)?;
116
117 if let Some(key) = idempotency_key {
119 headers.insert(
120 HeaderName::from_static("idempotency-key"),
121 HeaderValue::from_str(key).map_err(|_| Error::InvalidIdempotencyKey)?,
122 );
123 }
124
125 let method = Method::from_str(&operation.method).map_err(|_| Error::InvalidHttpMethod {
127 method: operation.method.clone(),
128 })?;
129
130 let headers_clone = headers.clone(); let mut request = client.request(method.clone(), &url).headers(headers);
132
133 let mut current_matches = matches;
136 while let Some((_name, sub_matches)) = current_matches.subcommand() {
137 current_matches = sub_matches;
138 }
139
140 let request_body = if operation.request_body.is_some() {
141 if let Some(body_value) = current_matches.get_one::<String>("body") {
142 let json_body: Value =
143 serde_json::from_str(body_value).map_err(|e| Error::InvalidJsonBody {
144 reason: e.to_string(),
145 })?;
146 request = request.json(&json_body);
147 Some(body_value.clone())
148 } else {
149 None
150 }
151 } else {
152 None
153 };
154
155 let cache_key = if let Some(cache_cfg) = cache_config {
157 if cache_cfg.enabled {
158 let header_map: HashMap<String, String> = headers_clone
160 .iter()
161 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
162 .collect();
163
164 let cache_key = CacheKey::from_request(
165 &spec.name,
166 &operation.operation_id,
167 method.as_ref(),
168 &url,
169 &header_map,
170 request_body.as_deref(),
171 )?;
172
173 let response_cache = ResponseCache::new(cache_cfg.clone())?;
174
175 if let Some(cached_response) = response_cache.get(&cache_key).await? {
177 let output = print_formatted_response(
179 &cached_response.body,
180 output_format,
181 jq_filter,
182 capture_output,
183 )?;
184 return Ok(output);
185 }
186
187 Some((cache_key, response_cache))
188 } else {
189 None
190 }
191 } else {
192 None
193 };
194
195 if dry_run {
197 let dry_run_info = serde_json::json!({
198 "dry_run": true,
199 "method": operation.method,
200 "url": url,
201 "headers": headers_clone.iter().map(|(k, v)| (k.as_str(), v.to_str().unwrap_or("<binary>"))).collect::<std::collections::HashMap<_, _>>(),
202 "operation_id": operation.operation_id
203 });
204 let dry_run_output =
205 serde_json::to_string_pretty(&dry_run_info).map_err(|e| Error::SerializationError {
206 reason: format!("Failed to serialize dry-run info: {e}"),
207 })?;
208
209 if capture_output {
210 return Ok(Some(dry_run_output));
211 }
212 println!("{dry_run_output}");
213 return Ok(None);
214 }
215
216 let response = request.send().await.map_err(|e| Error::RequestFailed {
218 reason: e.to_string(),
219 })?;
220
221 let status = response.status();
222 let response_headers: HashMap<String, String> = response
223 .headers()
224 .iter()
225 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
226 .collect();
227
228 let response_text = response
229 .text()
230 .await
231 .map_err(|e| Error::ResponseReadError {
232 reason: e.to_string(),
233 })?;
234
235 if !status.is_success() {
237 let api_name = spec.name.clone();
239 let operation_id = Some(operation.operation_id.clone());
240 let security_schemes: Vec<String> = operation
241 .security_requirements
242 .iter()
243 .filter_map(|scheme_name| {
244 spec.security_schemes
245 .get(scheme_name)
246 .and_then(|scheme| scheme.aperture_secret.as_ref())
247 .map(|aperture_secret| aperture_secret.name.clone())
248 })
249 .collect();
250
251 return Err(Error::HttpErrorWithContext {
252 status: status.as_u16(),
253 body: if response_text.is_empty() {
254 "(empty response)".to_string()
255 } else {
256 response_text
257 },
258 api_name,
259 operation_id,
260 security_schemes,
261 });
262 }
263
264 if let Some((cache_key, response_cache)) = cache_key {
266 let cached_request_info = CachedRequestInfo {
268 method: method.to_string(),
269 url: url.clone(),
270 headers: headers_clone
271 .iter()
272 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
273 .collect(),
274 body_hash: request_body.as_ref().map(|body| {
275 use sha2::{Digest, Sha256};
276 let mut hasher = Sha256::new();
277 hasher.update(body.as_bytes());
278 format!("{:x}", hasher.finalize())
279 }),
280 };
281
282 let cache_ttl = cache_config.and_then(|cfg| {
284 if cfg.default_ttl.as_secs() > 0 {
285 Some(cfg.default_ttl)
286 } else {
287 None
288 }
289 });
290
291 let _ = response_cache
292 .store(
293 &cache_key,
294 &response_text,
295 status.as_u16(),
296 &response_headers,
297 cached_request_info,
298 cache_ttl,
299 )
300 .await;
301 }
302
303 if response_text.is_empty() {
305 Ok(None)
306 } else {
307 print_formatted_response(&response_text, output_format, jq_filter, capture_output)
308 }
309}
310
311fn find_operation<'a>(
313 spec: &'a CachedSpec,
314 matches: &ArgMatches,
315) -> Result<&'a CachedCommand, Error> {
316 let mut current_matches = matches;
318 let mut subcommand_path = Vec::new();
319
320 while let Some((name, sub_matches)) = current_matches.subcommand() {
321 subcommand_path.push(name);
322 current_matches = sub_matches;
323 }
324
325 if let Some(operation_name) = subcommand_path.last() {
328 for command in &spec.commands {
329 let kebab_id = to_kebab_case(&command.operation_id);
331 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
332 return Ok(command);
333 }
334 }
335 }
336
337 Err(Error::OperationNotFound)
338}
339
340fn build_url(
345 base_url: &str,
346 path_template: &str,
347 operation: &CachedCommand,
348 matches: &ArgMatches,
349) -> Result<String, Error> {
350 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
351
352 let mut current_matches = matches;
354 while let Some((_name, sub_matches)) = current_matches.subcommand() {
355 current_matches = sub_matches;
356 }
357
358 let mut start = 0;
361 while let Some(open) = url[start..].find('{') {
362 let open_pos = start + open;
363 if let Some(close) = url[open_pos..].find('}') {
364 let close_pos = open_pos + close;
365 let param_name = &url[open_pos + 1..close_pos];
366
367 if let Some(value) = current_matches
368 .try_get_one::<String>(param_name)
369 .ok()
370 .flatten()
371 {
372 url.replace_range(open_pos..=close_pos, value);
373 start = open_pos + value.len();
374 } else {
375 return Err(Error::MissingPathParameter {
376 name: param_name.to_string(),
377 });
378 }
379 } else {
380 break;
381 }
382 }
383
384 let mut query_params = Vec::new();
386 for arg in current_matches.ids() {
387 let arg_str = arg.as_str();
388 let is_query_param = operation
390 .parameters
391 .iter()
392 .any(|p| p.name == arg_str && p.location == "query");
393 if is_query_param {
394 if let Some(value) = current_matches.get_one::<String>(arg_str) {
395 query_params.push(format!("{}={}", arg_str, urlencoding::encode(value)));
396 }
397 }
398 }
399
400 if !query_params.is_empty() {
401 url.push('?');
402 url.push_str(&query_params.join("&"));
403 }
404
405 Ok(url)
406}
407
408fn build_headers(
410 spec: &CachedSpec,
411 operation: &CachedCommand,
412 matches: &ArgMatches,
413 api_name: &str,
414 global_config: Option<&GlobalConfig>,
415) -> Result<HeaderMap, Error> {
416 let mut headers = HeaderMap::new();
417
418 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
420 headers.insert("Accept", HeaderValue::from_static("application/json"));
421
422 let mut current_matches = matches;
424 while let Some((_name, sub_matches)) = current_matches.subcommand() {
425 current_matches = sub_matches;
426 }
427
428 for param in &operation.parameters {
430 if param.location == "header" {
431 if let Some(value) = current_matches.get_one::<String>(¶m.name) {
432 let header_name =
433 HeaderName::from_str(¶m.name).map_err(|e| Error::InvalidHeaderName {
434 name: param.name.clone(),
435 reason: e.to_string(),
436 })?;
437 let header_value =
438 HeaderValue::from_str(value).map_err(|e| Error::InvalidHeaderValue {
439 name: param.name.clone(),
440 reason: e.to_string(),
441 })?;
442 headers.insert(header_name, header_value);
443 }
444 }
445 }
446
447 for security_scheme_name in &operation.security_requirements {
449 if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
450 add_authentication_header(&mut headers, security_scheme, api_name, global_config)?;
451 }
452 }
453
454 if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
457 for header_str in custom_headers {
458 let (name, value) = parse_custom_header(header_str)?;
459 let header_name =
460 HeaderName::from_str(&name).map_err(|e| Error::InvalidHeaderName {
461 name: name.clone(),
462 reason: e.to_string(),
463 })?;
464 let header_value =
465 HeaderValue::from_str(&value).map_err(|e| Error::InvalidHeaderValue {
466 name: name.clone(),
467 reason: e.to_string(),
468 })?;
469 headers.insert(header_name, header_value);
470 }
471 }
472
473 Ok(headers)
474}
475
476fn validate_header_value(name: &str, value: &str) -> Result<(), Error> {
478 if value.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
479 return Err(Error::InvalidHeaderValue {
480 name: name.to_string(),
481 reason: "Header value contains invalid control characters (newline, carriage return, or null)".to_string(),
482 });
483 }
484 Ok(())
485}
486
487fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
489 let colon_pos = header_str
491 .find(':')
492 .ok_or_else(|| Error::InvalidHeaderFormat {
493 header: header_str.to_string(),
494 })?;
495
496 let name = header_str[..colon_pos].trim();
497 let value = header_str[colon_pos + 1..].trim();
498
499 if name.is_empty() {
500 return Err(Error::EmptyHeaderName);
501 }
502
503 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
505 let var_name = &value[2..value.len() - 1];
507 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
508 } else {
509 value.to_string()
510 };
511
512 validate_header_value(name, &expanded_value)?;
514
515 Ok((name.to_string(), expanded_value))
516}
517
518#[allow(clippy::too_many_lines)]
520fn add_authentication_header(
521 headers: &mut HeaderMap,
522 security_scheme: &CachedSecurityScheme,
523 api_name: &str,
524 global_config: Option<&GlobalConfig>,
525) -> Result<(), Error> {
526 if std::env::var("RUST_LOG").is_ok() {
528 eprintln!(
529 "[DEBUG] Adding authentication header for scheme: {} (type: {})",
530 security_scheme.name, security_scheme.scheme_type
531 );
532 }
533
534 let secret_config = global_config
536 .and_then(|config| config.api_configs.get(api_name))
537 .and_then(|api_config| api_config.secrets.get(&security_scheme.name));
538
539 let (secret_value, env_var_name) = if let Some(config_secret) = secret_config {
540 let secret_value = std::env::var(&config_secret.name).map_err(|_| Error::SecretNotSet {
542 scheme_name: security_scheme.name.clone(),
543 env_var: config_secret.name.clone(),
544 })?;
545 (secret_value, config_secret.name.clone())
546 } else if let Some(aperture_secret) = &security_scheme.aperture_secret {
547 let secret_value =
549 std::env::var(&aperture_secret.name).map_err(|_| Error::SecretNotSet {
550 scheme_name: security_scheme.name.clone(),
551 env_var: aperture_secret.name.clone(),
552 })?;
553 (secret_value, aperture_secret.name.clone())
554 } else {
555 return Ok(());
557 };
558
559 if std::env::var("RUST_LOG").is_ok() {
561 let source = if secret_config.is_some() {
562 "config"
563 } else {
564 "x-aperture-secret"
565 };
566 eprintln!(
567 "[DEBUG] Using secret from {source} for scheme '{}': env var '{env_var_name}'",
568 security_scheme.name
569 );
570 }
571
572 validate_header_value("Authorization", &secret_value)?;
574
575 match security_scheme.scheme_type.as_str() {
577 "apiKey" => {
578 let (Some(location), Some(param_name)) =
579 (&security_scheme.location, &security_scheme.parameter_name)
580 else {
581 return Ok(());
582 };
583
584 if location == "header" {
585 let header_name =
586 HeaderName::from_str(param_name).map_err(|e| Error::InvalidHeaderName {
587 name: param_name.clone(),
588 reason: e.to_string(),
589 })?;
590 let header_value = HeaderValue::from_str(&secret_value).map_err(|e| {
591 Error::InvalidHeaderValue {
592 name: param_name.clone(),
593 reason: e.to_string(),
594 }
595 })?;
596 headers.insert(header_name, header_value);
597 }
598 }
600 "http" => {
601 if let Some(scheme_str) = &security_scheme.scheme {
602 let auth_scheme: AuthScheme = scheme_str.as_str().into();
603 let auth_value = match &auth_scheme {
604 AuthScheme::Bearer => {
605 format!("Bearer {secret_value}")
606 }
607 AuthScheme::Basic => {
608 use base64::{engine::general_purpose, Engine as _};
612 let encoded = general_purpose::STANDARD.encode(&secret_value);
613 format!("Basic {encoded}")
614 }
615 AuthScheme::Token
616 | AuthScheme::DSN
617 | AuthScheme::ApiKey
618 | AuthScheme::Custom(_) => {
619 format!("{scheme_str} {secret_value}")
623 }
624 };
625
626 let header_value =
627 HeaderValue::from_str(&auth_value).map_err(|e| Error::InvalidHeaderValue {
628 name: "Authorization".to_string(),
629 reason: e.to_string(),
630 })?;
631 headers.insert("Authorization", header_value);
632
633 if std::env::var("RUST_LOG").is_ok() {
635 match &auth_scheme {
636 AuthScheme::Bearer => {
637 eprintln!("[DEBUG] Added Bearer authentication header");
638 }
639 AuthScheme::Basic => {
640 eprintln!("[DEBUG] Added Basic authentication header (base64 encoded)");
641 }
642 _ => {
643 eprintln!(
644 "[DEBUG] Added custom HTTP auth header with scheme: {scheme_str}"
645 );
646 }
647 }
648 }
649 }
650 }
651 _ => {
652 return Err(Error::UnsupportedSecurityScheme {
653 scheme_type: security_scheme.scheme_type.clone(),
654 });
655 }
656 }
657
658 Ok(())
659}
660
661fn print_formatted_response(
663 response_text: &str,
664 output_format: &OutputFormat,
665 jq_filter: Option<&str>,
666 capture_output: bool,
667) -> Result<Option<String>, Error> {
668 let processed_text = if let Some(filter) = jq_filter {
670 apply_jq_filter(response_text, filter)?
671 } else {
672 response_text.to_string()
673 };
674
675 match output_format {
676 OutputFormat::Json => {
677 let output = serde_json::from_str::<Value>(&processed_text)
679 .ok()
680 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
681 .unwrap_or_else(|| processed_text.clone());
682
683 if capture_output {
684 return Ok(Some(output));
685 }
686 println!("{output}");
687 }
688 OutputFormat::Yaml => {
689 let output = serde_json::from_str::<Value>(&processed_text)
691 .ok()
692 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
693 .unwrap_or_else(|| processed_text.clone());
694
695 if capture_output {
696 return Ok(Some(output));
697 }
698 println!("{output}");
699 }
700 OutputFormat::Table => {
701 if let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) {
703 let table_output = print_as_table(&json_value, capture_output)?;
704 if capture_output {
705 return Ok(table_output);
706 }
707 } else {
708 if capture_output {
710 return Ok(Some(processed_text));
711 }
712 println!("{processed_text}");
713 }
714 }
715 }
716
717 Ok(None)
718}
719
720#[derive(tabled::Tabled)]
722struct TableRow {
723 #[tabled(rename = "Key")]
724 key: String,
725 #[tabled(rename = "Value")]
726 value: String,
727}
728
729#[derive(tabled::Tabled)]
730struct KeyValue {
731 #[tabled(rename = "Key")]
732 key: String,
733 #[tabled(rename = "Value")]
734 value: String,
735}
736
737#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
739fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
740 use std::collections::BTreeMap;
741 use tabled::Table;
742
743 match json_value {
744 Value::Array(items) => {
745 if items.is_empty() {
746 if capture_output {
747 return Ok(Some("(empty array)".to_string()));
748 }
749 println!("(empty array)");
750 return Ok(None);
751 }
752
753 if items.len() > MAX_TABLE_ROWS {
755 let msg1 = format!(
756 "Array too large: {} items (max {} for table display)",
757 items.len(),
758 MAX_TABLE_ROWS
759 );
760 let msg2 = "Use --format json or --jq to process the full data";
761
762 if capture_output {
763 return Ok(Some(format!("{msg1}\n{msg2}")));
764 }
765 println!("{msg1}");
766 println!("{msg2}");
767 return Ok(None);
768 }
769
770 if let Some(Value::Object(_)) = items.first() {
772 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
774
775 for item in items {
776 if let Value::Object(obj) = item {
777 let mut row = BTreeMap::new();
778 for (key, value) in obj {
779 row.insert(key.clone(), format_value_for_table(value));
780 }
781 table_data.push(row);
782 }
783 }
784
785 if !table_data.is_empty() {
786 let mut rows = Vec::new();
789 for (i, row) in table_data.iter().enumerate() {
790 if i > 0 {
791 rows.push(TableRow {
792 key: "---".to_string(),
793 value: "---".to_string(),
794 });
795 }
796 for (key, value) in row {
797 rows.push(TableRow {
798 key: key.clone(),
799 value: value.clone(),
800 });
801 }
802 }
803
804 let table = Table::new(&rows);
805 if capture_output {
806 return Ok(Some(table.to_string()));
807 }
808 println!("{table}");
809 return Ok(None);
810 }
811 }
812
813 if capture_output {
815 let mut output = String::new();
816 for (i, item) in items.iter().enumerate() {
817 use std::fmt::Write;
818 writeln!(&mut output, "{}: {}", i, format_value_for_table(item)).unwrap();
819 }
820 return Ok(Some(output.trim_end().to_string()));
821 }
822 for (i, item) in items.iter().enumerate() {
823 println!("{}: {}", i, format_value_for_table(item));
824 }
825 }
826 Value::Object(obj) => {
827 if obj.len() > MAX_TABLE_ROWS {
829 let msg1 = format!(
830 "Object too large: {} fields (max {} for table display)",
831 obj.len(),
832 MAX_TABLE_ROWS
833 );
834 let msg2 = "Use --format json or --jq to process the full data";
835
836 if capture_output {
837 return Ok(Some(format!("{msg1}\n{msg2}")));
838 }
839 println!("{msg1}");
840 println!("{msg2}");
841 return Ok(None);
842 }
843
844 let rows: Vec<KeyValue> = obj
846 .iter()
847 .map(|(key, value)| KeyValue {
848 key: key.clone(),
849 value: format_value_for_table(value),
850 })
851 .collect();
852
853 let table = Table::new(&rows);
854 if capture_output {
855 return Ok(Some(table.to_string()));
856 }
857 println!("{table}");
858 }
859 _ => {
860 let formatted = format_value_for_table(json_value);
862 if capture_output {
863 return Ok(Some(formatted));
864 }
865 println!("{formatted}");
866 }
867 }
868
869 Ok(None)
870}
871
872fn format_value_for_table(value: &Value) -> String {
874 match value {
875 Value::Null => "null".to_string(),
876 Value::Bool(b) => b.to_string(),
877 Value::Number(n) => n.to_string(),
878 Value::String(s) => s.clone(),
879 Value::Array(arr) => {
880 if arr.len() <= 3 {
881 format!(
882 "[{}]",
883 arr.iter()
884 .map(format_value_for_table)
885 .collect::<Vec<_>>()
886 .join(", ")
887 )
888 } else {
889 format!("[{} items]", arr.len())
890 }
891 }
892 Value::Object(obj) => {
893 if obj.len() <= 2 {
894 format!(
895 "{{{}}}",
896 obj.iter()
897 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
898 .collect::<Vec<_>>()
899 .join(", ")
900 )
901 } else {
902 format!("{{object with {} fields}}", obj.len())
903 }
904 }
905 }
906}
907
908pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
917 let json_value: Value =
919 serde_json::from_str(response_text).map_err(|e| Error::JqFilterError {
920 reason: format!("Response is not valid JSON: {e}"),
921 })?;
922
923 #[cfg(feature = "jq")]
924 {
925 use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
927 use jaq_parse::parse;
928 use jaq_std::std;
929
930 let (expr, errs) = parse(filter, jaq_parse::main());
932 if !errs.is_empty() {
933 return Err(Error::JqFilterError {
934 reason: format!("Parse error in jq expression: {}", errs[0]),
935 });
936 }
937
938 let mut ctx = ParseCtx::new(Vec::new());
940 ctx.insert_defs(std());
941 let filter = ctx.compile(expr.unwrap());
942
943 let jaq_value = serde_json_to_jaq_val(&json_value);
945
946 let inputs = RcIter::new(core::iter::empty());
948 let ctx = Ctx::new([], &inputs);
949 let results: Result<Vec<Val>, _> = filter.run((ctx, jaq_value.into())).collect();
950
951 match results {
952 Ok(vals) => {
953 if vals.is_empty() {
954 Ok("null".to_string())
955 } else if vals.len() == 1 {
956 let json_val = jaq_val_to_serde_json(&vals[0]);
958 serde_json::to_string_pretty(&json_val).map_err(|e| Error::JqFilterError {
959 reason: format!("Failed to serialize result: {e}"),
960 })
961 } else {
962 let json_vals: Vec<Value> = vals.iter().map(jaq_val_to_serde_json).collect();
964 let array = Value::Array(json_vals);
965 serde_json::to_string_pretty(&array).map_err(|e| Error::JqFilterError {
966 reason: format!("Failed to serialize results: {e}"),
967 })
968 }
969 }
970 Err(e) => Err(Error::JqFilterError {
971 reason: format!("Filter execution error: {e}"),
972 }),
973 }
974 }
975
976 #[cfg(not(feature = "jq"))]
977 {
978 apply_basic_jq_filter(&json_value, filter)
980 }
981}
982
983#[cfg(not(feature = "jq"))]
984fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
986 let uses_advanced_features = filter.contains('[')
988 || filter.contains(']')
989 || filter.contains('|')
990 || filter.contains('(')
991 || filter.contains(')')
992 || filter.contains("select")
993 || filter.contains("map")
994 || filter.contains("length");
995
996 if uses_advanced_features {
997 eprintln!("Warning: Advanced JQ features require building with --features jq");
998 eprintln!(" Currently only basic field access is supported (e.g., '.field', '.nested.field')");
999 eprintln!(" To enable full JQ support: cargo install aperture-cli --features jq");
1000 }
1001
1002 let result = match filter {
1003 "." => json_value.clone(),
1004 ".[]" => {
1005 match json_value {
1007 Value::Array(arr) => {
1008 Value::Array(arr.clone())
1010 }
1011 Value::Object(obj) => {
1012 Value::Array(obj.values().cloned().collect())
1014 }
1015 _ => Value::Null,
1016 }
1017 }
1018 ".length" => {
1019 match json_value {
1021 Value::Array(arr) => Value::Number(arr.len().into()),
1022 Value::Object(obj) => Value::Number(obj.len().into()),
1023 Value::String(s) => Value::Number(s.len().into()),
1024 _ => Value::Null,
1025 }
1026 }
1027 filter if filter.starts_with(".[].") => {
1028 let field_path = &filter[4..]; match json_value {
1031 Value::Array(arr) => {
1032 let mapped: Vec<Value> = arr
1033 .iter()
1034 .map(|item| get_nested_field(item, field_path))
1035 .collect();
1036 Value::Array(mapped)
1037 }
1038 _ => Value::Null,
1039 }
1040 }
1041 filter if filter.starts_with('.') => {
1042 let field_path = &filter[1..]; get_nested_field(json_value, field_path)
1045 }
1046 _ => {
1047 return Err(Error::JqFilterError {
1048 reason: format!("Unsupported JQ filter: '{filter}'. Only basic field access like '.name' or '.metadata.role' is supported without the full jq library."),
1049 });
1050 }
1051 };
1052
1053 serde_json::to_string_pretty(&result).map_err(|e| Error::JqFilterError {
1054 reason: format!("Failed to serialize filtered result: {e}"),
1055 })
1056}
1057
1058#[cfg(not(feature = "jq"))]
1059fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
1061 let parts: Vec<&str> = field_path.split('.').collect();
1062 let mut current = json_value;
1063
1064 for part in parts {
1065 if part.is_empty() {
1066 continue;
1067 }
1068
1069 if part.starts_with('[') && part.ends_with(']') {
1071 let index_str = &part[1..part.len() - 1];
1072 if let Ok(index) = index_str.parse::<usize>() {
1073 match current {
1074 Value::Array(arr) => {
1075 if let Some(item) = arr.get(index) {
1076 current = item;
1077 } else {
1078 return Value::Null;
1079 }
1080 }
1081 _ => return Value::Null,
1082 }
1083 } else {
1084 return Value::Null;
1085 }
1086 continue;
1087 }
1088
1089 match current {
1090 Value::Object(obj) => {
1091 if let Some(field) = obj.get(part) {
1092 current = field;
1093 } else {
1094 return Value::Null;
1095 }
1096 }
1097 Value::Array(arr) => {
1098 if let Ok(index) = part.parse::<usize>() {
1100 if let Some(item) = arr.get(index) {
1101 current = item;
1102 } else {
1103 return Value::Null;
1104 }
1105 } else {
1106 return Value::Null;
1107 }
1108 }
1109 _ => return Value::Null,
1110 }
1111 }
1112
1113 current.clone()
1114}
1115
1116#[cfg(feature = "jq")]
1117fn serde_json_to_jaq_val(value: &Value) -> jaq_interpret::Val {
1119 use jaq_interpret::Val;
1120 use std::rc::Rc;
1121
1122 match value {
1123 Value::Null => Val::Null,
1124 Value::Bool(b) => Val::Bool(*b),
1125 Value::Number(n) => {
1126 if let Some(i) = n.as_i64() {
1127 if let Ok(isize_val) = isize::try_from(i) {
1129 Val::Int(isize_val)
1130 } else {
1131 Val::Float(i as f64)
1133 }
1134 } else if let Some(f) = n.as_f64() {
1135 Val::Float(f)
1136 } else {
1137 Val::Null
1138 }
1139 }
1140 Value::String(s) => Val::Str(s.clone().into()),
1141 Value::Array(arr) => {
1142 let jaq_arr: Vec<Val> = arr.iter().map(serde_json_to_jaq_val).collect();
1143 Val::Arr(Rc::new(jaq_arr))
1144 }
1145 Value::Object(obj) => {
1146 let mut jaq_obj = indexmap::IndexMap::with_hasher(ahash::RandomState::new());
1147 for (k, v) in obj {
1148 jaq_obj.insert(Rc::new(k.clone()), serde_json_to_jaq_val(v));
1149 }
1150 Val::Obj(Rc::new(jaq_obj))
1151 }
1152 }
1153}
1154
1155#[cfg(feature = "jq")]
1156fn jaq_val_to_serde_json(val: &jaq_interpret::Val) -> Value {
1158 use jaq_interpret::Val;
1159
1160 match val {
1161 Val::Null => Value::Null,
1162 Val::Bool(b) => Value::Bool(*b),
1163 Val::Int(i) => {
1164 Value::Number((*i as i64).into())
1166 }
1167 Val::Float(f) => {
1168 if let Some(num) = serde_json::Number::from_f64(*f) {
1169 Value::Number(num)
1170 } else {
1171 Value::Null
1172 }
1173 }
1174 Val::Str(s) => Value::String(s.to_string()),
1175 Val::Arr(arr) => {
1176 let json_arr: Vec<Value> = arr.iter().map(jaq_val_to_serde_json).collect();
1177 Value::Array(json_arr)
1178 }
1179 Val::Obj(obj) => {
1180 let mut json_obj = serde_json::Map::new();
1181 for (k, v) in obj.iter() {
1182 json_obj.insert(k.to_string(), jaq_val_to_serde_json(v));
1183 }
1184 Value::Object(json_obj)
1185 }
1186 _ => Value::Null, }
1188}