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}