Skip to main content

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