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::MaskingPolicy;
11use super::PolicyError;
12use super::PolicyLocation;
13use super::RedactionFloor;
14use super::RedactionLimits;
15use super::RedactionLimitsBuilder;
16use super::RedactionPolicy;
17use super::RedactionRules;
18use super::RedactionRulesBuilder;
19use super::Sensitivity;
20#[cfg(feature = "json")]
21use super::UnkeyedJsonValuePolicy;
22
23// Provides the transactional application-field view.
24mod fields_builder;
25// Provides the independently configured HTTP field-context view.
26#[cfg(feature = "http")]
27mod http_context_builder_view;
28// Provides the grouped HTTP policy view.
29#[cfg(feature = "http")]
30mod http_policy_builder_view;
31// Provides the grouped URI policy view.
32#[cfg(feature = "uri")]
33mod uri_policy_builder_view;
34
35/// Mutable construction state for an immutable [`RedactionPolicy`].
36///
37/// Configuration setters are available through the grouped views returned by
38/// [`Self::fields`] and [`Self::limits`], with feature-specific HTTP and URI
39/// views when those formats are enabled.
40/// Duplicate consuming setters are intentionally not available at this level:
41///
42/// ```compile_fail
43/// use qubit_redact::{RedactionPolicy, Sensitivity};
44///
45/// let _ = RedactionPolicy::builder().raise("token", Sensitivity::Secret);
46/// ```
47///
48/// # Examples
49///
50/// ```
51/// use qubit_redact::RedactionPolicy;
52///
53/// let policy = RedactionPolicy::builder()
54///     .fields(|fields| {
55///         let _ = fields.secret_sensitive("api_token");
56///     })
57///     .expect("valid field rules")
58///     .build()
59///     .expect("valid policy");
60/// assert!(policy.sensitivity_for("api_token").is_some());
61/// ```
62#[derive(Debug, Clone)]
63pub struct RedactionPolicyBuilder {
64    /// Whether the resulting policy bypasses redaction.
65    disabled: bool,
66    /// Mutable application field rules.
67    rules: RedactionRulesBuilder,
68    /// Shared masking strategies by sensitivity.
69    masking: MaskingPolicy,
70    /// Optional minimum-protection floor.
71    floor: Option<RedactionFloor>,
72    /// Resource limits for each transaction.
73    limits: RedactionLimits,
74    /// Mutable HTTP-specific policy state.
75    #[cfg(feature = "http")]
76    http: crate::formats::http::HttpPolicyBuilder,
77    /// Mutable URI-specific policy state.
78    #[cfg(feature = "uri")]
79    uri: crate::formats::uri::UriPolicyBuilder,
80    /// Handling for JSON scalars without a field key.
81    #[cfg(feature = "json")]
82    unkeyed_json_value_policy: UnkeyedJsonValuePolicy,
83}
84
85impl RedactionPolicyBuilder {
86    /// Creates an empty application-rule builder with the standard floor.
87    #[must_use]
88    pub fn new() -> Self {
89        Self {
90            disabled: false,
91            rules: RedactionRulesBuilder::empty(PolicyLocation::Rules),
92            masking: MaskingPolicy::default(),
93            floor: Some(RedactionFloor::standard()),
94            limits: RedactionLimits::default(),
95            #[cfg(feature = "http")]
96            http: crate::formats::http::HttpPolicyBuilder::new(),
97            #[cfg(feature = "uri")]
98            uri: crate::formats::uri::UriPolicyBuilder::new(),
99            #[cfg(feature = "json")]
100            unkeyed_json_value_policy: UnkeyedJsonValuePolicy::PassThrough,
101        }
102    }
103
104    /// Copies the immutable policy into mutable builder state.
105    #[must_use]
106    pub(super) fn from_policy(policy: &RedactionPolicy) -> Self {
107        Self {
108            disabled: policy.is_disabled(),
109            rules: RedactionRulesBuilder::from_inner(&policy.rules().clone_application(), PolicyLocation::Rules),
110            masking: policy.masking().clone(),
111            floor: policy.rules().floor().cloned(),
112            limits: *policy.limits(),
113            #[cfg(feature = "http")]
114            http: crate::formats::http::HttpPolicyBuilder::from_policy(policy.http()),
115            #[cfg(feature = "uri")]
116            uri: crate::formats::uri::UriPolicyBuilder::from_policy(policy.uri()),
117            #[cfg(feature = "json")]
118            unkeyed_json_value_policy: policy.unkeyed_json_value_policy(),
119        }
120    }
121
122    /// Configures base field sensitivity rules transactionally.
123    ///
124    /// The closure writes into a temporary field draft. Field names are
125    /// validated after the closure returns and the draft is applied only when
126    /// every field is valid. The configuration methods inside the closure are
127    /// therefore infallible and can be chained without a trailing `Ok(())`.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`PolicyError`] when a field name is empty after
132    /// canonicalization. The builder remains unchanged on error.
133    pub fn fields<F>(self, configure: F) -> Result<Self, PolicyError>
134    where
135        F: FnOnce(&mut FieldsBuilder<'_>),
136    {
137        let mut draft = self.clone();
138        let error = {
139            let mut fields = FieldsBuilder {
140                builder: &mut draft,
141                error: None,
142            };
143            configure(&mut fields);
144            fields.error.take()
145        };
146        if let Some(error) = error {
147            return Err(error);
148        }
149        Ok(draft)
150    }
151
152    /// Configures HTTP policy through an isolated draft.
153    ///
154    /// The draft replaces this namespace only after the closure returns, so a
155    /// failed build never partially updates the caller's builder.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`PolicyError`] when the HTTP view records an invalid field
160    /// rule. The original builder remains unchanged.
161    #[cfg(feature = "http")]
162    pub fn http<F>(self, configure: F) -> Result<Self, PolicyError>
163    where
164        F: FnOnce(&mut HttpPolicyBuilderView<'_>),
165    {
166        let mut draft = self.clone();
167        let error = {
168            let mut view = HttpPolicyBuilderView {
169                builder: &mut draft,
170                error: None,
171            };
172            configure(&mut view);
173            view.error.take()
174        };
175        if let Some(error) = error {
176            return Err(error);
177        }
178        Ok(draft)
179    }
180
181    /// Configures URI policy through an isolated draft.
182    ///
183    /// # Errors
184    ///
185    /// This operation is currently infallible. The result preserves the
186    /// transactional shape shared by feature-specific builder views.
187    #[cfg(feature = "uri")]
188    pub fn uri<F>(self, configure: F) -> Result<Self, PolicyError>
189    where
190        F: FnOnce(&mut UriPolicyBuilderView<'_>),
191    {
192        let mut draft = self.clone();
193        let mut view = UriPolicyBuilderView {
194            builder: &mut draft.uri,
195        };
196        configure(&mut view);
197        Ok(draft)
198    }
199
200    /// Configures transaction limits through a draft that is applied
201    /// atomically.
202    ///
203    /// The closure mutates only a temporary limits builder. Once it returns,
204    /// the completed limits replace this builder's limits as one update.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`PolicyError`] when the completed limit set violates a
209    /// platform collection capacity limit. No invalid limit snapshot is
210    /// installed.
211    pub fn limits<F>(mut self, configure: F) -> Result<Self, PolicyError>
212    where
213        F: FnOnce(&mut RedactionLimitsBuilder),
214    {
215        let mut limits = RedactionLimits::builder_from(&self.limits);
216        configure(&mut limits);
217        let limits = limits.build();
218        limits.validate()?;
219        self.limits = limits;
220        Ok(self)
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    #[inline(always)]
230    pub const fn unkeyed_json_value_policy(mut self, policy: UnkeyedJsonValuePolicy) -> Self {
231        self.unkeyed_json_value_policy = policy;
232        self
233    }
234
235    /// Validates that `field` has a non-empty canonical application-rule name.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`PolicyError::EmptyFieldName`] at
240    /// [`PolicyLocation::Rules`] when canonicalization leaves no name.
241    #[inline(always)]
242    pub fn validate_field_name(field: &str) -> Result<(), PolicyError> {
243        RedactionRulesBuilder::validate_field_name(field, PolicyLocation::Rules)
244    }
245
246    /// Validates and returns the immutable policy snapshot.
247    ///
248    /// # Errors
249    ///
250    /// Returns a [`PolicyError`] when final policy validation fails.
251    pub fn build(self) -> Result<RedactionPolicy, PolicyError> {
252        let rules = RedactionRules::new(self.rules.build_inner()?, self.floor);
253        self.masking.validate(PolicyLocation::Rules)?;
254        #[cfg(feature = "http")]
255        let http = self.http.build()?;
256        #[cfg(feature = "uri")]
257        let uri = self.uri.build()?;
258        Ok(RedactionPolicy::from_rules(
259            rules,
260            self.masking,
261            self.limits,
262            #[cfg(feature = "http")]
263            http,
264            #[cfg(feature = "uri")]
265            uri,
266            #[cfg(feature = "json")]
267            self.unkeyed_json_value_policy,
268            self.disabled,
269        ))
270    }
271}
272
273pub use fields_builder::FieldsBuilder;
274#[cfg(feature = "http")]
275pub use http_context_builder_view::HttpContextBuilderView;
276#[cfg(feature = "http")]
277pub use http_policy_builder_view::HttpPolicyBuilderView;
278#[cfg(feature = "uri")]
279pub use uri_policy_builder_view::UriPolicyBuilderView;
280
281impl Default for RedactionPolicyBuilder {
282    /// Creates a builder with the standard floor and default limits.
283    #[inline(always)]
284    fn default() -> Self {
285        Self::new()
286    }
287}