1use crate::filter::Filter;
2use crate::jsonpath;
3use crate::model::{Capture, Entry};
4use crate::opaque::is_opaque;
5use crate::redact::REDACTED;
6use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Target {
10 Req,
11 Resp,
12}
13
14#[derive(Debug, Serialize)]
15pub struct ExtractResult {
16 pub values: Vec<ExtractValue>,
17}
18
19#[derive(Debug, Serialize)]
20pub struct ExtractValue {
21 pub id: String,
22 pub value: String,
23}
24
25fn body_of(e: &Entry, target: Target) -> Option<&str> {
26 let b = match target {
27 Target::Req => &e.req_body,
28 Target::Resp => &e.resp_body,
29 };
30 b.as_deref().filter(|s| !s.is_empty())
31}
32
33fn stringify(v: &serde_json::Value) -> String {
34 match v {
35 serde_json::Value::String(s) => s.clone(),
36 other => other.to_string(),
37 }
38}
39
40pub fn compute_extract(
42 cap: &Capture,
43 filter: &Filter,
44 path: &str,
45 target: Target,
46 top: usize,
47 unsafe_include: bool,
48) -> ExtractResult {
49 let mut values = Vec::new();
50 for e in cap.entries.iter().filter(|e| filter.matches(e)) {
51 let Some(body) = body_of(e, target) else {
52 continue;
53 };
54 let Ok(json) = serde_json::from_str::<serde_json::Value>(body) else {
55 continue;
56 };
57 for v in jsonpath::eval(&json, path) {
58 let s = stringify(&v);
59 let shown = if !unsafe_include && is_opaque(&s) {
60 REDACTED.to_string()
61 } else {
62 s
63 };
64 values.push(ExtractValue {
65 id: e.id.clone(),
66 value: shown,
67 });
68 if values.len() >= top {
69 return ExtractResult { values };
70 }
71 }
72 }
73 ExtractResult { values }
74}
75
76pub fn render_extract_text(r: &ExtractResult) -> String {
78 let mut out = String::new();
79 out.push_str("== wiretrail extract ==\n");
80 for v in &r.values {
81 out.push_str(&format!("{} {}\n", v.id, v.value));
82 }
83 out
84}
85
86#[cfg(test)]
87mod tests {
88 use super::{Target, compute_extract};
89 use crate::filter::Filter;
90 use crate::model::{Entry, sample_capture, sample_entry};
91
92 fn with_resp(index: usize, body: &str) -> Entry {
93 let mut e = sample_entry(index, "api.x", "GET", "/a", 200);
94 e.resp_body = Some(body.to_string());
95 e
96 }
97
98 #[test]
99 fn extracts_field_from_response_bodies() {
100 let cap = sample_capture(vec![
101 with_resp(0, r#"{"error":{"message":"boom"}}"#),
102 with_resp(1, r#"{"error":{"message":"nope"}}"#),
103 ]);
104 let r = compute_extract(
105 &cap,
106 &Filter::parse(&[]).unwrap(),
107 "$.error.message",
108 Target::Resp,
109 10,
110 false,
111 );
112 let vals: Vec<&str> = r.values.iter().map(|v| v.value.as_str()).collect();
113 assert!(vals.contains(&"boom"));
114 assert!(vals.contains(&"nope"));
115 }
116
117 #[test]
118 fn masks_opaque_value_by_default() {
119 let cap = sample_capture(vec![with_resp(
120 0,
121 r#"{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}"#,
122 )]);
123 let r = compute_extract(
124 &cap,
125 &Filter::parse(&[]).unwrap(),
126 "$.token",
127 Target::Resp,
128 10,
129 false,
130 );
131 assert_eq!(r.values[0].value, "<redacted>");
132 let r2 = compute_extract(
133 &cap,
134 &Filter::parse(&[]).unwrap(),
135 "$.token",
136 Target::Resp,
137 10,
138 true,
139 );
140 assert!(r2.values[0].value.starts_with("eyJ"));
141 }
142}