use super::EngineError;
use crate::config::ProjectConfig;
use std::path::PathBuf;
#[derive(Debug, PartialEq, Clone)]
pub enum Action {
Delete(PathBuf),
Rename {
from: PathBuf,
to: PathBuf,
},
ApplyTemplate {
path: PathBuf,
context: TemplateContext,
},
CreateFile {
path: PathBuf,
content: String,
context: TemplateContext,
},
}
#[derive(Debug, PartialEq, Clone)]
pub struct TemplateContext {
pub project_name: String,
pub author_name: String,
pub author_email: String,
pub app_identifier: String,
pub db_name: Option<String>,
pub db_owner_admin: Option<String>,
pub db_owner_pword: Option<String>,
pub include_server: bool,
pub include_frontend: bool,
pub include_tauri_desktop: bool,
pub deno_package_name: String,
}
#[derive(Debug, Default, PartialEq)]
pub struct TransformationPlan {
pub actions: Vec<Action>,
}
macro_rules! delete_actions {
( $( $x:expr ),* ) => {
{
let mut actions = Vec::new();
$(
actions.push(Action::Delete(PathBuf::from($x)));
)*
actions
}
};
}
pub fn build_plan(config: &ProjectConfig) -> Result<TransformationPlan, EngineError> {
let mut plan = TransformationPlan::default();
let actions = delete_actions!(".git", ".github", "ROADMAP.md", "CHANGELOG.md", "README.md");
plan.actions.extend(actions);
if !config.include_tauri_desktop {
plan.actions
.push(Action::Delete(PathBuf::from("client/src-tauri")));
}
if !config.include_server {
plan.actions.push(Action::Delete(PathBuf::from("backend")));
}
if !config.include_frontend {
plan.actions.push(Action::Delete(PathBuf::from("frontend")));
}
let context = TemplateContext {
project_name: config.project_name.clone(),
author_name: config.author_name.clone(),
author_email: config.author_email.clone(),
app_identifier: config.app_identifier.clone(),
db_name: config.db_name.clone(),
db_owner_admin: config.db_owner_admin.clone(),
db_owner_pword: config.db_owner_pword.clone(),
include_server: config.include_server,
include_frontend: config.include_frontend,
include_tauri_desktop: config.include_tauri_desktop,
deno_package_name: config.deno_package_name.clone(),
};
plan.actions.push(Action::ApplyTemplate {
path: PathBuf::from("docker-compose.yml"),
context: context.clone(),
});
plan.actions.push(Action::ApplyTemplate {
path: PathBuf::from("docker-compose.yml"),
context: context.clone(),
});
plan.actions.push(Action::ApplyTemplate {
path: PathBuf::from("Cargo.toml"),
context: context.clone(),
});
plan.actions.push(Action::ApplyTemplate {
path: PathBuf::from("client/package.json"),
context: context.clone(),
});
let readme_content = r#"# {{project_name}}
<div align="center">
<!-- Add your own badges here -->
</div>
## 🚀 Overview
This is a new full-stack application, `{{project_name}}`, generated by the [General Web App (GWA)](https://github.com/Yrrrrrf/gwa) scaffolding tool.
It provides a robust foundation with a backend API, a database, and a frontend client, all containerized and ready to run.
## 🛠️ Technology Stack
- **Backend**: Python with FastAPI, leveraging `prism-py` for automatic API generation.
- **Database**: PostgreSQL, fully containerized with Docker.
- **Frontend**: SvelteKit with Deno, styled with TailwindCSS.
- **Cross-Platform**: Ready to be packaged as a desktop application using Tauri.
## 🚦 Getting Started
### Prerequisites
- [Docker](https://www.docker.com/) and Docker Compose
- [Deno](https://deno.land/) (for frontend development)
- [Rust](https://www.rust-lang.org/) (for Tauri desktop builds)
### Running the Application
1. **Review the Environment File**: A `.env` file has been created in the project root with your database credentials. You can modify it if needed.
2. **Start the services using Docker Compose**:
```sh
docker compose up -d
```
3. **Access the Services**:
- **API Docs**: [http://localhost:8000/docs](http://localhost:8000/docs)
- **Frontend App**: [http://localhost:5173](http://localhost:5173)
## 📄 License
This project is licensed under the MIT License. Copyright (c) 2025 {{author_name}}.
"#.to_string();
plan.actions.push(Action::CreateFile {
path: PathBuf::from("README.md"),
content: readme_content,
context: context.clone(),
});
let env_content = r#"# * Database Configuration
# Primary database settings used by Docker Compose
DB_NAME={{db_name}}
DB_OWNER_ADMIN={{db_owner_admin}}
DB_OWNER_PWORD={{db_owner_pword}}
# Connection settings for connecting from your host machine
DB_HOST=localhost
"#.to_string();
plan.actions.push(Action::CreateFile {
path: PathBuf::from(".env"),
content: env_content,
context: context.clone(),
});
println!(
"📝 Transformation plan built with {} actions",
plan.actions.len()
);
Ok(plan)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ProjectConfig;
#[test]
fn test_plan_generation_with_all_components() {
let config = ProjectConfig {
project_name: "my-app".to_string(),
author_name: "Test User".to_string(),
author_email: "test@example.com".to_string(),
app_identifier: "com.example.myapp".to_string(),
db_name: Some("my_app".to_string()),
db_owner_admin: Some("my_app_owner".to_string()),
db_owner_pword: Some("password".to_string()),
include_server: true,
include_frontend: true,
include_tauri_desktop: true,
deno_package_name: "@test/my-app".to_string(),
};
let plan = build_plan(&config).unwrap();
assert!(
plan.actions
.contains(&Action::Delete(PathBuf::from(".github")))
);
assert!(
plan.actions
.contains(&Action::Delete(PathBuf::from("ROADMAP.md")))
);
assert!(
!plan
.actions
.contains(&Action::Delete(PathBuf::from("client/src-tauri")))
);
let expected_context = TemplateContext {
project_name: "my-app".to_string(),
author_name: "Test User".to_string(),
author_email: "test@example.com".to_string(),
app_identifier: "com.example.myapp".to_string(),
db_name: Some("my_app".to_string()),
db_owner_pword: Some("password".to_string()),
db_owner_admin: Some("my_app_owner".to_string()),
include_server: true,
include_frontend: true,
include_tauri_desktop: true,
deno_package_name: "@test/my-app".to_string(),
};
assert!(plan.actions.contains(&Action::ApplyTemplate {
path: PathBuf::from("README.md"),
context: expected_context.clone(),
}));
}
#[test]
fn test_plan_generation_without_tauri() {
let config = ProjectConfig {
project_name: "my-app".to_string(),
include_tauri_desktop: false,
db_owner_pword: Some("password".to_string()),
..Default::default()
};
let plan = build_plan(&config).unwrap();
assert!(
plan.actions
.contains(&Action::Delete(PathBuf::from("client/src-tauri")))
);
}
#[test]
fn test_plan_generation_without_server() {
let config = ProjectConfig {
project_name: "my-app".to_string(),
include_server: false,
db_owner_pword: Some("password".to_string()),
..Default::default()
};
let plan = build_plan(&config).unwrap();
assert!(
plan.actions
.contains(&Action::Delete(PathBuf::from("backend")))
);
}
#[test]
fn test_plan_generation_without_frontend() {
let config = ProjectConfig {
project_name: "my-app".to_string(),
include_frontend: false,
db_owner_pword: Some("password".to_string()),
..Default::default()
};
let plan = build_plan(&config).unwrap();
assert!(
plan.actions
.contains(&Action::Delete(PathBuf::from("frontend")))
);
}
#[test]
fn test_plan_generation_with_rename_action() {
let config = ProjectConfig {
project_name: "my-shiny-new-app".to_string(),
include_tauri_desktop: true,
db_owner_pword: Some("password".to_string()),
..Default::default()
};
let plan = build_plan(&config).unwrap();
assert!(plan.actions.contains(&Action::Rename {
from: PathBuf::from("client"),
to: PathBuf::from("my-shiny-new-app"),
}));
}
}