qubit_redact/http/http_redaction_policy_builder.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//! Builder for immutable HTTP redaction policy snapshots.
9
10use crate::{
11 DiagnosticBudget,
12 PolicyError,
13 RedactionPolicy,
14 RedactionPolicyBuilder,
15 Sensitivity,
16};
17
18use super::{
19 BodyBudget,
20 HttpRedactionPolicy,
21 TextBodyPolicy,
22 UnkeyedJsonValuePolicy,
23 UrlPathPolicy,
24};
25
26/// Mutable construction state for an [`HttpRedactionPolicy`].
27#[must_use]
28#[derive(Debug, Clone)]
29pub struct HttpRedactionPolicyBuilder {
30 /// Header-field policy construction state.
31 header: RedactionPolicyBuilder,
32 /// Query and form field-policy construction state.
33 query: RedactionPolicyBuilder,
34 /// Structured-body field-policy construction state.
35 body: RedactionPolicyBuilder,
36 /// Visibility choice for non-root URL paths.
37 url_path_policy: UrlPathPolicy,
38 /// Visibility choice for opaque UTF-8 text bodies.
39 text_body_policy: TextBodyPolicy,
40 /// Visibility choice for unkeyed JSON scalars.
41 unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
42 /// Finite body parser-input and log-output limits.
43 body_budget: BodyBudget,
44 /// Finite limits for HTTP diagnostics outside captured bodies.
45 diagnostic_budget: DiagnosticBudget,
46}
47
48impl HttpRedactionPolicyBuilder {
49 /// Creates a builder with empty field policies and default HTTP behavior.
50 ///
51 /// # Returns
52 ///
53 /// A builder with fail-closed behavior choices and finite default limits.
54 #[inline]
55 pub fn new() -> Self {
56 Self::empty()
57 }
58
59 /// Creates a builder with empty field policies and default HTTP behavior.
60 ///
61 /// # Returns
62 ///
63 /// A builder with fail-closed behavior choices and finite default limits.
64 #[inline]
65 pub(super) fn empty() -> Self {
66 Self {
67 header: RedactionPolicyBuilder::empty(),
68 query: RedactionPolicyBuilder::empty(),
69 body: RedactionPolicyBuilder::empty(),
70 url_path_policy: UrlPathPolicy::default(),
71 text_body_policy: TextBodyPolicy::default(),
72 unkeyed_json_value_policy: UnkeyedJsonValuePolicy::default(),
73 body_budget: BodyBudget::default(),
74 diagnostic_budget: DiagnosticBudget::default(),
75 }
76 }
77
78 /// Replaces this builder with the current default HTTP policy snapshot.
79 ///
80 /// # Returns
81 ///
82 /// A mutable copy of `HttpRedactionPolicy::default`.
83 ///
84 /// # Warning
85 ///
86 /// This replaces every builder component, including the header, query, and
87 /// body policies, behavior choices, and budgets. Call this method before
88 /// adding application-specific configuration.
89 #[inline]
90 pub fn load_default(self) -> Self {
91 Self::from_policy(&HttpRedactionPolicy::default())
92 }
93
94 /// Creates a builder by copying a complete immutable HTTP policy.
95 ///
96 /// # Parameters
97 ///
98 /// * `policy` - HTTP policy whose fields, behaviors, and budgets are
99 /// copied.
100 ///
101 /// # Returns
102 ///
103 /// Mutable construction state equivalent to `policy`.
104 #[inline]
105 pub fn from_policy(policy: &HttpRedactionPolicy) -> Self {
106 Self {
107 header: RedactionPolicy::builder_from(policy.header_policy()),
108 query: RedactionPolicy::builder_from(policy.query_policy()),
109 body: RedactionPolicy::builder_from(policy.body_policy()),
110 url_path_policy: policy.url_path_policy(),
111 text_body_policy: policy.text_body_policy(),
112 unkeyed_json_value_policy: policy.unkeyed_json_value_policy(),
113 body_budget: policy.body_budget(),
114 diagnostic_budget: policy.diagnostic_budget(),
115 }
116 }
117
118 /// Creates a builder with three mutable copies of `base`.
119 ///
120 /// # Parameters
121 ///
122 /// * `base` - Field policy copied for all three HTTP contexts.
123 ///
124 /// # Returns
125 ///
126 /// A builder with fail-closed behavior choices and finite default limits.
127 #[inline]
128 pub(super) fn from_base_policy(base: RedactionPolicy) -> Self {
129 Self {
130 header: RedactionPolicy::builder_from(&base),
131 query: RedactionPolicy::builder_from(&base),
132 body: RedactionPolicy::builder_from(&base),
133 url_path_policy: UrlPathPolicy::default(),
134 text_body_policy: TextBodyPolicy::default(),
135 unkeyed_json_value_policy: UnkeyedJsonValuePolicy::default(),
136 body_budget: BodyBudget::default(),
137 diagnostic_budget: base.diagnostic_budget(),
138 }
139 }
140
141 /// Replaces the header-field policy snapshot.
142 ///
143 /// # Parameters
144 ///
145 /// * `policy` - Immutable policy used for HTTP headers.
146 ///
147 /// # Returns
148 ///
149 /// The updated builder.
150 #[inline(always)]
151 pub fn header_policy(mut self, policy: RedactionPolicy) -> Self {
152 self.header = RedactionPolicy::builder_from(&policy);
153 self
154 }
155
156 /// Replaces the query and form field-policy snapshot.
157 ///
158 /// # Parameters
159 ///
160 /// * `policy` - Immutable policy used for query and form fields.
161 ///
162 /// # Returns
163 ///
164 /// The updated builder.
165 #[inline(always)]
166 pub fn query_policy(mut self, policy: RedactionPolicy) -> Self {
167 self.query = RedactionPolicy::builder_from(&policy);
168 self
169 }
170
171 /// Replaces the structured-body field-policy snapshot.
172 ///
173 /// # Parameters
174 ///
175 /// * `policy` - Immutable policy used for fields inside HTTP bodies.
176 ///
177 /// # Returns
178 ///
179 /// The updated builder.
180 #[inline]
181 pub fn body_policy(mut self, policy: RedactionPolicy) -> Self {
182 self.body = RedactionPolicy::builder_from(&policy);
183 self
184 }
185
186 /// Raises one header field to at least `level`.
187 ///
188 /// # Parameters
189 ///
190 /// * `name` - Header name to canonicalize.
191 /// * `level` - Minimum sensitivity level.
192 ///
193 /// # Returns
194 ///
195 /// The updated builder.
196 #[inline]
197 pub fn raise_header(mut self, name: &str, level: Sensitivity) -> Self {
198 self.header = self.header.raise(name, level);
199 self
200 }
201
202 /// Replaces one header field's sensitivity with `level`.
203 ///
204 /// # Parameters
205 ///
206 /// * `name` - Header name to canonicalize.
207 /// * `level` - Explicit replacement sensitivity.
208 ///
209 /// # Returns
210 ///
211 /// The updated builder.
212 #[inline]
213 pub fn override_header(mut self, name: &str, level: Sensitivity) -> Self {
214 self.header = self.header.override_level(name, level);
215 self
216 }
217
218 /// Allows one exact header name to remain visible.
219 ///
220 /// # Parameters
221 ///
222 /// * `name` - Exact header name to allow after canonicalization.
223 ///
224 /// # Returns
225 ///
226 /// The updated builder.
227 #[inline]
228 pub fn allow_header_exact(mut self, name: &str) -> Self {
229 self.header = self.header.allow_exact(name);
230 self
231 }
232
233 /// Allows one header name at token-suffix boundaries.
234 ///
235 /// # Parameters
236 ///
237 /// * `name` - Header suffix to allow after canonicalization.
238 ///
239 /// # Returns
240 ///
241 /// The updated builder.
242 #[inline]
243 pub fn allow_header_suffix(mut self, name: &str) -> Self {
244 self.header = self.header.allow_suffix(name);
245 self
246 }
247
248 /// Removes one exact header allow rule.
249 ///
250 /// # Parameters
251 ///
252 /// * `name` - Header name whose exact allow rule is removed.
253 ///
254 /// # Returns
255 ///
256 /// The updated builder.
257 #[inline]
258 pub fn remove_header_allow_exact(mut self, name: &str) -> Self {
259 self.header = self.header.remove_allow_exact(name);
260 self
261 }
262
263 /// Removes one token-suffix header allow rule.
264 ///
265 /// # Parameters
266 ///
267 /// * `name` - Header suffix whose allow rule is removed.
268 ///
269 /// # Returns
270 ///
271 /// The updated builder.
272 #[inline]
273 pub fn remove_header_allow_suffix(mut self, name: &str) -> Self {
274 self.header = self.header.remove_allow_suffix(name);
275 self
276 }
277
278 /// Removes every header allow rule.
279 ///
280 /// # Returns
281 ///
282 /// The updated builder.
283 #[inline]
284 pub fn clear_header_allow_rules(mut self) -> Self {
285 self.header = self.header.clear_allow_rules();
286 self
287 }
288
289 /// Raises one query or form field to at least `level`.
290 ///
291 /// # Parameters
292 ///
293 /// * `name` - Query field name to canonicalize.
294 /// * `level` - Minimum sensitivity level.
295 ///
296 /// # Returns
297 ///
298 /// The updated builder.
299 #[inline]
300 pub fn raise_query(mut self, name: &str, level: Sensitivity) -> Self {
301 self.query = self.query.raise(name, level);
302 self
303 }
304
305 /// Replaces one query or form field's sensitivity with `level`.
306 ///
307 /// # Parameters
308 ///
309 /// * `name` - Query field name to canonicalize.
310 /// * `level` - Explicit replacement sensitivity.
311 ///
312 /// # Returns
313 ///
314 /// The updated builder.
315 #[inline]
316 pub fn override_query(mut self, name: &str, level: Sensitivity) -> Self {
317 self.query = self.query.override_level(name, level);
318 self
319 }
320
321 /// Allows one exact query or form field name to remain visible.
322 ///
323 /// # Parameters
324 ///
325 /// * `name` - Exact query field name to allow after canonicalization.
326 ///
327 /// # Returns
328 ///
329 /// The updated builder.
330 #[inline]
331 pub fn allow_query_exact(mut self, name: &str) -> Self {
332 self.query = self.query.allow_exact(name);
333 self
334 }
335
336 /// Allows one query or form field at token-suffix boundaries.
337 ///
338 /// # Parameters
339 ///
340 /// * `name` - Query field suffix to allow after canonicalization.
341 ///
342 /// # Returns
343 ///
344 /// The updated builder.
345 #[inline]
346 pub fn allow_query_suffix(mut self, name: &str) -> Self {
347 self.query = self.query.allow_suffix(name);
348 self
349 }
350
351 /// Removes one exact query or form-field allow rule.
352 ///
353 /// # Parameters
354 ///
355 /// * `name` - Query or form-field name whose exact allow rule is removed.
356 ///
357 /// # Returns
358 ///
359 /// The updated builder.
360 #[inline]
361 pub fn remove_query_allow_exact(mut self, name: &str) -> Self {
362 self.query = self.query.remove_allow_exact(name);
363 self
364 }
365
366 /// Removes one token-suffix query or form-field allow rule.
367 ///
368 /// # Parameters
369 ///
370 /// * `name` - Query or form-field suffix whose allow rule is removed.
371 ///
372 /// # Returns
373 ///
374 /// The updated builder.
375 #[inline]
376 pub fn remove_query_allow_suffix(mut self, name: &str) -> Self {
377 self.query = self.query.remove_allow_suffix(name);
378 self
379 }
380
381 /// Removes every query and form-field allow rule.
382 ///
383 /// # Returns
384 ///
385 /// The updated builder.
386 #[inline]
387 pub fn clear_query_allow_rules(mut self) -> Self {
388 self.query = self.query.clear_allow_rules();
389 self
390 }
391
392 /// Raises one structured-body field to at least `level`.
393 ///
394 /// # Parameters
395 ///
396 /// * `name` - Body field name to canonicalize.
397 /// * `level` - Minimum sensitivity level.
398 ///
399 /// # Returns
400 ///
401 /// The updated builder.
402 #[inline]
403 pub fn raise_body(mut self, name: &str, level: Sensitivity) -> Self {
404 self.body = self.body.raise(name, level);
405 self
406 }
407
408 /// Replaces one structured-body field's sensitivity with `level`.
409 ///
410 /// # Parameters
411 ///
412 /// * `name` - Body field name to canonicalize.
413 /// * `level` - Explicit replacement sensitivity.
414 ///
415 /// # Returns
416 ///
417 /// The updated builder.
418 #[inline]
419 pub fn override_body(mut self, name: &str, level: Sensitivity) -> Self {
420 self.body = self.body.override_level(name, level);
421 self
422 }
423
424 /// Allows one exact structured-body field name to remain visible.
425 ///
426 /// # Parameters
427 ///
428 /// * `name` - Exact body field name to allow after canonicalization.
429 ///
430 /// # Returns
431 ///
432 /// The updated builder.
433 #[inline]
434 pub fn allow_body_exact(mut self, name: &str) -> Self {
435 self.body = self.body.allow_exact(name);
436 self
437 }
438
439 /// Allows one structured-body field at token-suffix boundaries.
440 ///
441 /// # Parameters
442 ///
443 /// * `name` - Body field suffix to allow after canonicalization.
444 ///
445 /// # Returns
446 ///
447 /// The updated builder.
448 #[inline]
449 pub fn allow_body_suffix(mut self, name: &str) -> Self {
450 self.body = self.body.allow_suffix(name);
451 self
452 }
453
454 /// Removes one exact structured-body allow rule.
455 ///
456 /// # Parameters
457 ///
458 /// * `name` - Body-field name whose exact allow rule is removed.
459 ///
460 /// # Returns
461 ///
462 /// The updated builder.
463 #[inline]
464 pub fn remove_body_allow_exact(mut self, name: &str) -> Self {
465 self.body = self.body.remove_allow_exact(name);
466 self
467 }
468
469 /// Removes one token-suffix structured-body allow rule.
470 ///
471 /// # Parameters
472 ///
473 /// * `name` - Body-field suffix whose allow rule is removed.
474 ///
475 /// # Returns
476 ///
477 /// The updated builder.
478 #[inline]
479 pub fn remove_body_allow_suffix(mut self, name: &str) -> Self {
480 self.body = self.body.remove_allow_suffix(name);
481 self
482 }
483
484 /// Removes every structured-body allow rule.
485 ///
486 /// # Returns
487 ///
488 /// The updated builder.
489 #[inline]
490 pub fn clear_body_allow_rules(mut self) -> Self {
491 self.body = self.body.clear_allow_rules();
492 self
493 }
494
495 /// Replaces the URL path visibility choice.
496 ///
497 /// # Parameters
498 ///
499 /// * `policy` - Visibility behavior for non-root URL paths.
500 ///
501 /// # Returns
502 ///
503 /// The updated builder.
504 #[inline]
505 pub const fn url_path_policy(mut self, policy: UrlPathPolicy) -> Self {
506 self.url_path_policy = policy;
507 self
508 }
509
510 /// Replaces the opaque text-body visibility choice.
511 ///
512 /// # Parameters
513 ///
514 /// * `policy` - Visibility behavior for opaque UTF-8 body text.
515 ///
516 /// # Returns
517 ///
518 /// The updated builder.
519 #[inline]
520 pub const fn text_body_policy(mut self, policy: TextBodyPolicy) -> Self {
521 self.text_body_policy = policy;
522 self
523 }
524
525 /// Replaces the unkeyed JSON scalar visibility choice.
526 ///
527 /// # Parameters
528 ///
529 /// * `policy` - Visibility behavior for JSON values without field names.
530 ///
531 /// # Returns
532 ///
533 /// The updated builder.
534 #[inline]
535 pub const fn unkeyed_json_value_policy(
536 mut self,
537 policy: UnkeyedJsonValuePolicy,
538 ) -> Self {
539 self.unkeyed_json_value_policy = policy;
540 self
541 }
542
543 /// Replaces the finite hard body limits.
544 ///
545 /// # Parameters
546 ///
547 /// * `budget` - Previously checked parser-input and output byte limits.
548 ///
549 /// # Returns
550 ///
551 /// The updated builder.
552 #[inline(always)]
553 pub const fn body_budget(mut self, budget: BodyBudget) -> Self {
554 self.body_budget = budget;
555 self
556 }
557
558 /// Replaces the finite hard HTTP diagnostic limits.
559 ///
560 /// # Parameters
561 ///
562 /// * `budget` - Previously checked diagnostic input and output byte limits.
563 ///
564 /// # Returns
565 ///
566 /// The updated builder.
567 #[inline(always)]
568 pub fn diagnostic_budget(mut self, budget: DiagnosticBudget) -> Self {
569 self.diagnostic_budget = budget;
570 self
571 }
572
573 /// Validates all field rules and builds the complete HTTP policy.
574 ///
575 /// # Returns
576 ///
577 /// A complete immutable HTTP policy snapshot.
578 ///
579 /// # Errors
580 ///
581 /// Returns the first [`PolicyError`] found while validating the header,
582 /// query, and body policy builders in that order.
583 pub fn build(self) -> Result<HttpRedactionPolicy, PolicyError> {
584 let header = self.header.build()?;
585 let query = self.query.build()?;
586 let body = self.body.build()?;
587 Ok(HttpRedactionPolicy::from_parts(
588 header,
589 query,
590 body,
591 self.url_path_policy,
592 self.text_body_policy,
593 self.unkeyed_json_value_policy,
594 self.body_budget,
595 )
596 .with_diagnostic_budget(self.diagnostic_budget))
597 }
598}
599
600impl Default for HttpRedactionPolicyBuilder {
601 /// Creates the same empty construction state as [`Self::new`].
602 ///
603 /// # Returns
604 ///
605 /// A builder with empty field policies and default HTTP behavior.
606 fn default() -> Self {
607 Self::new()
608 }
609}