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}