clawspec_core/client/response/redaction/redactor.rs
1//! Redactor trait and implementations for flexible value transformation.
2//!
3//! This module provides the [`Redactor`] trait which allows different types to be used
4//! for redaction operations. The trait is implemented for:
5//!
6//! - Static values (`&str`, `String`, `serde_json::Value`)
7//! - Functions `Fn(&str, &Value) -> Value`
8//!
9//! # Examples
10//!
11//! ```ignore
12//! // Static value
13//! .redact("/id", "stable-uuid")?
14//!
15//! // Function transformation (path available, ignore if not needed)
16//! .redact("$..timestamp", |_path, _val| {
17//! serde_json::json!("2024-01-01T00:00:00Z")
18//! })?
19//!
20//! // Path-aware transformation
21//! .redact("$.items[*].id", |path, _val| {
22//! let idx = path.split('/').nth(2).unwrap_or("0");
23//! serde_json::json!(format!("id-{idx}"))
24//! })?
25//! ```
26
27use serde_json::Value;
28
29/// Trait for types that can be used to redact values.
30///
31/// This trait defines how a redactor transforms a value at a given path.
32/// Implementations receive both the concrete JSON Pointer path and the current
33/// value, allowing for path-aware or value-based transformations.
34///
35/// # Implementations
36///
37/// - `&str` - Replace with a static string
38/// - `String` - Replace with a string
39/// - `serde_json::Value` - Replace with a JSON value
40/// - `Fn(&str, &Value) -> Value` - Transform using a function
41///
42/// # Examples
43///
44/// ```ignore
45/// // Static replacement
46/// .redact("/id", "stable-uuid")?
47///
48/// // Value-based transformation (ignore path)
49/// .redact("$..notes", |_path, val| {
50/// if val.as_str().map(|s| s.len() > 50).unwrap_or(false) {
51/// serde_json::json!("[REDACTED]")
52/// } else {
53/// val.clone()
54/// }
55/// })?
56///
57/// // Path-aware transformation
58/// .redact("$.items[*].id", |path, _val| {
59/// let idx = path.split('/').nth(2).unwrap_or("0");
60/// serde_json::json!(format!("stable-id-{idx}"))
61/// })?
62/// ```
63#[cfg_attr(docsrs, doc(cfg(feature = "redaction")))]
64pub trait Redactor {
65 /// Apply the redaction to a value at the given path.
66 ///
67 /// # Arguments
68 ///
69 /// * `path` - The concrete JSON Pointer path (e.g., `/items/0/id`)
70 /// * `current` - The current value at that path
71 ///
72 /// # Returns
73 ///
74 /// The new value to replace the current one.
75 fn apply(&self, path: &str, current: &Value) -> Value;
76}
77
78// Implementation for serde_json::Value (direct replacement)
79impl Redactor for Value {
80 fn apply(&self, _path: &str, _current: &Value) -> Value {
81 self.clone()
82 }
83}
84
85// Implementation for &str
86impl Redactor for &str {
87 fn apply(&self, _path: &str, _current: &Value) -> Value {
88 Value::String((*self).to_string())
89 }
90}
91
92// Implementation for String
93impl Redactor for String {
94 fn apply(&self, _path: &str, _current: &Value) -> Value {
95 Value::String(self.clone())
96 }
97}
98
99// Implementation for Fn(&str, &Value) -> Value
100impl<F> Redactor for F
101where
102 F: Fn(&str, &Value) -> Value,
103{
104 fn apply(&self, path: &str, current: &Value) -> Value {
105 self(path, current)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use serde_json::json;
113
114 #[test]
115 fn should_redact_with_static_str() {
116 let redactor = "replacement";
117 let result = redactor.apply("/id", &json!("original"));
118 assert_eq!(result, json!("replacement"));
119 }
120
121 #[test]
122 fn should_redact_with_static_string() {
123 let redactor = String::from("replacement");
124 let result = redactor.apply("/id", &json!("original"));
125 assert_eq!(result, json!("replacement"));
126 }
127
128 #[test]
129 fn should_redact_with_json_value() {
130 let redactor = json!({"nested": "value"});
131 let result = redactor.apply("/data", &json!("original"));
132 assert_eq!(result, json!({"nested": "value"}));
133 }
134
135 #[test]
136 fn should_redact_with_fn_ignoring_path() {
137 let redactor =
138 |_path: &str, val: &Value| json!(format!("masked-{}", val.as_str().unwrap_or("?")));
139 let result = redactor.apply("/id", &json!("secret"));
140 assert_eq!(result, json!("masked-secret"));
141 }
142
143 #[test]
144 fn should_redact_with_fn_using_path() {
145 let redactor = |path: &str, _val: &Value| {
146 let idx = path.split('/').nth(2).unwrap_or("0");
147 json!(format!("id-{idx}"))
148 };
149 let result = redactor.apply("/items/2/id", &json!("uuid-xxx"));
150 assert_eq!(result, json!("id-2"));
151 }
152
153 #[test]
154 fn should_redact_fn_clone_value() {
155 let redactor = |_path: &str, val: &Value| val.clone();
156 let result = redactor.apply("/any/path", &json!(42));
157 assert_eq!(result, json!(42));
158 }
159
160 #[test]
161 fn should_redact_fn_use_both_args() {
162 let redactor = |path: &str, val: &Value| json!(format!("{}={}", path, val));
163 let result = redactor.apply("/key", &json!("value"));
164 assert_eq!(result, json!("/key=\"value\""));
165 }
166}