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    RedactedKeyedValue,
27    RedactionPolicy,
28    text::internal::LogEscapeWriter,
29};
30
31/// A lazy map view that classifies each value by its key before recursion.
32///
33/// Unknown and explicitly allowed keys delegate to the corresponding value's
34/// [`Redact`] implementation. Sensitive keys mask the complete value through
35/// [`RedactValue`].
36///
37/// # Type Parameters
38///
39/// * `'a` - Lifetime of the borrowed map.
40/// * `M` - Borrowed map-like container type.
41/// * `K` - Runtime key type used for field classification.
42/// * `V` - Value type recursively rendered through redaction.
43#[must_use = "format the recursive keyed redaction view"]
44pub struct RedactedKeyedMap<
45    'a,
46    M: ?Sized,
47    K: ?Sized = String,
48    V: ?Sized = String,
49> {
50    /// Map borrowed without traversal.
51    map: &'a M,
52    /// Immutable policy snapshot shared by every keyed value view.
53    policy: RedactionPolicy,
54    /// Associates the view with the map entry types without storing them.
55    marker: PhantomData<fn() -> (*const K, *const V)>,
56}
57
58impl<'a, M: ?Sized, K: ?Sized, V: ?Sized> RedactedKeyedMap<'a, M, K, V> {
59    /// Creates a lazy recursive keyed map view without traversing the map.
60    ///
61    /// # Parameters
62    ///
63    /// * `map` - Map-like container to borrow.
64    /// * `policy` - Complete policy snapshot owned by the map view.
65    ///
66    /// # Returns
67    ///
68    /// A lazy borrowed map view that shares its policy across all entries.
69    #[must_use = "format the recursive keyed redaction view"]
70    #[inline(always)]
71    pub const fn new(map: &'a M, policy: RedactionPolicy) -> Self {
72        Self {
73            map,
74            policy,
75            marker: PhantomData,
76        }
77    }
78
79    /// Converts this view into a byte-bounded, log-safe display adapter.
80    ///
81    /// # Parameters
82    ///
83    /// * `limit` - Maximum rendered bytes including any truncation marker.
84    ///
85    /// # Returns
86    ///
87    /// A display-only adapter that owns this recursive keyed map view.
88    #[must_use = "format the bounded recursive keyed map display adapter"]
89    #[inline(always)]
90    pub const fn with_output_limit(
91        self,
92        limit: LogOutputLimit,
93    ) -> BoundedRedactedDisplay<Self> {
94        BoundedRedactedDisplay::new(self, limit)
95    }
96
97    /// Converts this view into a byte-bounded display adapter using its policy.
98    ///
99    /// # Returns
100    ///
101    /// A display-only adapter bounded by this view's diagnostic output budget.
102    #[must_use = "format the bounded recursive keyed map display adapter"]
103    #[inline]
104    pub fn with_policy_output_limit(self) -> BoundedRedactedDisplay<Self> {
105        let limit = LogOutputLimit::from(self.policy.diagnostic_budget());
106        BoundedRedactedDisplay::new(self, limit)
107    }
108}
109
110impl<
111    M: ?Sized,
112    K: AsRef<str> + Debug + ?Sized,
113    V: Redact + RedactValue + ?Sized,
114> Debug for RedactedKeyedMap<'_, M, K, V>
115where
116    for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
117{
118    /// Formats each entry through its key-selected redaction behavior.
119    ///
120    /// # Parameters
121    ///
122    /// * `formatter` - Destination debug formatter.
123    ///
124    /// # Returns
125    ///
126    /// The formatter result for the complete map.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`fmt::Error`] when the destination rejects an entry or the
131    /// completed map.
132    #[inline]
133    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
134        let mut output = formatter.debug_map();
135        for (key, value) in self.map {
136            output.entry(
137                &key,
138                &RedactedKeyedValue::new(key.as_ref(), value, &self.policy),
139            );
140        }
141        output.finish()
142    }
143}
144
145impl<
146    M: ?Sized,
147    K: AsRef<str> + Debug + ?Sized,
148    V: Redact + RedactValue + ?Sized,
149> Display for RedactedKeyedMap<'_, M, K, V>
150where
151    for<'entry> &'entry M: IntoIterator<Item = (&'entry K, &'entry V)>,
152{
153    /// Formats compact redacted debug output and escapes it for plain-text
154    /// logs.
155    ///
156    /// # Parameters
157    ///
158    /// * `formatter` - Destination formatting context.
159    ///
160    /// # Returns
161    ///
162    /// The formatter result for the escaped redacted representation.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`fmt::Error`] when the destination rejects the complete
167    /// log-safe representation.
168    #[inline]
169    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
170        let mut writer = LogEscapeWriter::new(formatter);
171        write!(&mut writer, "{self:?}")
172    }
173}