Skip to main content

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}