Skip to main content

qubit_redact/domain/
redacted_value.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//! Redacted representation of a plain or optional textual field.
9
10use std::{
11    borrow::Cow,
12    fmt::{
13        self,
14        Debug,
15        Display,
16        Formatter,
17    },
18};
19
20use crate::{
21    LogSafeText,
22    MaskingPolicy,
23    RedactedText,
24    Sensitivity,
25};
26
27/// Redacted text retaining its original plain or optional container shape.
28///
29/// # Type Parameters
30///
31/// * `'a` - Lifetime of any borrowed redacted text stored by the value.
32#[must_use = "format or otherwise consume the redacted value"]
33#[derive(Clone, PartialEq, Eq)]
34pub enum RedactedValue<'a> {
35    /// A plain textual value.
36    Text(
37        /// Masked text, borrowed when the masking policy permits it.
38        RedactedText<'a>,
39    ),
40    /// A present optional textual value.
41    Some(
42        /// Masked contents of the present option.
43        RedactedText<'a>,
44    ),
45    /// An absent optional textual value.
46    None,
47}
48
49impl<'a> RedactedValue<'a> {
50    /// Creates an opaque replacement for a sensitive non-text value.
51    ///
52    /// # Parameters
53    ///
54    /// * `level` - Sensitivity level selecting the complete replacement.
55    /// * `masking` - Complete masking configuration.
56    ///
57    /// # Returns
58    ///
59    /// A plain redacted value that borrows the configured opaque replacement.
60    #[inline(always)]
61    pub fn opaque(level: Sensitivity, masking: &'a MaskingPolicy) -> Self {
62        Self::Text(RedactedText::new(Cow::Borrowed(masking.mask_opaque(level))))
63    }
64}
65
66impl Debug for RedactedValue<'_> {
67    /// Writes the masked text while retaining normal text and option shapes.
68    ///
69    /// # Parameters
70    ///
71    /// * `formatter` - Destination formatting context.
72    ///
73    /// # Returns
74    ///
75    /// The formatter result for the complete redacted value.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`fmt::Error`] when the destination cannot accept the complete
80    /// representation.
81    #[inline]
82    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Text(text) => Debug::fmt(text.as_str(), formatter),
85            Self::Some(text) => {
86                formatter.debug_tuple("Some").field(&text.as_str()).finish()
87            }
88            Self::None => formatter.write_str("None"),
89        }
90    }
91}
92
93#[cfg(feature = "serde")]
94impl serde::Serialize for RedactedValue<'_> {
95    /// Preserves the original plain or optional container shape.
96    ///
97    /// # Type Parameters
98    ///
99    /// * `S` - Destination Serde serializer type.
100    ///
101    /// # Parameters
102    ///
103    /// * `serializer` - Destination Serde serializer.
104    ///
105    /// # Returns
106    ///
107    /// The serializer's successful text or option output.
108    ///
109    /// # Errors
110    ///
111    /// Returns the destination serializer's error unchanged.
112    #[inline]
113    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114    where
115        S: serde::Serializer,
116    {
117        match self {
118            Self::Text(text) => serializer.serialize_str(text.as_str()),
119            Self::Some(text) => serializer.serialize_some(text.as_str()),
120            Self::None => serializer.serialize_none(),
121        }
122    }
123}
124
125impl Display for RedactedValue<'_> {
126    /// Writes masked contents escaped for a plain-text log boundary.
127    ///
128    /// # Parameters
129    ///
130    /// * `formatter` - Destination formatting context.
131    ///
132    /// # Returns
133    ///
134    /// The formatter result for the complete log-safe value.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`fmt::Error`] when the destination cannot accept the complete
139    /// log-safe representation.
140    #[inline]
141    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
142        match self {
143            Self::Text(text) => Display::fmt(&log_safe(text), formatter),
144            Self::Some(text) => {
145                formatter.write_str("Some(")?;
146                Display::fmt(&log_safe(text), formatter)?;
147                formatter.write_str(")")
148            }
149            Self::None => formatter.write_str("None"),
150        }
151    }
152}
153
154/// Borrows masked text and escapes it for a plain-text log boundary.
155///
156/// # Type Parameters
157///
158/// * `'a` - Lifetime of the masked text and returned log-safe view.
159///
160/// # Parameters
161///
162/// * `text` - Masked text to render safely.
163///
164/// # Returns
165///
166/// A log-safe view that borrows `text` when it contains no unsafe controls.
167#[inline(always)]
168fn log_safe<'a>(text: &'a RedactedText<'_>) -> LogSafeText<'a> {
169    RedactedText::new(Cow::Borrowed(text.as_str())).escape_for_log()
170}