Skip to main content

atlas_local/models/
state.rs

1use std::{fmt::Display, str::FromStr};
2
3use bollard::secret::{ContainerInspectResponse, ContainerStateStatusEnum};
4
5/// The state of the container (from the Docker API)
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum State {
8    Created,
9    Dead,
10    Exited,
11    Paused,
12    Removing,
13    Restarting,
14    Running,
15}
16
17#[derive(Debug, thiserror::Error, PartialEq)]
18pub enum GetStateError {
19    #[error("Missing state")]
20    MissingState,
21    #[error(transparent)]
22    FromContainerStateStatusEnum(#[from] FromContainerStateStatusEnumError),
23}
24
25impl TryFrom<&ContainerInspectResponse> for State {
26    type Error = GetStateError;
27
28    fn try_from(value: &ContainerInspectResponse) -> Result<Self, Self::Error> {
29        let state = &value
30            .state
31            .as_ref()
32            .and_then(|s| s.status)
33            .ok_or(GetStateError::MissingState)?;
34        Ok(state.try_into()?)
35    }
36}
37
38#[derive(Debug, thiserror::Error, PartialEq)]
39pub enum FromContainerStateStatusEnumError {
40    #[error("Empty state")]
41    EmptyState,
42}
43
44impl TryFrom<&ContainerStateStatusEnum> for State {
45    type Error = FromContainerStateStatusEnumError;
46
47    fn try_from(value: &ContainerStateStatusEnum) -> Result<Self, Self::Error> {
48        Ok(match value {
49            ContainerStateStatusEnum::CREATED => State::Created,
50            ContainerStateStatusEnum::DEAD => State::Dead,
51            ContainerStateStatusEnum::EXITED => State::Exited,
52            ContainerStateStatusEnum::PAUSED => State::Paused,
53            ContainerStateStatusEnum::REMOVING => State::Removing,
54            ContainerStateStatusEnum::RESTARTING => State::Restarting,
55            ContainerStateStatusEnum::RUNNING => State::Running,
56
57            ContainerStateStatusEnum::EMPTY => {
58                return Err(FromContainerStateStatusEnumError::EmptyState);
59            }
60        })
61    }
62}
63
64impl Display for State {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            State::Created => write!(f, "created"),
68            State::Dead => write!(f, "dead"),
69            State::Exited => write!(f, "exited"),
70            State::Paused => write!(f, "paused"),
71            State::Removing => write!(f, "removing"),
72            State::Restarting => write!(f, "restarting"),
73            State::Running => write!(f, "running"),
74        }
75    }
76}
77
78#[derive(Debug, thiserror::Error, PartialEq)]
79pub enum FromStrStateError {
80    #[error("Invalid state: {0}")]
81    InvalidState(String),
82}
83
84impl FromStr for State {
85    type Err = FromStrStateError;
86
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        match s {
89            "created" => Ok(State::Created),
90            "dead" => Ok(State::Dead),
91            "exited" => Ok(State::Exited),
92            "paused" => Ok(State::Paused),
93            "removing" => Ok(State::Removing),
94            "restarting" => Ok(State::Restarting),
95            "running" => Ok(State::Running),
96            _ => Err(FromStrStateError::InvalidState(s.to_string())),
97        }
98    }
99}
100
101#[cfg(feature = "serde")]
102impl serde::Serialize for State {
103    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104    where
105        S: serde::Serializer,
106    {
107        serializer.serialize_str(&self.to_string())
108    }
109}
110
111#[cfg(feature = "serde")]
112impl<'de> serde::Deserialize<'de> for State {
113    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114    where
115        D: serde::Deserializer<'de>,
116    {
117        let s = String::deserialize(deserializer)?;
118        State::from_str(&s).map_err(serde::de::Error::custom)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_try_from_container_state_status_enum_success() {
128        // Test all successful conversions
129        assert_eq!(
130            State::try_from(&ContainerStateStatusEnum::CREATED).unwrap(),
131            State::Created
132        );
133        assert_eq!(
134            State::try_from(&ContainerStateStatusEnum::DEAD).unwrap(),
135            State::Dead
136        );
137        assert_eq!(
138            State::try_from(&ContainerStateStatusEnum::EXITED).unwrap(),
139            State::Exited
140        );
141        assert_eq!(
142            State::try_from(&ContainerStateStatusEnum::PAUSED).unwrap(),
143            State::Paused
144        );
145        assert_eq!(
146            State::try_from(&ContainerStateStatusEnum::REMOVING).unwrap(),
147            State::Removing
148        );
149        assert_eq!(
150            State::try_from(&ContainerStateStatusEnum::RESTARTING).unwrap(),
151            State::Restarting
152        );
153        assert_eq!(
154            State::try_from(&ContainerStateStatusEnum::RUNNING).unwrap(),
155            State::Running
156        );
157    }
158
159    #[test]
160    fn test_try_from_container_state_status_enum_empty_error() {
161        // Test error case for EMPTY status
162        let result = State::try_from(&ContainerStateStatusEnum::EMPTY);
163        assert!(result.is_err());
164        assert!(matches!(
165            result.unwrap_err(),
166            FromContainerStateStatusEnumError::EmptyState
167        ));
168    }
169
170    #[test]
171    fn test_try_from_container_inspect_response_success() {
172        use bollard::secret::ContainerState;
173
174        // Test successful conversion with a valid state
175        let container_state = ContainerState {
176            status: Some(ContainerStateStatusEnum::RUNNING),
177            ..Default::default()
178        };
179        let container_inspect = ContainerInspectResponse {
180            state: Some(container_state),
181            ..Default::default()
182        };
183
184        let result = State::try_from(&container_inspect);
185        assert!(result.is_ok());
186        assert_eq!(result.unwrap(), State::Running);
187    }
188
189    #[test]
190    fn test_try_from_container_inspect_response_missing_state() {
191        // Test error case when state is None
192        let container_inspect = ContainerInspectResponse::default();
193
194        let result = State::try_from(&container_inspect);
195        assert!(result.is_err());
196        assert!(matches!(result.unwrap_err(), GetStateError::MissingState));
197    }
198
199    #[test]
200    fn test_try_from_container_inspect_response_missing_status() {
201        use bollard::secret::ContainerState;
202
203        // Test error case when state exists but status is None
204        let container_inspect = ContainerInspectResponse {
205            state: Some(ContainerState::default()), // status is None by default
206            ..Default::default()
207        };
208
209        let result = State::try_from(&container_inspect);
210        assert!(result.is_err());
211        assert!(matches!(result.unwrap_err(), GetStateError::MissingState));
212    }
213
214    #[test]
215    fn test_try_from_container_inspect_response_empty_status() {
216        use bollard::secret::ContainerState;
217
218        // Test error case when status is EMPTY
219        let container_state = ContainerState {
220            status: Some(ContainerStateStatusEnum::EMPTY),
221            ..Default::default()
222        };
223        let container_inspect = ContainerInspectResponse {
224            state: Some(container_state),
225            ..Default::default()
226        };
227
228        let result = State::try_from(&container_inspect);
229        assert!(result.is_err());
230        assert!(matches!(
231            result.unwrap_err(),
232            GetStateError::FromContainerStateStatusEnum(_)
233        ));
234    }
235
236    #[test]
237    fn test_all_states_have_corresponding_enum_values() {
238        // Ensure all State variants can be created from ContainerStateStatusEnum
239        let test_cases = [
240            (ContainerStateStatusEnum::CREATED, State::Created),
241            (ContainerStateStatusEnum::DEAD, State::Dead),
242            (ContainerStateStatusEnum::EXITED, State::Exited),
243            (ContainerStateStatusEnum::PAUSED, State::Paused),
244            (ContainerStateStatusEnum::REMOVING, State::Removing),
245            (ContainerStateStatusEnum::RESTARTING, State::Restarting),
246            (ContainerStateStatusEnum::RUNNING, State::Running),
247        ];
248
249        for (enum_value, expected_state) in test_cases {
250            let result = State::try_from(&enum_value);
251            assert!(result.is_ok(), "Failed to convert {:?}", enum_value);
252            assert_eq!(result.unwrap(), expected_state);
253        }
254    }
255
256    #[test]
257    fn test_state_debug_and_clone() {
258        // Test that State implements Debug and Clone
259        let state = State::Running;
260        let cloned_state = state;
261        assert_eq!(state, cloned_state);
262
263        // Test Debug implementation
264        let debug_output = format!("{:?}", state);
265        assert_eq!(debug_output, "Running");
266    }
267
268    #[test]
269    fn test_error_types_debug() {
270        // Test Debug implementation for error types
271        let get_state_error = GetStateError::MissingState;
272        let debug_output = format!("{:?}", get_state_error);
273        assert!(debug_output.contains("MissingState"));
274
275        let from_enum_error = FromContainerStateStatusEnumError::EmptyState;
276        let debug_output = format!("{:?}", from_enum_error);
277        assert!(debug_output.contains("EmptyState"));
278    }
279
280    #[test]
281    fn test_display_all_states() {
282        // Test Display implementation for all State variants
283        assert_eq!(State::Created.to_string(), "created");
284        assert_eq!(State::Dead.to_string(), "dead");
285        assert_eq!(State::Exited.to_string(), "exited");
286        assert_eq!(State::Paused.to_string(), "paused");
287        assert_eq!(State::Removing.to_string(), "removing");
288        assert_eq!(State::Restarting.to_string(), "restarting");
289        assert_eq!(State::Running.to_string(), "running");
290    }
291
292    #[test]
293    fn test_from_str_success() {
294        // Test FromStr for all valid states
295        assert_eq!("created".parse::<State>().unwrap(), State::Created);
296        assert_eq!("dead".parse::<State>().unwrap(), State::Dead);
297        assert_eq!("exited".parse::<State>().unwrap(), State::Exited);
298        assert_eq!("paused".parse::<State>().unwrap(), State::Paused);
299        assert_eq!("removing".parse::<State>().unwrap(), State::Removing);
300        assert_eq!("restarting".parse::<State>().unwrap(), State::Restarting);
301        assert_eq!("running".parse::<State>().unwrap(), State::Running);
302    }
303
304    #[test]
305    fn test_from_str_invalid() {
306        // Test FromStr error branch
307        let result = "invalid".parse::<State>();
308        assert!(result.is_err());
309        assert_eq!(
310            result.unwrap_err(),
311            FromStrStateError::InvalidState("invalid".to_string())
312        );
313    }
314}