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 safe text and completion metadata from one redaction operation.
17///
18/// When redaction is enabled, [`Self::text`] remains confidentiality-safe for
19/// every completion state. `Truncated` and `Exhausted` describe incomplete
20/// diagnostics, not unsafe text. Callers need to reject or replace such text
21/// only when their own contract requires completeness.
22///
23/// # Examples
24///
25/// ```
26/// use qubit_redact::RedactionCompletion;
27/// use qubit_redact::Redactor;
28///
29/// let output = Redactor::strict().redact_field("password", "raw-secret");
30/// assert_eq!(output.summary().completion(), RedactionCompletion::Complete);
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RedactionTextOutput {
34 /// Final log-safe text owned by this completed transaction.
35 text: RedactedText,
36 /// Completion, provenance, and resource use for the same transaction.
37 summary: RedactionSummary,
38}
39
40impl RedactionTextOutput {
41 /// Creates a complete output.
42 #[must_use]
43 pub(crate) fn new(text: RedactedText, summary: RedactionSummary) -> Self {
44 Self { text, summary }
45 }
46
47 /// Borrows the final text.
48 #[must_use]
49 #[inline(always)]
50 pub const fn text(&self) -> &RedactedText {
51 &self.text
52 }
53
54 /// Borrows the execution summary.
55 #[must_use]
56 #[inline(always)]
57 pub const fn summary(&self) -> &RedactionSummary {
58 &self.summary
59 }
60
61 /// Borrows the final text when the operation completed without truncation
62 /// or exhaustion.
63 ///
64 /// # Errors
65 ///
66 /// Returns the execution summary when the safe output was truncated or
67 /// exhausted. The error reports completeness; it does not imply that the
68 /// published text is unsafe for diagnostics.
69 pub fn complete_text(&self) -> Result<&RedactedText, &RedactionSummary> {
70 if self.summary.completion() == RedactionCompletion::Complete {
71 Ok(&self.text)
72 } else {
73 Err(&self.summary)
74 }
75 }
76
77 /// Borrows the final text or returns an escaped caller-selected marker
78 /// when the operation was incomplete.
79 ///
80 /// The complete path does not allocate. An incomplete marker is escaped
81 /// before publication so control characters cannot forge diagnostic log
82 /// structure. The marker is selected after the transaction and therefore
83 /// does not consume its resource budget.
84 #[must_use]
85 pub fn text_or_marker(&self, marker: &str) -> Cow<'_, str> {
86 self.complete_text().map_or_else(
87 |_| {
88 Cow::Owned(crate::output::log_escape::escape_log_control_characters(Cow::Borrowed(marker)).into_owned())
89 },
90 |text| Cow::Borrowed(text.as_str()),
91 )
92 }
93
94 /// Consumes a complete output and returns its final text.
95 ///
96 /// # Errors
97 ///
98 /// Returns the execution summary when the safe output was truncated or
99 /// exhausted. The error reports completeness; it does not imply that the
100 /// published text is unsafe for diagnostics.
101 pub fn into_complete_text(self) -> Result<RedactedText, RedactionSummary> {
102 if self.summary.completion() == RedactionCompletion::Complete {
103 Ok(self.text)
104 } else {
105 Err(self.summary)
106 }
107 }
108
109 /// Consumes the output and returns a caller-selected marker when it is
110 /// incomplete.
111 ///
112 /// The marker is escaped before becoming [`RedactedText`], so it remains
113 /// safe for diagnostic presentation.
114 #[must_use]
115 pub fn into_text_or_marker(self, marker: &str) -> RedactedText {
116 self.into_complete_text().unwrap_or_else(|_| {
117 RedactedText::from_escaped(
118 crate::output::log_escape::escape_log_control_characters(std::borrow::Cow::Borrowed(marker))
119 .into_owned(),
120 )
121 })
122 }
123
124 /// Consumes the output and returns both parts.
125 #[must_use]
126 #[inline(always)]
127 pub fn into_parts(self) -> (RedactedText, RedactionSummary) {
128 (self.text, self.summary)
129 }
130}