From 6b9e01be9187f4d26c79bd45d3dcea087d4ed4bb Mon Sep 17 00:00:00 2001
From: Danil-Grigorev <danil.grigorev@suse.com>
Date: Wed, 17 Jul 2024 16:55:17 +0200
Subject: [PATCH] Implement SelectorExt and a Matcher trait

- Add SelectorExt to the prelude
- Remove Map in favor of more explicit BTreeMap<String, String>
- Add exmples of usage

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>
---
 kube-core/src/labels.rs     | 229 +++++++++++++++++++++++++++++-------
 kube-core/src/lib.rs        |   2 +
 kube-core/src/params.rs     |  35 ++++--
 kube-runtime/src/watcher.rs |  22 +++-
 kube/src/lib.rs             |   5 +-
 5 files changed, 239 insertions(+), 54 deletions(-)

diff --git a/kube-core/src/labels.rs b/kube-core/src/labels.rs
index 449c118ba..03754d176 100644
--- a/kube-core/src/labels.rs
+++ b/kube-core/src/labels.rs
@@ -1,25 +1,122 @@
 #![allow(missing_docs)]
+use core::fmt;
 use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, LabelSelectorRequirement};
 use serde::{Deserialize, Serialize};
 use std::{
     cmp::PartialEq,
     collections::{BTreeMap, BTreeSet},
+    fmt::Display,
     iter::FromIterator,
+    option::IntoIter,
 };
 
+use crate::ResourceExt;
+
 // local type aliases
-type Map = BTreeMap<String, String>;
 type Expressions = Vec<Expression>;
 
+/// Extensions to [`ResourceExt`](crate::ResourceExt)
+/// Helper methods for resource selection based on provided Selector
+pub trait SelectorExt {
+    fn selector_map(&self) -> &BTreeMap<String, String>;
+
+    /// Perform a match on the resource using Matcher trait
+    ///
+    /// ```
+    /// use k8s_openapi::api::core::v1::Pod;
+    /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
+    /// use kube_core::SelectorExt;
+    /// use kube_core::Expression;
+    /// let matches = Pod::default().selector_matches(&LabelSelector::default());
+    /// assert!(matches);
+    /// let matches = Pod::default().selector_matches(&Expression::Exists("foo".into()));
+    /// assert!(!matches);
+    /// ```
+    fn selector_matches(&self, selector: &impl Matcher) -> bool {
+        selector.matches(self.selector_map())
+    }
+}
+
+impl<R: ResourceExt> SelectorExt for R {
+    fn selector_map(&self) -> &BTreeMap<String, String> {
+        self.labels()
+    }
+}
+
+/// Matcher trait for implementing alternalive Selectors
+pub trait Matcher {
+    // Perform a match check on the resource labels
+    fn matches(&self, labels: &BTreeMap<String, String>) -> bool;
+}
+
 /// A selector expression with existing operations
 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
 pub enum Expression {
+    /// Key exists and in set:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::In("foo".into(), ["bar".into(), "baz".into()].into()).to_string();
+    /// assert_eq!(exp, "foo in (bar,baz)");
+    /// let exp = Expression::In("foo".into(), vec!["bar".into(), "baz".into()].into_iter().collect()).to_string();
+    /// assert_eq!(exp, "foo in (bar,baz)");
+    /// ```
     In(String, BTreeSet<String>),
+
+    /// Key does not exists or not in set:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into()).to_string();
+    /// assert_eq!(exp, "foo notin (bar,baz)");
+    /// let exp = Expression::NotIn("foo".into(), vec!["bar".into(), "baz".into()].into_iter().collect()).to_string();
+    /// assert_eq!(exp, "foo notin (bar,baz)");
+    /// ```
     NotIn(String, BTreeSet<String>),
+
+    /// Key exists and is equal:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::Equal("foo".into(), "bar".into()).to_string();
+    /// assert_eq!(exp, "foo=bar")
+    /// ```
     Equal(String, String),
+
+    /// Key does not exists or is not equal:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::NotEqual("foo".into(), "bar".into()).to_string();
+    /// assert_eq!(exp, "foo!=bar")
+    /// ```
     NotEqual(String, String),
+
+    /// Key exists:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::Exists("foo".into()).to_string();
+    /// assert_eq!(exp, "foo")
+    /// ```
     Exists(String),
+
+    /// Key does not exist:
+    ///
+    /// ```
+    /// use kube_core::Expression;
+    ///
+    /// let exp = Expression::DoesNotExist("foo".into()).to_string();
+    /// assert_eq!(exp, "!foo")
+    /// ```
     DoesNotExist(String),
+
+    /// Invalid combination. Always evaluates to false.
     Invalid,
 }
 
@@ -34,27 +131,43 @@ impl Selector {
     }
 
     /// Create a selector from a map of key=value label matches
-    fn from_map(map: Map) -> Self {
+    fn from_map(map: BTreeMap<String, String>) -> Self {
         Self(map.into_iter().map(|(k, v)| Expression::Equal(k, v)).collect())
     }
 
-    /// Convert a selector to a string for the API
-    pub fn to_selector_string(&self) -> String {
-        let selectors: Vec<String> = self
-            .0
-            .iter()
-            .filter(|&e| e != &Expression::Invalid)
-            .map(|e| e.to_string())
-            .collect();
-        selectors.join(",")
-    }
-
     /// Indicates whether this label selector matches all pods
     pub fn selects_all(&self) -> bool {
         self.0.is_empty()
     }
 
-    pub fn matches(&self, labels: &Map) -> bool {
+    /// Extend the list of expressions for the selector
+    ///
+    /// ```
+    /// use kube_core::Selector;
+    /// use kube_core::Expression;
+    /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
+    ///
+    /// Selector::default().extend(Expression::Exists("foo".into()));
+    /// Selector::default().extend(Selector::from_iter(Expression::Exists("foo".into())));
+    /// let label_selector: Selector = LabelSelector::default().into();
+    /// Selector::default().extend(label_selector);
+    /// ```
+    pub fn extend(mut self, exprs: impl IntoIterator<Item = Expression>) -> Self {
+        self.0.extend(exprs);
+        self
+    }
+}
+
+impl Matcher for LabelSelector {
+    fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
+        let selector: Selector = self.clone().into();
+        selector.matches(labels)
+    }
+}
+
+impl Matcher for Selector {
+    // Perform a match check on the resource labels
+    fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
         for expr in self.0.iter() {
             if !expr.matches(labels) {
                 return false;
@@ -64,33 +177,8 @@ impl Selector {
     }
 }
 
-// === Expression ===
-
-impl Expression {
-    /// Perform conversion to string
-    pub fn to_string(&self) -> String {
-        match self {
-            Expression::In(key, values) => {
-                format!(
-                    "{key} in ({})",
-                    values.into_iter().cloned().collect::<Vec<_>>().join(",")
-                )
-            }
-            Expression::NotIn(key, values) => {
-                format!(
-                    "{key} notin ({})",
-                    values.into_iter().cloned().collect::<Vec<_>>().join(",")
-                )
-            }
-            Expression::Equal(key, value) => format!("{key}={value}"),
-            Expression::NotEqual(key, value) => format!("{key}!={value}"),
-            Expression::Exists(key) => format!("{key}"),
-            Expression::DoesNotExist(key) => format!("!{key}"),
-            Expression::Invalid => "".into(),
-        }
-    }
-
-    fn matches(&self, labels: &Map) -> bool {
+impl Matcher for Expression {
+    fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
         match self {
             Expression::In(key, values) => match labels.get(key) {
                 Some(v) => values.contains(v),
@@ -109,8 +197,59 @@ impl Expression {
     }
 }
 
+impl Display for Expression {
+    /// Perform conversion to string
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Expression::In(key, values) => {
+                write!(
+                    f,
+                    "{key} in ({})",
+                    values.iter().cloned().collect::<Vec<_>>().join(",")
+                )
+            }
+            Expression::NotIn(key, values) => {
+                write!(
+                    f,
+                    "{key} notin ({})",
+                    values.iter().cloned().collect::<Vec<_>>().join(",")
+                )
+            }
+            Expression::Equal(key, value) => write!(f, "{key}={value}"),
+            Expression::NotEqual(key, value) => write!(f, "{key}!={value}"),
+            Expression::Exists(key) => write!(f, "{key}"),
+            Expression::DoesNotExist(key) => write!(f, "!{key}"),
+            Expression::Invalid => Ok(()),
+        }
+    }
+}
+
+impl Display for Selector {
+    /// Convert a selector to a string for the API
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let selectors: Vec<String> = self.0.iter().map(|e| e.to_string()).collect();
+        write!(f, "{}", selectors.join(","))
+    }
+}
+// convenience conversions for Selector and Expression
 
-// convenience conversions for Selector
+impl IntoIterator for Expression {
+    type IntoIter = IntoIter<Self::Item>;
+    type Item = Self;
+
+    fn into_iter(self) -> Self::IntoIter {
+        Some(self).into_iter()
+    }
+}
+
+impl IntoIterator for Selector {
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+    type Item = Expression;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
 
 impl FromIterator<(String, String)> for Selector {
     fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
@@ -148,7 +287,7 @@ impl From<LabelSelector> for Selector {
         };
         let mut equality: Selector = value
             .match_labels
-            .and_then(|labels| Some(labels.into_iter().collect()))
+            .map(|labels| labels.into_iter().collect())
             .unwrap_or_default();
         equality.0.extend(expressions);
         equality
@@ -393,7 +532,7 @@ mod tests {
     }
 
     #[test]
-    fn test_to_selector_string() {
+    fn test_to_string() {
         let selector = Selector(vec![
             Expression::In("foo".into(), ["bar".into(), "baz".into()].into()),
             Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into()),
@@ -402,7 +541,7 @@ mod tests {
             Expression::Exists("foo".into()),
             Expression::DoesNotExist("foo".into()),
         ])
-        .to_selector_string();
+        .to_string();
 
         assert_eq!(
             selector,
diff --git a/kube-core/src/lib.rs b/kube-core/src/lib.rs
index b9b2a7a2e..0b169e264 100644
--- a/kube-core/src/lib.rs
+++ b/kube-core/src/lib.rs
@@ -52,6 +52,8 @@ pub use resource::{
 pub mod response;
 pub use response::Status;
 
+pub use labels::{Expression, Matcher, Selector, SelectorExt};
+
 #[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
 #[cfg(feature = "schema")]
 pub mod schema;
diff --git a/kube-core/src/params.rs b/kube-core/src/params.rs
index f70e31596..116cb57b7 100644
--- a/kube-core/src/params.rs
+++ b/kube-core/src/params.rs
@@ -1,5 +1,5 @@
 //! A port of request parameter *Optionals from apimachinery/types.go
-use crate::{labels, request::Error};
+use crate::{request::Error, Selector};
 use serde::Serialize;
 
 /// Controls how the resource version parameter is applied for list calls
@@ -168,20 +168,21 @@ impl ListParams {
 
     /// Configure typed label selectors
     ///
-    /// Configure typed selectors from [`Selector`](crate::labels::Selector) and [`Expression`](crate::label::Expression) lists.
+    /// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists.
     ///
     /// ```
     /// use kube::api::ListParams;
-    /// use kube_core::labels::{Expression, Selector};
+    /// use kube_core::{Expression, Selector};
     /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
     /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
-    /// let lp = ListParams::default().labels_from(selector);
+    /// let lp = ListParams::default().labels_from(&selector);
+    /// let lp = ListParams::default().labels_from(&Expression::Exists("foo".into()).into());
     /// // Alternatively the raw LabelSelector is accepted
-    /// let lp = ListParams::default().labels_from(LabelSelector::default().into());
+    /// let lp = ListParams::default().labels_from(&LabelSelector::default().into());
     ///```
     #[must_use]
-    pub fn labels_from(mut self, selector: labels::Selector) -> Self {
-        self.label_selector = Some(selector.to_selector_string());
+    pub fn labels_from(mut self, selector: &Selector) -> Self {
+        self.label_selector = Some(selector.to_string());
         self
     }
 
@@ -448,6 +449,26 @@ impl WatchParams {
         self
     }
 
+    /// Configure typed label selectors
+    ///
+    /// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists.
+    ///
+    /// ```
+    /// use kube::api::WatchParams;
+    /// use kube_core::{Expression, Selector};
+    /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
+    /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
+    /// let wp = WatchParams::default().labels_from(&selector);
+    /// let wp = WatchParams::default().labels_from(&Expression::Exists("foo".into()).into());
+    /// // Alternatively the raw LabelSelector is accepted
+    /// let wp = WatchParams::default().labels_from(&LabelSelector::default().into());
+    ///```
+    #[must_use]
+    pub fn labels_from(mut self, selector: &Selector) -> Self {
+        self.label_selector = Some(selector.to_string());
+        self
+    }
+
     /// Disables watch bookmarks to simplify watch handling
     ///
     /// This is not recommended to use with production watchers as it can cause desyncs.
diff --git a/kube-runtime/src/watcher.rs b/kube-runtime/src/watcher.rs
index e259e154a..5329b9e8e 100644
--- a/kube-runtime/src/watcher.rs
+++ b/kube-runtime/src/watcher.rs
@@ -9,7 +9,7 @@ use derivative::Derivative;
 use futures::{stream::BoxStream, Stream, StreamExt};
 use kube_client::{
     api::{ListParams, Resource, ResourceExt, VersionMatch, WatchEvent, WatchParams},
-    core::{metadata::PartialObjectMeta, ObjectList},
+    core::{metadata::PartialObjectMeta, ObjectList, Selector},
     error::ErrorResponse,
     Api, Error as ClientErr,
 };
@@ -331,6 +331,26 @@ impl Config {
         self
     }
 
+    /// Configure typed label selectors
+    ///
+    /// Configure typed selectors from [`Selector`](kube_client::core::Selector) and [`Expression`](kube_client::core::Expression) lists.
+    ///
+    /// ```
+    /// use kube_runtime::watcher::Config;
+    /// use kube_client::core::{Expression, Selector};
+    /// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
+    /// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
+    /// let cfg = Config::default().labels_from(&selector);
+    /// let cfg = Config::default().labels_from(&Expression::Exists("foo".into()).into());
+    /// // Alternatively the raw LabelSelector is accepted
+    /// let cfg = Config::default().labels_from(&LabelSelector::default().into());
+    ///```
+    #[must_use]
+    pub fn labels_from(mut self, selector: &Selector) -> Self {
+        self.label_selector = Some(selector.to_string());
+        self
+    }
+
     /// Sets list semantic to configure re-list performance and consistency
     ///
     /// NB: This option only has an effect for [`InitialListStrategy::ListWatch`].
diff --git a/kube/src/lib.rs b/kube/src/lib.rs
index 52074ff3a..3fae1db73 100644
--- a/kube/src/lib.rs
+++ b/kube/src/lib.rs
@@ -199,7 +199,10 @@ pub mod prelude {
     #[cfg(feature = "unstable-client")] pub use crate::client::scope::NamespacedRef;
 
     #[allow(unreachable_pub)] pub use crate::core::PartialObjectMetaExt as _;
-    pub use crate::{core::crd::CustomResourceExt as _, Resource as _, ResourceExt as _};
+    pub use crate::{
+        core::{crd::CustomResourceExt as _, SelectorExt as _},
+        Resource as _, ResourceExt as _,
+    };
 
     #[cfg(feature = "runtime")] pub use crate::runtime::utils::WatchStreamExt as _;
 }