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