Skip to main content

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