Skip to main content

rskit_workload/
state.rs

1//! Workload runtime state and restart policy.
2
3use serde::{Deserialize, Serialize};
4
5/// Lifecycle state of a workload as reported by a provider.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
7#[serde(rename_all = "snake_case")]
8#[non_exhaustive]
9pub enum WorkloadState {
10    /// Created but not yet started.
11    Created,
12    /// Running.
13    Running,
14    /// Stopped by request.
15    Stopped,
16    /// Ran to completion successfully.
17    Completed,
18    /// Terminated with an error.
19    Error,
20    /// Restarting.
21    Restarting,
22    /// State could not be determined.
23    #[default]
24    Unknown,
25    /// No such workload exists.
26    NotFound,
27}
28
29impl WorkloadState {
30    /// Returns `true` when the workload is actively running.
31    #[must_use]
32    pub const fn is_running(self) -> bool {
33        matches!(self, Self::Running)
34    }
35
36    /// Returns `true` when the workload has reached a terminal state.
37    #[must_use]
38    pub const fn is_terminal(self) -> bool {
39        matches!(
40            self,
41            Self::Stopped | Self::Completed | Self::Error | Self::NotFound
42        )
43    }
44
45    /// Machine-readable identifier for the state.
46    #[must_use]
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::Created => "created",
50            Self::Running => "running",
51            Self::Stopped => "stopped",
52            Self::Completed => "completed",
53            Self::Error => "error",
54            Self::Restarting => "restarting",
55            Self::Unknown => "unknown",
56            Self::NotFound => "not_found",
57        }
58    }
59}
60
61impl std::fmt::Display for WorkloadState {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.write_str(self.as_str())
64    }
65}
66
67/// Policy governing whether a workload is restarted after it exits.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
69#[serde(rename_all = "kebab-case")]
70#[non_exhaustive]
71pub enum RestartPolicy {
72    /// Never restart.
73    #[default]
74    No,
75    /// Always restart.
76    Always,
77    /// Restart only on non-zero exit.
78    OnFailure,
79    /// Restart unless explicitly stopped.
80    UnlessStopped,
81}
82
83impl RestartPolicy {
84    /// Machine-readable identifier for the policy.
85    #[must_use]
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::No => "no",
89            Self::Always => "always",
90            Self::OnFailure => "on-failure",
91            Self::UnlessStopped => "unless-stopped",
92        }
93    }
94}
95
96impl std::fmt::Display for RestartPolicy {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(self.as_str())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn state_default_is_unknown() {
108        assert_eq!(WorkloadState::default(), WorkloadState::Unknown);
109    }
110
111    #[test]
112    fn state_running_and_terminal_classification() {
113        assert!(WorkloadState::Running.is_running());
114        assert!(!WorkloadState::Running.is_terminal());
115        assert!(WorkloadState::Completed.is_terminal());
116        assert!(WorkloadState::Error.is_terminal());
117        assert!(!WorkloadState::Created.is_terminal());
118    }
119
120    #[test]
121    fn state_as_str_covers_every_variant() {
122        assert_eq!(WorkloadState::Created.as_str(), "created");
123        assert_eq!(WorkloadState::Running.as_str(), "running");
124        assert_eq!(WorkloadState::Stopped.as_str(), "stopped");
125        assert_eq!(WorkloadState::Completed.as_str(), "completed");
126        assert_eq!(WorkloadState::Error.as_str(), "error");
127        assert_eq!(WorkloadState::Restarting.as_str(), "restarting");
128        assert_eq!(WorkloadState::Unknown.as_str(), "unknown");
129        assert_eq!(WorkloadState::NotFound.as_str(), "not_found");
130    }
131
132    #[test]
133    fn state_display_matches_as_str() {
134        assert_eq!(WorkloadState::NotFound.to_string(), "not_found");
135        assert_eq!(WorkloadState::Restarting.as_str(), "restarting");
136    }
137
138    #[test]
139    fn state_serializes_snake_case() {
140        let json = serde_json::to_string(&WorkloadState::NotFound).unwrap();
141        assert_eq!(json, "\"not_found\"");
142    }
143
144    #[test]
145    fn restart_policy_default_is_no() {
146        assert_eq!(RestartPolicy::default(), RestartPolicy::No);
147    }
148
149    #[test]
150    fn restart_policy_as_str_covers_every_variant() {
151        assert_eq!(RestartPolicy::No.as_str(), "no");
152        assert_eq!(RestartPolicy::Always.as_str(), "always");
153        assert_eq!(RestartPolicy::OnFailure.as_str(), "on-failure");
154        assert_eq!(RestartPolicy::UnlessStopped.as_str(), "unless-stopped");
155    }
156
157    #[test]
158    fn restart_policy_display_uses_kebab_case() {
159        assert_eq!(RestartPolicy::OnFailure.to_string(), "on-failure");
160        assert_eq!(RestartPolicy::UnlessStopped.as_str(), "unless-stopped");
161    }
162
163    #[test]
164    fn restart_policy_serializes_kebab_case() {
165        let json = serde_json::to_string(&RestartPolicy::OnFailure).unwrap();
166        assert_eq!(json, "\"on-failure\"");
167    }
168}