docker_wrapper/command/compose/
version.rs

1//! Docker Compose version command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Docker Compose version command builder
9#[derive(Debug, Clone)]
10pub struct ComposeVersionCommand {
11    /// Base command executor
12    pub executor: CommandExecutor,
13    /// Base compose configuration
14    pub config: ComposeConfig,
15    /// Format output (pretty, json)
16    pub format: Option<VersionFormat>,
17    /// Short output
18    pub short: bool,
19}
20
21/// Version output format
22#[derive(Debug, Clone, Copy)]
23pub enum VersionFormat {
24    /// Pretty format (default)
25    Pretty,
26    /// JSON format
27    Json,
28}
29
30impl std::fmt::Display for VersionFormat {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::Pretty => write!(f, "pretty"),
34            Self::Json => write!(f, "json"),
35        }
36    }
37}
38
39/// Version information from JSON output
40#[derive(Debug, Clone, Deserialize)]
41#[serde(rename_all = "PascalCase")]
42pub struct VersionInfo {
43    /// Compose version
44    pub version: String,
45}
46
47/// Result from compose version command
48#[derive(Debug, Clone)]
49pub struct ComposeVersionResult {
50    /// Raw stdout output
51    pub stdout: String,
52    /// Raw stderr output
53    pub stderr: String,
54    /// Success status
55    pub success: bool,
56    /// Parsed version information (if JSON format)
57    pub version_info: Option<VersionInfo>,
58}
59
60impl ComposeVersionCommand {
61    /// Create a new compose version command
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            executor: CommandExecutor::new(),
66            config: ComposeConfig::new(),
67            format: None,
68            short: false,
69        }
70    }
71
72    /// Set output format
73    #[must_use]
74    pub fn format(mut self, format: VersionFormat) -> Self {
75        self.format = Some(format);
76        self
77    }
78
79    /// Set output format to JSON
80    #[must_use]
81    pub fn format_json(mut self) -> Self {
82        self.format = Some(VersionFormat::Json);
83        self
84    }
85
86    /// Set output format to pretty
87    #[must_use]
88    pub fn format_pretty(mut self) -> Self {
89        self.format = Some(VersionFormat::Pretty);
90        self
91    }
92
93    /// Enable short output
94    #[must_use]
95    pub fn short(mut self) -> Self {
96        self.short = true;
97        self
98    }
99}
100
101impl Default for ComposeVersionCommand {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[async_trait]
108impl DockerCommand for ComposeVersionCommand {
109    type Output = ComposeVersionResult;
110
111    fn get_executor(&self) -> &CommandExecutor {
112        &self.executor
113    }
114
115    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
116        &mut self.executor
117    }
118
119    fn build_command_args(&self) -> Vec<String> {
120        // Use the ComposeCommand implementation explicitly
121        <Self as ComposeCommand>::build_command_args(self)
122    }
123
124    async fn execute(&self) -> Result<Self::Output> {
125        let args = <Self as ComposeCommand>::build_command_args(self);
126        let output = self.execute_command(args).await?;
127
128        // Parse JSON output if format is JSON
129        let version_info = if matches!(self.format, Some(VersionFormat::Json)) {
130            serde_json::from_str(&output.stdout).ok()
131        } else {
132            None
133        };
134
135        Ok(ComposeVersionResult {
136            stdout: output.stdout,
137            stderr: output.stderr,
138            success: output.success,
139            version_info,
140        })
141    }
142}
143
144impl ComposeCommand for ComposeVersionCommand {
145    fn get_config(&self) -> &ComposeConfig {
146        &self.config
147    }
148
149    fn get_config_mut(&mut self) -> &mut ComposeConfig {
150        &mut self.config
151    }
152
153    fn subcommand(&self) -> &'static str {
154        "version"
155    }
156
157    fn build_subcommand_args(&self) -> Vec<String> {
158        let mut args = Vec::new();
159
160        if self.short {
161            args.push("--short".to_string());
162        }
163
164        if let Some(format) = self.format {
165            args.push("--format".to_string());
166            args.push(format.to_string());
167        }
168
169        args
170    }
171}
172
173impl ComposeVersionResult {
174    /// Check if the command was successful
175    #[must_use]
176    pub fn success(&self) -> bool {
177        self.success
178    }
179
180    /// Get parsed version information (if JSON format was used)
181    #[must_use]
182    pub fn version_info(&self) -> Option<&VersionInfo> {
183        self.version_info.as_ref()
184    }
185
186    /// Get the version string (from parsed info or raw output)
187    #[must_use]
188    pub fn version_string(&self) -> Option<String> {
189        if let Some(info) = &self.version_info {
190            Some(info.version.clone())
191        } else {
192            // Try to extract version from raw output
193            self.stdout
194                .lines()
195                .find(|line| line.contains("version"))
196                .map(|line| line.trim().to_string())
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_compose_version_basic() {
207        let cmd = ComposeVersionCommand::new();
208        let args = cmd.build_subcommand_args();
209        assert!(args.is_empty());
210
211        let full_args = ComposeCommand::build_command_args(&cmd);
212        assert_eq!(full_args[0], "compose");
213        assert!(full_args.contains(&"version".to_string()));
214    }
215
216    #[test]
217    fn test_compose_version_with_format() {
218        let cmd = ComposeVersionCommand::new().format_json();
219        let args = cmd.build_subcommand_args();
220        assert!(args.contains(&"--format".to_string()));
221        assert!(args.contains(&"json".to_string()));
222    }
223
224    #[test]
225    fn test_compose_version_with_short() {
226        let cmd = ComposeVersionCommand::new().short();
227        let args = cmd.build_subcommand_args();
228        assert!(args.contains(&"--short".to_string()));
229    }
230
231    #[test]
232    fn test_compose_version_pretty_format() {
233        let cmd = ComposeVersionCommand::new().format_pretty();
234        let args = cmd.build_subcommand_args();
235        assert!(args.contains(&"--format".to_string()));
236        assert!(args.contains(&"pretty".to_string()));
237    }
238
239    #[test]
240    fn test_compose_version_all_options() {
241        let cmd = ComposeVersionCommand::new().format_json().short();
242
243        let args = cmd.build_subcommand_args();
244        assert!(args.contains(&"--short".to_string()));
245        assert!(args.contains(&"--format".to_string()));
246        assert!(args.contains(&"json".to_string()));
247    }
248
249    #[test]
250    fn test_version_format_display() {
251        assert_eq!(VersionFormat::Pretty.to_string(), "pretty");
252        assert_eq!(VersionFormat::Json.to_string(), "json");
253    }
254
255    #[test]
256    fn test_compose_config_integration() {
257        let cmd = ComposeVersionCommand::new()
258            .file("docker-compose.yml")
259            .project_name("myapp")
260            .format_json();
261
262        let args = ComposeCommand::build_command_args(&cmd);
263        assert!(args.contains(&"--file".to_string()));
264        assert!(args.contains(&"docker-compose.yml".to_string()));
265        assert!(args.contains(&"--project-name".to_string()));
266        assert!(args.contains(&"myapp".to_string()));
267        assert!(args.contains(&"--format".to_string()));
268        assert!(args.contains(&"json".to_string()));
269    }
270}