1use std::collections::HashMap;
5
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
13pub struct CaptureDefinition {
14 pub alias: String,
15 pub path: Vec<String>,
16}
17
18#[derive(Debug, Default)]
23pub struct CaptureState {
24 values: HashMap<String, String>,
25}
26
27impl CaptureState {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn clear(&mut self) {
33 self.values.clear();
34 }
35
36 pub fn insert(&mut self, key: String, value: String) {
37 self.values.insert(key, value);
38 }
39
40 pub fn get(&self, key: &str) -> Option<&str> {
41 self.values.get(key).map(|s| s.as_str())
42 }
43}
44
45pub fn parse_json_path(path: &str) -> Result<Vec<String>, String> {
52 let rest = path
53 .strip_prefix("$.")
54 .ok_or_else(|| format!("capture path must start with '$.' — got '{path}'"))?;
55
56 if rest.is_empty() {
57 return Err(format!("capture path is empty after '$.' — got '{path}'"));
58 }
59
60 let segments: Vec<String> = rest.split('.').map(|s| s.to_string()).collect();
61
62 if segments.iter().any(|s| s.is_empty()) {
63 return Err(format!(
64 "capture path contains empty segment — got '{path}'"
65 ));
66 }
67
68 Ok(segments)
69}
70
71pub fn value_to_string(value: &Value) -> Option<String> {
87 let raw = match value {
88 Value::String(s) => Some(s.clone()),
89 Value::Number(n) => Some(n.to_string()),
90 Value::Bool(b) => Some(b.to_string()),
91 Value::Null => return None,
92 other => serde_json::to_string(other).ok(),
93 };
94 raw.map(sanitize_captured_value)
95}
96
97fn sanitize_captured_value(s: String) -> String {
100 if s.bytes().any(|b| b == b'\r' || b == b'\n' || b == b'\0') {
101 s.replace(['\r', '\n', '\0'], "")
102 } else {
103 s
104 }
105}
106
107pub fn inject_captures(text: &str, state: &CaptureState) -> Result<String, String> {
114 let marker = "{{capture.";
115 let mut result = String::with_capacity(text.len());
116 let mut rest = text;
117
118 while let Some(start) = rest.find(marker) {
119 result.push_str(&rest[..start]);
120 let after_marker = &rest[start + marker.len()..];
121
122 let end = after_marker
123 .find("}}")
124 .ok_or_else(|| format!("unterminated capture placeholder in: {text}"))?;
125
126 let key = &after_marker[..end];
127
128 let value = state.get(key).ok_or_else(|| {
129 format!("capture key '{key}' not found in state — preceding step may have failed")
130 })?;
131
132 result.push_str(value);
133 rest = &after_marker[end + 2..];
134 }
135
136 result.push_str(rest);
137 Ok(result)
138}
139
140pub fn inject_captures_into_headers(
144 headers: &[(String, String)],
145 state: &CaptureState,
146) -> Result<Vec<(String, String)>, String> {
147 headers
148 .iter()
149 .map(|(name, value)| {
150 let injected = inject_captures(value, state)?;
151 Ok((name.clone(), injected))
152 })
153 .collect()
154}
155
156pub fn scan_capture_refs(text: &str) -> Result<Vec<String>, String> {
163 let marker = "{{capture.";
164 let mut refs = Vec::new();
165 let mut rest = text;
166
167 while let Some(start) = rest.find(marker) {
168 let after_marker = &rest[start + marker.len()..];
169 if let Some(end) = after_marker.find("}}") {
170 refs.push(after_marker[..end].to_string());
171 rest = &after_marker[end + 2..];
172 } else {
173 return Err(format!(
174 "unterminated capture placeholder: '{{{{capture.{}…'",
175 &after_marker[..after_marker.len().min(20)]
176 ));
177 }
178 }
179
180 Ok(refs)
181}
182
183#[cfg(test)]
186mod tests {
187 use super::*;
188 use serde_json::json;
189
190 #[test]
193 fn parse_json_path_simple() {
194 let path = parse_json_path("$.data.access_token").unwrap();
195 assert_eq!(path, vec!["data", "access_token"]);
196 }
197
198 #[test]
199 fn parse_json_path_single_segment() {
200 let path = parse_json_path("$.token").unwrap();
201 assert_eq!(path, vec!["token"]);
202 }
203
204 #[test]
205 fn parse_json_path_deep() {
206 let path = parse_json_path("$.a.b.c.d").unwrap();
207 assert_eq!(path, vec!["a", "b", "c", "d"]);
208 }
209
210 #[test]
211 fn parse_json_path_no_prefix() {
212 assert!(parse_json_path("data.token").is_err());
213 }
214
215 #[test]
216 fn parse_json_path_empty_after_prefix() {
217 assert!(parse_json_path("$.").is_err());
218 }
219
220 #[test]
221 fn parse_json_path_empty_segment() {
222 assert!(parse_json_path("$.a..b").is_err());
223 }
224
225 #[test]
228 fn value_to_string_string() {
229 assert_eq!(value_to_string(&json!("hello")), Some("hello".to_string()));
230 }
231
232 #[test]
233 fn value_to_string_number() {
234 assert_eq!(value_to_string(&json!(42)), Some("42".to_string()));
235 }
236
237 #[test]
238 fn value_to_string_float() {
239 assert_eq!(value_to_string(&json!(2.72)), Some("2.72".to_string()));
240 }
241
242 #[test]
243 fn value_to_string_bool() {
244 assert_eq!(value_to_string(&json!(true)), Some("true".to_string()));
245 }
246
247 #[test]
248 fn value_to_string_null() {
249 assert_eq!(value_to_string(&json!(null)), None);
250 }
251
252 #[test]
253 fn value_to_string_object() {
254 let val = json!({"a": 1});
255 let result = value_to_string(&val).unwrap();
256 assert!(result.contains("\"a\""));
257 assert!(result.contains("1"));
258 }
259
260 #[test]
261 fn value_to_string_array() {
262 let val = json!([1, 2, 3]);
263 let result = value_to_string(&val).unwrap();
264 assert_eq!(result, "[1,2,3]");
265 }
266
267 #[test]
270 fn inject_captures_single_replacement() {
271 let mut state = CaptureState::new();
272 state.insert("token".to_string(), "abc123".to_string());
273 let result = inject_captures("Bearer {{capture.token}}", &state).unwrap();
274 assert_eq!(result, "Bearer abc123");
275 }
276
277 #[test]
278 fn inject_captures_multiple_replacements() {
279 let mut state = CaptureState::new();
280 state.insert("token".to_string(), "tok".to_string());
281 state.insert("user_id".to_string(), "42".to_string());
282 let result = inject_captures("{{capture.token}} for {{capture.user_id}}", &state).unwrap();
283 assert_eq!(result, "tok for 42");
284 }
285
286 #[test]
287 fn inject_captures_no_placeholders() {
288 let state = CaptureState::new();
289 let result = inject_captures("no captures here", &state).unwrap();
290 assert_eq!(result, "no captures here");
291 }
292
293 #[test]
294 fn inject_captures_missing_key_returns_err() {
295 let state = CaptureState::new();
296 let result = inject_captures("{{capture.missing}}", &state);
297 assert!(result.is_err());
298 assert!(result.unwrap_err().contains("missing"));
299 }
300
301 #[test]
302 fn inject_captures_unterminated_returns_err() {
303 let state = CaptureState::new();
304 let result = inject_captures("{{capture.broken", &state);
305 assert!(result.is_err());
306 }
307
308 #[test]
311 fn inject_captures_into_headers_replaces_values() {
312 let mut state = CaptureState::new();
313 state.insert("token".to_string(), "secret".to_string());
314 let headers = vec![
315 (
316 "Authorization".to_string(),
317 "Bearer {{capture.token}}".to_string(),
318 ),
319 ("X-Static".to_string(), "no-capture".to_string()),
320 ];
321 let result = inject_captures_into_headers(&headers, &state).unwrap();
322 assert_eq!(result[0].1, "Bearer secret");
323 assert_eq!(result[1].1, "no-capture");
324 }
325
326 #[test]
327 fn inject_captures_into_headers_missing_key_returns_err() {
328 let state = CaptureState::new();
329 let headers = vec![("Auth".to_string(), "{{capture.nope}}".to_string())];
330 assert!(inject_captures_into_headers(&headers, &state).is_err());
331 }
332
333 #[test]
336 fn scan_capture_refs_extracts_keys() {
337 let refs = scan_capture_refs("{{capture.token}} and {{capture.user_id}}").unwrap();
338 assert_eq!(refs, vec!["token", "user_id"]);
339 }
340
341 #[test]
342 fn scan_capture_refs_no_captures() {
343 let refs = scan_capture_refs("no captures here").unwrap();
344 assert!(refs.is_empty());
345 }
346
347 #[test]
348 fn scan_capture_refs_unterminated_is_error() {
349 let err = scan_capture_refs("{{capture.ok}} then {{capture.broken").unwrap_err();
350 assert!(err.contains("unterminated"), "{err}");
351 }
352
353 #[test]
358 fn value_to_string_strips_crlf() {
359 let val = json!("evil\r\nX-Injected: true");
360 assert_eq!(
361 value_to_string(&val),
362 Some("evilX-Injected: true".to_string())
363 );
364 }
365
366 #[test]
367 fn value_to_string_strips_null_byte() {
368 let val = json!("hello\0world");
369 assert_eq!(value_to_string(&val), Some("helloworld".to_string()));
370 }
371
372 #[test]
373 fn value_to_string_clean_string_unchanged() {
374 let val = json!("clean-value");
375 assert_eq!(value_to_string(&val), Some("clean-value".to_string()));
376 }
377
378 #[test]
381 fn capture_state_clear() {
382 let mut state = CaptureState::new();
383 state.insert("a".to_string(), "1".to_string());
384 assert!(state.get("a").is_some());
385 state.clear();
386 assert!(state.get("a").is_none());
387 }
388}