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}