Skip to main content

qubit_redact/domain/
redacted_keyed_map.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 borrowed view of a map whose values support recursive redaction.
9
10use std::{
11    fmt::{
12        self,
13        Debug,
14        Display,
15        Formatter,
16        Write as _,
17    },
18    marker::PhantomData,
19};
20
21use crate::{
22    BoundedRedactedDisplay,
23    LogOutputLimit,
24    Redact,
25    RedactValue,
26    RedactionPolicy,
27    RedactionSession,
28    text::internal::LogEscapeWriter,
29};
30
31use super::{
32    bounded_redacted_display::format_bounded,
33    bounded_redacted_display::format_debug_bounded,
34    internal::mask_byte_limit,
35};
36
37/// A lazy map view that classifies each value by its key before recursion.
38///
39/// Unknown and explicitly allowed keys delegate to the corresponding value's
40/// [`Redact`] implementation. Sensitive keys mask the complete value through
41/// [`RedactValue`].
42///
43/// # Type Parameters
44///
45/// * `'a` - Lifetime of the borrowed map.
46/// * `M` - Borrowed map-like container type.
47/// * `K` - Runtime key type used for field classification.
48/// * `V` - Value type recursively rendered through redaction.
49#[must_use = "format the recursive keyed redaction view"]
50pub struct RedactedKeyedMap<
51    'a,
52    M: ?Sized,
53    K: ?Sized = String,
54    V: ?Sized = String,
55> {
56    /// Map borrowed without traversal.
57    map: &'a M,
58    /// Immutable policy snapshot shared by every keyed value view.
59    policy: RedactionPolicy,
60    /// Associates the view with the map entry types without storing them.
61    marker: PhantomData<fn() -> (*const K, *const V)>,
62}
63
64impl<'a, M: ?Sized, K: ?Sized, V: ?Sized> RedactedKeyedMap<'a, M, K, V> {
65    /// Creates a lazy recursive keyed map view without traversing the map.
66    ///
67    /// # Parameters
68    ///
69    /// * `map` - Map-like container to borrow.
70    /// * `policy` - Complete policy snapshot owned by the map view.
71    ///
72    /// # Returns
73    ///
74    /// A lazy borrowed map view that shares its policy across all entries.
75    #[must_use = "format the recursive keyed redaction view"]
76    #[inline(always)]
77    pub const fn new(map: &'a M, policy: RedactionPolicy) -> Self {
78        Self {
79            map,
80            policy,
81            marker: PhantomData,
82        }
83    }
84
85    /// Converts this view into a byte-bounded, log-safe display adapter.
86    ///
87    /// # Parameters
88    ///
89    /// * `limit` - Maximum rendered bytes including any truncation marker.
90    ///
91    /// # Returns
92    ///
93    /// A bounded formatting adapter that owns this recursive keyed map view.
94    #[must_use = "format the bounded recursive keyed map display adapter"]
95    #[inline(always)]
96    pub const fn with_output_limit(
97        self,
98        limit: LogOutputLimit,
99    ) -> BoundedRedactedDisplay<Self> {
100        BoundedRedactedDisplay::new(self, limit)
101    }
102
103    /// Converts this view into a byte-bounded display adapter using its policy.
104    ///
105    /// # Returns
106    ///
107    /// A formatting adapter bounded by this view's diagnostic output budget.
108    #[must_use = "format the bounded recursive keyed map display adapter"]
109    #[inline]
110    pub fn with_policy_output_limit(self) -> BoundedRedactedDisplay<Self> {
111        let limit =
112            LogOutputLimit::from(self.policy.limits().diagnostic_event());
113        BoundedRedactedDisplay::new(self, limit)
114    }
115}
116
117impl<
118    M: ?Sized,
119    K: AsRef<str> + Debug + ?Sized,
120    V: Redact + RedactValue + ?Sized,
121> Debug for RedactedKeyedMap<'_, M, K, V>
122where
123    for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
124{
125    /// Formats each entry through its key-selected redaction behavior.
126    ///
127    /// # Parameters
128    ///
129    /// * `formatter` - Destination debug formatter.
130    ///
131    /// # Returns
132    ///
133    /// The formatter result for the complete map.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`fmt::Error`] when the destination rejects an entry or the
138    /// completed map.
139    #[inline]
140    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
141        let session = RedactionSession::diagnostic(&self.policy);
142        let view = RedactedKeyedMapSession::new(self.map, &session);
143        if mask_byte_limit().is_some() {
144            return Debug::fmt(&view, formatter);
145        }
146        format_debug_bounded(
147            &view,
148            LogOutputLimit::from(self.policy.limits().diagnostic_event()),
149            formatter,
150        )
151    }
152}
153
154mod session_view {
155    use std::{
156        fmt::{
157            self,
158            Debug,
159            Formatter,
160        },
161        marker::PhantomData,
162    };
163
164    use crate::{
165        Redact,
166        RedactValue,
167        RedactedKeyedValueSession,
168        RedactionSession,
169    };
170
171    /// A nested keyed-map view that reuses an existing diagnostic session.
172    #[must_use = "format the nested keyed redaction view"]
173    pub struct RedactedKeyedMapSession<
174        'map,
175        'session,
176        'policy,
177        M: ?Sized,
178        K: ?Sized = String,
179        V: ?Sized = String,
180    > {
181        map: &'map M,
182        session: &'session RedactionSession<'policy>,
183        marker: PhantomData<fn() -> (*const K, *const V)>,
184    }
185
186    impl<'map, 'session, 'policy, M: ?Sized, K: ?Sized, V: ?Sized>
187        RedactedKeyedMapSession<'map, 'session, 'policy, M, K, V>
188    {
189        /// Creates a nested keyed-map view using an existing diagnostic
190        /// session.
191        #[inline(always)]
192        pub fn new(
193            map: &'map M,
194            session: &'session RedactionSession<'policy>,
195        ) -> Self {
196            Self {
197                map,
198                session,
199                marker: PhantomData,
200            }
201        }
202    }
203
204    impl<
205        M: ?Sized,
206        K: AsRef<str> + Debug + ?Sized,
207        V: Redact + RedactValue + ?Sized,
208    > Debug for RedactedKeyedMapSession<'_, '_, '_, M, K, V>
209    where
210        for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
211    {
212        /// Formats each entry through the existing keyed diagnostic session.
213        #[inline]
214        fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
215            let mut output = formatter.debug_map();
216            for (key, value) in self.map {
217                output.entry(
218                    &key,
219                    &RedactedKeyedValueSession::new(
220                        key.as_ref(),
221                        value,
222                        self.session,
223                    ),
224                );
225            }
226            output.finish()
227        }
228    }
229}
230
231pub use session_view::RedactedKeyedMapSession;
232
233impl<
234    M: ?Sized,
235    K: AsRef<str> + Debug + ?Sized,
236    V: Redact + RedactValue + ?Sized,
237> Display for RedactedKeyedMapSession<'_, '_, '_, M, K, V>
238where
239    for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
240{
241    /// Escapes nested keyed-map debug output for plain-text logs.
242    #[inline]
243    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
244        let mut writer = LogEscapeWriter::new(formatter);
245        write!(&mut writer, "{self:?}")
246    }
247}
248
249impl<
250    M: ?Sized,
251    K: AsRef<str> + Debug + ?Sized,
252    V: Redact + RedactValue + ?Sized,
253> Display for RedactedKeyedMap<'_, M, K, V>
254where
255    for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
256{
257    /// Formats bounded compact redacted debug output and escapes it for
258    /// plain-text logs.
259    ///
260    /// # Parameters
261    ///
262    /// * `formatter` - Destination formatting context.
263    ///
264    /// # Returns
265    ///
266    /// The formatter result for the escaped redacted representation.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`fmt::Error`] when the destination rejects the complete
271    /// log-safe representation.
272    #[inline]
273    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
274        let session = RedactionSession::diagnostic(&self.policy);
275        let view = RedactedKeyedMapSession::new(self.map, &session);
276        format_bounded(
277            &view,
278            LogOutputLimit::from(self.policy.limits().diagnostic_event()),
279            formatter,
280        )
281    }
282}