use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct ComposePsCommand {
pub executor: CommandExecutor,
pub config: ComposeConfig,
pub services: Vec<String>,
pub all: bool,
pub quiet: bool,
pub show_services: bool,
pub filter: Vec<String>,
pub format: Option<String>,
pub status: Option<Vec<ContainerStatus>>,
}
#[derive(Debug, Clone, Copy)]
pub enum ContainerStatus {
Paused,
Restarting,
Running,
Stopped,
}
impl std::fmt::Display for ContainerStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Paused => write!(f, "paused"),
Self::Restarting => write!(f, "restarting"),
Self::Running => write!(f, "running"),
Self::Stopped => write!(f, "stopped"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComposeContainerInfo {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Service")]
pub service: String,
#[serde(rename = "State")]
pub state: String,
#[serde(rename = "Health")]
pub health: Option<String>,
#[serde(rename = "ExitCode")]
pub exit_code: Option<i32>,
#[serde(rename = "Publishers")]
pub publishers: Option<Vec<PortPublisher>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortPublisher {
#[serde(rename = "TargetPort")]
pub target_port: u16,
#[serde(rename = "PublishedPort")]
pub published_port: Option<u16>,
#[serde(rename = "Protocol")]
pub protocol: String,
}
#[derive(Debug, Clone)]
pub struct ComposePsResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub containers: Vec<ComposeContainerInfo>,
}
impl ComposePsCommand {
#[must_use]
pub fn new() -> Self {
Self {
executor: CommandExecutor::new(),
config: ComposeConfig::new(),
services: Vec::new(),
all: false,
quiet: false,
show_services: false,
filter: Vec::new(),
format: None,
status: None,
}
}
#[must_use]
pub fn service(mut self, service: impl Into<String>) -> Self {
self.services.push(service.into());
self
}
#[must_use]
pub fn all(mut self) -> Self {
self.all = true;
self
}
#[must_use]
pub fn quiet(mut self) -> Self {
self.quiet = true;
self
}
#[must_use]
pub fn services(mut self) -> Self {
self.show_services = true;
self
}
#[must_use]
pub fn filter(mut self, filter: impl Into<String>) -> Self {
self.filter.push(filter.into());
self
}
#[must_use]
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
#[must_use]
pub fn status(mut self, status: ContainerStatus) -> Self {
self.status.get_or_insert_with(Vec::new).push(status);
self
}
#[must_use]
pub fn json(mut self) -> Self {
self.format = Some("json".to_string());
self
}
fn parse_json_output(stdout: &str) -> Vec<ComposeContainerInfo> {
stdout
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str(line).ok())
.collect()
}
}
impl Default for ComposePsCommand {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DockerCommand for ComposePsCommand {
type Output = ComposePsResult;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
<Self as ComposeCommand>::build_command_args(self)
}
async fn execute(&self) -> Result<Self::Output> {
let args = <Self as ComposeCommand>::build_command_args(self);
let output = self.execute_command(args).await?;
let containers = if self.format.as_deref() == Some("json") {
Self::parse_json_output(&output.stdout)
} else {
Vec::new()
};
Ok(ComposePsResult {
stdout: output.stdout,
stderr: output.stderr,
success: output.success,
containers,
})
}
}
impl ComposeCommand for ComposePsCommand {
fn get_config(&self) -> &ComposeConfig {
&self.config
}
fn get_config_mut(&mut self) -> &mut ComposeConfig {
&mut self.config
}
fn subcommand(&self) -> &'static str {
"ps"
}
fn build_subcommand_args(&self) -> Vec<String> {
let mut args = Vec::new();
if self.all {
args.push("--all".to_string());
}
if self.quiet {
args.push("--quiet".to_string());
}
if self.show_services {
args.push("--services".to_string());
}
for filter in &self.filter {
args.push("--filter".to_string());
args.push(filter.clone());
}
if let Some(ref format) = self.format {
args.push("--format".to_string());
args.push(format.clone());
}
if let Some(ref statuses) = self.status {
for status in statuses {
args.push("--status".to_string());
args.push(status.to_string());
}
}
args.extend(self.services.clone());
args
}
}
impl ComposePsResult {
#[must_use]
pub fn success(&self) -> bool {
self.success
}
#[must_use]
pub fn containers(&self) -> &[ComposeContainerInfo] {
&self.containers
}
#[must_use]
pub fn container_ids(&self) -> Vec<String> {
if self.containers.is_empty() {
self.stdout
.lines()
.skip(1) .filter_map(|line| line.split_whitespace().next())
.map(String::from)
.collect()
} else {
self.containers.iter().map(|c| c.id.clone()).collect()
}
}
#[must_use]
pub fn stdout_lines(&self) -> Vec<&str> {
self.stdout.lines().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compose_ps_basic() {
let cmd = ComposePsCommand::new();
let args = cmd.build_subcommand_args();
assert!(args.is_empty());
let full_args = ComposeCommand::build_command_args(&cmd);
assert_eq!(full_args[0], "compose");
assert!(full_args.contains(&"ps".to_string()));
}
#[test]
fn test_compose_ps_all() {
let cmd = ComposePsCommand::new().all();
let args = cmd.build_subcommand_args();
assert_eq!(args, vec!["--all"]);
}
#[test]
fn test_compose_ps_with_format() {
let cmd = ComposePsCommand::new().format("json").all();
let args = cmd.build_subcommand_args();
assert_eq!(args, vec!["--all", "--format", "json"]);
}
#[test]
fn test_compose_ps_with_filters() {
let cmd = ComposePsCommand::new()
.filter("status=running")
.quiet()
.service("web");
let args = cmd.build_subcommand_args();
assert_eq!(args, vec!["--quiet", "--filter", "status=running", "web"]);
}
#[test]
fn test_container_status_display() {
assert_eq!(ContainerStatus::Running.to_string(), "running");
assert_eq!(ContainerStatus::Stopped.to_string(), "stopped");
assert_eq!(ContainerStatus::Paused.to_string(), "paused");
assert_eq!(ContainerStatus::Restarting.to_string(), "restarting");
}
#[test]
fn test_compose_config_integration() {
let cmd = ComposePsCommand::new()
.file("docker-compose.yml")
.project_name("my-project")
.all();
let args = ComposeCommand::build_command_args(&cmd);
assert!(args.contains(&"--file".to_string()));
assert!(args.contains(&"docker-compose.yml".to_string()));
assert!(args.contains(&"--project-name".to_string()));
assert!(args.contains(&"my-project".to_string()));
assert!(args.contains(&"--all".to_string()));
}
#[test]
fn test_compose_args_no_docker_prefix() {
let cmd = ComposePsCommand::new()
.file("/path/to/docker-compose.yaml")
.service("php");
let args = ComposeCommand::build_command_args(&cmd);
assert_eq!(args[0], "compose");
assert!(
!args.iter().any(|arg| arg == "docker"),
"args should not contain 'docker': {args:?}"
);
}
}