1use lmrc_config_validator::LmrcConfig;
2use lmrc_toml_writer::WorkspaceToml;
3use std::fs;
4use std::path::Path;
5
6use crate::error::Result;
7
8include!(concat!(env!("OUT_DIR"), "/versions.rs"));
10
11pub fn generate_workspace_toml(project_path: &Path, config: &LmrcConfig) -> Result<()> {
12 let mut builder = WorkspaceToml::new().resolver("2");
13
14 if project_path.join("infra/pipeline/Cargo.toml").exists() {
16 builder = builder.member("infra/pipeline");
17 }
18
19 for app in ["gateway", "infra-api", "infra-migrator", "app-migrator"] {
21 if project_path.join(format!("apps/{}/Cargo.toml", app)).exists() {
22 builder = builder.member(format!("apps/{}", app));
23 }
24 }
25
26 for app in &config.apps.applications {
28 if matches!(
30 app.app_type,
31 Some(lmrc_config_validator::AppType::Gateway)
32 | Some(lmrc_config_validator::AppType::Migrator)
33 ) {
34 continue;
35 }
36 builder = builder.member(format!("apps/{}", app.name));
37 }
38
39 let authors = config
41 .infrastructure
42 .gitlab
43 .as_ref()
44 .map(|g| format!("{} Team", g.namespace))
45 .unwrap_or_else(|| "LMRC Team".to_string());
46
47 let repository = config
49 .infrastructure
50 .gitlab
51 .as_ref()
52 .map(|g| format!("{}/{}/{}", g.url, g.namespace, config.project.name))
53 .unwrap_or_else(|| "https://example.com/repo".to_string());
54
55 builder = builder
56 .workspace_version("0.1.0")
57 .workspace_edition("2021")
58 .workspace_author(authors)
59 .workspace_license("MIT OR Apache-2.0")
60 .workspace_repository(repository);
61
62 builder = builder
64 .dependency("lmrc-gitlab", LMRC_GITLAB_VERSION)
65 .dependency("lmrc-hetzner", LMRC_HETZNER_VERSION)
66 .dependency("lmrc-k3s", LMRC_K3S_VERSION)
67 .dependency("lmrc-cloudflare", LMRC_CLOUDFLARE_VERSION)
68 .dependency("lmrc-postgres", LMRC_POSTGRES_VERSION)
69 .dependency("lmrc-ssh", LMRC_SSH_VERSION)
70 .dependency("lmrc-docker", LMRC_DOCKER_VERSION)
71 .dependency("lmrc-kubernetes", LMRC_KUBERNETES_VERSION)
72 .dependency("lmrc-pipeline", LMRC_PIPELINE_VERSION)
73 .dependency("lmrc-http-common", LMRC_HTTP_COMMON_VERSION)
74 .dependency("lmrc-config-validator", LMRC_CONFIG_VALIDATOR_VERSION);
75
76 builder = builder
78 .dependency("tokio", r#"{ version = "1.0", features = ["full"] }"#)
79 .dependency("serde", r#"{ version = "1.0", features = ["derive"] }"#)
80 .dependency("serde_json", "1.0")
81 .dependency("thiserror", "2.0")
82 .dependency("tracing", "0.1")
83 .dependency("tracing-subscriber", "0.3")
84 .dependency("clap", r#"{ version = "4.5", features = ["derive"] }"#)
85 .dependency("toml", "0.8")
86 .dependency("axum", "0.7")
88 .dependency("tower", "0.5")
89 .dependency("tower-http", r#"{ version = "0.6", features = ["trace", "cors", "request-id", "util"] }"#)
90 .dependency("hyper", r#"{ version = "1.0", features = ["full"] }"#)
91 .dependency("hyper-util", r#"{ version = "0.1", features = ["tokio"] }"#)
92 .dependency("sea-orm", r#"{ version = "1.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }"#)
93 .dependency("sea-orm-migration", "1.1")
94 .dependency("jsonwebtoken", "9.3")
95 .dependency("bcrypt", "0.16")
96 .dependency("cookie", "0.18")
97 .dependency("chrono", r#"{ version = "0.4", features = ["serde"] }"#)
98 .dependency("time", "0.3")
99 .dependency("dotenvy", "0.15")
100 .dependency("validator", r#"{ version = "0.18", features = ["derive"] }"#)
101 .dependency("uuid", r#"{ version = "1.0", features = ["v4"] }"#)
102 .dependency("anyhow", "1.0")
103 .dependency("async-trait", "0.1")
104 .dependency("reqwest", r#"{ version = "0.12", default-features = false, features = ["rustls-tls", "json"] }"#)
105 ;
106
107 let workspace_toml = builder.build();
108
109 let cargo_path = project_path.join("Cargo.toml");
110 fs::write(&cargo_path, workspace_toml)?;
111
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use lmrc_config_validator::*;
119 use std::collections::HashMap;
120 use tempfile::TempDir;
121
122 fn create_test_config() -> LmrcConfig {
123 LmrcConfig {
124 project: ProjectConfig {
125 name: "test-project".to_string(),
126 description: "Test project".to_string(),
127 },
128 providers: ProviderConfig {
129 server: "hetzner".to_string(),
130 kubernetes: "k3s".to_string(),
131 database: "postgres".to_string(),
132 queue: "rabbitmq".to_string(),
133 dns: "cloudflare".to_string(),
134 git: "gitlab".to_string(),
135 },
136 apps: AppsConfig {
137 applications: vec![
138 ApplicationEntry {
139 name: "app1".to_string(),
140 app_type: Some(lmrc_config_validator::AppType::Api),
141 docker: None,
142 deployment: None,
143 },
144 ApplicationEntry {
145 name: "app2".to_string(),
146 app_type: Some(lmrc_config_validator::AppType::Basic),
147 docker: None,
148 deployment: None,
149 },
150 ],
151 },
152 infrastructure: InfrastructureConfig {
153 provider: "hetzner".to_string(),
154 network: None,
155 servers: vec![ServerGroup {
156 name: "k3s-server".to_string(),
157 role: ServerRole::K3sControl,
158 server_type: "cx11".to_string(),
159 location: "nbg1".to_string(),
160 count: 1,
161 labels: HashMap::new(),
162 ssh_keys: vec![],
163 image: None,
164 }],
165 k3s: Some(K3sConfig {
166 version: "v1.28.5+k3s1".to_string(),
167 deploy_on: vec!["k3s-server".to_string()],
168 control_plane_servers: vec!["k3s-server".to_string()],
169 worker_servers: vec![],
170 enable_traefik: true,
171 enable_metrics_server: false,
172 server_flags: vec![],
173 agent_flags: vec![],
174 }),
175 postgres: Some(PostgresConfig {
176 version: "16".to_string(),
177 database_name: "testdb".to_string(),
178 deployment_mode: PostgresDeploymentMode::InCluster,
179 standalone: None,
180 in_cluster: Some(PostgresInClusterConfig {
181 namespace: "default".to_string(),
182 storage_class: "local-path".to_string(),
183 storage_size: "10Gi".to_string(),
184 use_operator: false,
185 }),
186 }),
187 rabbitmq: None,
188 vault: None,
189 dns: Some(DnsConfig {
190 provider: "cloudflare".to_string(),
191 domain: "test.example.com".to_string(),
192 records: vec![],
193 }),
194 gitlab: Some(GitLabConfig {
195 url: "https://gitlab.com".to_string(),
196 namespace: "testuser".to_string(),
197 }),
198 },
199 }
200 }
201
202 #[test]
203 fn test_generate_workspace_toml_creates_file() {
204 let temp_dir = TempDir::new().unwrap();
205 let config = create_test_config();
206
207 let result = generate_workspace_toml(temp_dir.path(), &config);
208 assert!(result.is_ok());
209
210 let cargo_toml = temp_dir.path().join("Cargo.toml");
211 assert!(cargo_toml.exists());
212 }
213
214 #[test]
215 fn test_workspace_toml_has_workspace_section() {
216 let temp_dir = TempDir::new().unwrap();
217 let config = create_test_config();
218
219 generate_workspace_toml(temp_dir.path(), &config).unwrap();
220
221 let cargo_toml = temp_dir.path().join("Cargo.toml");
222 let content = fs::read_to_string(cargo_toml).unwrap();
223
224 assert!(content.contains("[workspace]"));
225 }
226
227 #[test]
228 fn test_workspace_toml_has_correct_resolver() {
229 let temp_dir = TempDir::new().unwrap();
230 let config = create_test_config();
231
232 generate_workspace_toml(temp_dir.path(), &config).unwrap();
233
234 let cargo_toml = temp_dir.path().join("Cargo.toml");
235 let content = fs::read_to_string(cargo_toml).unwrap();
236
237 assert!(content.contains("resolver = \"2\""));
238 }
239
240 #[test]
241 fn test_workspace_toml_includes_all_app_members() {
242 let temp_dir = TempDir::new().unwrap();
243 let config = create_test_config();
244
245 generate_workspace_toml(temp_dir.path(), &config).unwrap();
246
247 let cargo_toml = temp_dir.path().join("Cargo.toml");
248 let content = fs::read_to_string(cargo_toml).unwrap();
249
250 assert!(content.contains("\"apps/app1\""));
251 assert!(content.contains("\"apps/app2\""));
252 }
253
254 #[test]
255 fn test_workspace_toml_includes_pipeline_member() {
256 let temp_dir = TempDir::new().unwrap();
257 let config = create_test_config();
258
259 let pipeline_dir = temp_dir.path().join("infra/pipeline");
261 fs::create_dir_all(&pipeline_dir).unwrap();
262 fs::write(pipeline_dir.join("Cargo.toml"), "[package]\nname = \"pipeline\"\n").unwrap();
263
264 generate_workspace_toml(temp_dir.path(), &config).unwrap();
265
266 let cargo_toml = temp_dir.path().join("Cargo.toml");
267 let content = fs::read_to_string(cargo_toml).unwrap();
268
269 assert!(content.contains("\"infra/pipeline\""));
270 }
271
272 #[test]
273 fn test_workspace_toml_includes_lmrc_dependencies() {
274 let temp_dir = TempDir::new().unwrap();
275 let config = create_test_config();
276
277 generate_workspace_toml(temp_dir.path(), &config).unwrap();
278
279 let cargo_toml = temp_dir.path().join("Cargo.toml");
280 let content = fs::read_to_string(cargo_toml).unwrap();
281
282 assert!(content.contains("[workspace.dependencies]"));
283
284 let lmrc_deps = [
286 "lmrc-gitlab",
287 "lmrc-hetzner",
288 "lmrc-k3s",
289 "lmrc-cloudflare",
290 "lmrc-postgres",
291 "lmrc-ssh",
292 "lmrc-docker",
293 "lmrc-kubernetes",
294 "lmrc-pipeline",
295 ];
296
297 for dep in &lmrc_deps {
298 assert!(
299 content.contains(dep),
300 "Missing workspace dependency: {}",
301 dep
302 );
303 }
304 }
305
306 #[test]
307 fn test_workspace_toml_includes_common_dependencies() {
308 let temp_dir = TempDir::new().unwrap();
309 let config = create_test_config();
310
311 generate_workspace_toml(temp_dir.path(), &config).unwrap();
312
313 let cargo_toml = temp_dir.path().join("Cargo.toml");
314 let content = fs::read_to_string(cargo_toml).unwrap();
315
316 let common_deps = [
318 "tokio",
319 "serde",
320 "serde_json",
321 "thiserror",
322 "tracing",
323 "clap",
324 "toml",
325 ];
326
327 for dep in &common_deps {
328 assert!(content.contains(dep), "Missing common dependency: {}", dep);
329 }
330 }
331
332 #[test]
333 fn test_workspace_toml_with_single_app() {
334 let temp_dir = TempDir::new().unwrap();
335 let mut config = create_test_config();
336 config.apps.applications = vec![ApplicationEntry {
337 name: "single-app".to_string(),
338 app_type: Some(lmrc_config_validator::AppType::Api),
339 docker: None,
340 deployment: None,
341 }];
342
343 generate_workspace_toml(temp_dir.path(), &config).unwrap();
344
345 let cargo_toml = temp_dir.path().join("Cargo.toml");
346 let content = fs::read_to_string(cargo_toml).unwrap();
347
348 assert!(content.contains("\"apps/single-app\""));
349 assert!(!content.contains("\"apps/app1\""));
350 }
351
352 #[test]
353 fn test_workspace_toml_sets_workspace_metadata() {
354 let temp_dir = TempDir::new().unwrap();
355 let config = create_test_config();
356
357 generate_workspace_toml(temp_dir.path(), &config).unwrap();
358
359 let cargo_toml = temp_dir.path().join("Cargo.toml");
360 let content = fs::read_to_string(cargo_toml).unwrap();
361
362 assert!(content.contains("[workspace.package]"));
363 assert!(content.contains("version = \"0.1.0\""));
364 assert!(content.contains("edition = \"2021\""));
365 }
366
367 #[test]
368 fn test_workspace_toml_author_from_gitlab_namespace() {
369 let temp_dir = TempDir::new().unwrap();
370 let config = create_test_config();
371
372 generate_workspace_toml(temp_dir.path(), &config).unwrap();
373
374 let cargo_toml = temp_dir.path().join("Cargo.toml");
375 let content = fs::read_to_string(cargo_toml).unwrap();
376
377 assert!(content.contains("testuser Team"));
379 }
380}