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}