qubit-metadata 0.6.0

Type-safe metadata model with schemas and composable filters
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! [`MetadataFilter`].

use std::cell::RefCell;
#[cfg(feature = "json")]
use std::io::Write;
use std::rc::Rc;

#[cfg(feature = "json")]
use qubit_budget::json::JsonDecodeSession;
#[cfg(feature = "json")]
use qubit_budget::json::JsonEncodeLimits;
#[cfg(feature = "json")]
use qubit_budget::json::JsonEncodeSession;
#[cfg(feature = "json")]
use qubit_json::decode::JsonDecoder;
#[cfg(feature = "json")]
use qubit_json::encode::JsonEncoder;
use qubit_utils::Transient;
#[cfg(feature = "json")]
use qubit_value::ValueWireEncodePreflight;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use serde::de;
use serde::de::DeserializeSeed;
use serde::ser::Error as SerError;

use super::metadata_filter_builder::MetadataFilterBuilder;
#[cfg(feature = "json")]
use super::wire::METADATA_FILTER_WIRE_VERSION_V1;
use super::wire::MetadataFilterWireV1Ref;
use super::wire::MetadataFilterWireV1Seed;
#[cfg(feature = "schema")]
use crate::Condition;
use crate::FilterExpression;
#[cfg(feature = "json")]
use crate::FilterExpressionView;
use crate::FilterLimits;
use crate::FilterMatchOptions;
use crate::Metadata;
#[cfg(feature = "schema")]
use crate::MetadataResult;
#[cfg(feature = "json")]
use crate::metadata_limits::MetadataLimits;

/// An expression, its matching policy, and its resource limits.
///
/// Boolean composition belongs to [`FilterExpression`]. This type only binds
/// an already-built expression to the options and limits used to evaluate it.
///
/// # Examples
///
/// ```
/// use qubit_metadata::FilterExpression;
/// use qubit_metadata::Metadata;
/// use qubit_metadata::MetadataFilter;
///
/// # fn main() -> qubit_metadata::MetadataResult<()> {
/// let metadata = Metadata::new().with("tenant", "acme");
/// let expression = FilterExpression::builder()
///     .eq("tenant", "acme")
///     .build()?;
/// let filter = MetadataFilter::builder().expression(expression).build()?;
/// assert!(filter.matches(&metadata));
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct MetadataFilter {
    /// Root Boolean expression.
    expression: FilterExpression,
    /// Evaluation options.
    options: FilterMatchOptions,
    /// Bounds that the expression satisfies.
    limits: Transient<FilterLimits>,
}

impl MetadataFilter {
    /// Creates a filter that matches every metadata object.
    ///
    /// # Returns
    ///
    /// A constant-true filter using default match options and library hard
    /// limits.
    #[inline]
    #[must_use = "the constructed all-matching filter should be used"]
    pub fn all() -> Self {
        Self::new(
            FilterExpression::match_all(),
            FilterMatchOptions::default(),
            FilterLimits::MAX,
        )
    }

    /// Creates a filter that matches no metadata object.
    ///
    /// # Returns
    ///
    /// A constant-false filter using default match options and library hard
    /// limits.
    #[inline]
    #[must_use = "the constructed no-match filter should be used"]
    pub fn none() -> Self {
        Self::new(
            FilterExpression::match_none(),
            FilterMatchOptions::default(),
            FilterLimits::MAX,
        )
    }

    /// Creates a builder for a metadata filter.
    #[inline]
    #[must_use]
    pub const fn builder() -> MetadataFilterBuilder {
        MetadataFilterBuilder::new()
    }

    /// Deserializes a filter and validates a receiver-controlled AST bound.
    ///
    /// The receiver bound is charged while the AST is being decoded. Callers
    /// must separately bound raw input bytes and any deserializer-specific
    /// resources before accepting untrusted data.
    ///
    /// # Parameters
    ///
    /// * `deserializer` - Source of the versioned filter representation.
    /// * `receiver_limits` - Local upper bounds applied to the decoded AST.
    ///
    /// # Errors
    ///
    /// Returns a deserialization error for malformed V1 data or an unsupported
    /// version. The underlying deserializer's error type receives the
    /// structured filter-limit or policy failure through `D::Error::custom`;
    /// this generic API cannot return [`crate::MetadataWireDecodeError`]
    /// directly.
    pub fn deserialize_with_filter_limits<'de, D>(
        deserializer: D,
        receiver_limits: FilterLimits,
    ) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let error_slot = Rc::new(RefCell::new(None));
        let wire = MetadataFilterWireV1Seed::new(receiver_limits, Rc::clone(&error_slot))
            .deserialize(deserializer)
            .map_err(|error| {
                error_slot
                    .borrow_mut()
                    .take()
                    .map_or_else(|| de::Error::custom(error), de::Error::custom)
            })?;
        wire.into_filter(receiver_limits).map_err(de::Error::custom)
    }

    /// Decodes a strict metadata-filter JSON envelope using the default wire
    /// byte and receiver AST limits.
    ///
    /// # Parameters
    ///
    /// * `input` - Complete untrusted JSON input.
    ///
    /// # Returns
    ///
    /// The decoded filter.
    ///
    /// # Errors
    ///
    /// Returns an input-size error before parsing, a nested-value limit error,
    /// `UnsupportedVersion` for a version mismatch, a structured
    /// filter-contract error, or `InvalidJson` for malformed strict filter
    /// input and receiver-limit failures found during incremental decoding.
    #[cfg(feature = "json")]
    #[inline]
    pub fn decode_json_slice(input: &[u8]) -> Result<Self, crate::MetadataWireDecodeError> {
        Self::decode_json_slice_with_limits(input, MetadataLimits::default(), FilterLimits::MAX)
    }

    /// Decodes a strict metadata-filter JSON envelope after validating both
    /// wire-byte and receiver-controlled AST limits.
    ///
    /// # Parameters
    ///
    /// * `input` - Complete untrusted JSON input.
    /// * `limits` - Shared JSON limits for this decoding session.
    /// * `receiver_filter_limits` - Local AST limits validated after decoding.
    ///
    /// # Returns
    ///
    /// The decoded filter constrained by receiver-controlled limits.
    ///
    /// # Errors
    ///
    /// Returns an input-size error before parsing, `UnsupportedVersion` for a
    /// version mismatch, a structured filter-contract error in `Filter`, or
    /// `InvalidJson` for syntax, strict-envelope, nested value, and
    /// receiver-limit failures. Receiver AST limits are charged while the
    /// expression tree is read; generic JSON traversal is handled by the
    /// shared budget adapter. Individual
    /// JSON strings and embedded value payloads remain bounded by the outer
    /// input-byte limit.
    #[cfg(feature = "json")]
    pub fn decode_json_slice_with_limits(
        input: &[u8],
        limits: MetadataLimits,
        receiver_filter_limits: FilterLimits,
    ) -> Result<Self, crate::MetadataWireDecodeError> {
        limits
            .validate()
            .map_err(crate::MetadataWireDecodeError::InvalidLimits)?;
        let mut decoder = JsonDecoder::new(JsonDecodeSession::from_limits(limits.json_decode()));
        let error_slot = Rc::new(RefCell::new(None));
        let wire = decoder
            .decode_seed_utf8(
                MetadataFilterWireV1Seed::new(receiver_filter_limits, Rc::clone(&error_slot)),
                input,
            )
            .map_err(|error| {
                error_slot.borrow_mut().take().map_or_else(
                    || Into::<crate::MetadataWireDecodeError>::into(error),
                    crate::MetadataWireDecodeError::Filter,
                )
            })?;
        if wire.version() != METADATA_FILTER_WIRE_VERSION_V1 {
            return Err(crate::MetadataWireDecodeError::UnsupportedVersion {
                expected: METADATA_FILTER_WIRE_VERSION_V1,
                actual: wire.version(),
            });
        }
        wire.into_filter(receiver_filter_limits)
            .map_err(crate::MetadataWireDecodeError::Filter)
    }

    /// Encodes this filter with the default JSON budget profile.
    #[cfg(feature = "json")]
    pub fn to_json_vec(&self) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
        self.to_json_vec_with_limits(crate::metadata_limits::default_json_encode_limits())
    }

    /// Encodes this filter with caller-provided JSON budgets.
    ///
    /// # Parameters
    ///
    /// * `limits` - Output and JSON-value budgets for this operation.
    ///
    /// # Errors
    ///
    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
    /// budget or the filter cannot be represented by the V1 wire format.
    #[cfg(feature = "json")]
    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
        self.preflight_wire_values(limits)?;
        let session = JsonEncodeSession::from_limits(limits);
        JsonEncoder::new(session).to_vec(self).map_err(Into::into)
    }

    /// Encodes this filter to a writer with the default JSON budget profile.
    #[cfg(feature = "json")]
    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), crate::MetadataWireEncodeError>
    where
        W: Write,
    {
        self.to_json_writer_with_limits(writer, crate::metadata_limits::default_json_encode_limits())
    }

    /// Encodes this filter to a writer with caller-provided JSON budgets.
    ///
    /// # Parameters
    ///
    /// * `writer` - Destination receiving the compact JSON document.
    /// * `limits` - Output and JSON-value budgets for this operation.
    ///
    /// # Errors
    ///
    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
    /// budget, the filter cannot be represented by the V1 wire format, or the
    /// writer rejects the output.
    #[cfg(feature = "json")]
    pub fn to_json_writer_with_limits<W>(
        &self,
        writer: W,
        limits: JsonEncodeLimits,
    ) -> Result<(), crate::MetadataWireEncodeError>
    where
        W: Write,
    {
        self.preflight_wire_values(limits)?;
        let session = JsonEncodeSession::from_limits(limits);
        JsonEncoder::new(session)
            .write_buffered(writer, self)
            .map_err(Into::into)
    }

    /// Creates a filter from already validated parts.
    #[inline]
    pub(crate) const fn new(expression: FilterExpression, options: FilterMatchOptions, limits: FilterLimits) -> Self {
        Self {
            expression,
            options,
            limits: Transient::new(limits),
        }
    }

    /// Returns the root expression.
    #[inline]
    #[must_use = "the filter expression should be inspected"]
    pub const fn expression(&self) -> &FilterExpression {
        &self.expression
    }

    /// Returns the evaluation options.
    #[inline]
    #[must_use]
    pub const fn options(&self) -> FilterMatchOptions {
        self.options
    }

    /// Returns the resource limits.
    #[inline]
    #[must_use = "the filter limits should be inspected"]
    pub const fn limits(&self) -> FilterLimits {
        *self.limits.get()
    }

    /// Preflights every operand against the shared JSON encoding budget.
    #[cfg(feature = "json")]
    fn preflight_wire_values(&self, limits: JsonEncodeLimits) -> Result<(), crate::MetadataWireEncodeError> {
        fn visit(
            expression: &FilterExpression,
            checker: &mut ValueWireEncodePreflight,
        ) -> Result<(), crate::MetadataWireEncodeError> {
            match expression.view() {
                FilterExpressionView::Condition(condition) => condition.visit_operands(&mut |value| {
                    checker.check_value(value).map_err(crate::MetadataWireEncodeError::from)
                }),
                FilterExpressionView::And(children) | FilterExpressionView::Or(children) => {
                    for child in children {
                        visit(child, checker)?;
                    }
                    Ok(())
                }
                FilterExpressionView::Not(inner) => visit(inner, checker),
                FilterExpressionView::True | FilterExpressionView::False => Ok(()),
            }
        }
        let mut checker = ValueWireEncodePreflight::new(limits);
        visit(&self.expression, &mut checker)
    }

    /// Returns whether `metadata` satisfies this filter.
    #[inline]
    #[must_use]
    pub fn matches(&self, metadata: &Metadata) -> bool {
        self.expression.evaluate(metadata, self.options).is_match()
    }

    /// Visits every leaf condition in the expression.
    ///
    /// # Errors
    ///
    /// Returns the first error produced by `visitor`.
    #[cfg(feature = "schema")]
    #[inline]
    pub(crate) fn visit_conditions<F>(&self, mut visitor: F) -> MetadataResult<()>
    where
        F: FnMut(&Condition) -> MetadataResult<()>,
    {
        self.expression.visit_conditions(&mut visitor)
    }
}

impl Serialize for MetadataFilter {
    /// Serializes this filter through its versioned wire representation.
    #[inline]
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        MetadataFilterWireV1Ref::try_from(self)
            .map_err(<S::Error as SerError>::custom)?
            .serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for MetadataFilter {
    /// Deserializes a filter using library hard limits as receiver limits.
    #[inline]
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Self::deserialize_with_filter_limits(deserializer, FilterLimits::MAX)
    }
}