use serde_json::Value; pub fn redact_paths(value: &mut Value, paths: &[String]) { for path in paths { let segments = parse_path(path); if segments.is_empty() { continue; } redact_path(value, &segments); } } pub fn truncate_item_fields(value: &mut Value, max_len: usize) { match value { Value::String(text) => { if text != "[REDACTED]" && text.chars().count() > max_len { let truncated = text.chars().take(max_len).collect::(); *text = format!("{truncated}..."); } } Value::Array(items) => { for item in items { truncate_item_fields(item, max_len); } } Value::Object(map) => { for value in map.values_mut() { truncate_item_fields(value, max_len); } } Value::Null | Value::Bool(_) | Value::Number(_) => {} } } #[derive(Clone, Debug, PartialEq, Eq)] enum PathSegment { Field(String), Wildcard, } fn parse_path(path: &str) -> Vec { let path = path.strip_prefix("$.").or_else(|| path.strip_prefix('$')); let Some(path) = path else { return Vec::new(); }; path.split('.') .flat_map(|segment| { if let Some(field) = segment.strip_suffix("[*]") { vec![PathSegment::Field(field.to_owned()), PathSegment::Wildcard] } else if segment == "*" { vec![PathSegment::Wildcard] } else { vec![PathSegment::Field(segment.to_owned())] } }) .collect() } fn redact_path(value: &mut Value, segments: &[PathSegment]) { let Some((current, rest)) = segments.split_first() else { *value = Value::String("[REDACTED]".to_owned()); return; }; match current { PathSegment::Field(field) => { if let Some(next) = value .as_object_mut() .and_then(|object| object.get_mut(field)) { redact_path(next, rest); } } PathSegment::Wildcard => { if let Some(items) = value.as_array_mut() { for item in items { redact_path(item, rest); } } } } } #[cfg(test)] mod tests { use serde_json::json; use super::{redact_paths, truncate_item_fields}; #[test] fn redacts_simple_and_wildcard_paths() { let mut value = json!({ "summary": { "token": "secret" }, "items": [ { "token": "a", "message": "keep" }, { "token": "b", "message": "keep" } ] }); redact_paths( &mut value, &["$.summary.token".to_owned(), "$.items[*].token".to_owned()], ); assert_eq!(value["summary"]["token"], json!("[REDACTED]")); assert_eq!(value["items"][0]["token"], json!("[REDACTED]")); assert_eq!(value["items"][1]["token"], json!("[REDACTED]")); } #[test] fn truncates_long_strings_recursively() { let mut value = json!({ "message": "abcdefghijklmnop", "nested": [{ "message": "qrstuvwxyz" }] }); truncate_item_fields(&mut value, 4); assert_eq!(value["message"], json!("abcd...")); assert_eq!(value["nested"][0]["message"], json!("qrst...")); } }