Skip to main content

orodruin_cli/
state.rs

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