1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum DockerLayerType {
9 Base,
11 TechStack,
13 Agent,
15 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct DockerLayer {
33 pub layer_type: DockerLayerType,
35 pub name: String,
37}
38
39impl DockerLayer {
40 pub fn new(layer_type: DockerLayerType, name: String) -> Self {
42 Self { layer_type, name }
43 }
44
45 pub fn base() -> Self {
47 Self {
48 layer_type: DockerLayerType::Base,
49 name: "base".to_string(),
50 }
51 }
52
53 pub fn tech_stack(name: impl Into<String>) -> Self {
55 Self {
56 layer_type: DockerLayerType::TechStack,
57 name: name.into(),
58 }
59 }
60
61 pub fn agent(name: impl Into<String>) -> Self {
63 Self {
64 layer_type: DockerLayerType::Agent,
65 name: name.into(),
66 }
67 }
68
69 pub fn project(name: impl Into<String>) -> Self {
71 Self {
72 layer_type: DockerLayerType::Project,
73 name: name.into(),
74 }
75 }
76
77 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#[derive(Debug, Clone)]
94pub struct DockerLayerContent {
95 pub dockerfile_content: String,
97 pub additional_files: Vec<(String, Vec<u8>)>,
99}
100
101impl DockerLayerContent {
102 pub fn new(dockerfile_content: String) -> Self {
104 Self {
105 dockerfile_content,
106 additional_files: Vec::new(),
107 }
108 }
109
110 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#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DockerImageConfig {
125 pub tech_stack: String,
127 pub agent: String,
129 pub project: String,
131}
132
133impl DockerImageConfig {
134 pub fn new(tech_stack: String, agent: String, project: String) -> Self {
136 Self {
137 tech_stack,
138 agent,
139 project,
140 }
141 }
142
143 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 pub fn image_tag(&self) -> String {
154 format!("tsk/{}/{}/{}", self.tech_stack, self.agent, self.project)
155 }
156
157 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}