use colored::Colorize;
use include_dir::{Dir, include_dir};
use lmrc_config_validator::{AppType, ApplicationEntry, LmrcConfig};
use std::fs;
use std::path::Path;
use crate::error::Result;
static API_SERVICE_TEMPLATE: Dir =
include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/api-service-template");
static GATEWAY_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/gateway");
static INFRA_API_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-api");
static INFRA_MIGRATOR_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-migrator");
static APP_MIGRATOR_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/app-migrator");
static INFRA_FRONT_APP: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/infra-front");
pub fn generate_applications(project_path: &Path, config: &LmrcConfig) -> Result<()> {
bundle_infrastructure_apps(project_path)?;
for app in &config.apps.applications {
generate_single_app_internal(project_path, app, config)?;
}
Ok(())
}
pub fn generate_single_app(
project_path: &Path,
app_name: &str,
app_type: Option<&AppType>,
) -> Result<()> {
let app = ApplicationEntry {
name: app_name.to_string(),
app_type: app_type.cloned(),
docker: None,
deployment: None,
};
let config = create_minimal_config_for_templating(app_name);
generate_single_app_internal(project_path, &app, &config)
}
fn create_minimal_config_for_templating(app_name: &str) -> LmrcConfig {
use lmrc_config_validator::*;
LmrcConfig {
project: ProjectConfig {
name: "project".to_string(),
description: "Project".to_string(),
},
providers: ProviderConfig {
server: "hetzner".to_string(),
kubernetes: "k3s".to_string(),
database: "postgres".to_string(),
queue: "rabbitmq".to_string(),
dns: "cloudflare".to_string(),
git: "gitlab".to_string(),
},
apps: AppsConfig {
applications: vec![],
},
infrastructure: InfrastructureConfig {
provider: "hetzner".to_string(),
network: None,
servers: vec![],
load_balancer: None,
k3s: None,
postgres: None,
rabbitmq: None,
vault: None,
dns: None,
gitlab: None,
},
}
}
fn bundle_infrastructure_apps(project_path: &Path) -> Result<()> {
println!(" {} Infrastructure apps...", "Bundling:".cyan());
let gateway_path = project_path.join("apps").join("gateway");
copy_app_as_is(&GATEWAY_APP, &gateway_path)?;
println!(" {} apps/gateway", "✓".green());
let infra_api_path = project_path.join("apps").join("infra-api");
copy_app_as_is(&INFRA_API_APP, &infra_api_path)?;
println!(" {} apps/infra-api", "✓".green());
let infra_migrator_path = project_path.join("apps").join("infra-migrator");
copy_app_as_is(&INFRA_MIGRATOR_APP, &infra_migrator_path)?;
println!(" {} apps/infra-migrator", "✓".green());
let app_migrator_path = project_path.join("apps").join("app-migrator");
copy_app_as_is(&APP_MIGRATOR_APP, &app_migrator_path)?;
println!(" {} apps/app-migrator", "✓".green());
let infra_front_path = project_path.join("apps").join("infra-front");
copy_app_as_is(&INFRA_FRONT_APP, &infra_front_path)?;
println!(" {} apps/infra-front", "✓".green());
Ok(())
}
fn copy_app_as_is(app_dir: &Dir, dest_path: &Path) -> Result<()> {
fs::create_dir_all(dest_path)?;
for entry in app_dir.entries() {
copy_entry_as_is(entry, dest_path)?;
}
Ok(())
}
fn copy_entry_as_is(entry: &include_dir::DirEntry, base_path: &Path) -> Result<()> {
match entry {
include_dir::DirEntry::Dir(dir) => {
let dir_path = base_path.join(dir.path());
fs::create_dir_all(&dir_path)?;
for child in dir.entries() {
copy_entry_as_is(child, base_path)?;
}
}
include_dir::DirEntry::File(file) => {
let mut file_path = base_path.join(file.path());
if let Some(path_str) = file_path.to_str() {
if path_str.ends_with(".template") {
file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
}
}
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&file_path, file.contents())?;
}
}
Ok(())
}
fn generate_single_app_internal(
project_path: &Path,
app: &ApplicationEntry,
config: &LmrcConfig,
) -> Result<()> {
let app_path = project_path.join("apps").join(&app.name);
fs::create_dir_all(&app_path)?;
match &app.app_type {
Some(AppType::Gateway) => {
copy_app_as_is(&GATEWAY_APP, &app_path)?;
println!(" {} apps/{} (API Gateway with auth)", "Created:".green(), app.name);
}
Some(AppType::Api) => {
generate_from_template(&API_SERVICE_TEMPLATE, &app_path, app, config)?;
println!(" {} apps/{} (API service)", "Created:".green(), app.name);
}
Some(AppType::Migrator) => {
copy_app_as_is(&APP_MIGRATOR_APP, &app_path)?;
println!(" {} apps/{} (Database migrator)", "Created:".green(), app.name);
}
Some(AppType::Basic) | None => {
generate_basic_app(&app_path, app)?;
println!(" {} apps/{} (basic app)", "Created:".green(), app.name);
}
}
Ok(())
}
fn generate_from_template(
template: &Dir,
app_path: &Path,
app: &ApplicationEntry,
config: &LmrcConfig,
) -> Result<()> {
for entry in template.entries() {
extract_and_process_entry(entry, app_path, app, config)?;
}
Ok(())
}
fn extract_and_process_entry(
entry: &include_dir::DirEntry,
base_path: &Path,
app: &ApplicationEntry,
config: &LmrcConfig,
) -> Result<()> {
match entry {
include_dir::DirEntry::Dir(dir) => {
let dir_path = base_path.join(dir.path());
fs::create_dir_all(&dir_path)?;
for child in dir.entries() {
extract_and_process_entry(child, base_path, app, config)?;
}
}
include_dir::DirEntry::File(file) => {
let mut file_path = base_path.join(file.path());
if let Some(path_str) = file_path.to_str() {
if path_str.ends_with(".template") {
file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
}
}
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let contents = file.contents();
if let Ok(text) = std::str::from_utf8(contents) {
let processed = replace_placeholders(text, app, config);
fs::write(&file_path, processed)?;
} else {
fs::write(&file_path, contents)?;
}
}
}
Ok(())
}
fn replace_placeholders(content: &str, app: &ApplicationEntry, config: &LmrcConfig) -> String {
let mut result = content.to_string();
result = result.replace("{{app_name}}", &app.name);
let port = app
.deployment
.as_ref()
.map(|d| d.port.to_string())
.unwrap_or_else(|| "8080".to_string());
result = result.replace("{{app_port}}", &port);
result = result.replace("{{project_name}}", &config.project.name);
result = result.replace("{{project_description}}", &config.project.description);
result
}
fn generate_basic_app(app_path: &Path, app: &ApplicationEntry) -> Result<()> {
use lmrc_toml_writer::PackageToml;
let cargo_toml = PackageToml::new(&app.name)
.version("0.1.0")
.edition("2021")
.dependency_inline("tokio", r#"{ version = "1.0", features = ["full"] }"#)
.build();
fs::write(app_path.join("Cargo.toml"), cargo_toml)?;
let src_dir = app_path.join("src");
fs::create_dir_all(&src_dir)?;
let main_rs = format!(
r#"#[tokio::main]
async fn main() {{
println!("Hello from {}!");
}}
"#,
app.name
);
fs::write(src_dir.join("main.rs"), main_rs)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use lmrc_config_validator::*;
#[test]
fn test_gateway_app_is_embedded() {
assert!(GATEWAY_APP.get_file("Cargo.toml.template").is_some());
assert!(GATEWAY_APP.get_file("src/main.rs").is_some());
}
#[test]
fn test_infra_api_app_is_embedded() {
assert!(INFRA_API_APP.get_file("Cargo.toml.template").is_some());
assert!(INFRA_API_APP.get_file("src/main.rs").is_some());
}
#[test]
fn test_infra_migrator_app_is_embedded() {
assert!(INFRA_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
assert!(INFRA_MIGRATOR_APP.get_file("src/main.rs").is_some());
}
#[test]
fn test_app_migrator_app_is_embedded() {
assert!(APP_MIGRATOR_APP.get_file("Cargo.toml.template").is_some());
assert!(APP_MIGRATOR_APP.get_file("src/main.rs").is_some());
}
#[test]
fn test_api_service_template_is_embedded() {
assert!(
API_SERVICE_TEMPLATE
.get_file("Cargo.toml.template")
.is_some()
);
assert!(API_SERVICE_TEMPLATE.get_file("src/main.rs").is_some());
}
#[test]
fn test_replace_placeholders() {
let app = ApplicationEntry {
name: "my-gateway".to_string(),
app_type: Some(AppType::Gateway),
docker: None,
deployment: Some(DeploymentConfig {
replicas: 1,
port: 3000,
cpu_request: None,
memory_request: None,
cpu_limit: None,
memory_limit: None,
env: vec![],
}),
};
let config = create_test_config();
let template =
"name = \"{{app_name}}\"\nport = {{app_port}}\nproject = \"{{project_name}}\"";
let result = replace_placeholders(template, &app, &config);
assert!(result.contains("name = \"my-gateway\""));
assert!(result.contains("port = 3000"));
assert!(result.contains("project = \"test-project\""));
}
#[test]
fn test_replace_placeholders_with_default_port() {
let app = ApplicationEntry {
name: "my-api".to_string(),
app_type: Some(AppType::Api),
docker: None,
deployment: None, };
let config = create_test_config();
let template = "PORT={{app_port}}";
let result = replace_placeholders(template, &app, &config);
assert!(result.contains("PORT=8080"));
}
fn create_test_config() -> LmrcConfig {
LmrcConfig {
project: ProjectConfig {
name: "test-project".to_string(),
description: "Test project".to_string(),
},
providers: ProviderConfig {
server: "hetzner".to_string(),
kubernetes: "k3s".to_string(),
database: "postgres".to_string(),
queue: "rabbitmq".to_string(),
dns: "cloudflare".to_string(),
git: "gitlab".to_string(),
},
apps: AppsConfig {
applications: vec![],
},
infrastructure: InfrastructureConfig {
provider: "hetzner".to_string(),
network: None,
servers: vec![],
k3s: None,
postgres: None,
rabbitmq: None,
vault: None,
dns: None,
gitlab: None,
load_balancer: None,
},
}
}
}