Skip to main content

qubit_redact/facade/
diagnostic_redaction_batch.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//! Independently resolvable redaction items owned by one batch transaction.
9
10use std::ffi::OsStr;
11use std::fmt::Display;
12
13#[cfg(feature = "http")]
14use http::HeaderMap;
15#[cfg(feature = "http")]
16use http::HeaderValue;
17#[cfg(feature = "json")]
18use serde_json::Value;
19
20use super::DiagnosticRedactionBatchOutput;
21use super::DiagnosticRedactionHandle;
22use super::DiagnosticRedactionOutput;
23use crate::domain::Redact;
24#[cfg(feature = "http")]
25use crate::formats::http::BodyCapture;
26use crate::runtime::BatchSession;
27use crate::runtime::RedactionHandle;
28
29/// Escaped at publication when callers choose the default diagnostic finish.
30const DEFAULT_DIAGNOSTIC_MARKER: &str = "<redaction incomplete>";
31
32/// Accumulates independently resolvable redaction items under one budget.
33///
34/// Each operation returns an opaque handle. Handles are usable only with the
35/// [`DiagnosticRedactionOutput`] produced by consuming this batch with
36/// [`Self::finish_with_marker`].
37///
38/// # Examples
39///
40/// ```
41/// use qubit_redact::Redactor;
42///
43/// let mut batch = Redactor::strict().diagnostic_batch();
44/// let handle = batch.redact_field("password", "raw-secret");
45/// let output = batch.finish_with_marker("<redaction incomplete>");
46/// assert!(!output.text(handle).as_str().contains("raw-secret"));
47/// ```
48pub struct DiagnosticRedactionBatch {
49    /// Typed transaction that owns unpublished independently resolvable items.
50    session: BatchSession,
51}
52
53impl DiagnosticRedactionBatch {
54    /// Creates a batch backed by one private runtime transaction.
55    ///
56    /// # Parameters
57    ///
58    /// - `session`: Fresh batch transaction exclusively owned by this facade.
59    ///
60    /// # Returns
61    ///
62    /// A batch retaining the transaction’s policy and shared budget.
63    #[must_use]
64    #[inline(always)]
65    pub(crate) const fn from_session(session: BatchSession) -> Self {
66        Self { session }
67    }
68
69    /// Converts the runtime-private handle into its public batch counterpart.
70    ///
71    /// # Parameters
72    ///
73    /// - `handle`: Runtime capability retaining its transaction identity and
74    ///   item index.
75    ///
76    /// # Returns
77    ///
78    /// The equivalent public opaque handle without changing identity.
79    #[must_use]
80    #[inline(always)]
81    fn wrap(handle: RedactionHandle) -> DiagnosticRedactionHandle {
82        let (batch_id, item_index) = handle.parts();
83        DiagnosticRedactionHandle { batch_id, item_index }
84    }
85
86    /// Returns whether the shared output budget has closed this batch.
87    ///
88    /// # Returns
89    ///
90    /// True after the shared output boundary closes; later item operations
91    /// cannot reopen it.
92    #[must_use]
93    #[inline(always)]
94    pub fn is_output_exhausted(&self) -> bool {
95        self.session.is_output_exhausted()
96    }
97
98    /// Redacts one named scalar field and returns its opaque batch handle.
99    ///
100    /// `field` selects the policy rule applied to `value`. The result remains
101    /// unpublished until [`Self::finish_with_marker`] consumes this batch.
102    ///
103    /// # Type Parameters
104    ///
105    /// - `T`: Lazily evaluated scalar formatter.
106    ///
107    /// # Parameters
108    ///
109    /// - `field`: Raw key admitted before classification and value access.
110    /// - `value`: Scalar formatter evaluated only when admission and masking
111    ///   require it.
112    ///
113    /// # Returns
114    ///
115    /// An opaque handle to the recorded item, resolvable after this batch is
116    /// finished.
117    #[must_use]
118    #[inline(always)]
119    pub fn redact_field<T>(&mut self, field: &str, value: &T) -> DiagnosticRedactionHandle
120    where
121        T: Display + ?Sized,
122    {
123        let handle = self.session.redact_field(field, value);
124        let (batch_id, item_index) = handle.parts();
125        DiagnosticRedactionHandle { batch_id, item_index }
126    }
127
128    /// Redacts one domain value and returns its opaque batch handle.
129    ///
130    /// `value` is rendered only through its [`Redact`] implementation; the
131    /// result remains unpublished until
132    /// [`Self::finish_with_marker`] consumes this batch.
133    ///
134    /// # Type Parameters
135    ///
136    /// - `T`: Domain type exposing structured redaction.
137    ///
138    /// # Parameters
139    ///
140    /// - `value`: Borrowed domain value visited through structured redaction.
141    ///
142    /// # Returns
143    ///
144    /// An opaque handle to the recorded item, resolvable after this batch is
145    /// finished.
146    #[must_use]
147    #[inline(always)]
148    pub fn redact_value<T>(&mut self, value: &T) -> DiagnosticRedactionHandle
149    where
150        T: Redact + ?Sized,
151    {
152        let handle = self.session.redact_value(value);
153        let (batch_id, item_index) = handle.parts();
154        DiagnosticRedactionHandle { batch_id, item_index }
155    }
156
157    /// Redacts an explicitly classified argv sequence as one item.
158    ///
159    /// The finite `items` iterator is admitted under this batch's shared
160    /// resource budget before its values are inspected.
161    ///
162    /// # Type Parameters
163    ///
164    /// - `'items`: Lifetime of borrowed argument contents.
165    /// - `I`: One-pass iterator of argument items.
166    ///
167    /// # Parameters
168    ///
169    /// - `items`: Argument items visited once in source order.
170    ///
171    /// # Returns
172    ///
173    /// An opaque handle to the recorded item, resolvable after this batch is
174    /// finished.
175    #[must_use]
176    #[inline(always)]
177    pub fn redact_argv<'items, I>(&mut self, items: I) -> DiagnosticRedactionHandle
178    where
179        I: IntoIterator<Item = crate::formats::argv::ArgvItem<'items>>,
180    {
181        Self::wrap(self.session.redact_argv(items))
182    }
183
184    /// Redacts an argv sequence with heuristic option classification as one
185    /// item.
186    ///
187    /// The finite `items` iterator is admitted under this batch's shared
188    /// resource budget before its values are inspected.
189    ///
190    /// # Type Parameters
191    ///
192    /// - `'items`: Lifetime of borrowed argument contents.
193    /// - `I`: One-pass iterator of argument items.
194    ///
195    /// # Parameters
196    ///
197    /// - `items`: Argument items visited once in source order.
198    ///
199    /// # Returns
200    ///
201    /// An opaque handle to the recorded item, resolvable after this batch is
202    /// finished.
203    #[must_use]
204    #[inline(always)]
205    pub fn redact_heuristic_argv<'items, I>(&mut self, items: I) -> DiagnosticRedactionHandle
206    where
207        I: IntoIterator<Item = crate::formats::argv::ArgvItem<'items>>,
208    {
209        Self::wrap(self.session.redact_heuristic_argv(items))
210    }
211
212    /// Redacts one environment assignment as one item.
213    ///
214    /// `name` selects the environment rule applied to `value`.
215    ///
216    /// # Parameters
217    ///
218    /// - `name`: Environment name selecting the classification rule.
219    /// - `value`: Environment value admitted and transformed within the shared
220    ///   budget.
221    ///
222    /// # Returns
223    ///
224    /// An opaque handle to the recorded item, resolvable after this batch is
225    /// finished.
226    #[must_use]
227    #[inline(always)]
228    pub fn redact_env(&mut self, name: &str, value: &str) -> DiagnosticRedactionHandle {
229        Self::wrap(self.session.redact_env(name, value))
230    }
231
232    /// Redacts environment assignments as one item.
233    ///
234    /// The finite `pairs` iterator is admitted before the renderer observes
235    /// later entries.
236    ///
237    /// # Type Parameters
238    ///
239    /// - `'items`: Lifetime of borrowed names and values.
240    /// - `I`: One-pass iterator of environment assignments.
241    ///
242    /// # Parameters
243    ///
244    /// - `pairs`: Native environment names and values visited once in source
245    ///   order.
246    ///
247    /// # Returns
248    ///
249    /// An opaque handle to the recorded item, resolvable after this batch is
250    /// finished.
251    #[must_use]
252    #[inline(always)]
253    pub fn redact_env_pairs<'items, I>(&mut self, pairs: I) -> DiagnosticRedactionHandle
254    where
255        I: IntoIterator<Item = (&'items OsStr, &'items OsStr)>,
256    {
257        Self::wrap(self.session.redact_env_pairs(pairs))
258    }
259
260    /// Redacts one process command as one item.
261    ///
262    /// `program` precedes `arguments`; `variables` are rendered after argv
263    /// when the shared budget still admits them.
264    ///
265    /// # Type Parameters
266    ///
267    /// - `'arguments`: Borrow of the executable and arguments.
268    /// - `'variables`: Borrow of environment names and values.
269    /// - `A`: One-pass iterator of arguments.
270    /// - `E`: One-pass iterator of environment assignments.
271    ///
272    /// # Parameters
273    ///
274    /// - `program`: Native executable name preceding the arguments.
275    /// - `arguments`: Arguments visited once in command order.
276    /// - `variables`: Environment assignments visited after argv when admission
277    ///   remains open.
278    ///
279    /// # Returns
280    ///
281    /// An opaque handle to the recorded item, resolvable after this batch is
282    /// finished.
283    #[must_use]
284    #[inline(always)]
285    pub fn redact_process<'arguments, 'variables, A, E>(
286        &mut self,
287        program: &'arguments OsStr,
288        arguments: A,
289        variables: E,
290    ) -> DiagnosticRedactionHandle
291    where
292        A: IntoIterator<Item = crate::formats::argv::ArgvItem<'arguments>>,
293        E: IntoIterator<Item = (&'variables OsStr, &'variables OsStr)>,
294    {
295        Self::wrap(self.session.redact_process(program, arguments, variables))
296    }
297
298    /// Redacts one JSON document as one item.
299    ///
300    /// Invalid JSON produces a safe result carrying `InvalidJson` provenance.
301    ///
302    /// # Parameters
303    ///
304    /// - `text`: Raw JSON document admitted and parsed within the shared
305    ///   budget.
306    ///
307    /// # Returns
308    ///
309    /// An opaque handle to the recorded item, resolvable after this batch is
310    /// finished.
311    #[cfg(feature = "json")]
312    #[must_use]
313    #[inline(always)]
314    pub fn redact_json(&mut self, text: &str) -> DiagnosticRedactionHandle {
315        Self::wrap(self.session.redact_json(text))
316    }
317
318    /// Redacts a borrowed parsed JSON value without taking ownership of it.
319    ///
320    /// # Parameters
321    ///
322    /// - `value`: Parsed JSON value borrowed for admission and transformation.
323    ///
324    /// # Returns
325    ///
326    /// An opaque handle to the recorded item, resolvable after this batch is
327    /// finished.
328    #[cfg(feature = "json")]
329    #[must_use]
330    #[inline(always)]
331    pub fn redact_json_value(&mut self, value: &Value) -> DiagnosticRedactionHandle {
332        Self::wrap(self.session.redact_json_value(value))
333    }
334
335    /// Redacts one HTTP URL as one item.
336    ///
337    /// Invalid URLs produce a safe result carrying `InvalidUri` provenance.
338    ///
339    /// # Parameters
340    ///
341    /// - `value`: Raw HTTP URL admitted before parsing.
342    ///
343    /// # Returns
344    ///
345    /// An opaque handle to the recorded item, resolvable after this batch is
346    /// finished.
347    #[cfg(feature = "http")]
348    #[must_use]
349    #[inline(always)]
350    pub fn redact_http_url(&mut self, value: &str) -> DiagnosticRedactionHandle {
351        Self::wrap(self.session.redact_http_url(value))
352    }
353
354    /// Redacts one HTTP header map as one item.
355    ///
356    /// # Parameters
357    ///
358    /// - `headers`: Header collection including repeated values.
359    ///
360    /// # Returns
361    ///
362    /// An opaque handle to the recorded item, resolvable after this batch is
363    /// finished.
364    #[cfg(feature = "http")]
365    #[must_use]
366    #[inline(always)]
367    pub fn redact_http_headers(&mut self, headers: &HeaderMap) -> DiagnosticRedactionHandle {
368        Self::wrap(self.session.redact_http_headers(headers))
369    }
370
371    /// Redacts one captured HTTP body as one item using optional parsed
372    /// content-type metadata.
373    ///
374    /// # Parameters
375    ///
376    /// - `capture`: Captured bytes with their completeness metadata.
377    /// - `content_type`: Optional media type; None preserves missing-type
378    ///   policy behavior.
379    ///
380    /// # Returns
381    ///
382    /// An opaque handle to the recorded item, resolvable after this batch is
383    /// finished.
384    #[cfg(feature = "http")]
385    #[must_use]
386    #[inline(always)]
387    pub fn redact_http_body(
388        &mut self,
389        capture: BodyCapture<'_>,
390        content_type: Option<&HeaderValue>,
391    ) -> DiagnosticRedactionHandle {
392        Self::wrap(self.session.redact_http_body(capture, content_type))
393    }
394
395    /// Redacts one captured HTTP body using optional textual content-type
396    /// metadata.
397    ///
398    /// # Parameters
399    ///
400    /// - `capture`: Captured bytes with their completeness metadata.
401    /// - `content_type`: Optional media type; None preserves missing-type
402    ///   policy behavior.
403    ///
404    /// # Returns
405    ///
406    /// An opaque handle to the recorded item, resolvable after this batch is
407    /// finished.
408    #[cfg(feature = "http")]
409    #[must_use]
410    #[inline(always)]
411    pub fn redact_http_body_with_content_type_text(
412        &mut self,
413        capture: BodyCapture<'_>,
414        content_type: Option<&str>,
415    ) -> DiagnosticRedactionHandle {
416        Self::wrap(
417            self.session
418                .redact_http_body_with_content_type_text(capture, content_type),
419        )
420    }
421
422    /// Redacts one URI as one item.
423    ///
424    /// # Parameters
425    ///
426    /// - `value`: Raw URI admitted before parsing.
427    ///
428    /// # Returns
429    ///
430    /// An opaque handle to the recorded item, resolvable after this batch is
431    /// finished.
432    #[cfg(feature = "uri")]
433    #[must_use]
434    #[inline(always)]
435    pub fn redact_uri(&mut self, value: &str) -> DiagnosticRedactionHandle {
436        Self::wrap(self.session.redact_uri(value))
437    }
438
439    /// Consumes the batch and prepares fail-closed diagnostic presentation.
440    ///
441    /// Complete items retain their redacted text. Incomplete items and invalid
442    /// handles resolve to the escaped `marker` without returning an error.
443    ///
444    /// # Parameters
445    ///
446    /// - `marker`: Caller-selected fallback escaped once outside the batch’s
447    ///   output budget.
448    ///
449    /// # Returns
450    ///
451    /// A diagnostic publication resolving complete items to their text and all
452    /// others to the marker.
453    #[must_use]
454    #[inline(always)]
455    pub fn finish_with_marker(self, marker: &str) -> DiagnosticRedactionOutput {
456        DiagnosticRedactionOutput::new(self.finish_publication(), marker)
457    }
458
459    /// Consumes the batch and publishes fail-closed diagnostics using the
460    /// standard incomplete-redaction marker.
461    #[must_use]
462    #[inline(always)]
463    pub fn finish(self) -> DiagnosticRedactionOutput {
464        self.finish_with_marker(DEFAULT_DIAGNOSTIC_MARKER)
465    }
466
467    /// Consumes the batch and publishes its item results and summary.
468    ///
469    /// # Returns
470    ///
471    /// The crate-private publication owning all recorded items and aggregate
472    /// accounting.
473    #[must_use]
474    #[inline(always)]
475    pub(crate) fn finish_publication(self) -> DiagnosticRedactionBatchOutput {
476        DiagnosticRedactionBatchOutput::from_publication(self.session.finish())
477    }
478}