From f55ad3ab8b104e360c19f497ec1d21f5aac1cb37 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 12 Nov 2026 16:13:54 +0100 Subject: [PATCH 1/2] feat(shared): Add time::DurationWithNever This exapnds Duration with the "never" keyword. This is useful for intervals or timeouts which should never trigger. --- crates/stackable-shared/src/time/duration.rs | 89 ++++++++++++++----- .../stackable-shared/src/time/serde_impl.rs | 43 ++++++++- 2 files changed, 109 insertions(+), 23 deletions(-) diff --git a/crates/stackable-shared/src/time/duration.rs b/crates/stackable-shared/src/time/duration.rs index 0cdae9920..6def579db 100644 --- a/crates/stackable-shared/src/time/duration.rs +++ b/crates/stackable-shared/src/time/duration.rs @@ -421,13 +421,61 @@ impl DurationUnit { } } +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum DurationWithNever { + Never, + Duration(Duration), +} + +impl FromStr for DurationWithNever { + type Err = DurationParseError; + + fn from_str(s: &str) -> Result { + match s { + Self::NEVER_KEYWORD => Ok(Self::Never), + s => Ok(Self::Duration(Duration::from_str(s)?)), + } + } +} + +impl Display for DurationWithNever { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + Self::Never => f.write_str(Self::NEVER_KEYWORD), + Self::Duration(duration) => duration.fmt(f), + } + } +} + +impl JsonSchema for DurationWithNever { + fn schema_name() -> Cow<'static, str> { + "DurationWithNever".into() + } + + fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + String::json_schema(generator) + } +} + +impl DurationWithNever { + /// The "never" keyword. + pub(crate) const NEVER_KEYWORD: &str = "never"; +} + #[cfg(test)] -mod test { +mod tests { + use std::assert_matches; + use rstest::rstest; use serde::{Deserialize, Serialize}; use super::*; + #[derive(Deserialize, Serialize)] + struct StructWithNever { + dur: DurationWithNever, + } + #[test] fn const_from() { assert_eq!(Duration::from_secs(42).as_secs(), 42); @@ -508,33 +556,30 @@ mod test { } } - #[test] - fn deserialize() { - #[derive(Deserialize)] - struct S { - dur: Duration, + #[rstest] + #[case("dur: 15d2m2s", Some(1_296_122))] + #[case("dur: never", None)] + fn deserialize(#[case] input: &str, #[case] expected_secs: Option) { + let st: StructWithNever = + yaml_serde::from_str(input).expect("valid duration must deserialize"); + + match expected_secs { + Some(s) => assert_matches!(st.dur, DurationWithNever::Duration(d) if d.as_secs() == s), + None => assert_eq!(st.dur, DurationWithNever::Never), } - - let s: S = yaml_serde::from_str("dur: 15d2m2s").expect("valid duration must deserialize"); - assert_eq!(s.dur.as_secs(), 1_296_122); } - #[test] - fn serialize() { - #[derive(Serialize)] - struct S { - dur: Duration, - } - - let s = S { - dur: "15d2m2s" - .parse() - .expect("static string must be valid duration"), + #[rstest] + #[case("15d2m2s", "dur: 15d2m2s\n")] + #[case("never", "dur: never\n")] + fn serialize(#[case] input: &str, #[case] expected: &str) { + let st = StructWithNever { + dur: input.parse().expect("static string must be valid duration"), }; assert_eq!( - yaml_serde::to_string(&s).expect("valid duration must serialize"), - "dur: 15d2m2s\n" + yaml_serde::to_string(&st).expect("valid duration must serialize"), + expected ); } diff --git a/crates/stackable-shared/src/time/serde_impl.rs b/crates/stackable-shared/src/time/serde_impl.rs index c365b9456..404105e27 100644 --- a/crates/stackable-shared/src/time/serde_impl.rs +++ b/crates/stackable-shared/src/time/serde_impl.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize, de::Visitor}; -use crate::time::Duration; +use crate::time::{Duration, DurationWithNever}; struct DurationVisitor; @@ -37,3 +37,44 @@ impl Serialize for Duration { serializer.serialize_str(&self.to_string()) } } + +struct DurationWithNeverVisitor; + +impl Visitor<'_> for DurationWithNeverVisitor { + type Value = DurationWithNever; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_fmt(format_args!( + "the string \"{keyword}\" or a string in any of the supported formats", + keyword = DurationWithNever::NEVER_KEYWORD + )) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let dur = v + .parse::() + .map_err(serde::de::Error::custom)?; + Ok(dur) + } +} + +impl<'de> Deserialize<'de> for DurationWithNever { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(DurationWithNeverVisitor) + } +} + +impl Serialize for DurationWithNever { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} From 7cf47c66ae4ace03f3d275803724eb0dbd75ebae Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 13 Nov 2026 10:55:58 +0100 Subject: [PATCH 2/2] docs(shared): Add and adjust doc comments for Duration(WithNever) --- crates/stackable-shared/src/time/duration.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/stackable-shared/src/time/duration.rs b/crates/stackable-shared/src/time/duration.rs index 6def579db..a9b54c0b4 100644 --- a/crates/stackable-shared/src/time/duration.rs +++ b/crates/stackable-shared/src/time/duration.rs @@ -1,6 +1,7 @@ //! This module contains a common [`Duration`] struct which is able to parse -//! human-readable duration formats, like `5s`, `24h`, `2y2h20m42s` or`15d2m2s`. It -//! additionally implements many required traits, like [`JsonSchema`], +//! human-readable duration formats, like `5s`, `24h`, `2y2h20m42s` or`15d2m2s`. +//! +//! It additionally implements many required traits, like [`JsonSchema`], //! [`Deserialize`][serde::Deserialize], and [`Serialize`][serde::Serialize]. //! //! Furthermore, it implements [`Deref`], which enables us to use all associated @@ -60,8 +61,10 @@ pub enum DurationParseError { } /// A common [`Duration`] struct which is able to parse human-readable duration -/// formats, like `5s`, `24h`, `2y2h20m42s` or`15d2m2s`. It additionally -/// implements many required traits (for CRD deserialization and serialization). +/// formats, like `5s`, `24h`, `2y2h20m42s` or`15d2m2s`. +/// +/// It additionally implements many required traits (for CRD deserialization and +/// serialization). #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Duration(std::time::Duration); @@ -421,6 +424,10 @@ impl DurationUnit { } } +/// A [`Duration`] expanded with the "never" keyword. +/// +/// This is useful for settings which allow setting a duration or "never" to disable that setting +/// entirely. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum DurationWithNever { Never,