qubit_redact/facade/redaction_usage.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//! Resource accounting for one redaction transaction.
9
10/// Measured resource use for one redaction transaction.
11///
12/// # Examples
13///
14/// ```
15/// use qubit_redact::Redactor;
16///
17/// let output = Redactor::standard().redact_field("id", "42");
18/// assert_eq!(output.summary().usage().inspected_input_bytes(), 4);
19/// assert_eq!(output.summary().usage().output_bytes(), 2);
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct RedactionUsage {
23 /// Bytes presented at public input boundaries.
24 presented_input_bytes: usize,
25 /// Presented bytes admitted for inspection.
26 inspected_input_bytes: usize,
27 /// Escaped bytes retained in final output.
28 output_bytes: usize,
29 /// Structural nodes admitted during traversal.
30 visited_nodes: usize,
31 /// Sequence and map items admitted during traversal.
32 visited_collection_items: usize,
33 /// Greatest active structural depth observed.
34 max_depth: usize,
35 /// Known bytes omitted at admission boundaries.
36 omitted_input_bytes: Option<usize>,
37}
38
39impl Default for RedactionUsage {
40 /// Creates the empty resource measurement used by a fresh transaction.
41 ///
42 /// # Returns
43 ///
44 /// An empty measurement with all counters zero and known zero omissions.
45 #[inline(always)]
46 fn default() -> Self {
47 Self::empty()
48 }
49}
50
51impl RedactionUsage {
52 /// Creates an empty measurement for a newly started transaction.
53 ///
54 /// # Returns
55 ///
56 /// An empty measurement with all counters zero and known zero omissions.
57 #[must_use]
58 #[inline(always)]
59 pub const fn empty() -> Self {
60 Self {
61 presented_input_bytes: 0,
62 inspected_input_bytes: 0,
63 output_bytes: 0,
64 visited_nodes: 0,
65 visited_collection_items: 0,
66 max_depth: 0,
67 omitted_input_bytes: Some(0),
68 }
69 }
70
71 /// Returns the bytes supplied by callers before input admission.
72 ///
73 /// # Returns
74 ///
75 /// Bytes presented at admitted or rejected public input boundaries.
76 #[must_use]
77 #[inline(always)]
78 pub const fn presented_input_bytes(self) -> usize {
79 self.presented_input_bytes
80 }
81
82 /// Returns the bytes the transaction actually inspected.
83 ///
84 /// # Returns
85 ///
86 /// Source bytes successfully admitted for inspection, even if later
87 /// discarded.
88 #[must_use]
89 #[inline(always)]
90 pub const fn inspected_input_bytes(self) -> usize {
91 self.inspected_input_bytes
92 }
93
94 /// Returns the final escaped bytes retained by the transaction.
95 ///
96 /// # Returns
97 ///
98 /// Escaped bytes retained in the completed text or batch output.
99 #[must_use]
100 #[inline(always)]
101 pub const fn output_bytes(self) -> usize {
102 self.output_bytes
103 }
104
105 /// Returns the admitted domain or format nodes visited by the transaction.
106 ///
107 /// # Returns
108 ///
109 /// Structural nodes successfully admitted during traversal.
110 #[must_use]
111 #[inline(always)]
112 pub const fn visited_nodes(self) -> usize {
113 self.visited_nodes
114 }
115
116 /// Returns the admitted collection items visited by the transaction.
117 ///
118 /// # Returns
119 ///
120 /// Sequence and map entries successfully admitted during traversal.
121 #[must_use]
122 #[inline(always)]
123 pub const fn visited_collection_items(self) -> usize {
124 self.visited_collection_items
125 }
126
127 /// Returns the greatest active structural depth observed by the
128 /// transaction.
129 ///
130 /// # Returns
131 ///
132 /// The greatest active structural depth, or zero when no node was admitted.
133 #[must_use]
134 #[inline(always)]
135 pub const fn max_depth(self) -> usize {
136 self.max_depth
137 }
138
139 /// Returns omitted source bytes when the source length is known.
140 ///
141 /// # Returns
142 ///
143 /// `Some(bytes)` counts known omissions; `None` means at least one source
144 /// had unknown total length, so the aggregate omission is unknown.
145 #[must_use]
146 #[inline(always)]
147 pub const fn omitted_input_bytes(self) -> Option<usize> {
148 self.omitted_input_bytes
149 }
150
151 /// Adds bytes written to the final output buffer.
152 ///
153 /// # Parameters
154 ///
155 /// - `bytes`: Additional retained output bytes.
156 ///
157 /// # Returns
158 ///
159 /// A copy with output bytes added using saturating arithmetic.
160 #[must_use]
161 #[inline(always)]
162 pub(crate) const fn with_added_output_bytes(mut self, bytes: usize) -> Self {
163 self.output_bytes = self.output_bytes.saturating_add(bytes);
164 self
165 }
166
167 /// Records input supplied to and, when admitted, inspected by an adapter.
168 ///
169 /// # Parameters
170 ///
171 /// - `presented`: Additional source bytes presented at the boundary.
172 /// - `inspected`: Additional source bytes admitted for inspection.
173 ///
174 /// # Returns
175 ///
176 /// A copy with cumulative byte counts and known omissions updated;
177 /// previously unknown omissions remain unknown.
178 #[must_use]
179 #[inline]
180 pub(crate) const fn with_input(mut self, presented: usize, inspected: usize) -> Self {
181 self.presented_input_bytes = self.presented_input_bytes.saturating_add(presented);
182 self.inspected_input_bytes = self.inspected_input_bytes.saturating_add(inspected);
183 self.omitted_input_bytes = match self.omitted_input_bytes {
184 Some(omitted) => Some(omitted.saturating_add(presented.saturating_sub(inspected))),
185 None => None,
186 };
187 self
188 }
189
190 /// Records input whose omitted-byte count is supplied by the source.
191 ///
192 /// # Parameters
193 ///
194 /// - `presented`: Additional source bytes presented at the boundary.
195 /// - `inspected`: Additional source bytes admitted for inspection.
196 /// - `omitted`: Some known omitted bytes, or None when the total source
197 /// length is unknown.
198 ///
199 /// # Returns
200 ///
201 /// A copy with cumulative input counts and source-reported omissions;
202 /// any unknown omission makes the aggregate unknown.
203 #[cfg(feature = "http")]
204 #[must_use]
205 #[inline]
206 pub(crate) const fn with_source_input(
207 mut self,
208 presented: usize,
209 inspected: usize,
210 omitted: Option<usize>,
211 ) -> Self {
212 self.presented_input_bytes = self.presented_input_bytes.saturating_add(presented);
213 self.inspected_input_bytes = self.inspected_input_bytes.saturating_add(inspected);
214 self.omitted_input_bytes = match (self.omitted_input_bytes, omitted) {
215 (Some(previous), Some(current)) => Some(previous.saturating_add(current)),
216 _ => None,
217 };
218 self
219 }
220
221 /// Records one admitted structural node.
222 ///
223 /// # Parameters
224 ///
225 /// - `depth`: Active structural depth of the newly admitted node.
226 ///
227 /// # Returns
228 ///
229 /// A copy counting one additional node and retaining the greatest depth.
230 #[must_use]
231 #[inline]
232 pub(crate) const fn with_domain_node(mut self, depth: usize) -> Self {
233 self.visited_nodes = self.visited_nodes.saturating_add(1);
234 self.max_depth = if self.max_depth > depth { self.max_depth } else { depth };
235 self
236 }
237
238 /// Records one admitted collection item.
239 ///
240 /// # Returns
241 ///
242 /// A copy counting one additional collection item using saturating
243 /// arithmetic.
244 #[must_use]
245 #[inline(always)]
246 pub(crate) const fn with_collection_item(mut self) -> Self {
247 self.visited_collection_items = self.visited_collection_items.saturating_add(1);
248 self
249 }
250}