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}