use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct NetworkLsCommand {
filters: HashMap<String, String>,
format: Option<String>,
no_trunc: bool,
quiet: bool,
pub executor: CommandExecutor,
}
impl NetworkLsCommand {
#[must_use]
pub fn new() -> Self {
Self {
filters: HashMap::new(),
format: None,
no_trunc: false,
quiet: false,
executor: CommandExecutor::new(),
}
}
#[must_use]
pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.filters.insert(key.into(), value.into());
self
}
#[must_use]
pub fn driver_filter(self, driver: impl Into<String>) -> Self {
self.filter("driver", driver)
}
#[must_use]
pub fn id_filter(self, id: impl Into<String>) -> Self {
self.filter("id", id)
}
#[must_use]
pub fn label_filter(self, label: impl Into<String>) -> Self {
self.filter("label", label)
}
#[must_use]
pub fn name_filter(self, name: impl Into<String>) -> Self {
self.filter("name", name)
}
#[must_use]
pub fn scope_filter(self, scope: impl Into<String>) -> Self {
self.filter("scope", scope)
}
#[must_use]
pub fn type_filter(self, typ: impl Into<String>) -> Self {
self.filter("type", typ)
}
#[must_use]
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn format_json(self) -> Self {
self.format("json")
}
#[must_use]
pub fn no_trunc(mut self) -> Self {
self.no_trunc = true;
self
}
#[must_use]
pub fn quiet(mut self) -> Self {
self.quiet = true;
self
}
pub async fn run(&self) -> Result<NetworkLsOutput> {
self.execute().await.map(NetworkLsOutput::from)
}
}
impl Default for NetworkLsCommand {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DockerCommand for NetworkLsCommand {
type Output = CommandOutput;
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["network".to_string(), "ls".to_string()];
for (key, value) in &self.filters {
args.push("--filter".to_string());
args.push(format!("{key}={value}"));
}
if let Some(ref format) = self.format {
args.push("--format".to_string());
args.push(format.clone());
}
if self.no_trunc {
args.push("--no-trunc".to_string());
}
if self.quiet {
args.push("--quiet".to_string());
}
args.extend(self.executor.raw_args.clone());
args
}
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let command_name = args[0].clone();
let command_args = args[1..].to_vec();
self.executor
.execute_command(&command_name, command_args)
.await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct NetworkInfo {
#[serde(rename = "ID", default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub driver: String,
#[serde(default)]
pub scope: String,
#[serde(rename = "IPv6", default)]
pub ipv6: bool,
#[serde(default)]
pub internal: bool,
#[serde(default)]
pub attachable: bool,
#[serde(default)]
pub ingress: bool,
#[serde(rename = "CreatedAt", default)]
pub created_at: String,
#[serde(default)]
pub labels: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct NetworkLsOutput {
pub networks: Vec<NetworkInfo>,
pub raw_output: CommandOutput,
}
impl From<CommandOutput> for NetworkLsOutput {
fn from(output: CommandOutput) -> Self {
let networks = if output.stdout.starts_with('[') {
serde_json::from_str(&output.stdout).unwrap_or_default()
} else if output.stdout.trim().is_empty() {
vec![]
} else {
parse_table_output(&output.stdout)
};
Self {
networks,
raw_output: output,
}
}
}
impl NetworkLsOutput {
#[must_use]
pub fn is_success(&self) -> bool {
self.raw_output.success
}
#[must_use]
pub fn count(&self) -> usize {
self.networks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.networks.is_empty()
}
#[must_use]
pub fn get_network(&self, name: &str) -> Option<&NetworkInfo> {
self.networks.iter().find(|n| n.name == name)
}
#[must_use]
pub fn ids(&self) -> Vec<String> {
self.networks.iter().map(|n| n.id.clone()).collect()
}
}
fn parse_table_output(output: &str) -> Vec<NetworkInfo> {
let mut networks = Vec::new();
let lines: Vec<&str> = output.lines().collect();
if lines.len() <= 1 {
return networks;
}
for line in lines.iter().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
networks.push(NetworkInfo {
id: parts[0].to_string(),
name: parts[1].to_string(),
driver: parts[2].to_string(),
scope: parts[3].to_string(),
ipv6: false,
internal: false,
attachable: false,
ingress: false,
created_at: String::new(),
labels: HashMap::new(),
});
}
}
networks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_ls_basic() {
let cmd = NetworkLsCommand::new();
let args = cmd.build_command_args();
assert_eq!(args, vec!["network", "ls"]);
}
#[test]
fn test_network_ls_with_filters() {
let cmd = NetworkLsCommand::new()
.driver_filter("bridge")
.name_filter("my-network");
let args = cmd.build_command_args();
assert!(args.contains(&"--filter".to_string()));
assert!(args.iter().any(|a| a.contains("driver=bridge")));
assert!(args.iter().any(|a| a.contains("name=my-network")));
}
#[test]
fn test_network_ls_with_format() {
let cmd = NetworkLsCommand::new().format_json();
let args = cmd.build_command_args();
assert_eq!(args, vec!["network", "ls", "--format", "json"]);
}
#[test]
fn test_network_ls_quiet() {
let cmd = NetworkLsCommand::new().quiet();
let args = cmd.build_command_args();
assert_eq!(args, vec!["network", "ls", "--quiet"]);
}
#[test]
fn test_parse_table_output() {
let output = "NETWORK ID NAME DRIVER SCOPE
f2de39df4171 bridge bridge local
9fb1e39c5d12 host host local
94b82a6c5b45 none null local";
let networks = parse_table_output(output);
assert_eq!(networks.len(), 3);
assert_eq!(networks[0].name, "bridge");
assert_eq!(networks[1].name, "host");
assert_eq!(networks[2].name, "none");
}
}