Skip to main content

compose_lens/model/
lifecycle.rs

1//! Raw-preserving service lifecycle controls.
2
3/// A service stop grace period retained before interpolation.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum StopGracePeriod {
6    /// A duration accepted by `ComposeLens`'s raw-preserving policy, with its spelling retained.
7    Value(String),
8    /// An interpolation-shaped scalar classified by the existing dollar-marker convention.
9    Expression(String),
10    /// An invalid or provider-specific scalar retained for diagnostics.
11    Other(String),
12}
13
14impl StopGracePeriod {
15    pub(crate) fn parse(value: String) -> Self {
16        if value.contains('$') {
17            Self::Expression(value)
18        } else if valid_stop_grace_period(&value) {
19            Self::Value(value)
20        } else {
21            Self::Other(value)
22        }
23    }
24
25    /// Reports whether this is accepted by the raw-preserving policy or is interpolation-shaped.
26    #[must_use]
27    pub const fn is_valid(&self) -> bool {
28        !matches!(self, Self::Other(_))
29    }
30
31    /// Returns the retained scalar spelling.
32    #[must_use]
33    pub fn raw(&self) -> &str {
34        match self {
35            Self::Value(value) | Self::Expression(value) | Self::Other(value) => value,
36        }
37    }
38}
39
40pub(crate) fn valid_stop_grace_period(mut value: &str) -> bool {
41    let mut found = false;
42    while !value.is_empty() {
43        let number_end = value
44            .char_indices()
45            .take_while(|(_, character)| character.is_ascii_digit() || *character == '.')
46            .map(|(index, character)| index + character.len_utf8())
47            .last()
48            .unwrap_or(0);
49        if number_end == 0 {
50            return false;
51        }
52        let number = &value[..number_end];
53        if number.matches('.').count() > 1 || number == "." || number.ends_with('.') {
54            return false;
55        }
56        value = &value[number_end..];
57        let Some(unit) = ["us", "ms", "s", "m", "h"]
58            .into_iter()
59            .find(|unit| value.starts_with(unit))
60        else {
61            return false;
62        };
63        value = &value[unit.len()..];
64        found = true;
65    }
66    found
67}
68
69#[cfg(test)]
70mod tests {
71    use super::{StopGracePeriod, valid_stop_grace_period};
72
73    #[test]
74    fn applies_the_raw_preserving_policy_using_documented_compose_units() {
75        for value in ["1us", "1ms", "1s", "1m", "1h", "1m30s", "0s", "1.5s", ".5s"] {
76            assert!(valid_stop_grace_period(value), "expected valid duration {value}");
77        }
78        for value in ["", "0", "1", "1ns", "1µs", "1μs", "1d", "s", ".s", "1.s", "1..5s"] {
79            assert!(!valid_stop_grace_period(value), "expected invalid duration {value}");
80        }
81    }
82
83    #[test]
84    fn retains_valid_expression_and_other_spelling() {
85        assert_eq!(
86            StopGracePeriod::parse("1m30s".to_owned()),
87            StopGracePeriod::Value("1m30s".to_owned())
88        );
89        assert_eq!(
90            StopGracePeriod::parse("${STOP_GRACE_PERIOD:-1s}".to_owned()),
91            StopGracePeriod::Expression("${STOP_GRACE_PERIOD:-1s}".to_owned())
92        );
93        assert_eq!(
94            StopGracePeriod::parse("1ns".to_owned()),
95            StopGracePeriod::Other("1ns".to_owned())
96        );
97    }
98
99    #[test]
100    fn uses_the_existing_dollar_marker_as_a_lexical_expression_classification() {
101        assert_eq!(
102            StopGracePeriod::parse("literal$5".to_owned()),
103            StopGracePeriod::Expression("literal$5".to_owned())
104        );
105    }
106}