diff --git a/README.md b/README.md index 0c596f1..001a24a 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Version](https://img.shields.io/badge/Version-0.1.0-green.svg)](Cargo.toml) -Athena is a powerful CLI toolkit for back-end developers and DevOps engineers that simplifies project creation and infrastructure setup. It provides two main capabilities: - -1. **Docker Compose Generator**: Transform a COBOL-inspired DSL into production-ready Docker Compose configurations with minimal effort. -2. **Project Boilerplate Generator**: Quickly scaffold full-stack back-end projects using frameworks like FastAPI, Go (Gin/Echo/Fiber), and Flask, with modern best practices and Docker integration. - +Athena is a powerful CLI tool for back-end developers and DevOps engineers that transforms a COBOL-inspired DSL into production-ready Docker Compose configurations with minimal effort. Built with performance and maintainability in mind, Athena uses intelligent defaults and modern Docker standards to generate optimized configurations with minimal configuration. @@ -103,7 +99,7 @@ Suggestion: Use different host ports, e.g., 8080, 8081 ### Installation ```bash # Install from source -git clone https://github.com/your-org/athena.git +git clone https://github.com/Jeck0v/Athena cd athena cargo install --path . @@ -131,15 +127,6 @@ END SERVICE' > deploy.ath athena build deploy.ath ``` -### Generate Full-Stack Project -```bash -# Create FastAPI + PostgreSQL project -athena init fastapi my-api --with-postgresql - -# Create Go + MongoDB microservice -athena init go my-service --framework gin --with-mongodb -``` - ## Key Features ### Enhanced Error Handling System (New!) @@ -149,14 +136,14 @@ athena init go my-service --framework gin --with-mongodb - **Fail-Fast Processing** => Immediate feedback with no partial generation ### Intelligent Defaults 2025+ -- No more `version` field modern Docker Compose spec compliance +- Auto check for the Dockerfile - Auto-detects service types database, Cache, WebApp, Proxy patterns - Smart restart policies `always` for databases, `unless-stopped` for apps - Optimized health checks different intervals per service type - Container naming follows modern conventions (`project-service`) ### Docker-First Approach -- Dockerfile by default => No image? Uses `build.dockerfile: Dockerfile` +- Dockerfile by default => No image? Just dont configure it and athena will check for your Dockerfile nativement - Intelligent networking => Auto-configured networks with proper isolation - Production-ready => Security, resource limits, and health monitoring - Standards compliant => Follows Docker Compose 2025 best practices @@ -167,13 +154,7 @@ athena init go my-service --framework gin --with-mongodb - Optimized parsing => **<1ms parse time, <2ms generation** - Memory efficient => Pre-allocated structures for large compositions -### Full-Stack Boilerplates -- FastAPI + PostgreSQL/MongoDB => Production authentication, async drivers -- Go + Gin/Echo/Fiber => Clean architecture, proper middleware -- Flask + PostgreSQL => Modern Python web development -- Docker ready => Multi-stage builds, Nginx reverse proxy included - -### Syntax Highlighting (New!) +### Syntax Highlighting (SOON) - **Beautiful DSL highlighting** for `.ath` files with customizable colors - **Zed editor extension** ready to install in `syntax-highlighting/` - **Smart color coding** for keywords, directives, template variables, and more @@ -186,7 +167,6 @@ athena init go my-service --framework gin --with-mongodb - [Syntax Highlighting (**New**)](syntax-highlighting/README.md) - Beautiful colors for `.ath` files in Zed editor. - [Installation Guide](docs/INSTALLATION.md) - [Docker Compose Generator Usage](docs/DSL_REFERENCE.md) -- [Boilerplate Project Generator](docs/BOILERPLATE.md) - [Examples](docs/EXAMPLES.md) ### Development @@ -196,26 +176,15 @@ athena init go my-service --framework gin --with-mongodb ## Basic Usage -### Docker Compose Generator ```bash athena build deploy.ath # Generate docker-compose.yml athena build deploy.ath -o custom.yml # Custom output file athena validate deploy.ath # Validate syntax only +athena info # Show DSL information +athena info --examples # Show usage examples +athena info --directives # Show all directives ``` -### Boilerplate Generator -```bash -# FastAPI projects -athena init fastapi my-api --with-postgresql -athena init fastapi my-api --with-mongodb - -# Go projects -athena init go my-service --framework gin -athena init go my-service --framework echo --with-postgresql - -# Flask projects -athena init flask my-app --with-postgresql -``` ## What Athena Adds Automatically - Smart service detection (Database, Cache, WebApp, Proxy) @@ -241,4 +210,4 @@ This project is licensed under the MIT License see the [LICENSE](LICENSE) file f --- -Built with ❤️ using Rust | Production-ready DevOps made simple +Built with ❤️ using Rust | Make DevOps great again. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41352d7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +# Generated by Athena v0.1.0 from test_no_conflicts deployment +# Developed by UNFAIR Team: https://github.com/Jeck0v/Athena +# Generated: 2025-11-24 09:32:38 UTC +# Features: Intelligent defaults, optimized networking, enhanced health checks + +# Services: 3 configured with intelligent defaults + +services: + app3: + image: apache:latest + container_name: test-no-conflicts-app3 + ports: + - 9000:80 + restart: always + networks: + - test_no_conflicts_network + pull_policy: missing + labels: + athena.type: proxy + athena.project: test_no_conflicts + athena.service: app3 + athena.generated: 2025-11-24 + + app1: + image: nginx:alpine + container_name: test-no-conflicts-app1 + ports: + - 8080:80 + restart: always + networks: + - test_no_conflicts_network + pull_policy: missing + labels: + athena.type: proxy + athena.project: test_no_conflicts + athena.service: app1 + athena.generated: 2025-11-24 + + app2: + image: httpd:alpine + container_name: test-no-conflicts-app2 + ports: + - 8081:8000 + restart: unless-stopped + networks: + - test_no_conflicts_network + pull_policy: missing + labels: + athena.service: app2 + athena.project: test_no_conflicts + athena.type: generic + athena.generated: 2025-11-24 +networks: + test_no_conflicts_network: + driver: bridge +name: test_no_conflicts \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4bc2494..45cf939 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -19,12 +19,6 @@ athena/ │ │ │ ├── compose.rs # Main generator │ │ │ └── defaults.rs # Intelligent defaults engine │ │ └── error.rs # Typed error handling -│ ├── boilerplate/ # Project generators -│ │ ├── fastapi.rs # FastAPI project generator -│ │ ├── go.rs # Go project generator -│ │ ├── flask.rs # Flask project generator -│ │ ├── templates.rs # Embedded templates -│ │ └── utils.rs # Template utilities │ └── main.rs # Application entrypoint ├── docs/ # Documentation ├── tests/ # Comprehensive test suite @@ -32,11 +26,6 @@ athena/ │ │ ├── cli_commands_test.rs # CLI command tests │ │ ├── docker_compose_generation_test.rs # YAML generation tests │ │ ├── error_handling_test.rs # Error scenario tests -│ │ ├── boilerplate/ # Modular boilerplate tests -│ │ │ ├── fastapi_tests.rs # FastAPI project generation -│ │ │ ├── flask_tests.rs # Flask project generation -│ │ │ ├── go_tests.rs # Go project generation -│ │ │ └── common_tests.rs # Common init functionality │ │ └── structural/ # Lightweight structural tests │ │ ├── basic_structure.rs # YAML structure validation │ │ ├── service_configuration.rs # Service config tests @@ -68,5 +57,5 @@ athena/ - **Parsing** using `pest` for grammar-based parsing - **YAML generation** using `serde_yaml` for safe serialization - **Testing** using comprehensive integration tests with GitHub Actions CI/CD -- **Test structure** organized by functionality (structural, boilerplate, CLI, error handling) +- **Test structure** organized by functionality (structural, CLI, error handling) - **Lightweight testing** approach focusing on logic over format \ No newline at end of file diff --git a/docs/BOILERPLATE.md b/docs/BOILERPLATE.md deleted file mode 100644 index 61076ab..0000000 --- a/docs/BOILERPLATE.md +++ /dev/null @@ -1,419 +0,0 @@ -# Boilerplate Project Generator - -Generate production-ready full-stack applications with modern best practices. - -## FastAPI Projects -```bash -# FastAPI + PostgreSQL -athena init fastapi my-api --with-postgresql - -# FastAPI + MongoDB (default) -athena init fastapi my-api --with-mongodb - -# Without Docker files -athena init fastapi my-api --no-docker -``` - -**Generated FastAPI Structure:** -``` -my-api/ -├── app/ -│ ├── __init__.py -│ ├── main.py # FastAPI application -│ ├── core/ -│ │ ├── config.py # Settings management -│ │ ├── security.py # JWT + password hashing -│ │ └── database.py # Async database config -│ ├── api/ -│ │ ├── v1/ # API versioning -│ │ │ ├── auth.py # Authentication endpoints -│ │ │ └── users.py # User management -│ ├── models/ # Database models -│ ├── schemas/ # Pydantic models -│ └── services/ # Business logic -├── tests/ # Comprehensive test suite -├── nginx/ # Reverse proxy config -├── logs/ # Application logs -├── requirements.txt # Python dependencies -├── Dockerfile # Production Docker build -├── docker-compose.yml # Full stack deployment -└── .env.example # Environment template -``` - -## Go Projects -```bash -# Go + Gin (default) -athena init go my-service - -# Go + Echo framework -athena init go my-service --framework echo --with-postgresql - -# Go + Fiber framework -athena init go my-service --framework fiber --with-mongodb -``` - -**Generated Go Structure:** -``` -my-service/ -├── cmd/ -│ └── server/ -│ └── main.go # Application entrypoint -├── internal/ -│ ├── config/ # Configuration management -│ ├── handler/ # HTTP handlers -│ ├── middleware/ # Custom middleware -│ ├── model/ # Data models -│ ├── repository/ # Data access layer -│ └── service/ # Business logic -├── pkg/ # Public packages -├── tests/ # Test suite -├── scripts/ # Build & deployment scripts -├── Dockerfile # Production build -├── docker-compose.yml # Development environment -├── go.mod # Go modules -└── .env.example # Environment template -``` - -## Flask Projects -```bash -# Flask + PostgreSQL (default) -athena init flask my-app - -# Flask + MySQL -athena init flask my-app --with-mysql - -# Without Docker files -athena init flask my-app --no-docker -``` - -**Generated Flask Structure:** -``` -my-app/ -├── app/ -│ ├── __init__.py # Flask application factory -│ ├── core/ -│ │ ├── config.py # Configuration management -│ │ ├── extensions.py # Flask extensions -│ │ └── logging.py # Structured logging -│ ├── api/ -│ │ ├── health.py # Health check endpoints -│ │ └── v1/ # API versioning -│ │ ├── auth.py # JWT authentication -│ │ └── users.py # User management -│ ├── models/ # SQLAlchemy models -│ ├── schemas/ # Marshmallow schemas -│ └── services/ # Business logic layer -├── tests/ # Comprehensive test suite -├── nginx/ # Reverse proxy config -├── requirements.txt # Python dependencies -├── Dockerfile # Multi-stage production build -├── docker-compose.yml # Full stack deployment -└── .env.example # Environment template -``` - -## Laravel Projects (Clean Architecture) -```bash -# Laravel + PostgreSQL (default) -athena init laravel my-project - -# Laravel + MySQL -athena init laravel my-project --with-mysql - -# Without Docker files -athena init laravel my-project --no-docker -``` - -**Generated Laravel Structure:** -``` -my-project/ -├── app/ -│ ├── Domain/ # Domain layer (Clean Architecture) -│ │ └── User/ -│ │ ├── Entities/ # Domain entities -│ │ │ └── User.php # User entity with business logic -│ │ ├── Repositories/ # Repository interfaces -│ │ └── Services/ # Domain services -│ ├── Application/ # Application layer -│ │ └── User/ -│ │ ├── UseCases/ # Use cases (business logic) -│ │ ├── DTOs/ # Data Transfer Objects -│ │ └── Services/ # Application services -│ └── Infrastructure/ # Infrastructure layer -│ ├── Http/ -│ │ ├── Controllers/ # API controllers -│ │ └── Middleware/ # Custom middleware -│ ├── Persistence/ # Data persistence -│ │ ├── Eloquent/ # Eloquent models -│ │ └── Repositories/ # Repository implementations -│ └── Providers/ # Service providers -├── config/ # Laravel configuration -├── database/ -│ ├── migrations/ # Database migrations -│ └── seeders/ # Data seeders -├── tests/ # Feature & Unit tests -├── docker/ # Docker configurations -├── nginx/ # Nginx configuration -├── composer.json # PHP dependencies (Laravel 11, PHP 8.2) -├── Dockerfile # Multi-stage production build -├── docker-compose.yml # Full stack deployment -└── .env.example # Environment template -``` - -## Symfony Projects (Hexagonal Architecture) -```bash -# Symfony + PostgreSQL (default) -athena init symfony my-api - -# Symfony + MySQL -athena init symfony my-api --with-mysql - -# Without Docker files -athena init symfony my-api --no-docker -``` - -**Generated Symfony Structure:** -``` -my-api/ -├── src/ -│ ├── Domain/ # Domain layer (Hexagonal Architecture) -│ │ └── User/ -│ │ ├── Entities/ # Domain entities -│ │ │ └── User.php # Pure domain entity -│ │ ├── ValueObjects/ # Value objects -│ │ │ ├── UserId.php # User ID value object -│ │ │ ├── Email.php # Email value object -│ │ │ ├── UserName.php # User name value object -│ │ │ └── HashedPassword.php -│ │ └── Repositories/ # Repository interfaces -│ │ └── UserRepositoryInterface.php -│ ├── Application/ # Application layer -│ │ └── User/ -│ │ ├── Commands/ # CQRS Commands -│ │ │ ├── CreateUserCommand.php -│ │ │ └── LoginCommand.php -│ │ ├── Queries/ # CQRS Queries -│ │ │ └── GetUserQuery.php -│ │ ├── Handlers/ # Command/Query handlers -│ │ │ ├── UserHandler.php -│ │ │ └── AuthHandler.php -│ │ └── Services/ # Application services -│ │ ├── UserService.php -│ │ └── AuthService.php -│ └── Infrastructure/ # Infrastructure layer -│ ├── Http/ -│ │ └── Controllers/ # API controllers -│ │ └── UserController.php -│ └── Persistence/ -│ └── Doctrine/ -│ ├── Entities/ # Doctrine entities -│ │ └── User.php # Infrastructure User entity -│ └── Repositories/ # Repository implementations -│ └── DoctrineUserRepository.php -├── config/ # Symfony configuration -├── migrations/ # Doctrine migrations -├── tests/ # Functional & Unit tests -├── docker/ # Docker configurations -├── nginx/ # Nginx configuration -├── composer.json # PHP dependencies (Symfony 7, PHP 8.2) -├── Dockerfile # Multi-stage production build -├── docker-compose.yml # Full stack deployment -└── .env.example # Environment template -``` - -## Features & Best Practices 2025 - -### **Architecture Patterns** -- **Laravel**: Clean Architecture with Domain/Application/Infrastructure layers -- **Symfony**: Hexagonal Architecture with CQRS pattern -- **FastAPI**: Async-first architecture with dependency injection -- **Flask**: Layered architecture with factory pattern -- **Go**: Clean architecture with interfaces and dependency injection - -### **Security & Authentication** -- **JWT Authentication** with refresh tokens -- **Password hashing** with modern algorithms (bcrypt/argon2) -- **CORS configuration** for cross-origin requests -- **Input validation** and sanitization -- **Security headers** in Nginx configuration -- **Environment-based secrets** management - -### **Modern Language Features** -- **PHP 8.2+**: Strict types, readonly properties, attributes -- **Python 3.12+**: Type hints, async/await, dataclasses -- **Go 1.22+**: Generics, structured logging with slog -- **Dependency injection** and inversion of control -- **Value objects** and domain-driven design - -### **Production-Ready Infrastructure** -- **Multi-stage Dockerfiles** for optimized builds -- **Nginx reverse proxy** with caching and compression -- **Health checks** and monitoring endpoints -- **Structured logging** with correlation IDs -- **Database migrations** and seeding -- **Redis caching** integration - -### **Testing & Quality** -- **Comprehensive test suites** (unit, integration, functional) -- **PHPUnit 10** / **pytest** / **testify** frameworks -- **Code quality tools**: PHPStan, mypy, golangci-lint -- **Code formatting**: PHP-CS-Fixer, black, gofmt -- **Test coverage** reporting -- **CI/CD ready** configurations - -### **Development Experience** -- **Hot reload** in development environments -- **Environment-based configuration** (.env files) -- **Database GUI tools** (Adminer/phpMyAdmin) -- **API documentation** with OpenAPI/Swagger -- **Pre-commit hooks** for code quality -- **Development scripts** and automation - -## Quick Start Example - -```bash -# Create a modern Laravel API -athena init laravel my-laravel-api -cd my-laravel-api -cp .env.example .env - -# Start with Docker -docker-compose up --build - -# Install dependencies and migrate -docker-compose exec app composer install -docker-compose exec app php artisan migrate - -# Test the API -curl http://localhost/api/health -``` - -```bash -# Create a Symfony hexagonal API -athena init symfony my-symfony-api --with-mysql -cd my-symfony-api -cp .env.example .env - -# Start with Docker -docker-compose up --build - -# Install dependencies and migrate -docker-compose exec app composer install -docker-compose exec app php bin/console doctrine:migrations:migrate - -# Test the API -curl http://localhost/api/health -``` - -## PHP Vanilla Projects (Clean Architecture) -```bash -# PHP Vanilla + PostgreSQL (default) -athena init vanilla my-api - -# PHP Vanilla + MySQL -athena init vanilla my-api --with-mysql - -# Without Docker files -athena init vanilla my-api --no-docker -``` - -**Generated PHP Vanilla Structure:** -``` -my-api/ -├── src/ -│ ├── Domain/ # Domain layer (Clean Architecture) -│ │ └── User/ -│ │ ├── Entity/ # Domain entities -│ │ │ └── User.php # Pure domain entity with business logic -│ │ ├── Repository/ # Repository interfaces -│ │ │ └── UserRepositoryInterface.php -│ │ ├── Service/ # Domain services -│ │ │ └── UserService.php # User business logic -│ │ └── ValueObject/ # Value objects -│ │ ├── UserId.php # UUID-based user ID -│ │ └── Email.php # Email validation value object -│ ├── Application/ # Application layer -│ │ ├── User/ -│ │ │ ├── Command/ # Command objects -│ │ │ │ └── CreateUserCommand.php -│ │ │ └── Handler/ # Command handlers -│ │ │ └── CreateUserHandler.php -│ │ └── Auth/ -│ │ ├── Command/ # Authentication commands -│ │ │ └── LoginCommand.php -│ │ └── Handler/ # Authentication handlers -│ │ └── LoginHandler.php -│ └── Infrastructure/ # Infrastructure layer -│ ├── Http/ -│ │ ├── Router.php # Custom routing system -│ │ ├── Request.php # HTTP request abstraction -│ │ ├── Response.php # HTTP response abstraction -│ │ ├── Controller/ -│ │ │ └── Api/V1/ # Versioned API controllers -│ │ │ ├── AuthController.php # JWT authentication -│ │ │ └── UserController.php # User management -│ │ └── Middleware/ # HTTP middleware -│ │ ├── AuthMiddleware.php # JWT validation -│ │ └── CorsMiddleware.php # CORS handling -│ ├── Persistence/ -│ │ └── PDO/ # PDO implementations -│ │ └── UserRepository.php # User data access -│ ├── Database/ -│ │ └── PDOConnection.php # Database connection management -│ ├── Security/ -│ │ └── JWTManager.php # JWT token management -│ └── Config/ -│ └── AppConfig.php # Configuration management -├── public/ -│ ├── index.php # Application entry point -│ └── .htaccess # Apache rewrite rules -├── config/ -│ ├── app.php # Application configuration -│ └── database.php # Database configuration -├── database/ -│ └── migrations/ -│ └── 001_create_users_table.sql # Database schema -├── tests/ # Comprehensive test suite -│ ├── Unit/ # Unit tests -│ │ └── UserTest.php # Domain entity tests -│ ├── Integration/ # Integration tests -│ └── Functional/ # Functional tests -│ └── AuthTest.php # API endpoint tests -├── docker/ # Docker configurations -├── composer.json # PHP dependencies (PHP 8.2+) -├── phpunit.xml # Testing configuration -├── Dockerfile # Multi-stage production build -├── docker-compose.yml # Full stack deployment -├── .env.example # Environment template -└── README.md # Setup and API documentation -``` - -**PHP Vanilla Architecture Features:** -- **Pure Clean Architecture**: Domain-driven design without framework constraints -- **Custom HTTP Layer**: Built-in Router, Request/Response handling -- **PDO Database Layer**: Multi-database support (PostgreSQL, MySQL) -- **JWT Authentication**: Secure token-based authentication -- **Value Objects**: Type-safe domain modeling -- **Command/Handler Pattern**: CQRS-lite for business operations -- **PSR-4 Autoloading**: Modern PHP namespace organization -- **Dependency Injection**: Manual DI for learning and control - -**Database Support:** -- **PostgreSQL** (default): Production-ready with UUID support -- **MySQL**: Alternative with charset configuration -- **PDO Abstraction**: Database-agnostic query layer -- **Migration System**: SQL-based schema management -- **Connection Pooling**: Singleton pattern for efficiency - -**API Endpoints:** -``` -GET /api/v1/health # Health check -POST /api/v1/auth/register # User registration -POST /api/v1/auth/login # JWT authentication -POST /api/v1/auth/logout # Token invalidation -GET /api/v1/auth/me # Current user info -GET /api/v1/users # List users -GET /api/v1/users/{id} # Get user by ID -POST /api/v1/users # Create user -``` - -All generated projects include comprehensive README files with setup instructions, API documentation, and deployment guides. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5438b0d..d9bdc71 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -30,7 +30,6 @@ cargo test --test integration_tests structural cargo test --test integration_tests cli_commands_test cargo test --test integration_tests docker_compose_generation_test cargo test --test integration_tests error_handling_test -cargo test --test integration_tests boilerplate # Run with verbose output cargo test --test integration_tests structural --verbose @@ -49,7 +48,6 @@ cargo test --test integration_tests structural --verbose ## Test Requirements - Add structural tests for Docker Compose generation changes -- Add boilerplate tests for new project generators - Add CLI tests for new command options - All integration tests must pass on Ubuntu latest via GitHub Actions diff --git a/docs/TESTING.md b/docs/TESTING.md index 9479e8f..a916599 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -23,12 +23,6 @@ tests/ │ ├── enhanced_error_handling_test.rs # Advanced error scenarios with suggestions │ ├── build_args_cli_tests.rs # Dockerfile integration and BUILD-ARGS tests │ ├── swarm_features_test.rs # Docker Swarm support and error handling -│ ├── boilerplate/ # Modular boilerplate tests by framework -│ │ ├── mod.rs # Common utilities and shared functions -│ │ ├── fastapi_tests.rs # FastAPI project generation tests -│ │ ├── flask_tests.rs # Flask project generation tests -│ │ ├── go_tests.rs # Go project generation tests (Gin, Echo, Fiber) -│ │ └── common_tests.rs # Common init command tests and validation │ └── structural/ # Organized structural tests (lightweight) │ ├── mod.rs # Common utilities and module declarations │ ├── basic_structure.rs # Basic YAML structure validation @@ -105,18 +99,9 @@ cargo test --test integration_tests build_args_cli_tests # Docker Swarm feature tests cargo test --test integration_tests swarm_features_test -# Boilerplate generation tests -cargo test --test integration_tests boilerplate - # All structural tests (lightweight YAML validation) cargo test --test integration_tests structural -# Specific boilerplate test categories -cargo test --test integration_tests boilerplate::fastapi_tests -cargo test --test integration_tests boilerplate::flask_tests -cargo test --test integration_tests boilerplate::go_tests -cargo test --test integration_tests boilerplate::common_tests - # Specific structural test categories cargo test --test integration_tests structural::basic_structure cargo test --test integration_tests structural::service_configuration @@ -192,15 +177,7 @@ cargo test --test integration_tests structural --verbose - Complete integration tests with Swarm + Compose features - 13 dedicated error handling tests for edge cases -### 7. Boilerplate Generation Tests (`boilerplate/`) -- **Modular organization** by framework for better maintainability -- **FastAPI tests** (`fastapi_tests.rs`): Basic init, PostgreSQL/MongoDB options, Docker/no-Docker modes -- **Flask tests** (`flask_tests.rs`): Basic init, MySQL integration -- **Go tests** (`go_tests.rs`): Gin, Echo, and Fiber framework support -- **Common tests** (`common_tests.rs`): Error handling, help commands, project validation -- Tests project structure generation, configuration files, and dependency setup - -### 8. Structural Tests (`structural/`) +### 7. Structural Tests (`structural/`) - **Organized by functional categories** for better maintainability - **Lightweight YAML validation** without heavy snapshots - Tests **structure and logic** rather than exact formatting @@ -315,26 +292,16 @@ fn test_service_configuration_structure() { - **Logic correctness** (restart policies, health checks) - **Docker Compose compliance** (valid modern format) -### Boilerplate Tests -Modular boilerplate tests organized by framework, each verifying: -- **Project directory creation** and proper file structure -- **Framework-specific configurations** (dependencies, settings) -- **Database integration** (PostgreSQL, MongoDB, MySQL setup) -- **Docker configuration** generation and optional exclusion -- **Custom directory** and project name handling -- **Error scenarios** and validation - ### Test Performance & Statistics **Current test suite:** -- **Total tests**: 117 integration tests +- **Total tests**: 103 integration tests - **CLI tests**: 13 tests (command parsing, help, validation) - **Docker Compose generation**: 11 tests (YAML generation, validation, port conflict detection) - **Error handling**: 21 tests (comprehensive error scenarios including port conflicts) - **Enhanced error handling**: 6 tests (advanced error scenarios with suggestions) - **Build args CLI**: 8 tests (Dockerfile integration and validation) - **Swarm features**: 21 tests (Docker Swarm support with comprehensive error handling) -- **Boilerplate generation**: 14 tests (modular by framework) - **Structural tests**: 23 tests (organized in 6 categories including comments) - **Execution time**: < 1 second for structural tests - **Test organization**: Modular structure for easy maintenance @@ -350,12 +317,6 @@ Modular boilerplate tests organized by framework, each verifying: - `comments.rs`: 11 tests (comment parsing, edge cases, multi-line comments) - `complex_scenarios.rs`: 1 test (complex microservices architecture) -**Boilerplate tests:** -- `fastapi_tests.rs`: 6 tests (basic, PostgreSQL, MongoDB, no-Docker, custom directory, help) -- `flask_tests.rs`: 2 tests (basic, MySQL integration) -- `go_tests.rs`: 3 tests (Gin, Echo, Fiber frameworks) -- `common_tests.rs`: 3 tests (error handling, validation, help commands) - ## Port Conflict Detection Tests ### Overview @@ -501,10 +462,3 @@ For new structural tests, place them in the appropriate category: - `policies.rs`: Restart policies and health checks - `formatting.rs`: YAML validity and output formatting - `complex_scenarios.rs`: Multi-service architecture tests - -### Adding Boilerplate Tests -For new boilerplate tests, place them in the appropriate framework file: -- `fastapi_tests.rs`: FastAPI-specific features and configurations -- `flask_tests.rs`: Flask-specific features and database integrations -- `go_tests.rs`: Go framework-specific tests (Gin, Echo, Fiber) -- `common_tests.rs`: Cross-framework functionality (validation, help, errors) diff --git a/src/boilerplate/fastapi.rs b/src/boilerplate/fastapi.rs deleted file mode 100644 index d85ff58..0000000 --- a/src/boilerplate/fastapi.rs +++ /dev/null @@ -1,1102 +0,0 @@ -//! FastAPI boilerplate generator with production-ready features - -use crate::boilerplate::{BoilerplateGenerator, BoilerplateResult, ProjectConfig, DatabaseType}; -use crate::boilerplate::utils::{create_directory_structure, write_file, replace_template_vars_string, generate_secret_key, ProjectNames}; -use crate::boilerplate::templates::fastapi::*; -use std::path::Path; - -pub struct FastAPIGenerator; - -impl Default for FastAPIGenerator { - fn default() -> Self { - Self::new() - } -} - -impl FastAPIGenerator { - pub fn new() -> Self { - Self - } - - fn get_template_vars(&self, config: &ProjectConfig) -> Vec<(&str, String)> { - let names = ProjectNames::new(&config.name); - let secret_key = generate_secret_key(); - - vec![ - ("project_name", config.name.clone()), - ("snake_case", names.snake_case.clone()), - ("kebab_case", names.kebab_case.clone()), - ("pascal_case", names.pascal_case), - ("upper_case", names.upper_case), - ("secret_key", secret_key), - ("module_name", names.kebab_case), - ] - } - - fn create_fastapi_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - "app", - "app/api", - "app/api/v1", - "app/core", - "app/database", - "app/models", - "app/schemas", - "app/services", - "tests", - "tests/api", - "logs", - "nginx", - "nginx/conf.d", - "scripts", - ]; - - create_directory_structure(base_path, &directories) - } - - fn generate_core_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Main application file - let main_content = replace_template_vars_string(MAIN_PY, &vars); - write_file(base_path.join("app/main.py"), &main_content)?; - - // Core configuration - let mut config_content = CONFIG_PY.to_string(); - match config.database { - DatabaseType::MongoDB => { - config_content = config_content.replace("{{#if mongodb}}", ""); - config_content = config_content.replace("{{/if}}", ""); - config_content = config_content.replace("{{#if postgresql}}", ""); - config_content = config_content.replace("{{/if}}", ""); - } - DatabaseType::PostgreSQL => { - config_content = config_content.replace("{{#if postgresql}}", ""); - config_content = config_content.replace("{{/if}}", ""); - config_content = config_content.replace("{{#if mongodb}}", ""); - config_content = config_content.replace("{{/if}}", ""); - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - } - config_content = replace_template_vars_string(&config_content, &vars); - write_file(base_path.join("app/core/config.py"), &config_content)?; - write_file(base_path.join("app/core/__init__.py"), "")?; - - // Security module - let security_content = replace_template_vars_string(SECURITY_PY, &vars); - write_file(base_path.join("app/core/security.py"), &security_content)?; - - // Structured logging module (new in 2025) - let logging_content = replace_template_vars_string(crate::boilerplate::templates::LOGGING_PY, &vars); - write_file(base_path.join("app/core/logging.py"), &logging_content)?; - - // Rate limiting module (new in 2025) - let rate_limiting_content = replace_template_vars_string(crate::boilerplate::templates::RATE_LIMITING_PY, &vars); - write_file(base_path.join("app/core/rate_limiting.py"), &rate_limiting_content)?; - - Ok(()) - } - - fn generate_api_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // API init files - write_file(base_path.join("app/api/__init__.py"), "")?; - write_file(base_path.join("app/api/v1/__init__.py"), "")?; - - // Enhanced health endpoint with readiness and liveness (2025 best practices) - let health_py = r#"from __future__ import annotations - -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.ext.asyncio import AsyncSession -import structlog -import time -from datetime import datetime, timezone - -from app.core.config import settings -from app.database.connection import get_async_session - -router = APIRouter() -logger = structlog.get_logger() - -# Store start time for uptime calculation -start_time = time.time() - -# Health check dependencies -async def check_database(db: AsyncSession = Depends(get_async_session)) -> bool: - """Check database connectivity""" - try: - await db.execute("SELECT 1") - return True - except Exception as e: - logger.error("Database health check failed", error=str(e)) - return False - -@router.get("/health") -async def health_check() -> Dict[str, Any]: - """Basic health check endpoint""" - return { - "status": "healthy", - "service": "{{project_name}} API", - "version": "1.0.0", - "timestamp": datetime.now(timezone.utc).isoformat(), - "environment": settings.ENVIRONMENT - } - -@router.get("/health/ready") -async def readiness_check(db_healthy: bool = Depends(check_database)) -> Dict[str, Any]: - """Readiness check - can the service handle requests?""" - checks = { - "database": db_healthy, - "service": True # Add more checks as needed - } - - all_healthy = all(checks.values()) - - if not all_healthy: - logger.warning("Readiness check failed", checks=checks) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail={"status": "not ready", "checks": checks} - ) - - return { - "status": "ready", - "service": "{{project_name}} API", - "checks": checks, - "timestamp": datetime.now(timezone.utc).isoformat() - } - -@router.get("/health/live") -async def liveness_check() -> Dict[str, Any]: - """Liveness check - is the service alive?""" - return { - "status": "alive", - "service": "{{project_name}} API", - "timestamp": datetime.now(timezone.utc).isoformat(), - "uptime_seconds": time.time() - start_time - } -"#; - let health_content = replace_template_vars_string(health_py, &vars); - write_file(base_path.join("app/api/health.py"), &health_content)?; - - // Auth endpoints - let auth_py = r#"from fastapi import APIRouter, HTTPException, Depends, status -from fastapi.security import HTTPBearer -from pydantic import BaseModel -from typing import Optional - -from app.core.security import ( - verify_password, get_password_hash, create_access_token, - create_refresh_token, verify_token -) -from app.services.user_service import UserService - -router = APIRouter() -security = HTTPBearer() - -class LoginRequest(BaseModel): - email: str - password: str - -class RegisterRequest(BaseModel): - email: str - password: str - full_name: str - -class TokenResponse(BaseModel): - access_token: str - refresh_token: str - token_type: str = "bearer" - -class RefreshRequest(BaseModel): - refresh_token: str - -@router.post("/login", response_model=TokenResponse) -async def login(request: LoginRequest): - user_service = UserService() - - # Find user by email - user = await user_service.get_by_email(request.email) - if not user or not verify_password(request.password, user.hashed_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid credentials" - ) - - # Create tokens - access_token = create_access_token({"sub": str(user.id), "email": user.email}) - refresh_token = create_refresh_token({"sub": str(user.id)}) - - return TokenResponse( - access_token=access_token, - refresh_token=refresh_token - ) - -@router.post("/register", response_model=TokenResponse) -async def register(request: RegisterRequest): - user_service = UserService() - - # Check if user already exists - existing_user = await user_service.get_by_email(request.email) - if existing_user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="User already exists" - ) - - # Create user - hashed_password = get_password_hash(request.password) - user = await user_service.create({ - "email": request.email, - "hashed_password": hashed_password, - "full_name": request.full_name, - "is_active": True - }) - - # Create tokens - access_token = create_access_token({"sub": str(user.id), "email": user.email}) - refresh_token = create_refresh_token({"sub": str(user.id)}) - - return TokenResponse( - access_token=access_token, - refresh_token=refresh_token - ) - -@router.post("/refresh", response_model=TokenResponse) -async def refresh_token(request: RefreshRequest): - # Verify refresh token - payload = verify_token(request.refresh_token, "refresh") - user_id = payload.get("sub") - - if not user_id: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid refresh token" - ) - - user_service = UserService() - user = await user_service.get_by_id(user_id) - - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) - - # Create new tokens - access_token = create_access_token({"sub": str(user.id), "email": user.email}) - new_refresh_token = create_refresh_token({"sub": str(user.id)}) - - return TokenResponse( - access_token=access_token, - refresh_token=new_refresh_token - ) -"#; - let auth_content = replace_template_vars_string(auth_py, &vars); - write_file(base_path.join("app/api/v1/auth.py"), &auth_content)?; - - // Users endpoints - let users_py = r#"from fastapi import APIRouter, HTTPException, Depends, status -from fastapi.security import HTTPAuthorizationCredentials -from pydantic import BaseModel -from typing import List, Optional - -from app.core.security import verify_token -from fastapi.security import HTTPBearer - -security = HTTPBearer() -from app.services.user_service import UserService - -router = APIRouter() - -class UserResponse(BaseModel): - id: str - email: str - full_name: str - is_active: bool - -async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): - payload = verify_token(credentials.credentials) - user_id = payload.get("sub") - - if not user_id: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token" - ) - - user_service = UserService() - user = await user_service.get_by_id(user_id) - - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) - - return user - -@router.get("/me", response_model=UserResponse) -async def get_current_user_info(current_user = Depends(get_current_user)): - return UserResponse( - id=str(current_user.id), - email=current_user.email, - full_name=current_user.full_name, - is_active=current_user.is_active - ) -"#; - let users_content = replace_template_vars_string(users_py, &vars); - write_file(base_path.join("app/api/v1/users.py"), &users_content)?; - - Ok(()) - } - - fn generate_database_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let _vars = self.get_template_vars(config); - - write_file(base_path.join("app/database/__init__.py"), "")?; - - match config.database { - DatabaseType::MongoDB => { - let mongodb_connection = r#"from motor.motor_asyncio import AsyncIOMotorClient -from app.core.config import settings -import logging - -logger = logging.getLogger(__name__) - -class Database: - client: AsyncIOMotorClient = None - database = None - -db = Database() - -async def init_database(): - """Initialize database connection""" - try: - db.client = AsyncIOMotorClient(settings.MONGODB_URL) - db.database = db.client[settings.DATABASE_NAME] - # Test connection - await db.client.admin.command('ping') - logger.info("Successfully connected to MongoDB") - except Exception as e: - logger.error(f"Error connecting to MongoDB: {e}") - raise - -async def close_database_connection(): - """Close database connection""" - if db.client: - db.client.close() - logger.info("Disconnected from MongoDB") - -def get_database(): - """Get database instance""" - return db.database -"#; - write_file(base_path.join("app/database/connection.py"), mongodb_connection)?; - } - DatabaseType::PostgreSQL => { - let postgres_connection = r#"from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import DeclarativeBase -from app.core.config import settings -import logging - -logger = logging.getLogger(__name__) - -# Create async engine -engine = None -async_session_maker = None - -# Base class for models -class Base(DeclarativeBase): - pass - -async def init_database(): - """Initialize database connection""" - global engine, async_session_maker - - try: - # Create async engine - engine = create_async_engine( - settings.DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://"), - echo=settings.ENVIRONMENT == "development", - future=True - ) - - # Create session maker - async_session_maker = async_sessionmaker( - engine, - class_=AsyncSession, - expire_on_commit=False - ) - - # Test connection - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - - logger.info("Successfully connected to PostgreSQL") - except Exception as e: - logger.error(f"Error connecting to PostgreSQL: {e}") - raise - -async def close_database_connection(): - """Close database connection""" - global engine - - if engine: - await engine.dispose() - logger.info("Disconnected from PostgreSQL") - -async def get_async_session() -> AsyncSession: - """Get async database session""" - if not async_session_maker: - raise RuntimeError("Database not initialized") - - async with async_session_maker() as session: - try: - yield session - except Exception: - await session.rollback() - raise - finally: - await session.close() - -def get_engine(): - """Get database engine""" - return engine -"#; - write_file(base_path.join("app/database/connection.py"), postgres_connection)?; - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - } - - Ok(()) - } - - fn generate_models_and_services(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let _vars = self.get_template_vars(config); - - // Models - write_file(base_path.join("app/models/__init__.py"), "")?; - - let user_model = match config.database { - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - DatabaseType::MongoDB => r#"from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from bson import ObjectId - -class PyObjectId(ObjectId): - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - if not ObjectId.is_valid(v): - raise ValueError("Invalid ObjectId") - return ObjectId(v) - - @classmethod - def __get_pydantic_json_schema__(cls, field_schema, handler): - json_schema = handler(str) - json_schema.update(type="string") - return json_schema - -class User(BaseModel): - id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") - email: str - hashed_password: str - full_name: str - is_active: bool = True - - model_config = ConfigDict( - populate_by_name=True, - arbitrary_types_allowed=True, - json_encoders={ObjectId: str} - ) -"#.to_string(), - DatabaseType::PostgreSQL => r#"from sqlalchemy import Column, String, Boolean, DateTime -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.sql import func -from pydantic import BaseModel, ConfigDict -from typing import Optional -import uuid -from datetime import datetime - -from app.database.connection import Base - -# SQLAlchemy model -class UserTable(Base): - __tablename__ = "users" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - email = Column(String, unique=True, nullable=False, index=True) - hashed_password = Column(String, nullable=False) - full_name = Column(String, nullable=False) - is_active = Column(Boolean, default=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - -# Pydantic models -class User(BaseModel): - id: Optional[uuid.UUID] = None - email: str - hashed_password: str - full_name: str - is_active: bool = True - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - - model_config = ConfigDict(from_attributes=True) - -class UserCreate(BaseModel): - email: str - password: str - full_name: str - -class UserResponse(BaseModel): - id: uuid.UUID - email: str - full_name: str - is_active: bool - created_at: datetime - - model_config = ConfigDict(from_attributes=True) -"#.to_string() - }; - - write_file(base_path.join("app/models/user.py"), &user_model)?; - - // Services - write_file(base_path.join("app/services/__init__.py"), "")?; - - let user_service = match config.database { - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - DatabaseType::MongoDB => r#"from typing import Optional, Dict, Any -from bson import ObjectId -from app.models.user import User -from app.database.connection import get_database - -class UserService: - def __init__(self): - self.db = get_database() - self.collection = self.db.users - - async def create(self, user_data: Dict[str, Any]) -> User: - """Create a new user""" - result = await self.collection.insert_one(user_data) - user_data["_id"] = result.inserted_id - return User(**user_data) - - async def get_by_id(self, user_id: str) -> Optional[User]: - """Get user by ID""" - user_doc = await self.collection.find_one({"_id": ObjectId(user_id)}) - return User(**user_doc) if user_doc else None - - async def get_by_email(self, email: str) -> Optional[User]: - """Get user by email""" - user_doc = await self.collection.find_one({"email": email}) - return User(**user_doc) if user_doc else None - - async def update(self, user_id: str, update_data: Dict[str, Any]) -> Optional[User]: - """Update user""" - await self.collection.update_one( - {"_id": ObjectId(user_id)}, - {"$set": update_data} - ) - return await self.get_by_id(user_id) - - async def delete(self, user_id: str) -> bool: - """Delete user""" - result = await self.collection.delete_one({"_id": ObjectId(user_id)}) - return result.deleted_count > 0 -"#.to_string(), - DatabaseType::PostgreSQL => r#"from typing import Optional, Dict, Any -from sqlalchemy import select, insert, update, delete -from sqlalchemy.ext.asyncio import AsyncSession -import uuid - -from app.models.user import User, UserTable -from app.database.connection import get_async_session - -class UserService: - async def create(self, user_data: Dict[str, Any]) -> User: - """Create a new user""" - async for session in get_async_session(): - # Create new user - new_user = UserTable( - email=user_data["email"], - hashed_password=user_data["hashed_password"], - full_name=user_data["full_name"], - is_active=user_data.get("is_active", True) - ) - - session.add(new_user) - await session.commit() - await session.refresh(new_user) - - return User.from_orm(new_user) - - async def get_by_id(self, user_id: str) -> Optional[User]: - """Get user by ID""" - async for session in get_async_session(): - stmt = select(UserTable).where(UserTable.id == uuid.UUID(user_id)) - result = await session.execute(stmt) - user_row = result.scalar_one_or_none() - - return User.from_orm(user_row) if user_row else None - - async def get_by_email(self, email: str) -> Optional[User]: - """Get user by email""" - async for session in get_async_session(): - stmt = select(UserTable).where(UserTable.email == email) - result = await session.execute(stmt) - user_row = result.scalar_one_or_none() - - return User.from_orm(user_row) if user_row else None - - async def update(self, user_id: str, update_data: Dict[str, Any]) -> Optional[User]: - """Update user""" - async for session in get_async_session(): - stmt = ( - update(UserTable) - .where(UserTable.id == uuid.UUID(user_id)) - .values(**update_data) - .returning(UserTable) - ) - result = await session.execute(stmt) - await session.commit() - user_row = result.scalar_one_or_none() - - return User.from_orm(user_row) if user_row else None - - async def delete(self, user_id: str) -> bool: - """Delete user""" - async for session in get_async_session(): - stmt = delete(UserTable).where(UserTable.id == uuid.UUID(user_id)) - result = await session.execute(stmt) - await session.commit() - - return result.rowcount > 0 -"#.to_string() - }; - - write_file(base_path.join("app/services/user_service.py"), &user_service)?; - - Ok(()) - } - - fn generate_docker_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - if !config.include_docker { - return Ok(()); - } - - let vars = self.get_template_vars(config); - - // Requirements - let mut requirements = REQUIREMENTS_TXT.to_string(); - match config.database { - DatabaseType::MongoDB => { - requirements = requirements.replace("{{#if mongodb}}", ""); - requirements = requirements.replace("{{/if}}", ""); - requirements = requirements.replace("{{#if postgresql}}", ""); - requirements = requirements.replace("{{/if}}", ""); - } - DatabaseType::PostgreSQL => { - requirements = requirements.replace("{{#if postgresql}}", ""); - requirements = requirements.replace("{{/if}}", ""); - requirements = requirements.replace("{{#if mongodb}}", ""); - requirements = requirements.replace("{{/if}}", ""); - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - } - write_file(base_path.join("requirements.txt"), &requirements)?; - - // Dockerfile - write_file(base_path.join("Dockerfile"), DOCKERFILE)?; - - // Docker Compose - let mut compose_content = DOCKER_COMPOSE_YML.to_string(); - match config.database { - DatabaseType::MongoDB => { - compose_content = compose_content.replace("{{#if mongodb}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - compose_content = compose_content.replace("{{#if postgresql}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - } - DatabaseType::PostgreSQL => { - compose_content = compose_content.replace("{{#if postgresql}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - compose_content = compose_content.replace("{{#if mongodb}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for FastAPI projects. Use Flask for MySQL support.".to_string() - )); - } - } - compose_content = replace_template_vars_string(&compose_content, &vars); - write_file(base_path.join("docker-compose.yml"), &compose_content)?; - - // Nginx configurations - let nginx_content = replace_template_vars_string(NGINX_CONF, &vars); - write_file(base_path.join("nginx/nginx.conf"), &nginx_content)?; - - let nginx_default_content = replace_template_vars_string(crate::boilerplate::templates::fastapi::NGINX_DEFAULT_CONF, &vars); - write_file(base_path.join("nginx/conf.d/default.conf"), &nginx_default_content)?; - - // .env template - let env_template = format!(r#"# Environment Configuration -ENVIRONMENT=development - -# Security -SECRET_KEY=your-secret-key-here-change-in-production - -# Database{}{} - -# Redis -REDIS_URL=redis://localhost:6379 - -# CORS -ALLOWED_HOSTS=["http://localhost:3000","http://127.0.0.1:3000"] -"#, - if matches!(config.database, DatabaseType::MongoDB) { - "\nMONGODB_URL=mongodb://localhost:27017\nDATABASE_NAME=".to_string() + &ProjectNames::new(&config.name).snake_case + "_db" - } else { "".to_string() }, - if matches!(config.database, DatabaseType::PostgreSQL) { - "\nDATABASE_URL=postgresql://user:password@localhost/".to_string() + &ProjectNames::new(&config.name).snake_case + "_db\nPOSTGRES_PASSWORD=your-postgres-password" - } else { "".to_string() } - ); - write_file(base_path.join(".env.example"), &env_template)?; - - Ok(()) - } - - fn generate_test_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - write_file(base_path.join("tests/__init__.py"), "")?; - - let test_main = r#"import pytest -from fastapi.testclient import TestClient -from app.main import app - -client = TestClient(app) - -def test_root(): - response = client.get("/") - assert response.status_code == 200 - assert response.json()["message"] == "{{project_name}} API is running" - -def test_health(): - response = client.get("/health") - assert response.status_code == 200 - assert response.json()["status"] == "healthy" -"#; - let test_content = replace_template_vars_string(test_main, &vars); - write_file(base_path.join("tests/test_main.py"), &test_content)?; - - let test_auth = r#"import pytest -from fastapi.testclient import TestClient -from app.main import app - -client = TestClient(app) - -def test_register_user(): - response = client.post( - "/api/v1/auth/register", - json={ - "email": "test@example.com", - "password": "testpassword123", - "full_name": "Test User" - } - ) - assert response.status_code == 200 - data = response.json() - assert "access_token" in data - assert "refresh_token" in data - assert data["token_type"] == "bearer" - -def test_login_user(): - # First register a user - client.post( - "/api/v1/auth/register", - json={ - "email": "login@example.com", - "password": "testpassword123", - "full_name": "Login User" - } - ) - - # Then login - response = client.post( - "/api/v1/auth/login", - json={ - "email": "login@example.com", - "password": "testpassword123" - } - ) - assert response.status_code == 200 - data = response.json() - assert "access_token" in data - assert "refresh_token" in data - -def test_invalid_login(): - response = client.post( - "/api/v1/auth/login", - json={ - "email": "nonexistent@example.com", - "password": "wrongpassword" - } - ) - assert response.status_code == 401 -"#; - write_file(base_path.join("tests/test_auth.py"), test_auth)?; - - Ok(()) - } - - fn generate_documentation(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let _vars = self.get_template_vars(config); - let names = ProjectNames::new(&config.name); - - let readme = format!(r#"# {project_name} - -Production-ready FastAPI application with authentication and security features. - -## Features - -- **FastAPI** - Modern, fast web framework -- **JWT Authentication** - Access & refresh tokens -- **Password Security** - bcrypt hashing -- **{database}** - Database integration -- **Docker** - Containerized deployment -- **Nginx** - Reverse proxy with security headers -- **Tests** - Comprehensive test suite -- **Security** - CORS, rate limiting, input validation - -## Quick Start - -### Development - -```bash -# Install dependencies -pip install -r requirements.txt - -# Set up environment -cp .env.example .env -# Edit .env with your configuration - -# Run the application -uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -``` - -### With Docker - -```bash -# Build and run with Docker Compose -docker-compose up --build - -# The API will be available at http://localhost -``` - -## API Documentation - -Once running, visit: -- **Swagger UI**: http://localhost:8000/docs -- **ReDoc**: http://localhost:8000/redoc - -## API Endpoints - -### Authentication -- `POST /api/v1/auth/register` - Register new user -- `POST /api/v1/auth/login` - User login -- `POST /api/v1/auth/refresh` - Refresh access token - -### Users -- `GET /api/v1/users/me` - Get current user info - -### System -- `GET /health` - Health check -- `GET /` - API info - -## Testing - -```bash -# Run tests -pytest - -# Run tests with coverage -pytest --cov=app - -# Run specific test -pytest tests/test_auth.py::test_register_user -``` - -## Project Structure - -``` -{snake_case}/ -├── app/ -│ ├── api/ # API routes -│ ├── core/ # Core functionality -│ ├── database/ # Database connection -│ ├── models/ # Data models -│ ├── schemas/ # Pydantic schemas -│ ├── services/ # Business logic -│ └── main.py # FastAPI application -├── tests/ # Test suite -├── nginx/ # Nginx configuration -├── logs/ # Application logs -├── requirements.txt # Python dependencies -├── Dockerfile # Docker configuration -└── docker-compose.yml # Docker Compose setup -``` - -## Configuration - -Key environment variables: - -```env -ENVIRONMENT=development -SECRET_KEY=your-secret-key-here -{database_config} -REDIS_URL=redis://localhost:6379 -ALLOWED_HOSTS=["http://localhost:3000"] -``` - -## Security Features - -- JWT-based authentication with refresh tokens -- Password hashing with bcrypt -- CORS configuration -- Security headers via Nginx -- Rate limiting -- Input validation -- SQL injection prevention - -## Deployment - -1. Set `ENVIRONMENT=production` in your environment -2. Use strong `SECRET_KEY` -3. Configure your database connection -4. Set up SSL certificates for HTTPS -5. Configure firewall rules - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Add tests for new features -4. Run the test suite -5. Submit a pull request - -Generated with love by Athena CLI -"#, - project_name = config.name, - database = match config.database { - DatabaseType::MySQL => "MySQL", - DatabaseType::MongoDB => "MongoDB", - DatabaseType::PostgreSQL => "PostgreSQL", - }, - snake_case = names.snake_case, - database_config = match config.database { - DatabaseType::MySQL => format!("DATABASE_URL=mysql+pymysql://root:password@localhost/{}_db", names.snake_case), - DatabaseType::MongoDB => format!("MONGODB_URL=mongodb://localhost:27017\nDATABASE_NAME={}_db", names.snake_case), - DatabaseType::PostgreSQL => format!("DATABASE_URL=postgresql://user:password@localhost/{}_db", names.snake_case), - } - ); - - write_file(base_path.join("README.md"), &readme)?; - - Ok(()) - } -} - -impl BoilerplateGenerator for FastAPIGenerator { - fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - crate::boilerplate::validate_project_name(&config.name)?; - crate::boilerplate::check_directory_availability(Path::new(&config.directory))?; - Ok(()) - } - - fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - - println!("Generating FastAPI project: {}", config.name); - - // Create directory structure - println!(" Creating directory structure..."); - self.create_fastapi_structure(base_path)?; - - // Generate core files - println!(" Generating core application files..."); - self.generate_core_files(config, base_path)?; - - // Generate API files - println!(" Generating API endpoints..."); - self.generate_api_files(config, base_path)?; - - // Generate database files - println!(" 💾 Setting up database integration..."); - self.generate_database_files(config, base_path)?; - - // Generate models and services - println!(" 📊 Creating models and services..."); - self.generate_models_and_services(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path)?; - - println!("FastAPI project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" pip install -r requirements.txt"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" uvicorn app.main:app --reload"); - } - - Ok(()) - } -} \ No newline at end of file diff --git a/src/boilerplate/flask.rs b/src/boilerplate/flask.rs deleted file mode 100644 index c750a87..0000000 --- a/src/boilerplate/flask.rs +++ /dev/null @@ -1,550 +0,0 @@ -//! Flask boilerplate generator with production-ready features - -use crate::boilerplate::{BoilerplateGenerator, BoilerplateResult, ProjectConfig, DatabaseType}; -use crate::boilerplate::utils::{create_directory_structure, write_file, replace_template_vars_string, generate_secret_key, ProjectNames}; -use crate::boilerplate::templates::flask::*; -use std::path::Path; - -pub struct FlaskGenerator; - -impl Default for FlaskGenerator { - fn default() -> Self { - Self::new() - } -} - -impl FlaskGenerator { - pub fn new() -> Self { - Self - } - - fn get_template_vars(&self, config: &ProjectConfig) -> Vec<(&str, String)> { - let names = ProjectNames::new(&config.name); - let secret_key = generate_secret_key(); - - vec![ - ("project_name", config.name.clone()), - ("snake_case", names.snake_case.clone()), - ("kebab_case", names.kebab_case.clone()), - ("pascal_case", names.pascal_case), - ("upper_case", names.upper_case), - ("secret_key", secret_key), - ("module_name", names.kebab_case), - ] - } - - fn create_flask_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - "app", - "app/api", - "app/api/v1", - "app/core", - "app/database", - "app/models", - "app/schemas", - "app/services", - "app/utils", - "tests", - "tests/api", - "migrations", - "logs", - "nginx", - "nginx/conf.d", - "scripts", - "instance", - ]; - - create_directory_structure(base_path, &directories) - } - - fn generate_core_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Main application factory - let app_init = replace_template_vars_string(APP_INIT_PY, &vars); - write_file(base_path.join("app/__init__.py"), &app_init)?; - - // Configuration - let config_content = replace_template_vars_string(CONFIG_PY, &vars); - write_file(base_path.join("app/core/config.py"), &config_content)?; - write_file(base_path.join("app/core/__init__.py"), "")?; - - // Structured logging module (new in 2025) - let logging_content = replace_template_vars_string(crate::boilerplate::templates::flask::FLASK_LOGGING_PY, &vars); - write_file(base_path.join("app/core/logging.py"), &logging_content)?; - - // Extensions (Flask extensions initialization) - let extensions_content = replace_template_vars_string(EXTENSIONS_PY, &vars); - write_file(base_path.join("app/core/extensions.py"), &extensions_content)?; - - // Security module - let security_content = replace_template_vars_string(SECURITY_PY, &vars); - write_file(base_path.join("app/core/security.py"), &security_content)?; - - // Main application entry point - let main_content = replace_template_vars_string(MAIN_PY, &vars); - write_file(base_path.join("run.py"), &main_content)?; - - Ok(()) - } - - fn generate_api_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // API init files - write_file(base_path.join("app/api/__init__.py"), "")?; - write_file(base_path.join("app/api/v1/__init__.py"), "")?; - - // Health endpoint - let health_content = replace_template_vars_string(HEALTH_BP, &vars); - write_file(base_path.join("app/api/health.py"), &health_content)?; - - // Auth endpoints - let auth_content = replace_template_vars_string(AUTH_BP, &vars); - write_file(base_path.join("app/api/v1/auth.py"), &auth_content)?; - - // Users endpoints - let users_content = replace_template_vars_string(USERS_BP, &vars); - write_file(base_path.join("app/api/v1/users.py"), &users_content)?; - - Ok(()) - } - - fn generate_database_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - write_file(base_path.join("app/database/__init__.py"), "")?; - - // Choose database connection based on config - match config.database { - DatabaseType::MySQL => { - let mysql_connection = replace_template_vars_string(MYSQL_CONNECTION, &vars); - write_file(base_path.join("app/database/connection.py"), &mysql_connection)?; - } - DatabaseType::PostgreSQL => { - let postgres_connection = replace_template_vars_string(POSTGRES_CONNECTION, &vars); - write_file(base_path.join("app/database/connection.py"), &postgres_connection)?; - } - _ => { - return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )); - } - } - - Ok(()) - } - - fn generate_models_and_services(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Models - write_file(base_path.join("app/models/__init__.py"), "")?; - - // User model - choose based on database type - let user_model = match config.database { - DatabaseType::MySQL => replace_template_vars_string(USER_MODEL_MYSQL, &vars), - DatabaseType::PostgreSQL => replace_template_vars_string(USER_MODEL, &vars), - _ => return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )), - }; - write_file(base_path.join("app/models/user.py"), &user_model)?; - - // Schemas (Marshmallow for serialization) - write_file(base_path.join("app/schemas/__init__.py"), "")?; - let user_schema = replace_template_vars_string(USER_SCHEMA, &vars); - write_file(base_path.join("app/schemas/user.py"), &user_schema)?; - - // Services - write_file(base_path.join("app/services/__init__.py"), "")?; - let user_service = replace_template_vars_string(USER_SERVICE, &vars); - write_file(base_path.join("app/services/user_service.py"), &user_service)?; - - // Utils - write_file(base_path.join("app/utils/__init__.py"), "")?; - let decorators = replace_template_vars_string(DECORATORS, &vars); - write_file(base_path.join("app/utils/decorators.py"), &decorators)?; - - Ok(()) - } - - fn generate_docker_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - if !config.include_docker { - return Ok(()); - } - - let vars = self.get_template_vars(config); - - // Requirements - choose based on database type - let requirements = match config.database { - DatabaseType::MySQL => REQUIREMENTS_TXT_MYSQL, - DatabaseType::PostgreSQL => REQUIREMENTS_TXT, - _ => return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )), - }; - write_file(base_path.join("requirements.txt"), requirements)?; - - // Dockerfile - choose based on database type - let dockerfile_template = match config.database { - DatabaseType::MySQL => DOCKERFILE_MYSQL, - DatabaseType::PostgreSQL => DOCKERFILE, - _ => return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )), - }; - let dockerfile_content = replace_template_vars_string(dockerfile_template, &vars); - write_file(base_path.join("Dockerfile"), &dockerfile_content)?; - - // Docker Compose - choose based on database type - let compose_template = match config.database { - DatabaseType::MySQL => DOCKER_COMPOSE_YML_MYSQL, - DatabaseType::PostgreSQL => DOCKER_COMPOSE_YML, - _ => return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )), - }; - let compose_content = replace_template_vars_string(compose_template, &vars); - write_file(base_path.join("docker-compose.yml"), &compose_content)?; - - // Nginx configurations - let nginx_content = replace_template_vars_string(NGINX_CONF, &vars); - write_file(base_path.join("nginx/nginx.conf"), &nginx_content)?; - - let nginx_default_content = replace_template_vars_string(NGINX_DEFAULT_CONF, &vars); - write_file(base_path.join("nginx/conf.d/default.conf"), &nginx_default_content)?; - - // .env template - choose based on database type - let names = ProjectNames::new(&config.name); - let env_template = match config.database { - DatabaseType::MySQL => format!(r#"# Environment Configuration -FLASK_ENV=development -FLASK_DEBUG=1 - -# Security -SECRET_KEY=your-secret-key-here-change-in-production - -# Database -DATABASE_URL=mysql+pymysql://root:yourpassword@localhost/{}_db -MYSQL_ROOT_PASSWORD=your-mysql-root-password -MYSQL_PASSWORD=your-mysql-password - -# Redis -REDIS_URL=redis://localhost:6379 - -# JWT -JWT_SECRET_KEY=your-jwt-secret-key-here -JWT_ACCESS_TOKEN_EXPIRES=3600 -JWT_REFRESH_TOKEN_EXPIRES=2592000 - -# CORS -ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 -"#, names.snake_case), - DatabaseType::PostgreSQL => format!(r#"# Environment Configuration -FLASK_ENV=development -FLASK_DEBUG=1 - -# Security -SECRET_KEY=your-secret-key-here-change-in-production - -# Database -DATABASE_URL=postgresql://user:password@localhost/{}_db -POSTGRES_PASSWORD=your-postgres-password - -# Redis -REDIS_URL=redis://localhost:6379 - -# JWT -JWT_SECRET_KEY=your-jwt-secret-key-here -JWT_ACCESS_TOKEN_EXPIRES=3600 -JWT_REFRESH_TOKEN_EXPIRES=2592000 - -# CORS -ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 -"#, names.snake_case), - _ => return Err(crate::athena::AthenaError::validation_error_simple( - "Unsupported database type for Flask".to_string() - )), - }; - write_file(base_path.join(".env.example"), &env_template)?; - - Ok(()) - } - - fn generate_test_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - write_file(base_path.join("tests/__init__.py"), "")?; - - // Test configuration - let test_config = replace_template_vars_string(TEST_CONFIG, &vars); - write_file(base_path.join("tests/conftest.py"), &test_config)?; - - // Main tests - let test_main = replace_template_vars_string(TEST_MAIN, &vars); - write_file(base_path.join("tests/test_main.py"), &test_main)?; - - // Auth tests - let test_auth = replace_template_vars_string(TEST_AUTH, &vars); - write_file(base_path.join("tests/test_auth.py"), &test_auth)?; - - Ok(()) - } - - fn generate_documentation(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let names = ProjectNames::new(&config.name); - - let database_name = match config.database { - DatabaseType::MySQL => "MySQL", - DatabaseType::PostgreSQL => "PostgreSQL", - _ => "Database", - }; - - let database_config = match config.database { - DatabaseType::MySQL => format!("DATABASE_URL=mysql+pymysql://root:password@localhost/{}_db\nMYSQL_ROOT_PASSWORD=your-mysql-password", names.snake_case), - DatabaseType::PostgreSQL => format!("DATABASE_URL=postgresql://user:password@localhost/{}_db\nPOSTGRES_PASSWORD=your-postgres-password", names.snake_case), - _ => "DATABASE_URL=your-database-url".to_string(), - }; - - let readme = format!(r#"# {project_name} - -Production-ready Flask application with authentication, {database_name}, and Nginx. - -## Features - -- **Flask** - Micro web framework with flexibility -- **JWT Authentication** - Access & refresh tokens with Flask-JWT-Extended -- **Password Security** - Werkzeug password hashing -- **{database_name}** - Robust relational database with SQLAlchemy ORM -- **Docker** - Containerized deployment with multi-stage builds -- **Nginx** - Reverse proxy with security headers -- **Tests** - Comprehensive test suite with pytest -- **Security** - CORS, rate limiting, input validation -- **Migrations** - Database migrations with Flask-Migrate - -## Quick Start - -### Development - -```bash -# Install dependencies -pip install -r requirements.txt - -# Set up environment -cp .env.example .env -# Edit .env with your configuration - -# Initialize database -flask db init -flask db migrate -m "Initial migration" -flask db upgrade - -# Run the application -python run.py -``` - -### With Docker - -```bash -# Build and run with Docker Compose -docker-compose up --build - -# The API will be available at http://localhost -``` - -## API Documentation - -Once running, the API will be available at: -- **Base URL**: http://localhost:5000 -- **Health Check**: http://localhost:5000/health - -## API Endpoints - -### Authentication -- `POST /api/v1/auth/register` - Register new user -- `POST /api/v1/auth/login` - User login -- `POST /api/v1/auth/refresh` - Refresh access token - -### Users -- `GET /api/v1/users/me` - Get current user info - -### System -- `GET /health` - Health check -- `GET /` - API info - -## Testing - -```bash -# Run tests -pytest - -# Run tests with coverage -pytest --cov=app - -# Run specific test -pytest tests/test_auth.py::test_register_user -``` - -## Project Structure - -``` -{snake_case}/ -├── app/ -│ ├── api/ # API routes -│ │ └── v1/ # API version 1 -│ ├── core/ # Core functionality -│ ├── database/ # Database connection -│ ├── models/ # SQLAlchemy models -│ ├── schemas/ # Marshmallow schemas -│ ├── services/ # Business logic -│ ├── utils/ # Utilities and decorators -│ └── __init__.py # Flask application factory -├── tests/ # Test suite -├── migrations/ # Database migrations -├── nginx/ # Nginx configuration -├── logs/ # Application logs -├── instance/ # Instance-specific files -├── requirements.txt # Python dependencies -├── Dockerfile # Docker configuration -├── docker-compose.yml # Docker Compose setup -└── run.py # Application entry point -``` - -## Configuration - -Key environment variables: - -```env -FLASK_ENV=development -FLASK_DEBUG=1 -SECRET_KEY=your-secret-key-here -{database_config} -REDIS_URL=redis://localhost:6379 -JWT_SECRET_KEY=your-jwt-secret-key-here -ALLOWED_ORIGINS=http://localhost:3000 -``` - -## Security Features - -- JWT-based authentication with refresh tokens -- Password hashing with Werkzeug -- CORS configuration -- Security headers via Nginx -- Rate limiting with Flask-Limiter -- Input validation with Marshmallow -- SQL injection prevention with SQLAlchemy ORM - -## Database Migrations - -```bash -# Create a new migration -flask db migrate -m "Description of changes" - -# Apply migrations -flask db upgrade - -# Downgrade migrations -flask db downgrade -``` - -## Deployment - -1. Set `FLASK_ENV=production` and `FLASK_DEBUG=0` -2. Use a strong `SECRET_KEY` and `JWT_SECRET_KEY` -3. Configure your PostgreSQL database connection -4. Set up SSL certificates for HTTPS -5. Configure firewall rules -6. Use a WSGI server like Gunicorn in production - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Add tests for new features -4. Run the test suite -5. Submit a pull request - -Generated with love by Athena CLI -"#, - project_name = config.name, - snake_case = names.snake_case - ); - - write_file(base_path.join("README.md"), &readme)?; - - Ok(()) - } -} - -impl BoilerplateGenerator for FlaskGenerator { - fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - crate::boilerplate::validate_project_name(&config.name)?; - crate::boilerplate::check_directory_availability(Path::new(&config.directory))?; - Ok(()) - } - - fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - - println!("Generating Flask project: {}", config.name); - - // Create directory structure - println!(" Creating directory structure..."); - self.create_flask_structure(base_path)?; - - // Generate core files - println!(" Generating core application files..."); - self.generate_core_files(config, base_path)?; - - // Generate API files - println!(" Generating API endpoints..."); - self.generate_api_files(config, base_path)?; - - // Generate database files - let db_name = match config.database { - DatabaseType::MySQL => "MySQL", - DatabaseType::PostgreSQL => "PostgreSQL", - _ => "database", - }; - println!(" 💾 Setting up {} integration...", db_name); - self.generate_database_files(config, base_path)?; - - // Generate models and services - println!(" 📊 Creating models and services..."); - self.generate_models_and_services(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path)?; - - println!("Flask project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" pip install -r requirements.txt"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" flask db init && flask db migrate -m 'Initial migration' && flask db upgrade"); - println!(" python run.py"); - } - - Ok(()) - } -} \ No newline at end of file diff --git a/src/boilerplate/go.rs b/src/boilerplate/go.rs deleted file mode 100644 index 9cc7ca2..0000000 --- a/src/boilerplate/go.rs +++ /dev/null @@ -1,1708 +0,0 @@ -//! Go boilerplate generator with production-ready features - -use crate::boilerplate::{BoilerplateGenerator, BoilerplateResult, ProjectConfig, DatabaseType}; -use crate::boilerplate::utils::{create_directory_structure, write_file, replace_template_vars_string, generate_secret_key, ProjectNames}; -use crate::boilerplate::templates::go::*; -use std::path::Path; - -pub struct GoGenerator; - -impl Default for GoGenerator { - fn default() -> Self { - Self::new() - } -} - -impl GoGenerator { - pub fn new() -> Self { - Self - } - - fn get_template_vars(&self, config: &ProjectConfig) -> Vec<(&str, String)> { - let names = ProjectNames::new(&config.name); - let secret_key = generate_secret_key(); - - vec![ - ("project_name", config.name.clone()), - ("snake_case", names.snake_case.clone()), - ("kebab_case", names.kebab_case.clone()), - ("pascal_case", names.pascal_case), - ("upper_case", names.upper_case), - ("secret_key", secret_key), - ("module_name", format!("github.com/yourusername/{}", names.kebab_case)), - ] - } - - fn create_go_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - "cmd", - "internal/config", - "internal/database", - "internal/handlers", - "internal/middleware", - "internal/models", - "internal/routes", - "internal/services", - "internal/utils", - "pkg/auth", - "pkg/errors", - "pkg/logger", - "tests", - "tests/integration", - "tests/unit", - "deployments", - "scripts", - "docs", - ]; - - create_directory_structure(base_path, &directories) - } - - fn generate_main_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Main application - let main_content = replace_template_vars_string(MAIN_GO, &vars); - write_file(base_path.join("cmd/main.go"), &main_content)?; - - // Go module - let go_mod_content = replace_template_vars_string(GO_MOD, &vars); - write_file(base_path.join("go.mod"), &go_mod_content)?; - - Ok(()) - } - - fn generate_config_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let config_go = r#"package config - -import ( - "os" - "strconv" - "strings" -) - -type Config struct { - // Application - ProjectName string - Version string - Environment string - Port string - - // Security - JWTSecret string - AccessTokenExpireMinutes int - RefreshTokenExpireDays int - - // Database{{#if mongodb}} - MongoURL string - DatabaseName string{{/if}}{{#if postgresql}} - DatabaseURL string{{/if}} - - // Redis - RedisURL string - - // CORS - AllowedOrigins []string - - // Rate Limiting - RateLimitRPS int -} - -func Load() *Config { - return &Config{ - ProjectName: getEnv("PROJECT_NAME", "{{project_name}}"), - Version: getEnv("VERSION", "1.0.0"), - Environment: getEnv("ENVIRONMENT", "development"), - Port: getEnv("PORT", "8080"), - - JWTSecret: getEnv("JWT_SECRET", "{{secret_key}}"), - AccessTokenExpireMinutes: getEnvAsInt("ACCESS_TOKEN_EXPIRE_MINUTES", 30), - RefreshTokenExpireDays: getEnvAsInt("REFRESH_TOKEN_EXPIRE_DAYS", 7), -{{#if mongodb}} - MongoURL: getEnv("MONGO_URL", "mongodb://localhost:27017"), - DatabaseName: getEnv("DATABASE_NAME", "{{snake_case}}_db"),{{/if}}{{#if postgresql}} - DatabaseURL: getEnv("DATABASE_URL", "postgres://user:password@localhost/{{snake_case}}_db?sslmode=disable"),{{/if}} - - RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), - - AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"), ","), - RateLimitRPS: getEnvAsInt("RATE_LIMIT_RPS", 10), - } -} - -func getEnv(key, fallback string) string { - if value := os.Getenv(key); value != "" { - return value - } - return fallback -} - -func getEnvAsInt(key string, fallback int) int { - if value := os.Getenv(key); value != "" { - if intValue, err := strconv.Atoi(value); err == nil { - return intValue - } - } - return fallback -} -"#; - - let mut config_content = config_go.to_string(); - match config.database { - DatabaseType::MongoDB => { - config_content = config_content.replace("{{#if mongodb}}", ""); - config_content = config_content.replace("{{/if}}", ""); - config_content = config_content.replace("{{#if postgresql}}", ""); - config_content = config_content.replace("{{/if}}", ""); - } - DatabaseType::PostgreSQL => { - config_content = config_content.replace("{{#if postgresql}}", ""); - config_content = config_content.replace("{{/if}}", ""); - config_content = config_content.replace("{{#if mongodb}}", ""); - config_content = config_content.replace("{{/if}}", ""); - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for Go projects. Use Flask for MySQL support.".to_string() - )); - } - } - config_content = replace_template_vars_string(&config_content, &vars); - write_file(base_path.join("internal/config/config.go"), &config_content)?; - - Ok(()) - } - - fn generate_database_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - match config.database { - DatabaseType::MongoDB => { - let mongodb_go = r#"package database - -import ( - "context" - "log" - "time" - - "{{module_name}}/internal/config" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -type Database struct { - Client *mongo.Client - Database *mongo.Database -} - -func Connect(cfg *config.Config) (*Database, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // Connect to MongoDB - client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.MongoURL)) - if err != nil { - return nil, err - } - - // Ping the database - if err = client.Ping(ctx, nil); err != nil { - return nil, err - } - - database := client.Database(cfg.DatabaseName) - - log.Println("Successfully connected to MongoDB") - - return &Database{ - Client: client, - Database: database, - }, nil -} - -func Close(db *Database) { - if db.Client != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := db.Client.Disconnect(ctx); err != nil { - log.Printf("Error disconnecting from MongoDB: %v", err) - } else { - log.Println("Disconnected from MongoDB") - } - } -} - -func (db *Database) GetCollection(name string) *mongo.Collection { - return db.Database.Collection(name) -} -"#; - let mongodb_content = replace_template_vars_string(mongodb_go, &vars); - write_file(base_path.join("internal/database/mongodb.go"), &mongodb_content)?; - } - DatabaseType::PostgreSQL => { - let postgres_go = r#"package database - -import ( - "database/sql" - "log" - - "{{module_name}}/internal/config" - _ "github.com/lib/pq" - "github.com/jmoiron/sqlx" -) - -type Database struct { - DB *sqlx.DB -} - -func Connect(cfg *config.Config) (*Database, error) { - // Connect to PostgreSQL - db, err := sqlx.Connect("postgres", cfg.DatabaseURL) - if err != nil { - return nil, err - } - - // Test the connection - if err = db.Ping(); err != nil { - return nil, err - } - - // Set connection pool settings - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(5) - - log.Println("Successfully connected to PostgreSQL") - - // Run migrations - if err = runMigrations(db); err != nil { - log.Printf("Warning: Migration failed: %v", err) - } - - return &Database{DB: db}, nil -} - -func Close(db *Database) { - if db.DB != nil { - if err := db.DB.Close(); err != nil { - log.Printf("Error closing database: %v", err) - } else { - log.Println("Database connection closed") - } - } -} - -func runMigrations(db *sqlx.DB) error { - // Create users table - query := ` - CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email VARCHAR(255) UNIQUE NOT NULL, - hashed_password VARCHAR(255) NOT NULL, - full_name VARCHAR(255) NOT NULL, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); - ` - - _, err := db.Exec(query) - if err != nil { - return err - } - - log.Println("Database migrations completed successfully") - return nil -} -"#; - let postgres_content = replace_template_vars_string(postgres_go, &vars); - write_file(base_path.join("internal/database/postgres.go"), &postgres_content)?; - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for Go projects. Use Flask for MySQL support.".to_string() - )); - } - } - - Ok(()) - } - - fn generate_logger_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let logger_content = replace_template_vars_string(crate::boilerplate::templates::go::GO_LOGGER, &vars); - write_file(base_path.join("pkg/logger/logger.go"), &logger_content)?; - - Ok(()) - } - - fn generate_auth_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let auth_go = r#"package auth - -import ( - "errors" - "time" - - "github.com/golang-jwt/jwt/v5" - "golang.org/x/crypto/bcrypt" -) - -type Claims struct { - UserID string `json:"user_id"` - Email string `json:"email"` - Type string `json:"type"` // "access" or "refresh" - jwt.RegisteredClaims -} - -type TokenPair struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` -} - -func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(bytes), err -} - -func CheckPassword(hashedPassword, password string) error { - return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) -} - -func GenerateTokenPair(userID, email, jwtSecret string, accessExpireMinutes, refreshExpireDays int) (*TokenPair, error) { - // Generate access token - accessToken, err := generateToken(userID, email, "access", jwtSecret, time.Duration(accessExpireMinutes)*time.Minute) - if err != nil { - return nil, err - } - - // Generate refresh token - refreshToken, err := generateToken(userID, email, "refresh", jwtSecret, time.Duration(refreshExpireDays)*24*time.Hour) - if err != nil { - return nil, err - } - - return &TokenPair{ - AccessToken: accessToken, - RefreshToken: refreshToken, - TokenType: "bearer", - }, nil -} - -func generateToken(userID, email, tokenType, jwtSecret string, expiration time.Duration) (string, error) { - claims := Claims{ - UserID: userID, - Email: email, - Type: tokenType, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)), - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(jwtSecret)) -} - -func VerifyToken(tokenString, jwtSecret, expectedType string) (*Claims, error) { - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(jwtSecret), nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*Claims); ok && token.Valid { - if claims.Type != expectedType { - return nil, errors.New("invalid token type") - } - return claims, nil - } - - return nil, errors.New("invalid token") -} -"#; - - let auth_content = replace_template_vars_string(auth_go, &vars); - write_file(base_path.join("pkg/auth/auth.go"), &auth_content)?; - - Ok(()) - } - - fn generate_models_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let _vars = self.get_template_vars(config); - - let models_go = match config.database { - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for Go projects. Use Flask for MySQL support.".to_string() - )); - } - DatabaseType::MongoDB => r#"package models - -import ( - "time" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -type User struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` - Email string `bson:"email" json:"email"` - HashedPassword string `bson:"hashed_password" json:"-"` - FullName string `bson:"full_name" json:"full_name"` - IsActive bool `bson:"is_active" json:"is_active"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` -} - -type LoginRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` -} - -type RegisterRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - FullName string `json:"full_name" binding:"required"` -} - -type UserResponse struct { - ID string `json:"id"` - Email string `json:"email"` - FullName string `json:"full_name"` - IsActive bool `json:"is_active"` - CreatedAt time.Time `json:"created_at"` -} - -func (u *User) ToResponse() *UserResponse { - return &UserResponse{ - ID: u.ID.Hex(), - Email: u.Email, - FullName: u.FullName, - IsActive: u.IsActive, - CreatedAt: u.CreatedAt, - } -} -"#.to_string(), - DatabaseType::PostgreSQL => r#"package models - -import ( - "time" - "github.com/google/uuid" -) - -type User struct { - ID uuid.UUID `db:"id" json:"id"` - Email string `db:"email" json:"email"` - HashedPassword string `db:"hashed_password" json:"-"` - FullName string `db:"full_name" json:"full_name"` - IsActive bool `db:"is_active" json:"is_active"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` -} - -type LoginRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` -} - -type RegisterRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - FullName string `json:"full_name" binding:"required"` -} - -type UserResponse struct { - ID string `json:"id"` - Email string `json:"email"` - FullName string `json:"full_name"` - IsActive bool `json:"is_active"` - CreatedAt time.Time `json:"created_at"` -} - -func (u *User) ToResponse() *UserResponse { - return &UserResponse{ - ID: u.ID.String(), - Email: u.Email, - FullName: u.FullName, - IsActive: u.IsActive, - CreatedAt: u.CreatedAt, - } -} -"#.to_string() - }; - - write_file(base_path.join("internal/models/user.go"), &models_go)?; - - Ok(()) - } - - fn generate_middleware_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let cors_go = r#"package middleware - -import ( - "{{module_name}}/internal/config" - "github.com/gin-gonic/gin" -) - -func CORS() gin.HandlerFunc { - return gin.HandlerFunc(func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") - c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - }) -} -"#; - - let security_go = r#"package middleware - -import ( - "github.com/gin-gonic/gin" -) - -func Security() gin.HandlerFunc { - return gin.HandlerFunc(func(c *gin.Context) { - // Security headers - c.Writer.Header().Set("X-Frame-Options", "DENY") - c.Writer.Header().Set("X-Content-Type-Options", "nosniff") - c.Writer.Header().Set("X-XSS-Protection", "1; mode=block") - c.Writer.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'") - - c.Next() - }) -} -"#; - - let auth_middleware_go = r#"package middleware - -import ( - "net/http" - "strings" - - "{{module_name}}/internal/config" - "{{module_name}}/pkg/auth" - "github.com/gin-gonic/gin" -) - -func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { - return gin.HandlerFunc(func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) - c.Abort() - return - } - - // Check Bearer token format - bearerToken := strings.Split(authHeader, " ") - if len(bearerToken) != 2 || bearerToken[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"}) - c.Abort() - return - } - - // Verify token - claims, err := auth.VerifyToken(bearerToken[1], cfg.JWTSecret, "access") - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - c.Abort() - return - } - - // Set user info in context - c.Set("user_id", claims.UserID) - c.Set("user_email", claims.Email) - c.Next() - }) -} -"#; - - let cors_content = replace_template_vars_string(cors_go, &vars); - let auth_middleware_content = replace_template_vars_string(auth_middleware_go, &vars); - - write_file(base_path.join("internal/middleware/cors.go"), &cors_content)?; - write_file(base_path.join("internal/middleware/security.go"), security_go)?; - write_file(base_path.join("internal/middleware/auth.go"), &auth_middleware_content)?; - - Ok(()) - } - - fn generate_handlers_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let handlers_go = r#"package handlers - -import ( - "{{module_name}}/internal/config" - "{{module_name}}/internal/database" - "{{module_name}}/internal/services" -) - -type Handlers struct { - Auth *AuthHandler - User *UserHandler - Health *HealthHandler -} - -func New(db *database.Database, cfg *config.Config) *Handlers { - userService := services.NewUserService(db) - - return &Handlers{ - Auth: NewAuthHandler(userService, cfg), - User: NewUserHandler(userService), - Health: NewHealthHandler(), - } -} -"#; - - let auth_handler_go = r#"package handlers - -import ( - "net/http" - - "{{module_name}}/internal/config" - "{{module_name}}/internal/models" - "{{module_name}}/internal/services" - "{{module_name}}/pkg/auth" - "github.com/gin-gonic/gin" -) - -type AuthHandler struct { - userService *services.UserService - config *config.Config -} - -func NewAuthHandler(userService *services.UserService, cfg *config.Config) *AuthHandler { - return &AuthHandler{ - userService: userService, - config: cfg, - } -} - -func (h *AuthHandler) Register(c *gin.Context) { - var req models.RegisterRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Check if user already exists - existingUser, _ := h.userService.GetByEmail(req.Email) - if existingUser != nil { - c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) - return - } - - // Hash password - hashedPassword, err := auth.HashPassword(req.Password) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process password"}) - return - } - - // Create user - user, err := h.userService.Create(&models.User{ - Email: req.Email, - HashedPassword: hashedPassword, - FullName: req.FullName, - IsActive: true, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) - return - } - - // Generate tokens - tokens, err := auth.GenerateTokenPair( - user.ID.String(), - user.Email, - h.config.JWTSecret, - h.config.AccessTokenExpireMinutes, - h.config.RefreshTokenExpireDays, - ) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) - return - } - - c.JSON(http.StatusOK, tokens) -} - -func (h *AuthHandler) Login(c *gin.Context) { - var req models.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Find user - user, err := h.userService.GetByEmail(req.Email) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) - return - } - - // Check password - if err := auth.CheckPassword(user.HashedPassword, req.Password); err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) - return - } - - // Generate tokens - tokens, err := auth.GenerateTokenPair( - user.ID.String(), - user.Email, - h.config.JWTSecret, - h.config.AccessTokenExpireMinutes, - h.config.RefreshTokenExpireDays, - ) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) - return - } - - c.JSON(http.StatusOK, tokens) -} - -type RefreshRequest struct { - RefreshToken string `json:"refresh_token" binding:"required"` -} - -func (h *AuthHandler) RefreshToken(c *gin.Context) { - var req RefreshRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Verify refresh token - claims, err := auth.VerifyToken(req.RefreshToken, h.config.JWTSecret, "refresh") - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"}) - return - } - - // Get user - user, err := h.userService.GetByID(claims.UserID) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) - return - } - - // Generate new tokens - tokens, err := auth.GenerateTokenPair( - user.ID.String(), - user.Email, - h.config.JWTSecret, - h.config.AccessTokenExpireMinutes, - h.config.RefreshTokenExpireDays, - ) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) - return - } - - c.JSON(http.StatusOK, tokens) -} -"#; - - let user_handler_go = r#"package handlers - -import ( - "net/http" - - "{{module_name}}/internal/services" - "github.com/gin-gonic/gin" -) - -type UserHandler struct { - userService *services.UserService -} - -func NewUserHandler(userService *services.UserService) *UserHandler { - return &UserHandler{ - userService: userService, - } -} - -func (h *UserHandler) GetMe(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found"}) - return - } - - user, err := h.userService.GetByID(userID.(string)) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) - return - } - - c.JSON(http.StatusOK, user.ToResponse()) -} -"#; - - let health_handler_go = r#"package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -type HealthHandler struct{} - -func NewHealthHandler() *HealthHandler { - return &HealthHandler{} -} - -func (h *HealthHandler) Health(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "healthy", - "service": "{{project_name}} API", - "version": "1.0.0", - }) -} -"#; - - let handlers_content = replace_template_vars_string(handlers_go, &vars); - let auth_handler_content = replace_template_vars_string(auth_handler_go, &vars); - let user_handler_content = replace_template_vars_string(user_handler_go, &vars); - let health_handler_content = replace_template_vars_string(health_handler_go, &vars); - - write_file(base_path.join("internal/handlers/handlers.go"), &handlers_content)?; - write_file(base_path.join("internal/handlers/auth.go"), &auth_handler_content)?; - write_file(base_path.join("internal/handlers/user.go"), &user_handler_content)?; - write_file(base_path.join("internal/handlers/health.go"), &health_handler_content)?; - - Ok(()) - } - - fn generate_services_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let user_service_go = match config.database { - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for Go projects. Use Flask for MySQL support.".to_string() - )); - } - DatabaseType::MongoDB => r#"package services - -import ( - "context" - "time" - - "{{module_name}}/internal/database" - "{{module_name}}/internal/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" -) - -type UserService struct { - collection *mongo.Collection -} - -func NewUserService(db *database.Database) *UserService { - return &UserService{ - collection: db.GetCollection("users"), - } -} - -func (s *UserService) Create(user *models.User) (*models.User, error) { - user.ID = primitive.NewObjectID() - user.CreatedAt = time.Now() - user.UpdatedAt = time.Now() - - _, err := s.collection.InsertOne(context.Background(), user) - if err != nil { - return nil, err - } - - return user, nil -} - -func (s *UserService) GetByID(id string) (*models.User, error) { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return nil, err - } - - var user models.User - err = s.collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&user) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *UserService) GetByEmail(email string) (*models.User, error) { - var user models.User - err := s.collection.FindOne(context.Background(), bson.M{"email": email}).Decode(&user) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *UserService) Update(id string, updateData bson.M) (*models.User, error) { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return nil, err - } - - updateData["updated_at"] = time.Now() - - _, err = s.collection.UpdateOne( - context.Background(), - bson.M{"_id": objectID}, - bson.M{"$set": updateData}, - ) - if err != nil { - return nil, err - } - - return s.GetByID(id) -} - -func (s *UserService) Delete(id string) error { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return err - } - - _, err = s.collection.DeleteOne(context.Background(), bson.M{"_id": objectID}) - return err -} -"#.to_string(), - DatabaseType::PostgreSQL => r#"package services - -import ( - "time" - - "{{module_name}}/internal/database" - "{{module_name}}/internal/models" - "github.com/google/uuid" -) - -type UserService struct { - db *database.Database -} - -func NewUserService(db *database.Database) *UserService { - return &UserService{db: db} -} - -func (s *UserService) Create(user *models.User) (*models.User, error) { - user.ID = uuid.New() - user.CreatedAt = time.Now() - user.UpdatedAt = time.Now() - - query := ` - INSERT INTO users (id, email, hashed_password, full_name, is_active, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING id, email, hashed_password, full_name, is_active, created_at, updated_at - ` - - err := s.db.DB.Get(user, query, - user.ID, user.Email, user.HashedPassword, user.FullName, - user.IsActive, user.CreatedAt, user.UpdatedAt) - - if err != nil { - return nil, err - } - - return user, nil -} - -func (s *UserService) GetByID(id string) (*models.User, error) { - userID, err := uuid.Parse(id) - if err != nil { - return nil, err - } - - var user models.User - query := "SELECT * FROM users WHERE id = $1" - err = s.db.DB.Get(&user, query, userID) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *UserService) GetByEmail(email string) (*models.User, error) { - var user models.User - query := "SELECT * FROM users WHERE email = $1" - err := s.db.DB.Get(&user, query, email) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *UserService) Update(id string, user *models.User) (*models.User, error) { - userID, err := uuid.Parse(id) - if err != nil { - return nil, err - } - - user.UpdatedAt = time.Now() - - query := ` - UPDATE users - SET email = $2, hashed_password = $3, full_name = $4, is_active = $5, updated_at = $6 - WHERE id = $1 - RETURNING id, email, hashed_password, full_name, is_active, created_at, updated_at - ` - - err = s.db.DB.Get(user, query, userID, user.Email, user.HashedPassword, - user.FullName, user.IsActive, user.UpdatedAt) - - if err != nil { - return nil, err - } - - return user, nil -} - -func (s *UserService) Delete(id string) error { - userID, err := uuid.Parse(id) - if err != nil { - return err - } - - query := "DELETE FROM users WHERE id = $1" - _, err = s.db.DB.Exec(query, userID) - return err -} -"#.to_string() - }; - - let user_service_content = replace_template_vars_string(&user_service_go, &vars); - write_file(base_path.join("internal/services/user.go"), &user_service_content)?; - - Ok(()) - } - - fn generate_routes_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let routes_go = r#"package routes - -import ( - "{{module_name}}/internal/config" - "{{module_name}}/internal/handlers" - "{{module_name}}/internal/middleware" - "github.com/gin-gonic/gin" -) - -func Setup(router *gin.Engine, h *handlers.Handlers) { - cfg := config.Load() - - // Health check - router.GET("/health", h.Health.Health) - - // Root endpoint - router.GET("/", func(c *gin.Context) { - c.JSON(200, gin.H{ - "message": "{{project_name}} API is running", - "version": "1.0.0", - }) - }) - - // API v1 routes - api := router.Group("/api/v1") - { - // Auth routes (no middleware) - auth := api.Group("/auth") - { - auth.POST("/register", h.Auth.Register) - auth.POST("/login", h.Auth.Login) - auth.POST("/refresh", h.Auth.RefreshToken) - } - - // Protected routes - protected := api.Group("/") - protected.Use(middleware.AuthMiddleware(cfg)) - { - // User routes - users := protected.Group("/users") - { - users.GET("/me", h.User.GetMe) - } - } - } -} -"#; - - let routes_content = replace_template_vars_string(routes_go, &vars); - write_file(base_path.join("internal/routes/routes.go"), &routes_content)?; - - Ok(()) - } - - fn generate_docker_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - if !config.include_docker { - return Ok(()); - } - - let vars = self.get_template_vars(config); - - // Dockerfile - let dockerfile_content = replace_template_vars_string(DOCKERFILE_GO, &vars); - write_file(base_path.join("Dockerfile"), &dockerfile_content)?; - - // Docker Compose - let docker_compose = r#"services: - {{kebab_case}}-api: - build: . - ports: - - "8080:8080" - environment: - - ENVIRONMENT=production{{#if mongodb}} - - MONGO_URL=mongodb://mongo:27017 - - DATABASE_NAME={{snake_case}}_db{{/if}}{{#if postgresql}} - - DATABASE_URL=postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/{{snake_case}}_db?sslmode=disable{{/if}} - - REDIS_URL=redis://redis:6379 - - JWT_SECRET=${JWT_SECRET} - depends_on:{{#if mongodb}} - - mongo{{/if}}{{#if postgresql}} - - postgres{{/if}} - - redis - restart: unless-stopped - networks: - - {{kebab_case}}-network -{{#if mongodb}} - mongo: - image: mongo:7 - environment: - - MONGO_INITDB_DATABASE={{snake_case}}_db - volumes: - - mongo_data:/data/db - restart: unless-stopped - networks: - - {{kebab_case}}-network -{{/if}}{{#if postgresql}} - postgres: - image: postgres:15 - environment: - - POSTGRES_DB={{snake_case}}_db - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - networks: - - {{kebab_case}}-network -{{/if}} - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - restart: unless-stopped - networks: - - {{kebab_case}}-network - -volumes:{{#if mongodb}} - mongo_data:{{/if}}{{#if postgresql}} - postgres_data:{{/if}} - redis_data: - -networks: - {{kebab_case}}-network: - driver: bridge -"#; - - let mut compose_content = docker_compose.to_string(); - match config.database { - DatabaseType::MongoDB => { - compose_content = compose_content.replace("{{#if mongodb}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - compose_content = compose_content.replace("{{#if postgresql}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - } - DatabaseType::PostgreSQL => { - compose_content = compose_content.replace("{{#if postgresql}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - compose_content = compose_content.replace("{{#if mongodb}}", ""); - compose_content = compose_content.replace("{{/if}}", ""); - } - DatabaseType::MySQL => { - return Err(crate::athena::AthenaError::validation_error_simple( - "MySQL is not supported for Go projects. Use Flask for MySQL support.".to_string() - )); - } - } - compose_content = replace_template_vars_string(&compose_content, &vars); - write_file(base_path.join("docker-compose.yml"), &compose_content)?; - - // .env template - let env_template = format!(r#"# Environment Configuration -ENVIRONMENT=development -PORT=8080 - -# Security -JWT_SECRET=your-jwt-secret-here-change-in-production -ACCESS_TOKEN_EXPIRE_MINUTES=30 -REFRESH_TOKEN_EXPIRE_DAYS=7 - -# Database{}{} - -# Redis -REDIS_URL=redis://localhost:6379 - -# CORS -ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 - -# Rate Limiting -RATE_LIMIT_RPS=10 -"#, - if matches!(config.database, DatabaseType::MongoDB) { - "\nMONGO_URL=mongodb://localhost:27017\nDATABASE_NAME=".to_string() + &ProjectNames::new(&config.name).snake_case + "_db" - } else { "".to_string() }, - if matches!(config.database, DatabaseType::PostgreSQL) { - "\nDATABASE_URL=postgres://user:password@localhost/".to_string() + &ProjectNames::new(&config.name).snake_case + "_db?sslmode=disable\nPOSTGRES_PASSWORD=your-postgres-password" - } else { "".to_string() } - ); - - write_file(base_path.join(".env.example"), &env_template)?; - - Ok(()) - } - - fn generate_test_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let test_main_go = r#"package main - -import ( - "os" - "testing" -) - -func TestMain(m *testing.M) { - // Setup test environment - os.Setenv("ENVIRONMENT", "test") - os.Setenv("JWT_SECRET", "test-secret-key") - - // Run tests - code := m.Run() - - // Cleanup - os.Exit(code) -} -"#; - - let test_auth_go = r#"package main - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "{{module_name}}/internal/config" - "{{module_name}}/internal/database" - "{{module_name}}/internal/handlers" - "{{module_name}}/internal/routes" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func setupRouter() *gin.Engine { - gin.SetMode(gin.TestMode) - - cfg := config.Load() - db, _ := database.Connect(cfg) - h := handlers.New(db, cfg) - - router := gin.New() - routes.Setup(router, h) - - return router -} - -func TestRegisterUser(t *testing.T) { - router := setupRouter() - - user := map[string]string{ - "email": "test@example.com", - "password": "testpassword123", - "full_name": "Test User", - } - - jsonData, _ := json.Marshal(user) - req, _ := http.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Contains(t, response, "access_token") - assert.Contains(t, response, "refresh_token") - assert.Equal(t, "bearer", response["token_type"]) -} - -func TestLoginUser(t *testing.T) { - router := setupRouter() - - // First register a user - registerUser := map[string]string{ - "email": "login@example.com", - "password": "testpassword123", - "full_name": "Login User", - } - - jsonData, _ := json.Marshal(registerUser) - req, _ := http.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - // Then login - loginUser := map[string]string{ - "email": "login@example.com", - "password": "testpassword123", - } - - jsonData, _ = json.Marshal(loginUser) - req, _ = http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - - w = httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Contains(t, response, "access_token") - assert.Contains(t, response, "refresh_token") -} - -func TestHealth(t *testing.T) { - router := setupRouter() - - req, _ := http.NewRequest("GET", "/health", nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Equal(t, "healthy", response["status"]) -} -"#; - - write_file(base_path.join("tests/main_test.go"), test_main_go)?; - - let test_auth_content = replace_template_vars_string(test_auth_go, &vars); - write_file(base_path.join("tests/auth_test.go"), &test_auth_content)?; - - Ok(()) - } - - fn generate_documentation(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let _vars = self.get_template_vars(config); - let names = ProjectNames::new(&config.name); - - let readme = format!(r#"# {project_name} - -Production-ready Go API with Gin framework, JWT authentication, and comprehensive security features. - -## Features - -- **Gin Framework** - High-performance HTTP web framework -- **JWT Authentication** - Access & refresh token system -- **Password Security** - bcrypt hashing -- **{database}** - Database integration with migrations -- **Middleware** - CORS, security headers, rate limiting -- **Docker** - Containerized deployment -- **Tests** - Comprehensive test suite -- **Graceful Shutdown** - Proper application lifecycle management - -## Quick Start - -### Development - -```bash -# Install dependencies -go mod download - -# Set up environment -cp .env.example .env -# Edit .env with your configuration - -# Run the application -go run cmd/main.go -``` - -### With Docker - -```bash -# Build and run with Docker Compose -docker-compose up --build - -# The API will be available at http://localhost:8080 -``` - -## API Documentation - -### Authentication -- `POST /api/v1/auth/register` - Register new user -- `POST /api/v1/auth/login` - User login -- `POST /api/v1/auth/refresh` - Refresh access token - -### Users (Protected) -- `GET /api/v1/users/me` - Get current user info - -### System -- `GET /health` - Health check -- `GET /` - API info - -## Testing - -```bash -# Run all tests -go test ./... - -# Run tests with verbose output -go test -v ./... - -# Run specific test -go test -v ./tests -run TestRegisterUser - -# Run tests with coverage -go test -cover ./... -``` - -## Project Structure - -``` -{snake_case}/ -├── cmd/ # Application entrypoints -├── internal/ # Private application code -│ ├── config/ # Configuration -│ ├── database/ # Database connection -│ ├── handlers/ # HTTP handlers -│ ├── middleware/ # HTTP middleware -│ ├── models/ # Data models -│ ├── routes/ # Route definitions -│ └── services/ # Business logic -├── pkg/ # Public library code -│ ├── auth/ # Authentication utilities -│ ├── errors/ # Error handling -│ └── logger/ # Logging utilities -├── tests/ # Test files -├── deployments/ # Deployment configurations -├── go.mod # Go modules -└── Dockerfile # Docker configuration -``` - -## Configuration - -Key environment variables: - -```env -ENVIRONMENT=development -PORT=8080 -JWT_SECRET=your-jwt-secret-here -ACCESS_TOKEN_EXPIRE_MINUTES=30 -REFRESH_TOKEN_EXPIRE_DAYS=7 -{database_config} -REDIS_URL=redis://localhost:6379 -ALLOWED_ORIGINS=http://localhost:3000 -``` - -## Security Features - -- JWT-based authentication with refresh tokens -- Password hashing with bcrypt -- Security headers middleware -- CORS configuration -- Rate limiting middleware -- Input validation -- SQL injection prevention -- Graceful error handling - -## Database - -This project uses {database} for data persistence. - -{database_instructions} - -## Deployment - -1. Set `ENVIRONMENT=production` -2. Use strong `JWT_SECRET` -3. Configure your database connection -4. Set up load balancing if needed -5. Configure monitoring and logging - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Add tests for new features -4. Run the test suite -5. Submit a pull request - -Generated with love by Athena CLI -"#, - project_name = config.name, - database = match config.database { - DatabaseType::MySQL => "MySQL", - DatabaseType::MongoDB => "MongoDB", - DatabaseType::PostgreSQL => "PostgreSQL", - }, - snake_case = names.snake_case, - database_config = match config.database { - DatabaseType::MySQL => format!("DATABASE_URL=mysql://user:password@localhost/{}_db", names.snake_case), - DatabaseType::MongoDB => format!("MONGO_URL=mongodb://localhost:27017\nDATABASE_NAME={}_db", names.snake_case), - DatabaseType::PostgreSQL => format!("DATABASE_URL=postgres://user:password@localhost/{}_db?sslmode=disable", names.snake_case), - }, - database_instructions = match config.database { - DatabaseType::MySQL => r#" -### MySQL Setup - -For MySQL support in Go projects, please use Flask with MySQL option instead: `athena init flask project --with-mysql` -"#, - DatabaseType::MongoDB => r#" -### MongoDB Setup - -The application automatically connects to MongoDB using the configured URL. Collections are created automatically when first accessed."#, - DatabaseType::PostgreSQL => r#" -### PostgreSQL Setup - -The application includes automatic database migrations that create the necessary tables on startup. Ensure your PostgreSQL instance is running and accessible."#, - } - ); - - write_file(base_path.join("README.md"), &readme)?; - - Ok(()) - } -} - -impl BoilerplateGenerator for GoGenerator { - fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - crate::boilerplate::validate_project_name(&config.name)?; - crate::boilerplate::check_directory_availability(Path::new(&config.directory))?; - Ok(()) - } - - fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - - println!("Generating Go project: {}", config.name); - - // Create directory structure - println!(" Creating directory structure..."); - self.create_go_structure(base_path)?; - - // Generate main files - println!(" Generating main application files..."); - self.generate_main_files(config, base_path)?; - - // Generate config files - println!(" 🔧 Setting up configuration..."); - self.generate_config_files(config, base_path)?; - - // Generate database files - println!(" 💾 Setting up database integration..."); - self.generate_database_files(config, base_path)?; - - // Generate logger module with slog (new in 2025) - println!(" 📋 Setting up structured logging..."); - self.generate_logger_files(config, base_path)?; - - // Generate auth files - println!(" 🔐 Creating authentication system..."); - self.generate_auth_files(config, base_path)?; - - // Generate models - println!(" 📊 Creating data models..."); - self.generate_models_files(config, base_path)?; - - // Generate middleware - println!(" Setting up middleware..."); - self.generate_middleware_files(config, base_path)?; - - // Generate handlers - println!(" Creating HTTP handlers..."); - self.generate_handlers_files(config, base_path)?; - - // Generate services - println!(" Setting up business logic services..."); - self.generate_services_files(config, base_path)?; - - // Generate routes - println!(" Configuring routes..."); - self.generate_routes_files(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path)?; - - println!("Go project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" go mod download"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" go run cmd/main.go"); - } - - Ok(()) - } -} \ No newline at end of file diff --git a/src/boilerplate/mod.rs b/src/boilerplate/mod.rs deleted file mode 100644 index 89aeac4..0000000 --- a/src/boilerplate/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Boilerplate generation module for FastAPI, Flask, Go, and PHP projects -//! -//! This module provides production-ready project templates with: -//! - Authentication systems (JWT with refresh tokens) -//! - Security best practices (bcrypt/argon2, AES-256) -//! - Database integration (MongoDB/PostgreSQL/MySQL) -//! - Docker containerization -//! - Nginx reverse proxy configuration -//! - Clean Architecture and DDD patterns - -pub mod fastapi; -pub mod flask; -pub mod go; -pub mod php; -pub mod templates; -pub mod utils; - -use crate::athena::AthenaError; -use std::path::Path; - -pub type BoilerplateResult = Result; - -#[derive(Debug, Clone)] -pub enum DatabaseType { - MongoDB, - PostgreSQL, - MySQL, -} - -#[derive(Debug, Clone)] -pub enum GoFramework { - Gin, - Echo, - Fiber, -} - -#[derive(Debug, Clone)] -pub struct ProjectConfig { - pub name: String, - pub directory: String, - pub database: DatabaseType, - pub include_docker: bool, - #[allow(dead_code)] - pub framework: Option, -} - -pub trait BoilerplateGenerator { - fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()>; - fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()>; -} - -/// Generate a FastAPI boilerplate project -pub fn generate_fastapi_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = fastapi::FastAPIGenerator::new(); - generator.validate_config(config)?; - generator.generate_project(config) -} - -/// Generate a Flask boilerplate project -pub fn generate_flask_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = flask::FlaskGenerator::new(); - generator.validate_config(config)?; - generator.generate_project(config) -} - -/// Generate a Go boilerplate project -pub fn generate_go_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = go::GoGenerator::new(); - generator.validate_config(config)?; - generator.generate_project(config) -} - -/// Generate a Laravel PHP boilerplate project -pub fn generate_laravel_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = php::PhpGenerator::new(); - generator.validate_config(config)?; - generator.generate_laravel_project(config) -} - -/// Generate a Symfony PHP boilerplate project -pub fn generate_symfony_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = php::PhpGenerator::new(); - generator.validate_config(config)?; - generator.generate_symfony_project(config) -} - -/// Generate a PHP Vanilla boilerplate project -pub fn generate_vanilla_project(config: &ProjectConfig) -> BoilerplateResult<()> { - let generator = php::PhpGenerator::new(); - generator.validate_config(config)?; - generator.generate_vanilla_project(config) -} - -/// Validate project name -pub fn validate_project_name(name: &str) -> BoilerplateResult<()> { - if name.is_empty() { - return Err(AthenaError::validation_error_simple("Project name cannot be empty".to_string())); - } - - if !name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') { - return Err(AthenaError::validation_error_simple( - "Project name can only contain alphanumeric characters, underscores, and hyphens".to_string() - )); - } - - if name.len() > 50 { - return Err(AthenaError::validation_error_simple("Project name must be 50 characters or less".to_string())); - } - - Ok(()) -} - -/// Check if directory already exists -pub fn check_directory_availability(path: &Path) -> BoilerplateResult<()> { - if path.exists() { - return Err(AthenaError::validation_error_simple( - format!("Directory '{}' already exists", path.display()) - )); - } - Ok(()) -} \ No newline at end of file diff --git a/src/boilerplate/php.rs b/src/boilerplate/php.rs deleted file mode 100644 index 0c31353..0000000 --- a/src/boilerplate/php.rs +++ /dev/null @@ -1,731 +0,0 @@ -//! PHP boilerplate generator with Laravel and Symfony support using Clean Architecture - -use crate::boilerplate::{BoilerplateGenerator, BoilerplateResult, ProjectConfig}; -use crate::boilerplate::utils::{create_directory_structure, write_file, replace_template_vars_string, generate_secret_key, ProjectNames}; -use crate::boilerplate::templates::php::*; -use std::path::Path; - -/// Simple base64 encoding (without external dependency) -fn base64_encode(input: &str) -> String { - const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let mut result = String::new(); - let bytes = input.as_bytes(); - - for chunk in bytes.chunks(3) { - let mut buf = [0u8; 3]; - for (i, &byte) in chunk.iter().enumerate() { - buf[i] = byte; - } - - let b = ((buf[0] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[2] as u32); - - result.push(ALPHABET[((b >> 18) & 63) as usize] as char); - result.push(ALPHABET[((b >> 12) & 63) as usize] as char); - result.push(if chunk.len() > 1 { ALPHABET[((b >> 6) & 63) as usize] as char } else { '=' }); - result.push(if chunk.len() > 2 { ALPHABET[(b & 63) as usize] as char } else { '=' }); - } - - result -} - -pub struct PhpGenerator; - -#[derive(Debug, Clone)] -pub enum PhpFramework { - Laravel, - Symfony, - Vanilla, -} - -impl Default for PhpGenerator { - fn default() -> Self { - Self::new() - } -} - -impl PhpGenerator { - pub fn new() -> Self { - Self - } - - fn get_template_vars(&self, config: &ProjectConfig) -> Vec<(&str, String)> { - let names = ProjectNames::new(&config.name); - let secret_key = generate_secret_key(); - - vec![ - ("project_name", config.name.clone()), - ("snake_case", names.snake_case.clone()), - ("kebab_case", names.kebab_case.clone()), - ("pascal_case", names.pascal_case), - ("upper_case", names.upper_case), - ("secret_key", secret_key), - ("app_key", format!("base64:{}", base64_encode(&generate_secret_key()))), - ] - } - - fn create_laravel_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - // Laravel-specific clean architecture structure - "app/Domain/User/Entities", - "app/Domain/User/Repositories", - "app/Domain/User/Services", - "app/Domain/User/ValueObjects", - "app/Domain/Auth/Services", - "app/Application/User/Commands", - "app/Application/User/Queries", - "app/Application/User/Handlers", - "app/Application/Auth/Commands", - "app/Application/Auth/Handlers", - "app/Infrastructure/Persistence/Eloquent", - "app/Infrastructure/Http/Controllers/Api/V1", - "app/Infrastructure/Http/Middleware", - "app/Infrastructure/Http/Requests", - "app/Infrastructure/Http/Resources", - "app/Infrastructure/Providers", - "app/Infrastructure/Exceptions", - "app/Shared/Events", - "app/Shared/Notifications", - "app/Shared/Services", - "config", - "database/migrations", - "database/seeders", - "routes", - "tests/Unit/Domain", - "tests/Unit/Application", - "tests/Feature/Api", - "tests/Integration", - "storage/logs", - "public", - "resources/lang", - "docker/nginx", - "docker/php", - ]; - - create_directory_structure(base_path, &directories) - } - - fn create_symfony_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - // Symfony-specific hexagonal architecture structure - "src/Domain/User/Entity", - "src/Domain/User/Repository", - "src/Domain/User/Service", - "src/Domain/User/ValueObject", - "src/Domain/Auth/Service", - "src/Application/User/Command", - "src/Application/User/Query", - "src/Application/User/Handler", - "src/Application/Auth/Command", - "src/Application/Auth/Handler", - "src/Infrastructure/Persistence/Doctrine", - "src/Infrastructure/Http/Controller/Api/V1", - "src/Infrastructure/Http/EventListener", - "src/Infrastructure/Security", - "src/Infrastructure/Serializer", - "src/Shared/Event", - "src/Shared/Service", - "config/packages", - "config/routes", - "migrations", - "tests/Unit/Domain", - "tests/Unit/Application", - "tests/Integration/Infrastructure", - "tests/Functional/Api", - "var/log", - "public", - "translations", - "docker/nginx", - "docker/php", - "templates", - ]; - - create_directory_structure(base_path, &directories) - } - - - fn generate_laravel_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Generate Laravel-specific files - let composer_json = replace_template_vars_string(LARAVEL_COMPOSER_JSON, &vars); - write_file(base_path.join("composer.json"), &composer_json)?; - - let artisan = replace_template_vars_string(LARAVEL_ARTISAN, &vars); - write_file(base_path.join("artisan"), &artisan)?; - - // Laravel configuration files - let app_config = replace_template_vars_string(LARAVEL_APP_CONFIG, &vars); - write_file(base_path.join("config/app.php"), &app_config)?; - - let database_config = replace_template_vars_string(LARAVEL_DATABASE_CONFIG, &vars); - write_file(base_path.join("config/database.php"), &database_config)?; - - let auth_config = replace_template_vars_string(LARAVEL_AUTH_CONFIG, &vars); - write_file(base_path.join("config/auth.php"), &auth_config)?; - - // Clean Architecture - Domain Layer - self.generate_laravel_domain_layer(config, base_path)?; - - // Clean Architecture - Application Layer - self.generate_laravel_application_layer(config, base_path)?; - - // Clean Architecture - Infrastructure Layer - self.generate_laravel_infrastructure_layer(config, base_path)?; - - Ok(()) - } - - fn generate_laravel_domain_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // User Entity - let user_entity = replace_template_vars_string(LARAVEL_USER_ENTITY, &vars); - write_file(base_path.join("app/Domain/User/Entities/User.php"), &user_entity)?; - - // User Repository Interface - let user_repository = replace_template_vars_string(LARAVEL_USER_REPOSITORY, &vars); - write_file(base_path.join("app/Domain/User/Repositories/UserRepositoryInterface.php"), &user_repository)?; - - // User Service - let user_service = replace_template_vars_string(LARAVEL_USER_SERVICE, &vars); - write_file(base_path.join("app/Domain/User/Services/UserService.php"), &user_service)?; - - // Auth Service - let auth_service = replace_template_vars_string(LARAVEL_AUTH_SERVICE, &vars); - write_file(base_path.join("app/Domain/Auth/Services/AuthService.php"), &auth_service)?; - - Ok(()) - } - - fn generate_laravel_application_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // User Commands - let create_user_command = replace_template_vars_string(LARAVEL_CREATE_USER_COMMAND, &vars); - write_file(base_path.join("app/Application/User/Commands/CreateUserCommand.php"), &create_user_command)?; - - // User Queries - let get_user_query = replace_template_vars_string(LARAVEL_GET_USER_QUERY, &vars); - write_file(base_path.join("app/Application/User/Queries/GetUserQuery.php"), &get_user_query)?; - - // User Handlers - let user_handler = replace_template_vars_string(LARAVEL_USER_HANDLER, &vars); - write_file(base_path.join("app/Application/User/Handlers/UserHandler.php"), &user_handler)?; - - // Auth Commands & Handlers - let login_command = replace_template_vars_string(LARAVEL_LOGIN_COMMAND, &vars); - write_file(base_path.join("app/Application/Auth/Commands/LoginCommand.php"), &login_command)?; - - let auth_handler = replace_template_vars_string(LARAVEL_AUTH_HANDLER, &vars); - write_file(base_path.join("app/Application/Auth/Handlers/AuthHandler.php"), &auth_handler)?; - - Ok(()) - } - - fn generate_laravel_infrastructure_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Eloquent Repository Implementation - let eloquent_user_repository = replace_template_vars_string(LARAVEL_ELOQUENT_USER_REPOSITORY, &vars); - write_file(base_path.join("app/Infrastructure/Persistence/Eloquent/EloquentUserRepository.php"), &eloquent_user_repository)?; - - // API Controllers - let user_controller = replace_template_vars_string(LARAVEL_USER_CONTROLLER, &vars); - write_file(base_path.join("app/Infrastructure/Http/Controllers/Api/V1/UserController.php"), &user_controller)?; - - let auth_controller = replace_template_vars_string(LARAVEL_AUTH_CONTROLLER, &vars); - write_file(base_path.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php"), &auth_controller)?; - - // HTTP Requests - let register_request = replace_template_vars_string(LARAVEL_REGISTER_REQUEST, &vars); - write_file(base_path.join("app/Infrastructure/Http/Requests/RegisterRequest.php"), ®ister_request)?; - - let login_request = replace_template_vars_string(LARAVEL_LOGIN_REQUEST, &vars); - write_file(base_path.join("app/Infrastructure/Http/Requests/LoginRequest.php"), &login_request)?; - - // API Routes - let api_routes = replace_template_vars_string(LARAVEL_API_ROUTES, &vars); - write_file(base_path.join("routes/api.php"), &api_routes)?; - - // Service Provider - let app_service_provider = replace_template_vars_string(LARAVEL_APP_SERVICE_PROVIDER, &vars); - write_file(base_path.join("app/Infrastructure/Providers/AppServiceProvider.php"), &app_service_provider)?; - - Ok(()) - } - - fn generate_symfony_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Generate Symfony-specific files - let composer_json = replace_template_vars_string(SYMFONY_COMPOSER_JSON, &vars); - write_file(base_path.join("composer.json"), &composer_json)?; - - // Symfony configuration - let services_yaml = replace_template_vars_string(SYMFONY_SERVICES_YAML, &vars); - write_file(base_path.join("config/services.yaml"), &services_yaml)?; - - let doctrine_yaml = replace_template_vars_string(SYMFONY_DOCTRINE_CONFIG, &vars); - write_file(base_path.join("config/packages/doctrine.yaml"), &doctrine_yaml)?; - - let security_yaml = replace_template_vars_string(SYMFONY_SECURITY_CONFIG, &vars); - write_file(base_path.join("config/packages/security.yaml"), &security_yaml)?; - - // Hexagonal Architecture - Domain Layer - self.generate_symfony_domain_layer(config, base_path)?; - - // Hexagonal Architecture - Application Layer - self.generate_symfony_application_layer(config, base_path)?; - - // Hexagonal Architecture - Infrastructure Layer - self.generate_symfony_infrastructure_layer(config, base_path)?; - - Ok(()) - } - - fn generate_symfony_domain_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // User Entity - let user_entity = replace_template_vars_string(SYMFONY_USER_ENTITY, &vars); - write_file(base_path.join("src/Domain/User/Entity/User.php"), &user_entity)?; - - // User Repository Interface - let user_repository = replace_template_vars_string(SYMFONY_USER_REPOSITORY, &vars); - write_file(base_path.join("src/Domain/User/Repository/UserRepositoryInterface.php"), &user_repository)?; - - // User Service - let user_service = replace_template_vars_string(SYMFONY_USER_SERVICE, &vars); - write_file(base_path.join("src/Domain/User/Service/UserService.php"), &user_service)?; - - // Auth Service - let auth_service = replace_template_vars_string(SYMFONY_AUTH_SERVICE, &vars); - write_file(base_path.join("src/Domain/Auth/Service/AuthService.php"), &auth_service)?; - - Ok(()) - } - - fn generate_symfony_application_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // User Commands - let create_user_command = replace_template_vars_string(SYMFONY_CREATE_USER_COMMAND, &vars); - write_file(base_path.join("src/Application/User/Command/CreateUserCommand.php"), &create_user_command)?; - - // User Queries - let get_user_query = replace_template_vars_string(SYMFONY_GET_USER_QUERY, &vars); - write_file(base_path.join("src/Application/User/Query/GetUserQuery.php"), &get_user_query)?; - - // User Handlers - let user_handler = replace_template_vars_string(SYMFONY_USER_HANDLER, &vars); - write_file(base_path.join("src/Application/User/Handler/UserHandler.php"), &user_handler)?; - - // Auth Commands & Handlers - let login_command = replace_template_vars_string(SYMFONY_LOGIN_COMMAND, &vars); - write_file(base_path.join("src/Application/Auth/Command/LoginCommand.php"), &login_command)?; - - let auth_handler = replace_template_vars_string(SYMFONY_AUTH_HANDLER, &vars); - write_file(base_path.join("src/Application/Auth/Handler/AuthHandler.php"), &auth_handler)?; - - Ok(()) - } - - fn generate_symfony_infrastructure_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Doctrine Repository Implementation - let doctrine_user_repository = replace_template_vars_string(SYMFONY_DOCTRINE_USER_REPOSITORY, &vars); - write_file(base_path.join("src/Infrastructure/Persistence/Doctrine/DoctrineUserRepository.php"), &doctrine_user_repository)?; - - // API Controllers - let user_controller = replace_template_vars_string(SYMFONY_USER_CONTROLLER, &vars); - write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/UserController.php"), &user_controller)?; - - let auth_controller = replace_template_vars_string(SYMFONY_AUTH_CONTROLLER, &vars); - write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php"), &auth_controller)?; - - // API Routes - let api_routes = replace_template_vars_string(SYMFONY_API_ROUTES, &vars); - write_file(base_path.join("config/routes/api.yaml"), &api_routes)?; - - Ok(()) - } - - fn generate_docker_files(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { - if !config.include_docker { - return Ok(()); - } - - let vars = self.get_template_vars(config); - - // Optimized PHP Dockerfile - let dockerfile_content = replace_template_vars_string(PHP_OPTIMIZED_DOCKERFILE, &vars); - write_file(base_path.join("docker/php/Dockerfile"), &dockerfile_content)?; - - // Nginx Dockerfile - let nginx_dockerfile = replace_template_vars_string(PHP_NGINX_DOCKERFILE, &vars); - write_file(base_path.join("docker/nginx/Dockerfile"), &nginx_dockerfile)?; - - // Docker Compose (production-ready) - let docker_compose = match framework { - PhpFramework::Laravel => replace_template_vars_string(LARAVEL_DOCKER_COMPOSE, &vars), - PhpFramework::Symfony => replace_template_vars_string(SYMFONY_DOCKER_COMPOSE, &vars), - PhpFramework::Vanilla => replace_template_vars_string(SYMFONY_DOCKER_COMPOSE, &vars), // Reuse Symfony's Docker setup - }; - write_file(base_path.join("docker-compose.yml"), &docker_compose)?; - - // Docker Compose Development Override - let docker_compose_dev = replace_template_vars_string(PHP_DOCKER_COMPOSE_DEV, &vars); - write_file(base_path.join("docker-compose.dev.yml"), &docker_compose_dev)?; - - // Environment files - let env_docker = replace_template_vars_string(PHP_ENV_DOCKER, &vars); - write_file(base_path.join(".env.docker.example"), &env_docker)?; - - let env_example = match framework { - PhpFramework::Laravel => replace_template_vars_string(LARAVEL_ENV_EXAMPLE, &vars), - PhpFramework::Symfony => replace_template_vars_string(SYMFONY_ENV_EXAMPLE, &vars), - PhpFramework::Vanilla => { - // For Vanilla, the .env.example is already generated in generate_vanilla_files - // So we skip it here to avoid overwriting - return Ok(()); - } - }; - write_file(base_path.join(".env.example"), &env_example)?; - - // Nginx configuration - let nginx_conf = replace_template_vars_string(PHP_NGINX_CONF, &vars); - write_file(base_path.join("docker/nginx/nginx.conf"), &nginx_conf)?; - - let nginx_default_conf = match framework { - PhpFramework::Laravel => replace_template_vars_string(LARAVEL_NGINX_DEFAULT_CONF, &vars), - PhpFramework::Symfony => replace_template_vars_string(SYMFONY_NGINX_DEFAULT_CONF, &vars), - PhpFramework::Vanilla => replace_template_vars_string(SYMFONY_NGINX_DEFAULT_CONF, &vars), // Reuse Symfony's nginx config - }; - write_file(base_path.join("docker/nginx/default.conf"), &nginx_default_conf)?; - - // PHP-FPM configuration - let php_fpm_conf = replace_template_vars_string(PHP_FPM_CONF, &vars); - write_file(base_path.join("docker/php/php-fpm.conf"), &php_fpm_conf)?; - - Ok(()) - } - - fn generate_test_files(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - match framework { - PhpFramework::Laravel => { - let phpunit_xml = replace_template_vars_string(LARAVEL_PHPUNIT_XML, &vars); - write_file(base_path.join("phpunit.xml"), &phpunit_xml)?; - - let feature_test = replace_template_vars_string(LARAVEL_AUTH_FEATURE_TEST, &vars); - write_file(base_path.join("tests/Feature/Api/AuthTest.php"), &feature_test)?; - - let unit_test = replace_template_vars_string(LARAVEL_USER_UNIT_TEST, &vars); - write_file(base_path.join("tests/Unit/Domain/UserTest.php"), &unit_test)?; - } - PhpFramework::Symfony => { - let phpunit_xml = replace_template_vars_string(SYMFONY_PHPUNIT_XML, &vars); - write_file(base_path.join("phpunit.xml.dist"), &phpunit_xml)?; - - let functional_test = replace_template_vars_string(SYMFONY_AUTH_FUNCTIONAL_TEST, &vars); - write_file(base_path.join("tests/Functional/Api/AuthTest.php"), &functional_test)?; - - let unit_test = replace_template_vars_string(SYMFONY_USER_UNIT_TEST, &vars); - write_file(base_path.join("tests/Unit/Domain/UserTest.php"), &unit_test)?; - } - PhpFramework::Vanilla => { - // Test files are already generated in generate_vanilla_files - // This is to avoid duplication since Vanilla handles its own test generation - } - } - - Ok(()) - } - - fn generate_documentation(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - let readme = match framework { - PhpFramework::Laravel => replace_template_vars_string(LARAVEL_README, &vars), - PhpFramework::Symfony => replace_template_vars_string(SYMFONY_README, &vars), - PhpFramework::Vanilla => replace_template_vars_string(VANILLA_README, &vars), - }; - - write_file(base_path.join("README.md"), &readme)?; - - Ok(()) - } - - pub fn generate_laravel_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - let framework = PhpFramework::Laravel; - - println!("Generating Laravel project with Clean Architecture: {}", config.name); - - // Create directory structure - println!(" 📁 Creating clean architecture structure..."); - self.create_laravel_structure(base_path)?; - - // Generate Laravel files - println!(" ⚡ Generating Laravel application files..."); - self.generate_laravel_files(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path, &framework)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path, &framework)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path, &framework)?; - - println!("Laravel project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - println!(" docker-compose exec app composer install"); - println!(" docker-compose exec app php artisan migrate"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" composer install"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" php artisan key:generate"); - println!(" php artisan migrate"); - println!(" php artisan serve"); - } - - Ok(()) - } - - pub fn generate_symfony_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - let framework = PhpFramework::Symfony; - - println!("Generating Symfony project with Hexagonal Architecture: {}", config.name); - - // Create directory structure - println!(" 📁 Creating hexagonal architecture structure..."); - self.create_symfony_structure(base_path)?; - - // Generate Symfony files - println!(" 🎼 Generating Symfony application files..."); - self.generate_symfony_files(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path, &framework)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path, &framework)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path, &framework)?; - - println!("Symfony project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - println!(" docker-compose exec app composer install"); - println!(" docker-compose exec app php bin/console doctrine:migrations:migrate"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" composer install"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" php bin/console doctrine:migrations:migrate"); - println!(" symfony server:start"); - } - - Ok(()) - } - - pub fn generate_vanilla_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - let base_path = Path::new(&config.directory); - let framework = PhpFramework::Vanilla; - - println!("Generating PHP Vanilla project with Clean Architecture: {}", config.name); - - // Create directory structure - println!(" 📁 Creating clean architecture structure..."); - self.create_vanilla_structure(base_path)?; - - // Generate PHP Vanilla files - println!(" 🐘 Generating PHP vanilla application files..."); - self.generate_vanilla_files(config, base_path)?; - - // Generate Docker files - if config.include_docker { - println!(" 🐳 Generating Docker configuration..."); - self.generate_docker_files(config, base_path, &framework)?; - } - - // Generate test files - println!(" 🧪 Creating test suite..."); - self.generate_test_files(config, base_path, &framework)?; - - // Generate documentation - println!(" 📚 Generating documentation..."); - self.generate_documentation(config, base_path, &framework)?; - - println!("PHP Vanilla project '{}' created successfully!", config.name); - println!("📍 Location: {}", base_path.display()); - - if config.include_docker { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" docker-compose up --build"); - println!(" docker-compose exec app composer install"); - } else { - println!("\n🔧 Next steps:"); - println!(" cd {}", config.directory); - println!(" composer install"); - println!(" cp .env.example .env # Edit with your configuration"); - println!(" php -S localhost:8000 public/index.php"); - } - - Ok(()) - } - - fn create_vanilla_structure(&self, base_path: &Path) -> BoilerplateResult<()> { - let directories = vec![ - // Clean Architecture structure - "src", - "src/Domain", - "src/Domain/User", - "src/Domain/User/Entity", - "src/Domain/User/Repository", - "src/Domain/User/Service", - "src/Domain/User/ValueObject", - "src/Domain/Auth", - "src/Domain/Auth/Service", - "src/Application", - "src/Application/User", - "src/Application/User/Command", - "src/Application/User/Query", - "src/Application/User/Handler", - "src/Application/Auth", - "src/Application/Auth/Command", - "src/Application/Auth/Handler", - "src/Infrastructure", - "src/Infrastructure/Http", - "src/Infrastructure/Http/Controller", - "src/Infrastructure/Http/Controller/Api", - "src/Infrastructure/Http/Controller/Api/V1", - "src/Infrastructure/Http/Middleware", - "src/Infrastructure/Persistence", - "src/Infrastructure/Persistence/PDO", - "src/Infrastructure/Security", - "src/Infrastructure/Routing", - "src/Infrastructure/Database", - "src/Infrastructure/Config", - // Public directory - "public", - // Config directory - "config", - // Database directory - "database", - "database/migrations", - // Tests - "tests", - "tests/Unit", - "tests/Integration", - "tests/Functional", - // Docker if needed - "docker", - "docker/php", - "docker/nginx", - ]; - - create_directory_structure(base_path, &directories) - } - - fn generate_vanilla_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { - let vars = self.get_template_vars(config); - - // Core application files - write_file(base_path.join("composer.json"), &replace_template_vars_string(VANILLA_COMPOSER_JSON, &vars))?; - write_file(base_path.join("public/index.php"), &replace_template_vars_string(VANILLA_INDEX_PHP, &vars))?; - write_file(base_path.join("public/.htaccess"), &replace_template_vars_string(VANILLA_HTACCESS, &vars))?; - - // Configuration files - write_file(base_path.join("config/database.php"), &replace_template_vars_string(VANILLA_DATABASE_CONFIG, &vars))?; - write_file(base_path.join("config/app.php"), &replace_template_vars_string(VANILLA_APP_CONFIG, &vars))?; - - // Environment files - write_file(base_path.join(".env.example"), &replace_template_vars_string(VANILLA_ENV_EXAMPLE, &vars))?; - - // Core framework files - write_file(base_path.join("src/Infrastructure/Http/Router.php"), &replace_template_vars_string(VANILLA_ROUTER, &vars))?; - write_file(base_path.join("src/Infrastructure/Http/Request.php"), &replace_template_vars_string(VANILLA_REQUEST, &vars))?; - write_file(base_path.join("src/Infrastructure/Http/Response.php"), &replace_template_vars_string(VANILLA_RESPONSE, &vars))?; - write_file(base_path.join("src/Infrastructure/Database/PDOConnection.php"), &replace_template_vars_string(VANILLA_PDO_CONNECTION, &vars))?; - write_file(base_path.join("src/Infrastructure/Security/JWTManager.php"), &replace_template_vars_string(VANILLA_JWT_MANAGER, &vars))?; - write_file(base_path.join("src/Infrastructure/Config/AppConfig.php"), &replace_template_vars_string(VANILLA_APP_CONFIG_CLASS, &vars))?; - - // Domain layer - write_file(base_path.join("src/Domain/User/Entity/User.php"), &replace_template_vars_string(VANILLA_USER_ENTITY, &vars))?; - write_file(base_path.join("src/Domain/User/Repository/UserRepositoryInterface.php"), &replace_template_vars_string(VANILLA_USER_REPOSITORY_INTERFACE, &vars))?; - write_file(base_path.join("src/Domain/User/Service/UserService.php"), &replace_template_vars_string(VANILLA_USER_SERVICE, &vars))?; - write_file(base_path.join("src/Domain/User/ValueObject/Email.php"), &replace_template_vars_string(VANILLA_EMAIL_VALUE_OBJECT, &vars))?; - write_file(base_path.join("src/Domain/User/ValueObject/UserId.php"), &replace_template_vars_string(VANILLA_USER_ID_VALUE_OBJECT, &vars))?; - - // Application layer - write_file(base_path.join("src/Application/User/Command/CreateUserCommand.php"), &replace_template_vars_string(VANILLA_CREATE_USER_COMMAND, &vars))?; - write_file(base_path.join("src/Application/User/Handler/CreateUserHandler.php"), &replace_template_vars_string(VANILLA_CREATE_USER_HANDLER, &vars))?; - write_file(base_path.join("src/Application/Auth/Command/LoginCommand.php"), &replace_template_vars_string(VANILLA_LOGIN_COMMAND, &vars))?; - write_file(base_path.join("src/Application/Auth/Handler/LoginHandler.php"), &replace_template_vars_string(VANILLA_LOGIN_HANDLER, &vars))?; - - // Infrastructure layer - write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php"), &replace_template_vars_string(VANILLA_AUTH_CONTROLLER, &vars))?; - write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/UserController.php"), &replace_template_vars_string(VANILLA_USER_CONTROLLER, &vars))?; - write_file(base_path.join("src/Infrastructure/Persistence/PDO/UserRepository.php"), &replace_template_vars_string(VANILLA_USER_REPOSITORY, &vars))?; - write_file(base_path.join("src/Infrastructure/Http/Middleware/AuthMiddleware.php"), &replace_template_vars_string(VANILLA_AUTH_MIDDLEWARE, &vars))?; - write_file(base_path.join("src/Infrastructure/Http/Middleware/CorsMiddleware.php"), &replace_template_vars_string(VANILLA_CORS_MIDDLEWARE, &vars))?; - - // Database migrations - write_file(base_path.join("database/migrations/001_create_users_table.sql"), &replace_template_vars_string(VANILLA_USERS_MIGRATION, &vars))?; - - // Testing configuration - write_file(base_path.join("phpunit.xml"), &replace_template_vars_string(VANILLA_PHPUNIT_XML, &vars))?; - - // Test files - write_file(base_path.join("tests/Unit/UserTest.php"), &replace_template_vars_string(VANILLA_USER_TEST, &vars))?; - write_file(base_path.join("tests/Functional/AuthTest.php"), &replace_template_vars_string(VANILLA_AUTH_FUNCTIONAL_TEST, &vars))?; - - Ok(()) - } -} - -impl BoilerplateGenerator for PhpGenerator { - fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - crate::boilerplate::validate_project_name(&config.name)?; - crate::boilerplate::check_directory_availability(Path::new(&config.directory))?; - Ok(()) - } - - fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { - // Default to Laravel, but this can be extended to support framework selection - self.generate_laravel_project(config) - } -} \ No newline at end of file diff --git a/src/boilerplate/templates.rs b/src/boilerplate/templates.rs deleted file mode 100644 index 2cf4671..0000000 --- a/src/boilerplate/templates.rs +++ /dev/null @@ -1,7521 +0,0 @@ -//! Template definitions for FastAPI, Flask, and Go boilerplates - -pub mod fastapi { - pub const MAIN_PY: &str = r#"from __future__ import annotations - -from typing import Any -from fastapi import FastAPI, APIRouter, Request, Response, status -from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.trustedhost import TrustedHostMiddleware -from fastapi.responses import JSONResponse -import uvicorn -import structlog -import uuid -from contextlib import asynccontextmanager - -from app.core.config import settings -from app.core.logging import setup_logging, RequestIdMiddleware -from app.core.security import verify_token -from app.database.connection import init_database, close_database_connection -from app.api import health -from app.api.v1 import auth, users -from app.core.rate_limiting import RateLimitMiddleware - -# Setup structured logging -setup_logging() -logger = structlog.get_logger() - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup - logger.info("Starting up {{project_name}} application", service="{{project_name}}", version="1.0.0") - await init_database() - yield - # Shutdown - logger.info("Shutting down {{project_name}} application", service="{{project_name}}") - await close_database_connection() - -app = FastAPI( - title="{{project_name}} API", - description="Production-ready {{project_name}} API with authentication and observability", - version="1.0.0", - lifespan=lifespan, - openapi_tags=[ - {"name": "health", "description": "Health and readiness checks"}, - {"name": "authentication", "description": "User authentication operations"}, - {"name": "users", "description": "User management operations"}, - ], -) - -# Add request ID middleware first -app.add_middleware(RequestIdMiddleware) - -# Rate limiting middleware -app.add_middleware(RateLimitMiddleware, calls=100, period=60) - -# Security middleware -app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.ALLOWED_HOSTS + ["localhost", "127.0.0.1"]) -app.add_middleware( - CORSMiddleware, - allow_origins=settings.ALLOWED_HOSTS, - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=["*"], - expose_headers=["X-Request-ID"], -) - -# Exception handler for better error responses -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: - request_id = getattr(request.state, "request_id", str(uuid.uuid4())) - logger.error( - "Unhandled exception", - request_id=request_id, - path=request.url.path, - method=request.method, - error=str(exc), - exc_info=True - ) - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={ - "error": "Internal server error", - "request_id": request_id, - "message": "An unexpected error occurred" - }, - headers={"X-Request-ID": request_id} - ) - -# Include routers -app.include_router(health.router, tags=["health"]) - -# API v1 routes -api_v1 = APIRouter(prefix="/api/v1") -api_v1.include_router(auth.router, prefix="/auth", tags=["authentication"]) -api_v1.include_router(users.router, prefix="/users", tags=["users"]) -app.include_router(api_v1) - -@app.get("/") -async def root(request: Request) -> dict[str, Any]: - request_id = getattr(request.state, "request_id", str(uuid.uuid4())) - logger.info("Root endpoint accessed", request_id=request_id) - return { - "message": "{{project_name}} API is running", - "version": "1.0.0", - "environment": settings.ENVIRONMENT, - "request_id": request_id - } - -if __name__ == "__main__": - uvicorn.run( - "app.main:app", - host="0.0.0.0", - port=8000, - reload=settings.ENVIRONMENT == "development", - access_log=False, # Use structured logging instead - log_config=None # Disable default logging - ) -"#; - - pub const CONFIG_PY: &str = r#"from __future__ import annotations - -from pydantic_settings import BaseSettings -from pydantic import ConfigDict, Field, validator -from typing import List -import os - -class Settings(BaseSettings): - # Application - PROJECT_NAME: str = "{{project_name}}" - VERSION: str = "1.0.0" - ENVIRONMENT: str = Field(default="development", description="Environment: development, staging, production") - DEBUG: bool = Field(default=False, description="Enable debug mode") - - # Security - SECRET_KEY: str = Field("{{secret_key}}", min_length=32, description="Secret key for JWT signing") - ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, ge=5, le=1440) - REFRESH_TOKEN_EXPIRE_MINUTES: int = Field(default=60 * 24 * 7, ge=60) # 7 days - ALGORITHM: str = "HS256" - - # CORS and Security - ALLOWED_HOSTS: List[str] = Field( - default=["http://localhost:3000", "http://127.0.0.1:3000"], - description="Allowed CORS origins" - ) - TRUSTED_HOSTS: List[str] = Field( - default=["localhost", "127.0.0.1"], - description="Trusted hosts for TrustedHostMiddleware" - ) - - # Database - MONGODB_URL: str = "mongodb://localhost:27017" - DATABASE_NAME: str = "{{snake_case}}_db" - DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost/{{snake_case}}_db" - POSTGRES_PASSWORD: str = "changeme" - - # Connection Pool Settings - DB_POOL_SIZE: int = Field(default=20, ge=5, le=100) - DB_POOL_OVERFLOW: int = Field(default=0, ge=0, le=50) - - # Redis (for caching/sessions) - REDIS_URL: str = "redis://localhost:6379" - - # Logging - LOG_LEVEL: str = Field(default="INFO", description="Logging level") - LOG_FORMAT: str = Field(default="json", description="Logging format: json or text") - - # Rate Limiting - RATE_LIMIT_ENABLED: bool = Field(default=True, description="Enable rate limiting") - RATE_LIMIT_CALLS: int = Field(default=100, ge=1, description="Rate limit calls per period") - RATE_LIMIT_PERIOD: int = Field(default=60, ge=1, description="Rate limit period in seconds") - - # OpenTelemetry (optional) - OTEL_ENABLED: bool = Field(default=False, description="Enable OpenTelemetry") - OTEL_ENDPOINT: str = Field(default="", description="OpenTelemetry endpoint") - - @validator("ENVIRONMENT") - def validate_environment(cls, v: str) -> str: - if v not in ["development", "staging", "production"]: - raise ValueError("ENVIRONMENT must be development, staging, or production") - return v - - @validator("LOG_LEVEL") - def validate_log_level(cls, v: str) -> str: - if v.upper() not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: - raise ValueError("LOG_LEVEL must be DEBUG, INFO, WARNING, ERROR, or CRITICAL") - return v.upper() - - @validator("LOG_FORMAT") - def validate_log_format(cls, v: str) -> str: - if v not in ["json", "text"]: - raise ValueError("LOG_FORMAT must be json or text") - return v - - model_config = ConfigDict( - env_file=".env", - case_sensitive=True, - validate_assignment=True, - extra="forbid" - ) - - @property - def is_development(self) -> bool: - return self.ENVIRONMENT == "development" - - @property - def is_production(self) -> bool: - return self.ENVIRONMENT == "production" - -settings = Settings() -"#; - - pub const SECURITY_PY: &str = r#"from datetime import datetime, timedelta -from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext -from fastapi import HTTPException, status -import secrets - -from app.core.config import settings - -# Password hashing -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - -def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verify a password against its hash""" - return pwd_context.verify(plain_password, hashed_password) - -def get_password_hash(password: str) -> str: - """Hash a password""" - return pwd_context.hash(password) - -def create_access_token(data: Dict[str, Any]) -> str: - """Create JWT access token""" - to_encode = data.copy() - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - to_encode.update({"exp": expire, "type": "access"}) - - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - return encoded_jwt - -def create_refresh_token(data: Dict[str, Any]) -> str: - """Create JWT refresh token""" - to_encode = data.copy() - expire = datetime.utcnow() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) - to_encode.update({"exp": expire, "type": "refresh"}) - - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - return encoded_jwt - -def verify_token(token: str, token_type: str = "access") -> Dict[str, Any]: - """Verify and decode JWT token""" - try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - - # Verify token type - if payload.get("type") != token_type: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token type" - ) - - return payload - except JWTError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials" - ) - -def generate_secure_secret() -> str: - """Generate a secure secret key""" - return secrets.token_urlsafe(32) -"#; - - pub const REQUIREMENTS_TXT: &str = r#"# Core Framework - Latest 2025 versions -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -gunicorn==23.0.0 -pydantic==2.9.0 -pydantic-settings==2.5.0 - -# Authentication & Security -python-jose[cryptography]==3.3.0 -passlib[bcrypt]==1.7.4 -python-multipart==0.0.12 - -# Structured Logging -structlog==24.4.0 -python-json-logger==2.0.7 - -# Database{{#if mongodb}} -motor==3.6.0 -pymongo==4.10.1{{/if}}{{#if postgresql}} -asyncpg==0.29.0 -sqlalchemy[asyncio]==2.0.36 -sqlmodel==0.0.22 -alembic==1.14.0{{/if}} - -# Caching & Storage -redis==5.1.1 - -# Performance & Monitoring -slowapi==0.1.9 -prometheus-fastapi-instrumentator==7.0.0 - -# Optional: OpenTelemetry -opentelemetry-api==1.27.0 -opentelemetry-sdk==1.27.0 -opentelemetry-instrumentation-fastapi==0.48b0 -opentelemetry-instrumentation-sqlalchemy==0.48b0 -opentelemetry-exporter-otlp==1.27.0 - -# Development & Testing -pytest==8.3.3 -pytest-asyncio==0.24.0 -pytest-cov==5.0.0 -httpx==0.27.2 -factory-boy==3.3.1 - -# Code Quality -ruff==0.7.4 -mypy==1.13.0 -pre-commit==4.0.1 -"#; - - pub const DOCKERFILE: &str = r#"FROM python:3.12-slim AS builder - -# Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends gcc \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -# Copy requirements first for better caching -COPY requirements.txt . - -# Install Python dependencies in user space -RUN pip install --no-cache-dir --user -r requirements.txt - -# Production stage -FROM python:3.11-slim - -# Set environment variables -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 \ - PATH=/home/appuser/.local/bin:$PATH - -WORKDIR /app - -# Create non-root user first -RUN useradd --uid 1000 --create-home --shell /bin/bash appuser - -# Copy dependencies from builder stage -COPY --from=builder /root/.local /home/appuser/.local - -# Install wget for health check -RUN apt-get update && apt-get install -y --no-install-recommends wget \ - && rm -rf /var/lib/apt/lists/* - -# Copy application code -COPY . . - -# Change ownership to non-root user -RUN chown -R appuser:appuser /app - -# Switch to non-root user -USER appuser - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget -qO- http://localhost:8000/health || exit 1 - -# Expose port -EXPOSE 8000 - -# Run with gunicorn for production -CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000", "--workers", "4", "--timeout", "120"] -"#; - - pub const DOCKER_COMPOSE_YML: &str = r#"services: - {{kebab_case}}-api: - build: . - ports: - - "8000:8000" - environment: - - ENVIRONMENT=production{{#if mongodb}} - - MONGODB_URL=mongodb://mongo:27017{{/if}}{{#if postgresql}} - - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/{{snake_case}}_db{{/if}} - - REDIS_URL=redis://redis:6379 - - SECRET_KEY=${SECRET_KEY} - depends_on:{{#if mongodb}} - - mongo{{/if}}{{#if postgresql}} - - postgres{{/if}} - - redis - volumes: - - ./logs:/app/logs - restart: unless-stopped - networks: - - {{kebab_case}}-network - -{{#if mongodb}} - mongo: - image: mongo:7 - environment: - - MONGO_INITDB_DATABASE={{snake_case}}_db - volumes: - - mongo_data:/data/db - restart: unless-stopped - networks: - - {{kebab_case}}-network -{{/if}} - -{{#if postgresql}} - postgres: - image: postgres:15 - environment: - - POSTGRES_DB={{snake_case}}_db - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - networks: - - {{kebab_case}}-network -{{/if}} - - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - restart: unless-stopped - networks: - - {{kebab_case}}-network - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/conf.d:/etc/nginx/conf.d:ro - depends_on: - - {{kebab_case}}-api - restart: unless-stopped - networks: - - {{kebab_case}}-network - -volumes:{{#if mongodb}} - mongo_data:{{/if}}{{#if postgresql}} - postgres_data:{{/if}} - redis_data: - -networks: - {{kebab_case}}-network: - driver: bridge -"#; - - pub const NGINX_CONF: &str = r#"# Main Nginx configuration -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; - use epoll; - multi_accept on; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Basic settings - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - client_max_body_size 16M; - - # Security headers - add_header X-Frame-Options DENY always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always; - - # Rate limiting - limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/json - application/javascript - application/xml+rss - application/atom+xml; - - # Logging format - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - # Include server configurations - include /etc/nginx/conf.d/*.conf; -} -"#; - - pub const NGINX_DEFAULT_CONF: &str = r#"# Default server configuration for {{project_name}} -upstream {{kebab_case}}_api { - server {{kebab_case}}-api:8000; - keepalive 32; -} - -server { - listen 80; - server_name _; - - # Apply rate limiting - limit_req zone=api burst=20 nodelay; - - # API proxy - location /api/ { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } - - # Health check endpoint - location /health { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - access_log off; - } - - # Root endpoint - location / { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } -} -"#; -} - -// New structured logging module -pub const LOGGING_PY: &str = r#"from __future__ import annotations - -import logging -import logging.config -import structlog -import uuid -from typing import Any, Dict -from fastapi import Request, Response -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.requests import Request as StarletteRequest - -from app.core.config import settings - -def setup_logging() -> None: - """Configure structured logging with structlog""" - - # Configure structlog - if settings.LOG_FORMAT == "json": - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - structlog.processors.JSONRenderer() - ] - else: - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.dev.ConsoleRenderer() - ] - - structlog.configure( - processors=processors, - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - # Configure standard library logging - logging.basicConfig( - format="%(message)s", - level=getattr(logging, settings.LOG_LEVEL), - force=True, - ) - -class RequestIdMiddleware(BaseHTTPMiddleware): - """Middleware to add request ID to each request""" - - async def dispatch(self, request: Request, call_next) -> Response: - # Generate or extract request ID - request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) - - # Store request ID in request state - request.state.request_id = request_id - - # Set up structured logging context - logger = structlog.get_logger() - logger = logger.bind( - request_id=request_id, - method=request.method, - path=request.url.path, - user_agent=request.headers.get("User-Agent", ""), - remote_addr=request.client.host if request.client else None - ) - - # Log request - logger.info( - "Request started", - query_params=dict(request.query_params) - ) - - try: - response = await call_next(request) - - # Log response - logger.info( - "Request completed", - status_code=response.status_code, - response_time_ms=None # Could add timing here - ) - - # Add request ID to response headers - response.headers["X-Request-ID"] = request_id - - return response - - except Exception as exc: - logger.error( - "Request failed", - error=str(exc), - exc_info=True - ) - raise -"#; - -// Rate limiting middleware -pub const RATE_LIMITING_PY: &str = r#"from __future__ import annotations - -import time -from typing import Optional -from fastapi import Request, HTTPException, status -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.responses import Response -import structlog -from collections import defaultdict, deque -from threading import Lock - -from app.core.config import settings - -logger = structlog.get_logger() - -class InMemoryRateLimiter: - """Simple in-memory rate limiter using sliding window""" - - def __init__(self): - self.requests = defaultdict(deque) - self.lock = Lock() - - def is_allowed(self, key: str, limit: int, window: int) -> bool: - """Check if request is allowed under rate limit""" - now = time.time() - - with self.lock: - # Remove old requests outside the window - while self.requests[key] and self.requests[key][0] <= now - window: - self.requests[key].popleft() - - # Check if under limit - if len(self.requests[key]) >= limit: - return False - - # Add current request - self.requests[key].append(now) - return True - - def get_remaining(self, key: str, limit: int, window: int) -> int: - """Get remaining requests allowed""" - now = time.time() - - with self.lock: - # Clean old requests - while self.requests[key] and self.requests[key][0] <= now - window: - self.requests[key].popleft() - - return max(0, limit - len(self.requests[key])) - - def get_reset_time(self, key: str, window: int) -> Optional[float]: - """Get time when rate limit resets""" - with self.lock: - if not self.requests[key]: - return None - return self.requests[key][0] + window - -class RateLimitMiddleware(BaseHTTPMiddleware): - """Rate limiting middleware""" - - def __init__(self, app, calls: int = 100, period: int = 60): - super().__init__(app) - self.calls = calls - self.period = period - self.limiter = InMemoryRateLimiter() - - def get_client_key(self, request: Request) -> str: - """Get client identifier for rate limiting""" - # Use X-Forwarded-For if available (behind proxy) - forwarded_for = request.headers.get("X-Forwarded-For") - if forwarded_for: - return forwarded_for.split(",")[0].strip() - - # Fall back to direct client IP - return request.client.host if request.client else "unknown" - - async def dispatch(self, request: Request, call_next) -> Response: - if not settings.RATE_LIMIT_ENABLED: - return await call_next(request) - - # Skip rate limiting for health checks - if request.url.path in ["/health", "/health/ready", "/health/live"]: - return await call_next(request) - - client_key = self.get_client_key(request) - - # Check rate limit - if not self.limiter.is_allowed(client_key, self.calls, self.period): - logger.warning( - "Rate limit exceeded", - client=client_key, - path=request.url.path, - method=request.method - ) - - reset_time = self.limiter.get_reset_time(client_key, self.period) - - raise HTTPException( - status_code=status.HTTP_429_TOO_MANY_REQUESTS, - detail="Rate limit exceeded", - headers={ - "X-RateLimit-Limit": str(self.calls), - "X-RateLimit-Remaining": "0", - "X-RateLimit-Reset": str(int(reset_time)) if reset_time else "", - "Retry-After": str(self.period) - } - ) - - # Add rate limit headers to response - response = await call_next(request) - - remaining = self.limiter.get_remaining(client_key, self.calls, self.period) - reset_time = self.limiter.get_reset_time(client_key, self.period) - - response.headers["X-RateLimit-Limit"] = str(self.calls) - response.headers["X-RateLimit-Remaining"] = str(remaining) - if reset_time: - response.headers["X-RateLimit-Reset"] = str(int(reset_time)) - - return response -"#; - -pub mod go { - pub const MAIN_GO: &str = r#"package main - -import ( - "context" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "{{module_name}}/internal/config" - "{{module_name}}/internal/database" - "{{module_name}}/internal/handlers" - "{{module_name}}/internal/middleware" - "{{module_name}}/internal/routes" - "{{module_name}}/pkg/logger" - - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" -) - -func main() { - // Setup structured logging first - logger.Setup() - log := slog.Default() - - // Load environment variables - if err := godotenv.Load(); err != nil { - log.Warn("No .env file found", "error", err) - } - - // Load configuration - cfg := config.Load() - - log.Info("Starting {{project_name}} application", - "environment", cfg.Environment, - "port", cfg.Port, - "version", "1.0.0") - - // Set gin mode and disable default logging (we use slog) - if cfg.Environment == "production" { - gin.SetMode(gin.ReleaseMode) - } - gin.DisableConsoleColor() - - // Initialize database with context - ctx := context.Background() - db, err := database.Connect(ctx, cfg) - if err != nil { - log.Error("Failed to connect to database", "error", err) - os.Exit(1) - } - defer database.Close(db) - - // Initialize handlers - h := handlers.New(db, cfg) - - // Setup router with structured logging middleware - router := gin.New() - router.Use(middleware.StructuredLogger()) - router.Use(middleware.Recovery()) - router.Use(middleware.CORS()) - router.Use(middleware.Security()) - router.Use(middleware.RequestID()) - - // Setup routes - routes.Setup(router, h) - - // Create server with improved settings - srv := &http.Server{ - Addr: ":" + cfg.Port, - Handler: router, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 60 * time.Second, - MaxHeaderBytes: 1 << 20, // 1MB - } - - // Start server in goroutine - go func() { - log.Info("Server starting", "port", cfg.Port, "environment", cfg.Environment) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Error("Server failed to start", "error", err) - os.Exit(1) - } - }() - - // Wait for interrupt signal with graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Info("Shutting down server gracefully...") - - // Graceful shutdown with extended timeout - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Error("Server forced shutdown", "error", err) - os.Exit(1) - } - - log.Info("Server shutdown completed successfully") -} -"#; - - pub const GO_MOD: &str = r#"module {{module_name}} - -go 1.22 - -require ( - // Core framework - github.com/gin-gonic/gin v1.10.0 - - // Authentication & Security - github.com/golang-jwt/jwt/v5 v5.2.1 - golang.org/x/crypto v0.28.0 - - // Configuration - github.com/joho/godotenv v1.5.1 - - // Database drivers & ORM - go.mongodb.org/mongo-driver v1.17.1 - github.com/lib/pq v1.10.9 - github.com/jmoiron/sqlx v1.4.0 - - // Utilities - github.com/google/uuid v1.6.0 - - // Validation - github.com/go-playground/validator/v10 v10.22.1 - - // Testing - github.com/stretchr/testify v1.9.0 -) -"#; - - pub const DOCKERFILE_GO: &str = r#"# Build stage -FROM golang:1.21-alpine AS builder - -# Install git and ca-certificates -RUN apk add --no-cache git ca-certificates tzdata - -# Set working directory -WORKDIR /app - -# Copy source code -COPY . . - -# Download dependencies and tidy -RUN go mod tidy && go mod download - -# Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/main.go - -# Final stage -FROM alpine:latest - -# Install ca-certificates -RUN apk --no-cache add ca-certificates - -# Create app directory -WORKDIR /root/ - -# Copy binary from builder -COPY --from=builder /app/main . - -# Create non-root user -RUN adduser -D -s /bin/sh appuser -USER appuser - -# Expose port -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -# Run the application -CMD ["./main"] -"#; - - // New structured logging module for Go with slog - pub const GO_LOGGER: &str = r#"package logger - -import ( - "context" - "log/slog" - "os" - "strings" -) - -// LogFormat represents the logging format -type LogFormat string - -const ( - FormatJSON LogFormat = "json" - FormatText LogFormat = "text" -) - -// Setup initializes structured logging with slog -func Setup() { - logLevel := getLogLevel() - logFormat := getLogFormat() - - var handler slog.Handler - - switch logFormat { - case FormatJSON: - handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: logLevel, - AddSource: true, - }) - default: - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: logLevel, - AddSource: true, - }) - } - - logger := slog.New(handler) - slog.SetDefault(logger) -} - -// getLogLevel returns the log level from environment -func getLogLevel() slog.Level { - level := strings.ToUpper(os.Getenv("LOG_LEVEL")) - switch level { - case "DEBUG": - return slog.LevelDebug - case "INFO": - return slog.LevelInfo - case "WARN", "WARNING": - return slog.LevelWarn - case "ERROR": - return slog.LevelError - default: - return slog.LevelInfo - } -} - -// getLogFormat returns the log format from environment -func getLogFormat() LogFormat { - format := strings.ToLower(os.Getenv("LOG_FORMAT")) - switch format { - case "json": - return FormatJSON - default: - return FormatText - } -} - -// WithRequestID adds request ID to the logger context -func WithRequestID(ctx context.Context, requestID string) context.Context { - logger := slog.Default().With("request_id", requestID) - return context.WithValue(ctx, "logger", logger) -} - -// FromContext returns a logger with context information -func FromContext(ctx context.Context) *slog.Logger { - if logger, ok := ctx.Value("logger").(*slog.Logger); ok { - return logger - } - return slog.Default() -} -"#; -} - -pub mod flask { - pub const APP_INIT_PY: &str = r#"from __future__ import annotations - -from typing import Type -from flask import Flask, request, g -from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate -from flask_jwt_extended import JWTManager -from flask_cors import CORS -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address -import structlog -import uuid -import time -import os - -from app.core.config import Config -from app.core.extensions import db, migrate, jwt, cors, limiter -from app.core.logging import setup_logging -from app.api import health -from app.api.v1 import auth, users - -def create_app(config_class: Type[Config] = Config) -> Flask: - app = Flask(__name__) - app.config.from_object(config_class) - - # Setup structured logging first - setup_logging(app) - logger = structlog.get_logger() - - # Initialize extensions - db.init_app(app) - migrate.init_app(app, db) - jwt.init_app(app) - cors.init_app(app) - limiter.init_app(app) - - # Request ID middleware - @app.before_request - def before_request() -> None: - g.request_id = request.headers.get('X-Request-ID', str(uuid.uuid4())) - g.start_time = time.time() - - logger.info( - \"Request started\", - request_id=g.request_id, - method=request.method, - path=request.path, - remote_addr=request.remote_addr, - user_agent=request.headers.get('User-Agent') - ) - - @app.after_request - def after_request(response): - response.headers['X-Request-ID'] = g.get('request_id', 'unknown') - - duration = time.time() - g.get('start_time', time.time()) - - logger.info( - \"Request completed\", - request_id=g.get('request_id'), - status_code=response.status_code, - duration_ms=round(duration * 1000, 2) - ) - - return response - - # Error handlers with structured logging - @app.errorhandler(404) - def not_found(error): - logger.warning( - \"Resource not found\", - request_id=g.get('request_id'), - path=request.path - ) - return {'error': 'Resource not found', 'request_id': g.get('request_id')}, 404 - - @app.errorhandler(500) - def internal_error(error): - logger.error( - \"Internal server error\", - request_id=g.get('request_id'), - error=str(error) - ) - return {'error': 'Internal server error', 'request_id': g.get('request_id')}, 500 - - # Register blueprints - app.register_blueprint(health.bp) - app.register_blueprint(auth.bp, url_prefix='/api/v1/auth') - app.register_blueprint(users.bp, url_prefix='/api/v1/users') - - @app.route('/') - def index(): - return { - 'message': '{{project_name}} API is running', - 'version': '1.0.0', - 'environment': app.config.get('FLASK_ENV', 'unknown'), - 'request_id': g.get('request_id') - } - - logger.info('{{project_name}} application created successfully') - return app -"#; - - pub const CONFIG_PY: &str = r#"from __future__ import annotations - -import os -from datetime import timedelta -from typing import Dict, Type - -class Config: - \"\"\"Base configuration class with 2025 best practices\"\"\" - - # Basic Flask configuration - SECRET_KEY: str = os.environ.get('SECRET_KEY', '{{secret_key}}') - - # Application settings - PROJECT_NAME: str = '{{project_name}}' - VERSION: str = '1.0.0' - - # Database configuration with connection pooling - SQLALCHEMY_DATABASE_URI: str = os.environ.get( - 'DATABASE_URL', - 'postgresql://user:password@localhost/{{snake_case}}_db' - ) - SQLALCHEMY_TRACK_MODIFICATIONS: bool = False - SQLALCHEMY_ENGINE_OPTIONS: Dict = { - 'pool_size': int(os.environ.get('DB_POOL_SIZE', '20')), - 'pool_timeout': int(os.environ.get('DB_POOL_TIMEOUT', '30')), - 'pool_recycle': int(os.environ.get('DB_POOL_RECYCLE', '3600')), - 'max_overflow': int(os.environ.get('DB_MAX_OVERFLOW', '0')) - } - - # JWT Configuration with enhanced security - JWT_SECRET_KEY: str = os.environ.get('JWT_SECRET_KEY', '{{secret_key}}') - JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta( - minutes=int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRES', '30')) - ) - JWT_REFRESH_TOKEN_EXPIRES: timedelta = timedelta( - days=int(os.environ.get('JWT_REFRESH_TOKEN_EXPIRES', '7')) - ) - JWT_ALGORITHM: str = 'HS256' - JWT_BLACKLIST_ENABLED: bool = True - JWT_BLACKLIST_TOKEN_CHECKS: list = ['access', 'refresh'] - - # CORS with enhanced security - CORS_ORIGINS: list = os.environ.get( - 'ALLOWED_ORIGINS', - 'http://localhost:3000,http://127.0.0.1:3000' - ).split(',') - CORS_METHODS: list = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] - CORS_ALLOW_HEADERS: list = ['Content-Type', 'Authorization'] - - # Rate limiting - RATELIMIT_STORAGE_URL: str = os.environ.get('REDIS_URL', 'redis://localhost:6379') - RATELIMIT_DEFAULT: str = \"200 per day, 50 per hour\" - - # Logging configuration - LOG_LEVEL: str = os.environ.get('LOG_LEVEL', 'INFO') - LOG_FORMAT: str = os.environ.get('LOG_FORMAT', 'json') - - # Security headers - SECURITY_HEADERS: Dict = { - 'X-Frame-Options': 'DENY', - 'X-Content-Type-Options': 'nosniff', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Content-Security-Policy': \"default-src 'self'\" - } - - # Environment - FLASK_ENV: str = os.environ.get('FLASK_ENV', 'development') - DEBUG: bool = os.environ.get('FLASK_DEBUG', '0') == '1' - TESTING: bool = False - - @property - def is_development(self) -> bool: - return self.FLASK_ENV == 'development' - - @property - def is_production(self) -> bool: - return self.FLASK_ENV == 'production' - - @property - def is_testing(self) -> bool: - return self.TESTING - -class DevelopmentConfig(Config): - \"\"\"Development configuration\"\"\" - DEBUG: bool = True - LOG_LEVEL: str = 'DEBUG' - -class ProductionConfig(Config): - \"\"\"Production configuration with enhanced security\"\"\" - DEBUG: bool = False - TESTING: bool = False - - # Enhanced security for production - SESSION_COOKIE_SECURE: bool = True - SESSION_COOKIE_HTTPONLY: bool = True - SESSION_COOKIE_SAMESITE: str = 'Lax' - - # Force HTTPS in production - PREFERRED_URL_SCHEME: str = 'https' - -class TestingConfig(Config): - \"\"\"Testing configuration\"\"\" - TESTING: bool = True - DEBUG: bool = True - SQLALCHEMY_DATABASE_URI: str = 'sqlite:///:memory:' - JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta(minutes=5) - - # Disable rate limiting in tests - RATELIMIT_ENABLED: bool = False - -config: Dict[str, Type[Config]] = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig, - 'default': DevelopmentConfig -} -"#; - - pub const EXTENSIONS_PY: &str = r#"from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate -from flask_jwt_extended import JWTManager -from flask_cors import CORS -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address - -# Initialize extensions -db = SQLAlchemy() -migrate = Migrate() -jwt = JWTManager() -cors = CORS() -limiter = Limiter( - key_func=get_remote_address, - default_limits=["200 per day", "50 per hour"] -) -"#; - - pub const SECURITY_PY: &str = r#"from werkzeug.security import generate_password_hash, check_password_hash -from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity -import secrets - -def hash_password(password): - """Hash a password using Werkzeug's secure methods""" - return generate_password_hash(password) - -def verify_password(password, password_hash): - """Verify a password against its hash""" - return check_password_hash(password_hash, password) - -def generate_tokens(identity): - """Generate access and refresh tokens for a user""" - access_token = create_access_token(identity=identity) - refresh_token = create_refresh_token(identity=identity) - return { - 'access_token': access_token, - 'refresh_token': refresh_token, - 'token_type': 'bearer' - } - -def generate_secret_key(): - """Generate a secure secret key""" - return secrets.token_urlsafe(32) -"#; - - pub const MAIN_PY: &str = r#"#!/usr/bin/env python3 -import os -from app import create_app -from app.core.config import config - -# Get environment -config_name = os.getenv('FLASK_ENV', 'development') -app = create_app(config[config_name]) - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=app.config['DEBUG']) -"#; - - pub const HEALTH_BP: &str = r#"from flask import Blueprint, jsonify - -bp = Blueprint('health', __name__) - -@bp.route('/health') -def health_check(): - return jsonify({ - 'status': 'healthy', - 'service': '{{project_name}} API' - }) -"#; - - pub const AUTH_BP: &str = r#"from flask import Blueprint, request, jsonify -from flask_jwt_extended import jwt_required, create_access_token, create_refresh_token, get_jwt_identity -from marshmallow import ValidationError -import logging - -from app.core.extensions import db, limiter -from app.core.security import hash_password, verify_password, generate_tokens -from app.models.user import User -from app.schemas.user import UserRegistrationSchema, UserLoginSchema, TokenRefreshSchema -from app.services.user_service import UserService - -bp = Blueprint('auth', __name__) -logger = logging.getLogger(__name__) - -@bp.route('/register', methods=['POST']) -@limiter.limit("5 per minute") -def register(): - try: - schema = UserRegistrationSchema() - data = schema.load(request.get_json()) - except ValidationError as err: - return jsonify({'errors': err.messages}), 400 - - user_service = UserService() - - # Check if user already exists - if user_service.get_by_email(data['email']): - return jsonify({'message': 'User already exists'}), 409 - - # Create new user - hashed_password = hash_password(data['password']) - user = user_service.create({ - 'email': data['email'], - 'password_hash': hashed_password, - 'full_name': data['full_name'] - }) - - # Generate tokens - tokens = generate_tokens(str(user.id)) - - logger.info(f"New user registered: {user.email}") - return jsonify(tokens), 201 - -@bp.route('/login', methods=['POST']) -@limiter.limit("10 per minute") -def login(): - try: - schema = UserLoginSchema() - data = schema.load(request.get_json()) - except ValidationError as err: - return jsonify({'errors': err.messages}), 400 - - user_service = UserService() - user = user_service.get_by_email(data['email']) - - if not user or not verify_password(data['password'], user.password_hash): - logger.warning(f"Failed login attempt for email: {data['email']}") - return jsonify({'message': 'Invalid credentials'}), 401 - - if not user.is_active: - return jsonify({'message': 'Account is deactivated'}), 403 - - # Generate tokens - tokens = generate_tokens(str(user.id)) - - logger.info(f"User logged in: {user.email}") - return jsonify(tokens), 200 - -@bp.route('/refresh', methods=['POST']) -@jwt_required(refresh=True) -def refresh(): - current_user_id = get_jwt_identity() - - user_service = UserService() - user = user_service.get_by_id(current_user_id) - - if not user or not user.is_active: - return jsonify({'message': 'User not found or inactive'}), 404 - - # Create new access token - new_access_token = create_access_token(identity=current_user_id) - - return jsonify({ - 'access_token': new_access_token, - 'token_type': 'bearer' - }), 200 -"#; - - pub const USERS_BP: &str = r#"from flask import Blueprint, jsonify -from flask_jwt_extended import jwt_required, get_jwt_identity - -from app.services.user_service import UserService -from app.schemas.user import UserResponseSchema - -bp = Blueprint('users', __name__) - -@bp.route('/me', methods=['GET']) -@jwt_required() -def get_current_user(): - current_user_id = get_jwt_identity() - - user_service = UserService() - user = user_service.get_by_id(current_user_id) - - if not user: - return jsonify({'message': 'User not found'}), 404 - - schema = UserResponseSchema() - return jsonify(schema.dump(user)), 200 -"#; - - pub const POSTGRES_CONNECTION: &str = r#"from app.core.extensions import db - -def init_db(app): - """Initialize database tables""" - with app.app_context(): - db.create_all() - -def get_db(): - """Get database session""" - return db.session -"#; - - pub const MYSQL_CONNECTION: &str = r#"from app.core.extensions import db - -def init_db(app): - """Initialize database tables""" - with app.app_context(): - db.create_all() - -def get_db(): - """Get database session""" - return db.session -"#; - - pub const USER_MODEL: &str = r#"from datetime import datetime -import uuid -from sqlalchemy.dialects.postgresql import UUID -from app.core.extensions import db - -class User(db.Model): - __tablename__ = 'users' - - id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - email = db.Column(db.String(120), unique=True, nullable=False, index=True) - password_hash = db.Column(db.String(255), nullable=False) - full_name = db.Column(db.String(100), nullable=False) - is_active = db.Column(db.Boolean, default=True) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': str(self.id), - 'email': self.email, - 'full_name': self.full_name, - 'is_active': self.is_active, - 'created_at': self.created_at.isoformat(), - 'updated_at': self.updated_at.isoformat() - } -"#; - - pub const USER_MODEL_MYSQL: &str = r#"from datetime import datetime -import uuid -from app.core.extensions import db - -class User(db.Model): - __tablename__ = 'users' - - id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) - email = db.Column(db.String(120), unique=True, nullable=False, index=True) - password_hash = db.Column(db.String(255), nullable=False) - full_name = db.Column(db.String(100), nullable=False) - is_active = db.Column(db.Boolean, default=True) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': self.id, - 'email': self.email, - 'full_name': self.full_name, - 'is_active': self.is_active, - 'created_at': self.created_at.isoformat(), - 'updated_at': self.updated_at.isoformat() - } -"#; - - pub const USER_SCHEMA: &str = r#"from marshmallow import Schema, fields, validate, ValidationError - -class UserRegistrationSchema(Schema): - email = fields.Email(required=True) - password = fields.Str(required=True, validate=validate.Length(min=8)) - full_name = fields.Str(required=True, validate=validate.Length(min=2, max=100)) - -class UserLoginSchema(Schema): - email = fields.Email(required=True) - password = fields.Str(required=True) - -class UserResponseSchema(Schema): - id = fields.Str() - email = fields.Email() - full_name = fields.Str() - is_active = fields.Bool() - created_at = fields.DateTime() - updated_at = fields.DateTime() - -class TokenRefreshSchema(Schema): - refresh_token = fields.Str(required=True) -"#; - - pub const USER_SERVICE: &str = r#"from typing import Optional, Dict, Any -from app.core.extensions import db -from app.models.user import User - -class UserService: - def create(self, user_data: Dict[str, Any]) -> User: - """Create a new user""" - user = User( - email=user_data['email'], - password_hash=user_data['password_hash'], - full_name=user_data['full_name'], - is_active=user_data.get('is_active', True) - ) - - db.session.add(user) - db.session.commit() - return user - - def get_by_id(self, user_id: str) -> Optional[User]: - """Get user by ID""" - return User.query.filter_by(id=user_id).first() - - def get_by_email(self, email: str) -> Optional[User]: - """Get user by email""" - return User.query.filter_by(email=email).first() - - def update(self, user_id: str, update_data: Dict[str, Any]) -> Optional[User]: - """Update user""" - user = self.get_by_id(user_id) - if not user: - return None - - for key, value in update_data.items(): - if hasattr(user, key): - setattr(user, key, value) - - db.session.commit() - return user - - def delete(self, user_id: str) -> bool: - """Delete user""" - user = self.get_by_id(user_id) - if not user: - return False - - db.session.delete(user) - db.session.commit() - return True - - def list_users(self, page: int = 1, per_page: int = 20) -> Dict[str, Any]: - """List users with pagination""" - users = User.query.paginate( - page=page, - per_page=per_page, - error_out=False - ) - - return { - 'users': [user.to_dict() for user in users.items], - 'total': users.total, - 'pages': users.pages, - 'page': page, - 'per_page': per_page - } -"#; - - pub const DECORATORS: &str = r#"from functools import wraps -from flask import jsonify -from flask_jwt_extended import get_jwt_identity, jwt_required -from app.services.user_service import UserService - -def admin_required(f): - @wraps(f) - @jwt_required() - def decorated_function(*args, **kwargs): - current_user_id = get_jwt_identity() - user_service = UserService() - user = user_service.get_by_id(current_user_id) - - if not user or not getattr(user, 'is_admin', False): - return jsonify({'message': 'Admin access required'}), 403 - - return f(*args, **kwargs) - return decorated_function - -def active_user_required(f): - @wraps(f) - @jwt_required() - def decorated_function(*args, **kwargs): - current_user_id = get_jwt_identity() - user_service = UserService() - user = user_service.get_by_id(current_user_id) - - if not user or not user.is_active: - return jsonify({'message': 'Account is inactive'}), 403 - - return f(*args, **kwargs) - return decorated_function -"#; - - pub const REQUIREMENTS_TXT: &str = r#"# Core Framework - Latest 2025 versions -Flask==3.1.0 -Flask-SQLAlchemy==3.2.0 -Flask-Migrate==4.1.0 -Flask-JWT-Extended==4.7.1 -Flask-CORS==5.0.0 -Flask-Limiter==3.8.0 - -# Database drivers -psycopg2-binary==2.9.10 -SQLAlchemy==2.0.36 - -# Structured Logging -structlog==24.4.0 -python-json-logger==2.0.7 - -# Serialization & Validation -marshmallow==3.23.0 -marshmallow-sqlalchemy==1.1.0 - -# Security & Password hashing -Werkzeug==3.1.0 -bcrypt==4.2.0 - -# Production server -gunicorn==23.0.0 - -# Configuration & Environment -python-dotenv==1.0.1 - -# Caching & Storage -redis==5.1.1 - -# Development & Testing -pytest==8.3.3 -pytest-flask==1.3.0 -pytest-cov==5.0.0 -factory-boy==3.3.1 - -# Type checking & Code quality -mypy==1.13.0 -ruff==0.7.4 -pre-commit==4.0.1 - -# Monitoring (optional) -prometheus-flask-exporter==0.24.0 -"#; - - pub const REQUIREMENTS_TXT_MYSQL: &str = r#"Flask==3.0.0 -Flask-SQLAlchemy==3.1.1 -Flask-Migrate==4.0.5 -Flask-JWT-Extended==4.6.0 -Flask-CORS==4.0.0 -Flask-Limiter==3.5.0 -PyMySQL==1.1.0 -cryptography==41.0.7 -marshmallow==3.20.1 -Werkzeug==3.0.1 -gunicorn==21.2.0 -python-dotenv==1.0.0 -redis==5.0.1 -pytest==7.4.3 -pytest-flask==1.3.0 -"#; - - pub const DOCKERFILE: &str = r#"# ========================= -# Build stage -# ========================= -FROM python:3.12-slim AS builder - -ENV VENV_PATH=/opt/venv -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - libpq-dev \ - && python -m venv $VENV_PATH \ - && rm -rf /var/lib/apt/lists/* - -ENV PATH="$VENV_PATH/bin:$PATH" - -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# ========================= -# Runtime stage -# ========================= -FROM python:3.11-slim - -ENV VENV_PATH=/opt/venv \ - PATH="/opt/venv/bin:$PATH" \ - PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 \ - FLASK_ENV=production - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq5 \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Création user non-root sans shell -RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin appuser - -WORKDIR /app - -# Copier seulement le venv depuis le builder -COPY --from=builder $VENV_PATH $VENV_PATH -COPY . . - -# Répertoires nécessaires -RUN mkdir -p /app/logs /app/instance \ - && chown -R appuser:appuser /app - -USER appuser - -# Healthcheck basique -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/health || exit 1 - -EXPOSE 5000 - -# Variables Gunicorn configurables -ENV WORKERS=4 \ - TIMEOUT=120 - -CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:5000 --workers ${WORKERS} --timeout ${TIMEOUT} run:app"] -"#; - - pub const DOCKERFILE_MYSQL: &str = r#"# ========================= -# Build stage -# ========================= -FROM python:3.11-slim AS builder - -ENV VENV_PATH=/opt/venv -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - pkg-config \ - default-libmysqlclient-dev \ - && python -m venv $VENV_PATH \ - && rm -rf /var/lib/apt/lists/* - -ENV PATH="$VENV_PATH/bin:$PATH" - -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# ========================= -# Runtime stage -# ========================= -FROM python:3.11-slim - -ENV VENV_PATH=/opt/venv \ - PATH="/opt/venv/bin:$PATH" \ - PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 \ - FLASK_ENV=production - -RUN apt-get update && apt-get install -y --no-install-recommends \ - default-mysql-client \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Création user non-root sans shell -RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin appuser - -WORKDIR /app - -# Copier seulement le venv depuis le builder -COPY --from=builder $VENV_PATH $VENV_PATH -COPY . . - -# Répertoires nécessaires -RUN mkdir -p /app/logs /app/instance \ - && chown -R appuser:appuser /app - -USER appuser - -# Healthcheck basique -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/health || exit 1 - -EXPOSE 5000 - -# Variables Gunicorn configurables -ENV WORKERS=4 \ - TIMEOUT=120 - -CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:5000 --workers ${WORKERS} --timeout ${TIMEOUT} run:app"] -"#; - - pub const DOCKER_COMPOSE_YML: &str = r#"services: - {{kebab_case}}-api: - build: . - ports: - - "5000:5000" - environment: - - FLASK_ENV=production - - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/{{snake_case}}_db - - REDIS_URL=redis://redis:6379 - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - depends_on: - - postgres - - redis - volumes: - - ./logs:/app/logs - restart: unless-stopped - networks: - - {{kebab_case}}-network - - postgres: - image: postgres:15 - environment: - - POSTGRES_DB={{snake_case}}_db - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - networks: - - {{kebab_case}}-network - - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - restart: unless-stopped - networks: - - {{kebab_case}}-network - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - depends_on: - - {{kebab_case}}-api - restart: unless-stopped - networks: - - {{kebab_case}}-network - -volumes: - postgres_data: - redis_data: - -networks: - {{kebab_case}}-network: - driver: bridge -"#; - - pub const DOCKER_COMPOSE_YML_MYSQL: &str = r#"services: - {{kebab_case}}-api: - build: . - ports: - - "5000:5000" - environment: - - FLASK_ENV=production - - DATABASE_URL=mysql+pymysql://root:${MYSQL_ROOT_PASSWORD}@mysql:3306/{{snake_case}}_db - - REDIS_URL=redis://redis:6379 - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - depends_on: - - mysql - - redis - volumes: - - ./logs:/app/logs - restart: unless-stopped - networks: - - {{kebab_case}}-network - - mysql: - image: mysql:8.0 - environment: - - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - - MYSQL_DATABASE={{snake_case}}_db - - MYSQL_USER=app_user - - MYSQL_PASSWORD=${MYSQL_PASSWORD} - volumes: - - mysql_data:/var/lib/mysql - restart: unless-stopped - networks: - - {{kebab_case}}-network - command: --default-authentication-plugin=mysql_native_password - - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - restart: unless-stopped - networks: - - {{kebab_case}}-network - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - depends_on: - - {{kebab_case}}-api - restart: unless-stopped - networks: - - {{kebab_case}}-network - -volumes: - mysql_data: - redis_data: - -networks: - {{kebab_case}}-network: - driver: bridge -"#; - - pub const NGINX_CONF: &str = r#"# Main Nginx configuration -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; - use epoll; - multi_accept on; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Basic settings - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - client_max_body_size 16M; - - # Security headers - add_header X-Frame-Options DENY always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always; - - # Rate limiting - limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/json - application/javascript - application/xml+rss - application/atom+xml; - - # Logging format - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - # Include server configurations - include /etc/nginx/conf.d/*.conf; -} -"#; - - pub const NGINX_DEFAULT_CONF: &str = r#"# Default server configuration for {{project_name}} -upstream {{kebab_case}}_api { - server {{kebab_case}}-api:5000; - keepalive 32; -} - -server { - listen 80; - server_name _; - - # Apply rate limiting - limit_req zone=api burst=20 nodelay; - - # API proxy - location /api/ { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } - - # Health check endpoint - location /health { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - access_log off; - } - - # Root endpoint - location / { - proxy_pass http://{{kebab_case}}_api; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } -} -"#; - - pub const TEST_CONFIG: &str = r#"import pytest -import tempfile -import os -from app import create_app -from app.core.extensions import db -from app.core.config import TestingConfig - -@pytest.fixture -def app(): - """Create application for the tests.""" - db_fd, db_path = tempfile.mkstemp() - - app = create_app(TestingConfig) - app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' - - with app.app_context(): - db.create_all() - yield app - db.drop_all() - - os.close(db_fd) - os.unlink(db_path) - -@pytest.fixture -def client(app): - """Create a test client for the app.""" - return app.test_client() - -@pytest.fixture -def runner(app): - """Create a test runner for the app's Click commands.""" - return app.test_cli_runner() -"#; - - pub const TEST_MAIN: &str = r#"def test_index(client): - """Test the index endpoint.""" - response = client.get('/') - assert response.status_code == 200 - data = response.get_json() - assert data['message'] == '{{project_name}} API is running' - assert data['version'] == '1.0.0' - -def test_health_check(client): - """Test the health check endpoint.""" - response = client.get('/health') - assert response.status_code == 200 - data = response.get_json() - assert data['status'] == 'healthy' - assert data['service'] == '{{project_name}} API' -"#; - - pub const TEST_AUTH: &str = r#"import json -import pytest - -def test_register_user(client): - """Test user registration.""" - response = client.post('/api/v1/auth/register', - data=json.dumps({ - 'email': 'test@example.com', - 'password': 'testpassword123', - 'full_name': 'Test User' - }), - content_type='application/json') - - assert response.status_code == 201 - data = response.get_json() - assert 'access_token' in data - assert 'refresh_token' in data - assert data['token_type'] == 'bearer' - -def test_login_user(client): - """Test user login.""" - # First register a user - client.post('/api/v1/auth/register', - data=json.dumps({ - 'email': 'login@example.com', - 'password': 'testpassword123', - 'full_name': 'Login User' - }), - content_type='application/json') - - # Then login - response = client.post('/api/v1/auth/login', - data=json.dumps({ - 'email': 'login@example.com', - 'password': 'testpassword123' - }), - content_type='application/json') - - assert response.status_code == 200 - data = response.get_json() - assert 'access_token' in data - assert 'refresh_token' in data - -def test_invalid_login(client): - """Test invalid login.""" - response = client.post('/api/v1/auth/login', - data=json.dumps({ - 'email': 'nonexistent@example.com', - 'password': 'wrongpassword' - }), - content_type='application/json') - - assert response.status_code == 401 - data = response.get_json() - assert data['message'] == 'Invalid credentials' - -def test_duplicate_registration(client): - """Test registering with existing email.""" - user_data = { - 'email': 'duplicate@example.com', - 'password': 'testpassword123', - 'full_name': 'Duplicate User' - } - - # Register first time - client.post('/api/v1/auth/register', - data=json.dumps(user_data), - content_type='application/json') - - # Try to register again - response = client.post('/api/v1/auth/register', - data=json.dumps(user_data), - content_type='application/json') - - assert response.status_code == 409 - data = response.get_json() - assert data['message'] == 'User already exists' -"#; - - // New structured logging module for Flask - pub const FLASK_LOGGING_PY: &str = r#"from __future__ import annotations - -import logging -import logging.config -import structlog -from typing import Dict, Any -from flask import Flask, has_request_context, g - -def setup_logging(app: Flask) -> None: - """Configure structured logging for Flask with structlog""" - - log_level = app.config.get('LOG_LEVEL', 'INFO') - log_format = app.config.get('LOG_FORMAT', 'json') - - # Configure structlog - if log_format == 'json': - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt='iso'), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - add_flask_context, - structlog.processors.JSONRenderer() - ] - else: - processors = [ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S'), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - add_flask_context, - structlog.dev.ConsoleRenderer() - ] - - structlog.configure( - processors=processors, - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) - - # Configure standard library logging - logging.basicConfig( - format='%(message)s', - level=getattr(logging, log_level.upper()), - force=True, - ) - - # Disable werkzeug logs in production - if not app.debug: - logging.getLogger('werkzeug').setLevel(logging.WARNING) - -def add_flask_context(logger, method_name: str, event_dict: Dict[str, Any]) -> Dict[str, Any]: - """Add Flask request context to log events""" - if has_request_context(): - event_dict['request_id'] = getattr(g, 'request_id', None) - return event_dict -"#; -} - -pub mod php { - // Laravel Templates with Clean Architecture - pub const LARAVEL_COMPOSER_JSON: &str = r#"{ - "name": "{{kebab_case}}/{{kebab_case}}", - "type": "project", - "description": "{{project_name}} - Laravel application with Clean Architecture", - "keywords": ["laravel", "clean-architecture", "ddd", "api"], - "license": "MIT", - "require": { - "php": "^8.2", - "laravel/framework": "^11.0", - "laravel/sanctum": "^4.0", - "tymon/jwt-auth": "^2.0", - "guzzlehttp/guzzle": "^7.2", - "spatie/laravel-data": "^4.0", - "spatie/laravel-query-builder": "^5.0" - }, - "require-dev": { - "fakerphp/faker": "^1.23", - "laravel/pint": "^1.0", - "laravel/sail": "^1.18", - "mockery/mockery": "^1.4.4", - "nunomaduro/collision": "^8.0", - "phpunit/phpunit": "^10.0", - "spatie/laravel-ignition": "^2.0", - "larastan/larastan": "^2.0" - }, - "autoload": { - "psr-4": { - "App\\": "app/", - "Database\\Factories\\": "database/factories/", - "Database\\Seeders\\": "database/seeders/" - } - }, - "autoload-dev": { - "psr-4": { - "Tests\\": "tests/" - } - }, - "scripts": { - "post-autoload-dump": [ - "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", - "@php artisan package:discover --ansi" - ], - "post-update-cmd": [ - "@php artisan vendor:publish --tag=laravel-assets --ansi --force" - ], - "post-root-package-install": [ - "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" - ], - "post-create-project-cmd": [ - "@php artisan key:generate --ansi" - ], - "test": "phpunit", - "test-coverage": "phpunit --coverage-html coverage", - "pint": "pint", - "stan": "phpstan analyse" - }, - "extra": { - "laravel": { - "dont-discover": [] - } - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true, - "php-http/discovery": true - } - }, - "minimum-stability": "stable", - "prefer-stable": true -} -"#; - - pub const LARAVEL_ARTISAN: &str = r#"#!/usr/bin/env php -make(Illuminate\Contracts\Console\Kernel::class); - -$status = $kernel->handle( - $input = new Symfony\Component\Console\Input\ArgvInput, - new Symfony\Component\Console\Output\ConsoleOutput -); - -$kernel->terminate($input, $status); - -exit($status); -"#; - - pub const LARAVEL_APP_CONFIG: &str = r#" env('APP_NAME', '{{project_name}}'), - - /* - |-------------------------------------------------------------------------- - | Application Environment - |-------------------------------------------------------------------------- - */ - - 'env' => env('APP_ENV', 'production'), - - /* - |-------------------------------------------------------------------------- - | Application Debug Mode - |-------------------------------------------------------------------------- - */ - - 'debug' => (bool) env('APP_DEBUG', false), - - /* - |-------------------------------------------------------------------------- - | Application URL - |-------------------------------------------------------------------------- - */ - - 'url' => env('APP_URL', 'http://localhost'), - - 'asset_url' => env('ASSET_URL'), - - /* - |-------------------------------------------------------------------------- - | Application Timezone - |-------------------------------------------------------------------------- - */ - - 'timezone' => 'UTC', - - /* - |-------------------------------------------------------------------------- - | Application Locale Configuration - |-------------------------------------------------------------------------- - */ - - 'locale' => 'en', - - 'fallback_locale' => 'en', - - 'faker_locale' => 'en_US', - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - */ - - 'key' => env('APP_KEY'), - - 'cipher' => 'AES-256-CBC', - - /* - |-------------------------------------------------------------------------- - | Maintenance Mode Driver - |-------------------------------------------------------------------------- - */ - - 'maintenance' => [ - 'driver' => 'file', - ], - - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - */ - - 'providers' => ServiceProvider::defaultProviders()->merge([ - /* - * Package Service Providers... - */ - Tymon\JWTAuth\Providers\LaravelServiceProvider::class, - - /* - * Application Service Providers... - */ - App\Infrastructure\Providers\AppServiceProvider::class, - App\Infrastructure\Providers\AuthServiceProvider::class, - App\Infrastructure\Providers\EventServiceProvider::class, - App\Infrastructure\Providers\RouteServiceProvider::class, - ])->toArray(), - - /* - |-------------------------------------------------------------------------- - | Class Aliases - |-------------------------------------------------------------------------- - */ - - 'aliases' => Facade::defaultAliases()->merge([ - 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, - 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class, - ])->toArray(), - -]; -"#; - - pub const LARAVEL_DATABASE_CONFIG: &str = r#" env('DB_CONNECTION', 'mysql'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - */ - - 'connections' => [ - - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - ], - - 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', '{{snake_case}}_db'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], - ], - - 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', '{{snake_case}}_db'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - 'search_path' => 'public', - 'sslmode' => 'prefer', - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - */ - - 'migrations' => 'migrations', - - /* - |-------------------------------------------------------------------------- - | Redis Databases - |-------------------------------------------------------------------------- - */ - - 'redis' => [ - - 'client' => env('REDIS_CLIENT', 'phpredis'), - - 'options' => [ - 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), - ], - - 'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_DB', '0'), - ], - - 'cache' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_CACHE_DB', '1'), - ], - - ], - -]; -"#; - - pub const LARAVEL_AUTH_CONFIG: &str = r#" [ - 'guard' => 'api', - 'passwords' => 'users', - ], - - /* - |-------------------------------------------------------------------------- - | Authentication Guards - |-------------------------------------------------------------------------- - */ - - 'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], - - 'api' => [ - 'driver' => 'jwt', - 'provider' => 'users', - ], - ], - - /* - |-------------------------------------------------------------------------- - | User Providers - |-------------------------------------------------------------------------- - */ - - 'providers' => [ - 'users' => [ - 'driver' => 'eloquent', - 'model' => App\Domain\User\Entities\User::class, - ], - ], - - /* - |-------------------------------------------------------------------------- - | Resetting Passwords - |-------------------------------------------------------------------------- - */ - - 'passwords' => [ - 'users' => [ - 'provider' => 'users', - 'table' => 'password_reset_tokens', - 'expire' => 60, - 'throttle' => 60, - ], - ], - - /* - |-------------------------------------------------------------------------- - | Password Confirmation Timeout - |-------------------------------------------------------------------------- - */ - - 'password_timeout' => 10800, - -]; -"#; - - // User Entity (Domain Layer) - pub const LARAVEL_USER_ENTITY: &str = r#" - */ - protected $fillable = [ - 'name', - 'email', - 'password', - 'email_verified_at', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - - /** - * Get the identifier that will be stored in the subject claim of the JWT. - */ - public function getJWTIdentifier() - { - return $this->getKey(); - } - - /** - * Return a key value array, containing any custom claims to be added to the JWT. - */ - public function getJWTCustomClaims(): array - { - return []; - } - - /** - * Business logic methods - */ - public function isEmailVerified(): bool - { - return !is_null($this->email_verified_at); - } - - public function markEmailAsVerified(): void - { - if (is_null($this->email_verified_at)) { - $this->email_verified_at = now(); - $this->save(); - } - } - - public function getDisplayName(): string - { - return $this->name ?? $this->email; - } -} -"#; - - // User Repository Interface (Domain Layer) - pub const LARAVEL_USER_REPOSITORY: &str = r#"userRepository->existsByEmail($userData['email'])) { - throw ValidationException::withMessages([ - 'email' => ['A user with this email already exists.'] - ]); - } - - $userData['password'] = Hash::make($userData['password']); - - return $this->userRepository->create($userData); - } - - public function updateUser(User $user, array $userData): User - { - if (isset($userData['email']) && - $userData['email'] !== $user->email && - $this->userRepository->existsByEmail($userData['email'])) { - throw ValidationException::withMessages([ - 'email' => ['A user with this email already exists.'] - ]); - } - - if (isset($userData['password'])) { - $userData['password'] = Hash::make($userData['password']); - } - - return $this->userRepository->update($user, $userData); - } - - public function getUserById(int $id): ?User - { - return $this->userRepository->findById($id); - } - - public function getUserByEmail(string $email): ?User - { - return $this->userRepository->findByEmail($email); - } - - public function deleteUser(User $user): bool - { - return $this->userRepository->delete($user); - } - - public function getPaginatedUsers(int $perPage = 15) - { - return $this->userRepository->paginate($perPage); - } -} -"#; - - // Auth Service (Domain Layer) - pub const LARAVEL_AUTH_SERVICE: &str = r#"userRepository->findByEmail($email); - - if (!$user || !Hash::check($password, $user->password)) { - throw ValidationException::withMessages([ - 'email' => ['The provided credentials are incorrect.'] - ]); - } - - $token = JWTAuth::fromUser($user); - - return [ - 'access_token' => $token, - 'token_type' => 'bearer', - 'expires_in' => config('jwt.ttl') * 60, - 'user' => $user->only(['id', 'name', 'email', 'email_verified_at']) - ]; - } - - public function register(array $userData): array - { - if ($this->userRepository->existsByEmail($userData['email'])) { - throw ValidationException::withMessages([ - 'email' => ['A user with this email already exists.'] - ]); - } - - $userData['password'] = Hash::make($userData['password']); - $user = $this->userRepository->create($userData); - - $token = JWTAuth::fromUser($user); - - return [ - 'access_token' => $token, - 'token_type' => 'bearer', - 'expires_in' => config('jwt.ttl') * 60, - 'user' => $user->only(['id', 'name', 'email', 'email_verified_at']) - ]; - } - - public function logout(): void - { - JWTAuth::invalidate(JWTAuth::getToken()); - } - - public function refresh(): string - { - return JWTAuth::refresh(JWTAuth::getToken()); - } - - public function me(): User - { - return JWTAuth::parseToken()->authenticate(); - } -} -"#; - - // Create User Command (Application Layer) - pub const LARAVEL_CREATE_USER_COMMAND: &str = r#" $this->name, - 'email' => $this->email, - 'password' => $this->password, - 'email_verified_at' => $this->emailVerifiedAt, - ]; - } -} -"#; - - // Get User Query (Application Layer) - pub const LARAVEL_GET_USER_QUERY: &str = r#"userService->createUser($command->toArray()); - } - - public function handleGetUser(GetUserQuery $query): ?User - { - return $this->userService->getUserById($query->userId); - } -} -"#; - - // Login Command (Application Layer) - pub const LARAVEL_LOGIN_COMMAND: &str = r#"authService->login($command->email, $command->password); - } - - public function handleRegister(CreateUserCommand $command): array - { - return $this->authService->register($command->toArray()); - } - - public function handleLogout(): void - { - $this->authService->logout(); - } - - public function handleRefresh(): string - { - return $this->authService->refresh(); - } - - public function handleMe() - { - return $this->authService->me(); - } -} -"#; - - // Eloquent User Repository (Infrastructure Layer) - pub const LARAVEL_ELOQUENT_USER_REPOSITORY: &str = r#"first(); - } - - public function create(array $data): User - { - return User::create($data); - } - - public function update(User $user, array $data): User - { - $user->update($data); - return $user->fresh(); - } - - public function delete(User $user): bool - { - return $user->delete(); - } - - public function paginate(int $perPage = 15): LengthAwarePaginator - { - return User::paginate($perPage); - } - - public function existsByEmail(string $email): bool - { - return User::where('email', $email)->exists(); - } -} -"#; - - // User Controller (Infrastructure Layer) - pub const LARAVEL_USER_CONTROLLER: &str = r#"json(['message' => 'Users list']); - } - - public function show(int $id): JsonResponse - { - $query = new GetUserQuery($id); - $user = $this->userHandler->handleGetUser($query); - - if (!$user) { - return response()->json(['message' => 'User not found'], 404); - } - - return response()->json([ - 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) - ]); - } - - public function store(CreateUserRequest $request): JsonResponse - { - $command = new CreateUserCommand( - name: $request->validated('name'), - email: $request->validated('email'), - password: $request->validated('password') - ); - - $user = $this->userHandler->handleCreateUser($command); - - return response()->json([ - 'message' => 'User created successfully', - 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) - ], 201); - } -} -"#; - - // Auth Controller (Infrastructure Layer) - pub const LARAVEL_AUTH_CONTROLLER: &str = r#"validated('name'), - email: $request->validated('email'), - password: $request->validated('password') - ); - - $result = $this->authHandler->handleRegister($command); - - return response()->json([ - 'message' => 'User registered successfully', - 'data' => $result - ], 201); - } catch (ValidationException $e) { - return response()->json([ - 'message' => 'Validation failed', - 'errors' => $e->errors() - ], 422); - } - } - - public function login(LoginRequest $request): JsonResponse - { - try { - $command = new LoginCommand( - email: $request->validated('email'), - password: $request->validated('password') - ); - - $result = $this->authHandler->handleLogin($command); - - return response()->json([ - 'message' => 'Login successful', - 'data' => $result - ]); - } catch (ValidationException $e) { - return response()->json([ - 'message' => 'Invalid credentials', - 'errors' => $e->errors() - ], 401); - } - } - - public function logout(): JsonResponse - { - $this->authHandler->handleLogout(); - - return response()->json([ - 'message' => 'Successfully logged out' - ]); - } - - public function refresh(): JsonResponse - { - $token = $this->authHandler->handleRefresh(); - - return response()->json([ - 'access_token' => $token, - 'token_type' => 'bearer', - 'expires_in' => config('jwt.ttl') * 60 - ]); - } - - public function me(): JsonResponse - { - $user = $this->authHandler->handleMe(); - - return response()->json([ - 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) - ]); - } -} -"#; - - // Register Request (Infrastructure Layer) - pub const LARAVEL_REGISTER_REQUEST: &str = r#" ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], - 'password' => ['required', 'string', 'min:8', 'confirmed'], - ]; - } - - public function messages(): array - { - return [ - 'name.required' => 'Name is required', - 'email.required' => 'Email is required', - 'email.email' => 'Email must be a valid email address', - 'email.unique' => 'This email is already registered', - 'password.required' => 'Password is required', - 'password.min' => 'Password must be at least 8 characters', - 'password.confirmed' => 'Password confirmation does not match', - ]; - } -} -"#; - - // Login Request (Infrastructure Layer) - pub const LARAVEL_LOGIN_REQUEST: &str = r#" ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]; - } - - public function messages(): array - { - return [ - 'email.required' => 'Email is required', - 'email.email' => 'Email must be a valid email address', - 'password.required' => 'Password is required', - ]; - } -} -"#; - - // API Routes - pub const LARAVEL_API_ROUTES: &str = r#"json([ - 'status' => 'healthy', - 'service' => '{{project_name}} API', - 'version' => '1.0.0', - 'timestamp' => now()->toISOString() - ]); -}); - -Route::prefix('v1')->group(function () { - // Authentication routes - Route::prefix('auth')->group(function () { - Route::post('register', [AuthController::class, 'register']); - Route::post('login', [AuthController::class, 'login']); - - Route::middleware('auth:api')->group(function () { - Route::post('logout', [AuthController::class, 'logout']); - Route::post('refresh', [AuthController::class, 'refresh']); - Route::get('me', [AuthController::class, 'me']); - }); - }); - - // Protected routes - Route::middleware('auth:api')->group(function () { - Route::apiResource('users', UserController::class); - }); -}); -"#; - - // App Service Provider - pub const LARAVEL_APP_SERVICE_PROVIDER: &str = r#"app->bind(UserRepositoryInterface::class, EloquentUserRepository::class); - } - - /** - * Bootstrap any application services. - */ - public function boot(): void - { - // - } -} -"#; - - // Multi-stage Dockerfile for PHP - #[allow(dead_code)] - pub const PHP_DOCKERFILE: &str = r#"# ========================= -# Build stage -# ========================= -FROM composer:2.6 AS composer - -COPY composer.json composer.lock ./ -RUN composer install --no-dev --no-scripts --optimize-autoloader --no-interaction - -# ========================= -# Runtime stage -# ========================= -FROM php:8.2-fpm-alpine AS runtime - -# Install system dependencies -RUN apk add --no-cache \ - nginx \ - postgresql-dev \ - libpng-dev \ - libjpeg-turbo-dev \ - freetype-dev \ - zip \ - libzip-dev \ - icu-dev \ - oniguruma-dev \ - curl \ - git \ - supervisor - -# Install PHP extensions -RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install -j$(nproc) \ - gd \ - pdo \ - pdo_pgsql \ - pdo_mysql \ - zip \ - intl \ - mbstring \ - opcache \ - bcmath - -# Install Redis extension -RUN apk add --no-cache $PHPIZE_DEPS \ - && pecl install redis \ - && docker-php-ext-enable redis \ - && apk del $PHPIZE_DEPS - -# Create application user -RUN addgroup -g 1000 -S www && \ - adduser -u 1000 -S www -G www - -# Set working directory -WORKDIR /var/www/html - -# Copy composer dependencies -COPY --from=composer --chown=www:www /app/vendor ./vendor - -# Copy application code -COPY --chown=www:www . . - -# Copy configuration files -COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf -COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf -COPY docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf -COPY docker/php/php.ini /usr/local/etc/php/php.ini - -# Create necessary directories and set permissions -RUN mkdir -p /var/www/html/storage/logs \ - /var/www/html/storage/framework/cache \ - /var/www/html/storage/framework/sessions \ - /var/www/html/storage/framework/views \ - /var/www/html/bootstrap/cache \ - /run/nginx \ - /var/log/supervisor \ - && chown -R www:www /var/www/html/storage \ - && chown -R www:www /var/www/html/bootstrap/cache \ - && chmod -R 775 /var/www/html/storage \ - && chmod -R 775 /var/www/html/bootstrap/cache - -# Copy supervisor configuration -COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Switch to www user -USER www - -# Expose port -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost/health || exit 1 - -# Start supervisor -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -"#; - - // Laravel Docker Compose - pub const LARAVEL_DOCKER_COMPOSE: &str = r#"services: - app: - build: - context: . - dockerfile: docker/php/Dockerfile - target: production - container_name: {{kebab_case}}-app - env_file: - - .env.docker - environment: - - APP_NAME={{project_name}} - - DB_CONNECTION=pgsql - - DB_HOST=postgres - - DB_PORT=5432 - - DB_DATABASE={{snake_case}}_db - - REDIS_HOST=redis - - REDIS_PORT=6379 - - CACHE_DRIVER=redis - - SESSION_DRIVER=redis - - QUEUE_CONNECTION=redis - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_started - volumes: - - app_data:/var/www/html - - app_logs:/var/www/html/storage/logs - networks: - - {{kebab_case}}-network - restart: unless-stopped - healthcheck: - test: ["CMD", "php", "artisan", "app:health-check"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - nginx: - build: - context: . - dockerfile: docker/nginx/Dockerfile - container_name: {{kebab_case}}-nginx - ports: - - "80:80" - - "443:443" - volumes: - - app_data:/var/www/html:ro - - nginx_logs:/var/log/nginx - depends_on: - app: - condition: service_healthy - networks: - - {{kebab_case}}-network - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/api/health"] - interval: 30s - timeout: 10s - retries: 3 - - postgres: - image: postgres:16-alpine - container_name: {{kebab_case}}-postgres - env_file: - - .env.docker - environment: - - POSTGRES_DB={{snake_case}}_db - - POSTGRES_USER=postgres - volumes: - - postgres_data:/var/lib/postgresql/data - expose: - - "5432" - networks: - - {{kebab_case}}-network - restart: unless-stopped - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres", "-d", "{{snake_case}}_db"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 30s - - redis: - image: redis:7-alpine - container_name: {{kebab_case}}-redis - volumes: - - redis_data:/data - - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro - expose: - - "6379" - networks: - - {{kebab_case}}-network - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 3 - command: redis-server /usr/local/etc/redis/redis.conf - -volumes: - postgres_data: - redis_data: - app_data: - app_logs: - nginx_logs: - -networks: - {{kebab_case}}-network: - driver: bridge -"#; - - // Nginx Configuration - pub const PHP_NGINX_CONF: &str = r#"user www; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /run/nginx.pid; - -events { - worker_connections 1024; - use epoll; - multi_accept on; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Logging - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - # Basic Settings - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - client_max_body_size 16M; - - # Gzip Settings - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/json - application/javascript - application/xml+rss - application/atom+xml; - - # Security Headers - add_header X-Frame-Options DENY always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - # Include server configurations - include /etc/nginx/http.d/*.conf; -} -"#; - - // Laravel Nginx Default Configuration - pub const LARAVEL_NGINX_DEFAULT_CONF: &str = r#"server { - listen 80; - server_name _; - root /var/www/html/public; - index index.php index.html; - - # Security - server_tokens off; - - # Logging - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - # Laravel-specific configuration - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - # Handle PHP files - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - - # FastCGI settings - fastcgi_connect_timeout 60; - fastcgi_send_timeout 180; - fastcgi_read_timeout 180; - fastcgi_buffer_size 128k; - fastcgi_buffers 4 256k; - fastcgi_busy_buffers_size 256k; - fastcgi_temp_file_write_size 256k; - fastcgi_intercept_errors on; - } - - # Deny access to sensitive files - location ~ /\.(?!well-known).* { - deny all; - } - - # Static files caching - location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } -} -"#; - - // PHP-FPM Configuration - pub const PHP_FPM_CONF: &str = r#"[www] -user = www -group = www - -listen = 127.0.0.1:9000 -listen.owner = www -listen.group = www -listen.mode = 0660 - -pm = dynamic -pm.max_children = 20 -pm.start_servers = 2 -pm.min_spare_servers = 1 -pm.max_spare_servers = 3 -pm.max_requests = 1000 - -; Logging -access.log = /var/log/php-fpm.access.log -access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%" - -; Environment variables -env[PATH] = /usr/local/bin:/usr/bin:/bin -env[TMP] = /tmp -env[TMPDIR] = /tmp -env[TEMP] = /tmp - -; PHP admin values -php_admin_value[error_log] = /var/log/php-fpm.error.log -php_admin_flag[log_errors] = on -php_admin_value[memory_limit] = 256M -php_admin_value[upload_max_filesize] = 16M -php_admin_value[post_max_size] = 16M -php_admin_value[max_execution_time] = 120 -php_admin_value[max_input_time] = 120 -"#; - - // Laravel Environment Example - pub const LARAVEL_ENV_EXAMPLE: &str = r#"APP_NAME={{project_name}} -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost - -LOG_CHANNEL=stack -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=pgsql -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_DATABASE={{snake_case}}_db -DB_USERNAME=postgres -DB_PASSWORD= - -BROADCAST_DRIVER=log -CACHE_DRIVER=redis -FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=redis -SESSION_LIFETIME=120 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=smtp -MAIL_HOST=mailpit -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -JWT_SECRET= -JWT_TTL=60 -JWT_REFRESH_TTL=20160 - -VITE_APP_NAME="${APP_NAME}" -"#; - - // Laravel PHPUnit Configuration - pub const LARAVEL_PHPUNIT_XML: &str = r#" - - - - tests/Unit - - - tests/Feature - - - tests/Integration - - - - - app - - - - - - - - - - - - - - -"#; - - // Laravel Auth Feature Test - pub const LARAVEL_AUTH_FEATURE_TEST: &str = r#" 'John Doe', - 'email' => 'john@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]; - - $response = $this->postJson('/api/v1/auth/register', $userData); - - $response->assertStatus(201) - ->assertJsonStructure([ - 'message', - 'data' => [ - 'access_token', - 'token_type', - 'expires_in', - 'user' => [ - 'id', - 'name', - 'email', - ] - ] - ]); - - $this->assertDatabaseHas('users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - ]); - } - - public function test_user_can_login_with_valid_credentials(): void - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => bcrypt('password123'), - ]); - - $loginData = [ - 'email' => 'john@example.com', - 'password' => 'password123', - ]; - - $response = $this->postJson('/api/v1/auth/login', $loginData); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'message', - 'data' => [ - 'access_token', - 'token_type', - 'expires_in', - 'user' - ] - ]); - } - - public function test_user_cannot_login_with_invalid_credentials(): void - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => bcrypt('password123'), - ]); - - $loginData = [ - 'email' => 'john@example.com', - 'password' => 'wrongpassword', - ]; - - $response = $this->postJson('/api/v1/auth/login', $loginData); - - $response->assertStatus(401) - ->assertJson([ - 'message' => 'Invalid credentials', - ]); - } - - public function test_authenticated_user_can_get_profile(): void - { - $user = User::factory()->create(); - $token = auth('api')->login($user); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token, - ])->getJson('/api/v1/auth/me'); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'data' => [ - 'id', - 'name', - 'email', - ] - ]); - } - - public function test_authenticated_user_can_logout(): void - { - $user = User::factory()->create(); - $token = auth('api')->login($user); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token, - ])->postJson('/api/v1/auth/logout'); - - $response->assertStatus(200) - ->assertJson([ - 'message' => 'Successfully logged out', - ]); - } -} -"#; - - // Laravel User Unit Test - pub const LARAVEL_USER_UNIT_TEST: &str = r#" now(), - ]); - - $userWithoutVerifiedEmail = new User([ - 'email_verified_at' => null, - ]); - - $this->assertTrue($userWithVerifiedEmail->isEmailVerified()); - $this->assertFalse($userWithoutVerifiedEmail->isEmailVerified()); - } - - public function test_user_can_get_display_name(): void - { - $userWithName = new User([ - 'name' => 'John Doe', - 'email' => 'john@example.com', - ]); - - $userWithoutName = new User([ - 'name' => null, - 'email' => 'jane@example.com', - ]); - - $this->assertEquals('John Doe', $userWithName->getDisplayName()); - $this->assertEquals('jane@example.com', $userWithoutName->getDisplayName()); - } - - public function test_user_can_mark_email_as_verified(): void - { - $user = new User([ - 'email_verified_at' => null, - ]); - - $this->assertFalse($user->isEmailVerified()); - - $user->markEmailAsVerified(); - - $this->assertTrue($user->isEmailVerified()); - $this->assertNotNull($user->email_verified_at); - } -} -"#; - - // Laravel README - pub const LARAVEL_README: &str = r#"# {{project_name}} - -Production-ready Laravel application with Clean Architecture, Domain-Driven Design (DDD), and comprehensive JWT authentication. - -## Architecture - -This project follows **Clean Architecture** principles with clear separation of concerns: - -### Directory Structure - -``` -app/ -├── Domain/ # Business logic and entities -│ ├── User/ -│ │ ├── Entities/ # Domain entities (User.php) -│ │ ├── Repositories/ # Repository interfaces -│ │ └── Services/ # Domain services -│ └── Auth/ -│ └── Services/ # Authentication domain services -├── Application/ # Use cases and application logic -│ ├── User/ -│ │ ├── Commands/ # Command objects -│ │ ├── Queries/ # Query objects -│ │ └── Handlers/ # Command/Query handlers -│ └── Auth/ -│ ├── Commands/ -│ └── Handlers/ -└── Infrastructure/ # External concerns - ├── Http/ - │ ├── Controllers/ # API controllers - │ └── Requests/ # Form requests - └── Persistence/ - └── Eloquent/ # Repository implementations -``` - -## Features - -- **Clean Architecture** with Domain-Driven Design -- **JWT Authentication** with access tokens -- **Repository Pattern** for data access abstraction -- **Command/Query Separation** (CQRS-lite) -- **Docker** containerization with Nginx + PHP-FPM -- **PostgreSQL** database with migrations -- **Redis** for caching and sessions -- **Comprehensive Testing** (Unit, Feature, Integration) -- **Code Quality** tools (PHPStan, Pint) - -## Quick Start - -### With Docker (Recommended) - -```bash -# Clone and setup -git clone -cd {{kebab_case}} - -# Environment setup -cp .env.example .env -# Edit .env with your configuration - -# Build and start containers -docker-compose up --build -d - -# Install dependencies -docker-compose exec app composer install - -# Generate application key -docker-compose exec app php artisan key:generate - -# Generate JWT secret -docker-compose exec app php artisan jwt:secret - -# Run migrations -docker-compose exec app php artisan migrate - -# The API will be available at http://localhost:8000 -``` - -### Local Development - -```bash -# Install dependencies -composer install - -# Environment setup -cp .env.example .env -php artisan key:generate -php artisan jwt:secret - -# Database setup -php artisan migrate - -# Start development server -php artisan serve -``` - -## API Documentation - -### Authentication Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/api/v1/auth/register` | User registration | -| POST | `/api/v1/auth/login` | User login | -| POST | `/api/v1/auth/logout` | User logout | -| POST | `/api/v1/auth/refresh` | Refresh token | -| GET | `/api/v1/auth/me` | Get current user | - -### User Endpoints (Protected) - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/v1/users` | List users | -| GET | `/api/v1/users/{id}` | Get user by ID | -| POST | `/api/v1/users` | Create user | - -### System Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/health` | Health check | - -## Testing - -```bash -# Run all tests -composer test - -# Run with coverage -composer test-coverage - -# Run specific test suite -./vendor/bin/phpunit --testsuite=Unit -./vendor/bin/phpunit --testsuite=Feature -./vendor/bin/phpunit --testsuite=Integration -``` - -## Code Quality - -```bash -# Fix code style -composer pint - -# Run static analysis -composer stan - -# Run all quality checks -composer pint && composer stan && composer test -``` - -## Docker Services - -- **app**: PHP 8.2-FPM with Laravel application -- **nginx**: Nginx web server (reverse proxy) -- **postgres**: PostgreSQL 16 database -- **redis**: Redis for caching and sessions - -## Security Features - -- JWT token-based authentication -- Password hashing with bcrypt -- CORS configuration -- Security headers (X-Frame-Options, CSP, etc.) -- Input validation and sanitization -- SQL injection prevention with Eloquent ORM - -## Environment Variables - -Key environment variables to configure: - -```env -APP_NAME={{project_name}} -APP_ENV=production -APP_KEY=base64:... -APP_URL=https://yourdomain.com - -DB_CONNECTION=pgsql -DB_HOST=postgres -DB_DATABASE={{snake_case}}_db -DB_USERNAME=postgres -DB_PASSWORD=your-secure-password - -JWT_SECRET=your-jwt-secret -JWT_TTL=60 - -REDIS_HOST=redis -REDIS_PORT=6379 -``` - -## Deployment - -1. Set up your production environment -2. Configure environment variables -3. Build Docker images -4. Deploy with docker-compose or Kubernetes -5. Run migrations: `php artisan migrate --force` - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Add tests for new features -4. Ensure code quality checks pass -5. Submit a pull request - ---- - -Generated by [Athena CLI](https://github.com/Jeck0v/Athena) -"#; - - // Symfony templates will be added here... - // For now, we'll add placeholder constants to avoid compilation errors - - pub const SYMFONY_COMPOSER_JSON: &str = r#"{ - "name": "{{kebab_case}}/{{kebab_case}}", - "type": "project", - "description": "{{project_name}} - Symfony application with Hexagonal Architecture", - "keywords": ["symfony", "hexagonal-architecture", "ddd", "api"], - "license": "MIT", - "require": { - "php": "^8.2", - "symfony/framework-bundle": "^7.0", - "symfony/security-bundle": "^7.0", - "symfony/console": "^7.0", - "symfony/dotenv": "^7.0", - "symfony/flex": "^2.0", - "symfony/runtime": "^7.0", - "symfony/yaml": "^7.0", - "symfony/maker-bundle": "^1.0", - "symfony/orm-pack": "^2.0", - "symfony/validator": "^7.0", - "symfony/serializer": "^7.0", - "symfony/property-access": "^7.0", - "symfony/property-info": "^7.0", - "lexik/jwt-authentication-bundle": "^3.0", - "doctrine/orm": "^3.0", - "doctrine/doctrine-bundle": "^2.0", - "doctrine/doctrine-migrations-bundle": "^3.0", - "gesdinet/jwt-refresh-token-bundle": "^1.0", - "nelmio/cors-bundle": "^2.0", - "ramsey/uuid": "^4.0", - "symfony/uid": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/phpunit-bridge": "^7.0", - "symfony/test-pack": "^1.0", - "phpstan/phpstan": "^1.0", - "friendsofphp/php-cs-fixer": "^3.0", - "doctrine/doctrine-fixtures-bundle": "^3.0" - }, - "autoload": { - "psr-4": { - "App\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "App\\Tests\\": "tests/" - } - }, - "scripts": { - "auto-scripts": { - "cache:clear": "symfony-cmd", - "assets:install %PUBLIC_DIR%": "symfony-cmd" - }, - "post-install-cmd": [ - "@auto-scripts" - ], - "post-update-cmd": [ - "@auto-scripts" - ], - "test": "phpunit", - "cs-fix": "php-cs-fixer fix", - "stan": "phpstan analyse" - }, - "extra": { - "symfony": { - "allow-contrib": false, - "require": "7.0.*" - } - }, - "config": { - "allow-plugins": { - "composer/package-versions-deprecated": true, - "symfony/flex": true, - "symfony/runtime": true - }, - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true - }, - "minimum-stability": "stable", - "prefer-stable": true -} -"#; - - pub const SYMFONY_SERVICES_YAML: &str = r#"services: - _defaults: - autowire: true - autoconfigure: true - public: false - - App\: - resource: '../src/' - exclude: - - '../src/DependencyInjection/' - - '../src/Domain/*/Entities/' - - '../src/Domain/*/ValueObjects/' - - '../src/Kernel.php' - - # Domain Services - App\Domain\User\Repositories\UserRepositoryInterface: - alias: App\Infrastructure\Persistence\Doctrine\Repositories\DoctrineUserRepository - - # Application Services - App\Application\User\Services\: - resource: '../src/Application/User/Services/' - tags: ['app.application_service'] - - # Infrastructure Services - App\Infrastructure\: - resource: '../src/Infrastructure/' - exclude: - - '../src/Infrastructure/Persistence/Doctrine/Entities/' - - # Controllers - App\Infrastructure\Http\Controllers\: - resource: '../src/Infrastructure/Http/Controllers/' - tags: ['controller.service_arguments'] - - # Security - App\Infrastructure\Security\: - resource: '../src/Infrastructure/Security/' - - # Event Handlers - App\Application\User\EventHandlers\: - resource: '../src/Application/User/EventHandlers/' - tags: [kernel.event_listener] -"#; - - // Placeholder constants for Symfony (to be implemented) - pub const SYMFONY_DOCTRINE_CONFIG: &str = r#"doctrine: - dbal: - url: '%env(resolve:DATABASE_URL)%' - driver: 'pdo_{{database_driver}}' - server_version: '{{database_version}}' - charset: utf8mb4 - default_table_options: - charset: utf8mb4 - collate: utf8mb4_unicode_ci - - orm: - auto_generate_proxy_classes: true - enable_lazy_ghost_objects: true - naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware - auto_mapping: true - mappings: - App: - type: attribute - is_bundle: false - dir: '%kernel.project_dir%/src/Infrastructure/Persistence/Doctrine/Entities' - prefix: 'App\Infrastructure\Persistence\Doctrine\Entities' - alias: App - -when@prod: - doctrine: - orm: - auto_generate_proxy_classes: false - proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' - query_cache_driver: - type: pool - pool: doctrine.query_cache_pool - result_cache_driver: - type: pool - pool: doctrine.result_cache_pool - - framework: - cache: - pools: - doctrine.query_cache_pool: - adapter: cache.app - doctrine.result_cache_pool: - adapter: cache.app -"#; - pub const SYMFONY_SECURITY_CONFIG: &str = r#"security: - password_hashers: - App\Infrastructure\Persistence\Doctrine\Entities\User: - algorithm: auto - - providers: - app_user_provider: - entity: - class: App\Infrastructure\Persistence\Doctrine\Entities\User - property: email - - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - - api_login: - pattern: ^/api/auth/login - stateless: true - json_login: - check_path: /api/auth/login - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - - api_register: - pattern: ^/api/auth/register - stateless: true - security: false - - api: - pattern: ^/api - stateless: true - jwt: ~ - - main: - lazy: true - provider: app_user_provider - - access_control: - - { path: ^/api/auth, roles: PUBLIC_ACCESS } - - { path: ^/api/health, roles: PUBLIC_ACCESS } - - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } - -when@test: - security: - password_hashers: - App\Infrastructure\Persistence\Doctrine\Entities\User: - algorithm: auto - cost: 4 - time_cost: 3 - memory_cost: 10 -"#; - pub const SYMFONY_USER_ENTITY: &str = r#"id; - } - - public function getEmail(): Email - { - return $this->email; - } - - public function getName(): UserName - { - return $this->name; - } - - public function getPassword(): HashedPassword - { - return $this->password; - } - - public function isActive(): bool - { - return $this->isActive; - } - - public function activate(): void - { - $this->isActive = true; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function deactivate(): void - { - $this->isActive = false; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } -} -"#; - pub const SYMFONY_USER_REPOSITORY: &str = r#"userRepository->findByEmail(new Email($email)); - if ($existingUser) { - throw new \DomainException('User with this email already exists'); - } - - $userId = new UserId(Uuid::v4()->toRfc4122()); - $userEmail = new Email($email); - $userName = new UserName($name); - - // Create a temporary doctrine entity for password hashing - $tempDoctrineUser = new \App\Infrastructure\Persistence\Doctrine\Entities\User( - $userId->getValue(), - $userEmail->getValue(), - $userName->getValue(), - '' - ); - - $hashedPassword = $this->passwordHasher->hashPassword($tempDoctrineUser, $plainPassword); - $password = new HashedPassword($hashedPassword); - - $user = new User($userId, $userEmail, $userName, $password); - - $this->userRepository->save($user); - - return $user; - } - - public function getUserById(string $id): ?User - { - return $this->userRepository->findById(new UserId($id)); - } - - public function getUserByEmail(string $email): ?User - { - return $this->userRepository->findByEmail(new Email($email)); - } - - public function updateUser(User $user): void - { - $this->userRepository->save($user); - } - - public function deleteUser(User $user): void - { - $this->userRepository->delete($user); - } - - public function listUsers(int $limit = 20, int $offset = 0): array - { - return $this->userRepository->findAll($limit, $offset); - } - - public function getTotalUsersCount(): int - { - return $this->userRepository->count(); - } -} -"#; - pub const SYMFONY_AUTH_SERVICE: &str = r#"userService->createUser($email, $name, $plainPassword); - - // Convert to Doctrine entity for JWT token generation - $doctrineUser = \App\Infrastructure\Persistence\Doctrine\Entities\User::fromDomain($user); - $token = $this->jwtManager->create($doctrineUser); - - return [ - 'user' => $user, - 'token' => $token - ]; - } - - public function authenticate(string $email, string $plainPassword): array - { - $user = $this->userService->getUserByEmail($email); - - if (!$user || !$user->isActive()) { - throw new \InvalidArgumentException('Invalid credentials'); - } - - // Convert to Doctrine entity for password verification - $doctrineUser = \App\Infrastructure\Persistence\Doctrine\Entities\User::fromDomain($user); - - if (!$this->passwordHasher->isPasswordValid($doctrineUser, $plainPassword)) { - throw new \InvalidArgumentException('Invalid credentials'); - } - - $token = $this->jwtManager->create($doctrineUser); - - return [ - 'user' => $user, - 'token' => $token - ]; - } -} -"#; - pub const SYMFONY_CREATE_USER_COMMAND: &str = r#"userService->createUser( - $command->email, - $command->name, - $command->password - ); - } - - public function handleGetUser(GetUserQuery $query): ?User - { - return $this->userService->getUserById($query->userId); - } -} -"#; - pub const SYMFONY_LOGIN_COMMAND: &str = r#"authService->authenticate($command->email, $command->password); - } - - public function handleRegister(CreateUserCommand $command): array - { - return $this->authService->register( - $command->email, - $command->name, - $command->password - ); - } -} -"#; - pub const SYMFONY_DOCTRINE_USER_REPOSITORY: &str = r#"findDoctrineUserById($user->getId()->getValue()); - - if ($doctrineUser) { - // Update existing - $doctrineUser = DoctrineUser::fromDomain($user); - } else { - // Create new - $doctrineUser = DoctrineUser::fromDomain($user); - $this->getEntityManager()->persist($doctrineUser); - } - - $this->getEntityManager()->flush(); - } - - public function findById(UserId $id): ?User - { - $doctrineUser = $this->findDoctrineUserById($id->getValue()); - - return $doctrineUser ? $doctrineUser->toDomain() : null; - } - - public function findByEmail(Email $email): ?User - { - $doctrineUser = $this->findOneBy(['email' => $email->getValue()]); - - return $doctrineUser ? $doctrineUser->toDomain() : null; - } - - public function delete(User $user): void - { - $doctrineUser = $this->findDoctrineUserById($user->getId()->getValue()); - - if ($doctrineUser) { - $this->getEntityManager()->remove($doctrineUser); - $this->getEntityManager()->flush(); - } - } - - public function findAll(int $limit = 20, int $offset = 0): array - { - $doctrineUsers = $this->createQueryBuilder('u') - ->setMaxResults($limit) - ->setFirstResult($offset) - ->orderBy('u.createdAt', 'DESC') - ->getQuery() - ->getResult(); - - return array_map(fn(DoctrineUser $user) => $user->toDomain(), $doctrineUsers); - } - - public function count(): int - { - return $this->createQueryBuilder('u') - ->select('COUNT(u.id)') - ->getQuery() - ->getSingleScalarResult(); - } - - private function findDoctrineUserById(string $id): ?DoctrineUser - { - return $this->find($id); - } -} -"#; - #[allow(dead_code)] - pub const SYMFONY_DOCTRINE_USER_ENTITY: &str = r#"id = $id; - $this->email = $email; - $this->name = $name; - $this->password = $password; - $this->createdAt = new \DateTimeImmutable(); - $this->updatedAt = new \DateTimeImmutable(); - } - - public static function fromDomain(DomainUser $domainUser): self - { - $user = new self( - $domainUser->getId()->getValue(), - $domainUser->getEmail()->getValue(), - $domainUser->getName()->getValue(), - $domainUser->getPassword()->getValue() - ); - - $user->isActive = $domainUser->isActive(); - $user->createdAt = $domainUser->getCreatedAt(); - $user->updatedAt = $domainUser->getUpdatedAt(); - - return $user; - } - - public function toDomain(): DomainUser - { - return new DomainUser( - new UserId($this->id), - new Email($this->email), - new UserName($this->name), - new HashedPassword($this->password), - $this->isActive, - $this->createdAt, - $this->updatedAt - ); - } - - public function getId(): string - { - return $this->id; - } - - public function getEmail(): string - { - return $this->email; - } - - public function getName(): string - { - return $this->name; - } - - public function getUserIdentifier(): string - { - return $this->email; - } - - public function getRoles(): array - { - $roles = $this->roles; - $roles[] = 'ROLE_USER'; - - return array_unique($roles); - } - - public function setRoles(array $roles): void - { - $this->roles = $roles; - } - - public function getPassword(): string - { - return $this->password; - } - - public function eraseCredentials(): void - { - // Implement if needed - } - - public function isActive(): bool - { - return $this->isActive; - } - - public function setIsActive(bool $isActive): void - { - $this->isActive = $isActive; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } -} -"#; - - pub const SYMFONY_USER_CONTROLLER: &str = r#"getContent(), true); - - $constraints = new Assert\Collection([ - 'email' => [new Assert\NotBlank(), new Assert\Email()], - 'name' => [new Assert\NotBlank(), new Assert\Length(min: 2, max: 100)], - 'password' => [new Assert\NotBlank(), new Assert\Length(min: 8)] - ]); - - $violations = $this->validator->validate($data, $constraints); - if (count($violations) > 0) { - return $this->json(['errors' => (string) $violations], Response::HTTP_BAD_REQUEST); - } - - try { - $command = new CreateUserCommand($data['email'], $data['name'], $data['password']); - $result = $this->authHandler->handleRegister($command); - - return $this->json([ - 'user' => [ - 'id' => $result['user']->getId()->getValue(), - 'email' => $result['user']->getEmail()->getValue(), - 'name' => $result['user']->getName()->getValue(), - ], - 'token' => $result['token'] - ], Response::HTTP_CREATED); - } catch (\DomainException $e) { - return $this->json(['error' => $e->getMessage()], Response::HTTP_CONFLICT); - } - } - - #[Route('/auth/login', name: 'auth_login', methods: ['POST'])] - public function login(Request $request): JsonResponse - { - $data = json_decode($request->getContent(), true); - - $constraints = new Assert\Collection([ - 'email' => [new Assert\NotBlank(), new Assert\Email()], - 'password' => [new Assert\NotBlank()] - ]); - - $violations = $this->validator->validate($data, $constraints); - if (count($violations) > 0) { - return $this->json(['errors' => (string) $violations], Response::HTTP_BAD_REQUEST); - } - - try { - $command = new LoginCommand($data['email'], $data['password']); - $result = $this->authHandler->handleLogin($command); - - return $this->json([ - 'user' => [ - 'id' => $result['user']->getId()->getValue(), - 'email' => $result['user']->getEmail()->getValue(), - 'name' => $result['user']->getName()->getValue(), - ], - 'token' => $result['token'] - ]); - } catch (\InvalidArgumentException $e) { - return $this->json(['error' => 'Invalid credentials'], Response::HTTP_UNAUTHORIZED); - } - } - - #[Route('/users/{id}', name: 'get_user', methods: ['GET'])] - public function getUser(string $id): JsonResponse - { - $query = new GetUserQuery($id); - $user = $this->userHandler->handleGetUser($query); - - if (!$user) { - return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); - } - - return $this->json([ - 'id' => $user->getId()->getValue(), - 'email' => $user->getEmail()->getValue(), - 'name' => $user->getName()->getValue(), - 'isActive' => $user->isActive(), - 'createdAt' => $user->getCreatedAt()->format('c'), - 'updatedAt' => $user->getUpdatedAt()->format('c'), - ]); - } - - #[Route('/health', name: 'health_check', methods: ['GET'])] - public function healthCheck(): JsonResponse - { - return $this->json([ - 'status' => 'healthy', - 'service' => '{{project_name}} API', - 'timestamp' => (new \DateTimeImmutable())->format('c') - ]); - } -} -"#; - - // Symfony Value Objects - #[allow(dead_code)] - pub const SYMFONY_USER_ID_VALUE_OBJECT: &str = r#"value)) { - throw new \InvalidArgumentException('Invalid user ID format'); - } - } - - public static function generate(): self - { - return new self(Uuid::v4()->toRfc4122()); - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(UserId $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} -"#; - - #[allow(dead_code)] - pub const SYMFONY_EMAIL_VALUE_OBJECT: &str = r#"value, FILTER_VALIDATE_EMAIL)) { - throw new \InvalidArgumentException('Invalid email format'); - } - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(Email $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} -"#; - - #[allow(dead_code)] - pub const SYMFONY_USER_NAME_VALUE_OBJECT: &str = r#"value))) { - throw new \InvalidArgumentException('User name cannot be empty'); - } - - if (strlen($this->value) < 2 || strlen($this->value) > 100) { - throw new \InvalidArgumentException('User name must be between 2 and 100 characters'); - } - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(UserName $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} -"#; - - #[allow(dead_code)] - pub const SYMFONY_HASHED_PASSWORD_VALUE_OBJECT: &str = r#"value)) { - throw new \InvalidArgumentException('Hashed password cannot be empty'); - } - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(HashedPassword $other): bool - { - return $this->value === $other->value; - } -} -"#; - // .env.docker for secure environment variables - pub const PHP_ENV_DOCKER: &str = r#"# Docker Environment Variables -# These should be kept secure and not committed to version control - -# Application secrets -APP_KEY=base64:$(openssl rand -base64 32) -APP_SECRET=$(openssl rand -hex 32) -JWT_SECRET=$(openssl rand -hex 32) -JWT_PASSPHRASE=$(openssl rand -hex 16) - -# Database credentials -DB_PASSWORD=$(openssl rand -hex 16) -POSTGRES_PASSWORD=${DB_PASSWORD} - -# Other secrets -REDIS_PASSWORD= -MAIL_PASSWORD= -"#; - - // Docker Compose Development Override - pub const PHP_DOCKER_COMPOSE_DEV: &str = r#"# Development overrides for docker-compose.yml -# Usage: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up - -services: - app: - build: - target: development - environment: - - APP_ENV=local - - APP_DEBUG=true - volumes: - - .:/var/www/html - - ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini - - adminer: - image: adminer:latest - container_name: {{kebab_case}}-adminer - ports: - - "8080:8080" - environment: - - ADMINER_DEFAULT_SERVER=postgres - depends_on: - postgres: - condition: service_healthy - networks: - - {{kebab_case}}-network - restart: unless-stopped - - postgres: - ports: - - "5432:5432" - - redis: - ports: - - "6379:6379" - - mailhog: - ports: - - "1025:1025" - - "8025:8025" -"#; - - // Optimized Dockerfile for PHP - pub const PHP_OPTIMIZED_DOCKERFILE: &str = r#"# Multi-stage Dockerfile for PHP applications -FROM php:8.2-fpm-alpine AS base - -# Install system dependencies -RUN apk add --no-cache \ - nginx \ - supervisor \ - curl \ - postgresql-dev \ - libpng-dev \ - libjpeg-turbo-dev \ - freetype-dev \ - libzip-dev \ - oniguruma-dev \ - && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install -j$(nproc) gd \ - && docker-php-ext-install pdo pdo_pgsql zip bcmath opcache - -# Install Composer -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /var/www/html - -# Copy composer files -COPY composer.json composer.lock ./ - -# Development stage -FROM base AS development -RUN composer install --prefer-dist --no-scripts --no-autoloader -COPY . . -RUN composer dump-autoload --optimize - -# Production stage -FROM base AS production -RUN composer install --no-dev --optimize-autoloader --no-scripts -COPY . . -RUN composer dump-autoload --optimize --classmap-authoritative \ - && php artisan config:cache \ - && php artisan route:cache \ - && php artisan view:cache - -# Set permissions -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 755 /var/www/html/storage - -# Copy supervisor config -COPY docker/php/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -EXPOSE 9000 - -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -"#; - - // Nginx Dockerfile - pub const PHP_NGINX_DOCKERFILE: &str = r#"FROM nginx:alpine - -# Copy nginx configuration -COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf -COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf - -# Create log directory -RUN mkdir -p /var/log/nginx - -EXPOSE 80 443 - -CMD ["nginx", "-g", "daemon off;"] -"#; - - pub const SYMFONY_AUTH_CONTROLLER: &str = r#""#; - pub const SYMFONY_AUTH_FUNCTIONAL_TEST: &str = r#"load(); -} - -// Error handling -error_reporting(E_ALL); -ini_set('display_errors', '1'); - -// Initialize configuration -AppConfig::load(); - -// Create request object -$request = Request::createFromGlobals(); - -// Apply CORS middleware -$corsMiddleware = new CorsMiddleware(); -$corsMiddleware->handle($request); - -// Initialize router -$router = new Router(); - -// Define API routes -$router->get('/api/v1/health', function() { - return new Response(['status' => 'OK', 'timestamp' => time()], 200); -}); - -// User routes -$router->post('/api/v1/auth/register', 'App\Infrastructure\Http\Controller\Api\V1\AuthController@register'); -$router->post('/api/v1/auth/login', 'App\Infrastructure\Http\Controller\Api\V1\AuthController@login'); -$router->post('/api/v1/auth/logout', 'App\Infrastructure\Http\Controller\Api\V1\AuthController@logout'); -$router->get('/api/v1/auth/me', 'App\Infrastructure\Http\Controller\Api\V1\AuthController@me'); - -$router->get('/api/v1/users', 'App\Infrastructure\Http\Controller\Api\V1\UserController@index'); -$router->get('/api/v1/users/{id}', 'App\Infrastructure\Http\Controller\Api\V1\UserController@show'); -$router->post('/api/v1/users', 'App\Infrastructure\Http\Controller\Api\V1\UserController@store'); - -// Handle the request -try { - $response = $router->handle($request); - $response->send(); -} catch (Exception $e) { - $errorResponse = new Response([ - 'error' => $e->getMessage(), - 'code' => $e->getCode() ?: 500 - ], 500); - $errorResponse->send(); -} -"#; - - pub const VANILLA_HTACCESS: &str = r#"RewriteEngine On - -# Handle Angular and React routing, send all requests to index.php -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^(.*)$ index.php [QSA,L] - -# Security headers -Header always set X-Content-Type-Options nosniff -Header always set X-Frame-Options DENY -Header always set X-XSS-Protection "1; mode=block" -Header always set Referrer-Policy "strict-origin-when-cross-origin" -Header always set Content-Security-Policy "default-src 'self'" - -# CORS headers -Header always set Access-Control-Allow-Origin "*" -Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" -Header always set Access-Control-Allow-Headers "Content-Type, Authorization" - -# Handle preflight requests -RewriteCond %{REQUEST_METHOD} OPTIONS -RewriteRule ^(.*)$ index.php [QSA,L] -"#; - - pub const VANILLA_DATABASE_CONFIG: &str = r#" 'pgsql', - - 'connections' => [ - 'pgsql' => [ - 'driver' => 'pgsql', - 'host' => $_ENV['DB_HOST'] ?? 'localhost', - 'port' => $_ENV['DB_PORT'] ?? '5432', - 'database' => $_ENV['DB_DATABASE'] ?? '{{snake_case}}_db', - 'username' => $_ENV['DB_USERNAME'] ?? 'postgres', - 'password' => $_ENV['DB_PASSWORD'] ?? '', - 'charset' => 'utf8', - 'prefix' => '', - 'schema' => 'public', - 'sslmode' => 'prefer', - ], - - 'mysql' => [ - 'driver' => 'mysql', - 'host' => $_ENV['DB_HOST'] ?? 'localhost', - 'port' => $_ENV['DB_PORT'] ?? '3306', - 'database' => $_ENV['DB_DATABASE'] ?? '{{snake_case}}_db', - 'username' => $_ENV['DB_USERNAME'] ?? 'root', - 'password' => $_ENV['DB_PASSWORD'] ?? '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - ], - ], -]; -"#; - - pub const VANILLA_APP_CONFIG: &str = r#" $_ENV['APP_NAME'] ?? '{{project_name}}', - 'env' => $_ENV['APP_ENV'] ?? 'production', - 'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN), - 'url' => $_ENV['APP_URL'] ?? 'http://localhost:8000', - 'timezone' => $_ENV['APP_TIMEZONE'] ?? 'UTC', - - 'jwt' => [ - 'secret' => $_ENV['JWT_SECRET'] ?? '', - 'ttl' => (int) ($_ENV['JWT_TTL'] ?? 3600), // 1 hour - 'algorithm' => 'HS256', - ], - - 'cors' => [ - 'allowed_origins' => explode(',', $_ENV['CORS_ALLOWED_ORIGINS'] ?? '*'), - 'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - 'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'], - 'expose_headers' => [], - 'max_age' => 86400, - 'supports_credentials' => false, - ], -]; -"#; - - pub const VANILLA_ENV_EXAMPLE: &str = r#"# Application Configuration -APP_NAME={{project_name}} -APP_ENV=production -APP_DEBUG=false -APP_URL=http://localhost:8000 -APP_TIMEZONE=UTC - -# Database Configuration -DB_CONNECTION=pgsql -DB_HOST=postgres -DB_PORT=5432 -DB_DATABASE={{snake_case}}_db -DB_USERNAME=postgres -DB_PASSWORD=your-secure-password - -# JWT Configuration -JWT_SECRET=your-jwt-secret-key -JWT_TTL=3600 - -# CORS Configuration -CORS_ALLOWED_ORIGINS=* - -# Security -BCRYPT_ROUNDS=12 -"#; - - pub const VANILLA_ROUTER: &str = r#"addRoute('GET', $path, $handler); - } - - public function post(string $path, $handler): void - { - $this->addRoute('POST', $path, $handler); - } - - public function put(string $path, $handler): void - { - $this->addRoute('PUT', $path, $handler); - } - - public function delete(string $path, $handler): void - { - $this->addRoute('DELETE', $path, $handler); - } - - public function options(string $path, $handler): void - { - $this->addRoute('OPTIONS', $path, $handler); - } - - public function middleware(string $middleware): self - { - $this->middlewares[] = $middleware; - return $this; - } - - private function addRoute(string $method, string $path, $handler): void - { - $this->routes[] = [ - 'method' => $method, - 'path' => $path, - 'handler' => $handler, - 'middlewares' => $this->middlewares - ]; - $this->middlewares = []; // Reset middlewares for next route - } - - public function handle(Request $request): Response - { - $method = $request->getMethod(); - $path = $request->getPath(); - - foreach ($this->routes as $route) { - if ($route['method'] === $method && $this->matchPath($route['path'], $path)) { - // Apply middlewares - foreach ($route['middlewares'] as $middlewareClass) { - $middleware = new $middlewareClass(); - $middleware->handle($request); - } - - $params = $this->extractParams($route['path'], $path); - return $this->callHandler($route['handler'], $request, $params); - } - } - - return new Response(['error' => 'Route not found'], 404); - } - - private function matchPath(string $routePath, string $requestPath): bool - { - $pattern = preg_replace('/\{[^}]+\}/', '([^/]+)', $routePath); - $pattern = '#^' . $pattern . '$#'; - return preg_match($pattern, $requestPath); - } - - private function extractParams(string $routePath, string $requestPath): array - { - $routeParts = explode('/', $routePath); - $requestParts = explode('/', $requestPath); - $params = []; - - foreach ($routeParts as $index => $part) { - if (strpos($part, '{') === 0 && strpos($part, '}') === strlen($part) - 1) { - $paramName = substr($part, 1, -1); - $params[$paramName] = $requestParts[$index] ?? null; - } - } - - return $params; - } - - private function callHandler($handler, Request $request, array $params): Response - { - if (is_callable($handler)) { - return $handler($request, $params); - } - - if (is_string($handler) && strpos($handler, '@') !== false) { - [$className, $method] = explode('@', $handler); - $controller = new $className(); - return $controller->$method($request, $params); - } - - return new Response(['error' => 'Invalid handler'], 500); - } -} -"#; - - pub const VANILLA_REQUEST: &str = r#"method = strtoupper($method); - $this->path = $path; - $this->query = $query; - $this->body = $body; - $this->headers = $headers; - } - - public static function createFromGlobals(): self - { - $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; - $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); - $query = $_GET; - - $body = []; - if (in_array($method, ['POST', 'PUT', 'PATCH'])) { - $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; - if (strpos($contentType, 'application/json') !== false) { - $input = file_get_contents('php://input'); - $body = json_decode($input, true) ?? []; - } else { - $body = $_POST; - } - } - - $headers = []; - foreach ($_SERVER as $key => $value) { - if (strpos($key, 'HTTP_') === 0) { - $headerName = str_replace('_', '-', substr($key, 5)); - $headers[strtolower($headerName)] = $value; - } - } - - return new self($method, $path, $query, $body, $headers); - } - - public function getMethod(): string - { - return $this->method; - } - - public function getPath(): string - { - return $this->path; - } - - public function getQuery(string $key = null, $default = null) - { - if ($key === null) { - return $this->query; - } - return $this->query[$key] ?? $default; - } - - public function getBody(string $key = null, $default = null) - { - if ($key === null) { - return $this->body; - } - return $this->body[$key] ?? $default; - } - - public function getHeader(string $name): ?string - { - return $this->headers[strtolower($name)] ?? null; - } - - public function getHeaders(): array - { - return $this->headers; - } - - public function hasHeader(string $name): bool - { - return isset($this->headers[strtolower($name)]); - } - - public function getBearerToken(): ?string - { - $authorization = $this->getHeader('authorization'); - if ($authorization && strpos($authorization, 'Bearer ') === 0) { - return substr($authorization, 7); - } - return null; - } -} -"#; - - pub const VANILLA_RESPONSE: &str = r#"data = $data; - $this->statusCode = $statusCode; - $this->headers = array_merge([ - 'Content-Type' => 'application/json', - 'Access-Control-Allow-Origin' => '*', - 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers' => 'Content-Type, Authorization', - ], $headers); - } - - public function send(): void - { - http_response_code($this->statusCode); - - foreach ($this->headers as $name => $value) { - header("$name: $value"); - } - - if ($this->data !== null) { - echo json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - } - - public function withHeader(string $name, string $value): self - { - $this->headers[$name] = $value; - return $this; - } - - public function withStatus(int $statusCode): self - { - $this->statusCode = $statusCode; - return $this; - } - - public static function json($data, int $statusCode = 200): self - { - return new self($data, $statusCode); - } - - public static function success($data = null, string $message = 'Success'): self - { - return new self([ - 'success' => true, - 'message' => $message, - 'data' => $data - ], 200); - } - - public static function error(string $message, int $statusCode = 400, $errors = null): self - { - $response = [ - 'success' => false, - 'message' => $message, - ]; - - if ($errors !== null) { - $response['errors'] = $errors; - } - - return new self($response, $statusCode); - } -} -"#; - - pub const VANILLA_PDO_CONNECTION: &str = r#" PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - ] - ); - } catch (PDOException $e) { - throw new PDOException("Database connection failed: " . $e->getMessage()); - } - } - - return self::$instance; - } - - private static function buildDsn(array $config): string - { - $driver = $config['driver']; - $host = $config['host']; - $port = $config['port']; - $database = $config['database']; - - switch ($driver) { - case 'pgsql': - return "pgsql:host=$host;port=$port;dbname=$database"; - case 'mysql': - $charset = $config['charset'] ?? 'utf8mb4'; - return "mysql:host=$host;port=$port;dbname=$database;charset=$charset"; - default: - throw new \InvalidArgumentException("Unsupported database driver: $driver"); - } - } - - public static function beginTransaction(): void - { - self::getInstance()->beginTransaction(); - } - - public static function commit(): void - { - self::getInstance()->commit(); - } - - public static function rollback(): void - { - self::getInstance()->rollback(); - } -} -"#; - - pub const VANILLA_JWT_MANAGER: &str = r#"secret = $config['secret']; - $this->algorithm = $config['algorithm']; - $this->ttl = $config['ttl']; - } - - public function encode(array $payload): string - { - $now = time(); - $payload = array_merge($payload, [ - 'iat' => $now, - 'exp' => $now + $this->ttl, - 'iss' => AppConfig::get('app.url'), - ]); - - return JWT::encode($payload, $this->secret, $this->algorithm); - } - - public function decode(string $token): array - { - try { - $decoded = JWT::decode($token, new Key($this->secret, $this->algorithm)); - return (array) $decoded; - } catch (ExpiredException $e) { - throw new \InvalidArgumentException('Token has expired'); - } catch (SignatureInvalidException $e) { - throw new \InvalidArgumentException('Invalid token signature'); - } catch (\Exception $e) { - throw new \InvalidArgumentException('Invalid token'); - } - } - - public function validate(string $token): bool - { - try { - $this->decode($token); - return true; - } catch (\Exception $e) { - return false; - } - } - - public function getUserIdFromToken(string $token): ?string - { - try { - $payload = $this->decode($token); - return $payload['user_id'] ?? null; - } catch (\Exception $e) { - return null; - } - } -} -"#; - - pub const VANILLA_USER_ENTITY: &str = r#"id = $id; - $this->name = $name; - $this->email = $email; - $this->passwordHash = $passwordHash; - $this->createdAt = $createdAt ?? new \DateTimeImmutable(); - $this->updatedAt = $updatedAt; - } - - public function getId(): UserId - { - return $this->id; - } - - public function getName(): string - { - return $this->name; - } - - public function getEmail(): Email - { - return $this->email; - } - - public function getPasswordHash(): string - { - return $this->passwordHash; - } - - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } - - public function getUpdatedAt(): ?\DateTimeImmutable - { - return $this->updatedAt; - } - - public function changeName(string $name): void - { - $this->name = $name; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function changeEmail(Email $email): void - { - $this->email = $email; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function changePassword(string $passwordHash): void - { - $this->passwordHash = $passwordHash; - $this->updatedAt = new \DateTimeImmutable(); - } - - public function verifyPassword(string $password): bool - { - return password_verify($password, $this->passwordHash); - } - - public function toArray(): array - { - return [ - 'id' => $this->id->getValue(), - 'name' => $this->name, - 'email' => $this->email->getValue(), - 'created_at' => $this->createdAt->format('c'), - 'updated_at' => $this->updatedAt?->format('c'), - ]; - } -} -"#; - - pub const VANILLA_USER_REPOSITORY_INTERFACE: &str = r#"userRepository = $userRepository; - } - - public function emailExists(Email $email): bool - { - return $this->userRepository->existsByEmail($email); - } - - public function createPasswordHash(string $password): string - { - $cost = (int) ($_ENV['BCRYPT_ROUNDS'] ?? 12); - return password_hash($password, PASSWORD_BCRYPT, ['cost' => $cost]); - } - - public function validatePassword(string $password): array - { - $errors = []; - - if (strlen($password) < 8) { - $errors[] = 'Password must be at least 8 characters long'; - } - - if (!preg_match('/[A-Z]/', $password)) { - $errors[] = 'Password must contain at least one uppercase letter'; - } - - if (!preg_match('/[a-z]/', $password)) { - $errors[] = 'Password must contain at least one lowercase letter'; - } - - if (!preg_match('/[0-9]/', $password)) { - $errors[] = 'Password must contain at least one number'; - } - - return $errors; - } - - public function authenticateUser(Email $email, string $password): ?User - { - $user = $this->userRepository->findByEmail($email); - - if ($user && $user->verifyPassword($password)) { - return $user; - } - - return null; - } -} -"#; - - pub const VANILLA_EMAIL_VALUE_OBJECT: &str = r#"value = strtolower(trim($value)); - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(Email $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} -"#; - - pub const VANILLA_USER_ID_VALUE_OBJECT: &str = r#"value = $value; - } - - public static function generate(): self - { - return new self(Uuid::uuid4()->toString()); - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(UserId $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} -"#; - - pub const VANILLA_CREATE_USER_COMMAND: &str = r#"name = trim($name); - $this->email = trim($email); - $this->password = $password; - } - - public function getName(): string - { - return $this->name; - } - - public function getEmail(): string - { - return $this->email; - } - - public function getPassword(): string - { - return $this->password; - } -} -"#; - - pub const VANILLA_CREATE_USER_HANDLER: &str = r#"userRepository = $userRepository; - $this->userService = $userService; - } - - public function handle(CreateUserCommand $command): User - { - // Validate input - if (empty($command->getName())) { - throw new \InvalidArgumentException('Name is required'); - } - - $email = new Email($command->getEmail()); - - // Check if email already exists - if ($this->userService->emailExists($email)) { - throw new \InvalidArgumentException('Email already exists'); - } - - // Validate password - $passwordErrors = $this->userService->validatePassword($command->getPassword()); - if (!empty($passwordErrors)) { - throw new \InvalidArgumentException('Password validation failed: ' . implode(', ', $passwordErrors)); - } - - // Create user - $user = new User( - UserId::generate(), - $command->getName(), - $email, - $this->userService->createPasswordHash($command->getPassword()) - ); - - $this->userRepository->save($user); - - return $user; - } -} -"#; - - pub const VANILLA_LOGIN_COMMAND: &str = r#"email = trim($email); - $this->password = $password; - } - - public function getEmail(): string - { - return $this->email; - } - - public function getPassword(): string - { - return $this->password; - } -} -"#; - - pub const VANILLA_LOGIN_HANDLER: &str = r#"userService = $userService; - $this->jwtManager = $jwtManager; - } - - public function handle(LoginCommand $command): array - { - if (empty($command->getEmail()) || empty($command->getPassword())) { - throw new \InvalidArgumentException('Email and password are required'); - } - - $email = new Email($command->getEmail()); - $user = $this->userService->authenticateUser($email, $command->getPassword()); - - if (!$user) { - throw new \InvalidArgumentException('Invalid credentials'); - } - - $token = $this->jwtManager->encode([ - 'user_id' => $user->getId()->getValue(), - 'email' => $user->getEmail()->getValue(), - ]); - - return [ - 'token' => $token, - 'user' => $user->toArray(), - ]; - } -} -"#; - - pub const VANILLA_AUTH_CONTROLLER: &str = r#"jwtManager = new JWTManager(); - - $this->createUserHandler = new CreateUserHandler($userRepository, $userService); - $this->loginHandler = new LoginHandler($userService, $this->jwtManager); - } - - public function register(Request $request): Response - { - try { - $data = $request->getBody(); - - $command = new CreateUserCommand( - $data['name'] ?? '', - $data['email'] ?? '', - $data['password'] ?? '' - ); - - $user = $this->createUserHandler->handle($command); - - return Response::success($user->toArray(), 'User created successfully'); - } catch (\InvalidArgumentException $e) { - return Response::error($e->getMessage(), 400); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } - - public function login(Request $request): Response - { - try { - $data = $request->getBody(); - - $command = new LoginCommand( - $data['email'] ?? '', - $data['password'] ?? '' - ); - - $result = $this->loginHandler->handle($command); - - return Response::success($result, 'Login successful'); - } catch (\InvalidArgumentException $e) { - return Response::error($e->getMessage(), 401); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } - - public function logout(Request $request): Response - { - // In a stateless JWT implementation, logout is handled client-side - // You could implement a token blacklist here if needed - return Response::success(null, 'Logout successful'); - } - - public function me(Request $request): Response - { - try { - $token = $request->getBearerToken(); - - if (!$token) { - return Response::error('Token not provided', 401); - } - - $userId = $this->jwtManager->getUserIdFromToken($token); - - if (!$userId) { - return Response::error('Invalid token', 401); - } - - $userRepository = new UserRepository(); - $user = $userRepository->findById(new \App\Domain\User\ValueObject\UserId($userId)); - - if (!$user) { - return Response::error('User not found', 404); - } - - return Response::success($user->toArray()); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } -} -"#; - - pub const VANILLA_USER_CONTROLLER: &str = r#"userRepository = new UserRepository(); - $userService = new UserService($this->userRepository); - $this->createUserHandler = new CreateUserHandler($this->userRepository, $userService); - } - - public function index(Request $request): Response - { - try { - $users = $this->userRepository->findAll(); - $usersArray = array_map(fn($user) => $user->toArray(), $users); - - return Response::success($usersArray); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } - - public function show(Request $request, array $params): Response - { - try { - $userId = new UserId($params['id']); - $user = $this->userRepository->findById($userId); - - if (!$user) { - return Response::error('User not found', 404); - } - - return Response::success($user->toArray()); - } catch (\InvalidArgumentException $e) { - return Response::error('Invalid user ID', 400); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } - - public function store(Request $request): Response - { - try { - $data = $request->getBody(); - - $command = new CreateUserCommand( - $data['name'] ?? '', - $data['email'] ?? '', - $data['password'] ?? '' - ); - - $user = $this->createUserHandler->handle($command); - - return Response::success($user->toArray(), 'User created successfully'); - } catch (\InvalidArgumentException $e) { - return Response::error($e->getMessage(), 400); - } catch (\Exception $e) { - return Response::error('Internal server error', 500); - } - } -} -"#; - - pub const VANILLA_USER_REPOSITORY: &str = r#"pdo = PDOConnection::getInstance(); - } - - public function save(User $user): void - { - $stmt = $this->pdo->prepare( - 'INSERT INTO users (id, name, email, password_hash, created_at, updated_at) - VALUES (:id, :name, :email, :password_hash, :created_at, :updated_at) - ON CONFLICT (id) DO UPDATE SET - name = EXCLUDED.name, - email = EXCLUDED.email, - password_hash = EXCLUDED.password_hash, - updated_at = EXCLUDED.updated_at' - ); - - $stmt->execute([ - 'id' => $user->getId()->getValue(), - 'name' => $user->getName(), - 'email' => $user->getEmail()->getValue(), - 'password_hash' => $user->getPasswordHash(), - 'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s'), - 'updated_at' => $user->getUpdatedAt()?->format('Y-m-d H:i:s'), - ]); - } - - public function findById(UserId $id): ?User - { - $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id'); - $stmt->execute(['id' => $id->getValue()]); - $data = $stmt->fetch(); - - return $data ? $this->mapToUser($data) : null; - } - - public function findByEmail(Email $email): ?User - { - $stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = :email'); - $stmt->execute(['email' => $email->getValue()]); - $data = $stmt->fetch(); - - return $data ? $this->mapToUser($data) : null; - } - - public function findAll(): array - { - $stmt = $this->pdo->query('SELECT * FROM users ORDER BY created_at DESC'); - $results = $stmt->fetchAll(); - - return array_map([$this, 'mapToUser'], $results); - } - - public function delete(UserId $id): void - { - $stmt = $this->pdo->prepare('DELETE FROM users WHERE id = :id'); - $stmt->execute(['id' => $id->getValue()]); - } - - public function existsByEmail(Email $email): bool - { - $stmt = $this->pdo->prepare('SELECT COUNT(*) FROM users WHERE email = :email'); - $stmt->execute(['email' => $email->getValue()]); - - return $stmt->fetchColumn() > 0; - } - - private function mapToUser(array $data): User - { - return new User( - new UserId($data['id']), - $data['name'], - new Email($data['email']), - $data['password_hash'], - new \DateTimeImmutable($data['created_at']), - $data['updated_at'] ? new \DateTimeImmutable($data['updated_at']) : null - ); - } -} -"#; - - pub const VANILLA_AUTH_MIDDLEWARE: &str = r#"jwtManager = new JWTManager(); - } - - public function handle(Request $request): void - { - $token = $request->getBearerToken(); - - if (!$token) { - $this->unauthorized('Token not provided'); - } - - if (!$this->jwtManager->validate($token)) { - $this->unauthorized('Invalid or expired token'); - } - - // Token is valid, continue with the request - } - - private function unauthorized(string $message): void - { - $response = Response::error($message, 401); - $response->send(); - exit; - } -} -"#; - - pub const VANILLA_CORS_MIDDLEWARE: &str = r#"getMethod() === 'OPTIONS') { - $this->sendCorsHeaders($config); - http_response_code(200); - exit; - } - - // Add CORS headers to all responses - $this->sendCorsHeaders($config); - } - - private function sendCorsHeaders(array $config): void - { - $origin = $_SERVER['HTTP_ORIGIN'] ?? '*'; - $allowedOrigins = $config['allowed_origins']; - - if (in_array('*', $allowedOrigins) || in_array($origin, $allowedOrigins)) { - header("Access-Control-Allow-Origin: $origin"); - } - - header('Access-Control-Allow-Methods: ' . implode(', ', $config['allowed_methods'])); - header('Access-Control-Allow-Headers: ' . implode(', ', $config['allowed_headers'])); - - if (!empty($config['expose_headers'])) { - header('Access-Control-Expose-Headers: ' . implode(', ', $config['expose_headers'])); - } - - if ($config['supports_credentials']) { - header('Access-Control-Allow-Credentials: true'); - } - - header('Access-Control-Max-Age: ' . $config['max_age']); - } -} -"#; - - pub const VANILLA_USERS_MIGRATION: &str = r#"-- Create users table -CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NULL -); - --- Create indexes -CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at); -"#; - - pub const VANILLA_README: &str = r#"# {{project_name}} - -Production-ready PHP Vanilla application with Clean Architecture, Domain-Driven Design (DDD), and comprehensive JWT authentication. - -## Architecture - -This project follows **Clean Architecture** principles with clear separation of concerns: - -### Directory Structure - -``` -src/ -├── Domain/ # Business logic and entities -│ ├── User/ -│ │ ├── Entity/ # Domain entities (User.php) -│ │ ├── Repository/ # Repository interfaces -│ │ ├── Service/ # Domain services -│ │ └── ValueObject/ # Value objects (Email, UserId) -│ └── Auth/ -│ └── Service/ # Authentication domain services -├── Application/ # Use cases and application logic -│ ├── User/ -│ │ ├── Command/ # Command objects -│ │ └── Handler/ # Command handlers -│ └── Auth/ -│ ├── Command/ -│ └── Handler/ -└── Infrastructure/ # External concerns - ├── Http/ - │ ├── Controller/ # API controllers - │ └── Middleware/ # HTTP middleware - ├── Persistence/ - │ └── PDO/ # Repository implementations - ├── Security/ # JWT management - └── Database/ # Database connections -``` - -## Features - -- **Clean Architecture** with Domain-Driven Design -- **JWT Authentication** with access tokens -- **Repository Pattern** for data access abstraction -- **Command/Handler Pattern** (CQRS-lite) -- **Docker** containerization with Nginx + PHP-FPM -- **PostgreSQL** database with migrations -- **PSR-4** autoloading -- **Comprehensive Testing** (Unit, Integration, Functional) -- **Code Quality** tools (PHPStan, PHP-CS-Fixer) - -## Quick Start - -### With Docker (Recommended) - -```bash -# Clone and setup -git clone -cd {{kebab_case}} - -# Environment setup -cp .env.example .env -# Edit .env with your configuration - -# Build and start containers -docker-compose up --build -d - -# Install dependencies -docker-compose exec app composer install - -# Run database migrations -docker-compose exec app php database/migrate.php - -# The API will be available at http://localhost:8000 -``` - -### Local Development - -```bash -# Install dependencies -composer install - -# Environment setup -cp .env.example .env -# Edit .env with your configuration - -# Database setup (make sure PostgreSQL is running) -php database/migrate.php - -# Start development server -php -S localhost:8000 public/index.php -``` - -## API Documentation - -### Authentication Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/api/v1/auth/register` | User registration | -| POST | `/api/v1/auth/login` | User login | -| POST | `/api/v1/auth/logout` | User logout | -| GET | `/api/v1/auth/me` | Get current user | - -### User Endpoints (Protected) - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/v1/users` | List users | -| GET | `/api/v1/users/{id}` | Get user by ID | -| POST | `/api/v1/users` | Create user | - -### System Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/v1/health` | Health check | - -## Testing - -```bash -# Run all tests -composer test - -# Run specific test suite -./vendor/bin/phpunit --testsuite=Unit -./vendor/bin/phpunit --testsuite=Integration -./vendor/bin/phpunit --testsuite=Functional -``` - -## Code Quality - -```bash -# Fix code style -composer cs-fix - -# Run static analysis -composer stan - -# Run all quality checks -composer cs-fix && composer stan && composer test -``` - -## Docker Services - -- **app**: PHP 8.2-FPM with vanilla PHP application -- **nginx**: Nginx web server (reverse proxy) -- **postgres**: PostgreSQL 16 database - -## Security Features - -- JWT token-based authentication -- Password hashing with bcrypt -- CORS configuration -- Security headers (X-Frame-Options, CSP, etc.) -- Input validation and sanitization -- SQL injection prevention with prepared statements - -## Environment Variables - -Key environment variables to configure: - -```env -APP_NAME={{project_name}} -APP_ENV=production -APP_URL=https://yourdomain.com - -DB_CONNECTION=pgsql -DB_HOST=postgres -DB_DATABASE={{snake_case}}_db -DB_USERNAME=postgres -DB_PASSWORD=your-secure-password - -JWT_SECRET=your-jwt-secret -JWT_TTL=3600 - -BCRYPT_ROUNDS=12 -``` - -## Deployment - -1. Set up your production environment -2. Configure environment variables -3. Build Docker images -4. Deploy with docker-compose or Kubernetes -5. Run migrations: `php database/migrate.php` - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Add tests for new features -4. Ensure code quality checks pass -5. Submit a pull request - ---- - -Generated by Athena CLI -"#; - - pub const VANILLA_APP_CONFIG_CLASS: &str = r#" include __DIR__ . '/../../../config/app.php', - 'database' => include __DIR__ . '/../../../config/database.php', - ]; - } - } - - public static function get(string $key, $default = null) - { - if (empty(self::$config)) { - self::load(); - } - - $keys = explode('.', $key); - $value = self::$config; - - foreach ($keys as $k) { - if (!isset($value[$k])) { - return $default; - } - $value = $value[$k]; - } - - return $value; - } - - public static function all(): array - { - if (empty(self::$config)) { - self::load(); - } - - return self::$config; - } -} -"#; - - pub const VANILLA_PHPUNIT_XML: &str = r#" - - - - ./tests/Unit - - - ./tests/Integration - - - ./tests/Functional - - - - - ./src - - - -"#; - - pub const VANILLA_USER_TEST: &str = r#"assertEquals($id, $user->getId()); - $this->assertEquals($name, $user->getName()); - $this->assertEquals($email, $user->getEmail()); - $this->assertEquals($passwordHash, $user->getPasswordHash()); - $this->assertInstanceOf(\DateTimeImmutable::class, $user->getCreatedAt()); - $this->assertNull($user->getUpdatedAt()); - } - - public function testUserCanChangePassword(): void - { - $user = $this->createUser(); - $newPasswordHash = password_hash('newpassword123', PASSWORD_BCRYPT); - - $user->changePassword($newPasswordHash); - - $this->assertEquals($newPasswordHash, $user->getPasswordHash()); - $this->assertInstanceOf(\DateTimeImmutable::class, $user->getUpdatedAt()); - } - - public function testUserCanVerifyPassword(): void - { - $password = 'password123'; - $user = $this->createUser($password); - - $this->assertTrue($user->verifyPassword($password)); - $this->assertFalse($user->verifyPassword('wrongpassword')); - } - - public function testUserCanBeConvertedToArray(): void - { - $user = $this->createUser(); - $array = $user->toArray(); - - $this->assertIsArray($array); - $this->assertArrayHasKey('id', $array); - $this->assertArrayHasKey('name', $array); - $this->assertArrayHasKey('email', $array); - $this->assertArrayHasKey('created_at', $array); - $this->assertArrayHasKey('updated_at', $array); - } - - private function createUser(string $password = 'password123'): User - { - return new User( - UserId::generate(), - 'John Doe', - new Email('john@example.com'), - password_hash($password, PASSWORD_BCRYPT) - ); - } -} -"#; - - pub const VANILLA_AUTH_FUNCTIONAL_TEST: &str = r#"assertTrue(true); - } - - public function testUserRegistration(): void - { - // This is a placeholder for user registration functional test - $this->assertTrue(true); - } - - public function testUserLogin(): void - { - // This is a placeholder for user login functional test - $this->assertTrue(true); - } -} -"#; -} diff --git a/src/boilerplate/utils.rs b/src/boilerplate/utils.rs deleted file mode 100644 index eba1f39..0000000 --- a/src/boilerplate/utils.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Utilities for boilerplate generation - -use crate::athena::AthenaError; -use std::fs; -use std::path::Path; - -pub type UtilResult = Result; - -/// Create directory structure recursively -pub fn create_directory_structure(base_path: &Path, dirs: &[&str]) -> UtilResult<()> { - for dir in dirs { - let full_path = base_path.join(dir); - fs::create_dir_all(&full_path) - .map_err(AthenaError::IoError)?; - } - Ok(()) -} - -/// Write file with content, creating parent directories if needed -pub fn write_file>(path: P, content: &str) -> UtilResult<()> { - let path = path.as_ref(); - - // Create parent directories if they don't exist - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(AthenaError::IoError)?; - } - - fs::write(path, content) - .map_err(AthenaError::IoError)?; - - Ok(()) -} - -/// Replace template variables in content -#[allow(dead_code)] -pub fn replace_template_vars(content: &str, vars: &[(&str, &str)]) -> String { - let mut result = content.to_string(); - for (key, value) in vars { - result = result.replace(&format!("{{{{{}}}}}", key), value); - } - result -} - -/// Replace template variables in content with String values -pub fn replace_template_vars_string(content: &str, vars: &[(&str, String)]) -> String { - let mut result = content.to_string(); - for (key, value) in vars { - result = result.replace(&format!("{{{{{}}}}}", key), value); - } - result -} - -/// Generate secure random string for secrets -pub fn generate_secret_key() -> String { - use uuid::Uuid; - format!("{}{}", Uuid::new_v4().to_string().replace("-", ""), Uuid::new_v4().to_string().replace("-", "")) -} - -/// Convert project name to different cases -pub struct ProjectNames { - #[allow(dead_code)] - pub original: String, - pub snake_case: String, - pub kebab_case: String, - pub pascal_case: String, - pub upper_case: String, -} - -impl ProjectNames { - pub fn new(name: &str) -> Self { - let snake_case = name.replace("-", "_").to_lowercase(); - let kebab_case = name.replace("_", "-").to_lowercase(); - let pascal_case = name - .split(['_', '-']) - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } - }) - .collect::(); - let upper_case = snake_case.to_uppercase(); - - Self { - original: name.to_string(), - snake_case, - kebab_case, - pascal_case, - upper_case, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn test_create_directory_structure() { - let temp_dir = TempDir::new().unwrap(); - let base_path = temp_dir.path(); - - let dirs = vec!["src/models", "src/routes", "tests"]; - create_directory_structure(base_path, &dirs).unwrap(); - - assert!(base_path.join("src/models").exists()); - assert!(base_path.join("src/routes").exists()); - assert!(base_path.join("tests").exists()); - } - - #[test] - fn test_replace_template_vars() { - let template = "Hello {{name}}, your project is {{project_name}}!"; - let vars = vec![("name", "Alice"), ("project_name", "MyApp")]; - let result = replace_template_vars(template, &vars); - assert_eq!(result, "Hello Alice, your project is MyApp!"); - } - - #[test] - fn test_project_names() { - let names = ProjectNames::new("my-awesome_project"); - assert_eq!(names.snake_case, "my_awesome_project"); - assert_eq!(names.kebab_case, "my-awesome-project"); - assert_eq!(names.pascal_case, "MyAwesomeProject"); - assert_eq!(names.upper_case, "MY_AWESOME_PROJECT"); - } -} \ No newline at end of file diff --git a/src/cli/args.rs b/src/cli/args.rs index b2b314a..83efe96 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; #[command( name = "athena", version = "0.1.0", - about = "A powerful CLI tool for DSL-based Docker Compose generation and boilerplate creation", + about = "A powerful CLI tool for DSL-based Docker Compose generation", long_about = None )] pub struct Cli { @@ -39,10 +39,6 @@ pub enum Commands { quiet: bool, }, - /// Initialize new project with boilerplate code - #[command(subcommand, alias = "i")] - Init(InitCommands), - /// Validate Athena DSL file syntax #[command(alias = "v")] Validate { @@ -61,144 +57,4 @@ pub enum Commands { #[arg(long)] directives: bool, }, -} - -#[derive(Subcommand, Debug)] -pub enum InitCommands { - /// Initialize FastAPI project with production-ready setup - Fastapi { - /// Project name - #[arg(value_name = "NAME")] - name: String, - - /// Project directory (defaults to project name) - #[arg(short, long, value_name = "DIR")] - directory: Option, - - /// Include MongoDB configuration - #[arg(long)] - with_mongodb: bool, - - /// Include PostgreSQL configuration instead of MongoDB - #[arg(long)] - with_postgresql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, - - /// Initialize Flask project with production-ready setup - Flask { - /// Project name - #[arg(value_name = "NAME")] - name: String, - - /// Project directory (defaults to project name) - #[arg(short, long, value_name = "DIR")] - directory: Option, - - /// Use MySQL database instead of PostgreSQL - #[arg(long)] - with_mysql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, - - /// Initialize Go project with production-ready setup - Go { - /// Project name - #[arg(value_name = "NAME")] - name: String, - - /// Project directory (defaults to project name) - #[arg(short, long, value_name = "DIR")] - directory: Option, - - /// Web framework choice - #[arg(long, value_enum, default_value = "gin")] - framework: GoFramework, - - /// Include MongoDB configuration - #[arg(long)] - with_mongodb: bool, - - /// Include PostgreSQL configuration instead of MongoDB - #[arg(long)] - with_postgresql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, - - /// Generate a Laravel PHP project boilerplate with Clean Architecture - Laravel { - /// Project name - name: String, - - /// Output directory (defaults to project name) - directory: Option, - - /// Include MySQL configuration instead of PostgreSQL - #[arg(long)] - with_mysql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, - - /// Generate a Symfony PHP project boilerplate with Hexagonal Architecture - Symfony { - /// Project name - name: String, - - /// Output directory (defaults to project name) - directory: Option, - - /// Include MySQL configuration instead of PostgreSQL - #[arg(long)] - with_mysql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, - - /// Generate a PHP Vanilla project boilerplate with Clean Architecture - Vanilla { - /// Project name - name: String, - - /// Output directory (defaults to project name) - directory: Option, - - /// Include MySQL configuration instead of PostgreSQL - #[arg(long)] - with_mysql: bool, - - /// Skip Docker files generation - #[arg(long)] - no_docker: bool, - }, -} - -#[derive(clap::ValueEnum, Debug, Clone)] -pub enum GoFramework { - Gin, - Echo, - Fiber, -} - -impl std::fmt::Display for GoFramework { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GoFramework::Gin => write!(f, "gin"), - GoFramework::Echo => write!(f, "echo"), - GoFramework::Fiber => write!(f, "fiber"), - } - } } \ No newline at end of file diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 43e1f40..63ba60c 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -1,13 +1,7 @@ use std::fs; -use std::path::Path; use crate::athena::{generate_docker_compose, parse_athena_file, AthenaError, AthenaResult}; -use crate::boilerplate::{ - generate_fastapi_project, generate_flask_project, generate_go_project, - generate_laravel_project, generate_symfony_project, generate_vanilla_project, - DatabaseType, GoFramework, ProjectConfig, -}; -use crate::cli::args::{Commands, InitCommands}; +use crate::cli::args::Commands; use crate::cli::utils::{auto_detect_ath_file, should_be_verbose}; pub fn execute_command(command: Option, verbose: bool) -> AthenaResult<()> { @@ -29,8 +23,6 @@ pub fn execute_command(command: Option, verbose: bool) -> AthenaResult execute_build(input, output, validate_only, verbose) } - Some(Commands::Init(init_cmd)) => execute_init(init_cmd, verbose), - Some(Commands::Validate { input }) => execute_validate(input, verbose), Some(Commands::Info { @@ -106,215 +98,6 @@ fn execute_build( Ok(()) } -fn execute_init(init_cmd: InitCommands, verbose: bool) -> AthenaResult<()> { - match init_cmd { - InitCommands::Fastapi { - name, - directory, - with_mongodb: _, - with_postgresql, - no_docker, - } => { - if verbose { - println!("Initializing FastAPI project: {}", name); - } - - // Determine database type - let database = if with_postgresql { - DatabaseType::PostgreSQL - } else { - DatabaseType::MongoDB // Default to MongoDB - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: None, // Not applicable for FastAPI - }; - - generate_fastapi_project(&config)?; - Ok(()) - } - - InitCommands::Flask { - name, - directory, - with_mysql, - no_docker, - } => { - let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; - if verbose { - println!("Initializing Flask project with {}: {}", db_type, name); - } - - // Choose database type - let database = if with_mysql { - DatabaseType::MySQL - } else { - DatabaseType::PostgreSQL - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: None, // Not applicable for Flask - }; - - generate_flask_project(&config)?; - Ok(()) - } - - InitCommands::Go { - name, - directory, - framework, - with_mongodb: _, - with_postgresql, - no_docker, - } => { - if verbose { - println!("Initializing Go project: {} with {}", name, framework); - } - - // Determine database type - let database = if with_postgresql { - DatabaseType::PostgreSQL - } else { - DatabaseType::MongoDB // Default to MongoDB - }; - - // Convert framework - let go_framework = match framework { - crate::cli::args::GoFramework::Gin => GoFramework::Gin, - crate::cli::args::GoFramework::Echo => GoFramework::Echo, - crate::cli::args::GoFramework::Fiber => GoFramework::Fiber, - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: Some(go_framework), - }; - - generate_go_project(&config)?; - Ok(()) - } - - InitCommands::Laravel { - name, - directory, - with_mysql, - no_docker, - } => { - let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; - if verbose { - println!("Initializing Laravel project with {}: {}", db_type, name); - } - - // Choose database type - let database = if with_mysql { - DatabaseType::MySQL - } else { - DatabaseType::PostgreSQL - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: None, // Not applicable for Laravel - }; - - generate_laravel_project(&config)?; - Ok(()) - } - - InitCommands::Symfony { - name, - directory, - with_mysql, - no_docker, - } => { - let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; - if verbose { - println!("Initializing Symfony project with {}: {}", db_type, name); - } - - // Choose database type - let database = if with_mysql { - DatabaseType::MySQL - } else { - DatabaseType::PostgreSQL - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: None, // Not applicable for Symfony - }; - - generate_symfony_project(&config)?; - Ok(()) - } - - InitCommands::Vanilla { - name, - directory, - with_mysql, - no_docker, - } => { - let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; - if verbose { - println!("Initializing PHP Vanilla project with {}: {}", db_type, name); - } - - // Choose database type - let database = if with_mysql { - DatabaseType::MySQL - } else { - DatabaseType::PostgreSQL - }; - - // Determine directory - let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); - - let config = ProjectConfig { - name: name.clone(), - directory: project_dir.to_string_lossy().to_string(), - database, - include_docker: !no_docker, - framework: None, // Not applicable for Vanilla - }; - - generate_vanilla_project(&config)?; - Ok(()) - } - } -} fn execute_validate(input: Option, verbose: bool) -> AthenaResult<()> { // Auto-detection of the .ath file diff --git a/src/lib.rs b/src/lib.rs index b49f5ca..0e48cd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod cli; pub mod athena; -pub mod boilerplate; pub use athena::{AthenaConfig, AthenaError, AthenaResult}; pub use cli::Cli; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7f66de8..9a45bb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use std::process; mod cli; mod athena; -mod boilerplate; use cli::{Cli, execute_command}; diff --git a/tests/integration/boilerplate/common_tests.rs b/tests/integration/boilerplate/common_tests.rs deleted file mode 100644 index 1e22e6f..0000000 --- a/tests/integration/boilerplate/common_tests.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_project_already_exists_handling() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "existing_project"; - - // Pre-create the directory - let project_path = temp_dir.path().join(project_name); - fs::create_dir_all(&project_path).expect("Failed to create directory"); - fs::write(project_path.join("existing_file.txt"), "content") - .expect("Failed to create file"); - - let mut cmd = run_init_command("fastapi", project_name, &[]); - cmd.current_dir(&temp_dir); - - // The command might succeed (overwrite) or fail (directory exists) - // This documents the current behavior - let _result = cmd.assert(); - - // Either way, we should get some indication - // If it fails, there should be an error message - // If it succeeds, the directory should contain project files -} - -#[test] -fn test_invalid_project_name() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init") - .arg("fastapi") - .arg(""); // Empty project name - - cmd.assert() - .failure() - .stderr(predicate::str::contains("required").or( - predicate::str::contains("invalid").or( - predicate::str::contains("NAME").or( - predicate::str::contains("cannot be empty") - ) - ) - )); -} - -#[test] -fn test_init_help_commands() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg("--help"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Initialize new project")) - .stdout(predicate::str::contains("fastapi")) - .stdout(predicate::str::contains("flask")) - .stdout(predicate::str::contains("go")); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/fastapi_tests.rs b/tests/integration/boilerplate/fastapi_tests.rs deleted file mode 100644 index f575a27..0000000 --- a/tests/integration/boilerplate/fastapi_tests.rs +++ /dev/null @@ -1,130 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_fastapi_init_basic() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_fastapi_basic"; - - let mut cmd = run_init_command("fastapi", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")) - .stdout(predicate::str::contains(project_name)); - - // Verify project structure was created - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for FastAPI files that we know exist - assert!(project_dir.join("requirements.txt").exists(), "requirements.txt should exist"); - assert!(project_dir.join("Dockerfile").exists(), "Dockerfile should exist"); - assert!(project_dir.join("docker-compose.yml").exists(), "docker-compose.yml should exist"); - assert!(project_dir.join(".env.example").exists(), ".env.example should exist"); - - // Check for app directory structure - assert!(project_dir.join("app").exists(), "app directory should exist"); -} - -#[test] -#[serial] -fn test_fastapi_init_with_postgresql() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_fastapi_postgres"; - - let mut cmd = run_init_command("fastapi", project_name, &["--with-postgresql"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for PostgreSQL-specific files/configuration - let has_postgres_config = check_for_postgres_configuration(&project_dir); - assert!(has_postgres_config, "PostgreSQL configuration should be present"); -} - -#[test] -#[serial] -fn test_fastapi_init_with_mongodb() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_fastapi_mongo"; - - let mut cmd = run_init_command("fastapi", project_name, &["--with-mongodb"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for MongoDB-specific files/configuration - let has_mongo_config = check_for_mongo_configuration(&project_dir); - assert!(has_mongo_config, "MongoDB configuration should be present"); -} - -#[test] -#[serial] -fn test_fastapi_init_no_docker() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_fastapi_no_docker"; - - let mut cmd = run_init_command("fastapi", project_name, &["--no-docker"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Dockerfile should NOT exist - assert!(!project_dir.join("Dockerfile").exists(), - "Dockerfile should not exist with --no-docker"); - assert!(!project_dir.join("docker-compose.yml").exists(), - "docker-compose.yml should not exist with --no-docker"); -} - -#[test] -#[serial] -fn test_fastapi_custom_directory() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "custom_name"; - let custom_dir = "custom_directory_path"; - - let mut cmd = run_init_command("fastapi", project_name, &["--directory", custom_dir]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")); - - // Project should be created in custom directory, not project name - let project_dir = temp_dir.path().join(custom_dir); - assert!(project_dir.exists(), "Custom directory should be created"); - assert!(!temp_dir.path().join(project_name).exists(), - "Project name directory should not exist"); -} - -#[test] -fn test_fastapi_init_help() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg("fastapi").arg("--help"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("FastAPI project")) - .stdout(predicate::str::contains("--with-mongodb")) - .stdout(predicate::str::contains("--with-postgresql")) - .stdout(predicate::str::contains("--no-docker")); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/flask_tests.rs b/tests/integration/boilerplate/flask_tests.rs deleted file mode 100644 index 74a1154..0000000 --- a/tests/integration/boilerplate/flask_tests.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_flask_init_basic() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_flask_basic"; - - let mut cmd = run_init_command("flask", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Flask project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for Flask-specific files - let has_flask_app = project_dir.join("app.py").exists() || - project_dir.join("run.py").exists() || - project_dir.join("wsgi.py").exists() || - project_dir.join("app").join("__init__.py").exists(); - assert!(has_flask_app, "Flask application file should exist"); - - let has_deps = project_dir.join("requirements.txt").exists() || - project_dir.join("Pipfile").exists(); - assert!(has_deps, "Dependencies file should exist"); -} - -#[test] -#[serial] -fn test_flask_init_with_mysql() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_flask_mysql"; - - let mut cmd = run_init_command("flask", project_name, &["--with-mysql"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Flask project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for MySQL-specific configuration - let has_mysql_config = check_for_mysql_configuration(&project_dir); - assert!(has_mysql_config, "MySQL configuration should be present"); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/go_tests.rs b/tests/integration/boilerplate/go_tests.rs deleted file mode 100644 index c36c165..0000000 --- a/tests/integration/boilerplate/go_tests.rs +++ /dev/null @@ -1,73 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_go_init_with_gin() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_go_gin"; - - let mut cmd = run_init_command("go", project_name, &["--framework", "gin"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Go project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for Go-specific files - let has_main = project_dir.join("main.go").exists() || - project_dir.join("cmd").join("main.go").exists(); - assert!(has_main, "Go main file should exist"); - - assert!(project_dir.join("go.mod").exists(), "go.mod file should exist"); - - // Check for Gin-specific imports or configuration - let has_gin_config = check_for_gin_configuration(&project_dir); - assert!(has_gin_config, "Gin framework configuration should be present"); -} - -#[test] -#[serial] -fn test_go_init_with_echo() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_go_echo"; - - let mut cmd = run_init_command("go", project_name, &["--framework", "echo"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Go project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - assert!(project_dir.join("go.mod").exists(), "go.mod file should exist"); - - // Note: Current implementation may default to Gin even when Echo is specified - // This test documents the current behavior rather than enforcing strict Echo usage - let has_go_framework = check_for_gin_configuration(&project_dir) || - check_for_echo_configuration(&project_dir); - assert!(has_go_framework, "Some Go framework configuration should be present"); -} - -#[test] -#[serial] -fn test_go_init_with_fiber() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_go_fiber"; - - let mut cmd = run_init_command("go", project_name, &["--framework", "fiber"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Go project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - assert!(project_dir.join("go.mod").exists(), "go.mod file should exist"); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/laravel_tests.rs b/tests/integration/boilerplate/laravel_tests.rs deleted file mode 100644 index e56e5f2..0000000 --- a/tests/integration/boilerplate/laravel_tests.rs +++ /dev/null @@ -1,235 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_laravel_init_basic() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_basic"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Laravel project")) - .stdout(predicate::str::contains(project_name)); - - // Verify project structure was created - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for Laravel files that we know exist - let expected_files = &[ - "composer.json", - "docker-compose.yml", - ".env.docker.example", - "README.md", - "docker/php/Dockerfile", - "docker/nginx/Dockerfile", - "app/Domain/User/Entities/User.php", - "app/Application/User/Commands/CreateUserCommand.php", - "app/Infrastructure/Http/Controllers/Api/V1/AuthController.php", - ]; - - for file in expected_files { - assert!(project_dir.join(file).exists(), "{} should exist", file); - } -} - -#[test] -#[serial] -fn test_laravel_docker_compose_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_docker"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check docker-compose.yml contains production-ready configuration - let docker_compose_path = project_dir.join("docker-compose.yml"); - assert!(docker_compose_path.exists(), "docker-compose.yml should exist"); - - let docker_compose_content = fs::read_to_string(&docker_compose_path) - .expect("Should be able to read docker-compose.yml"); - - // Check for production-ready features - assert!(docker_compose_content.contains("env_file:"), "Should use env_file for security"); - assert!(docker_compose_content.contains("expose:"), "Should use expose instead of ports for internal services"); - assert!(docker_compose_content.contains("healthcheck:"), "Should have health checks"); - assert!(docker_compose_content.contains("depends_on:"), "Should have service dependencies"); - assert!(docker_compose_content.contains("restart: unless-stopped"), "Should have restart policy"); -} - -#[test] -#[serial] -fn test_laravel_clean_architecture_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_architecture"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for Clean Architecture directories - let architecture_dirs = &[ - "app/Domain", - "app/Domain/User", - "app/Domain/User/Entities", - "app/Domain/User/Repositories", - "app/Domain/User/Services", - "app/Application", - "app/Application/User/Commands", - "app/Application/User/Queries", - "app/Application/User/Handlers", - "app/Infrastructure", - "app/Infrastructure/Http/Controllers", - "app/Infrastructure/Persistence/Eloquent", - ]; - - for dir in architecture_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } -} - -#[test] -#[serial] -fn test_laravel_jwt_authentication() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_jwt"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains JWT dependency - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("tymon/jwt-auth"), "Should include JWT authentication package"); - - // Check for JWT-related files - assert!(project_dir.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php").exists(), - "AuthController should exist"); - - let auth_controller_path = project_dir.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php"); - let auth_content = fs::read_to_string(&auth_controller_path) - .expect("Should be able to read AuthController"); - - assert!(auth_content.contains("login"), "AuthController should have login method"); - assert!(auth_content.contains("register"), "AuthController should have register method"); - assert!(auth_content.contains("logout"), "AuthController should have logout method"); -} - -#[test] -#[serial] -fn test_laravel_environment_security() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_security"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check .env.docker.example exists with secure variables - let env_example_path = project_dir.join(".env.docker.example"); - assert!(env_example_path.exists(), ".env.docker.example should exist"); - - let env_content = fs::read_to_string(&env_example_path) - .expect("Should be able to read .env.docker.example"); - - // Should contain variable templates, not actual secrets - assert!(env_content.contains("APP_KEY="), "Should have APP_KEY template"); - assert!(env_content.contains("DB_PASSWORD="), "Should have DB_PASSWORD template"); - assert!(env_content.contains("JWT_SECRET="), "Should have JWT_SECRET template"); - assert!(env_content.contains("openssl rand"), "Should use openssl for secret generation"); -} - -#[test] -#[serial] -fn test_laravel_testing_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_testing"; - - let mut cmd = run_init_command("laravel", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for testing configuration - assert!(project_dir.join("phpunit.xml").exists(), "phpunit.xml should exist"); - - // Check for test directories - let test_dirs = &[ - "tests/Unit", - "tests/Feature", - "tests/Integration", - ]; - - for dir in test_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } - - // Check composer.json contains testing dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("phpunit/phpunit"), "Should include PHPUnit"); -} - -#[test] -#[serial] -fn test_laravel_no_docker() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_laravel_no_docker"; - - let mut cmd = run_init_command("laravel", project_name, &["--no-docker"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Laravel project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Docker files should NOT exist - assert!(!project_dir.join("docker-compose.yml").exists(), - "docker-compose.yml should not exist with --no-docker"); - assert!(!project_dir.join("docker/php/Dockerfile").exists(), - "Dockerfile should not exist with --no-docker"); - assert!(!project_dir.join(".env.docker.example").exists(), - ".env.docker.example should not exist with --no-docker"); - - // But regular Laravel files should exist - assert!(project_dir.join("composer.json").exists(), "composer.json should exist"); -} - -#[test] -fn test_laravel_init_help() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg("laravel").arg("--help"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Laravel")) - .stdout(predicate::str::contains("--no-docker")); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/mod.rs b/tests/integration/boilerplate/mod.rs deleted file mode 100644 index 036be23..0000000 --- a/tests/integration/boilerplate/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -use assert_cmd::Command; -use predicates::prelude::*; -use std::fs; -use std::path::Path; - -pub mod fastapi_tests; -pub mod flask_tests; -pub mod go_tests; -pub mod common_tests; -pub mod laravel_tests; -pub mod symfony_tests; -pub mod vanilla_tests; - -// Common test utilities for boilerplate generation tests - -pub fn cleanup_project_directory(dir_name: &str) { - if Path::new(dir_name).exists() { - fs::remove_dir_all(dir_name).ok(); - } -} - -pub fn run_init_command(framework: &str, project_name: &str, args: &[&str]) -> Command { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg(framework).arg(project_name); - - for arg in args { - cmd.arg(arg); - } - - cmd -} - -pub fn check_basic_project_structure(project_dir: &Path, expected_files: &[&str]) -> bool { - for file in expected_files { - if !project_dir.join(file).exists() { - return false; - } - } - true -} - -// Helper functions to check for specific configurations - -pub fn check_file_contains_any(project_dir: &Path, file_names: &[&str], patterns: &[&str]) -> bool { - for file_name in file_names { - let file_path = project_dir.join(file_name); - if file_path.exists() { - if let Ok(content) = fs::read_to_string(&file_path) { - for pattern in patterns { - if content.contains(pattern) { - return true; - } - } - } - } - } - false -} - -pub fn check_directory_contains_any(project_dir: &Path, dir_names: &[&str], patterns: &[&str]) -> bool { - for dir_name in dir_names { - let dir_path = project_dir.join(dir_name); - if dir_path.exists() && dir_path.is_dir() { - if let Ok(entries) = fs::read_dir(&dir_path) { - for entry in entries.flatten() { - if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) { - if let Some(file_name) = entry.file_name().to_str() { - if file_name.ends_with(".go") { - if let Ok(content) = fs::read_to_string(entry.path()) { - for pattern in patterns { - if content.contains(pattern) { - return true; - } - } - } - } - } - } - } - } - } - } - false -} - -pub fn check_for_postgres_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "requirements.txt", "pyproject.toml", "docker-compose.yml", - ".env.example", "config.py", "settings.py" - ], &["postgres", "psycopg", "postgresql", "POSTGRES_"]) -} - -pub fn check_for_mongo_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "requirements.txt", "pyproject.toml", "docker-compose.yml", - ".env.example", "config.py", "settings.py" - ], &["mongo", "pymongo", "mongodb", "MONGO_"]) -} - -pub fn check_for_mysql_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "requirements.txt", "Pipfile", "docker-compose.yml", - ".env.example", "config.py", "app.py" - ], &["mysql", "pymysql", "MySQL", "MYSQL_"]) -} - -pub fn check_for_gin_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "main.go", "go.mod", "go.sum" - ], &["gin-gonic", "gin.Default", "gin.Engine"]) || - check_directory_contains_any(project_dir, &["cmd", "internal", "pkg"], &[ - "gin-gonic", "gin.Default", "gin.Engine" - ]) -} - -pub fn check_for_echo_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "main.go", "go.mod", "go.sum" - ], &["labstack/echo", "echo.New", "echo.Echo"]) || - check_directory_contains_any(project_dir, &["cmd", "internal", "pkg"], &[ - "labstack/echo", "echo.New", "echo.Echo" - ]) -} - -// PHP-specific helper functions - -pub fn check_for_laravel_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "composer.json", "config/app.php", "artisan" - ], &["laravel/framework", "Laravel", "Illuminate"]) -} - -pub fn check_for_symfony_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "composer.json", "config/services.yaml", "bin/console" - ], &["symfony/framework-bundle", "Symfony", "symfony/console"]) -} - -pub fn check_for_jwt_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "composer.json", "config/jwt.php", "config/packages/lexik_jwt_authentication.yaml" - ], &["tymon/jwt-auth", "lexik/jwt-authentication-bundle", "JWT_SECRET", "jwt"]) -} - -pub fn check_for_doctrine_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "composer.json", "config/packages/doctrine.yaml" - ], &["doctrine/orm", "doctrine/doctrine-bundle", "doctrine/migrations"]) -} - -pub fn check_for_vanilla_configuration(project_dir: &Path) -> bool { - check_file_contains_any(project_dir, &[ - "composer.json", "public/index.php", "src/Infrastructure/Http/Router.php" - ], &["firebase/php-jwt", "App\\Infrastructure\\Http\\Router", "Clean Architecture"]) -} \ No newline at end of file diff --git a/tests/integration/boilerplate/symfony_tests.rs b/tests/integration/boilerplate/symfony_tests.rs deleted file mode 100644 index 0fe0d2f..0000000 --- a/tests/integration/boilerplate/symfony_tests.rs +++ /dev/null @@ -1,285 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_symfony_init_basic() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_basic"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Symfony project")) - .stdout(predicate::str::contains(project_name)); - - // Verify project structure was created - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for Symfony files that we know exist - let expected_files = &[ - "composer.json", - "docker-compose.yml", - ".env.docker.example", - "README.md", - "docker/php/Dockerfile", - "docker/nginx/Dockerfile", - "src/Domain/User/Entity/User.php", - "src/Application/User/Command/CreateUserCommand.php", - "src/Infrastructure/Http/Controller/Api/V1/AuthController.php", - ]; - - for file in expected_files { - assert!(project_dir.join(file).exists(), "{} should exist", file); - } -} - -#[test] -#[serial] -fn test_symfony_docker_compose_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_docker"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check docker-compose.yml contains production-ready configuration - let docker_compose_path = project_dir.join("docker-compose.yml"); - assert!(docker_compose_path.exists(), "docker-compose.yml should exist"); - - let docker_compose_content = fs::read_to_string(&docker_compose_path) - .expect("Should be able to read docker-compose.yml"); - - // Check for production-ready features - assert!(docker_compose_content.contains("env_file:"), "Should use env_file for security"); - assert!(docker_compose_content.contains("expose:"), "Should use expose instead of ports for internal services"); - assert!(docker_compose_content.contains("healthcheck:"), "Should have health checks"); - assert!(docker_compose_content.contains("depends_on:"), "Should have service dependencies"); - assert!(docker_compose_content.contains("restart: unless-stopped"), "Should have restart policy"); -} - -#[test] -#[serial] -fn test_symfony_hexagonal_architecture_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_architecture"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for Hexagonal Architecture directories - let architecture_dirs = &[ - "src/Domain", - "src/Domain/User", - "src/Domain/User/Entity", - "src/Domain/User/Repository", - "src/Domain/User/Service", - "src/Application", - "src/Application/User/Command", - "src/Application/User/Query", - "src/Application/User/Handler", - "src/Infrastructure", - "src/Infrastructure/Http/Controller", - "src/Infrastructure/Persistence/Doctrine", - ]; - - for dir in architecture_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } -} - -#[test] -#[serial] -fn test_symfony_jwt_authentication() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_jwt"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains JWT dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("lexik/jwt-authentication-bundle"), - "Should include JWT authentication bundle"); - assert!(composer_content.contains("gesdinet/jwt-refresh-token-bundle"), - "Should include JWT refresh token bundle"); - - // Check for JWT-related files - assert!(project_dir.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php").exists(), - "AuthController should exist"); -} - -#[test] -#[serial] -fn test_symfony_doctrine_configuration() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_doctrine"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains Doctrine dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("doctrine/orm"), "Should include Doctrine ORM"); - assert!(composer_content.contains("doctrine/doctrine-bundle"), "Should include Doctrine bundle"); - assert!(composer_content.contains("doctrine/doctrine-migrations-bundle"), - "Should include Doctrine migrations"); - - // Check for Doctrine configuration files - assert!(project_dir.join("config/packages/doctrine.yaml").exists(), - "Doctrine configuration should exist"); -} - -#[test] -#[serial] -fn test_symfony_environment_security() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_security"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check .env.docker.example exists with secure variables - let env_example_path = project_dir.join(".env.docker.example"); - assert!(env_example_path.exists(), ".env.docker.example should exist"); - - let env_content = fs::read_to_string(&env_example_path) - .expect("Should be able to read .env.docker.example"); - - // Should contain variable templates, not actual secrets - assert!(env_content.contains("APP_SECRET="), "Should have APP_SECRET template"); - assert!(env_content.contains("DB_PASSWORD="), "Should have DB_PASSWORD template"); - assert!(env_content.contains("JWT_SECRET="), "Should have JWT_SECRET template"); - assert!(env_content.contains("openssl rand"), "Should use openssl for secret generation"); -} - -#[test] -#[serial] -fn test_symfony_testing_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_testing"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for testing configuration - assert!(project_dir.join("phpunit.xml.dist").exists(), "phpunit.xml.dist should exist"); - - // Check composer.json contains testing dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("phpunit/phpunit"), "Should include PHPUnit"); - assert!(composer_content.contains("symfony/phpunit-bridge"), "Should include Symfony PHPUnit bridge"); - assert!(composer_content.contains("symfony/test-pack"), "Should include Symfony test pack"); - - // Check for test directories - let test_dirs = &[ - "tests/Unit", - "tests/Functional", - ]; - - for dir in test_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } - - // Should have testing scripts in composer.json - assert!(composer_content.contains("\"test\""), "Should have test script"); -} - -#[test] -#[serial] -fn test_symfony_no_docker() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_no_docker"; - - let mut cmd = run_init_command("symfony", project_name, &["--no-docker"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Symfony project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Docker files should NOT exist - assert!(!project_dir.join("docker-compose.yml").exists(), - "docker-compose.yml should not exist with --no-docker"); - assert!(!project_dir.join("docker/php/Dockerfile").exists(), - "Dockerfile should not exist with --no-docker"); - assert!(!project_dir.join(".env.docker.example").exists(), - ".env.docker.example should not exist with --no-docker"); - - // But regular Symfony files should exist - assert!(project_dir.join("composer.json").exists(), "composer.json should exist"); -} - -#[test] -#[serial] -fn test_symfony_api_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_symfony_api"; - - let mut cmd = run_init_command("symfony", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains API-specific dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("symfony/serializer"), "Should include Serializer component"); - assert!(composer_content.contains("symfony/validator"), "Should include Validator component"); - assert!(composer_content.contains("nelmio/cors-bundle"), "Should include CORS bundle"); -} - -#[test] -fn test_symfony_init_help() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg("symfony").arg("--help"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("Symfony")) - .stdout(predicate::str::contains("--no-docker")); -} \ No newline at end of file diff --git a/tests/integration/boilerplate/vanilla_tests.rs b/tests/integration/boilerplate/vanilla_tests.rs deleted file mode 100644 index 98e40da..0000000 --- a/tests/integration/boilerplate/vanilla_tests.rs +++ /dev/null @@ -1,324 +0,0 @@ -use super::*; -use serial_test::serial; -use tempfile::TempDir; - -#[test] -#[serial] -fn test_vanilla_init_basic() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_basic"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("PHP Vanilla project")) - .stdout(predicate::str::contains(project_name)); - - // Verify project structure was created - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Check for PHP Vanilla files that we know exist - let expected_files = &[ - "composer.json", - "docker-compose.yml", - ".env.docker.example", - ".env.example", - "README.md", - "public/index.php", - "public/.htaccess", - "src/Domain/User/Entity/User.php", - "src/Application/User/Command/CreateUserCommand.php", - "src/Infrastructure/Http/Controller/Api/V1/AuthController.php", - "src/Infrastructure/Http/Router.php", - "src/Infrastructure/Database/PDOConnection.php", - "src/Infrastructure/Security/JWTManager.php", - "src/Infrastructure/Config/AppConfig.php", - ]; - - for file in expected_files { - assert!(project_dir.join(file).exists(), "{} should exist", file); - } -} - -#[test] -#[serial] -fn test_vanilla_docker_compose_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_docker"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check docker-compose.yml contains production-ready configuration - let docker_compose_path = project_dir.join("docker-compose.yml"); - assert!(docker_compose_path.exists(), "docker-compose.yml should exist"); - - let docker_compose_content = fs::read_to_string(&docker_compose_path) - .expect("Should be able to read docker-compose.yml"); - - // Check for production-ready features - assert!(docker_compose_content.contains("env_file:"), "Should use env_file for security"); - assert!(docker_compose_content.contains("expose:"), "Should use expose instead of ports for internal services"); - assert!(docker_compose_content.contains("healthcheck:"), "Should have health checks"); - assert!(docker_compose_content.contains("depends_on:"), "Should have service dependencies"); - assert!(docker_compose_content.contains("restart: unless-stopped"), "Should have restart policy"); -} - -#[test] -#[serial] -fn test_vanilla_clean_architecture_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_architecture"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for Clean Architecture directories - let architecture_dirs = &[ - "src/Domain", - "src/Domain/User", - "src/Domain/User/Entity", - "src/Domain/User/Repository", - "src/Domain/User/Service", - "src/Domain/User/ValueObject", - "src/Application", - "src/Application/User/Command", - "src/Application/User/Handler", - "src/Application/Auth", - "src/Infrastructure", - "src/Infrastructure/Http/Controller", - "src/Infrastructure/Persistence/PDO", - "src/Infrastructure/Security", - "src/Infrastructure/Config", - ]; - - for dir in architecture_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } -} - -#[test] -#[serial] -fn test_vanilla_jwt_authentication() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_jwt"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains JWT dependency - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("firebase/php-jwt"), "Should include JWT library"); - - // Check for JWT-related files - assert!(project_dir.join("src/Infrastructure/Security/JWTManager.php").exists(), - "JWTManager should exist"); - assert!(project_dir.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php").exists(), - "AuthController should exist"); -} - -#[test] -#[serial] -fn test_vanilla_pdo_database() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_database"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for database configuration - assert!(project_dir.join("config/database.php").exists(), "Database config should exist"); - assert!(project_dir.join("src/Infrastructure/Database/PDOConnection.php").exists(), - "PDO connection should exist"); - assert!(project_dir.join("database/migrations/001_create_users_table.sql").exists(), - "Database migration should exist"); - - // Check database config content - let db_config_path = project_dir.join("config/database.php"); - let db_config_content = fs::read_to_string(&db_config_path) - .expect("Should be able to read database config"); - - assert!(db_config_content.contains("pgsql"), "Should support PostgreSQL"); - assert!(db_config_content.contains("mysql"), "Should support MySQL"); -} - -#[test] -#[serial] -fn test_vanilla_environment_security() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_security"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check .env.example exists with secure variables - let env_example_path = project_dir.join(".env.example"); - assert!(env_example_path.exists(), ".env.example should exist"); - - let env_content = fs::read_to_string(&env_example_path) - .expect("Should be able to read .env.example"); - - // Should contain environment variable templates - assert!(env_content.contains("APP_NAME="), "Should have APP_NAME template"); - assert!(env_content.contains("DB_PASSWORD="), "Should have DB_PASSWORD template"); - assert!(env_content.contains("JWT_SECRET="), "Should have JWT_SECRET template"); - assert!(env_content.contains("BCRYPT_ROUNDS="), "Should have BCRYPT_ROUNDS setting"); -} - -#[test] -#[serial] -fn test_vanilla_testing_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_testing"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for testing configuration - assert!(project_dir.join("phpunit.xml").exists(), "phpunit.xml should exist"); - - // Check composer.json contains testing dependencies - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("phpunit/phpunit"), "Should include PHPUnit"); - assert!(composer_content.contains("phpstan/phpstan"), "Should include PHPStan"); - assert!(composer_content.contains("friendsofphp/php-cs-fixer"), "Should include PHP CS Fixer"); - - // Check for test directories - let test_dirs = &[ - "tests/Unit", - "tests/Integration", - "tests/Functional", - ]; - - for dir in test_dirs { - assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); - } - - // Check for test files - assert!(project_dir.join("tests/Unit/UserTest.php").exists(), "Unit test should exist"); - assert!(project_dir.join("tests/Functional/AuthTest.php").exists(), "Functional test should exist"); -} - -#[test] -#[serial] -fn test_vanilla_no_docker() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_no_docker"; - - let mut cmd = run_init_command("vanilla", project_name, &["--no-docker"]); - cmd.current_dir(&temp_dir); - - cmd.assert() - .success() - .stdout(predicate::str::contains("PHP Vanilla project")); - - let project_dir = temp_dir.path().join(project_name); - assert!(project_dir.exists(), "Project directory should be created"); - - // Docker files should NOT exist - assert!(!project_dir.join("docker-compose.yml").exists(), - "docker-compose.yml should not exist with --no-docker"); - assert!(!project_dir.join("docker/php/Dockerfile").exists(), - "Dockerfile should not exist with --no-docker"); - assert!(!project_dir.join(".env.docker.example").exists(), - ".env.docker.example should not exist with --no-docker"); - - // But regular PHP files should exist - assert!(project_dir.join("composer.json").exists(), "composer.json should exist"); - assert!(project_dir.join("public/index.php").exists(), "index.php should exist"); -} - -#[test] -#[serial] -fn test_vanilla_api_structure() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_api"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check for API structure - assert!(project_dir.join("src/Infrastructure/Http/Router.php").exists(), "Router should exist"); - assert!(project_dir.join("src/Infrastructure/Http/Request.php").exists(), "Request class should exist"); - assert!(project_dir.join("src/Infrastructure/Http/Response.php").exists(), "Response class should exist"); - - // Check public/index.php has API routes defined - let index_php_path = project_dir.join("public/index.php"); - let index_content = fs::read_to_string(&index_php_path) - .expect("Should be able to read index.php"); - - assert!(index_content.contains("/api/v1/health"), "Should have health endpoint"); - assert!(index_content.contains("/api/v1/auth/register"), "Should have register endpoint"); - assert!(index_content.contains("/api/v1/auth/login"), "Should have login endpoint"); - assert!(index_content.contains("/api/v1/users"), "Should have users endpoint"); -} - -#[test] -#[serial] -fn test_vanilla_psr4_autoloading() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let project_name = "test_vanilla_psr4"; - - let mut cmd = run_init_command("vanilla", project_name, &[]); - cmd.current_dir(&temp_dir); - - cmd.assert().success(); - - let project_dir = temp_dir.path().join(project_name); - - // Check composer.json contains PSR-4 autoloading - let composer_path = project_dir.join("composer.json"); - let composer_content = fs::read_to_string(&composer_path) - .expect("Should be able to read composer.json"); - - assert!(composer_content.contains("\"App\\\\\""), "Should have PSR-4 autoloading for App namespace"); - assert!(composer_content.contains("\"Tests\\\\\""), "Should have PSR-4 autoloading for Tests namespace"); -} - -#[test] -fn test_vanilla_init_help() { - let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); - cmd.arg("init").arg("vanilla").arg("--help"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("PHP Vanilla")) - .stdout(predicate::str::contains("--no-docker")); -} \ No newline at end of file diff --git a/tests/integration/cli_commands_test.rs b/tests/integration/cli_commands_test.rs index e92f443..04bb7bd 100644 --- a/tests/integration/cli_commands_test.rs +++ b/tests/integration/cli_commands_test.rs @@ -236,7 +236,6 @@ fn test_cli_help() { .stdout(predicate::str::contains("A powerful CLI tool for DSL-based Docker Compose generation")) .stdout(predicate::str::contains("Commands:")) .stdout(predicate::str::contains("build")) - .stdout(predicate::str::contains("init")) .stdout(predicate::str::contains("validate")) .stdout(predicate::str::contains("info")); } diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 3eaf7d9..22e9d28 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -3,7 +3,6 @@ pub mod cli_commands_test; pub mod docker_compose_generation_test; pub mod error_handling_test; pub mod enhanced_error_handling_test; -pub mod boilerplate; pub mod structural; // BUILD-ARGS feature tests