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}