Skip to main content

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}