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 load_balancer: None,
80 k3s: None,
81 postgres: None,
82 rabbitmq: None,
83 vault: None,
84 dns: None,
85 gitlab: None,
86 },
87 }
88}
89
90fn bundle_infrastructure_apps(project_path: &Path) -> Result<()> {
92 println!(" {} Infrastructure apps...", "Bundling:".cyan());
93
94 let gateway_path = project_path.join("apps").join("gateway");
96 copy_app_as_is(&GATEWAY_APP, &gateway_path)?;
97 println!(" {} apps/gateway", "✓".green());
98
99 let infra_api_path = project_path.join("apps").join("infra-api");
101 copy_app_as_is(&INFRA_API_APP, &infra_api_path)?;
102 println!(" {} apps/infra-api", "✓".green());
103
104 let infra_migrator_path = project_path.join("apps").join("infra-migrator");
106 copy_app_as_is(&INFRA_MIGRATOR_APP, &infra_migrator_path)?;
107 println!(" {} apps/infra-migrator", "✓".green());
108
109 let app_migrator_path = project_path.join("apps").join("app-migrator");
111 copy_app_as_is(&APP_MIGRATOR_APP, &app_migrator_path)?;
112 println!(" {} apps/app-migrator", "✓".green());
113
114 let infra_front_path = project_path.join("apps").join("infra-front");
116 copy_app_as_is(&INFRA_FRONT_APP, &infra_front_path)?;
117 println!(" {} apps/infra-front", "✓".green());
118
119 Ok(())
120}
121
122fn copy_app_as_is(app_dir: &Dir, dest_path: &Path) -> Result<()> {
124 fs::create_dir_all(dest_path)?;
125
126 for entry in app_dir.entries() {
127 copy_entry_as_is(entry, dest_path)?;
128 }
129
130 Ok(())
131}
132
133fn copy_entry_as_is(entry: &include_dir::DirEntry, base_path: &Path) -> Result<()> {
136 match entry {
137 include_dir::DirEntry::Dir(dir) => {
138 let dir_path = base_path.join(dir.path());
139 fs::create_dir_all(&dir_path)?;
140 for child in dir.entries() {
141 copy_entry_as_is(child, base_path)?;
142 }
143 }
144 include_dir::DirEntry::File(file) => {
145 let mut file_path = base_path.join(file.path());
146
147 if let Some(path_str) = file_path.to_str() {
149 if path_str.ends_with(".template") {
150 file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
151 }
152 }
153
154 if let Some(parent) = file_path.parent() {
155 fs::create_dir_all(parent)?;
156 }
157 fs::write(&file_path, file.contents())?;
158 }
159 }
160 Ok(())
161}
162
163fn generate_single_app_internal(
165 project_path: &Path,
166 app: &ApplicationEntry,
167 config: &LmrcConfig,
168) -> Result<()> {
169 let app_path = project_path.join("apps").join(&app.name);
170 fs::create_dir_all(&app_path)?;
171
172 match &app.app_type {
173 Some(AppType::Gateway) => {
174 copy_app_as_is(&GATEWAY_APP, &app_path)?;
176 println!(" {} apps/{} (API Gateway with auth)", "Created:".green(), app.name);
177 }
178 Some(AppType::Api) => {
179 generate_from_template(&API_SERVICE_TEMPLATE, &app_path, app, config)?;
180 println!(" {} apps/{} (API service)", "Created:".green(), app.name);
181 }
182 Some(AppType::Migrator) => {
183 copy_app_as_is(&APP_MIGRATOR_APP, &app_path)?;
185 println!(" {} apps/{} (Database migrator)", "Created:".green(), app.name);
186 }
187 Some(AppType::Basic) | None => {
188 generate_basic_app(&app_path, app)?;
190 println!(" {} apps/{} (basic app)", "Created:".green(), app.name);
191 }
192 }
193
194 Ok(())
195}
196
197fn generate_from_template(
199 template: &Dir,
200 app_path: &Path,
201 app: &ApplicationEntry,
202 config: &LmrcConfig,
203) -> Result<()> {
204 for entry in template.entries() {
205 extract_and_process_entry(entry, app_path, app, config)?;
206 }
207 Ok(())
208}
209
210fn extract_and_process_entry(
212 entry: &include_dir::DirEntry,
213 base_path: &Path,
214 app: &ApplicationEntry,
215 config: &LmrcConfig,
216) -> Result<()> {
217 match entry {
218 include_dir::DirEntry::Dir(dir) => {
219 let dir_path = base_path.join(dir.path());
220 fs::create_dir_all(&dir_path)?;
221 for child in dir.entries() {
222 extract_and_process_entry(child, base_path, app, config)?;
223 }
224 }
225 include_dir::DirEntry::File(file) => {
226 let mut file_path = base_path.join(file.path());
227
228 if let Some(path_str) = file_path.to_str() {
230 if path_str.ends_with(".template") {
231 file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
232 }
233 }
234
235 if let Some(parent) = file_path.parent() {
236 fs::create_dir_all(parent)?;
237 }
238
239 let contents = file.contents();
241
242 if let Ok(text) = std::str::from_utf8(contents) {
244 let processed = replace_placeholders(text, app, config);
245 fs::write(&file_path, processed)?;
246 } else {
247 fs::write(&file_path, contents)?;
249 }
250 }
251 }
252 Ok(())
253}
254
255fn replace_placeholders(content: &str, app: &ApplicationEntry, config: &LmrcConfig) -> String {
257 let mut result = content.to_string();
258
259 result = result.replace("{{app_name}}", &app.name);
261
262 let port = app
264 .deployment
265 .as_ref()
266 .map(|d| d.port.to_string())
267 .unwrap_or_else(|| "8080".to_string());
268 result = result.replace("{{app_port}}", &port);
269
270 result = result.replace("{{project_name}}", &config.project.name);
272 result = result.replace("{{project_description}}", &config.project.description);
273
274 result
275}
276
277fn generate_basic_app(app_path: &Path, app: &ApplicationEntry) -> Result<()> {
279 use lmrc_toml_writer::PackageToml;
280
281 let cargo_toml = PackageToml::new(&app.name)
283 .version("0.1.0")
284 .edition("2021")
285 .dependency_inline("tokio", r#"{ version = "1.0", features = ["full"] }"#)
286 .build();
287
288 fs::write(app_path.join("Cargo.toml"), cargo_toml)?;
289
290 let src_dir = app_path.join("src");
292 fs::create_dir_all(&src_dir)?;
293
294 let main_rs = format!(
295 r#"#[tokio::main]
296async fn main() {{
297 println!("Hello from {}!");
298}}
299"#,
300 app.name
301 );
302
303 fs::write(src_dir.join("main.rs"), main_rs)?;
304
305 Ok(())
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use lmrc_config_validator::*;
312
313 #[test]
314 fn test_gateway_app_is_embedded() {
315 assert!(GATEWAY_APP.get_file("Cargo.toml.template").is_some());
316 assert!(GATEWAY_APP.get_file("src/main.rs").is_some());
317 }
318
319 #[test]
320 fn test_infra_api_app_is_embedded() {
321 assert!(INFRA_API_APP.get_file("Cargo.toml.template").is_some());
322 assert!(INFRA_API_APP.get_file("src/main.rs").is_some());
323 }
324
325 #[test]
326 fn test_infra_migrator_app_is_embedded() {
327 assert!(INFRA_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
328 assert!(INFRA_MIGRATOR_APP.get_file("src/main.rs").is_some());
329 }
330
331 #[test]
332 fn test_app_migrator_app_is_embedded() {
333 assert!(APP_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
334 assert!(APP_MIGRATOR_APP.get_file("src/main.rs").is_some());
335 }
336
337 #[test]
338 fn test_api_service_template_is_embedded() {
339 assert!(
340 API_SERVICE_TEMPLATE
341 .get_file("Cargo.toml.template")
342 .is_some()
343 );
344 assert!(API_SERVICE_TEMPLATE.get_file("src/main.rs").is_some());
345 }
346
347 #[test]
348 fn test_replace_placeholders() {
349 let app = ApplicationEntry {
350 name: "my-gateway".to_string(),
351 app_type: Some(AppType::Gateway),
352 docker: None,
353 deployment: Some(DeploymentConfig {
354 replicas: 1,
355 port: 3000,
356 cpu_request: None,
357 memory_request: None,
358 cpu_limit: None,
359 memory_limit: None,
360 env: vec![],
361 }),
362 };
363
364 let config = create_test_config();
365
366 let template =
367 "name = \"{{app_name}}\"\nport = {{app_port}}\nproject = \"{{project_name}}\"";
368 let result = replace_placeholders(template, &app, &config);
369
370 assert!(result.contains("name = \"my-gateway\""));
371 assert!(result.contains("port = 3000"));
372 assert!(result.contains("project = \"test-project\""));
373 }
374
375 #[test]
376 fn test_replace_placeholders_with_default_port() {
377 let app = ApplicationEntry {
378 name: "my-api".to_string(),
379 app_type: Some(AppType::Api),
380 docker: None,
381 deployment: None, };
383
384 let config = create_test_config();
385
386 let template = "PORT={{app_port}}";
387 let result = replace_placeholders(template, &app, &config);
388
389 assert!(result.contains("PORT=8080"));
390 }
391
392 fn create_test_config() -> LmrcConfig {
393 LmrcConfig {
394 project: ProjectConfig {
395 name: "test-project".to_string(),
396 description: "Test project".to_string(),
397 },
398 providers: ProviderConfig {
399 server: "hetzner".to_string(),
400 kubernetes: "k3s".to_string(),
401 database: "postgres".to_string(),
402 queue: "rabbitmq".to_string(),
403 dns: "cloudflare".to_string(),
404 git: "gitlab".to_string(),
405 },
406 apps: AppsConfig {
407 applications: vec![],
408 },
409 infrastructure: InfrastructureConfig {
410 provider: "hetzner".to_string(),
411 network: None,
412 servers: vec![],
413 k3s: None,
414 postgres: None,
415 rabbitmq: None,
416 vault: None,
417 dns: None,
418 gitlab: None,
419 load_balancer: None,
420 },
421 }
422 }
423}