docker_wrapper/command/network/
ls.rs

1//! Docker network ls command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Docker network ls command builder
10#[derive(Debug, Clone)]
11pub struct NetworkLsCommand {
12    /// Filter output
13    filters: HashMap<String, String>,
14    /// Format output
15    format: Option<String>,
16    /// Don't truncate output
17    no_trunc: bool,
18    /// Only show IDs
19    quiet: bool,
20    /// Command executor
21    pub executor: CommandExecutor,
22}
23
24impl NetworkLsCommand {
25    /// Create a new network ls command
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            filters: HashMap::new(),
30            format: None,
31            no_trunc: false,
32            quiet: false,
33            executor: CommandExecutor::new(),
34        }
35    }
36
37    /// Add a filter
38    #[must_use]
39    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
40        self.filters.insert(key.into(), value.into());
41        self
42    }
43
44    /// Filter by driver
45    #[must_use]
46    pub fn driver_filter(self, driver: impl Into<String>) -> Self {
47        self.filter("driver", driver)
48    }
49
50    /// Filter by ID
51    #[must_use]
52    pub fn id_filter(self, id: impl Into<String>) -> Self {
53        self.filter("id", id)
54    }
55
56    /// Filter by label
57    #[must_use]
58    pub fn label_filter(self, label: impl Into<String>) -> Self {
59        self.filter("label", label)
60    }
61
62    /// Filter by name
63    #[must_use]
64    pub fn name_filter(self, name: impl Into<String>) -> Self {
65        self.filter("name", name)
66    }
67
68    /// Filter by scope
69    #[must_use]
70    pub fn scope_filter(self, scope: impl Into<String>) -> Self {
71        self.filter("scope", scope)
72    }
73
74    /// Filter by type (custom or builtin)
75    #[must_use]
76    pub fn type_filter(self, typ: impl Into<String>) -> Self {
77        self.filter("type", typ)
78    }
79
80    /// Set output format
81    #[must_use]
82    pub fn format(mut self, format: impl Into<String>) -> Self {
83        self.format = Some(format.into());
84        self
85    }
86
87    /// Format output as JSON
88    #[must_use]
89    pub fn format_json(self) -> Self {
90        self.format("json")
91    }
92
93    /// Don't truncate output
94    #[must_use]
95    pub fn no_trunc(mut self) -> Self {
96        self.no_trunc = true;
97        self
98    }
99
100    /// Only display network IDs
101    #[must_use]
102    pub fn quiet(mut self) -> Self {
103        self.quiet = true;
104        self
105    }
106
107    /// Execute the command
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the Docker daemon is not running or the command fails
112    pub async fn run(&self) -> Result<NetworkLsOutput> {
113        self.execute().await.map(NetworkLsOutput::from)
114    }
115}
116
117impl Default for NetworkLsCommand {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123#[async_trait]
124impl DockerCommand for NetworkLsCommand {
125    type Output = CommandOutput;
126
127    fn build_command_args(&self) -> Vec<String> {
128        let mut args = vec!["network".to_string(), "ls".to_string()];
129
130        for (key, value) in &self.filters {
131            args.push("--filter".to_string());
132            args.push(format!("{key}={value}"));
133        }
134
135        if let Some(ref format) = self.format {
136            args.push("--format".to_string());
137            args.push(format.clone());
138        }
139
140        if self.no_trunc {
141            args.push("--no-trunc".to_string());
142        }
143
144        if self.quiet {
145            args.push("--quiet".to_string());
146        }
147
148        args.extend(self.executor.raw_args.clone());
149        args
150    }
151
152    fn get_executor(&self) -> &CommandExecutor {
153        &self.executor
154    }
155
156    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
157        &mut self.executor
158    }
159
160    async fn execute(&self) -> Result<Self::Output> {
161        let args = self.build_command_args();
162        let command_name = args[0].clone();
163        let command_args = args[1..].to_vec();
164        self.executor
165            .execute_command(&command_name, command_args)
166            .await
167    }
168}
169
170/// Information about a Docker network
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "PascalCase")]
173#[allow(clippy::struct_excessive_bools)]
174pub struct NetworkInfo {
175    /// Network ID
176    #[serde(rename = "ID", default)]
177    pub id: String,
178    /// Network name
179    #[serde(default)]
180    pub name: String,
181    /// Network driver
182    #[serde(default)]
183    pub driver: String,
184    /// Network scope
185    #[serde(default)]
186    pub scope: String,
187    /// IPv6 enabled
188    #[serde(rename = "IPv6", default)]
189    pub ipv6: bool,
190    /// Internal network
191    #[serde(default)]
192    pub internal: bool,
193    /// Attachable network
194    #[serde(default)]
195    pub attachable: bool,
196    /// Ingress network
197    #[serde(default)]
198    pub ingress: bool,
199    /// Creation time
200    #[serde(rename = "CreatedAt", default)]
201    pub created_at: String,
202    /// Labels
203    #[serde(default)]
204    pub labels: HashMap<String, String>,
205}
206
207/// Output from network ls command
208#[derive(Debug, Clone)]
209pub struct NetworkLsOutput {
210    /// List of networks
211    pub networks: Vec<NetworkInfo>,
212    /// Raw command output
213    pub raw_output: CommandOutput,
214}
215
216impl From<CommandOutput> for NetworkLsOutput {
217    fn from(output: CommandOutput) -> Self {
218        let networks = if output.stdout.starts_with('[') {
219            // JSON format
220            serde_json::from_str(&output.stdout).unwrap_or_default()
221        } else if output.stdout.trim().is_empty() {
222            vec![]
223        } else {
224            // Parse table format
225            parse_table_output(&output.stdout)
226        };
227
228        Self {
229            networks,
230            raw_output: output,
231        }
232    }
233}
234
235impl NetworkLsOutput {
236    /// Check if the command was successful
237    #[must_use]
238    pub fn is_success(&self) -> bool {
239        self.raw_output.success
240    }
241
242    /// Get network count
243    #[must_use]
244    pub fn count(&self) -> usize {
245        self.networks.len()
246    }
247
248    /// Check if any networks exist
249    #[must_use]
250    pub fn is_empty(&self) -> bool {
251        self.networks.is_empty()
252    }
253
254    /// Get network by name
255    #[must_use]
256    pub fn get_network(&self, name: &str) -> Option<&NetworkInfo> {
257        self.networks.iter().find(|n| n.name == name)
258    }
259
260    /// Get network IDs
261    #[must_use]
262    pub fn ids(&self) -> Vec<String> {
263        self.networks.iter().map(|n| n.id.clone()).collect()
264    }
265}
266
267fn parse_table_output(output: &str) -> Vec<NetworkInfo> {
268    let mut networks = Vec::new();
269    let lines: Vec<&str> = output.lines().collect();
270
271    if lines.len() <= 1 {
272        return networks;
273    }
274
275    // Skip header line
276    for line in lines.iter().skip(1) {
277        let parts: Vec<&str> = line.split_whitespace().collect();
278        if parts.len() >= 4 {
279            networks.push(NetworkInfo {
280                id: parts[0].to_string(),
281                name: parts[1].to_string(),
282                driver: parts[2].to_string(),
283                scope: parts[3].to_string(),
284                ipv6: false,
285                internal: false,
286                attachable: false,
287                ingress: false,
288                created_at: String::new(),
289                labels: HashMap::new(),
290            });
291        }
292    }
293
294    networks
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_network_ls_basic() {
303        let cmd = NetworkLsCommand::new();
304        let args = cmd.build_command_args();
305        assert_eq!(args, vec!["network", "ls"]);
306    }
307
308    #[test]
309    fn test_network_ls_with_filters() {
310        let cmd = NetworkLsCommand::new()
311            .driver_filter("bridge")
312            .name_filter("my-network");
313        let args = cmd.build_command_args();
314        assert!(args.contains(&"--filter".to_string()));
315        assert!(args.iter().any(|a| a.contains("driver=bridge")));
316        assert!(args.iter().any(|a| a.contains("name=my-network")));
317    }
318
319    #[test]
320    fn test_network_ls_with_format() {
321        let cmd = NetworkLsCommand::new().format_json();
322        let args = cmd.build_command_args();
323        assert_eq!(args, vec!["network", "ls", "--format", "json"]);
324    }
325
326    #[test]
327    fn test_network_ls_quiet() {
328        let cmd = NetworkLsCommand::new().quiet();
329        let args = cmd.build_command_args();
330        assert_eq!(args, vec!["network", "ls", "--quiet"]);
331    }
332
333    #[test]
334    fn test_parse_table_output() {
335        let output = "NETWORK ID     NAME      DRIVER    SCOPE
336f2de39df4171   bridge    bridge    local
3379fb1e39c5d12   host      host      local
33894b82a6c5b45   none      null      local";
339
340        let networks = parse_table_output(output);
341        assert_eq!(networks.len(), 3);
342        assert_eq!(networks[0].name, "bridge");
343        assert_eq!(networks[1].name, "host");
344        assert_eq!(networks[2].name, "none");
345    }
346}