gwa 0.1.4

General Web App (GWA) project generator engine CLI tool.
Documentation
//! Transformation plan module - defines what transformations need to be applied

use super::EngineError;
use crate::config::ProjectConfig;
use std::path::PathBuf;

// An action to be performed on the cloned template
#[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,
    },
}

// Data to be injected into template files
#[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,
}

// The complete list of actions to transform the template
#[derive(Debug, Default, PartialEq)]
pub struct TransformationPlan {
    pub actions: Vec<Action>,
}

// create a delete macro in actions that receive a list of str
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();

    // 1. Plan basic deletions (files/directories not needed in final project)
    // plan.actions.push(Action::Delete(PathBuf::from(".git")));
    // plan.actions.push(Action::Delete(PathBuf::from(".github")));
    // plan.actions.push(Action::Delete(PathBuf::from("ROADMAP.md")));
    // plan.actions.push(Action::Delete(PathBuf::from("CHANGELOG.md")));

    let actions = delete_actions!(".git", ".github", "ROADMAP.md", "CHANGELOG.md", "README.md");
    plan.actions.extend(actions);

    // 2. Plan conditional deletions based on configuration
    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")));
    }

    // 3. Plan file content transformations using template context
    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(),
    };

    // NOTE: README.md is now deleted from template and will be created fresh below
    // Apply template to key files that contain placeholders
    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(),
    });

    // 4. Plan creation of README.md with comprehensive content
    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(),
    });

    // 5. Plan creation of .env file with database configuration
    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() {
        // Arrange: Create a sample config with all components enabled
        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(),
        };

        // Act: Build the plan
        let plan = build_plan(&config).unwrap();

        // Assert: Check for expected actions
        // 1. Check for basic deletions
        assert!(
            plan.actions
                .contains(&Action::Delete(PathBuf::from(".github")))
        );
        assert!(
            plan.actions
                .contains(&Action::Delete(PathBuf::from("ROADMAP.md")))
        );

        // 2. Check that Tauri directory is NOT deleted when include_tauri_desktop is true
        assert!(
            !plan
                .actions
                .contains(&Action::Delete(PathBuf::from("client/src-tauri")))
        );

        // 3. Check for template application
        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() {
        // Arrange: Create a config with Tauri disabled
        let config = ProjectConfig {
            project_name: "my-app".to_string(),
            include_tauri_desktop: false,
            db_owner_pword: Some("password".to_string()),
            ..Default::default()
        };

        // Act: Build the plan
        let plan = build_plan(&config).unwrap();

        // Assert: Tauri directory SHOULD be in the delete list
        assert!(
            plan.actions
                .contains(&Action::Delete(PathBuf::from("client/src-tauri")))
        );
    }

    #[test]
    fn test_plan_generation_without_server() {
        // Arrange: Create a config with server disabled
        let config = ProjectConfig {
            project_name: "my-app".to_string(),
            include_server: false,
            db_owner_pword: Some("password".to_string()),
            ..Default::default()
        };

        // Act: Build the plan
        let plan = build_plan(&config).unwrap();

        // Assert: Backend directory SHOULD be in the delete list
        assert!(
            plan.actions
                .contains(&Action::Delete(PathBuf::from("backend")))
        );
    }

    #[test]
    fn test_plan_generation_without_frontend() {
        // Arrange: Create a config with frontend disabled
        let config = ProjectConfig {
            project_name: "my-app".to_string(),
            include_frontend: false,
            db_owner_pword: Some("password".to_string()),
            ..Default::default()
        };

        // Act: Build the plan
        let plan = build_plan(&config).unwrap();

        // Assert: Frontend directory SHOULD be in the delete list
        assert!(
            plan.actions
                .contains(&Action::Delete(PathBuf::from("frontend")))
        );
    }

    #[test]
    fn test_plan_generation_with_rename_action() {
        // Arrange: Create a sample config
        let config = ProjectConfig {
            project_name: "my-shiny-new-app".to_string(),
            include_tauri_desktop: true,
            db_owner_pword: Some("password".to_string()),
            ..Default::default()
        };

        // Act: Build the plan
        let plan = build_plan(&config).unwrap();

        // Assert: Check for the rename action
        assert!(plan.actions.contains(&Action::Rename {
            from: PathBuf::from("client"),
            to: PathBuf::from("my-shiny-new-app"),
        }));
    }
}