1use crate::cache::models::{CachedCommand, CachedSecurityScheme, CachedSpec};
2use crate::cli::OutputFormat;
3use crate::config::models::GlobalConfig;
4use crate::config::url_resolver::BaseUrlResolver;
5use crate::error::Error;
6use crate::response_cache::{CacheConfig, CacheKey, CachedRequestInfo, ResponseCache};
7use clap::ArgMatches;
8use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
9use reqwest::Method;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::str::FromStr;
13
14const MAX_TABLE_ROWS: usize = 1000;
16
17#[allow(clippy::too_many_lines)]
44#[allow(clippy::too_many_arguments)]
45pub async fn execute_request(
46 spec: &CachedSpec,
47 matches: &ArgMatches,
48 base_url: Option<&str>,
49 dry_run: bool,
50 idempotency_key: Option<&str>,
51 global_config: Option<&GlobalConfig>,
52 output_format: &OutputFormat,
53 jq_filter: Option<&str>,
54 cache_config: Option<&CacheConfig>,
55 capture_output: bool,
56) -> Result<Option<String>, Error> {
57 let operation = find_operation(spec, matches)?;
59
60 let resolver = BaseUrlResolver::new(spec);
62 let resolver = if let Some(config) = global_config {
63 resolver.with_global_config(config)
64 } else {
65 resolver
66 };
67 let base_url = resolver.resolve(base_url);
68
69 let url = build_url(&base_url, &operation.path, operation, matches)?;
71
72 let client = reqwest::Client::builder()
74 .timeout(std::time::Duration::from_secs(30))
75 .build()
76 .map_err(|e| Error::RequestFailed {
77 reason: format!("Failed to create HTTP client: {e}"),
78 })?;
79
80 let mut headers = build_headers(spec, operation, matches)?;
82
83 if let Some(key) = idempotency_key {
85 headers.insert(
86 HeaderName::from_static("idempotency-key"),
87 HeaderValue::from_str(key).map_err(|_| Error::InvalidIdempotencyKey)?,
88 );
89 }
90
91 let method = Method::from_str(&operation.method).map_err(|_| Error::InvalidHttpMethod {
93 method: operation.method.clone(),
94 })?;
95
96 let headers_clone = headers.clone(); let mut request = client.request(method.clone(), &url).headers(headers);
98
99 let mut current_matches = matches;
102 while let Some((_name, sub_matches)) = current_matches.subcommand() {
103 current_matches = sub_matches;
104 }
105
106 let request_body = if operation.request_body.is_some() {
107 if let Some(body_value) = current_matches.get_one::<String>("body") {
108 let json_body: Value =
109 serde_json::from_str(body_value).map_err(|e| Error::InvalidJsonBody {
110 reason: e.to_string(),
111 })?;
112 request = request.json(&json_body);
113 Some(body_value.clone())
114 } else {
115 None
116 }
117 } else {
118 None
119 };
120
121 let cache_key = if let Some(cache_cfg) = cache_config {
123 if cache_cfg.enabled {
124 let header_map: HashMap<String, String> = headers_clone
126 .iter()
127 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
128 .collect();
129
130 let cache_key = CacheKey::from_request(
131 &spec.name,
132 &operation.operation_id,
133 method.as_ref(),
134 &url,
135 &header_map,
136 request_body.as_deref(),
137 )?;
138
139 let response_cache = ResponseCache::new(cache_cfg.clone())?;
140
141 if let Some(cached_response) = response_cache.get(&cache_key).await? {
143 let output = print_formatted_response(
145 &cached_response.body,
146 output_format,
147 jq_filter,
148 capture_output,
149 )?;
150 return Ok(output);
151 }
152
153 Some((cache_key, response_cache))
154 } else {
155 None
156 }
157 } else {
158 None
159 };
160
161 if dry_run {
163 let dry_run_info = serde_json::json!({
164 "dry_run": true,
165 "method": operation.method,
166 "url": url,
167 "headers": headers_clone.iter().map(|(k, v)| (k.as_str(), v.to_str().unwrap_or("<binary>"))).collect::<std::collections::HashMap<_, _>>(),
168 "operation_id": operation.operation_id
169 });
170 let dry_run_output =
171 serde_json::to_string_pretty(&dry_run_info).map_err(|e| Error::SerializationError {
172 reason: format!("Failed to serialize dry-run info: {e}"),
173 })?;
174
175 if capture_output {
176 return Ok(Some(dry_run_output));
177 }
178 println!("{dry_run_output}");
179 return Ok(None);
180 }
181
182 let response = request.send().await.map_err(|e| Error::RequestFailed {
184 reason: e.to_string(),
185 })?;
186
187 let status = response.status();
188 let response_headers: HashMap<String, String> = response
189 .headers()
190 .iter()
191 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
192 .collect();
193
194 let response_text = response
195 .text()
196 .await
197 .map_err(|e| Error::ResponseReadError {
198 reason: e.to_string(),
199 })?;
200
201 if !status.is_success() {
203 let api_name = spec.name.clone();
205 let operation_id = Some(operation.operation_id.clone());
206 let security_schemes: Vec<String> = operation
207 .security_requirements
208 .iter()
209 .filter_map(|scheme_name| {
210 spec.security_schemes
211 .get(scheme_name)
212 .and_then(|scheme| scheme.aperture_secret.as_ref())
213 .map(|aperture_secret| aperture_secret.name.clone())
214 })
215 .collect();
216
217 return Err(Error::HttpErrorWithContext {
218 status: status.as_u16(),
219 body: if response_text.is_empty() {
220 "(empty response)".to_string()
221 } else {
222 response_text
223 },
224 api_name,
225 operation_id,
226 security_schemes,
227 });
228 }
229
230 if let Some((cache_key, response_cache)) = cache_key {
232 let cached_request_info = CachedRequestInfo {
234 method: method.to_string(),
235 url: url.clone(),
236 headers: headers_clone
237 .iter()
238 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
239 .collect(),
240 body_hash: request_body.as_ref().map(|body| {
241 use sha2::{Digest, Sha256};
242 let mut hasher = Sha256::new();
243 hasher.update(body.as_bytes());
244 format!("{:x}", hasher.finalize())
245 }),
246 };
247
248 let cache_ttl = cache_config.and_then(|cfg| {
250 if cfg.default_ttl.as_secs() > 0 {
251 Some(cfg.default_ttl)
252 } else {
253 None
254 }
255 });
256
257 let _ = response_cache
258 .store(
259 &cache_key,
260 &response_text,
261 status.as_u16(),
262 &response_headers,
263 cached_request_info,
264 cache_ttl,
265 )
266 .await;
267 }
268
269 if response_text.is_empty() {
271 Ok(None)
272 } else {
273 print_formatted_response(&response_text, output_format, jq_filter, capture_output)
274 }
275}
276
277fn find_operation<'a>(
279 spec: &'a CachedSpec,
280 matches: &ArgMatches,
281) -> Result<&'a CachedCommand, Error> {
282 let mut current_matches = matches;
284 let mut subcommand_path = Vec::new();
285
286 while let Some((name, sub_matches)) = current_matches.subcommand() {
287 subcommand_path.push(name);
288 current_matches = sub_matches;
289 }
290
291 if let Some(operation_name) = subcommand_path.last() {
294 for command in &spec.commands {
295 let kebab_id = to_kebab_case(&command.operation_id);
297 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
298 return Ok(command);
299 }
300 }
301 }
302
303 Err(Error::OperationNotFound)
304}
305
306fn build_url(
308 base_url: &str,
309 path_template: &str,
310 operation: &CachedCommand,
311 matches: &ArgMatches,
312) -> Result<String, Error> {
313 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
314
315 let mut current_matches = matches;
317 while let Some((_name, sub_matches)) = current_matches.subcommand() {
318 current_matches = sub_matches;
319 }
320
321 let mut start = 0;
324 while let Some(open) = url[start..].find('{') {
325 let open_pos = start + open;
326 if let Some(close) = url[open_pos..].find('}') {
327 let close_pos = open_pos + close;
328 let param_name = &url[open_pos + 1..close_pos];
329
330 if let Some(value) = current_matches.get_one::<String>(param_name) {
331 url.replace_range(open_pos..=close_pos, value);
332 start = open_pos + value.len();
333 } else {
334 return Err(Error::MissingPathParameter {
335 name: param_name.to_string(),
336 });
337 }
338 } else {
339 break;
340 }
341 }
342
343 let mut query_params = Vec::new();
345 for arg in current_matches.ids() {
346 let arg_str = arg.as_str();
347 let is_query_param = operation
349 .parameters
350 .iter()
351 .any(|p| p.name == arg_str && p.location == "query");
352 if is_query_param {
353 if let Some(value) = current_matches.get_one::<String>(arg_str) {
354 query_params.push(format!("{}={}", arg_str, urlencoding::encode(value)));
355 }
356 }
357 }
358
359 if !query_params.is_empty() {
360 url.push('?');
361 url.push_str(&query_params.join("&"));
362 }
363
364 Ok(url)
365}
366
367fn build_headers(
369 spec: &CachedSpec,
370 operation: &CachedCommand,
371 matches: &ArgMatches,
372) -> Result<HeaderMap, Error> {
373 let mut headers = HeaderMap::new();
374
375 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
377 headers.insert("Accept", HeaderValue::from_static("application/json"));
378
379 let mut current_matches = matches;
381 while let Some((_name, sub_matches)) = current_matches.subcommand() {
382 current_matches = sub_matches;
383 }
384
385 for param in &operation.parameters {
387 if param.location == "header" {
388 if let Some(value) = current_matches.get_one::<String>(¶m.name) {
389 let header_name =
390 HeaderName::from_str(¶m.name).map_err(|e| Error::InvalidHeaderName {
391 name: param.name.clone(),
392 reason: e.to_string(),
393 })?;
394 let header_value =
395 HeaderValue::from_str(value).map_err(|e| Error::InvalidHeaderValue {
396 name: param.name.clone(),
397 reason: e.to_string(),
398 })?;
399 headers.insert(header_name, header_value);
400 }
401 }
402 }
403
404 for security_scheme_name in &operation.security_requirements {
406 if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
407 add_authentication_header(&mut headers, security_scheme)?;
408 }
409 }
410
411 if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
414 for header_str in custom_headers {
415 let (name, value) = parse_custom_header(header_str)?;
416 let header_name =
417 HeaderName::from_str(&name).map_err(|e| Error::InvalidHeaderName {
418 name: name.clone(),
419 reason: e.to_string(),
420 })?;
421 let header_value =
422 HeaderValue::from_str(&value).map_err(|e| Error::InvalidHeaderValue {
423 name: name.clone(),
424 reason: e.to_string(),
425 })?;
426 headers.insert(header_name, header_value);
427 }
428 }
429
430 Ok(headers)
431}
432
433fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
435 let colon_pos = header_str
437 .find(':')
438 .ok_or_else(|| Error::InvalidHeaderFormat {
439 header: header_str.to_string(),
440 })?;
441
442 let name = header_str[..colon_pos].trim();
443 let value = header_str[colon_pos + 1..].trim();
444
445 if name.is_empty() {
446 return Err(Error::EmptyHeaderName);
447 }
448
449 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
451 let var_name = &value[2..value.len() - 1];
453 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
454 } else {
455 value.to_string()
456 };
457
458 Ok((name.to_string(), expanded_value))
459}
460
461fn add_authentication_header(
463 headers: &mut HeaderMap,
464 security_scheme: &CachedSecurityScheme,
465) -> Result<(), Error> {
466 if let Some(aperture_secret) = &security_scheme.aperture_secret {
468 let secret_value =
470 std::env::var(&aperture_secret.name).map_err(|_| Error::SecretNotSet {
471 scheme_name: security_scheme.name.clone(),
472 env_var: aperture_secret.name.clone(),
473 })?;
474
475 match security_scheme.scheme_type.as_str() {
477 "apiKey" => {
478 if let (Some(location), Some(param_name)) =
479 (&security_scheme.location, &security_scheme.parameter_name)
480 {
481 if location == "header" {
482 let header_name = HeaderName::from_str(param_name).map_err(|e| {
483 Error::InvalidHeaderName {
484 name: param_name.clone(),
485 reason: e.to_string(),
486 }
487 })?;
488 let header_value = HeaderValue::from_str(&secret_value).map_err(|e| {
489 Error::InvalidHeaderValue {
490 name: param_name.clone(),
491 reason: e.to_string(),
492 }
493 })?;
494 headers.insert(header_name, header_value);
495 }
496 }
498 }
499 "http" => {
500 if let Some(scheme) = &security_scheme.scheme {
501 match scheme.as_str() {
502 "bearer" => {
503 let auth_value = format!("Bearer {secret_value}");
504 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
505 Error::InvalidHeaderValue {
506 name: "Authorization".to_string(),
507 reason: e.to_string(),
508 }
509 })?;
510 headers.insert("Authorization", header_value);
511 }
512 "basic" => {
513 let auth_value = format!("Basic {secret_value}");
515 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
516 Error::InvalidHeaderValue {
517 name: "Authorization".to_string(),
518 reason: e.to_string(),
519 }
520 })?;
521 headers.insert("Authorization", header_value);
522 }
523 _ => {
524 return Err(Error::UnsupportedAuthScheme {
525 scheme: scheme.clone(),
526 });
527 }
528 }
529 }
530 }
531 _ => {
532 return Err(Error::UnsupportedSecurityScheme {
533 scheme_type: security_scheme.scheme_type.clone(),
534 });
535 }
536 }
537 }
538
539 Ok(())
540}
541
542fn to_kebab_case(s: &str) -> String {
544 let mut result = String::new();
545 let mut prev_lowercase = false;
546
547 for (i, ch) in s.chars().enumerate() {
548 if ch.is_uppercase() && i > 0 && prev_lowercase {
549 result.push('-');
550 }
551 result.push(ch.to_ascii_lowercase());
552 prev_lowercase = ch.is_lowercase();
553 }
554
555 result
556}
557
558fn print_formatted_response(
560 response_text: &str,
561 output_format: &OutputFormat,
562 jq_filter: Option<&str>,
563 capture_output: bool,
564) -> Result<Option<String>, Error> {
565 let processed_text = if let Some(filter) = jq_filter {
567 apply_jq_filter(response_text, filter)?
568 } else {
569 response_text.to_string()
570 };
571
572 match output_format {
573 OutputFormat::Json => {
574 let output = serde_json::from_str::<Value>(&processed_text)
576 .ok()
577 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
578 .unwrap_or_else(|| processed_text.clone());
579
580 if capture_output {
581 return Ok(Some(output));
582 }
583 println!("{output}");
584 }
585 OutputFormat::Yaml => {
586 let output = serde_json::from_str::<Value>(&processed_text)
588 .ok()
589 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
590 .unwrap_or_else(|| processed_text.clone());
591
592 if capture_output {
593 return Ok(Some(output));
594 }
595 println!("{output}");
596 }
597 OutputFormat::Table => {
598 if let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) {
600 let table_output = print_as_table(&json_value, capture_output)?;
601 if capture_output {
602 return Ok(table_output);
603 }
604 } else {
605 if capture_output {
607 return Ok(Some(processed_text));
608 }
609 println!("{processed_text}");
610 }
611 }
612 }
613
614 Ok(None)
615}
616
617#[derive(tabled::Tabled)]
619struct TableRow {
620 #[tabled(rename = "Key")]
621 key: String,
622 #[tabled(rename = "Value")]
623 value: String,
624}
625
626#[derive(tabled::Tabled)]
627struct KeyValue {
628 #[tabled(rename = "Key")]
629 key: String,
630 #[tabled(rename = "Value")]
631 value: String,
632}
633
634#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
636fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
637 use std::collections::BTreeMap;
638 use tabled::Table;
639
640 match json_value {
641 Value::Array(items) => {
642 if items.is_empty() {
643 if capture_output {
644 return Ok(Some("(empty array)".to_string()));
645 }
646 println!("(empty array)");
647 return Ok(None);
648 }
649
650 if items.len() > MAX_TABLE_ROWS {
652 let msg1 = format!(
653 "Array too large: {} items (max {} for table display)",
654 items.len(),
655 MAX_TABLE_ROWS
656 );
657 let msg2 = "Use --format json or --jq to process the full data";
658
659 if capture_output {
660 return Ok(Some(format!("{msg1}\n{msg2}")));
661 }
662 println!("{msg1}");
663 println!("{msg2}");
664 return Ok(None);
665 }
666
667 if let Some(Value::Object(_)) = items.first() {
669 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
671
672 for item in items {
673 if let Value::Object(obj) = item {
674 let mut row = BTreeMap::new();
675 for (key, value) in obj {
676 row.insert(key.clone(), format_value_for_table(value));
677 }
678 table_data.push(row);
679 }
680 }
681
682 if !table_data.is_empty() {
683 let mut rows = Vec::new();
686 for (i, row) in table_data.iter().enumerate() {
687 if i > 0 {
688 rows.push(TableRow {
689 key: "---".to_string(),
690 value: "---".to_string(),
691 });
692 }
693 for (key, value) in row {
694 rows.push(TableRow {
695 key: key.clone(),
696 value: value.clone(),
697 });
698 }
699 }
700
701 let table = Table::new(&rows);
702 if capture_output {
703 return Ok(Some(table.to_string()));
704 }
705 println!("{table}");
706 return Ok(None);
707 }
708 }
709
710 if capture_output {
712 let mut output = String::new();
713 for (i, item) in items.iter().enumerate() {
714 use std::fmt::Write;
715 writeln!(&mut output, "{}: {}", i, format_value_for_table(item)).unwrap();
716 }
717 return Ok(Some(output.trim_end().to_string()));
718 }
719 for (i, item) in items.iter().enumerate() {
720 println!("{}: {}", i, format_value_for_table(item));
721 }
722 }
723 Value::Object(obj) => {
724 if obj.len() > MAX_TABLE_ROWS {
726 let msg1 = format!(
727 "Object too large: {} fields (max {} for table display)",
728 obj.len(),
729 MAX_TABLE_ROWS
730 );
731 let msg2 = "Use --format json or --jq to process the full data";
732
733 if capture_output {
734 return Ok(Some(format!("{msg1}\n{msg2}")));
735 }
736 println!("{msg1}");
737 println!("{msg2}");
738 return Ok(None);
739 }
740
741 let rows: Vec<KeyValue> = obj
743 .iter()
744 .map(|(key, value)| KeyValue {
745 key: key.clone(),
746 value: format_value_for_table(value),
747 })
748 .collect();
749
750 let table = Table::new(&rows);
751 if capture_output {
752 return Ok(Some(table.to_string()));
753 }
754 println!("{table}");
755 }
756 _ => {
757 let formatted = format_value_for_table(json_value);
759 if capture_output {
760 return Ok(Some(formatted));
761 }
762 println!("{formatted}");
763 }
764 }
765
766 Ok(None)
767}
768
769fn format_value_for_table(value: &Value) -> String {
771 match value {
772 Value::Null => "null".to_string(),
773 Value::Bool(b) => b.to_string(),
774 Value::Number(n) => n.to_string(),
775 Value::String(s) => s.clone(),
776 Value::Array(arr) => {
777 if arr.len() <= 3 {
778 format!(
779 "[{}]",
780 arr.iter()
781 .map(format_value_for_table)
782 .collect::<Vec<_>>()
783 .join(", ")
784 )
785 } else {
786 format!("[{} items]", arr.len())
787 }
788 }
789 Value::Object(obj) => {
790 if obj.len() <= 2 {
791 format!(
792 "{{{}}}",
793 obj.iter()
794 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
795 .collect::<Vec<_>>()
796 .join(", ")
797 )
798 } else {
799 format!("{{object with {} fields}}", obj.len())
800 }
801 }
802 }
803}
804
805pub fn apply_jq_filter(response_text: &str, filter: &str) -> Result<String, Error> {
814 let json_value: Value =
816 serde_json::from_str(response_text).map_err(|e| Error::JqFilterError {
817 reason: format!("Response is not valid JSON: {e}"),
818 })?;
819
820 #[cfg(feature = "jq")]
821 {
822 use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
824 use jaq_parse::parse;
825 use jaq_std::std;
826
827 let (expr, errs) = parse(filter, jaq_parse::main());
829 if !errs.is_empty() {
830 return Err(Error::JqFilterError {
831 reason: format!("Parse error in jq expression: {}", errs[0]),
832 });
833 }
834
835 let mut ctx = ParseCtx::new(Vec::new());
837 ctx.insert_defs(std());
838 let filter = ctx.compile(expr.unwrap());
839
840 let jaq_value = serde_json_to_jaq_val(&json_value);
842
843 let inputs = RcIter::new(core::iter::empty());
845 let ctx = Ctx::new([], &inputs);
846 let results: Result<Vec<Val>, _> = filter.run((ctx, jaq_value.into())).collect();
847
848 match results {
849 Ok(vals) => {
850 if vals.is_empty() {
851 Ok("null".to_string())
852 } else if vals.len() == 1 {
853 let json_val = jaq_val_to_serde_json(&vals[0]);
855 serde_json::to_string_pretty(&json_val).map_err(|e| Error::JqFilterError {
856 reason: format!("Failed to serialize result: {e}"),
857 })
858 } else {
859 let json_vals: Vec<Value> = vals.iter().map(jaq_val_to_serde_json).collect();
861 let array = Value::Array(json_vals);
862 serde_json::to_string_pretty(&array).map_err(|e| Error::JqFilterError {
863 reason: format!("Failed to serialize results: {e}"),
864 })
865 }
866 }
867 Err(e) => Err(Error::JqFilterError {
868 reason: format!("Filter execution error: {e}"),
869 }),
870 }
871 }
872
873 #[cfg(not(feature = "jq"))]
874 {
875 apply_basic_jq_filter(&json_value, filter)
877 }
878}
879
880#[cfg(not(feature = "jq"))]
881fn apply_basic_jq_filter(json_value: &Value, filter: &str) -> Result<String, Error> {
883 let uses_advanced_features = filter.contains('[')
885 || filter.contains(']')
886 || filter.contains('|')
887 || filter.contains('(')
888 || filter.contains(')')
889 || filter.contains("select")
890 || filter.contains("map")
891 || filter.contains("length");
892
893 if uses_advanced_features {
894 eprintln!("Warning: Advanced JQ features require building with --features jq");
895 eprintln!(" Currently only basic field access is supported (e.g., '.field', '.nested.field')");
896 eprintln!(" To enable full JQ support: cargo install aperture-cli --features jq");
897 }
898
899 let result = match filter {
900 "." => json_value.clone(),
901 ".[]" => {
902 match json_value {
904 Value::Array(arr) => {
905 Value::Array(arr.clone())
907 }
908 Value::Object(obj) => {
909 Value::Array(obj.values().cloned().collect())
911 }
912 _ => Value::Null,
913 }
914 }
915 ".length" => {
916 match json_value {
918 Value::Array(arr) => Value::Number(arr.len().into()),
919 Value::Object(obj) => Value::Number(obj.len().into()),
920 Value::String(s) => Value::Number(s.len().into()),
921 _ => Value::Null,
922 }
923 }
924 filter if filter.starts_with(".[].") => {
925 let field_path = &filter[4..]; match json_value {
928 Value::Array(arr) => {
929 let mapped: Vec<Value> = arr
930 .iter()
931 .map(|item| get_nested_field(item, field_path))
932 .collect();
933 Value::Array(mapped)
934 }
935 _ => Value::Null,
936 }
937 }
938 filter if filter.starts_with('.') => {
939 let field_path = &filter[1..]; get_nested_field(json_value, field_path)
942 }
943 _ => {
944 return Err(Error::JqFilterError {
945 reason: format!("Unsupported JQ filter: '{filter}'. Only basic field access like '.name' or '.metadata.role' is supported without the full jq library."),
946 });
947 }
948 };
949
950 serde_json::to_string_pretty(&result).map_err(|e| Error::JqFilterError {
951 reason: format!("Failed to serialize filtered result: {e}"),
952 })
953}
954
955#[cfg(not(feature = "jq"))]
956fn get_nested_field(json_value: &Value, field_path: &str) -> Value {
958 let parts: Vec<&str> = field_path.split('.').collect();
959 let mut current = json_value;
960
961 for part in parts {
962 if part.is_empty() {
963 continue;
964 }
965
966 if part.starts_with('[') && part.ends_with(']') {
968 let index_str = &part[1..part.len() - 1];
969 if let Ok(index) = index_str.parse::<usize>() {
970 match current {
971 Value::Array(arr) => {
972 if let Some(item) = arr.get(index) {
973 current = item;
974 } else {
975 return Value::Null;
976 }
977 }
978 _ => return Value::Null,
979 }
980 } else {
981 return Value::Null;
982 }
983 continue;
984 }
985
986 match current {
987 Value::Object(obj) => {
988 if let Some(field) = obj.get(part) {
989 current = field;
990 } else {
991 return Value::Null;
992 }
993 }
994 Value::Array(arr) => {
995 if let Ok(index) = part.parse::<usize>() {
997 if let Some(item) = arr.get(index) {
998 current = item;
999 } else {
1000 return Value::Null;
1001 }
1002 } else {
1003 return Value::Null;
1004 }
1005 }
1006 _ => return Value::Null,
1007 }
1008 }
1009
1010 current.clone()
1011}
1012
1013#[cfg(feature = "jq")]
1014fn serde_json_to_jaq_val(value: &Value) -> jaq_interpret::Val {
1016 use jaq_interpret::Val;
1017 use std::rc::Rc;
1018
1019 match value {
1020 Value::Null => Val::Null,
1021 Value::Bool(b) => Val::Bool(*b),
1022 Value::Number(n) => {
1023 if let Some(i) = n.as_i64() {
1024 if let Ok(isize_val) = isize::try_from(i) {
1026 Val::Int(isize_val)
1027 } else {
1028 Val::Float(i as f64)
1030 }
1031 } else if let Some(f) = n.as_f64() {
1032 Val::Float(f)
1033 } else {
1034 Val::Null
1035 }
1036 }
1037 Value::String(s) => Val::Str(s.clone().into()),
1038 Value::Array(arr) => {
1039 let jaq_arr: Vec<Val> = arr.iter().map(serde_json_to_jaq_val).collect();
1040 Val::Arr(Rc::new(jaq_arr))
1041 }
1042 Value::Object(obj) => {
1043 let mut jaq_obj = indexmap::IndexMap::with_hasher(ahash::RandomState::new());
1044 for (k, v) in obj {
1045 jaq_obj.insert(Rc::new(k.clone()), serde_json_to_jaq_val(v));
1046 }
1047 Val::Obj(Rc::new(jaq_obj))
1048 }
1049 }
1050}
1051
1052#[cfg(feature = "jq")]
1053fn jaq_val_to_serde_json(val: &jaq_interpret::Val) -> Value {
1055 use jaq_interpret::Val;
1056
1057 match val {
1058 Val::Null => Value::Null,
1059 Val::Bool(b) => Value::Bool(*b),
1060 Val::Int(i) => {
1061 Value::Number((*i as i64).into())
1063 }
1064 Val::Float(f) => {
1065 if let Some(num) = serde_json::Number::from_f64(*f) {
1066 Value::Number(num)
1067 } else {
1068 Value::Null
1069 }
1070 }
1071 Val::Str(s) => Value::String(s.to_string()),
1072 Val::Arr(arr) => {
1073 let json_arr: Vec<Value> = arr.iter().map(jaq_val_to_serde_json).collect();
1074 Value::Array(json_arr)
1075 }
1076 Val::Obj(obj) => {
1077 let mut json_obj = serde_json::Map::new();
1078 for (k, v) in obj.iter() {
1079 json_obj.insert(k.to_string(), jaq_val_to_serde_json(v));
1080 }
1081 Value::Object(json_obj)
1082 }
1083 _ => Value::Null, }
1085}