use serde_json::Value;
use super::RedactOptions;
use super::apply::{apply_redaction, apply_remove};
use super::redactor::Redactor;
use crate::client::error::ApiClientError;
#[derive(Debug, Clone)]
#[cfg_attr(docsrs, doc(cfg(feature = "redaction")))]
pub struct ValueRedactionBuilder {
value: Value,
}
impl ValueRedactionBuilder {
pub fn new(value: Value) -> Self {
Self { value }
}
pub fn redact<R: Redactor>(self, path: &str, redactor: R) -> Result<Self, ApiClientError> {
self.redact_with_options(path, redactor, RedactOptions::default())
}
pub fn redact_with_options<R: Redactor>(
mut self,
path: &str,
redactor: R,
options: RedactOptions,
) -> Result<Self, ApiClientError> {
apply_redaction(&mut self.value, path, redactor, options)?;
Ok(self)
}
pub fn redact_remove(self, path: &str) -> Result<Self, ApiClientError> {
self.redact_remove_with(path, RedactOptions::default())
}
pub fn redact_remove_with(
mut self,
path: &str,
options: RedactOptions,
) -> Result<Self, ApiClientError> {
apply_remove(&mut self.value, path, options)?;
Ok(self)
}
pub fn finish(self) -> Value {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn should_redact_with_json_pointer() {
let value = json!({
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Test"
});
let redacted = ValueRedactionBuilder::new(value)
.redact("/id", "REDACTED_ID")
.expect("redaction should succeed")
.finish();
assert_eq!(redacted["id"], "REDACTED_ID");
assert_eq!(redacted["name"], "Test");
}
#[test]
fn should_redact_with_jsonpath_wildcards() {
let value = json!({
"items": [
{"id": "uuid-1", "name": "Item 1"},
{"id": "uuid-2", "name": "Item 2"}
]
});
let redacted = ValueRedactionBuilder::new(value)
.redact("$.items[*].id", "REDACTED")
.expect("redaction should succeed")
.finish();
let items = redacted["items"].as_array().expect("should be array");
assert_eq!(items[0]["id"], "REDACTED");
assert_eq!(items[0]["name"], "Item 1");
assert_eq!(items[1]["id"], "REDACTED");
assert_eq!(items[1]["name"], "Item 2");
}
#[test]
fn should_redact_with_recursive_descent() {
let value = json!({
"id": "root-uuid",
"nested": {
"id": "nested-uuid",
"deep": {
"id": "deep-uuid"
}
}
});
let redacted = ValueRedactionBuilder::new(value)
.redact("$..id", "REDACTED")
.expect("redaction should succeed")
.finish();
assert_eq!(redacted["id"], "REDACTED");
assert_eq!(redacted["nested"]["id"], "REDACTED");
assert_eq!(redacted["nested"]["deep"]["id"], "REDACTED");
}
#[test]
fn should_redact_with_closure() {
let value = json!({
"price": 19.99,
"tax": 1.234567
});
let redacted = ValueRedactionBuilder::new(value)
.redact("$.*", |_path: &str, val: &Value| {
if let Some(n) = val.as_f64() {
json!((n * 100.0).round() / 100.0)
} else {
val.clone()
}
})
.expect("redaction should succeed")
.finish();
assert_eq!(redacted["price"], 19.99);
assert_eq!(redacted["tax"], 1.23);
}
#[test]
fn should_redact_with_index_based_closure() {
let value = json!({
"items": [
{"id": "uuid-a"},
{"id": "uuid-b"},
{"id": "uuid-c"}
]
});
let redacted = ValueRedactionBuilder::new(value)
.redact("$.items[*].id", |path: &str, _val: &Value| {
let idx = path.split('/').nth(2).unwrap_or("?");
json!(format!("item-{idx}"))
})
.expect("redaction should succeed")
.finish();
let items = redacted["items"].as_array().expect("should be array");
assert_eq!(items[0]["id"], "item-0");
assert_eq!(items[1]["id"], "item-1");
assert_eq!(items[2]["id"], "item-2");
}
#[test]
fn should_chain_multiple_redactions() {
let value = json!({
"entity_id": "uuid-123",
"created_at": "2024-12-28T10:30:00Z",
"nested": {
"entity_id": "uuid-456"
}
});
let redacted = ValueRedactionBuilder::new(value)
.redact("$..entity_id", "ENTITY_ID")
.expect("redaction should succeed")
.redact("$..created_at", "TIMESTAMP")
.expect("redaction should succeed")
.finish();
assert_eq!(redacted["entity_id"], "ENTITY_ID");
assert_eq!(redacted["created_at"], "TIMESTAMP");
assert_eq!(redacted["nested"]["entity_id"], "ENTITY_ID");
}
#[test]
fn should_handle_remove() {
let value = json!({
"id": "keep-this",
"secret": "remove-this"
});
let redacted = ValueRedactionBuilder::new(value)
.redact_remove("/secret")
.expect("removal should succeed")
.finish();
assert_eq!(redacted["id"], "keep-this");
assert!(redacted.get("secret").is_none());
}
#[test]
fn should_handle_remove_with_jsonpath() {
let value = json!({
"items": [
{"id": "a", "secret": "x"},
{"id": "b", "secret": "y"}
]
});
let redacted = ValueRedactionBuilder::new(value)
.redact_remove("$.items[*].secret")
.expect("removal should succeed")
.finish();
let items = redacted["items"].as_array().expect("should be array");
assert_eq!(items[0]["id"], "a");
assert!(items[0].get("secret").is_none());
assert_eq!(items[1]["id"], "b");
assert!(items[1].get("secret").is_none());
}
#[test]
fn should_fail_on_no_match_by_default() {
let value = json!({"id": "test"});
let err = ValueRedactionBuilder::new(value)
.redact("/nonexistent", "REDACTED")
.expect_err("should fail for missing path");
assert!(matches!(err, ApiClientError::RedactionError { .. }));
}
#[test]
fn should_respect_allow_empty_match_option() {
let value = json!({"id": "test"});
let options = RedactOptions {
allow_empty_match: true,
};
let redacted = ValueRedactionBuilder::new(value)
.redact_with_options("$.nonexistent", "REDACTED", options)
.expect("should succeed with allow_empty_match")
.finish();
assert_eq!(redacted["id"], "test");
}
#[test]
fn should_handle_json_value_redactor() {
let value = json!({"data": "old"});
let redacted = ValueRedactionBuilder::new(value)
.redact("/data", json!({"nested": "value"}))
.expect("redaction should succeed")
.finish();
assert_eq!(redacted["data"]["nested"], "value");
}
#[test]
fn should_handle_openapi_example_redaction() {
let openapi = json!({
"paths": {
"/users": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"id": "real-uuid-here",
"created_at": "2024-12-28T15:30:00Z"
}
}
}
}
}
}
}
}
});
let stabilized = ValueRedactionBuilder::new(openapi)
.redact("$..example.id", "ENTITY_ID")
.expect("redaction should succeed")
.redact("$..example.created_at", "TIMESTAMP")
.expect("redaction should succeed")
.finish();
let example = &stabilized["paths"]["/users"]["get"]["responses"]["200"]["content"]["application/json"]
["example"];
assert_eq!(example["id"], "ENTITY_ID");
assert_eq!(example["created_at"], "TIMESTAMP");
}
}