Skip to main content

qubit_redact/json/
redacted_json.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Lazy recursive formatting for an already parsed JSON value.
9
10use std::fmt;
11
12use serde_json::Value;
13
14use crate::{
15    RedactValue as _,
16    RedactedValue,
17    RedactionPolicy,
18};
19
20#[cfg(feature = "serde")]
21use super::internal::{
22    JsonRedactionState,
23    JsonUnkeyedValuePolicy,
24};
25
26/// A borrowed JSON value rendered with policy-aware object-key redaction.
27#[must_use = "format or serialize the redacted JSON view"]
28pub struct RedactedJson<'value, 'policy> {
29    /// Original parsed JSON borrowed without cloning for formatting.
30    value: &'value Value,
31    /// Policy used to classify every encountered object key.
32    policy: &'policy RedactionPolicy,
33}
34
35impl<'value, 'policy> RedactedJson<'value, 'policy> {
36    /// Creates a lazy redacted view over one parsed JSON value.
37    ///
38    /// # Parameters
39    ///
40    /// * value - Parsed JSON borrowed without cloning.
41    /// * policy - Immutable policy used to classify object keys.
42    ///
43    /// # Returns
44    ///
45    /// A borrowed JSON redaction view.
46    #[inline(always)]
47    pub const fn new(
48        value: &'value Value,
49        policy: &'policy RedactionPolicy,
50    ) -> Self {
51        Self { value, policy }
52    }
53
54    /// Clones and redacts the value for owned output protocols.
55    ///
56    /// # Returns
57    ///
58    /// An owned JSON value with every sensitive keyed value replaced.
59    #[cfg(feature = "serde")]
60    fn to_redacted_value(&self) -> Value {
61        let mut value = self.value.clone();
62        let mut remaining_mask_bytes = usize::MAX;
63        let mut state = JsonRedactionState::new(
64            self.policy,
65            JsonUnkeyedValuePolicy::PassThrough,
66            &mut remaining_mask_bytes,
67        );
68        let _ = state.redact(&mut value);
69        value
70    }
71}
72
73impl fmt::Debug for RedactedJson<'_, '_> {
74    /// Formats nested objects and arrays while masking policy-selected values.
75    ///
76    /// # Parameters
77    ///
78    /// * formatter - Destination formatting context.
79    ///
80    /// # Returns
81    ///
82    /// The formatter result for the redacted JSON representation.
83    ///
84    /// # Errors
85    ///
86    /// Returns a formatting error when the destination rejects output.
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        fmt_json(self.value, self.policy, formatter)
89    }
90}
91
92#[cfg(feature = "serde")]
93impl serde::Serialize for RedactedJson<'_, '_> {
94    /// Serializes a redacted clone while retaining the JSON value shape.
95    ///
96    /// # Type Parameters
97    ///
98    /// * S - Destination serializer type.
99    ///
100    /// # Parameters
101    ///
102    /// * serializer - Destination serde serializer.
103    ///
104    /// # Returns
105    ///
106    /// The destination serializer result.
107    ///
108    /// # Errors
109    ///
110    /// Returns the destination serializer error unchanged.
111    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112    where
113        S: serde::Serializer,
114    {
115        serde::Serialize::serialize(&self.to_redacted_value(), serializer)
116    }
117}
118
119/// Recursively formats one JSON node with policy-aware object keys.
120///
121/// # Parameters
122///
123/// * value - Current node borrowed without cloning.
124/// * policy - Immutable key-classification and masking policy.
125/// * formatter - Destination formatting context.
126///
127/// # Returns
128///
129/// The formatter result for the complete node.
130///
131/// # Errors
132///
133/// Returns a formatting error when the destination rejects output.
134fn fmt_json(
135    value: &Value,
136    policy: &RedactionPolicy,
137    formatter: &mut fmt::Formatter<'_>,
138) -> fmt::Result {
139    match value {
140        Value::Array(values) => {
141            let mut output = formatter.debug_list();
142            for value in values {
143                output.entry(&RedactedJson::new(value, policy));
144            }
145            output.finish()
146        }
147        Value::Object(values) => {
148            let mut output = formatter.debug_map();
149            for (key, value) in values {
150                if let Some(sensitivity) = policy.sensitivity_for(key) {
151                    fmt_masked_entry(
152                        &mut output,
153                        key,
154                        value,
155                        sensitivity,
156                        policy,
157                    );
158                } else {
159                    output.entry(key, &RedactedJson::new(value, policy));
160                }
161            }
162            output.finish()
163        }
164        value => fmt::Debug::fmt(value, formatter),
165    }
166}
167
168/// Writes one object entry whose key selected a sensitivity level.
169///
170/// # Parameters
171///
172/// * output - In-progress debug map.
173/// * key - Original object key preserved in output.
174/// * value - Sensitive value to replace.
175/// * sensitivity - Level selecting the configured mask.
176/// * policy - Immutable masking configuration.
177fn fmt_masked_entry(
178    output: &mut fmt::DebugMap<'_, '_>,
179    key: &str,
180    value: &Value,
181    sensitivity: crate::Sensitivity,
182    policy: &RedactionPolicy,
183) {
184    match value {
185        Value::String(text) => {
186            let redacted = text.redact_value(sensitivity, policy.masking());
187            output.entry(&key, &redacted);
188        }
189        _ => {
190            let redacted = RedactedValue::opaque(sensitivity, policy.masking());
191            output.entry(&key, &redacted);
192        }
193    };
194}