1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DockerComposeConfig {
11 pub version: String,
13 pub services: HashMap<String, ServiceConfig>,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub networks: Option<HashMap<String, NetworkConfig>>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub volumes: Option<HashMap<String, VolumeConfig>>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ServiceConfig {
26 pub image: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub build: Option<BuildConfig>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub ports: Option<Vec<String>>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub environment: Option<HashMap<String, String>>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub volumes: Option<Vec<String>>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub networks: Option<Vec<String>>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub depends_on: Option<Vec<String>>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub healthcheck: Option<HealthCheckConfig>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub command: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct BuildConfig {
57 pub context: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub dockerfile: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub args: Option<HashMap<String, String>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct NetworkConfig {
70 pub driver: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub driver_opts: Option<HashMap<String, String>>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct VolumeConfig {
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub driver: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub driver_opts: Option<HashMap<String, String>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct HealthCheckConfig {
91 pub test: Vec<String>,
93 pub interval: String,
95 pub timeout: String,
97 pub retries: u32,
99}
100
101#[derive(Debug, Clone)]
103pub struct MockServiceSpec {
104 pub name: String,
106 pub port: u16,
108 pub spec_path: Option<String>,
110 pub config_path: Option<String>,
112}
113
114pub struct DockerComposeGenerator {
116 network_name: String,
118 base_image: String,
120}
121
122impl DockerComposeGenerator {
123 pub fn new(network_name: String) -> Self {
128 Self {
129 network_name,
130 base_image: "mockforge:latest".to_string(),
131 }
132 }
133
134 pub fn with_image(mut self, image: String) -> Self {
139 self.base_image = image;
140 self
141 }
142
143 pub fn generate(&self, services: Vec<MockServiceSpec>) -> DockerComposeConfig {
145 let mut compose_services = HashMap::new();
146 let mut networks = HashMap::new();
147
148 networks.insert(
150 self.network_name.clone(),
151 NetworkConfig {
152 driver: "bridge".to_string(),
153 driver_opts: None,
154 },
155 );
156
157 for service_spec in services.iter() {
159 let service_name = format!("mockforge-{}", service_spec.name);
160
161 let mut environment = HashMap::new();
162 environment.insert("RUST_LOG".to_string(), "info".to_string());
163 environment.insert("MOCKFORGE_PORT".to_string(), service_spec.port.to_string());
164
165 if let Some(spec_path) = &service_spec.spec_path {
166 environment
167 .insert("MOCKFORGE_OPENAPI_SPEC".to_string(), format!("/specs/{}", spec_path));
168 }
169
170 let volumes = vec![
171 "./specs:/specs:ro".to_string(),
173 "./configs:/configs:ro".to_string(),
175 ];
176
177 if let Some(config_path) = &service_spec.config_path {
178 environment
179 .insert("MOCKFORGE_CONFIG".to_string(), format!("/configs/{}", config_path));
180 }
181
182 let service_config = ServiceConfig {
183 image: self.base_image.clone(),
184 build: None,
185 ports: Some(vec![format!("{}:{}", service_spec.port, service_spec.port)]),
186 environment: Some(environment),
187 volumes: Some(volumes),
188 networks: Some(vec![self.network_name.clone()]),
189 depends_on: None,
190 healthcheck: Some(HealthCheckConfig {
191 test: vec![
192 "CMD".to_string(),
193 "curl".to_string(),
194 "-f".to_string(),
195 format!("http://localhost:{}/health", service_spec.port),
196 ],
197 interval: "10s".to_string(),
198 timeout: "5s".to_string(),
199 retries: 3,
200 }),
201 command: Some(format!("mockforge http --port {}", service_spec.port)),
202 };
203
204 compose_services.insert(service_name, service_config);
205 }
206
207 DockerComposeConfig {
208 version: "3.8".to_string(),
209 services: compose_services,
210 networks: Some(networks),
211 volumes: None,
212 }
213 }
214
215 pub fn generate_with_dependencies(
217 &self,
218 services: Vec<MockServiceSpec>,
219 dependencies: HashMap<String, Vec<String>>,
220 ) -> DockerComposeConfig {
221 let mut config = self.generate(services);
222
223 for (service, deps) in dependencies {
225 let service_key = format!("mockforge-{}", service);
226 if let Some(service_config) = config.services.get_mut(&service_key) {
227 let formatted_deps: Vec<String> =
228 deps.iter().map(|d| format!("mockforge-{}", d)).collect();
229 service_config.depends_on = Some(formatted_deps);
230 }
231 }
232
233 config
234 }
235
236 pub fn to_yaml(&self, config: &DockerComposeConfig) -> Result<String, serde_yaml::Error> {
238 serde_yaml::to_string(config)
239 }
240
241 pub fn generate_microservices_setup(
243 &self,
244 api_services: Vec<(String, u16)>,
245 ) -> DockerComposeConfig {
246 let mut services = HashMap::new();
247 let mut networks = HashMap::new();
248
249 networks.insert(
251 self.network_name.clone(),
252 NetworkConfig {
253 driver: "bridge".to_string(),
254 driver_opts: None,
255 },
256 );
257
258 for (name, port) in api_services {
260 let service_name = format!("mock-{}", name);
261
262 let mut environment = HashMap::new();
263 environment.insert("RUST_LOG".to_string(), "info".to_string());
264 environment.insert("MOCKFORGE_PORT".to_string(), port.to_string());
265 environment
266 .insert("MOCKFORGE_OPENAPI_SPEC".to_string(), format!("/specs/{}.yaml", name));
267
268 services.insert(
269 service_name.clone(),
270 ServiceConfig {
271 image: self.base_image.clone(),
272 build: None,
273 ports: Some(vec![format!("{}:{}", port, port)]),
274 environment: Some(environment),
275 volumes: Some(vec![
276 "./specs:/specs:ro".to_string(),
277 "./configs:/configs:ro".to_string(),
278 "./logs:/logs".to_string(),
279 ]),
280 networks: Some(vec![self.network_name.clone()]),
281 depends_on: None,
282 healthcheck: Some(HealthCheckConfig {
283 test: vec![
284 "CMD".to_string(),
285 "curl".to_string(),
286 "-f".to_string(),
287 format!("http://localhost:{}/health", port),
288 ],
289 interval: "10s".to_string(),
290 timeout: "5s".to_string(),
291 retries: 3,
292 }),
293 command: Some(format!(
294 "mockforge http --port {} --spec /specs/{}.yaml",
295 port, name
296 )),
297 },
298 );
299 }
300
301 DockerComposeConfig {
302 version: "3.8".to_string(),
303 services,
304 networks: Some(networks),
305 volumes: None,
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_docker_compose_generator_basic() {
316 let generator = DockerComposeGenerator::new("mockforge-network".to_string());
317
318 let services = vec![MockServiceSpec {
319 name: "api".to_string(),
320 port: 3000,
321 spec_path: Some("api.yaml".to_string()),
322 config_path: None,
323 }];
324
325 let config = generator.generate(services);
326
327 assert_eq!(config.version, "3.8");
328 assert_eq!(config.services.len(), 1);
329 assert!(config.services.contains_key("mockforge-api"));
330 assert!(config.networks.is_some());
331 }
332
333 #[test]
334 fn test_docker_compose_with_dependencies() {
335 let generator = DockerComposeGenerator::new("test-network".to_string());
336
337 let services = vec![
338 MockServiceSpec {
339 name: "auth".to_string(),
340 port: 3001,
341 spec_path: Some("auth.yaml".to_string()),
342 config_path: None,
343 },
344 MockServiceSpec {
345 name: "api".to_string(),
346 port: 3000,
347 spec_path: Some("api.yaml".to_string()),
348 config_path: None,
349 },
350 ];
351
352 let mut dependencies = HashMap::new();
353 dependencies.insert("api".to_string(), vec!["auth".to_string()]);
354
355 let config = generator.generate_with_dependencies(services, dependencies);
356
357 assert_eq!(config.services.len(), 2);
358
359 let api_service = config.services.get("mockforge-api").unwrap();
361 assert!(api_service.depends_on.is_some());
362 assert_eq!(api_service.depends_on.as_ref().unwrap()[0], "mockforge-auth");
363 }
364
365 #[test]
366 fn test_microservices_setup_generation() {
367 let generator = DockerComposeGenerator::new("microservices".to_string());
368
369 let api_services = vec![
370 ("users".to_string(), 3001),
371 ("orders".to_string(), 3002),
372 ("payments".to_string(), 3003),
373 ];
374
375 let config = generator.generate_microservices_setup(api_services);
376
377 assert_eq!(config.services.len(), 3);
378 assert!(config.services.contains_key("mock-users"));
379 assert!(config.services.contains_key("mock-orders"));
380 assert!(config.services.contains_key("mock-payments"));
381 }
382
383 #[test]
384 fn test_yaml_export() {
385 let generator = DockerComposeGenerator::new("test-network".to_string());
386
387 let services = vec![MockServiceSpec {
388 name: "test".to_string(),
389 port: 3000,
390 spec_path: None,
391 config_path: None,
392 }];
393
394 let config = generator.generate(services);
395 let yaml = generator.to_yaml(&config);
396
397 assert!(yaml.is_ok());
398 let yaml_str = yaml.unwrap();
399 assert!(yaml_str.contains("version: '3.8'"));
400 assert!(yaml_str.contains("mockforge-test"));
401 }
402}