qubit_redact/domain/redacted_keyed_value.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 redaction view selected by an external field key.
9
10use std::fmt::{
11 self,
12 Debug,
13 Display,
14 Formatter,
15};
16
17#[cfg(feature = "serde")]
18use crate::policy::ResolvedField;
19use crate::{
20 LogOutputLimit,
21 Redact,
22 RedactValue,
23 RedactionPolicy,
24 RedactionSession,
25};
26
27use super::{
28 bounded_redacted_display::format_bounded,
29 bounded_redacted_display::format_debug_bounded,
30 internal::mask_byte_limit,
31};
32
33/// A borrowed value rendered according to a separate field key.
34///
35/// The key is used only to select a policy rule. The view itself renders or
36/// serializes the value, and borrows an immutable policy snapshot.
37///
38/// # Type Parameters
39///
40/// * `'value` - Lifetime of the borrowed key and value.
41/// * `'policy` - Lifetime of the borrowed policy snapshot.
42/// * `T` - Value type rendered or serialized through redaction.
43#[must_use = "format or serialize the keyed redaction view"]
44pub struct RedactedKeyedValue<'value, 'policy, T: ?Sized> {
45 /// Field name used for policy classification.
46 key: &'value str,
47 /// Value represented by this view.
48 value: &'value T,
49 /// Immutable policy snapshot borrowed by every output protocol.
50 policy: &'policy RedactionPolicy,
51}
52
53impl<'value, 'policy, T: ?Sized> RedactedKeyedValue<'value, 'policy, T> {
54 /// Creates a keyed view from borrowed inputs and a borrowed policy
55 /// snapshot.
56 ///
57 /// # Parameters
58 ///
59 /// * `key` - Field name used only for policy classification.
60 /// * `value` - Value to render or serialize lazily.
61 /// * `policy` - Complete policy snapshot borrowed by this view.
62 ///
63 /// # Returns
64 ///
65 /// A view that never modifies the original value.
66 #[must_use = "format or serialize the keyed redaction view"]
67 #[inline(always)]
68 pub const fn new(
69 key: &'value str,
70 value: &'value T,
71 policy: &'policy RedactionPolicy,
72 ) -> Self {
73 Self { key, value, policy }
74 }
75
76 /// Returns the external field key used by this view.
77 ///
78 /// # Returns
79 ///
80 /// The unchanged policy lookup key.
81 #[must_use]
82 #[inline(always)]
83 pub const fn key(&self) -> &'value str {
84 self.key
85 }
86}
87
88impl<T: Redact + RedactValue + ?Sized> Debug for RedactedKeyedValue<'_, '_, T> {
89 /// Formats the value through its selected field classification.
90 ///
91 /// # Parameters
92 ///
93 /// * `formatter` - Destination formatter whose flags are preserved.
94 ///
95 /// # Returns
96 ///
97 /// The complete redacted debug result.
98 ///
99 /// # Errors
100 ///
101 /// Returns [`fmt::Error`] when the destination formatter cannot accept the
102 /// complete representation.
103 #[inline]
104 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
105 let session = RedactionSession::diagnostic(self.policy);
106 let view =
107 RedactedKeyedValueSession::new(self.key, self.value, &session);
108 if mask_byte_limit().is_some() {
109 return Debug::fmt(&view, formatter);
110 }
111 format_debug_bounded(
112 &view,
113 LogOutputLimit::from(self.policy.limits().diagnostic_event()),
114 formatter,
115 )
116 }
117}
118
119mod session_view {
120 use std::fmt::{
121 self,
122 Debug,
123 Display,
124 Formatter,
125 Write as _,
126 };
127
128 use crate::{
129 Redact,
130 RedactValue,
131 RedactionSession,
132 policy::ResolvedField,
133 text::internal::LogEscapeWriter,
134 };
135
136 /// A keyed value view that reuses one diagnostic session.
137 #[must_use = "format the keyed redacted value view"]
138 pub struct RedactedKeyedValueSession<'value, 'session, 'policy, T: ?Sized> {
139 key: &'value str,
140 value: &'value T,
141 session: &'session RedactionSession<'policy>,
142 }
143
144 impl<'value, 'session, 'policy, T: ?Sized>
145 RedactedKeyedValueSession<'value, 'session, 'policy, T>
146 {
147 /// Creates a keyed view borrowing an existing diagnostic session.
148 #[inline(always)]
149 pub fn new(
150 key: &'value str,
151 value: &'value T,
152 session: &'session RedactionSession<'policy>,
153 ) -> Self {
154 Self {
155 key,
156 value,
157 session,
158 }
159 }
160 }
161
162 impl<T: Redact + RedactValue + ?Sized> Debug
163 for RedactedKeyedValueSession<'_, '_, '_, T>
164 {
165 /// Formats the value through its selected classification and shared
166 /// session.
167 #[inline]
168 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
169 let policy = self.session.policy();
170 let resolved = policy.resolve_field(self.key);
171 match resolved {
172 ResolvedField::Sensitive { sensitivity } => Debug::fmt(
173 &self.value.redact_value(sensitivity, policy.masking()),
174 formatter,
175 ),
176 ResolvedField::PassThrough => {
177 self.value.fmt_redacted(self.session, formatter)
178 }
179 }
180 }
181 }
182
183 impl<T: Redact + RedactValue + ?Sized> Display
184 for RedactedKeyedValueSession<'_, '_, '_, T>
185 {
186 /// Escapes the selected redacted representation for plain-text logs.
187 #[inline]
188 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
189 let mut writer = LogEscapeWriter::new(formatter);
190 write!(&mut writer, "{self:?}")
191 }
192 }
193}
194
195pub use session_view::RedactedKeyedValueSession;
196
197impl<T: Redact + RedactValue + ?Sized> Display
198 for RedactedKeyedValue<'_, '_, T>
199{
200 /// Formats the selected redacted representation for a bounded plain-text
201 /// log.
202 ///
203 /// # Parameters
204 ///
205 /// * `formatter` - Destination plain-text log boundary.
206 ///
207 /// # Returns
208 ///
209 /// The complete escaped redacted representation.
210 ///
211 /// # Errors
212 ///
213 /// Returns [`fmt::Error`] when the destination formatter cannot accept the
214 /// complete escaped representation.
215 #[inline]
216 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
217 let session = RedactionSession::diagnostic(self.policy);
218 let view =
219 RedactedKeyedValueSession::new(self.key, self.value, &session);
220 format_bounded(
221 &view,
222 LogOutputLimit::from(self.policy.limits().diagnostic_event()),
223 formatter,
224 )
225 }
226}
227
228#[cfg(feature = "serde")]
229impl<T: RedactValue + crate::domain::RedactSerialize + ?Sized> serde::Serialize
230 for RedactedKeyedValue<'_, '_, T>
231{
232 /// Serializes the value through its selected field classification.
233 ///
234 /// # Type Parameters
235 ///
236 /// * `S` - Destination Serde serializer type.
237 ///
238 /// # Parameters
239 ///
240 /// * `serializer` - Destination serde serializer.
241 ///
242 /// # Returns
243 ///
244 /// The serializer's successful redacted output.
245 ///
246 /// # Errors
247 ///
248 /// Returns the destination serializer's error unchanged.
249 #[inline]
250 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
251 where
252 S: serde::Serializer,
253 {
254 let resolved = self.policy.resolve_field(self.key);
255 match resolved {
256 ResolvedField::Sensitive { sensitivity } => {
257 serde::Serialize::serialize(
258 &self
259 .value
260 .redact_value(sensitivity, self.policy.masking()),
261 serializer,
262 )
263 }
264 ResolvedField::PassThrough => {
265 crate::domain::RedactSerialize::serialize_redacted(
266 self.value,
267 self.policy,
268 serializer,
269 )
270 }
271 }
272 }
273}