qubit_redact/facade/redaction_text_output.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Final output paired with its execution summary.
9
10use std::borrow::Cow;
11
12use super::RedactedText;
13use super::RedactionSummary;
14use crate::RedactionCompletion;
15
16/// Published policy-transformed text and completion metadata from one
17/// operation.
18///
19/// Incomplete output retains the selected policy's confidentiality guarantees.
20/// `Truncated` and `Exhausted` describe incomplete diagnostics; they do not
21/// authorize disclosure of fields protected by that policy. As with
22/// [`RedactedText`], disabled policies and explicitly unredacted operations can
23/// deliberately preserve source content. Callers can reject or replace
24/// incomplete text when their own contract requires completeness.
25///
26/// # Examples
27///
28/// ```
29/// use qubit_redact::RedactionCompletion;
30/// use qubit_redact::Redactor;
31///
32/// let output = Redactor::strict().redact_field("password", "raw-secret");
33/// assert_eq!(output.summary().completion(), RedactionCompletion::Complete);
34/// ```
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct RedactionTextOutput {
37 /// Final log-safe text owned by this completed transaction.
38 text: RedactedText,
39 /// Completion, provenance, and resource use for the same transaction.
40 summary: RedactionSummary,
41}
42
43impl RedactionTextOutput {
44 /// Pairs published text with its actual completion and accounting summary.
45 ///
46 /// # Parameters
47 ///
48 /// - `text`: Final policy-transformed and escaped text.
49 /// - `summary`: Accounting and completion from the same operation.
50 ///
51 /// # Returns
52 ///
53 /// An output pairing the text with its actual execution metadata.
54 #[must_use]
55 #[inline(always)]
56 pub(crate) fn new(text: RedactedText, summary: RedactionSummary) -> Self {
57 Self { text, summary }
58 }
59
60 /// Borrows the final text.
61 ///
62 /// # Returns
63 ///
64 /// Borrowed final text, including safe incomplete representations.
65 #[must_use]
66 #[inline(always)]
67 pub const fn text(&self) -> &RedactedText {
68 &self.text
69 }
70
71 /// Borrows the execution summary.
72 ///
73 /// # Returns
74 ///
75 /// Borrowed completion, reasons, and resource use for this output.
76 #[must_use]
77 #[inline(always)]
78 pub const fn summary(&self) -> &RedactionSummary {
79 &self.summary
80 }
81
82 /// Borrows the final text when the operation completed without truncation
83 /// or exhaustion.
84 ///
85 /// # Errors
86 ///
87 /// Returns the execution summary when the safe output was truncated or
88 /// exhausted. The error reports completeness; it does not imply that the
89 /// published text is unsafe for diagnostics.
90 ///
91 /// # Returns
92 ///
93 /// Borrowed text only for Complete; otherwise the borrowed execution
94 /// summary.
95 #[inline]
96 pub fn complete_text(&self) -> Result<&RedactedText, &RedactionSummary> {
97 if self.summary.completion() == RedactionCompletion::Complete {
98 Ok(&self.text)
99 } else {
100 Err(&self.summary)
101 }
102 }
103
104 /// Borrows the final text or returns an escaped caller-selected marker
105 /// when the operation was incomplete.
106 ///
107 /// The complete path does not allocate. An incomplete marker is escaped
108 /// before publication so control characters cannot forge diagnostic log
109 /// structure. The marker is selected after the transaction and therefore
110 /// does not consume its resource budget.
111 ///
112 /// # Parameters
113 ///
114 /// - `marker`: Fallback escaped outside the completed operation’s byte
115 /// budget.
116 ///
117 /// # Returns
118 ///
119 /// Borrowed complete text, or an owned escaped marker for incomplete
120 /// output.
121 #[must_use]
122 #[inline]
123 pub fn text_or_marker(&self, marker: &str) -> Cow<'_, str> {
124 self.complete_text().map_or_else(
125 |_| {
126 Cow::Owned(crate::output::log_escape::escape_log_control_characters(Cow::Borrowed(marker)).into_owned())
127 },
128 |text| Cow::Borrowed(text.as_str()),
129 )
130 }
131
132 /// Consumes a complete output and returns its final text.
133 ///
134 /// # Errors
135 ///
136 /// Returns the execution summary when the safe output was truncated or
137 /// exhausted. The error reports completeness; it does not imply that the
138 /// published text is unsafe for diagnostics.
139 ///
140 /// # Returns
141 ///
142 /// Owned text only for Complete; otherwise the owned execution summary.
143 #[inline]
144 pub fn into_complete_text(self) -> Result<RedactedText, RedactionSummary> {
145 if self.summary.completion() == RedactionCompletion::Complete {
146 Ok(self.text)
147 } else {
148 Err(self.summary)
149 }
150 }
151
152 /// Consumes the output and returns a caller-selected marker when it is
153 /// incomplete.
154 ///
155 /// The marker is escaped before becoming [`RedactedText`], so it remains
156 /// safe for diagnostic presentation.
157 ///
158 /// # Parameters
159 ///
160 /// - `marker`: Fallback escaped outside the completed operation’s byte
161 /// budget.
162 ///
163 /// # Returns
164 ///
165 /// Owned complete text, or a wrapper around the escaped fallback marker.
166 #[must_use]
167 #[inline]
168 pub fn into_text_or_marker(self, marker: &str) -> RedactedText {
169 self.into_complete_text().unwrap_or_else(|_| {
170 RedactedText::from_escaped(
171 crate::output::log_escape::escape_log_control_characters(Cow::Borrowed(marker)).into_owned(),
172 )
173 })
174 }
175
176 /// Consumes the output and returns both parts.
177 ///
178 /// # Returns
179 ///
180 /// The owned text and summary, in that order, without checking
181 /// completeness.
182 #[must_use]
183 #[inline(always)]
184 pub fn into_parts(self) -> (RedactedText, RedactionSummary) {
185 (self.text, self.summary)
186 }
187}