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
next prev parent 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