Skip to main content

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}