Skip to main content

orodruin_cli/
state.rs

1use serde_json::Value;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct ContainerSummary {
6    pub id: String,
7    pub name: Option<String>,
8    pub status: Option<String>,
9    pub running: bool,
10}
11
12impl ContainerSummary {
13    pub fn matches(&self, needle: &str) -> bool {
14        self.id == needle || self.name.as_deref() == Some(needle)
15    }
16}
17
18#[derive(Debug, Error)]
19pub enum StateError {
20    #[error("failed to parse container list output: {0}")]
21    ParseList(#[source] serde_json::Error),
22}
23
24pub fn parse_list_output(output: &str) -> Result<Vec<ContainerSummary>, StateError> {
25    let value = serde_json::from_str::<Value>(output).map_err(StateError::ParseList)?;
26    Ok(match value {
27        Value::Array(items) => items.into_iter().filter_map(parse_summary).collect(),
28        Value::Object(_) => parse_summary(value).into_iter().collect(),
29        _ => Vec::new(),
30    })
31}
32
33pub fn parse_inspect_output(output: &str) -> Option<Value> {
34    let value = serde_json::from_str::<Value>(output).ok()?;
35    match value {
36        Value::Array(items) => items.into_iter().next(),
37        other => Some(other),
38    }
39}
40
41fn parse_summary(value: Value) -> Option<ContainerSummary> {
42    let id = field(&value, &["id", "Id", "ID", "identifier", "name"])?;
43    let name = field(&value, &["name", "Name"]).or_else(|| first_string_field(&value, &["Names"]));
44    let status = field(&value, &["status", "Status", "state", "State"]);
45    let running = value
46        .get("running")
47        .and_then(Value::as_bool)
48        .or_else(|| {
49            status
50                .as_ref()
51                .map(|value| value.eq_ignore_ascii_case("running"))
52        })
53        .unwrap_or(false);
54    Some(ContainerSummary {
55        id,
56        name,
57        status,
58        running,
59    })
60}
61
62fn field(value: &Value, names: &[&str]) -> Option<String> {
63    names.iter().find_map(|name| {
64        value.get(name).and_then(|value| match value {
65            Value::String(value) => Some(value.clone()),
66            Value::Number(value) => Some(value.to_string()),
67            _ => None,
68        })
69    })
70}
71
72fn first_string_field(value: &Value, names: &[&str]) -> Option<String> {
73    names.iter().find_map(|name| {
74        value.get(name).and_then(|value| {
75            value
76                .as_array()
77                .and_then(|values| values.first())
78                .and_then(Value::as_str)
79                .map(str::to_owned)
80        })
81    })
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use serde_json::json;
88
89    #[test]
90    fn list_parse_error_bubbles_up() {
91        let error = parse_list_output("not-json").unwrap_err();
92        assert!(
93            error
94                .to_string()
95                .contains("failed to parse container list output")
96        );
97    }
98
99    #[test]
100    fn parse_list_output_supports_podman_id_and_names() {
101        let parsed = parse_list_output(
102            &json!([
103                {
104                    "Id": "abc123",
105                    "Names": ["orodruin-demo"],
106                    "State": "running",
107                    "Status": "Up 10 seconds"
108                }
109            ])
110            .to_string(),
111        )
112        .unwrap();
113
114        assert_eq!(parsed.len(), 1);
115        assert_eq!(parsed[0].id, "abc123");
116        assert_eq!(parsed[0].name.as_deref(), Some("orodruin-demo"));
117        assert!(parsed[0].matches("orodruin-demo"));
118    }
119}