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;
11#[cfg(feature = "json")]
12use std::io::Write;
13use std::rc::Rc;
14
15#[cfg(feature = "json")]
16use qubit_budget::json::JsonDecodeSession;
17#[cfg(feature = "json")]
18use qubit_budget::json::JsonEncodeLimits;
19#[cfg(feature = "json")]
20use qubit_budget::json::JsonEncodeSession;
21#[cfg(feature = "json")]
22use qubit_json::decode::JsonDecoder;
23#[cfg(feature = "json")]
24use qubit_json::encode::JsonEncoder;
25use qubit_utils::Transient;
26#[cfg(feature = "json")]
27use qubit_value::ValueWireEncodePreflight;
28use serde::Deserialize;
29use serde::Deserializer;
30use serde::Serialize;
31use serde::Serializer;
32use serde::de;
33use serde::de::DeserializeSeed;
34use serde::ser::Error as SerError;
35
36use super::metadata_filter_builder::MetadataFilterBuilder;
37#[cfg(feature = "json")]
38use super::wire::METADATA_FILTER_WIRE_VERSION_V1;
39use super::wire::MetadataFilterWireV1Ref;
40use super::wire::MetadataFilterWireV1Seed;
41#[cfg(feature = "schema")]
42use crate::Condition;
43use crate::FilterExpression;
44#[cfg(feature = "json")]
45use crate::FilterExpressionView;
46use crate::FilterLimits;
47use crate::FilterMatchOptions;
48use crate::Metadata;
49#[cfg(feature = "schema")]
50use crate::MetadataResult;
51#[cfg(feature = "json")]
52use crate::metadata_limits::MetadataLimits;
53
54/// An expression, its matching policy, and its resource limits.
55///
56/// Boolean composition belongs to [`FilterExpression`]. This type only binds
57/// an already-built expression to the options and limits used to evaluate it.
58///
59/// # Examples
60///
61/// ```
62/// use qubit_metadata::FilterExpression;
63/// use qubit_metadata::Metadata;
64/// use qubit_metadata::MetadataFilter;
65///
66/// # fn main() -> qubit_metadata::MetadataResult<()> {
67/// let metadata = Metadata::new().with("tenant", "acme");
68/// let expression = FilterExpression::builder()
69///     .eq("tenant", "acme")
70///     .build()?;
71/// let filter = MetadataFilter::builder().expression(expression).build()?;
72/// assert!(filter.matches(&metadata));
73/// # Ok(())
74/// # }
75/// ```
76#[derive(Debug, Clone, PartialEq)]
77pub struct MetadataFilter {
78    /// Root Boolean expression.
79    expression: FilterExpression,
80    /// Evaluation options.
81    options: FilterMatchOptions,
82    /// Bounds that the expression satisfies.
83    limits: Transient<FilterLimits>,
84}
85
86impl MetadataFilter {
87    /// Creates a filter that matches every metadata object.
88    ///
89    /// # Returns
90    ///
91    /// A constant-true filter using default match options and library hard
92    /// limits.
93    #[inline]
94    #[must_use = "the constructed all-matching filter should be used"]
95    pub fn all() -> Self {
96        Self::new(
97            FilterExpression::match_all(),
98            FilterMatchOptions::default(),
99            FilterLimits::MAX,
100        )
101    }
102
103    /// Creates a filter that matches no metadata object.
104    ///
105    /// # Returns
106    ///
107    /// A constant-false filter using default match options and library hard
108    /// limits.
109    #[inline]
110    #[must_use = "the constructed no-match filter should be used"]
111    pub fn none() -> Self {
112        Self::new(
113            FilterExpression::match_none(),
114            FilterMatchOptions::default(),
115            FilterLimits::MAX,
116        )
117    }
118
119    /// Creates a builder for a metadata filter.
120    #[inline]
121    #[must_use]
122    pub const fn builder() -> MetadataFilterBuilder {
123        MetadataFilterBuilder::new()
124    }
125
126    /// Deserializes a filter and validates a receiver-controlled AST bound.
127    ///
128    /// The receiver bound is charged while the AST is being decoded. Callers
129    /// must separately bound raw input bytes and any deserializer-specific
130    /// resources before accepting untrusted data.
131    ///
132    /// # Parameters
133    ///
134    /// * `deserializer` - Source of the versioned filter representation.
135    /// * `receiver_limits` - Local upper bounds applied to the decoded AST.
136    ///
137    /// # Errors
138    ///
139    /// Returns a deserialization error for malformed V1 data or an unsupported
140    /// version. The underlying deserializer's error type receives the
141    /// structured filter-limit or policy failure through `D::Error::custom`;
142    /// this generic API cannot return [`crate::MetadataWireDecodeError`]
143    /// directly.
144    pub fn deserialize_with_filter_limits<'de, D>(
145        deserializer: D,
146        receiver_limits: FilterLimits,
147    ) -> Result<Self, D::Error>
148    where
149        D: Deserializer<'de>,
150    {
151        let error_slot = Rc::new(RefCell::new(None));
152        let wire = MetadataFilterWireV1Seed::new(receiver_limits, Rc::clone(&error_slot))
153            .deserialize(deserializer)
154            .map_err(|error| {
155                error_slot
156                    .borrow_mut()
157                    .take()
158                    .map_or_else(|| de::Error::custom(error), de::Error::custom)
159            })?;
160        wire.into_filter(receiver_limits).map_err(de::Error::custom)
161    }
162
163    /// Decodes a strict metadata-filter JSON envelope using the default wire
164    /// byte and receiver AST limits.
165    ///
166    /// # Parameters
167    ///
168    /// * `input` - Complete untrusted JSON input.
169    ///
170    /// # Returns
171    ///
172    /// The decoded filter.
173    ///
174    /// # Errors
175    ///
176    /// Returns an input-size error before parsing, a nested-value limit error,
177    /// `UnsupportedVersion` for a version mismatch, a structured
178    /// filter-contract error, or `InvalidJson` for malformed strict filter
179    /// input and receiver-limit failures found during incremental decoding.
180    #[cfg(feature = "json")]
181    #[inline]
182    pub fn decode_json_slice(input: &[u8]) -> Result<Self, crate::MetadataWireDecodeError> {
183        Self::decode_json_slice_with_limits(input, MetadataLimits::default(), FilterLimits::MAX)
184    }
185
186    /// Decodes a strict metadata-filter JSON envelope after validating both
187    /// wire-byte and receiver-controlled AST limits.
188    ///
189    /// # Parameters
190    ///
191    /// * `input` - Complete untrusted JSON input.
192    /// * `limits` - Shared JSON limits for this decoding session.
193    /// * `receiver_filter_limits` - Local AST limits validated after decoding.
194    ///
195    /// # Returns
196    ///
197    /// The decoded filter constrained by receiver-controlled limits.
198    ///
199    /// # Errors
200    ///
201    /// Returns an input-size error before parsing, `UnsupportedVersion` for a
202    /// version mismatch, a structured filter-contract error in `Filter`, or
203    /// `InvalidJson` for syntax, strict-envelope, nested value, and
204    /// receiver-limit failures. Receiver AST limits are charged while the
205    /// expression tree is read; generic JSON traversal is handled by the
206    /// shared budget adapter. Individual
207    /// JSON strings and embedded value payloads remain bounded by the outer
208    /// input-byte limit.
209    #[cfg(feature = "json")]
210    pub fn decode_json_slice_with_limits(
211        input: &[u8],
212        limits: MetadataLimits,
213        receiver_filter_limits: FilterLimits,
214    ) -> Result<Self, crate::MetadataWireDecodeError> {
215        limits
216            .validate()
217            .map_err(crate::MetadataWireDecodeError::InvalidLimits)?;
218        let mut decoder = JsonDecoder::new(JsonDecodeSession::from_limits(limits.json_decode()));
219        let error_slot = Rc::new(RefCell::new(None));
220        let wire = decoder
221            .decode_seed_utf8(
222                MetadataFilterWireV1Seed::new(receiver_filter_limits, Rc::clone(&error_slot)),
223                input,
224            )
225            .map_err(|error| {
226                error_slot.borrow_mut().take().map_or_else(
227                    || Into::<crate::MetadataWireDecodeError>::into(error),
228                    crate::MetadataWireDecodeError::Filter,
229                )
230            })?;
231        if wire.version() != METADATA_FILTER_WIRE_VERSION_V1 {
232            return Err(crate::MetadataWireDecodeError::UnsupportedVersion {
233                expected: METADATA_FILTER_WIRE_VERSION_V1,
234                actual: wire.version(),
235            });
236        }
237        wire.into_filter(receiver_filter_limits)
238            .map_err(crate::MetadataWireDecodeError::Filter)
239    }
240
241    /// Encodes this filter with the default JSON budget profile.
242    #[cfg(feature = "json")]
243    pub fn to_json_vec(&self) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
244        self.to_json_vec_with_limits(crate::metadata_limits::default_json_encode_limits())
245    }
246
247    /// Encodes this filter with caller-provided JSON budgets.
248    ///
249    /// # Parameters
250    ///
251    /// * `limits` - Output and JSON-value budgets for this operation.
252    ///
253    /// # Errors
254    ///
255    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
256    /// budget or the filter cannot be represented by the V1 wire format.
257    #[cfg(feature = "json")]
258    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
259        self.preflight_wire_values(limits)?;
260        let session = JsonEncodeSession::from_limits(limits);
261        JsonEncoder::new(session).to_vec(self).map_err(Into::into)
262    }
263
264    /// Encodes this filter to a writer with the default JSON budget profile.
265    #[cfg(feature = "json")]
266    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), crate::MetadataWireEncodeError>
267    where
268        W: Write,
269    {
270        self.to_json_writer_with_limits(writer, crate::metadata_limits::default_json_encode_limits())
271    }
272
273    /// Encodes this filter to a writer with caller-provided JSON budgets.
274    ///
275    /// # Parameters
276    ///
277    /// * `writer` - Destination receiving the compact JSON document.
278    /// * `limits` - Output and JSON-value budgets for this operation.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
283    /// budget, the filter cannot be represented by the V1 wire format, or the
284    /// writer rejects the output.
285    #[cfg(feature = "json")]
286    pub fn to_json_writer_with_limits<W>(
287        &self,
288        writer: W,
289        limits: JsonEncodeLimits,
290    ) -> Result<(), crate::MetadataWireEncodeError>
291    where
292        W: Write,
293    {
294        self.preflight_wire_values(limits)?;
295        let session = JsonEncodeSession::from_limits(limits);
296        JsonEncoder::new(session)
297            .write_buffered(writer, self)
298            .map_err(Into::into)
299    }
300
301    /// Creates a filter from already validated parts.
302    #[inline]
303    pub(crate) const fn new(expression: FilterExpression, options: FilterMatchOptions, limits: FilterLimits) -> Self {
304        Self {
305            expression,
306            options,
307            limits: Transient::new(limits),
308        }
309    }
310
311    /// Returns the root expression.
312    #[inline]
313    #[must_use = "the filter expression should be inspected"]
314    pub const fn expression(&self) -> &FilterExpression {
315        &self.expression
316    }
317
318    /// Returns the evaluation options.
319    #[inline]
320    #[must_use]
321    pub const fn options(&self) -> FilterMatchOptions {
322        self.options
323    }
324
325    /// Returns the resource limits.
326    #[inline]
327    #[must_use = "the filter limits should be inspected"]
328    pub const fn limits(&self) -> FilterLimits {
329        *self.limits.get()
330    }
331
332    /// Preflights every operand against the shared JSON encoding budget.
333    #[cfg(feature = "json")]
334    fn preflight_wire_values(&self, limits: JsonEncodeLimits) -> Result<(), crate::MetadataWireEncodeError> {
335        fn visit(
336            expression: &FilterExpression,
337            checker: &mut ValueWireEncodePreflight,
338        ) -> Result<(), crate::MetadataWireEncodeError> {
339            match expression.view() {
340                FilterExpressionView::Condition(condition) => condition.visit_operands(&mut |value| {
341                    checker.check_value(value).map_err(crate::MetadataWireEncodeError::from)
342                }),
343                FilterExpressionView::And(children) | FilterExpressionView::Or(children) => {
344                    for child in children {
345                        visit(child, checker)?;
346                    }
347                    Ok(())
348                }
349                FilterExpressionView::Not(inner) => visit(inner, checker),
350                FilterExpressionView::True | FilterExpressionView::False => Ok(()),
351            }
352        }
353        let mut checker = ValueWireEncodePreflight::new(limits);
354        visit(&self.expression, &mut checker)
355    }
356
357    /// Returns whether `metadata` satisfies this filter.
358    #[inline]
359    #[must_use]
360    pub fn matches(&self, metadata: &Metadata) -> bool {
361        self.expression.evaluate(metadata, self.options).is_match()
362    }
363
364    /// Visits every leaf condition in the expression.
365    ///
366    /// # Errors
367    ///
368    /// Returns the first error produced by `visitor`.
369    #[cfg(feature = "schema")]
370    #[inline]
371    pub(crate) fn visit_conditions<F>(&self, mut visitor: F) -> MetadataResult<()>
372    where
373        F: FnMut(&Condition) -> MetadataResult<()>,
374    {
375        self.expression.visit_conditions(&mut visitor)
376    }
377}
378
379impl Serialize for MetadataFilter {
380    /// Serializes this filter through its versioned wire representation.
381    #[inline]
382    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
383    where
384        S: Serializer,
385    {
386        MetadataFilterWireV1Ref::try_from(self)
387            .map_err(<S::Error as SerError>::custom)?
388            .serialize(serializer)
389    }
390}
391
392impl<'de> Deserialize<'de> for MetadataFilter {
393    /// Deserializes a filter using library hard limits as receiver limits.
394    #[inline]
395    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        Self::deserialize_with_filter_limits(deserializer, FilterLimits::MAX)
400    }
401}