Skip to main content

compose_lens/model/
restart.rs

1//! Raw-preserving service restart policies.
2
3use super::Located;
4
5/// A service-level Compose `restart` value with its authored scalar retained.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RestartPolicy {
8    raw: Located<String>,
9    kind: RestartPolicyKind,
10}
11
12impl RestartPolicy {
13    pub(crate) fn parse(raw: Located<String>) -> Self {
14        let kind = match raw.value().as_str() {
15            "no" => RestartPolicyKind::No,
16            "always" => RestartPolicyKind::Always,
17            "on-failure" => RestartPolicyKind::OnFailure { maximum_retries: None },
18            "unless-stopped" => RestartPolicyKind::UnlessStopped,
19            value if value.contains('$') => RestartPolicyKind::Expression,
20            value => parse_maximum_retries(value).map_or(RestartPolicyKind::Other, |maximum_retries| {
21                RestartPolicyKind::OnFailure {
22                    maximum_retries: Some(maximum_retries.to_owned()),
23                }
24            }),
25        };
26        Self { raw, kind }
27    }
28
29    /// Returns the complete authored scalar and its source span.
30    #[must_use]
31    pub const fn raw(&self) -> &Located<String> {
32        &self.raw
33    }
34
35    /// Returns the non-destructive policy classification.
36    #[must_use]
37    pub const fn kind(&self) -> &RestartPolicyKind {
38        &self.kind
39    }
40
41    /// Reports whether the value is defined by Compose or is deferred through interpolation.
42    #[must_use]
43    pub const fn is_valid(&self) -> bool {
44        !matches!(self.kind, RestartPolicyKind::Other)
45    }
46}
47
48/// The recognized family of a service-level Compose restart policy.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum RestartPolicyKind {
52    /// Never restart the container automatically.
53    No,
54    /// Always restart the container until it is removed.
55    Always,
56    /// Restart after an error, optionally with the authored maximum-retry spelling.
57    OnFailure {
58        /// Decimal maximum-retry spelling, retained without numeric normalization.
59        maximum_retries: Option<String>,
60    },
61    /// Restart except after an explicit stop or removal.
62    UnlessStopped,
63    /// A policy that still contains a Compose interpolation expression.
64    Expression,
65    /// An invalid or provider-specific value retained for diagnostics.
66    Other,
67}
68
69fn parse_maximum_retries(value: &str) -> Option<&str> {
70    let retries = value.strip_prefix("on-failure:")?;
71    (!retries.is_empty() && retries.bytes().all(|byte| byte.is_ascii_digit())).then_some(retries)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::{RestartPolicy, RestartPolicyKind};
77    use crate::model::Located;
78    use crate::source::{SourceId, SourceSpan};
79
80    #[test]
81    fn retains_maximum_retry_spelling_without_normalizing_it() -> Result<(), &'static str> {
82        let value = "on-failure:003";
83        let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
84        let policy = RestartPolicy::parse(Located::new(value.to_owned(), span));
85
86        assert_eq!(policy.raw().value(), value);
87        assert_eq!(
88            policy.kind(),
89            &RestartPolicyKind::OnFailure {
90                maximum_retries: Some("003".to_owned()),
91            }
92        );
93        assert!(policy.is_valid());
94        Ok(())
95    }
96}