qubit_metadata/filter/metadata_filter.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//! [`MetadataFilter`].
9
10use std::cell::RefCell;
11use std::fmt;
12#[cfg(feature = "json")]
13use std::io::Write;
14use std::rc::Rc;
15
16#[cfg(feature = "json")]
17use qubit_budget::json::JsonDecodeSession;
18#[cfg(feature = "json")]
19use qubit_budget::json::JsonEncodeLimits;
20#[cfg(feature = "json")]
21use qubit_budget::json::JsonEncodeSession;
22#[cfg(feature = "json")]
23use qubit_json::decode::JsonDecoder;
24#[cfg(feature = "json")]
25use qubit_json::encode::JsonEncoder;
26use qubit_redact::Redact;
27use qubit_redact::RedactionWriter;
28use qubit_redact::Redactor;
29use qubit_utils::Transient;
30#[cfg(feature = "json")]
31use qubit_value::ValueWireEncodePreflight;
32use serde::Deserialize;
33use serde::Deserializer;
34use serde::Serialize;
35use serde::Serializer;
36use serde::de;
37use serde::de::DeserializeSeed;
38use serde::ser::Error as SerError;
39
40use super::metadata_filter_builder::MetadataFilterBuilder;
41#[cfg(feature = "json")]
42use super::wire::METADATA_FILTER_WIRE_VERSION_V1;
43use super::wire::MetadataFilterWireV1Ref;
44use super::wire::MetadataFilterWireV1Seed;
45#[cfg(feature = "schema")]
46use crate::Condition;
47use crate::FilterExpression;
48#[cfg(feature = "json")]
49use crate::FilterExpressionView;
50use crate::FilterLimits;
51use crate::FilterMatchOptions;
52use crate::Metadata;
53#[cfg(feature = "schema")]
54use crate::MetadataResult;
55#[cfg(feature = "json")]
56use crate::metadata_limits::MetadataLimits;
57
58/// An expression, its matching policy, and its resource limits.
59///
60/// Boolean composition belongs to [`FilterExpression`]. This type only binds
61/// an already-built expression to the options and limits used to evaluate it.
62///
63/// # Examples
64///
65/// ```
66/// use qubit_metadata::FilterExpression;
67/// use qubit_metadata::Metadata;
68/// use qubit_metadata::MetadataFilter;
69///
70/// # fn main() -> qubit_metadata::MetadataResult<()> {
71/// let metadata = Metadata::new().with("tenant", "acme");
72/// let expression = FilterExpression::builder()
73/// .eq("tenant", "acme")
74/// .build()?;
75/// let filter = MetadataFilter::builder().expression(expression).build()?;
76/// assert!(filter.matches(&metadata));
77/// # Ok(())
78/// # }
79/// ```
80#[derive(Clone, PartialEq)]
81#[must_use]
82pub struct MetadataFilter {
83 /// Root Boolean expression.
84 expression: FilterExpression,
85 /// Evaluation options.
86 options: FilterMatchOptions,
87 /// Bounds that the expression satisfies.
88 limits: Transient<FilterLimits>,
89}
90
91impl fmt::Debug for MetadataFilter {
92 /// Writes the strict-policy diagnostic representation.
93 ///
94 /// # Parameters
95 ///
96 /// * `formatter` - Formatter that receives the output.
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 let output = Redactor::strict().redact_text(self);
99 let text = output.text_or_marker("<redaction incomplete>");
100 formatter.write_str(text.as_ref())
101 }
102}
103
104impl Redact for MetadataFilter {
105 /// Writes the filter through one cumulative diagnostic budget.
106 ///
107 /// # Parameters
108 ///
109 /// * `writer` - Destination that receives the encoded or redacted output.
110 fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
111 writer.record("MetadataFilter", |fields| {
112 fields.nested("expression", &self.expression);
113 fields.unmarked("options", || self.options);
114 fields.unmarked("limits", || self.limits());
115 });
116 }
117}
118
119impl MetadataFilter {
120 /// Creates a filter that matches every metadata object.
121 ///
122 /// # Returns
123 ///
124 /// A constant-true filter using default match options and library hard
125 /// limits.
126 #[inline]
127 pub fn all() -> Self {
128 Self::new(
129 FilterExpression::match_all(),
130 FilterMatchOptions::default(),
131 FilterLimits::MAX,
132 )
133 }
134
135 /// Creates a filter that matches no metadata object.
136 ///
137 /// # Returns
138 ///
139 /// A constant-false filter using default match options and library hard
140 /// limits.
141 #[inline]
142 pub fn none() -> Self {
143 Self::new(
144 FilterExpression::match_none(),
145 FilterMatchOptions::default(),
146 FilterLimits::MAX,
147 )
148 }
149
150 /// Creates a builder for a metadata filter.
151 #[inline]
152 #[must_use]
153 pub const fn builder() -> MetadataFilterBuilder {
154 MetadataFilterBuilder::new()
155 }
156
157 /// Creates a filter from already validated parts.
158 ///
159 /// # Parameters
160 ///
161 /// * `expression` - Filter expression stored by the filter.
162 /// * `options` - Options used when matching.
163 /// * `limits` - Resource limits applied by this operation.
164 ///
165 /// # Returns
166 ///
167 /// A filter containing the supplied validated parts.
168 #[inline]
169 pub(crate) const fn new(expression: FilterExpression, options: FilterMatchOptions, limits: FilterLimits) -> Self {
170 Self {
171 expression,
172 options,
173 limits: Transient::new(limits),
174 }
175 }
176
177 /// Deserializes a filter and validates a receiver-controlled AST bound.
178 ///
179 /// The receiver bound is charged while the AST is being decoded. Callers
180 /// must separately bound raw input bytes and any deserializer-specific
181 /// resources before accepting untrusted data.
182 ///
183 /// # Type Parameters
184 ///
185 /// * `D` - Deserializer that provides the encoded representation.
186 ///
187 /// # Parameters
188 ///
189 /// * `deserializer` - Source of the versioned filter representation.
190 /// * `receiver_limits` - Local upper bounds applied to the decoded AST.
191 ///
192 /// # Returns
193 ///
194 /// The decoded filter using the supplied receiver limits.
195 ///
196 /// # Errors
197 ///
198 /// Returns a deserialization error for malformed V1 data or an unsupported
199 /// version. The underlying deserializer's error type receives the
200 /// structured filter-limit or policy failure through `D::Error::custom`;
201 /// this generic API cannot return `MetadataWireDecodeError` directly.
202 pub fn deserialize_with_filter_limits<'de, D>(
203 deserializer: D,
204 receiver_limits: FilterLimits,
205 ) -> Result<Self, D::Error>
206 where
207 D: Deserializer<'de>,
208 {
209 let error_slot = Rc::new(RefCell::new(None));
210 let wire = MetadataFilterWireV1Seed::new(receiver_limits, Rc::clone(&error_slot))
211 .deserialize(deserializer)
212 .map_err(|error| {
213 error_slot
214 .borrow_mut()
215 .take()
216 .map_or_else(|| de::Error::custom(error), de::Error::custom)
217 })?;
218 wire.into_filter(receiver_limits).map_err(de::Error::custom)
219 }
220
221 /// Decodes a strict metadata-filter JSON envelope using the default wire
222 /// byte and receiver AST limits.
223 ///
224 /// # Parameters
225 ///
226 /// * `input` - Complete untrusted JSON input.
227 ///
228 /// # Returns
229 ///
230 /// The decoded filter.
231 ///
232 /// # Errors
233 ///
234 /// Returns `Budget` or `Quantity` when shared JSON traversal exceeds a
235 /// configured budget, `Filter` when the receiver AST or filter contract
236 /// exceeds its limit, `UnsupportedVersion` for a version mismatch,
237 /// `InvalidLimits` for invalid metadata limits, and `Syntax` for empty
238 /// input or lexical JSON errors. Shared decoder failures such as invalid
239 /// UTF-8, unexpected top-level types, or deserialization failures return
240 /// `Decode`.
241 /// The complete unique-key JSON source tree is first materialized under
242 /// the shared JSON decode budgets. The domain seed then checks receiver AST
243 /// and membership limits while constructing the filter. Receiver AST or
244 /// filter-contract failures return `Filter`; tightening `FilterLimits`
245 /// does not stop the preceding source-tree materialization, so configure
246 /// both the generic JSON budgets and domain limits.
247 #[cfg(feature = "json")]
248 #[inline]
249 pub fn decode_json_slice(input: &[u8]) -> Result<Self, crate::MetadataWireDecodeError> {
250 Self::decode_json_slice_with_limits(input, MetadataLimits::default(), FilterLimits::MAX)
251 }
252
253 /// Decodes a strict metadata-filter JSON envelope after validating the
254 /// shared metadata limits. The complete unique-key JSON source tree is
255 /// first materialized under the shared JSON decode budgets. The domain
256 /// seed then checks receiver AST and membership limits while constructing
257 /// the filter. Tightening `receiver_filter_limits` does not stop the
258 /// preceding source-tree materialization, so configure both the generic
259 /// JSON budgets and domain limits.
260 ///
261 /// # Parameters
262 ///
263 /// * `input` - Complete untrusted JSON input.
264 /// * `limits` - Shared JSON limits for this decoding session.
265 /// * `receiver_filter_limits` - Local AST limits charged while decoding the
266 /// expression tree.
267 ///
268 /// # Returns
269 ///
270 /// The decoded filter constrained by receiver-controlled limits.
271 ///
272 /// # Errors
273 ///
274 /// Returns `Budget` or `Quantity` when generic JSON traversal exceeds a
275 /// configured budget, `Filter` when domain traversal detects an AST limit
276 /// or the decoded filter violates its contract, `UnsupportedVersion` for a
277 /// version mismatch, and `InvalidLimits` for invalid metadata limits.
278 /// Empty input and lexical JSON errors return `Syntax`; shared decoder
279 /// failures such as invalid UTF-8, unexpected top-level types, or other
280 /// deserialization failures return `Decode`. Receiver AST or filter
281 /// contract failures return `Filter` and do not retain source coordinates.
282 /// Individual JSON strings and embedded value payloads remain bounded by
283 /// the outer input-byte limit.
284 #[cfg(feature = "json")]
285 pub fn decode_json_slice_with_limits(
286 input: &[u8],
287 limits: MetadataLimits,
288 receiver_filter_limits: FilterLimits,
289 ) -> Result<Self, crate::MetadataWireDecodeError> {
290 limits
291 .validate()
292 .map_err(crate::MetadataWireDecodeError::InvalidLimits)?;
293 let mut decoder = JsonDecoder::new(JsonDecodeSession::from_limits(limits.json_decode()));
294 let error_slot = Rc::new(RefCell::new(None));
295 let wire = decoder
296 .decode_unique_seed_utf8(
297 MetadataFilterWireV1Seed::new(receiver_filter_limits, Rc::clone(&error_slot)),
298 input,
299 )
300 .map_err(|error| {
301 error_slot.borrow_mut().take().map_or_else(
302 || Into::<crate::MetadataWireDecodeError>::into(error),
303 crate::MetadataWireDecodeError::Filter,
304 )
305 })?;
306 if wire.version() != METADATA_FILTER_WIRE_VERSION_V1 {
307 return Err(crate::MetadataWireDecodeError::UnsupportedVersion {
308 expected: METADATA_FILTER_WIRE_VERSION_V1,
309 actual: wire.version(),
310 });
311 }
312 wire.into_filter(receiver_filter_limits)
313 .map_err(crate::MetadataWireDecodeError::Filter)
314 }
315
316 /// Encodes this filter with the default JSON budget profile.
317 ///
318 /// Uses [`crate::default_json_encode_limits`]. Decode limits do not
319 /// affect output budgets. Successful filter construction does not
320 /// guarantee that the encoded document fits these JSON budgets.
321 ///
322 /// # Returns
323 ///
324 /// Compact JSON bytes representing this filter in the V1 wire format.
325 ///
326 /// # Errors
327 ///
328 /// Returns [`crate::MetadataWireEncodeError`] when operand preflight,
329 /// JSON budget checks, or serialization fails.
330 #[cfg(feature = "json")]
331 pub fn to_json_vec(&self) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
332 self.to_json_vec_with_limits(crate::metadata_limits::default_json_encode_limits())
333 }
334
335 /// Encodes this filter with caller-provided JSON budgets.
336 ///
337 /// The supplied limits apply to this encoding operation independently
338 /// of the AST limits enforced during filter construction. A successfully
339 /// constructed filter can still exceed these JSON budgets.
340 ///
341 /// # Parameters
342 ///
343 /// * `limits` - Output and JSON-value budgets for this operation.
344 ///
345 /// # Returns
346 ///
347 /// Compact JSON bytes representing this filter in the V1 wire format.
348 ///
349 /// # Errors
350 ///
351 /// Returns [`crate::MetadataWireEncodeError`] when operand preflight,
352 /// JSON budget checks, or serialization fails.
353 #[cfg(feature = "json")]
354 pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
355 self.preflight_wire_values(limits)?;
356 let session = JsonEncodeSession::from_limits(limits);
357 JsonEncoder::new(session).to_vec(self).map_err(Into::into)
358 }
359
360 /// Encodes this filter to a writer with the default JSON budget profile.
361 ///
362 /// The complete compact JSON document is buffered after operand preflight
363 /// and budget checks, before any bytes are written to the destination. A
364 /// preflight, budget, or serialization error therefore leaves the writer
365 /// untouched. An I/O error during writing may leave a prefix in the writer;
366 /// this method does not provide atomicity, rollback, or automatic retries.
367 /// Applications are responsible for persistence and retry decisions.
368 /// The default profile is returned by
369 /// [`crate::default_json_encode_limits`]; decode limits do not affect
370 /// the output budget.
371 ///
372 /// # Type Parameters
373 ///
374 /// * `W` - Writer that receives the encoded representation.
375 ///
376 /// # Parameters
377 ///
378 /// * `writer` - Destination receiving the compact JSON document.
379 ///
380 /// # Returns
381 ///
382 /// Returns `Ok(())` after the complete document is written.
383 ///
384 /// # Errors
385 ///
386 /// Returns [`crate::MetadataWireEncodeError`] when operand preflight,
387 /// budget checks, serialization, or destination I/O fails. Only destination
388 /// I/O errors can occur after writing has started.
389 #[cfg(feature = "json")]
390 pub fn to_json_writer<W>(&self, writer: W) -> Result<(), crate::MetadataWireEncodeError>
391 where
392 W: Write,
393 {
394 self.to_json_writer_with_limits(writer, crate::metadata_limits::default_json_encode_limits())
395 }
396
397 /// Encodes this filter to a writer with caller-provided JSON budgets.
398 ///
399 /// The complete compact JSON document is buffered after operand preflight
400 /// and budget checks, before any bytes are written to the destination. A
401 /// preflight, budget, or serialization error therefore leaves the writer
402 /// untouched. An I/O error during writing may leave a prefix in the writer;
403 /// this method does not provide atomicity, rollback, or automatic retries.
404 /// Applications are responsible for persistence and retry decisions. The
405 /// supplied [`JsonEncodeLimits`] controls encoding; decode limits do not
406 /// affect the output budget.
407 ///
408 /// # Type Parameters
409 ///
410 /// * `W` - Writer that receives the encoded representation.
411 ///
412 /// # Parameters
413 ///
414 /// * `writer` - Destination receiving the compact JSON document.
415 /// * `limits` - Output and JSON-value budgets for this operation.
416 ///
417 /// # Returns
418 ///
419 /// Returns `Ok(())` after the complete document is written.
420 ///
421 /// # Errors
422 ///
423 /// Returns [`crate::MetadataWireEncodeError`] when operand preflight,
424 /// budget checks, serialization, or destination I/O fails. Only destination
425 /// I/O errors can occur after writing has started.
426 #[cfg(feature = "json")]
427 pub fn to_json_writer_with_limits<W>(
428 &self,
429 mut writer: W,
430 limits: JsonEncodeLimits,
431 ) -> Result<(), crate::MetadataWireEncodeError>
432 where
433 W: Write,
434 {
435 self.preflight_wire_values(limits)?;
436 let session = JsonEncodeSession::from_limits(limits);
437 JsonEncoder::new(session)
438 .write_buffered(&mut writer, self)
439 .map_err(Into::into)
440 }
441
442 /// Returns the root expression.
443 ///
444 /// # Returns
445 ///
446 /// The expression evaluated by this filter.
447 #[inline]
448 #[must_use = "the filter expression should be inspected"]
449 pub const fn expression(&self) -> &FilterExpression {
450 &self.expression
451 }
452
453 /// Returns the evaluation options.
454 ///
455 /// # Returns
456 ///
457 /// The policy used when evaluating conditions.
458 #[inline]
459 #[must_use]
460 pub const fn options(&self) -> FilterMatchOptions {
461 self.options
462 }
463
464 /// Returns the resource limits.
465 ///
466 /// # Returns
467 ///
468 /// The limits validated when this filter was constructed.
469 #[inline]
470 #[must_use = "the filter limits should be inspected"]
471 pub const fn limits(&self) -> FilterLimits {
472 *self.limits.get()
473 }
474
475 /// Returns whether `metadata` satisfies this filter.
476 ///
477 /// # Parameters
478 ///
479 /// * `metadata` - Metadata inspected by the filter.
480 ///
481 /// # Returns
482 ///
483 /// `true` when the metadata satisfies the expression and match options.
484 #[inline]
485 #[must_use]
486 pub fn matches(&self, metadata: &Metadata) -> bool {
487 self.expression.evaluate(metadata, self.options).is_match()
488 }
489
490 /// Visits every leaf condition in the expression.
491 ///
492 /// # Type Parameters
493 ///
494 /// * `F` - Callback invoked for each visited condition.
495 ///
496 /// # Parameters
497 ///
498 /// * `visitor` - Callback applied to each leaf condition.
499 ///
500 /// # Errors
501 ///
502 /// Returns the first error produced by `visitor`.
503 #[cfg(feature = "schema")]
504 #[inline]
505 pub(crate) fn visit_conditions<F>(&self, mut visitor: F) -> MetadataResult<()>
506 where
507 F: FnMut(&Condition) -> MetadataResult<()>,
508 {
509 self.expression.visit_conditions(&mut visitor)
510 }
511
512 /// Preflights every operand against the shared JSON encoding budget.
513 ///
514 /// # Parameters
515 ///
516 /// * `limits` - Resource limits applied by this operation.
517 ///
518 /// # Errors
519 ///
520 /// Returns an encoding error if any operand exceeds the budget.
521 #[cfg(feature = "json")]
522 fn preflight_wire_values(&self, limits: JsonEncodeLimits) -> Result<(), crate::MetadataWireEncodeError> {
523 /// Checks every value operand in one expression subtree.
524 ///
525 /// # Parameters
526 ///
527 /// * `expression` - Filter expression stored by the filter.
528 /// * `checker` - Shared preflight checker for encoded values.
529 ///
530 /// # Returns
531 ///
532 /// `Ok(())` after every operand in the subtree passes preflight.
533 ///
534 /// # Errors
535 ///
536 /// Returns an encoding error when an operand exceeds the shared budget.
537 fn visit(
538 expression: &FilterExpression,
539 checker: &mut ValueWireEncodePreflight,
540 ) -> Result<(), crate::MetadataWireEncodeError> {
541 match expression.view() {
542 FilterExpressionView::Condition(condition) => condition.visit_operands(&mut |value| {
543 checker.check_value(value).map_err(crate::MetadataWireEncodeError::from)
544 }),
545 FilterExpressionView::And(children) | FilterExpressionView::Or(children) => {
546 for child in children {
547 visit(child, checker)?;
548 }
549 Ok(())
550 }
551 FilterExpressionView::Not(inner) => visit(inner, checker),
552 FilterExpressionView::True | FilterExpressionView::False => Ok(()),
553 }
554 }
555 let mut checker = ValueWireEncodePreflight::new(limits);
556 visit(&self.expression, &mut checker)
557 }
558}
559
560impl Serialize for MetadataFilter {
561 /// Serializes this filter through its versioned wire representation.
562 ///
563 /// # Type Parameters
564 ///
565 /// * `S` - Serializer that receives the encoded representation.
566 ///
567 /// # Parameters
568 ///
569 /// * `serializer` - Serializer receiving the filter representation.
570 ///
571 /// # Returns
572 ///
573 /// The serializer's representation of this filter.
574 ///
575 /// # Errors
576 ///
577 /// Returns the serializer's error if conversion to the wire form or
578 /// serialization fails.
579 #[inline]
580 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
581 where
582 S: Serializer,
583 {
584 MetadataFilterWireV1Ref::try_from(self)
585 .map_err(<S::Error as SerError>::custom)?
586 .serialize(serializer)
587 }
588}
589
590impl<'de> Deserialize<'de> for MetadataFilter {
591 /// Deserializes a filter using library hard limits as receiver limits.
592 ///
593 /// # Type Parameters
594 ///
595 /// * `D` - Deserializer that provides the encoded representation.
596 ///
597 /// # Parameters
598 ///
599 /// * `deserializer` - Deserializer providing the filter representation.
600 ///
601 /// # Returns
602 ///
603 /// The reconstructed filter.
604 ///
605 /// # Errors
606 ///
607 /// Returns the deserializer's error if the versioned representation is
608 /// malformed or violates the filter limits.
609 #[inline]
610 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
611 where
612 D: Deserializer<'de>,
613 {
614 Self::deserialize_with_filter_limits(deserializer, FilterLimits::MAX)
615 }
616}