Skip to main content

qubit_redact/policy/
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//! Mutable builder for immutable redaction policies.
9
10use super::FieldNameMatching;
11use super::MaskPolicy;
12use super::MaskingPolicy;
13use super::PolicyError;
14use super::PolicyLocation;
15use super::RedactionFloor;
16use super::RedactionLimits;
17use super::RedactionLimitsBuilder;
18use super::RedactionPolicy;
19use super::RedactionRules;
20use super::RedactionRulesBuilder;
21use super::SensitiveFieldPreset;
22use super::Sensitivity;
23#[cfg(feature = "json")]
24use super::UnkeyedJsonValuePolicy;
25use super::UnknownFieldPolicy;
26
27/// Mutable construction state for an immutable [`RedactionPolicy`].
28///
29/// Configuration setters are available through the grouped views returned by
30/// [`Self::fields`] and [`Self::limits`], with feature-specific HTTP and URI
31/// views when those formats are enabled.
32/// Duplicate consuming setters are intentionally not available at this level:
33///
34/// ```compile_fail
35/// use qubit_redact::{RedactionPolicy, Sensitivity};
36///
37/// let _ = RedactionPolicy::builder().raise("token", Sensitivity::Secret);
38/// ```
39///
40/// # Examples
41///
42/// ```
43/// use qubit_redact::RedactionPolicy;
44///
45/// let policy = RedactionPolicy::builder()
46///     .fields(|fields| {
47///         let _ = fields.secret_sensitive("api_token");
48///     })
49///     .expect("valid field rules")
50///     .build()
51///     .expect("valid policy");
52/// assert!(policy.sensitivity_for("api_token").is_some());
53/// ```
54#[derive(Debug, Clone)]
55pub struct RedactionPolicyBuilder {
56    /// Whether the resulting policy bypasses redaction.
57    disabled: bool,
58    /// Mutable application field rules.
59    rules: RedactionRulesBuilder,
60    /// Shared masking strategies by sensitivity.
61    masking: MaskingPolicy,
62    /// Optional minimum-protection floor.
63    floor: Option<RedactionFloor>,
64    /// Resource limits for each transaction.
65    limits: RedactionLimits,
66    /// Mutable HTTP-specific policy state.
67    #[cfg(feature = "http")]
68    http: crate::formats::http::HttpPolicyBuilder,
69    /// Mutable URI-specific policy state.
70    #[cfg(feature = "uri")]
71    uri: crate::formats::uri::UriPolicyBuilder,
72    /// Handling for JSON scalars without a field key.
73    #[cfg(feature = "json")]
74    unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
75}
76
77impl RedactionPolicyBuilder {
78    /// Creates an empty application-rule builder with the standard floor.
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            disabled: false,
83            rules: RedactionRulesBuilder::empty(PolicyLocation::Rules),
84            masking: MaskingPolicy::default(),
85            floor: Some(RedactionFloor::standard()),
86            limits: RedactionLimits::default(),
87            #[cfg(feature = "http")]
88            http: crate::formats::http::HttpPolicyBuilder::new(),
89            #[cfg(feature = "uri")]
90            uri: crate::formats::uri::UriPolicyBuilder::new(),
91            #[cfg(feature = "json")]
92            unkeyed_json_value_policy: UnkeyedJsonValuePolicy::PassThrough,
93        }
94    }
95    /// Copies the immutable policy into mutable builder state.
96    #[must_use]
97    pub(super) fn from_policy(policy: &RedactionPolicy) -> Self {
98        Self {
99            disabled: policy.is_disabled(),
100            rules: RedactionRulesBuilder::from_inner(&policy.rules().clone_application(), PolicyLocation::Rules),
101            masking: policy.masking().clone(),
102            floor: policy.rules().floor().cloned(),
103            limits: *policy.limits(),
104            #[cfg(feature = "http")]
105            http: crate::formats::http::HttpPolicyBuilder::from_policy(policy.http()),
106            #[cfg(feature = "uri")]
107            uri: crate::formats::uri::UriPolicyBuilder::from_policy(policy.uri()),
108            #[cfg(feature = "json")]
109            unkeyed_json_value_policy: policy.unkeyed_json_value_policy(),
110        }
111    }
112
113    /// Configures base field sensitivity rules transactionally.
114    ///
115    /// The closure writes into a temporary field draft. Field names are
116    /// validated after the closure returns and the draft is applied only when
117    /// every field is valid. The configuration methods inside the closure are
118    /// therefore infallible and can be chained without a trailing `Ok(())`.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`PolicyError`] when a field name is empty after
123    /// canonicalization. The builder remains unchanged on error.
124    pub fn fields<F>(self, configure: F) -> Result<Self, PolicyError>
125    where
126        F: FnOnce(&mut FieldsBuilder<'_>),
127    {
128        let mut draft = self.clone();
129        let error = {
130            let mut fields = FieldsBuilder {
131                builder: &mut draft,
132                error: None,
133            };
134            configure(&mut fields);
135            fields.error.take()
136        };
137        if let Some(error) = error {
138            return Err(error);
139        }
140        Ok(draft)
141    }
142
143    /// Configures HTTP policy through an isolated draft.
144    ///
145    /// The draft replaces this namespace only after the closure returns, so a
146    /// failed build never partially updates the caller's builder.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`PolicyError`] when the HTTP view records an invalid field
151    /// rule. The original builder remains unchanged.
152    #[cfg(feature = "http")]
153    pub fn http<F>(self, configure: F) -> Result<Self, PolicyError>
154    where
155        F: FnOnce(&mut HttpPolicyBuilderView<'_>),
156    {
157        let mut draft = self.clone();
158        let error = {
159            let mut view = HttpPolicyBuilderView {
160                builder: &mut draft,
161                error: None,
162            };
163            configure(&mut view);
164            view.error.take()
165        };
166        if let Some(error) = error {
167            return Err(error);
168        }
169        Ok(draft)
170    }
171
172    /// Configures URI policy through an isolated draft.
173    ///
174    /// # Errors
175    ///
176    /// This operation is currently infallible. The result preserves the
177    /// transactional shape shared by feature-specific builder views.
178    #[cfg(feature = "uri")]
179    pub fn uri<F>(self, configure: F) -> Result<Self, PolicyError>
180    where
181        F: FnOnce(&mut UriPolicyBuilderView<'_>),
182    {
183        let mut draft = self.clone();
184        let mut view = UriPolicyBuilderView {
185            builder: &mut draft.uri,
186        };
187        configure(&mut view);
188        Ok(draft)
189    }
190
191    /// Configures transaction limits through a draft that is applied
192    /// atomically.
193    ///
194    /// The closure mutates only a temporary limits builder. Once it returns,
195    /// the completed limits replace this builder's limits as one update.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`PolicyError`] when the completed limit set violates a
200    /// cross-limit invariant. The original builder remains unchanged.
201    pub fn limits<F>(mut self, configure: F) -> Result<Self, PolicyError>
202    where
203        F: FnOnce(&mut RedactionLimitsBuilder),
204    {
205        let mut limits = RedactionLimits::builder_from(&self.limits);
206        configure(&mut limits);
207        let limits = limits.build();
208        limits.validate()?;
209        self.limits = limits;
210        Ok(self)
211    }
212
213    /// Validates that `field` has a non-empty canonical application-rule name.
214    ///
215    /// # Errors
216    ///
217    /// Returns [`PolicyError::EmptyFieldName`] at
218    /// [`PolicyLocation::Rules`] when canonicalization leaves no name.
219    pub fn validate_field_name(field: &str) -> Result<(), PolicyError> {
220        RedactionRulesBuilder::validate_field_name(field, PolicyLocation::Rules)
221    }
222
223    /// Sets behavior for root and array JSON scalar values.
224    ///
225    /// This setter remains on the root builder because the JSON feature does
226    /// not expose a separate grouped builder.
227    #[cfg(feature = "json")]
228    #[must_use]
229    pub const fn unkeyed_json_value_policy(mut self, policy: UnkeyedJsonValuePolicy) -> Self {
230        self.unkeyed_json_value_policy = policy;
231        self
232    }
233
234    /// Validates and returns the immutable policy snapshot.
235    ///
236    /// # Errors
237    ///
238    /// Returns a [`PolicyError`] when final policy validation fails.
239    pub fn build(self) -> Result<RedactionPolicy, PolicyError> {
240        let rules = RedactionRules::new(self.rules.build_inner()?, self.floor);
241        self.masking.validate(PolicyLocation::Rules)?;
242        #[cfg(feature = "http")]
243        let http = self.http.build()?;
244        #[cfg(feature = "uri")]
245        let uri = self.uri.build()?;
246        Ok(RedactionPolicy::from_rules(
247            rules,
248            self.masking,
249            self.limits,
250            #[cfg(feature = "http")]
251            http,
252            #[cfg(feature = "uri")]
253            uri,
254            #[cfg(feature = "json")]
255            self.unkeyed_json_value_policy,
256            self.disabled,
257        ))
258    }
259}
260
261mod views {
262    use super::FieldNameMatching;
263    use super::MaskPolicy;
264    use super::MaskingPolicy;
265    use super::PolicyError;
266    use super::PolicyLocation;
267    use super::RedactionFloor;
268    use super::RedactionPolicyBuilder;
269    #[cfg(feature = "http")]
270    use super::RedactionRules;
271    use super::SensitiveFieldPreset;
272    use super::Sensitivity;
273    use super::UnknownFieldPolicy;
274
275    /// Mutable view over the base field policy.
276    pub struct FieldsBuilder<'a> {
277        /// Root builder receiving validated field changes.
278        pub(super) builder: &'a mut RedactionPolicyBuilder,
279        /// First validation error recorded by the transactional view.
280        pub(super) error: Option<PolicyError>,
281    }
282
283    impl FieldsBuilder<'_> {
284        /// Raises one field's minimum sensitivity in a transactional draft.
285        #[inline(always)]
286        fn set_sensitive(&mut self, field: &str, level: Sensitivity) -> &mut Self {
287            if self.error.is_none()
288                && let Err(error) = self.builder.rules.raise(field, level)
289            {
290                self.error = Some(error);
291            }
292            self
293        }
294
295        /// Marks a field as low sensitivity.
296        #[inline(always)]
297        pub fn low_sensitive(&mut self, field: &str) -> &mut Self {
298            self.set_sensitive(field, Sensitivity::Low)
299        }
300
301        /// Marks a field as medium sensitivity.
302        #[inline(always)]
303        pub fn medium_sensitive(&mut self, field: &str) -> &mut Self {
304            self.set_sensitive(field, Sensitivity::Medium)
305        }
306
307        /// Marks a field as high sensitivity.
308        #[inline(always)]
309        pub fn high_sensitive(&mut self, field: &str) -> &mut Self {
310            self.set_sensitive(field, Sensitivity::High)
311        }
312
313        /// Marks a field as secret sensitivity.
314        #[inline(always)]
315        pub fn secret_sensitive(&mut self, field: &str) -> &mut Self {
316            self.set_sensitive(field, Sensitivity::Secret)
317        }
318
319        /// Raises a field's minimum sensitivity to `level`.
320        #[inline(always)]
321        pub fn sensitive(&mut self, level: Sensitivity, field: &str) -> &mut Self {
322            self.set_sensitive(field, level)
323        }
324
325        /// Sets field-name matching for the base policy.
326        #[inline(always)]
327        pub fn matching(&mut self, matching: FieldNameMatching) -> &mut Self {
328            self.builder.rules.matching(matching);
329            self
330        }
331
332        /// Sets the base fallback for unknown fields.
333        pub fn unknown_field_policy(&mut self, policy: UnknownFieldPolicy) -> &mut Self {
334            self.builder.rules.unknown_field_policy(policy);
335            self
336        }
337
338        /// Includes all fields from a built-in sensitive preset.
339        pub fn include_preset(&mut self, preset: SensitiveFieldPreset) -> &mut Self {
340            self.builder.rules.include_preset(preset);
341            self
342        }
343
344        /// Raises a base field's minimum sensitivity.
345        pub fn raise(&mut self, field: &str, level: Sensitivity) -> &mut Self {
346            if self.error.is_none()
347                && let Err(error) = self.builder.rules.raise(field, level)
348            {
349                self.error = Some(error);
350            }
351            self
352        }
353
354        /// Replaces one base field rule without weakening floors.
355        pub fn override_level(&mut self, field: &str, level: Sensitivity) -> &mut Self {
356            if self.error.is_none()
357                && let Err(error) = self.builder.rules.override_level(field, level)
358            {
359                self.error = Some(error);
360            }
361            self
362        }
363
364        /// Adds a base exact allow rule.
365        pub fn allow_exact(&mut self, field: &str) -> &mut Self {
366            if self.error.is_none()
367                && let Err(error) = self.builder.rules.allow_canonical_exact(field)
368            {
369                self.error = Some(error);
370            }
371            self
372        }
373
374        /// Adds a base suffix allow rule.
375        pub fn allow_suffix(&mut self, field: &str) -> &mut Self {
376            if self.error.is_none()
377                && let Err(error) = self.builder.rules.allow_suffix(field)
378            {
379                self.error = Some(error);
380            }
381            self
382        }
383
384        /// Removes a base exact allow rule.
385        pub fn remove_allow_exact(&mut self, field: &str) -> &mut Self {
386            if self.error.is_none()
387                && let Err(error) = self.builder.rules.remove_allow_canonical_exact(field)
388            {
389                self.error = Some(error);
390            }
391            self
392        }
393
394        /// Removes a base suffix allow rule.
395        pub fn remove_allow_suffix(&mut self, field: &str) -> &mut Self {
396            if self.error.is_none()
397                && let Err(error) = self.builder.rules.remove_allow_suffix(field)
398            {
399                self.error = Some(error);
400            }
401            self
402        }
403
404        /// Removes all base allow rules.
405        pub fn clear_allow_rules(&mut self) -> &mut Self {
406            self.builder.rules.clear_allow_rules();
407            self
408        }
409
410        /// Replaces the base minimum-protection floor.
411        #[inline(always)]
412        pub fn floor(&mut self, floor: RedactionFloor) -> &mut Self {
413            self.builder.floor = Some(floor);
414            self
415        }
416
417        /// Disables the base floor explicitly.
418        pub fn disable_floor(&mut self) -> &mut Self {
419            self.builder.floor = None;
420            self
421        }
422
423        /// Replaces one shared masking level.
424        pub fn mask(&mut self, level: Sensitivity, policy: MaskPolicy) -> &mut Self {
425            let mut masking = MaskingPolicy::builder_from(&self.builder.masking);
426            masking.policy(level, policy);
427            let masking = masking.build();
428            if self.error.is_none() {
429                match masking.validate(PolicyLocation::Rules) {
430                    Ok(()) => self.builder.masking = masking,
431                    Err(error) => self.error = Some(error),
432                }
433            }
434            self
435        }
436    }
437
438    /// Mutable view over all HTTP context differences.
439    #[cfg(feature = "http")]
440    pub struct HttpPolicyBuilderView<'a> {
441        /// Root builder receiving HTTP policy changes.
442        pub(super) builder: &'a mut RedactionPolicyBuilder,
443        /// First validation error recorded by the transactional view.
444        pub(super) error: Option<PolicyError>,
445    }
446
447    /// Mutable view over URI-specific behavior.
448    #[cfg(feature = "uri")]
449    pub struct UriPolicyBuilderView<'a> {
450        /// URI builder receiving view changes.
451        pub(super) builder: &'a mut crate::formats::uri::UriPolicyBuilder,
452    }
453
454    #[cfg(feature = "uri")]
455    impl UriPolicyBuilderView<'_> {
456        /// Sets URI path visibility.
457        pub fn path(&mut self, policy: crate::formats::uri::UriPathPolicy) -> &mut Self {
458            self.builder.path_policy_mut(policy);
459            self
460        }
461
462        /// Sets URI fragment visibility.
463        pub fn fragment(&mut self, policy: crate::formats::uri::UriFragmentPolicy) -> &mut Self {
464            self.builder.fragment_policy_mut(policy);
465            self
466        }
467    }
468
469    #[cfg(feature = "http")]
470    impl HttpPolicyBuilderView<'_> {
471        /// Returns the header context view.
472        #[must_use]
473        pub fn header(&mut self) -> HttpContextBuilderView<'_> {
474            HttpContextBuilderView {
475                builder: &mut self.builder.http,
476                error: &mut self.error,
477                context: crate::formats::http::HttpFieldContext::Header,
478            }
479        }
480
481        /// Returns the query/form context view.
482        #[must_use]
483        pub fn query(&mut self) -> HttpContextBuilderView<'_> {
484            HttpContextBuilderView {
485                builder: &mut self.builder.http,
486                error: &mut self.error,
487                context: crate::formats::http::HttpFieldContext::Query,
488            }
489        }
490
491        /// Returns the structured-body context view.
492        #[must_use]
493        pub fn body(&mut self) -> HttpContextBuilderView<'_> {
494            HttpContextBuilderView {
495                builder: &mut self.builder.http,
496                error: &mut self.error,
497                context: crate::formats::http::HttpFieldContext::Body,
498            }
499        }
500
501        /// Sets URL path visibility for HTTP diagnostics.
502        pub fn url_path(&mut self, policy: crate::formats::http::UrlPathPolicy) -> &mut Self {
503            self.builder.http.url_path_mut(policy);
504            self
505        }
506
507        /// Sets opaque text-body visibility for HTTP diagnostics.
508        pub fn text_body(&mut self, policy: crate::formats::http::TextBodyPolicy) -> &mut Self {
509            self.builder.http.text_body_mut(policy);
510            self
511        }
512
513        /// Sets the same floor for every HTTP field context.
514        pub fn floor_all(&mut self, floor: RedactionFloor) -> &mut Self {
515            self.builder.http.floor_all_mut(floor);
516            self
517        }
518
519        /// Disables every HTTP field-context floor explicitly.
520        pub fn disable_all_floors(&mut self) -> &mut Self {
521            self.builder.http.disable_all_floors_mut();
522            self
523        }
524
525        /// Sets the handling of root and array JSON scalar values in HTTP
526        /// bodies.
527        pub fn unkeyed_json(&mut self, policy: crate::UnkeyedJsonValuePolicy) -> &mut Self {
528            self.builder.unkeyed_json_value_policy = policy;
529            self
530        }
531    }
532
533    /// Mutable view over one HTTP field context.
534    #[cfg(feature = "http")]
535    pub struct HttpContextBuilderView<'a> {
536        /// HTTP builder receiving context-specific changes.
537        builder: &'a mut crate::formats::http::HttpPolicyBuilder,
538        /// Shared transaction error slot.
539        error: &'a mut Option<PolicyError>,
540        /// Field context targeted by this view.
541        context: crate::formats::http::HttpFieldContext,
542    }
543
544    #[cfg(feature = "http")]
545    impl HttpContextBuilderView<'_> {
546        /// Replaces all rules for this HTTP field context.
547        pub fn replace_rules(&mut self, rules: RedactionRules) -> &mut Self {
548            self.builder.rules_mut(self.context, rules);
549            self
550        }
551
552        /// Raises a context field's minimum sensitivity.
553        ///
554        /// # Errors
555        ///
556        /// Returns [`PolicyError`] when `field` has no canonical name.
557        pub fn raise(&mut self, field: &str, level: Sensitivity) -> Result<&mut Self, PolicyError> {
558            if self.error.is_none()
559                && let Err(error) = self.builder.raise_mut(self.context, field, level)
560            {
561                *self.error = Some(error.clone());
562                return Err(error);
563            }
564            Ok(self)
565        }
566
567        /// Replaces a context field rule without weakening the base policy.
568        ///
569        /// # Errors
570        ///
571        /// Returns [`PolicyError`] when `field` has no canonical name.
572        pub fn override_level(&mut self, field: &str, level: Sensitivity) -> Result<&mut Self, PolicyError> {
573            if self.error.is_none()
574                && let Err(error) = self.builder.override_level_mut(self.context, field, level)
575            {
576                *self.error = Some(error.clone());
577                return Err(error);
578            }
579            Ok(self)
580        }
581
582        /// Adds a context exact allow rule; the base policy still applies.
583        ///
584        /// # Errors
585        ///
586        /// Returns [`PolicyError`] when `field` has no canonical name.
587        pub fn allow_exact(&mut self, field: &str) -> Result<&mut Self, PolicyError> {
588            if self.error.is_none()
589                && let Err(error) = self.builder.allow_exact_mut(self.context, field)
590            {
591                *self.error = Some(error.clone());
592                return Err(error);
593            }
594            Ok(self)
595        }
596
597        /// Adds a context suffix allow rule; the base policy still applies.
598        ///
599        /// # Errors
600        ///
601        /// Returns [`PolicyError`] when `field` has no canonical suffix.
602        pub fn allow_suffix(&mut self, field: &str) -> Result<&mut Self, PolicyError> {
603            if self.error.is_none()
604                && let Err(error) = self.builder.allow_suffix_mut(self.context, field)
605            {
606                *self.error = Some(error.clone());
607                return Err(error);
608            }
609            Ok(self)
610        }
611
612        /// Removes a context exact allow rule.
613        ///
614        /// # Errors
615        ///
616        /// Returns [`PolicyError`] when `field` has no canonical name.
617        pub fn remove_allow_exact(&mut self, field: &str) -> Result<&mut Self, PolicyError> {
618            if self.error.is_none()
619                && let Err(error) = self.builder.remove_allow_exact_mut(self.context, field)
620            {
621                *self.error = Some(error.clone());
622                return Err(error);
623            }
624            Ok(self)
625        }
626
627        /// Removes a context suffix allow rule.
628        ///
629        /// # Errors
630        ///
631        /// Returns [`PolicyError`] when `field` has no canonical suffix.
632        pub fn remove_allow_suffix(&mut self, field: &str) -> Result<&mut Self, PolicyError> {
633            if self.error.is_none()
634                && let Err(error) = self.builder.remove_allow_suffix_mut(self.context, field)
635            {
636                *self.error = Some(error.clone());
637                return Err(error);
638            }
639            Ok(self)
640        }
641
642        /// Removes all context allow rules.
643        pub fn clear_allow_rules(&mut self) -> &mut Self {
644            self.builder.clear_allow_rules_mut(self.context);
645            self
646        }
647
648        /// Adds a context floor. Base protection remains independently
649        /// effective.
650        #[must_use]
651        #[inline(always)]
652        pub fn floor(&mut self, floor: RedactionFloor) -> &mut Self {
653            self.builder.floor_mut(self.context, floor);
654            self
655        }
656
657        /// Disables this context's explicit floor.
658        pub fn disable_floor(&mut self) -> &mut Self {
659            self.builder.disable_floor_mut(self.context);
660            self
661        }
662    }
663}
664
665pub use views::FieldsBuilder;
666#[cfg(feature = "http")]
667pub use views::HttpContextBuilderView;
668#[cfg(feature = "http")]
669pub use views::HttpPolicyBuilderView;
670#[cfg(feature = "uri")]
671pub use views::UriPolicyBuilderView;
672
673impl Default for RedactionPolicyBuilder {
674    /// Creates a builder with the standard floor and default limits.
675    fn default() -> Self {
676        Self::new()
677    }
678}