Skip to main content

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}