1use colored::Colorize;
7use include_dir::{Dir, include_dir};
8use lmrc_config_validator::{AppType, ApplicationEntry, LmrcConfig};
9use std::fs;
10use std::path::Path;
11
12use crate::error::Result;
13
14static API_SERVICE_TEMPLATE: Dir =
16 include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/api-service-template");
17
18static GATEWAY_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/gateway");
20static INFRA_API_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-api");
21static INFRA_MIGRATOR_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-migrator");
22static APP_MIGRATOR_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/app-migrator");
23static INFRA_FRONT_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-front");
24
25pub fn generate_applications(project_path: &Path, config: &LmrcConfig) -> Result<()> {
27 bundle_infrastructure_apps(project_path)?;
29
30 for app in &config.apps.applications {
32 generate_single_app_internal(project_path, app, config)?;
33 }
34 Ok(())
35}
36
37pub fn generate_single_app(
39 project_path: &Path,
40 app_name: &str,
41 app_type: Option<&AppType>,
42) -> Result<()> {
43 let app = ApplicationEntry {
44 name: app_name.to_string(),
45 app_type: app_type.cloned(),
46 docker: None,
47 deployment: None,
48 };
49
50 let config = create_minimal_config_for_templating(app_name);
52
53 generate_single_app_internal(project_path, &app, &config)
54}
55
56fn create_minimal_config_for_templating(app_name: &str) -> LmrcConfig {
57 use lmrc_config_validator::*;
58
59 LmrcConfig {
60 project: ProjectConfig {
61 name: "project".to_string(),
62 description: "Project".to_string(),
63 },
64 providers: ProviderConfig {
65 server: "hetzner".to_string(),
66 kubernetes: "k3s".to_string(),
67 database: "postgres".to_string(),
68 queue: "rabbitmq".to_string(),
69 dns: "cloudflare".to_string(),
70 git: "gitlab".to_string(),
71 },
72 apps: AppsConfig {
73 applications: vec![],
74 },
75 infrastructure: InfrastructureConfig {
76 provider: "hetzner".to_string(),
77 network: None,
78 servers: vec![],
79 k3s: None,
80 postgres: None,
81 rabbitmq: None,
82 vault: None,
83 dns: None,
84 gitlab: None,
85 },
86 }
87}
88
89fn bundle_infrastructure_apps(project_path: &Path) -> Result<()> {
91 println!(" {} Infrastructure apps...", "Bundling:".cyan());
92
93 let gateway_path = project_path.join("apps").join("gateway");
95 copy_app_as_is(&GATEWAY_APP, &gateway_path)?;
96 println!(" {} apps/gateway", "✓".green());
97
98 let infra_api_path = project_path.join("apps").join("infra-api");
100 copy_app_as_is(&INFRA_API_APP, &infra_api_path)?;
101 println!(" {} apps/infra-api", "✓".green());
102
103 let infra_migrator_path = project_path.join("apps").join("infra-migrator");
105 copy_app_as_is(&INFRA_MIGRATOR_APP, &infra_migrator_path)?;
106 println!(" {} apps/infra-migrator", "✓".green());
107
108 let app_migrator_path = project_path.join("apps").join("app-migrator");
110 copy_app_as_is(&APP_MIGRATOR_APP, &app_migrator_path)?;
111 println!(" {} apps/app-migrator", "✓".green());
112
113 let infra_front_path = project_path.join("apps").join("infra-front");
115 copy_app_as_is(&INFRA_FRONT_APP, &infra_front_path)?;
116 println!(" {} apps/infra-front", "✓".green());
117
118 Ok(())
119}
120
121fn copy_app_as_is(app_dir: &Dir, dest_path: &Path) -> Result<()> {
123 fs::create_dir_all(dest_path)?;
124
125 for entry in app_dir.entries() {
126 copy_entry_as_is(entry, dest_path)?;
127 }
128
129 Ok(())
130}
131
132fn copy_entry_as_is(entry: &include_dir::DirEntry, base_path: &Path) -> Result<()> {
135 match entry {
136 include_dir::DirEntry::Dir(dir) => {
137 let dir_path = base_path.join(dir.path());
138 fs::create_dir_all(&dir_path)?;
139 for child in dir.entries() {
140 copy_entry_as_is(child, base_path)?;
141 }
142 }
143 include_dir::DirEntry::File(file) => {
144 let mut file_path = base_path.join(file.path());
145
146 if let Some(path_str) = file_path.to_str() {
148 if path_str.ends_with(".template") {
149 file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
150 }
151 }
152
153 if let Some(parent) = file_path.parent() {
154 fs::create_dir_all(parent)?;
155 }
156 fs::write(&file_path, file.contents())?;
157 }
158 }
159 Ok(())
160}
161
162fn generate_single_app_internal(
164 project_path: &Path,
165 app: &ApplicationEntry,
166 config: &LmrcConfig,
167) -> Result<()> {
168 let app_path = project_path.join("apps").join(&app.name);
169 fs::create_dir_all(&app_path)?;
170
171 match &app.app_type {
172 Some(AppType::Gateway) => {
173 copy_app_as_is(&GATEWAY_APP, &app_path)?;
175 println!(" {} apps/{} (API Gateway with auth)", "Created:".green(), app.name);
176 }
177 Some(AppType::Api) => {
178 generate_from_template(&API_SERVICE_TEMPLATE, &app_path, app, config)?;
179 println!(" {} apps/{} (API service)", "Created:".green(), app.name);
180 }
181 Some(AppType::Migrator) => {
182 copy_app_as_is(&APP_MIGRATOR_APP, &app_path)?;
184 println!(" {} apps/{} (Database migrator)", "Created:".green(), app.name);
185 }
186 Some(AppType::Basic) | None => {
187 generate_basic_app(&app_path, app)?;
189 println!(" {} apps/{} (basic app)", "Created:".green(), app.name);
190 }
191 }
192
193 Ok(())
194}
195
196fn generate_from_template(
198 template: &Dir,
199 app_path: &Path,
200 app: &ApplicationEntry,
201 config: &LmrcConfig,
202) -> Result<()> {
203 for entry in template.entries() {
204 extract_and_process_entry(entry, app_path, app, config)?;
205 }
206 Ok(())
207}
208
209fn extract_and_process_entry(
211 entry: &include_dir::DirEntry,
212 base_path: &Path,
213 app: &ApplicationEntry,
214 config: &LmrcConfig,
215) -> Result<()> {
216 match entry {
217 include_dir::DirEntry::Dir(dir) => {
218 let dir_path = base_path.join(dir.path());
219 fs::create_dir_all(&dir_path)?;
220 for child in dir.entries() {
221 extract_and_process_entry(child, base_path, app, config)?;
222 }
223 }
224 include_dir::DirEntry::File(file) => {
225 let mut file_path = base_path.join(file.path());
226
227 if let Some(path_str) = file_path.to_str() {
229 if path_str.ends_with(".template") {
230 file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
231 }
232 }
233
234 if let Some(parent) = file_path.parent() {
235 fs::create_dir_all(parent)?;
236 }
237
238 let contents = file.contents();
240
241 if let Ok(text) = std::str::from_utf8(contents) {
243 let processed = replace_placeholders(text, app, config);
244 fs::write(&file_path, processed)?;
245 } else {
246 fs::write(&file_path, contents)?;
248 }
249 }
250 }
251 Ok(())
252}
253
254fn replace_placeholders(content: &str, app: &ApplicationEntry, config: &LmrcConfig) -> String {
256 let mut result = content.to_string();
257
258 result = result.replace("{{app_name}}", &app.name);
260
261 let port = app
263 .deployment
264 .as_ref()
265 .map(|d| d.port.to_string())
266 .unwrap_or_else(|| "8080".to_string());
267 result = result.replace("{{app_port}}", &port);
268
269 result = result.replace("{{project_name}}", &config.project.name);
271 result = result.replace("{{project_description}}", &config.project.description);
272
273 result
274}
275
276fn generate_basic_app(app_path: &Path, app: &ApplicationEntry) -> Result<()> {
278 use lmrc_toml_writer::PackageToml;
279
280 let cargo_toml = PackageToml::new(&app.name)
282 .version("0.1.0")
283 .edition("2021")
284 .dependency_inline("tokio", r#"{ version = "1.0", features = ["full"] }"#)
285 .build();
286
287 fs::write(app_path.join("Cargo.toml"), cargo_toml)?;
288
289 let src_dir = app_path.join("src");
291 fs::create_dir_all(&src_dir)?;
292
293 let main_rs = format!(
294 r#"#[tokio::main]
295async fn main() {{
296 println!("Hello from {}!");
297}}
298"#,
299 app.name
300 );
301
302 fs::write(src_dir.join("main.rs"), main_rs)?;
303
304 Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use lmrc_config_validator::*;
311
312 #[test]
313 fn test_gateway_app_is_embedded() {
314 assert!(GATEWAY_APP.get_file("Cargo.toml.template").is_some());
315 assert!(GATEWAY_APP.get_file("src/main.rs").is_some());
316 }
317
318 #[test]
319 fn test_infra_api_app_is_embedded() {
320 assert!(INFRA_API_APP.get_file("Cargo.toml.template").is_some());
321 assert!(INFRA_API_APP.get_file("src/main.rs").is_some());
322 }
323
324 #[test]
325 fn test_infra_migrator_app_is_embedded() {
326 assert!(INFRA_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
327 assert!(INFRA_MIGRATOR_APP.get_file("src/main.rs").is_some());
328 }
329
330 #[test]
331 fn test_app_migrator_app_is_embedded() {
332 assert!(APP_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
333 assert!(APP_MIGRATOR_APP.get_file("src/main.rs").is_some());
334 }
335
336 #[test]
337 fn test_api_service_template_is_embedded() {
338 assert!(
339 API_SERVICE_TEMPLATE
340 .get_file("Cargo.toml.template")
341 .is_some()
342 );
343 assert!(API_SERVICE_TEMPLATE.get_file("src/main.rs").is_some());
344 }
345
346 #[test]
347 fn test_replace_placeholders() {
348 let app = ApplicationEntry {
349 name: "my-gateway".to_string(),
350 app_type: Some(AppType::Gateway),
351 docker: None,
352 deployment: Some(DeploymentConfig {
353 replicas: 1,
354 port: 3000,
355 cpu_request: None,
356 memory_request: None,
357 cpu_limit: None,
358 memory_limit: None,
359 env: vec![],
360 }),
361 };
362
363 let config = create_test_config();
364
365 let template =
366 "name = \"{{app_name}}\"\nport = {{app_port}}\nproject = \"{{project_name}}\"";
367 let result = replace_placeholders(template, &app, &config);
368
369 assert!(result.contains("name = \"my-gateway\""));
370 assert!(result.contains("port = 3000"));
371 assert!(result.contains("project = \"test-project\""));
372 }
373
374 #[test]
375 fn test_replace_placeholders_with_default_port() {
376 let app = ApplicationEntry {
377 name: "my-api".to_string(),
378 app_type: Some(AppType::Api),
379 docker: None,
380 deployment: None, };
382
383 let config = create_test_config();
384
385 let template = "PORT={{app_port}}";
386 let result = replace_placeholders(template, &app, &config);
387
388 assert!(result.contains("PORT=8080"));
389 }
390
391 fn create_test_config() -> LmrcConfig {
392 LmrcConfig {
393 project: ProjectConfig {
394 name: "test-project".to_string(),
395 description: "Test project".to_string(),
396 },
397 providers: ProviderConfig {
398 server: "hetzner".to_string(),
399 kubernetes: "k3s".to_string(),
400 database: "postgres".to_string(),
401 queue: "rabbitmq".to_string(),
402 dns: "cloudflare".to_string(),
403 git: "gitlab".to_string(),
404 },
405 apps: AppsConfig {
406 applications: vec![],
407 },
408 infrastructure: InfrastructureConfig {
409 provider: "hetzner".to_string(),
410 network: None,
411 servers: vec![],
412 k3s: None,
413 postgres: None,
414 rabbitmq: None,
415 vault: None,
416 dns: None,
417 gitlab: None,
418 },
419 }
420 }
421}