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}