tsk/docker/
layers.rs

1//! Docker layer types and structures for the templating system
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Represents the different types of Docker layers
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum DockerLayerType {
9    /// Base OS and essential development tools
10    Base,
11    /// Technology stack (e.g., rust, python, node)
12    TechStack,
13    /// AI agent setup (e.g., claude, aider)
14    Agent,
15    /// Project-specific configuration
16    Project,
17}
18
19impl fmt::Display for DockerLayerType {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            DockerLayerType::Base => write!(f, "base"),
23            DockerLayerType::TechStack => write!(f, "tech-stack"),
24            DockerLayerType::Agent => write!(f, "agent"),
25            DockerLayerType::Project => write!(f, "project"),
26        }
27    }
28}
29
30/// Represents a specific Docker layer with its type and name
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct DockerLayer {
33    /// The type of layer
34    pub layer_type: DockerLayerType,
35    /// The name of the specific layer (e.g., "rust", "claude", "web-api")
36    pub name: String,
37}
38
39impl DockerLayer {
40    /// Creates a new DockerLayer
41    pub fn new(layer_type: DockerLayerType, name: String) -> Self {
42        Self { layer_type, name }
43    }
44
45    /// Creates a base layer
46    pub fn base() -> Self {
47        Self {
48            layer_type: DockerLayerType::Base,
49            name: "base".to_string(),
50        }
51    }
52
53    /// Creates a tech stack layer
54    pub fn tech_stack(name: impl Into<String>) -> Self {
55        Self {
56            layer_type: DockerLayerType::TechStack,
57            name: name.into(),
58        }
59    }
60
61    /// Creates an agent layer
62    pub fn agent(name: impl Into<String>) -> Self {
63        Self {
64            layer_type: DockerLayerType::Agent,
65            name: name.into(),
66        }
67    }
68
69    /// Creates a project layer
70    pub fn project(name: impl Into<String>) -> Self {
71        Self {
72            layer_type: DockerLayerType::Project,
73            name: name.into(),
74        }
75    }
76
77    /// Get the directory path for this layer in the asset structure
78    pub fn asset_path(&self) -> String {
79        match self.layer_type {
80            DockerLayerType::Base => "base".to_string(),
81            _ => format!("{}/{}", self.layer_type, self.name),
82        }
83    }
84}
85
86impl fmt::Display for DockerLayer {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "{}/{}", self.layer_type, self.name)
89    }
90}
91
92/// Represents the content of a Docker layer
93#[derive(Debug, Clone)]
94pub struct DockerLayerContent {
95    /// The Dockerfile fragment for this layer
96    pub dockerfile_content: String,
97    /// Additional files that should be copied alongside the Dockerfile
98    pub additional_files: Vec<(String, Vec<u8>)>,
99}
100
101impl DockerLayerContent {
102    /// Creates a new DockerLayerContent with just Dockerfile content
103    pub fn new(dockerfile_content: String) -> Self {
104        Self {
105            dockerfile_content,
106            additional_files: Vec::new(),
107        }
108    }
109
110    /// Creates a new DockerLayerContent with Dockerfile and additional files
111    pub fn with_files(
112        dockerfile_content: String,
113        additional_files: Vec<(String, Vec<u8>)>,
114    ) -> Self {
115        Self {
116            dockerfile_content,
117            additional_files,
118        }
119    }
120}
121
122/// Configuration for Docker image composition
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DockerImageConfig {
125    /// Technology stack name (e.g., "rust", "python", "node")
126    pub tech_stack: String,
127    /// Agent name (e.g., "claude", "aider")
128    pub agent: String,
129    /// Project name (e.g., "web-api", "cli-tool")
130    pub project: String,
131}
132
133impl DockerImageConfig {
134    /// Creates a new DockerImageConfig
135    pub fn new(tech_stack: String, agent: String, project: String) -> Self {
136        Self {
137            tech_stack,
138            agent,
139            project,
140        }
141    }
142
143    /// Creates a default configuration
144    pub fn default_config() -> Self {
145        Self {
146            tech_stack: "default".to_string(),
147            agent: "claude-code".to_string(),
148            project: "default".to_string(),
149        }
150    }
151
152    /// Generate the Docker image tag from the configuration
153    pub fn image_tag(&self) -> String {
154        format!("tsk/{}/{}/{}", self.tech_stack, self.agent, self.project)
155    }
156
157    /// Get all layers needed for this configuration in order
158    pub fn get_layers(&self) -> Vec<DockerLayer> {
159        vec![
160            DockerLayer::base(),
161            DockerLayer::tech_stack(&self.tech_stack),
162            DockerLayer::agent(&self.agent),
163            DockerLayer::project(&self.project),
164        ]
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_docker_layer_creation() {
174        let base = DockerLayer::base();
175        assert_eq!(base.layer_type, DockerLayerType::Base);
176        assert_eq!(base.name, "base");
177
178        let rust = DockerLayer::tech_stack("rust");
179        assert_eq!(rust.layer_type, DockerLayerType::TechStack);
180        assert_eq!(rust.name, "rust");
181
182        let claude = DockerLayer::agent("claude-code");
183        assert_eq!(claude.layer_type, DockerLayerType::Agent);
184        assert_eq!(claude.name, "claude-code");
185
186        let web_api = DockerLayer::project("web-api");
187        assert_eq!(web_api.layer_type, DockerLayerType::Project);
188        assert_eq!(web_api.name, "web-api");
189    }
190
191    #[test]
192    fn test_docker_layer_asset_path() {
193        assert_eq!(DockerLayer::base().asset_path(), "base");
194        assert_eq!(
195            DockerLayer::tech_stack("rust").asset_path(),
196            "tech-stack/rust"
197        );
198        assert_eq!(
199            DockerLayer::agent("claude-code").asset_path(),
200            "agent/claude-code"
201        );
202        assert_eq!(
203            DockerLayer::project("web-api").asset_path(),
204            "project/web-api"
205        );
206    }
207
208    #[test]
209    fn test_docker_image_config() {
210        let config = DockerImageConfig::new(
211            "rust".to_string(),
212            "claude".to_string(),
213            "web-api".to_string(),
214        );
215
216        assert_eq!(config.image_tag(), "tsk/rust/claude/web-api");
217
218        let layers = config.get_layers();
219        assert_eq!(layers.len(), 4);
220        assert_eq!(layers[0].layer_type, DockerLayerType::Base);
221        assert_eq!(layers[1].layer_type, DockerLayerType::TechStack);
222        assert_eq!(layers[2].layer_type, DockerLayerType::Agent);
223        assert_eq!(layers[3].layer_type, DockerLayerType::Project);
224    }
225
226    #[test]
227    fn test_default_config() {
228        let config = DockerImageConfig::default_config();
229        assert_eq!(config.tech_stack, "default");
230        assert_eq!(config.agent, "claude-code");
231        assert_eq!(config.project, "default");
232        assert_eq!(config.image_tag(), "tsk/default/claude-code/default");
233    }
234
235    #[test]
236    fn test_layer_content_creation() {
237        let content = DockerLayerContent::new("FROM ubuntu:22.04".to_string());
238        assert_eq!(content.dockerfile_content, "FROM ubuntu:22.04");
239        assert!(content.additional_files.is_empty());
240
241        let with_files = DockerLayerContent::with_files(
242            "FROM alpine".to_string(),
243            vec![("config.txt".to_string(), b"test".to_vec())],
244        );
245        assert_eq!(with_files.dockerfile_content, "FROM alpine");
246        assert_eq!(with_files.additional_files.len(), 1);
247    }
248}