docker_wrapper/command/context/
ls.rs

1//! Docker context ls command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7
8/// Information about a Docker context
9#[derive(Debug, Clone, Deserialize)]
10#[serde(rename_all = "PascalCase")]
11pub struct ContextInfo {
12    /// Context name
13    pub name: String,
14
15    /// Description of the context
16    #[serde(default)]
17    pub description: String,
18
19    /// Docker endpoint
20    #[serde(rename = "DockerEndpoint")]
21    pub docker_endpoint: String,
22
23    /// Kubernetes endpoint (if configured)
24    #[serde(rename = "KubernetesEndpoint", default)]
25    pub kubernetes_endpoint: String,
26
27    /// Whether this is the current context
28    #[serde(default)]
29    pub current: bool,
30}
31
32/// Docker context ls command builder
33///
34/// Lists all Docker contexts.
35///
36/// # Example
37///
38/// ```no_run
39/// use docker_wrapper::{ContextLsCommand, DockerCommand};
40///
41/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
42/// let contexts = ContextLsCommand::new()
43///     .quiet()
44///     .execute()
45///     .await?;
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Debug, Clone)]
50pub struct ContextLsCommand {
51    /// Format the output
52    format: Option<String>,
53    /// Only display context names
54    quiet: bool,
55    /// Command executor
56    pub executor: CommandExecutor,
57}
58
59impl ContextLsCommand {
60    /// Create a new context ls command
61    #[must_use]
62    pub fn new() -> Self {
63        Self {
64            format: None,
65            quiet: false,
66            executor: CommandExecutor::new(),
67        }
68    }
69
70    /// Format the output using a Go template
71    #[must_use]
72    pub fn format(mut self, template: impl Into<String>) -> Self {
73        self.format = Some(template.into());
74        self
75    }
76
77    /// Format output as JSON
78    #[must_use]
79    pub fn format_json(self) -> Self {
80        self.format("json")
81    }
82
83    /// Only display context names
84    #[must_use]
85    pub fn quiet(mut self) -> Self {
86        self.quiet = true;
87        self
88    }
89}
90
91impl Default for ContextLsCommand {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[async_trait]
98impl DockerCommand for ContextLsCommand {
99    type Output = CommandOutput;
100
101    fn get_executor(&self) -> &CommandExecutor {
102        &self.executor
103    }
104
105    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
106        &mut self.executor
107    }
108
109    fn build_command_args(&self) -> Vec<String> {
110        let mut args = vec!["context".to_string(), "ls".to_string()];
111
112        if let Some(format) = &self.format {
113            args.push("--format".to_string());
114            args.push(format.clone());
115        }
116
117        if self.quiet {
118            args.push("--quiet".to_string());
119        }
120
121        args.extend(self.executor.raw_args.clone());
122        args
123    }
124
125    async fn execute(&self) -> Result<Self::Output> {
126        let args = self.build_command_args();
127        self.execute_command(args).await
128    }
129}
130
131/// Extension methods for `ContextLsCommand` output
132impl CommandOutput {
133    /// Parse contexts from JSON output
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if the JSON parsing fails
138    pub fn parse_contexts(&self) -> Result<Vec<ContextInfo>> {
139        if self.stdout.trim().is_empty() {
140            return Ok(Vec::new());
141        }
142
143        let contexts: Vec<ContextInfo> = serde_json::from_str(&self.stdout)?;
144        Ok(contexts)
145    }
146
147    /// Get the current context name
148    #[must_use]
149    pub fn current_context(&self) -> Option<String> {
150        // Look for the line with an asterisk indicating current context
151        for line in self.stdout.lines() {
152            if line.contains('*') {
153                // Extract context name (usually second column after asterisk)
154                let parts: Vec<&str> = line.split_whitespace().collect();
155                if let Some(name) = parts.first() {
156                    if name == &"*" {
157                        if let Some(actual_name) = parts.get(1) {
158                            return Some((*actual_name).to_string());
159                        }
160                    }
161                }
162            }
163        }
164        None
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_context_ls_basic() {
174        let cmd = ContextLsCommand::new();
175        let args = cmd.build_command_args();
176        assert_eq!(args[0], "context");
177        assert_eq!(args[1], "ls");
178    }
179
180    #[test]
181    fn test_context_ls_with_format() {
182        let cmd = ContextLsCommand::new().format("{{.Name}}");
183        let args = cmd.build_command_args();
184        assert!(args.contains(&"--format".to_string()));
185        assert!(args.contains(&"{{.Name}}".to_string()));
186    }
187
188    #[test]
189    fn test_context_ls_quiet() {
190        let cmd = ContextLsCommand::new().quiet();
191        let args = cmd.build_command_args();
192        assert!(args.contains(&"--quiet".to_string()));
193    }
194
195    #[test]
196    fn test_current_context_parsing() {
197        let output = CommandOutput {
198            stdout: "default          Default local daemon                          \n* production *   Production environment  unix:///var/run/docker.sock".to_string(),
199            stderr: String::new(),
200            exit_code: 0,
201            success: true,
202        };
203
204        assert_eq!(output.current_context(), Some("production".to_string()));
205    }
206}