Skip to main content

qubit_metadata/filter/
condition.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//! A single comparison predicate against one metadata key.
9
10use std::cmp::Ordering;
11use std::fmt;
12
13use qubit_datatype::NumericComparisonPolicy;
14use qubit_redact::Redact;
15use qubit_redact::RedactionWriter;
16use qubit_redact::Redactor;
17use qubit_redact::Sensitivity;
18use qubit_value::Value;
19use qubit_value::ValueRef;
20use qubit_value::ValueWirePayloadRefV1;
21
22use super::internal::MatchOutcome;
23use crate::FilterLimitKind;
24use crate::FilterLimits;
25use crate::Metadata;
26use crate::MetadataError;
27use crate::MetadataResult;
28
29/// A single comparison operator applied to one metadata key.
30///
31/// Equality, range, and membership conditions require a concrete stored value.
32/// An absent key or [`Value::Unset`] produces an unknown outcome. Comparisons
33/// of concrete values can also be unknown when their types or operations are
34/// incompatible or a numeric comparison fails; compatible numeric types follow
35/// the configured [`NumericComparisonPolicy`], and values are not implicitly
36/// converted. Unknown remains unknown under logical negation, so `ne`, negated
37/// comparisons, and `not_in_set` cannot bypass an incomparable type.
38/// Existence conditions define presence in terms of a concrete value and return
39/// definite Boolean outcomes. Membership conditions compare candidates
40/// individually and prefer a definite result over an unknown candidate.
41///
42/// # Examples
43///
44/// ```
45/// use qubit_metadata::Condition;
46///
47/// let condition = Condition::Equal {
48///     key: "tenant".to_owned(),
49///     value: "acme".into(),
50/// };
51/// assert!(matches!(condition, Condition::Equal { .. }));
52/// ```
53#[derive(Clone, PartialEq)]
54#[must_use = "the constructed filter condition should be used"]
55#[non_exhaustive]
56pub enum Condition {
57    /// Key equals value.
58    Equal {
59        /// The metadata key.
60        key: String,
61        /// The expected value.
62        value: Value,
63    },
64    /// Key does not equal value.
65    NotEqual {
66        /// The metadata key.
67        key: String,
68        /// The value to compare against.
69        value: Value,
70    },
71    /// Key is less than value.
72    Less {
73        /// The metadata key.
74        key: String,
75        /// The upper bound (exclusive).
76        value: Value,
77    },
78    /// Key is less than or equal to value.
79    LessEqual {
80        /// The metadata key.
81        key: String,
82        /// The upper bound (inclusive).
83        value: Value,
84    },
85    /// Key is greater than value.
86    Greater {
87        /// The metadata key.
88        key: String,
89        /// The lower bound (exclusive).
90        value: Value,
91    },
92    /// Key is greater than or equal to value.
93    GreaterEqual {
94        /// The metadata key.
95        key: String,
96        /// The lower bound (inclusive).
97        value: Value,
98    },
99    /// The stored value is one of the listed candidates.
100    In {
101        /// The metadata key.
102        key: String,
103        /// The set of acceptable values.
104        values: Vec<Value>,
105    },
106    /// The stored value is not any of the listed candidates.
107    NotIn {
108        /// The metadata key.
109        key: String,
110        /// The set of excluded values.
111        values: Vec<Value>,
112    },
113    /// The key stores a concrete value.
114    ///
115    /// An absent key or a key storing [`Value::Unset`] does not exist for
116    /// filter matching.
117    Exists {
118        /// The metadata key.
119        key: String,
120    },
121    /// The key does not store a concrete value.
122    ///
123    /// An absent key or a key storing [`Value::Unset`] satisfies this
124    /// condition.
125    NotExists {
126        /// The metadata key.
127        key: String,
128    },
129}
130
131impl Condition {
132    /// Visits each value operand in wire order without allocating.
133    ///
134    /// # Type Parameters
135    ///
136    /// * `E` - Error type returned by the visitor callback.
137    ///
138    /// # Parameters
139    ///
140    /// * `visitor` - Callback invoked for each operand in wire order.
141    ///
142    /// # Errors
143    ///
144    /// Returns the first error produced by `visitor` and stops visiting.
145    #[cfg(feature = "json")]
146    pub(crate) fn visit_operands<E>(&self, visitor: &mut impl FnMut(&Value) -> Result<(), E>) -> Result<(), E> {
147        match self {
148            Self::Equal { value, .. }
149            | Self::NotEqual { value, .. }
150            | Self::Less { value, .. }
151            | Self::LessEqual { value, .. }
152            | Self::Greater { value, .. }
153            | Self::GreaterEqual { value, .. } => visitor(value),
154            Self::In { values, .. } | Self::NotIn { values, .. } => {
155                for value in values {
156                    visitor(value)?;
157                }
158                Ok(())
159            }
160            Self::Exists { .. } | Self::NotExists { .. } => Ok(()),
161        }
162    }
163
164    /// Validates that every comparison operand has stable matching and wire
165    /// serialization semantics.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`MetadataError::InvalidFilterOperand`] when an operand is
170    /// unset or cannot be represented by the V1 wire format.
171    pub(crate) fn validate_operands(&self) -> MetadataResult<()> {
172        match self {
173            Self::Equal { value, .. } => validate_operand("eq", value),
174            Self::NotEqual { value, .. } => validate_operand("ne", value),
175            Self::Less { value, .. } => validate_operand("lt", value),
176            Self::LessEqual { value, .. } => validate_operand("le", value),
177            Self::Greater { value, .. } => validate_operand("gt", value),
178            Self::GreaterEqual { value, .. } => validate_operand("ge", value),
179            Self::In { values, .. } => validate_operands("in_set", values),
180            Self::NotIn { values, .. } => validate_operands("not_in_set", values),
181            Self::Exists { .. } | Self::NotExists { .. } => Ok(()),
182        }
183    }
184
185    /// Validates this condition against resource limits.
186    ///
187    /// # Parameters
188    ///
189    /// * `limits` - Bounds to enforce.
190    ///
191    /// # Returns
192    ///
193    /// `Ok(())` when the key and membership values fit within `limits`.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`MetadataError::FilterLimitExceeded`] when a key or membership
198    /// condition exceeds its configured bound.
199    pub(crate) fn validate_limits(&self, limits: FilterLimits) -> MetadataResult<()> {
200        let key = self.key();
201        if key.len() > limits.max_key_bytes() {
202            return Err(MetadataError::FilterLimitExceeded {
203                kind: FilterLimitKind::KeyBytes,
204                value: key.len(),
205                maximum: limits.max_key_bytes(),
206            });
207        }
208        let values = match self {
209            Self::In { values, .. } | Self::NotIn { values, .. } => values,
210            _ => return Ok(()),
211        };
212        if values.len() > limits.max_set_values() {
213            return Err(MetadataError::FilterLimitExceeded {
214                kind: FilterLimitKind::SetValues,
215                value: values.len(),
216                maximum: limits.max_set_values(),
217            });
218        }
219        Ok(())
220    }
221
222    /// Evaluates this condition against the supplied metadata.
223    ///
224    /// # Parameters
225    ///
226    /// * `meta` - Metadata object being matched.
227    /// * `numeric_comparison_policy` - Policy for mixed numeric comparisons.
228    ///
229    /// # Returns
230    ///
231    /// A definite outcome for concrete comparisons and existence predicates,
232    /// or [`MatchOutcome::Unknown`] when a comparison depends on an absent or
233    /// unset value.
234    pub(crate) fn evaluate(&self, meta: &Metadata, numeric_comparison_policy: NumericComparisonPolicy) -> MatchOutcome {
235        match self {
236            Condition::Equal { key, value } => evaluate_concrete(meta, key, |stored| {
237                values_equal(stored, value, numeric_comparison_policy)
238                    .map_or(MatchOutcome::Unknown, MatchOutcome::from_bool)
239            }),
240            Condition::NotEqual { key, value } => evaluate_concrete(meta, key, |stored| {
241                values_equal(stored, value, numeric_comparison_policy)
242                    .map_or(MatchOutcome::Unknown, |equal| MatchOutcome::from_bool(!equal))
243            }),
244            Condition::Less { key, value } => evaluate_concrete(meta, key, |stored| {
245                compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
246                    MatchOutcome::from_bool(ordering == Ordering::Less)
247                })
248            }),
249            Condition::LessEqual { key, value } => evaluate_concrete(meta, key, |stored| {
250                compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
251                    MatchOutcome::from_bool(matches!(ordering, Ordering::Less | Ordering::Equal))
252                })
253            }),
254            Condition::Greater { key, value } => evaluate_concrete(meta, key, |stored| {
255                compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
256                    MatchOutcome::from_bool(ordering == Ordering::Greater)
257                })
258            }),
259            Condition::GreaterEqual { key, value } => evaluate_concrete(meta, key, |stored| {
260                compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
261                    MatchOutcome::from_bool(matches!(ordering, Ordering::Greater | Ordering::Equal))
262                })
263            }),
264            Condition::In { key, values } => evaluate_concrete(meta, key, |stored| {
265                evaluate_membership(stored, values, numeric_comparison_policy, false)
266            }),
267            Condition::NotIn { key, values } => evaluate_concrete(meta, key, |stored| {
268                evaluate_membership(stored, values, numeric_comparison_policy, true)
269            }),
270            Condition::Exists { key } => MatchOutcome::from_bool(concrete_value(meta, key).is_some()),
271            Condition::NotExists { key } => MatchOutcome::from_bool(concrete_value(meta, key).is_none()),
272        }
273    }
274
275    /// Returns the metadata key referenced by this condition.
276    #[must_use]
277    #[inline]
278    fn key(&self) -> &str {
279        match self {
280            Self::Equal { key, .. }
281            | Self::NotEqual { key, .. }
282            | Self::Less { key, .. }
283            | Self::LessEqual { key, .. }
284            | Self::Greater { key, .. }
285            | Self::GreaterEqual { key, .. }
286            | Self::In { key, .. }
287            | Self::NotIn { key, .. }
288            | Self::Exists { key }
289            | Self::NotExists { key } => key,
290        }
291    }
292}
293
294/// Validates one filter comparison operand.
295///
296/// # Parameters
297///
298/// * `operator` - Stable name of the operator using the operand.
299/// * `value` - Operand to validate.
300///
301/// # Errors
302///
303/// Returns [`MetadataError::InvalidFilterOperand`] when `value` is unset or
304/// cannot be represented by the V1 wire format.
305pub(super) fn validate_operand(operator: &'static str, value: &Value) -> MetadataResult<()> {
306    if value.is_unset() {
307        return Err(MetadataError::InvalidFilterOperand {
308            operator,
309            data_type: value.data_type(),
310            message: "filter operands must be concrete values".to_owned(),
311        });
312    }
313    if ValueWirePayloadRefV1::try_from(value).is_err() {
314        return Err(MetadataError::InvalidFilterOperand {
315            operator,
316            data_type: value.data_type(),
317            message: "filter operands must be representable by the V1 wire format".to_owned(),
318        });
319    }
320    Ok(())
321}
322
323/// Validates every operand in a set-membership condition.
324///
325/// # Parameters
326///
327/// * `operator` - Stable name of the set operator.
328/// * `values` - Candidate values to validate.
329///
330/// # Errors
331///
332/// Returns the first invalid operand error.
333fn validate_operands(operator: &'static str, values: &[Value]) -> MetadataResult<()> {
334    values.iter().try_for_each(|value| validate_operand(operator, value))
335}
336
337impl Redact for Condition {
338    /// Writes a diagnostic condition representation under the active policy.
339    ///
340    /// The discriminant is inspected and an optional operand is borrowed
341    /// before the record node is entered. The record and synthetic operator
342    /// field consume node budget, as do admitted key and operand fields.
343    /// Field formatting is deferred until admission; opaque masking skips
344    /// operand formatting. A node-budget rejection adds a structural
345    /// truncation field and stops the branch before formatting that field.
346    ///
347    /// # Parameters
348    ///
349    /// * `writer` - Structured destination carrying the active policy and
350    ///   cumulative domain budgets.
351    fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
352        let (operator, operand): (&str, Option<&dyn fmt::Debug>) = match self {
353            Self::Equal { value, .. } => ("equal", Some(value)),
354            Self::NotEqual { value, .. } => ("not_equal", Some(value)),
355            Self::Less { value, .. } => ("less", Some(value)),
356            Self::LessEqual { value, .. } => ("less_equal", Some(value)),
357            Self::Greater { value, .. } => ("greater", Some(value)),
358            Self::GreaterEqual { value, .. } => ("greater_equal", Some(value)),
359            Self::In { values, .. } => ("in", Some(values)),
360            Self::NotIn { values, .. } => ("not_in", Some(values)),
361            Self::Exists { .. } => ("exists", None),
362            Self::NotExists { .. } => ("not_exists", None),
363        };
364        writer.record("Condition", |fields| {
365            fields.unredacted("operator", || operator);
366            fields.unredacted("key", || self.key());
367            if let Some(operand) = operand {
368                fields.sensitive_at_least(Sensitivity::Secret, "value", || operand);
369            }
370        });
371    }
372}
373
374impl fmt::Debug for Condition {
375    /// Writes the strict-policy diagnostic representation.
376    /// # Parameters
377    ///
378    /// * `formatter` - Formatter that receives the output.
379    #[inline]
380    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381        let output = Redactor::strict().redact_text(self);
382        let text = output.text_or_marker("<redaction incomplete>");
383        formatter.write_str(text.as_ref())
384    }
385}
386
387/// Evaluates a predicate that requires a concrete stored value.
388///
389/// # Type Parameters
390///
391/// * `F` - Type used by this operation.
392///
393/// # Parameters
394///
395/// * `meta` - Metadata object being matched.
396/// * `key` - Metadata key to inspect.
397/// * `predicate` - Comparison to apply to a concrete value.
398///
399/// # Returns
400///
401/// The predicate result, or [`MatchOutcome::Unknown`] when the key is absent
402/// or stores [`Value::Unset`].
403#[inline]
404fn evaluate_concrete<F>(meta: &Metadata, key: &str, predicate: F) -> MatchOutcome
405where
406    F: FnOnce(&Value) -> MatchOutcome,
407{
408    concrete_value(meta, key).map_or(MatchOutcome::Unknown, predicate)
409}
410
411/// Evaluates one membership condition while preserving unknown comparisons.
412///
413/// # Parameters
414///
415/// * `stored` - Concrete metadata value being matched.
416/// * `candidates` - Values accepted or excluded by the condition.
417/// * `numeric_comparison_policy` - Policy for mixed numeric comparisons.
418/// * `negated` - Whether a matching candidate means the condition is false.
419///
420/// # Returns
421///
422/// `Unknown` when no candidate matches but at least one candidate cannot be
423/// compared to `stored`; otherwise the normal inclusion or exclusion result.
424fn evaluate_membership(
425    stored: &Value,
426    candidates: &[Value],
427    numeric_comparison_policy: NumericComparisonPolicy,
428    negated: bool,
429) -> MatchOutcome {
430    let mut unknown = false;
431    for candidate in candidates {
432        match values_equal(stored, candidate, numeric_comparison_policy) {
433            Some(true) => return MatchOutcome::from_bool(!negated),
434            Some(false) => {}
435            None => unknown = true,
436        }
437    }
438    if unknown {
439        MatchOutcome::Unknown
440    } else {
441        MatchOutcome::from_bool(negated)
442    }
443}
444
445/// Returns the concrete metadata value stored under `key`.
446///
447/// # Parameters
448///
449/// * `meta` - Metadata object being matched.
450/// * `key` - Metadata key to inspect.
451///
452/// # Returns
453///
454/// The stored value when it is concrete, or `None` when the key is absent or
455/// stores [`Value::Unset`].
456#[inline]
457fn concrete_value<'a>(meta: &'a Metadata, key: &str) -> Option<&'a Value> {
458    meta.get_raw(key).filter(|value| !value.is_unset())
459}
460
461/// Compares two values for equality, treating numeric variants by numeric
462/// value.
463///
464/// # Parameters
465///
466/// * `left` - Left comparison operand.
467/// * `right` - Right comparison operand.
468/// * `numeric_comparison_policy` - Policy for mixed numeric variants.
469///
470/// # Returns
471///
472/// `Some(true)` when comparable values are equal, `Some(false)` when they are
473/// comparable but unequal, or `None` when concrete types are incompatible or
474/// numeric comparison fails. Callers interpret `None` as an unknown outcome.
475#[inline]
476fn values_equal(left: &Value, right: &Value, numeric_comparison_policy: NumericComparisonPolicy) -> Option<bool> {
477    if left.is_numeric() && right.is_numeric() {
478        return left
479            .numeric_cmp(right, numeric_comparison_policy)
480            .ok()
481            .map(|ordering| ordering == Ordering::Equal);
482    }
483    if left.data_type() != right.data_type() {
484        return None;
485    }
486    Some(left == right)
487}
488
489/// Compares two numeric values or two strings.
490///
491/// # Parameters
492///
493/// * `left` - Left comparison operand.
494/// * `right` - Right comparison operand.
495/// * `numeric_comparison_policy` - Policy for mixed numeric variants.
496///
497/// # Returns
498///
499/// The relative ordering, or `None` when the values cannot be compared.
500#[inline]
501fn compare_values(left: &Value, right: &Value, numeric_comparison_policy: NumericComparisonPolicy) -> Option<Ordering> {
502    if left.is_numeric() && right.is_numeric() {
503        return left.numeric_cmp(right, numeric_comparison_policy).ok();
504    }
505    match (left.view(), right.view()) {
506        (ValueRef::String(left), ValueRef::String(right)) => left.partial_cmp(right),
507        _ => None,
508    }
509}