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