public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pbs-devel@lists.proxmox.com, pve-devel@lists.proxmox.com
Subject: [PATCH proxmox 01/29] add new proxmox-match-expression crate
Date: Thu,  9 Jul 2026 13:56:48 +0200	[thread overview]
Message-ID: <20260709115716.299836-2-l.wagner@proxmox.com> (raw)
In-Reply-To: <20260709115716.299836-1-l.wagner@proxmox.com>

This new crate is a generic implementation of a serializable expression
language. It supports basic combinators, such as AnyOf, OneOf and AllOf,
constants, Not, as well as custom matcher leave nodes that are injected
via a generic type parameter.

The first user of this implementation will be the notification stack.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                                    |   2 +
 proxmox-match-expression/Cargo.toml           |  18 +
 proxmox-match-expression/debian/changelog     |   6 +
 proxmox-match-expression/debian/control       |  34 ++
 proxmox-match-expression/debian/copyright     |  18 +
 proxmox-match-expression/debian/debcargo.toml |   7 +
 proxmox-match-expression/src/lib.rs           | 511 ++++++++++++++++++
 7 files changed, 596 insertions(+)
 create mode 100644 proxmox-match-expression/Cargo.toml
 create mode 100644 proxmox-match-expression/debian/changelog
 create mode 100644 proxmox-match-expression/debian/control
 create mode 100644 proxmox-match-expression/debian/copyright
 create mode 100644 proxmox-match-expression/debian/debcargo.toml
 create mode 100644 proxmox-match-expression/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index ef407166..fcb06704 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ members = [
     "proxmox-log",
     "proxmox-login",
     "proxmox-metrics",
+    "proxmox-match-expression",
     "proxmox-network-api",
     "proxmox-network-types",
     "proxmox-node-status",
@@ -177,6 +178,7 @@ proxmox-io = { version = "1.2.1", path = "proxmox-io" }
 proxmox-lang = { version = "1.5", path = "proxmox-lang" }
 proxmox-log = { version = "1.0.0", path = "proxmox-log" }
 proxmox-login = { version = "1.0.0", path = "proxmox-login" }
+proxmox-match-expression = { version = "0.1", path = "proxmox-match-expression" }
 proxmox-network-types = { version = "1.0.2", path = "proxmox-network-types" }
 proxmox-parallel-handler = { version = "1.0.0", path = "proxmox-parallel-handler" }
 proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
diff --git a/proxmox-match-expression/Cargo.toml b/proxmox-match-expression/Cargo.toml
new file mode 100644
index 00000000..294537bc
--- /dev/null
+++ b/proxmox-match-expression/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "proxmox-match-expression"
+description = "evaluation logic for nested match expressions"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde = { workspace = true, features = ["derive"] }
+
+[dev-dependencies]
+serde_json.workspace = true
+pretty_assertions.workspace = true
diff --git a/proxmox-match-expression/debian/changelog b/proxmox-match-expression/debian/changelog
new file mode 100644
index 00000000..44b20a10
--- /dev/null
+++ b/proxmox-match-expression/debian/changelog
@@ -0,0 +1,6 @@
+rust-proxmox-match-expression (0.1.0-1) unstable; urgency=medium
+
+  * initial version.
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 18 Jun 2026 14:02:03 +0200
+
diff --git a/proxmox-match-expression/debian/control b/proxmox-match-expression/debian/control
new file mode 100644
index 00000000..14bc8999
--- /dev/null
+++ b/proxmox-match-expression/debian/control
@@ -0,0 +1,34 @@
+Source: rust-proxmox-match-expression
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-match-expression
+
+Package: librust-proxmox-match-expression-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev
+Provides:
+ librust-proxmox-match-expression+default-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0+default-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0.1-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-match-expression-0.1.0+default-dev (= ${binary:Version})
+Description: Evaluation logic for nested match expressions - Rust source code
+ Source code for Debianized Rust crate "proxmox-match-expression"
diff --git a/proxmox-match-expression/debian/copyright b/proxmox-match-expression/debian/copyright
new file mode 100644
index 00000000..279c2689
--- /dev/null
+++ b/proxmox-match-expression/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2024 - 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-match-expression/debian/debcargo.toml b/proxmox-match-expression/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-match-expression/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-match-expression/src/lib.rs b/proxmox-match-expression/src/lib.rs
new file mode 100644
index 00000000..0b9b0208
--- /dev/null
+++ b/proxmox-match-expression/src/lib.rs
@@ -0,0 +1,511 @@
+//! De/serializable expression evaluation with custom leaf-nodes.
+//!
+//! ## Example:
+//!```
+//! use serde::{Serialize, Deserialize};
+//! use pretty_assertions::{assert_eq, assert_str_eq};
+//!
+//! use proxmox_match_expression::{any_of, Expression, MatchExpression};
+//!
+//! #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+//! #[serde(rename_all = "kebab-case", tag = "type")]
+//! enum StringMatcher {
+//!     AllUppercase,
+//!     Content { value: String },
+//! }
+//!
+//! impl MatchExpression for StringMatcher {
+//!     type Data = String;
+//!     type Error = ();
+//!
+//!    fn evaluate(&self, data: &Self::Data) -> Result<bool, ()> {
+//!         match self {
+//!             StringMatcher::AllUppercase => Ok(&data.to_uppercase() == data),
+//!             StringMatcher::Content { value } => Ok(value == data),
+//!         }
+//!     }
+//! }
+//!
+//! impl From<StringMatcher> for Expression<StringMatcher> {
+//!     fn from(matcher: StringMatcher) -> Self {
+//!         Expression::Match(matcher)
+//!     }
+//! }
+//!
+//! // You can use macros to conveniently construct expressions in code.
+//! let expr = any_of![
+//!     StringMatcher::Content {
+//!         value: "foo".to_string(),
+//!     }
+//!     .into(),
+//!     StringMatcher::AllUppercase.into(),
+//! ];
+//!
+//! // Expressions implement `[Serialize]` and `[Deserialize]`
+//! let expected_serialized_expr = r#"{
+//!  "any-of": [
+//!    {
+//!      "match": {
+//!        "type": "content",
+//!        "value": "foo"
+//!      }
+//!    },
+//!    {
+//!      "match": {
+//!        "type": "all-uppercase"
+//!      }
+//!    }
+//!  ]
+//!}"#;
+//!
+//! assert_eq!(serde_json::to_string_pretty(&expr).unwrap(), expected_serialized_expr);
+//!
+//! assert!(expr.evaluate(&"foo".to_string()).unwrap().is_match());
+//! assert!(!expr.evaluate(&"fo".to_string()).unwrap().is_match());
+//! assert!(!expr.evaluate(&"fooo".to_string()).unwrap().is_match());
+//!
+//! let evaluated_expression = expr.evaluate(&"AAAAA".to_string()).unwrap();
+//! assert!(evaluated_expression.is_match());
+//!
+//! // `[Expression::evaluate]` returns [`EvaluatedExpression`], which contains a full trace
+//! // of all expressions that were evaluated.
+//!
+//! let serialized = serde_json::to_string_pretty(&evaluated_expression).unwrap();
+//! let expected = r#"{
+//!  "any-of": [
+//!    {
+//!      "match": {
+//!        "type": "content",
+//!        "value": "foo"
+//!      },
+//!      "matches": false
+//!    },
+//!    {
+//!      "match": {
+//!        "type": "all-uppercase"
+//!      },
+//!      "matches": true
+//!    }
+//!  ],
+//!  "matches": true
+//!}"#;
+//!
+//! assert_str_eq!(serialized, expected);
+//!
+//! assert!(!evaluated_expression.children()[0].is_match());
+//! assert!(evaluated_expression.children()[1].is_match());
+//!
+//! assert_eq!(evaluated_expression.children()[1].children(), &[]);
+//!
+//! ```
+
+use serde::{Deserialize, Serialize};
+
+/// Trait for custom expression leaf-nodes.
+pub trait MatchExpression {
+    /// Use this type to specify the type of the data the expression is evaluated against.
+    type Data;
+
+    /// Error type returned when evaluating this custom expression.
+    type Error;
+
+    /// Evaluate this expression.
+    ///
+    /// If there are no errors, this should return `Ok(true)` or `Ok(false)`, depending on
+    /// whether this leaf-node expression matched the provided data or not.
+    fn evaluate(&self, data: &Self::Data) -> Result<bool, Self::Error>;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+/// Match expression.
+pub enum Expression<M> {
+    /// Match if all of the sub-expression match.
+    AllOf(Vec<Expression<M>>),
+    /// Match if any of the sub-expressions matches.
+    AnyOf(Vec<Expression<M>>),
+    /// Match if exactly one of the sub-expressions matches.
+    OneOf(Vec<Expression<M>>),
+    /// Match if the sub-expression does not match.
+    Not(Box<Expression<M>>),
+    /// Constant value (true or false).
+    Constant(bool),
+    /// Custom matcher leaf-nodes.
+    Match(M),
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+/// Evaluated match expression.
+pub enum EvaluatedExpression<M> {
+    /// Match if all of the sub-expressions match.
+    AllOf(Vec<EvaluatedExpressionWithResult<M>>),
+    /// Match if any of the sub-expressions matches.
+    AnyOf(Vec<EvaluatedExpressionWithResult<M>>),
+    /// Match if exactly one of the sub-expressions matches.
+    OneOf(Vec<EvaluatedExpressionWithResult<M>>),
+    /// Match if the sub-expression does not match.
+    Not(Box<EvaluatedExpressionWithResult<M>>),
+    /// Constant value (true or false).
+    Constant(bool),
+    /// Custom matcher leaf-nodes.
+    Match(M),
+}
+
+/// Evaluated expression resulting from [`Expression::evaluate`].
+///
+/// This type contains the original expression, augmented with per-node
+/// results. This is useful for recording 'traces', e.g. for recording
+/// exactly why a notification matcher matched a notification or not.
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+pub struct EvaluatedExpressionWithResult<M> {
+    /// The actual expression.
+    #[serde(flatten)]
+    expression: EvaluatedExpression<M>,
+    /// The evaluated expression matched the notification.
+    matches: bool,
+}
+
+impl<M> EvaluatedExpressionWithResult<M> {
+    /// Return whether the expression matched the notification.
+    pub fn is_match(&self) -> bool {
+        self.matches
+    }
+
+    /// Borrow the contained [`EvaluatedExpression`].
+    pub fn expression(&self) -> &EvaluatedExpression<M> {
+        &self.expression
+    }
+
+    /// Return the contained [`EvaluatedExpression`], consuming `self`.
+    pub fn into_expression(self) -> EvaluatedExpression<M> {
+        self.expression
+    }
+
+    /// The direct child nodes, uniform across all combinator kinds.
+    ///
+    /// 'not' yields its single operand as a one-element slice; leaf nodes
+    /// ('constant', 'match') yield an empty slice. This lets a generic tree
+    /// walker recurse without special-casing each variant.
+    pub fn children(&self) -> &[EvaluatedExpressionWithResult<M>] {
+        match &self.expression {
+            EvaluatedExpression::AllOf(expressions)
+            | EvaluatedExpression::AnyOf(expressions)
+            | EvaluatedExpression::OneOf(expressions) => expressions,
+            EvaluatedExpression::Not(expression) => std::slice::from_ref(&**expression),
+            EvaluatedExpression::Constant(_) | EvaluatedExpression::Match(_) => &[],
+        }
+    }
+}
+
+impl<M, D, E> Expression<M>
+where
+    M: Clone + MatchExpression<Data = D, Error = E>,
+{
+    /// Evaluate this expression against a provided variable.
+    ///
+    /// This function returns an `EvaluatedExpression`, replicating the exact structure
+    /// of `self`, but annotated with a per-subexpression trace to record which of
+    /// the subexpressions matched or not.
+    ///
+    /// # Note
+    /// This evaluates the expression and *all* sub-expressions fully and does not return
+    /// early if the final result is already determined by the already evaluated sub-expressions
+    /// (for instance, [`Expression::AnyOf`] and the first sub-expression matched).
+    /// This is done so that we can return the full trace via the [`EvaluatedExpressionWithResult`]
+    /// type.
+    ///
+    /// If you need fast evaluation and do not care about the trace, feel free to add a
+    /// `evaluate_fast` (or similar), which does early returns as appropriate and simply returns
+    /// `bool`.
+    pub fn evaluate(&self, data: &D) -> Result<EvaluatedExpressionWithResult<M>, E> {
+        let evaluated_expression = match self {
+            Expression::AllOf(expressions) => {
+                if expressions.is_empty() {
+                    // 'all-of' without any sub-expressions is *not* a match
+                    EvaluatedExpressionWithResult {
+                        expression: EvaluatedExpression::AllOf(Vec::new()),
+                        matches: false,
+                    }
+                } else {
+                    let (matches, evaluated_expressions) =
+                        Self::evaluate_subexpressions(data, expressions, true, |a, b| a && b)?;
+
+                    EvaluatedExpressionWithResult {
+                        expression: EvaluatedExpression::AllOf(evaluated_expressions),
+                        matches,
+                    }
+                }
+            }
+            Expression::AnyOf(expressions) => {
+                let (matches, evaluated_expressions) =
+                    Self::evaluate_subexpressions(data, expressions, false, |a, b| a || b)?;
+
+                EvaluatedExpressionWithResult {
+                    expression: EvaluatedExpression::AnyOf(evaluated_expressions),
+                    matches,
+                }
+            }
+            Expression::OneOf(expressions) => {
+                let mut matched_count = 0;
+                let mut evaluated_expressions = Vec::new();
+
+                for expression in expressions {
+                    let evaluated_expression = expression.evaluate(data)?;
+                    if evaluated_expression.is_match() {
+                        matched_count += 1;
+                    }
+                    evaluated_expressions.push(evaluated_expression)
+                }
+
+                EvaluatedExpressionWithResult {
+                    expression: EvaluatedExpression::OneOf(evaluated_expressions),
+                    matches: matched_count == 1,
+                }
+            }
+            Expression::Not(expression) => {
+                let evaluated_expression = expression.evaluate(data)?;
+                let matches = !evaluated_expression.is_match();
+                EvaluatedExpressionWithResult {
+                    expression: EvaluatedExpression::Not(Box::new(evaluated_expression)),
+                    matches,
+                }
+            }
+            Expression::Constant(value) => EvaluatedExpressionWithResult {
+                expression: EvaluatedExpression::Constant(*value),
+                matches: *value,
+            },
+            Expression::Match(inner) => {
+                let matches = inner.evaluate(data)?;
+                EvaluatedExpressionWithResult {
+                    expression: EvaluatedExpression::Match(inner.clone()),
+                    matches,
+                }
+            }
+        };
+
+        Ok(evaluated_expression)
+    }
+
+    fn evaluate_subexpressions(
+        data: &D,
+        expressions: &[Expression<M>],
+        initial: bool,
+        op: impl Fn(bool, bool) -> bool,
+    ) -> Result<(bool, Vec<EvaluatedExpressionWithResult<M>>), E> {
+        let mut matches = initial;
+
+        let mut evaluated_expressions = Vec::new();
+
+        for expression in expressions {
+            let evaluated_expression = expression.evaluate(data)?;
+            matches = op(matches, evaluated_expression.is_match());
+            evaluated_expressions.push(evaluated_expression);
+        }
+
+        Ok((matches, evaluated_expressions))
+    }
+}
+
+/// Convenience macro that can be used to create an 'AnyOf' expression.
+///
+/// ```
+/// use proxmox_match_expression::{any_of, Expression};
+///
+/// let x: Expression<()> = any_of![
+///     Expression::Constant(true),
+/// ];
+/// assert_eq!(x, Expression::AnyOf(vec![Expression::Constant(true)]));
+/// ```
+///
+#[macro_export]
+macro_rules! any_of {
+    ($($x:expr),* $(,)?) => (
+        $crate::Expression::AnyOf(vec![$($x),*])
+    );
+}
+
+/// Convenience macro that can be used to create an 'AllOf' expression.
+///
+/// ```
+/// use proxmox_match_expression::{all_of, Expression};
+///
+/// let x: Expression<()> = all_of![
+///     Expression::Constant(true),
+/// ];
+/// assert_eq!(x, Expression::AllOf(vec![Expression::Constant(true)]));
+/// ```
+#[macro_export]
+macro_rules! all_of {
+    ($($x:expr),* $(,)?) => (
+        $crate::Expression::AllOf(vec![$($x),*])
+    );
+}
+
+/// Convenience macro that can be used to create an 'OneOf' expression.
+///
+/// ```
+/// use proxmox_match_expression::{one_of, Expression};
+///
+/// let x: Expression<()> = one_of![
+///     Expression::Constant(true),
+/// ];
+/// assert_eq!(x, Expression::OneOf(vec![Expression::Constant(true)]));
+/// ```
+#[macro_export]
+macro_rules! one_of {
+    ($($x:expr),* $(,)?) => (
+        $crate::Expression::OneOf(vec![$($x),*])
+    );
+}
+
+/// Convenience macro that can be used to create a 'Not' expression;
+///
+/// ```
+/// use proxmox_match_expression::{not, Expression};
+///
+/// let x: Expression<()> = not!(Expression::Constant(true));
+/// assert_eq!(x, Expression::Not(Box::new(Expression::Constant(true))));
+/// ```
+#[macro_export]
+macro_rules! not {
+    ($e:expr) => {
+        $crate::Expression::Not(Box::new($e))
+    };
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[derive(Serialize, Deserialize, Debug, Clone)]
+    #[serde(rename_all = "kebab-case", tag = "type")]
+    pub enum TestMatcher {
+        CustomFailure,
+        CustomTrue,
+        CustomFalse,
+    }
+
+    impl MatchExpression for TestMatcher {
+        type Data = ();
+        type Error = ();
+
+        fn evaluate(&self, _data: &Self::Data) -> Result<bool, ()> {
+            match self {
+                TestMatcher::CustomFailure => Err(()),
+                TestMatcher::CustomTrue => Ok(true),
+                TestMatcher::CustomFalse => Ok(false),
+            }
+        }
+    }
+
+    fn eval(expr: Expression<TestMatcher>) -> Result<bool, ()> {
+        let e = expr.evaluate(&())?;
+
+        Ok(e.is_match())
+    }
+
+    #[test]
+    fn test_all_of() {
+        let expr = all_of![Expression::Constant(true), Expression::Constant(false)];
+        assert!(!eval(expr).unwrap());
+
+        let expr = all_of![Expression::Constant(true), Expression::Constant(true)];
+        assert!(eval(expr).unwrap());
+
+        let expr = all_of![Expression::Constant(true)];
+        assert!(eval(expr).unwrap());
+
+        let expr: Expression<TestMatcher> = all_of![];
+        assert!(!eval(expr).unwrap());
+
+        let expr = all_of![Expression::Match(TestMatcher::CustomFailure)];
+        assert!(eval(expr).is_err());
+    }
+
+    #[test]
+    fn test_any_of() {
+        let expr = any_of![Expression::Constant(true), Expression::Constant(false)];
+        assert!(eval(expr).unwrap());
+
+        let expr = any_of![Expression::Constant(false), Expression::Constant(false)];
+        assert!(!eval(expr).unwrap());
+
+        let expr = any_of![Expression::Constant(true)];
+        assert!(eval(expr).unwrap());
+
+        let expr: Expression<TestMatcher> = any_of![];
+        assert!(!eval(expr).unwrap());
+
+        let expr = any_of![Expression::Match(TestMatcher::CustomFailure)];
+        assert!(eval(expr).is_err());
+    }
+
+    #[test]
+    fn test_one_of() {
+        let expr = one_of![Expression::Constant(true), Expression::Constant(false)];
+        assert!(eval(expr).unwrap());
+
+        let expr = one_of![Expression::Constant(false)];
+        assert!(!eval(expr).unwrap());
+
+        let expr = one_of![Expression::Constant(true)];
+        assert!(eval(expr).unwrap());
+
+        let expr: Expression<TestMatcher> = one_of![];
+        assert!(!eval(expr).unwrap());
+
+        let expr = one_of![Expression::Match(TestMatcher::CustomFailure)];
+        assert!(eval(expr).is_err());
+
+        // Exactly one of three sub-expressions matches.
+        let expr = one_of![
+            Expression::Constant(false),
+            Expression::Constant(true),
+            Expression::Constant(false),
+        ];
+        assert!(eval(expr).unwrap());
+
+        // Two of three match. This must *not* match: it distinguishes
+        // 'exactly one' from XOR-parity, which would report a match for
+        // any odd number of matching sub-expressions.
+        let expr = one_of![
+            Expression::Constant(true),
+            Expression::Constant(true),
+            Expression::Constant(false),
+        ];
+        assert!(!eval(expr).unwrap());
+
+        // Three of three match. XOR-parity would (incorrectly) report a
+        // match here, since three is odd.
+        let expr = one_of![
+            Expression::Constant(true),
+            Expression::Constant(true),
+            Expression::Constant(true),
+        ];
+        assert!(!eval(expr).unwrap());
+
+        // None of three match.
+        let expr = one_of![
+            Expression::Constant(false),
+            Expression::Constant(false),
+            Expression::Constant(false),
+        ];
+        assert!(!eval(expr).unwrap());
+    }
+
+    #[test]
+    fn test_not() {
+        let expr = not!(Expression::Constant(false));
+        assert!(eval(expr).unwrap());
+    }
+
+    #[test]
+    fn test_constant() {
+        let expr: Expression<TestMatcher> = Expression::Constant(true);
+        assert!(eval(expr).unwrap());
+
+        let expr: Expression<TestMatcher> = Expression::Constant(false);
+        assert!(!eval(expr).unwrap());
+    }
+}
-- 
2.47.3





  reply	other threads:[~2026-07-09 12:02 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-09 11:56 [PATCH many 00/29] notifications: add nested match expressions Lukas Wagner
2026-07-09 11:56 ` Lukas Wagner [this message]
2026-07-09 11:56 ` [PATCH proxmox 02/29] notify: promote matcher to dir-style module Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 03/29] notify: fix doc comment Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 04/29] notify: matcher: break out severity matcher into submodule Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 05/29] notify: matcher: break out field " Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 06/29] notify: matcher: break out calendar " Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 07/29] notify: matcher: calendar: add basic unit test Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 08/29] notify: matcher: add InlineSeverityMatcher Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 09/29] notify: matcher: add InlineFieldMatcher Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 10/29] notify: matcher: add InlineCalendarMatcher Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 11/29] notify: matcher: add expression support Lukas Wagner
2026-07-09 11:56 ` [PATCH proxmox 12/29] notify: api: support new expression parameter Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox 13/29] notify: api: add `get_matcher_as_expression` Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox 14/29] notify: migrate PBS's and PVE's default matcher to expression syntax Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox 15/29] notify: move legacy matcher keys behind feature flag Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-widget-toolkit 16/29] notification: increase matcher window width Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-widget-toolkit 17/29] notifications: matcher: add support for match expressions Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-widget-toolkit 18/29] notification: matcher: add better calendar editor Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-widget-toolkit 19/29] notifications: matcher: consistently use title case for UI elements Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-backup 20/29] notification: opt into 'legacy-matchers' feature in proxmox-notify Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-backup 21/29] api: notification: add 'migrate-to-expression' parameter to get_matcher Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-backup 22/29] ui: notification: enable new matcher UI Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-perl-rs 23/29] notify: matcher: pass matcher config / updater directly Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-perl-rs 24/29] notify: opt into 'legacy-matchers' feature in proxmox-notify Lukas Wagner
2026-07-09 11:57 ` [PATCH proxmox-perl-rs 25/29] notify: add 'migrate_to_expression' parameter for get_matcher Lukas Wagner
2026-07-09 11:57 ` [PATCH manager 26/29] api: notification: pass config/updater directly to rust bindings Lukas Wagner
2026-07-09 11:57 ` [PATCH manager 27/29] api: notification: get_matcher: add 'migrate-to-expression' parameter Lukas Wagner
2026-07-09 11:57 ` [PATCH manager 28/29] api: notification: add 'expression' to matcher parameter schema Lukas Wagner
2026-07-09 11:57 ` [PATCH manager 29/29] ui: notification: enable new matcher UI Lukas Wagner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260709115716.299836-2-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal