diff --git a/Cargo.lock b/Cargo.lock index a5be0e3..2df7c60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" version = "0.6.21" @@ -61,21 +52,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - [[package]] name = "clap" version = "4.5.48" @@ -122,52 +98,13 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "developerStartSpringboot" version = "0.1.0" dependencies = [ "clap", - "regex", - "rust-embed", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", + "serde", + "serde_json", ] [[package]] @@ -183,10 +120,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] -name = "libc" -version = "0.2.176" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "memchr" @@ -219,86 +156,46 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.11.3" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "serde_core", + "serde_derive", ] [[package]] -name = "regex-automata" -version = "0.4.11" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "serde_derive", ] [[package]] -name = "regex-syntax" -version = "0.8.6" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "rust-embed" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "rust-embed-utils", "syn", - "walkdir", ] [[package]] -name = "rust-embed-utils" -version = "8.7.2" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] @@ -318,12 +215,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "unicode-ident" version = "1.0.19" @@ -336,31 +227,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - [[package]] name = "windows-link" version = "0.2.0" @@ -440,3 +306,9 @@ name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c7b3515..0c9eff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ lto = true codegen-units = 1 [dependencies] -regex = "1.11.3" clap = { version = "4.5.48", features = ["derive"] } -rust-embed = "8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [package.metadata.deb] maintainer = "erique.dev " diff --git a/README.md b/README.md index 1b3dfe7..f5eafb0 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,282 @@ -ATTENTION: For scalability reasons, I am migrating to a different way of generating projects. -Currently, it was done using Java file templates, but this ends up being less dynamic and difficult to change. -You can look at the branches to track the progress. +# developerStartSpringboot -## developerStartSpringboot -This is a terminal application designed for developers starting a Spring -Boot API who want to bypass the tedious initial steps of creating layers, -folders, and authentication. +A terminal application for developers who want to quickly bootstrap a Spring Boot API with a clean, layered architecture. Skip the tedious initial setup of creating layers, folders, entities, and authentication. ![dss terminal](terminal.png) -### How to Use -To get started, simply install the tool and run the command `dss init` in -your terminal. Answer a few basic questions, and in seconds, you'll have -the complete foundation for your Spring Boot API. +## Features + +- **Flexible Architecture**: Choose any number of physical layers (Maven modules) and logical packages +- **Multiple Entities**: Generate as many domain entities as you need +- **Spring Security**: Optional JWT-based authentication +- **Swagger/OpenAPI**: Built-in API documentation +- **Example Endpoints**: Ready-to-test controllers +- **JSON Configuration**: Import/export project configurations + +## Quick Start -# how to install - Ubuntu -### option A: ```bash -curl -fsSL https://eriquerocha.github.io/developerStartSpringboot/install-deb.sh | sudo bash +# Interactive mode - answer questions to generate your project +dss init + +# Generate from JSON configuration file +dss generate -c my-project.json + +# Export a configuration template +dss template -o my-template.json ``` -### option B: -run the following command sequence in the terminal: -1. Download the .deb package -```bash -wget https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Ubuntu-linux/developerstartspringboot_0.1.0-1_amd64.deb +## Commands + +| Command | Description | +|---------|-------------| +| `dss init` | Interactive mode - guides you through project setup | +| `dss generate -c ` | Generate project from JSON configuration | +| `dss template [-o ]` | Export default configuration template | +| `dss version` | Show version information | + +## JSON Configuration + +You can fully customize your project structure using a JSON configuration file: + +```json +{ + "project": { + "name": "myAPI", + "domain": "com.example.demo", + "description": "My Spring Boot API", + "developer": { + "name": "Your Name", + "email": "you@example.com", + "url": "example.com" + } + }, + "layers": { + "physical": [ + { + "name": "core", + "logical": ["domain/entities", "domain/valueobjects"], + "dependencies": [], + "is_main": false + }, + { + "name": "application", + "logical": ["usecases", "ports/repositories", "ports/services"], + "dependencies": ["core"], + "is_main": false + }, + { + "name": "infrastructure", + "logical": ["adapters/web/controllers", "adapters/web/dto", "adapters/repositories", "config"], + "dependencies": ["core", "application"], + "is_main": true + } + ] + }, + "entities": [ + { "name": "User", "role": "USER" }, + { "name": "Admin", "role": "ADMIN" } + ], + "features": { + "spring_security": true, + "example_endpoints": true, + "swagger": true, + "flyway": false + } +} ``` -Or, if you prefer to use curl: +### Configuration Options + +#### Physical Layers +Define your Maven modules. Each layer can have: +- `name`: Module name (e.g., "core", "domain", "infrastructure") +- `logical`: List of package paths within the module +- `dependencies`: Other modules this one depends on +- `is_main`: Set to `true` for the module containing the Spring Boot Application class + +#### Entities +List of domain entities to generate: +- `name`: Entity name in PascalCase +- `role`: Security role associated with this entity + +#### Features +Toggle optional features: +- `spring_security`: JWT authentication and authorization +- `example_endpoints`: Test controllers with `/api/test/*` endpoints +- `swagger`: OpenAPI documentation at `/swagger-ui.html` +- `flyway`: Database migration support + +## Supported Architectures + +The tool supports any layered architecture: + +| Architecture | Layers | Example | +|-------------|--------|---------| +| **Monolithic** | 1 | Single module with all code | +| **Clean Architecture** | 3 | core, application, infrastructure | +| **Hexagonal** | 3+ | domain, ports, adapters | +| **Custom** | N | Any combination you define | + +### Default 3-Layer Architecture + +| Layer | Responsibility | Depends On | +|-------|---------------|------------| +| **Core** | Domain entities, value objects, business rules | None | +| **Application** | Use cases, ports (interfaces for repositories/services) | Core | +| **Infrastructure** | Controllers, JPA repositories, configurations, external integrations | Core, Application | + +## Installation + +### Ubuntu / Debian + +**Option A - Quick install:** ```bash -curl -L -O https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Ubuntu-linux/developerstartspringboot_0.1.0-1_amd64.deb +curl -fsSL https://eriquerocha.github.io/developerStartSpringboot/install-deb.sh | sudo bash ``` -2. Install the .deb package (in the folder where the package was downloaded) +**Option B - Manual install:** ```bash +# Download +wget https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Ubuntu-linux/developerstartspringboot_0.1.0-1_amd64.deb + +# Install sudo dpkg -i developerstartspringboot_0.1.0-1_amd64.deb -``` -4. Start using -```bash +# Use dss init ``` -# how to install - Fedora -run the following command sequence in the terminal: -1. installing directly from the URL: +### Fedora / RHEL + +**Option A - Direct install:** ```bash sudo dnf install https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Fedora-linux/developerStartSpringboot-0.1.0-1.x86_64.rpm ``` -### Or -downloading and installing: -1. Download the .rpm package + +**Option B - Manual install:** ```bash +# Download wget https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Fedora-linux/developerStartSpringboot-0.1.0-1.x86_64.rpm + +# Install +sudo dnf install ./developerStartSpringboot-0.1.0-1.x86_64.rpm + +# Use +dss init ``` -Or, if you prefer to use curl: +### Build from Source + ```bash -curl -L -O https://github.com/EriqueRocha/developerStartSpringboot/releases/download/Fedora-linux/developerStartSpringboot-0.1.0-1.x86_64.rpm +# Clone the repository +git clone https://github.com/EriqueRocha/developerStartSpringboot.git +cd developerStartSpringboot + +# Build with Cargo +cargo build --release + +# Binary will be at ./target/release/dss ``` -2. Install the .rpm package (in the folder where the package was downloaded) +## Requirements + +- Java 21+ (for generated projects) +- Maven 3.9+ (for generated projects) + +## Example Usage + +### Interactive Mode + ```bash -sudo dnf install ./developerStartSpringboot-0.1.0-1.x86_64.rpm +$ dss init + +=== Project Configuration === + +Application name (e.g.: myAPI): orderService +Domain (e.g.: com.example.demo): com.mycompany.orders +Description [Spring Boot API]: Order Management API + +=== Physical Layers (Maven Modules) === + +Number of physical layers [3]: 3 +Layer 1 name [core]: domain +Logical packages [domain/entities,domain/valueobjects]: entities,valueobjects +... + +=== Features === + +Include Spring Security? [y/N]: y +Include example endpoints for testing? [Y/n]: y +Include Swagger/OpenAPI documentation? [Y/n]: y + +Generate project with this configuration? [Y/n]: y ``` -3. Start using +### From JSON File + ```bash -dss init +# Export template +dss template -o my-project.json + +# Edit the JSON file as needed +nano my-project.json + +# Generate project +dss generate -c my-project.json ``` -### The application is generated in three layers: core, application, and infrastructure. +## Generated Project Structure + +``` +myAPI/ +├── pom.xml # Parent POM +├── core/ +│ ├── pom.xml +│ └── src/main/java/.../core/ +│ └── domain/ +│ ├── entities/ +│ │ └── User.java +│ └── valueobjects/ +├── application/ +│ ├── pom.xml +│ └── src/main/java/.../application/ +│ ├── usecases/ +│ │ └── CreateUserUseCase.java +│ └── ports/ +│ └── repositories/ +│ └── UserRepository.java +└── infrastructure/ + ├── pom.xml + └── src/main/java/.../infrastructure/ + ├── adapters/ + │ ├── web/ + │ │ ├── controllers/ + │ │ │ ├── UserController.java + │ │ │ └── TestController.java + │ │ └── dto/ + │ │ ├── CreateUserRequest.java + │ │ └── CreateUserResponse.java + │ └── repositories/ + │ ├── JpaUserRepository.java + │ ├── entities/ + │ │ └── UserEntity.java + │ └── jpa/ + │ └── UserJpaRepository.java + ├── config/ + │ ├── BeanConfiguration.java + │ ├── doc/ + │ │ └── OpenAPIConfiguration.java + │ └── security/ # (if spring_security enabled) + │ ├── SecurityConfig.java + │ └── JwtAuthenticationFilter.java + └── MyAPIApplication.java +``` + +## License + +This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE](LICENSE) file for details. -| **Layer** | **Main Responsibility** | **Knows About** | **Example Classes / Components** | -|------------|-------------------------|------------------|-----------------------------------| -| **Core (Domain)** | Contains the core business logic and entities. Defines domain rules, invariants, and pure data models — completely independent of frameworks. | None | `User`, `Order`, `Product`, `Payment`, `Money`, `Email` | -| **Application** | Orchestrates use cases and coordinates the business logic using the domain entities. Handles transactions, validation, and flow control. | Core | `CreateUserService`, `ProcessPaymentUseCase`, `OrderManager`, `DtoMapper` | -| **Infrastructure** | Implements the technical details and integrates with external systems (databases, APIs, file storage, messaging, etc.). Contains controllers, repositories, and configurations. | Application (and sometimes Core) | `UserRepositoryJpa`, `PaymentApiClient`, `EmailServiceImpl`, `UserController`, `SpringConfig` | +## Contributing +Contributions are welcome! Feel free to open issues or submit pull requests. diff --git a/src/main.rs b/src/main.rs index 2800e00..0734bd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,11 @@ * See the LICENSE file for more details. */ -use std::collections::HashMap; use std::fs; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; -use regex::Regex; use clap::{Parser, Subcommand}; -use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; const BANNER: &str = concat!("\x1b[32m", r#" developerStartSpringboot @@ -39,6 +37,97 @@ const BANNER: &str = concat!("\x1b[32m", r#" "\x1b[0m" ); +const CYAN: &str = "\x1b[36m"; +const GREEN: &str = "\x1b[32m"; +const YELLOW: &str = "\x1b[33m"; +const MAGENTA: &str = "\x1b[35m"; +const BOLD: &str = "\x1b[1m"; +const RESET: &str = "\x1b[0m"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectConfig { + pub project: ProjectInfo, + pub layers: LayersConfig, + pub entities: Vec, + pub features: FeaturesConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectInfo { + pub name: String, + pub domain: String, + pub description: String, + pub developer: DeveloperInfo, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeveloperInfo { + pub name: String, + pub email: String, + pub url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LayersConfig { + pub physical: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub entity_location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntityLocation { + pub layer: String, + pub logical: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhysicalLayer { + pub name: String, + pub logical: Vec, + #[serde(default)] + pub dependencies: Vec, + #[serde(default)] + pub is_main: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntityConfig { + pub name: String, + #[serde(default = "default_role")] + pub role: String, +} + +fn default_role() -> String { + "USER".to_string() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FeaturesConfig { + #[serde(default)] + pub spring_security: bool, + #[serde(default)] + pub example_endpoints: bool, + #[serde(default = "default_true")] + pub swagger: bool, + #[serde(default)] + pub flyway: bool, +} + +fn default_true() -> bool { + true +} + +impl Default for FeaturesConfig { + fn default() -> Self { + FeaturesConfig { + spring_security: false, + example_endpoints: false, + swagger: true, + flyway: false, + } + } +} + #[derive(Parser)] #[command(name = "dss")] #[command(about = "Spring Boot project generator", long_about = None)] @@ -49,34 +138,22 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Initialize a new Spring Boot project Init, - /// Show version information + Generate { + #[arg(short, long)] + config: PathBuf, + }, + Template { + #[arg(short, long)] + output: Option, + }, Version, } - -#[derive(RustEmbed)] -#[folder = "template/3layer"] -struct Templates; - -fn extract_embedded_template(dst_root: &Path) -> std::io::Result<()> { - for file in Templates::iter() { - let rel = file.as_ref(); - if let Some(data) = Templates::get(rel) { - let target = dst_root.join(rel); - if let Some(parent) = target.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(&target, data.data.as_ref())?; - } - } - Ok(()) -} - fn prompt(label: &str, default: Option<&str>) -> String { let mut input = String::new(); - print!("{}{}: ", label, default.map(|d| format!(" [{}]", d)).unwrap_or_default()); + let default_display = default.map(|d| format!(" {}[{}]{}", YELLOW, d, RESET)).unwrap_or_default(); + print!(" {CYAN}{BOLD}>{RESET} {}{}: ", label, default_display); io::stdout().flush().unwrap(); io::stdin().read_line(&mut input).unwrap(); let s = input.trim().to_string(); @@ -89,10 +166,122 @@ fn prompt_required(label: &str) -> String { if !input.trim().is_empty() { return input; } - println!("This field is required. Please fill it in."); + println!(" {YELLOW}! This field is required. Please fill it in.{RESET}"); + } +} + +fn prompt_yes_no(label: &str, default: bool) -> bool { + let default_str = if default { "Y/n" } else { "y/N" }; + let input = prompt(label, Some(default_str)).to_lowercase(); + match input.as_str() { + "y" | "yes" | "s" | "sim" => true, + "n" | "no" | "nao" | "não" => false, + _ => default, } } +fn prompt_number(label: &str, default: usize) -> usize { + let input = prompt(label, Some(&default.to_string())); + input.parse().unwrap_or(default) +} + +enum KeyEvent { Up, Down, Enter, Other } + +fn raw_mode(enable: bool) { + let args: &[&str] = if enable { + &["-icanon", "-echo"] + } else { + &["icanon", "echo"] + }; + let _ = std::process::Command::new("stty").args(args).status(); +} + +fn read_key() -> KeyEvent { + let mut buf = [0u8; 1]; + if io::stdin().read(&mut buf).unwrap_or(0) == 0 { + return KeyEvent::Other; + } + match buf[0] { + 0x1b => { + let mut seq = [0u8; 2]; + if io::stdin().read(&mut seq).unwrap_or(0) == 2 { + match seq { [b'[', b'A'] => KeyEvent::Up, [b'[', b'B'] => KeyEvent::Down, _ => KeyEvent::Other } + } else { KeyEvent::Other } + } + b'\r' | b'\n' => KeyEvent::Enter, + b'k' => KeyEvent::Up, + b'j' => KeyEvent::Down, + _ => KeyEvent::Other, + } +} + +fn redraw_selector(items: &[String], selected: usize) { + print!("\x1b[{}A", items.len()); + for (i, item) in items.iter().enumerate() { + print!("\x1b[2K\r"); + if i == selected { + println!(" {GREEN}{BOLD}▶ {item}{RESET}"); + } else { + println!(" {item}"); + } + } + io::stdout().flush().unwrap(); +} + +fn select_interactive(title: &str, items: &[String]) -> usize { + println!("\n {CYAN}{BOLD}{title}{RESET}"); + println!(" {YELLOW}(↑/↓ or k/j to navigate, Enter to confirm){RESET}\n"); + + for (i, item) in items.iter().enumerate() { + if i == 0 { + println!(" {GREEN}{BOLD}▶ {item}{RESET}"); + } else { + println!(" {item}"); + } + } + io::stdout().flush().unwrap(); + + raw_mode(true); + let mut selected = 0usize; + + loop { + match read_key() { + KeyEvent::Up => { + if selected > 0 { + selected -= 1; + redraw_selector(items, selected); + } + } + KeyEvent::Down => { + if selected < items.len() - 1 { + selected += 1; + redraw_selector(items, selected); + } + } + KeyEvent::Enter => break, + KeyEvent::Other => {} + } + } + + raw_mode(false); + println!(); + selected +} + +fn print_section(title: &str) { + println!("\n{MAGENTA}{BOLD}══════════════════════════════════════════════════════════════{RESET}"); + println!("{MAGENTA}{BOLD} {}{RESET}", title); + println!("{MAGENTA}{BOLD}══════════════════════════════════════════════════════════════{RESET}\n"); +} + +fn print_subsection(title: &str) { + println!("\n {GREEN}{BOLD}── {} ──{RESET}\n", title); +} + +fn print_info(message: &str) { + println!(" {CYAN}ℹ {}{RESET}", message); +} + fn to_snake_case(s: &str) -> String { let mut out = String::new(); let mut prev_is_lower_or_digit = false; @@ -119,14 +308,11 @@ fn to_snake_case(s: &str) -> String { } } - let norm = out - .trim_matches('_') + out.trim_matches('_') .split('_') .filter(|seg| !seg.is_empty()) .collect::>() - .join("_"); - - norm + .join("_") } fn to_pascal_case(s: &str) -> String { @@ -143,8 +329,9 @@ fn to_pascal_case(s: &str) -> String { if out.is_empty() { s.to_string() } else { out } } -fn to_lower_first(s: &str) -> String { - let mut chars = s.chars(); +fn to_camel_case(s: &str) -> String { + let pascal = to_pascal_case(s); + let mut chars = pascal.chars(); match chars.next() { Some(first) => format!("{}{}", first.to_lowercase(), chars.as_str()), None => String::new(), @@ -165,227 +352,1519 @@ fn to_app_name_clean(name: &str) -> String { .collect(); let mut out = tokens.join(""); if out.is_empty() { out = "App".to_string(); } - let mut it = out.chars(); - if let Some(f) = it.next() { format!("{}{}", f.to_uppercase(), it.as_str()) } else { out } + out } -fn domain_to_parts(domain: &str) -> Vec { - domain.split('.').filter(|p| !p.is_empty()).map(|s| s.to_string()).collect() +fn domain_to_path(domain: &str) -> String { + domain.split('.').collect::>().join("/") } -fn is_textual_target(path: &Path) -> bool { - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - let ext = ext.to_ascii_lowercase(); - return matches!(ext.as_str(), "java" | "properties" | "pom" | "xml" | "sql"); +fn write_file(path: &Path, content: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; } - false + fs::write(path, content)?; + println!(" Created: {}", path.display()); + Ok(()) } -fn read_to_string_lossy(path: &Path) -> io::Result { - let bytes = fs::read(path)?; - Ok(String::from_utf8_lossy(&bytes).into_owned()) +fn build_config_interactive() -> ProjectConfig { + println!("{}", BANNER); + + print_section("Project Configuration"); + + let app_name = prompt_required("Application name (e.g.: myAPI)"); + let domain = prompt_required("Domain (e.g.: com.example.demo)"); + let description = prompt("Description", Some("Spring Boot API")); + + print_section("Developer Information"); + let dev_name = prompt("Developer name", Some("Developer")); + let dev_email = prompt("Developer email", Some("dev@example.com")); + let dev_url = prompt("Developer website", Some("example.com")); + + print_section("Physical Layers (Maven Modules)"); + print_info("Define the physical layers of your project."); + println!(" {CYAN}Common architectures:{RESET}"); + println!(" {GREEN}•{RESET} Monolithic: 1 layer (all in one)"); + println!(" {GREEN}•{RESET} Clean Architecture: 3 layers (core, application, infrastructure)"); + println!(" {GREEN}•{RESET} Hexagonal: 3+ layers (domain, ports, adapters)"); + println!(); + + let num_layers = prompt_number("Number of physical layers", 3); + + let mut physical_layers: Vec = Vec::new(); + + for i in 0..num_layers { + print_subsection(&format!("Layer {}", i + 1)); + + let default_name = match i { + 0 => "core", + 1 => "application", + 2 => "infrastructure", + _ => "", + }; + + let layer_name = prompt(&format!("Layer {} name", i + 1), Some(default_name)); + + print_info(&format!("Define logical packages for '{}' (comma-separated)", layer_name)); + println!(" {CYAN}Example:{RESET} domain/entities,domain/valueobjects,domain/services"); + + let default_logical = match layer_name.as_str() { + "core" => "domain/entities,domain/valueobjects", + "application" => "usecases,ports/repositories,ports/services", + "infrastructure" => "adapters/web/controllers,adapters/web/dto,adapters/repositories,adapters/services,config", + _ => "", + }; + + let logical_input = prompt("Logical packages", Some(default_logical)); + let logical: Vec = logical_input + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + let mut dependencies: Vec = Vec::new(); + if i > 0 && !physical_layers.is_empty() { + let available: Vec<&str> = physical_layers.iter().map(|l| l.name.as_str()).collect(); + print_info(&format!("Available layers for dependency: {}", available.join(", "))); + let deps_input = prompt("Dependencies (comma-separated, or empty)", None); + dependencies = deps_input + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + + let is_main = if i == num_layers - 1 { + prompt_yes_no("Is this the main module (contains Application class)?", true) + } else { + prompt_yes_no("Is this the main module (contains Application class)?", false) + }; + + physical_layers.push(PhysicalLayer { + name: layer_name, + logical, + dependencies, + is_main, + }); + } + + print_section("Entities"); + + let (entities, entity_location) = if prompt_yes_no("Create entities?", true) { + let mut options: Vec = Vec::new(); + let mut option_map: Vec<(String, String)> = Vec::new(); + for layer in &physical_layers { + for logical in &layer.logical { + options.push(format!("{} {} {}", layer.name, CYAN.to_string() + "›" + RESET, logical)); + option_map.push((layer.name.clone(), logical.clone())); + } + } + + let idx = select_interactive("Where should entities be placed?", &options); + let (chosen_layer, chosen_logical) = option_map[idx].clone(); + println!(" {GREEN}✓ Entities will be placed in: {chosen_layer} › {chosen_logical}{RESET}"); + + let num_entities = prompt_number("Number of entities to create", 1); + let mut entities: Vec = Vec::new(); + for i in 0..num_entities { + print_subsection(&format!("Entity {}", i + 1)); + let entity_name = prompt(&format!("Entity {} name", i + 1), Some("User")); + let role = prompt("Role for this entity", Some("USER")); + entities.push(EntityConfig { name: entity_name, role }); + } + + let location = EntityLocation { layer: chosen_layer, logical: chosen_logical }; + (entities, Some(location)) + } else { + (Vec::new(), None) + }; + + print_section("Features"); + let spring_security = prompt_yes_no("Include Spring Security (JWT)?", false); + let example_endpoints = prompt_yes_no("Include example endpoints for testing?", true); + let swagger = prompt_yes_no("Include Swagger/OpenAPI documentation?", true); + let flyway = prompt_yes_no("Include Flyway migrations?", false); + + ProjectConfig { + project: ProjectInfo { + name: app_name, + domain, + description, + developer: DeveloperInfo { + name: dev_name, + email: dev_email, + url: dev_url, + }, + }, + layers: LayersConfig { + physical: physical_layers, + entity_location, + }, + entities, + features: FeaturesConfig { + spring_security, + example_endpoints, + swagger, + flyway, + }, + } } -fn write_string(path: &Path, content: &str) -> io::Result<()> { - if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - fs::write(path, content) +struct CodeGenerator<'a> { + config: &'a ProjectConfig, + root: PathBuf, } -fn replace_placeholders(text: &str, map: &HashMap<&str, String>) -> String { - let mut out = text.to_string(); - for (k, v) in map { - let placeholder = format!("{{{{{}}}}}", k); - out = out.replace(&placeholder, v); +impl<'a> CodeGenerator<'a> { + fn new(config: &'a ProjectConfig, root: PathBuf) -> Self { + Self { config, root } } - out -} -fn do_content_replacements(text: &str, replacements: &[(Regex, String)]) -> String { - let mut out = text.to_string(); - for (re, rep) in replacements { - out = re.replace_all(&out, rep.as_str()).into_owned(); + fn generate(&self) -> io::Result<()> { + println!("\nGenerating project: {}\n", self.root.display()); + + self.generate_root_pom()?; + self.generate_gitignore()?; + self.generate_gitattributes()?; + self.generate_mvnw()?; + + for layer in &self.config.layers.physical { + self.generate_layer(layer)?; + } + + println!("\nProject generated successfully at: {}", self.root.display()); + Ok(()) + } + + fn generate_root_pom(&self) -> io::Result<()> { + let modules: Vec = self.config.layers.physical + .iter() + .map(|l| format!(" {}", l.name)) + .collect(); + + let content = format!(r#" + + 4.0.0 + + {domain} + {name} + 1.0.0 + pom + + {name} + {description} + + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + + + + 21 + 21 + 21 + UTF-8 + + + +{modules} + + + + + {dev_name} + {dev_email} + {dev_url} + + + + + + + + +"#, + domain = self.config.project.domain, + name = self.config.project.name, + description = self.config.project.description, + modules = modules.join("\n"), + dev_name = self.config.project.developer.name, + dev_email = self.config.project.developer.email, + dev_url = self.config.project.developer.url, + ); + + write_file(&self.root.join("pom.xml"), &content) } - out -} -fn walk_all_paths(root: &Path) -> io::Result> { - let mut stack = vec![root.to_path_buf()]; - let mut out = Vec::new(); - while let Some(p) = stack.pop() { - out.push(p.clone()); - if p.is_dir() { - for entry in fs::read_dir(&p)? { - let entry = entry?; - stack.push(entry.path()); + fn generate_layer(&self, layer: &PhysicalLayer) -> io::Result<()> { + let layer_root = self.root.join(&layer.name); + + self.generate_layer_pom(layer)?; + + let base_path = layer_root + .join("src/main/java") + .join(domain_to_path(&self.config.project.domain)) + .join(&layer.name); + + for logical in &layer.logical { + let package_path = base_path.join(logical.replace("/", std::path::MAIN_SEPARATOR_STR)); + fs::create_dir_all(&package_path)?; + + self.generate_logical_content(layer, logical, &package_path)?; + } + + let test_path = layer_root + .join("src/test/java") + .join(domain_to_path(&self.config.project.domain)) + .join(&layer.name); + fs::create_dir_all(&test_path)?; + + let resources_path = layer_root.join("src/main/resources"); + fs::create_dir_all(&resources_path)?; + self.generate_application_properties(layer, &resources_path)?; + + if layer.is_main { + self.generate_application_class(layer)?; + + if self.config.features.example_endpoints { + let has_controllers = layer.logical.iter().any(|l| l.to_lowercase().contains("controllers")); + if !has_controllers { + let controllers_path = base_path.join("controllers"); + fs::create_dir_all(&controllers_path)?; + self.generate_example_controller(layer, "controllers", &controllers_path)?; + } } } + + Ok(()) } - out.sort_by_key(|p| std::cmp::Reverse(p.components().count())); - Ok(out) -} -fn remove_empty_dirs(root: &Path) -> io::Result<()> { - let all = walk_all_paths(root)?; - for p in all { - if p.is_dir() && p != root { - let _ = fs::remove_dir(&p); + fn generate_layer_pom(&self, layer: &PhysicalLayer) -> io::Result<()> { + let layer_root = self.root.join(&layer.name); + + let mut deps = String::new(); + + for dep in &layer.dependencies { + deps.push_str(&format!(r#" + + {} + {} + 1.0.0 + "#, + self.config.project.domain, dep + )); } + + if layer.is_main { + deps.push_str(r#" + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + "#); + + if self.config.features.spring_security { + deps.push_str(r#" + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + "#); + } + + if self.config.features.swagger { + deps.push_str(r#" + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.3 + "#); + } + + if self.config.features.flyway { + deps.push_str(r#" + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + runtime + "#); + } + } + + let build_section = if layer.is_main { + r#" + + + + org.springframework.boot + spring-boot-maven-plugin + + + "# + } else { + "" + }; + + let content = format!(r#" + + 4.0.0 + + + {domain} + {parent_name} + 1.0.0 + + + {layer_name} + + {deps} + + org.springframework.boot + spring-boot-starter-test + test + + +{build_section} + +"#, + domain = self.config.project.domain, + parent_name = self.config.project.name, + layer_name = layer.name, + deps = deps, + build_section = build_section, + ); + + write_file(&layer_root.join("pom.xml"), &content) } - Ok(()) -} -fn remap_your_domain_paths(dst_root: &Path, domain_parts: &[String]) -> io::Result<()> { - let all = walk_all_paths(dst_root)?; - let mut files: Vec = all.iter().filter(|p| p.is_file()).cloned().collect(); + fn generate_logical_content(&self, layer: &PhysicalLayer, logical: &str, path: &Path) -> io::Result<()> { + let logical_lower = logical.to_lowercase(); - for file in files.drain(..) { - let rel = file.strip_prefix(dst_root).unwrap(); - let comps: Vec = rel.components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect(); + let is_entity_location = match &self.config.layers.entity_location { + Some(loc) => loc.layer == layer.name && loc.logical == logical, + None => logical_lower.contains("entities"), + }; + if is_entity_location { + for entity in &self.config.entities { + self.generate_entity(layer, logical, path, entity)?; + } + } - let mut i = 0usize; - let mut new_comps: Vec = Vec::new(); - let mut changed = false; - while i < comps.len() { - if i + 1 < comps.len() && comps[i] == "your" && comps[i + 1] == "domain" { - for dp in domain_parts { - new_comps.push(dp.clone()); + if logical_lower.contains("repositor") { + for entity in &self.config.entities { + if logical_lower.contains("adapters") || logical_lower.contains("adapter") { + self.generate_repository_adapter(layer, logical, path, entity)?; + } else { + self.generate_repository_port(layer, logical, path, entity)?; } - i += 2; - changed = true; - } else { - new_comps.push(comps[i].clone()); - i += 1; } } - if changed { - let mut new_path = dst_root.to_path_buf(); - for c in new_comps { new_path.push(c); } - if let Some(parent) = new_path.parent() { fs::create_dir_all(parent)?; } - if new_path.exists() { - fs::remove_file(&new_path).ok(); + if logical_lower.contains("controllers") || logical_lower.contains("web") && logical_lower.contains("controllers") { + for entity in &self.config.entities { + self.generate_controller(layer, logical, path, entity)?; + } + if self.config.features.example_endpoints { + self.generate_example_controller(layer, logical, path)?; } - fs::rename(&file, &new_path)?; } + + if logical_lower.contains("dto") { + for entity in &self.config.entities { + self.generate_dto(layer, logical, path, entity)?; + } + } + + if logical_lower == "config" && layer.is_main { + self.generate_config_files(layer, path)?; + } + + if logical_lower.contains("usecases") || logical_lower.contains("usecase") { + for entity in &self.config.entities { + self.generate_usecase(layer, logical, path, entity)?; + } + } + + Ok(()) } - remove_empty_dirs(dst_root)?; - Ok(()) -} + fn get_package(&self, layer: &PhysicalLayer, logical: &str) -> String { + format!("{}.{}.{}", + self.config.project.domain, + layer.name, + logical.replace("/", ".") + ) + } -fn rename_files_by_tokens(dst_root: &Path, user_entity_pascal: &str, app_name_clean: &str) -> io::Result<()> { - let all = walk_all_paths(dst_root)?; - for path in all.into_iter().filter(|p| p.is_file()) { - let orig_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("").to_string(); - if orig_name.is_empty() { continue; } - let mut new_name = orig_name.clone(); - if new_name.contains("AppNameCleanApplication") { - new_name = new_name.replace("AppNameCleanApplication", &format!("{}Application", app_name_clean)); + fn find_entity_package(&self) -> String { + if let Some(loc) = &self.config.layers.entity_location { + return format!("{}.{}.{}", + self.config.project.domain, + loc.layer, + loc.logical.replace("/", ".") + ); } - if new_name.contains("UserEntity") { - new_name = new_name.replace("UserEntity", user_entity_pascal); + for layer in &self.config.layers.physical { + for logical in &layer.logical { + if logical.contains("entities") { + return format!("{}.{}.{}", + self.config.project.domain, + layer.name, + logical.replace("/", ".") + ); + } + } } - if new_name != orig_name { - let mut new_path = path.clone(); - new_path.set_file_name(new_name); - if new_path.exists() { - fs::remove_file(&new_path).ok(); + format!("{}.domain.entities", self.config.project.domain) + } + + fn find_port_repositories_package(&self) -> String { + for layer in &self.config.layers.physical { + for logical in &layer.logical { + if logical.contains("ports") && logical.to_lowercase().contains("repositor") { + return format!("{}.{}.{}", + self.config.project.domain, + layer.name, + logical.replace("/", ".") + ); + } } - fs::rename(&path, &new_path)?; } + for layer in &self.config.layers.physical { + for logical in &layer.logical { + if logical.to_lowercase().contains("repositor") { + return format!("{}.{}.{}", + self.config.project.domain, + layer.name, + logical.replace("/", ".") + ); + } + } + } + format!("{}.application.ports.repositories", self.config.project.domain) } - Ok(()) -} -fn edit_file_contents(dst_root: &Path, ph: &HashMap<&str, String>, replacements: &[(Regex, String)]) -> io::Result<()> { - let all = walk_all_paths(dst_root)?; - for path in all.into_iter().filter(|p| p.is_file()) { - if !is_textual_target(&path) { continue; } - let orig = match read_to_string_lossy(&path) { - Ok(s) => s, - Err(_) => continue, + fn generate_entity(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let package = self.get_package(layer, logical); + + let content = format!(r#"package {package}; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class {pascal} {{ + private Long id; + private String email; + private String password; + private String name; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public {pascal}() {{}} + + public {pascal}(String email, String password, String name) {{ + this.email = email; + this.password = password; + this.name = name; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + }} + + public {pascal}(Long id, String email, String password, String name, LocalDateTime createdAt, LocalDateTime updatedAt) {{ + this.id = id; + this.email = email; + this.password = password; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + }} + + public String getRole() {{ + return "{role}"; + }} + + public Long getId() {{ return id; }} + public void setId(Long id) {{ this.id = id; }} + + public String getEmail() {{ return email; }} + public void setEmail(String email) {{ this.email = email; }} + + public String getPassword() {{ return password; }} + public void setPassword(String password) {{ this.password = password; }} + + public String getName() {{ return name; }} + public void setName(String name) {{ this.name = name; }} + + public LocalDateTime getCreatedAt() {{ return createdAt; }} + public void setCreatedAt(LocalDateTime createdAt) {{ this.createdAt = createdAt; }} + + public LocalDateTime getUpdatedAt() {{ return updatedAt; }} + public void setUpdatedAt(LocalDateTime updatedAt) {{ this.updatedAt = updatedAt; }} + + @Override + public boolean equals(Object o) {{ + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + {pascal} that = ({pascal}) o; + return Objects.equals(id, that.id) && Objects.equals(email, that.email); + }} + + @Override + public int hashCode() {{ + return Objects.hash(id, email); + }} +}} +"#, + package = package, + pascal = pascal, + role = entity.role, + ); + + write_file(&path.join(format!("{}.java", pascal)), &content) + } + + fn generate_repository_port(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let package = self.get_package(layer, logical); + let entity_package = self.find_entity_package(); + + let content = format!(r#"package {package}; + +import {entity_package}.{pascal}; +import java.util.Optional; + +public interface {pascal}Repository {{ + {pascal} save({pascal} entity); + Optional<{pascal}> findById(Long id); + Optional<{pascal}> findByEmail(String email); + void deleteById(Long id); +}} +"#, + package = package, + entity_package = entity_package, + pascal = pascal, + ); + + write_file(&path.join(format!("{}Repository.java", pascal)), &content) + } + + fn generate_repository_adapter(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let table_name = to_snake_case(&entity.name); + let package = self.get_package(layer, logical); + + let entity_package = self.find_entity_package(); + let port_package = self.find_port_repositories_package(); + + let jpa_entity_content = format!(r#"package {package}.entities; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "{table_name}") +public class {pascal}Entity {{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + private String name; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public {pascal}Entity() {{}} + + public {pascal}Entity(Long id, String email, String password, String name, LocalDateTime createdAt, LocalDateTime updatedAt) {{ + this.id = id; + this.email = email; + this.password = password; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + }} + + public Long getId() {{ return id; }} + public void setId(Long id) {{ this.id = id; }} + + public String getEmail() {{ return email; }} + public void setEmail(String email) {{ this.email = email; }} + + public String getPassword() {{ return password; }} + public void setPassword(String password) {{ this.password = password; }} + + public String getName() {{ return name; }} + public void setName(String name) {{ this.name = name; }} + + public LocalDateTime getCreatedAt() {{ return createdAt; }} + public void setCreatedAt(LocalDateTime createdAt) {{ this.createdAt = createdAt; }} + + public LocalDateTime getUpdatedAt() {{ return updatedAt; }} + public void setUpdatedAt(LocalDateTime updatedAt) {{ this.updatedAt = updatedAt; }} +}} +"#, + package = package, + table_name = table_name, + pascal = pascal, + ); + + let entities_path = path.join("entities"); + fs::create_dir_all(&entities_path)?; + write_file(&entities_path.join(format!("{}Entity.java", pascal)), &jpa_entity_content)?; + + let jpa_repo_content = format!(r#"package {package}.jpa; + +import {package}.entities.{pascal}Entity; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface {pascal}JpaRepository extends JpaRepository<{pascal}Entity, Long> {{ + Optional<{pascal}Entity> findByEmail(String email); +}} +"#, + package = package, + pascal = pascal, + ); + + let jpa_path = path.join("jpa"); + fs::create_dir_all(&jpa_path)?; + write_file(&jpa_path.join(format!("{}JpaRepository.java", pascal)), &jpa_repo_content)?; + + let repo_impl_content = format!(r#"package {package}; + +import {entity_package}.{pascal}; +import {port_package}.{pascal}Repository; +import {package}.entities.{pascal}Entity; +import {package}.jpa.{pascal}JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public class Jpa{pascal}Repository implements {pascal}Repository {{ + private final {pascal}JpaRepository jpaRepository; + + public Jpa{pascal}Repository({pascal}JpaRepository jpaRepository) {{ + this.jpaRepository = jpaRepository; + }} + + @Override + public {pascal} save({pascal} domain) {{ + {pascal}Entity entity = toEntity(domain); + {pascal}Entity saved = jpaRepository.save(entity); + return toDomain(saved); + }} + + @Override + public Optional<{pascal}> findById(Long id) {{ + return jpaRepository.findById(id).map(this::toDomain); + }} + + @Override + public Optional<{pascal}> findByEmail(String email) {{ + return jpaRepository.findByEmail(email).map(this::toDomain); + }} + + @Override + public void deleteById(Long id) {{ + jpaRepository.deleteById(id); + }} + + private {pascal}Entity toEntity({pascal} domain) {{ + return new {pascal}Entity( + domain.getId(), + domain.getEmail(), + domain.getPassword(), + domain.getName(), + domain.getCreatedAt(), + domain.getUpdatedAt() + ); + }} + + private {pascal} toDomain({pascal}Entity entity) {{ + return new {pascal}( + entity.getId(), + entity.getEmail(), + entity.getPassword(), + entity.getName(), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + }} +}} +"#, + package = package, + entity_package = entity_package, + port_package = port_package, + pascal = pascal, + ); + + write_file(&path.join(format!("Jpa{}Repository.java", pascal)), &repo_impl_content) + } + + fn generate_controller(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let camel = to_camel_case(&entity.name); + let package = self.get_package(layer, logical); + + let dto_package = package.replace(".controllers", ".dto"); + + let content = format!(r#"package {package}; + +import {dto_package}.Create{pascal}Request; +import {dto_package}.Create{pascal}Response; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/{camel}s") +public class {pascal}Controller {{ + + @PostMapping + public ResponseEntity create(@RequestBody Create{pascal}Request request) {{ + // TODO: Implement creation logic + return ResponseEntity.ok(new Create{pascal}Response(1L, request.email(), request.name())); + }} + + @GetMapping("/{{id}}") + public ResponseEntity getById(@PathVariable Long id) {{ + // TODO: Implement get by id logic + return ResponseEntity.ok(new Create{pascal}Response(id, "example@email.com", "Example")); + }} + + @DeleteMapping("/{{id}}") + public ResponseEntity delete(@PathVariable Long id) {{ + // TODO: Implement delete logic + return ResponseEntity.noContent().build(); + }} +}} +"#, + package = package, + dto_package = dto_package, + pascal = pascal, + camel = camel, + ); + + write_file(&path.join(format!("{}Controller.java", pascal)), &content) + } + + fn generate_example_controller(&self, layer: &PhysicalLayer, logical: &str, path: &Path) -> io::Result<()> { + let package = self.get_package(layer, logical); + + let security_imports = if self.config.features.spring_security { + "import org.springframework.security.core.Authentication;\n" + } else { + "" + }; + + let auth_param = if self.config.features.spring_security { + "Authentication authentication" + } else { + "" }; - let mut newc = replace_placeholders(&orig, ph); - newc = do_content_replacements(&newc, replacements); - if newc != orig { - write_string(&path, &newc)?; - println!("Project created: {}", path.display()); + + let auth_name = if self.config.features.spring_security { + "authentication.getName()" + } else { + "\"Guest\"" + }; + + let content = format!(r#"package {package}; + +{security_imports}import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/test") +public class TestController {{ + + @GetMapping("/hello") + public String hello({auth_param}) {{ + return "Hello, " + {auth_name} + "!"; + }} + + @GetMapping("/public") + public String publicEndpoint() {{ + return "This is a public endpoint!"; + }} + + @GetMapping("/health") + public String health() {{ + return "OK"; + }} +}} +"#, + package = package, + security_imports = security_imports, + auth_param = auth_param, + auth_name = auth_name, + ); + + write_file(&path.join("TestController.java"), &content) + } + + fn generate_dto(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let package = self.get_package(layer, logical); + + let request_content = format!(r#"package {package}; + +public record Create{pascal}Request( + String email, + String password, + String name +) {{}} +"#, + package = package, + pascal = pascal, + ); + + let response_content = format!(r#"package {package}; + +public record Create{pascal}Response( + Long id, + String email, + String name +) {{}} +"#, + package = package, + pascal = pascal, + ); + + write_file(&path.join(format!("Create{}Request.java", pascal)), &request_content)?; + write_file(&path.join(format!("Create{}Response.java", pascal)), &response_content) + } + + fn generate_usecase(&self, layer: &PhysicalLayer, logical: &str, path: &Path, entity: &EntityConfig) -> io::Result<()> { + let pascal = to_pascal_case(&entity.name); + let camel = to_camel_case(&entity.name); + let package = self.get_package(layer, logical); + + let entity_package = self.find_entity_package(); + let port_package = self.find_port_repositories_package(); + + let content = format!(r#"package {package}; + +import {entity_package}.{pascal}; +import {port_package}.{pascal}Repository; + +public class Create{pascal}UseCase {{ + private final {pascal}Repository {camel}Repository; + + public Create{pascal}UseCase({pascal}Repository {camel}Repository) {{ + this.{camel}Repository = {camel}Repository; + }} + + public {pascal} execute(String email, String password, String name) {{ + {pascal} {camel} = new {pascal}(email, password, name); + return {camel}Repository.save({camel}); + }} +}} +"#, + package = package, + entity_package = entity_package, + port_package = port_package, + pascal = pascal, + camel = camel, + ); + + write_file(&path.join(format!("Create{}UseCase.java", pascal)), &content) + } + + fn generate_config_files(&self, layer: &PhysicalLayer, path: &Path) -> io::Result<()> { + let bean_package = self.get_package(layer, "config"); + + let mut bean_imports = String::new(); + let mut bean_definitions = String::new(); + + let app_layer = self.config.layers.physical.iter() + .find(|l| l.logical.iter().any(|log| log.contains("usecases"))) + .map(|l| &l.name); + + if let Some(app) = app_layer { + for entity in &self.config.entities { + let pascal = to_pascal_case(&entity.name); + + bean_imports.push_str(&format!( + "import {}.{}.usecases.Create{}UseCase;\n", + self.config.project.domain, app, pascal + )); + bean_imports.push_str(&format!( + "import {}.{}.ports.repositories.{}Repository;\n", + self.config.project.domain, app, pascal + )); + + bean_definitions.push_str(&format!(r#" + @Bean + public Create{pascal}UseCase create{pascal}UseCase({pascal}Repository repository) {{ + return new Create{pascal}UseCase(repository); + }} +"#, + pascal = pascal, + )); + } + } + + let bean_content = format!(r#"package {bean_package}; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +{bean_imports} +@Configuration +public class BeanConfiguration {{ +{bean_definitions}}} +"#, + bean_package = bean_package, + bean_imports = bean_imports, + bean_definitions = bean_definitions, + ); + + write_file(&path.join("BeanConfiguration.java"), &bean_content)?; + + if self.config.features.swagger { + let swagger_content = format!(r#"package {bean_package}.doc; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.Contact; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAPIConfiguration {{ + + @Bean + public OpenAPI customOpenAPI() {{ + return new OpenAPI() + .info(new Info() + .title("{app_name} API") + .version("1.0.0") + .description("{description}") + .contact(new Contact() + .name("{dev_name}") + .email("{dev_email}") + .url("{dev_url}"))); + }} +}} +"#, + bean_package = bean_package, + app_name = self.config.project.name, + description = self.config.project.description, + dev_name = self.config.project.developer.name, + dev_email = self.config.project.developer.email, + dev_url = self.config.project.developer.url, + ); + + let doc_path = path.join("doc"); + fs::create_dir_all(&doc_path)?; + write_file(&doc_path.join("OpenAPIConfiguration.java"), &swagger_content)?; + } + + if self.config.features.spring_security { + self.generate_security_config(layer, path)?; } + + Ok(()) + } + + fn generate_security_config(&self, layer: &PhysicalLayer, path: &Path) -> io::Result<()> { + let package = self.get_package(layer, "config.security"); + let security_path = path.join("security"); + fs::create_dir_all(&security_path)?; + + let security_config = format!(r#"package {package}; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig {{ + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {{ + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + }} + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {{ + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authz -> authz + .requestMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll() + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/api/test/public", "/api/test/health").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + }} + + @Bean + public PasswordEncoder passwordEncoder() {{ + return new BCryptPasswordEncoder(); + }} +}} +"#, + package = package, + ); + + write_file(&security_path.join("SecurityConfig.java"), &security_config)?; + + let jwt_filter = format!(r#"package {package}; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter {{ + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException {{ + + String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) {{ + String token = authHeader.substring(7); + // TODO: Validate JWT token and extract user details + // For now, this is a placeholder implementation + + if (isValidToken(token)) {{ + var authorities = List.of(new SimpleGrantedAuthority("ROLE_USER")); + var auth = new UsernamePasswordAuthenticationToken("user", null, authorities); + SecurityContextHolder.getContext().setAuthentication(auth); + }} + }} + + filterChain.doFilter(request, response); + }} + + private boolean isValidToken(String token) {{ + // TODO: Implement actual JWT validation + return token != null && !token.isEmpty(); + }} +}} +"#, + package = package, + ); + + write_file(&security_path.join("JwtAuthenticationFilter.java"), &jwt_filter) + } + + fn generate_application_class(&self, layer: &PhysicalLayer) -> io::Result<()> { + let app_name_clean = to_app_name_clean(&self.config.project.name); + let package = format!("{}.{}", self.config.project.domain, layer.name); + + let layer_root = self.root.join(&layer.name); + let path = layer_root + .join("src/main/java") + .join(domain_to_path(&self.config.project.domain)) + .join(&layer.name); + + let content = format!(r#"package {package}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@ComponentScan(basePackages = "{domain}") +@EntityScan(basePackages = "{domain}") +@EnableJpaRepositories(basePackages = "{domain}") +public class {app_name_clean}Application {{ + + public static void main(String[] args) {{ + SpringApplication.run({app_name_clean}Application.class, args); + }} +}} +"#, + package = package, + domain = self.config.project.domain, + app_name_clean = app_name_clean, + ); + + write_file(&path.join(format!("{}Application.java", app_name_clean)), &content) + } + + fn generate_application_properties(&self, layer: &PhysicalLayer, path: &Path) -> io::Result<()> { + if !layer.is_main { + return Ok(()); + } + + let mut content = format!(r#"# Application Configuration +spring.application.name={app_name} + +# Server Configuration +server.port=8080 + +# Database Configuration (H2 for development) +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.h2.console.enabled=true + +# JPA Configuration +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# PostgreSQL Configuration (uncomment for production) +# spring.datasource.url=jdbc:postgresql://localhost:5432/{app_name_lower} +# spring.datasource.username=postgres +# spring.datasource.password=postgres +# spring.jpa.hibernate.ddl-auto=validate +"#, + app_name = self.config.project.name, + app_name_lower = self.config.project.name.to_lowercase(), + ); + + if self.config.features.swagger { + content.push_str(r#" +# Swagger/OpenAPI Configuration +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true +springdoc.swagger-ui.path=/swagger-ui.html +"#); + } + + if self.config.features.flyway { + content.push_str(r#" +# Flyway Configuration +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration +"#); + + let migration_path = path.join("db/migration"); + fs::create_dir_all(&migration_path)?; + } + + if self.config.features.spring_security { + content.push_str(r#" +# JWT Configuration +jwt.secret=your-secret-key-here-change-in-production +jwt.expiration=86400000 +"#); + } + + write_file(&path.join("application.properties"), &content) + } + + fn generate_gitignore(&self) -> io::Result<()> { + let content = r#"# Compiled class files +*.class + +# Log files +*.log + +# Package files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Application +application-local.properties +application-*.yml +!application.yml + +# Secrets +*.env +.env.local +"#; + + write_file(&self.root.join(".gitignore"), content) + } + + fn generate_gitattributes(&self) -> io::Result<()> { + let content = r#"* text=auto eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +"#; + + write_file(&self.root.join(".gitattributes"), content) + } + + fn generate_mvnw(&self) -> io::Result<()> { + let wrapper_path = self.root.join(".mvn/wrapper"); + fs::create_dir_all(&wrapper_path)?; + + let wrapper_props = r#"distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar +"#; + write_file(&wrapper_path.join("maven-wrapper.properties"), wrapper_props)?; + + let mvnw = r#"#!/bin/sh +exec mvn "$@" +"#; + let mvnw_path = self.root.join("mvnw"); + write_file(&mvnw_path, mvnw)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&mvnw_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&mvnw_path, perms)?; + } + + let mvnw_cmd = r#"@echo off +mvn %* +"#; + write_file(&self.root.join("mvnw.cmd"), mvnw_cmd) } - Ok(()) } -fn init_project() -> io::Result<()> { - println!("{}", BANNER); +fn create_default_template() -> ProjectConfig { + ProjectConfig { + project: ProjectInfo { + name: "myAPI".to_string(), + domain: "com.example.demo".to_string(), + description: "Spring Boot API".to_string(), + developer: DeveloperInfo { + name: "Developer".to_string(), + email: "dev@example.com".to_string(), + url: "example.com".to_string(), + }, + }, + layers: LayersConfig { + physical: vec![ + PhysicalLayer { + name: "core".to_string(), + logical: vec![ + "domain/entities".to_string(), + "domain/valueobjects".to_string(), + ], + dependencies: vec![], + is_main: false, + }, + PhysicalLayer { + name: "application".to_string(), + logical: vec![ + "usecases".to_string(), + "ports/repositories".to_string(), + "ports/services".to_string(), + ], + dependencies: vec!["core".to_string()], + is_main: false, + }, + PhysicalLayer { + name: "infrastructure".to_string(), + logical: vec![ + "adapters/web/controllers".to_string(), + "adapters/web/dto".to_string(), + "adapters/repositories".to_string(), + "adapters/services".to_string(), + "config".to_string(), + ], + dependencies: vec!["core".to_string(), "application".to_string()], + is_main: true, + }, + ], + entity_location: Some(EntityLocation { + layer: "core".to_string(), + logical: "domain/entities".to_string(), + }), + }, + entities: vec![ + EntityConfig { + name: "User".to_string(), + role: "USER".to_string(), + }, + ], + features: FeaturesConfig { + spring_security: false, + example_endpoints: true, + swagger: true, + flyway: false, + }, + } +} - let app_name = prompt_required("Application name (e.g.: myAPI)"); - let app_name_clean = to_app_name_clean(&app_name); - - let user_entity_input = prompt_required("Enter the name of a user entity (e.g.: UserAccount)"); - let user_entity_pascal = to_pascal_case(&user_entity_input); - let user_entity_lower = to_lower_first(&user_entity_pascal); - let table_name = to_snake_case(&user_entity_pascal); - - let your_domain = prompt_required("Your domain (e.g.: com.example.demo)"); - - let description = prompt("Description", Some("defauil -> api")); - let develop_name = prompt("Developer name", Some("defauil -> erique.dev")); - let develop_mail = prompt("Developer email", Some("defauil -> contato@erique.dev")); - let develop_url = prompt("Your website", Some("defauil -> erique.dev")); - - let mut ph = HashMap::new(); - ph.insert("userEntity", user_entity_lower.clone()); - ph.insert("UserEntity", user_entity_pascal.clone()); - ph.insert("tableName", table_name.clone()); - ph.insert("appName", app_name.clone()); - ph.insert("yourDomain", your_domain.clone()); - ph.insert("AppNameClean", app_name_clean.clone()); - ph.insert("description", description.clone()); - ph.insert("developName", develop_name.clone()); - ph.insert("developMail", develop_mail.clone()); - ph.insert("developUrl", develop_url.clone()); - - let dst_root = PathBuf::from(&app_name); - if dst_root.exists() { - eprintln!("there is already a folder named: {} (remove or choose another application name).", dst_root.display()); +fn init_interactive() -> io::Result<()> { + let config = build_config_interactive(); + + println!("\n=== Configuration Summary ===\n"); + println!("{}", serde_json::to_string_pretty(&config).unwrap()); + + if !prompt_yes_no("\nGenerate project with this configuration?", true) { + println!("Cancelled."); + return Ok(()); + } + + if prompt_yes_no("Save configuration to JSON file?", false) { + let mut json_path = prompt("JSON file path", Some(&format!("{}.json", config.project.name))); + if !json_path.ends_with(".json") { + json_path.push_str(".json"); + } + let json_content = serde_json::to_string_pretty(&config).unwrap(); + fs::write(&json_path, json_content)?; + println!(" {GREEN}✓ Configuration saved to: {}{RESET}", json_path); + } + + let root = PathBuf::from(&config.project.name); + if root.exists() { + eprintln!("Error: Directory '{}' already exists.", root.display()); std::process::exit(1); } - println!("\nGenerating project -> {}", dst_root.display()); - extract_embedded_template(&dst_root)?; + let generator = CodeGenerator::new(&config, root); + generator.generate() +} + +fn generate_from_json(config_path: &Path) -> io::Result<()> { + println!("{}", BANNER); + + let content = fs::read_to_string(config_path)?; + let config: ProjectConfig = serde_json::from_str(&content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - let re_user_entity = Regex::new(r"\bUserEntity\b").unwrap(); - let re_app_clean_app = Regex::new(r"\bAppNameCleanApplication\b").unwrap(); - let replacements: Vec<(Regex, String)> = vec![ - (re_user_entity, user_entity_pascal.clone()), - (re_app_clean_app, format!("{}Application", app_name_clean)), - ]; + println!("Loaded configuration from: {}", config_path.display()); + println!("Project name: {}", config.project.name); + println!("Domain: {}", config.project.domain); + println!("Physical layers: {:?}", config.layers.physical.iter().map(|l| &l.name).collect::>()); - let domain_parts = domain_to_parts(&your_domain); - remap_your_domain_paths(&dst_root, &domain_parts)?; + let root = PathBuf::from(&config.project.name); + if root.exists() { + eprintln!("Error: Directory '{}' already exists.", root.display()); + std::process::exit(1); + } - rename_files_by_tokens(&dst_root, &user_entity_pascal, &app_name_clean)?; + let generator = CodeGenerator::new(&config, root); + generator.generate() +} - edit_file_contents(&dst_root, &ph, &replacements)?; +fn export_template(output: Option<&Path>) -> io::Result<()> { + let template = create_default_template(); + let json = serde_json::to_string_pretty(&template).unwrap(); + + match output { + Some(path) => { + fs::write(path, &json)?; + println!("Template exported to: {}", path.display()); + } + None => { + println!("{}", json); + } + } - println!("\nCompleted, Project generated in: {}", dst_root.display()); Ok(()) } fn main() { let cli = Cli::parse(); - match &cli.command { - Commands::Init => { - if let Err(e) = init_project() { - eprintln!("Error initializing project: {}", e); - std::process::exit(1); - } - } + let result = match &cli.command { + Commands::Init => init_interactive(), + Commands::Generate { config } => generate_from_json(config), + Commands::Template { output } => export_template(output.as_deref()), Commands::Version => { - println!("dss version 0.1.0"); + println!("dss version 0.2.0"); + Ok(()) } + }; + + if let Err(e) = result { + eprintln!("Error: {}", e); + std::process::exit(1); } -} \ No newline at end of file +} diff --git a/template/3layer/.gitattributes b/template/3layer/.gitattributes deleted file mode 100644 index 3b41682..0000000 --- a/template/3layer/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/mvnw text eol=lf -*.cmd text eol=crlf diff --git a/template/3layer/.gitignore b/template/3layer/.gitignore deleted file mode 100644 index 667aaef..0000000 --- a/template/3layer/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/template/3layer/application/pom.xml b/template/3layer/application/pom.xml deleted file mode 100644 index 5bcae98..0000000 --- a/template/3layer/application/pom.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - 4.0.0 - - - {{yourDomain}} - {{appName}} - 1.0.0 - - - application - - - - {{yourDomain}} - core - 1.0.0 - - - \ No newline at end of file diff --git a/template/3layer/application/src/main/java/your/domain/application/ports/repositories/AdminRepository.java b/template/3layer/application/src/main/java/your/domain/application/ports/repositories/AdminRepository.java deleted file mode 100644 index ad29d2e..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/ports/repositories/AdminRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package {{yourDomain}}.application.ports.repositories; - -import {{yourDomain}}.core.domain.entities.Admin; - -import java.util.Optional; - -public interface AdminRepository { - Optional findByEmail(String email); - Admin save(Admin admin); - Optional findById(Long id); -} diff --git a/template/3layer/application/src/main/java/your/domain/application/ports/repositories/UserEntityRepository.java b/template/3layer/application/src/main/java/your/domain/application/ports/repositories/UserEntityRepository.java deleted file mode 100644 index fac5e17..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/ports/repositories/UserEntityRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package {{yourDomain}}.application.ports.repositories; - -import {{yourDomain}}.core.domain.entities.{{UserEntity}}; - -import java.util.Optional; - -public interface {{UserEntity}}Repository { - Optional<{{UserEntity}}> findByEmail(String email); - {{UserEntity}} save({{UserEntity}} {{userEntity}}); - Optional<{{UserEntity}}> findById(Long id); -} \ No newline at end of file diff --git a/template/3layer/application/src/main/java/your/domain/application/ports/services/PasswordService.java b/template/3layer/application/src/main/java/your/domain/application/ports/services/PasswordService.java deleted file mode 100644 index a9c6cbd..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/ports/services/PasswordService.java +++ /dev/null @@ -1,6 +0,0 @@ -package {{yourDomain}}.application.ports.services; - -public interface PasswordService { - String encode(String rawPassword); - boolean matches(String rawPassword, String encodedPassword); -} diff --git a/template/3layer/application/src/main/java/your/domain/application/ports/services/TokenService.java b/template/3layer/application/src/main/java/your/domain/application/ports/services/TokenService.java deleted file mode 100644 index 0a02890..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/ports/services/TokenService.java +++ /dev/null @@ -1,8 +0,0 @@ -package {{yourDomain}}.application.ports.services; - -public interface TokenService { - String generateToken(String email, String role); - boolean validateToken(String token); - String extractEmail(String token); - String extractRole(String token); -} diff --git a/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateAdminUseCase.java b/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateAdminUseCase.java deleted file mode 100644 index 58bdcd4..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateAdminUseCase.java +++ /dev/null @@ -1,48 +0,0 @@ -package {{yourDomain}}.application.usecases; - -import {{yourDomain}}.application.ports.repositories.AdminRepository; -import {{yourDomain}}.application.ports.services.PasswordService; -import {{yourDomain}}.application.ports.services.TokenService; -import {{yourDomain}}.core.domain.entities.Admin; -import {{yourDomain}}.core.domain.valueobjects.AuthenticationResult; - -import java.util.Optional; - -public class AuthenticateAdminUseCase { - - private final AdminRepository adminRepository; - private final PasswordService passwordService; - private final TokenService tokenService; - - public AuthenticateAdminUseCase(AdminRepository adminRepository, - PasswordService passwordService, - TokenService tokenService) { - this.adminRepository = adminRepository; - this.passwordService = passwordService; - this.tokenService = tokenService; - } - - public Optional execute(String email, String password) { - Optional adminOpt = adminRepository.findByEmail(email); - - if (adminOpt.isEmpty()) { - return Optional.empty(); - } - - Admin admin = adminOpt.get(); - - if (!passwordService.matches(password, admin.getPassword())) { - return Optional.empty(); - } - - String token = tokenService.generateToken(admin.getEmail(), admin.getRole()); - - return Optional.of(new AuthenticationResult( - token, - admin.getRole(), - admin.getEmail(), - admin.getName() - )); - } - -} diff --git a/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateUserEntityUseCase.java b/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateUserEntityUseCase.java deleted file mode 100644 index b59ebd2..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/usecases/AuthenticateUserEntityUseCase.java +++ /dev/null @@ -1,48 +0,0 @@ -package {{yourDomain}}.application.usecases; - -import {{yourDomain}}.application.ports.repositories.{{UserEntity}}Repository; -import {{yourDomain}}.application.ports.services.PasswordService; -import {{yourDomain}}.application.ports.services.TokenService; -import {{yourDomain}}.core.domain.entities.{{UserEntity}}; -import {{yourDomain}}.core.domain.valueobjects.AuthenticationResult; - -import java.util.Optional; - -public class Authenticate{{UserEntity}}UseCase { - - private final {{UserEntity}}Repository {{userEntity}}Repository; - private final PasswordService passwordService; - private final TokenService tokenService; - - public Authenticate{{UserEntity}}UseCase({{UserEntity}}Repository {{userEntity}}Repository, - PasswordService passwordService, - TokenService tokenService) { - this.{{userEntity}}Repository = {{userEntity}}Repository; - this.passwordService = passwordService; - this.tokenService = tokenService; - } - - public Optional execute(String email, String password) { - Optional<{{UserEntity}}> {{userEntity}}Opt = {{userEntity}}Repository.findByEmail(email); - - if ({{userEntity}}Opt.isEmpty()) { - return Optional.empty(); - } - - {{UserEntity}} {{userEntity}} = {{userEntity}}Opt.get(); - - if (!passwordService.matches(password, {{userEntity}}.getPassword())) { - return Optional.empty(); - } - - String token = tokenService.generateToken({{userEntity}}.getEmail(), {{userEntity}}.getRole()); - - return Optional.of(new AuthenticationResult( - token, - {{userEntity}}.getRole(), - {{userEntity}}.getEmail(), - {{userEntity}}.getName() - )); - } - -} diff --git a/template/3layer/application/src/main/java/your/domain/application/usecases/CreateUserEntityUseCase.java b/template/3layer/application/src/main/java/your/domain/application/usecases/CreateUserEntityUseCase.java deleted file mode 100644 index 16ab398..0000000 --- a/template/3layer/application/src/main/java/your/domain/application/usecases/CreateUserEntityUseCase.java +++ /dev/null @@ -1,30 +0,0 @@ -package {{yourDomain}}.application.usecases; - -import {{yourDomain}}.application.ports.repositories.{{UserEntity}}Repository; -import {{yourDomain}}.application.ports.services.PasswordService; -import {{yourDomain}}.core.domain.entities.UserEntity; - -import java.util.Optional; - -public class Create{{UserEntity}}UseCase { - private final {{UserEntity}}Repository {{userEntity}}Repository; - private final PasswordService passwordService; - - public Create{{UserEntity}}UseCase({{UserEntity}}Repository {{userEntity}}Repository, PasswordService passwordService) { - this.{{userEntity}}Repository = {{userEntity}}Repository; - this.passwordService = passwordService; - } - - public Optional<{{UserEntity}}> execute(String email, String password, String name) { - Optional<{{UserEntity}}> existing{{UserEntity}} = {{userEntity}}Repository.findByEmail(email); - if (existing{{UserEntity}}.isPresent()) { - return Optional.empty(); - } - - String encodedPassword = passwordService.encode(password); - {{UserEntity}} new{{UserEntity}} = new {{UserEntity}}(email, encodedPassword, name); - {{UserEntity}} saved{{UserEntity}} = {{userEntity}}Repository.save(new{{UserEntity}}); - - return Optional.of(saved{{UserEntity}}); - } -} diff --git a/template/3layer/application/src/main/resources/application.properties b/template/3layer/application/src/main/resources/application.properties deleted file mode 100644 index de94ecd..0000000 --- a/template/3layer/application/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name={{appName}} diff --git a/template/3layer/application/src/test/java/dev/falae/api/ApplicationTests.java b/template/3layer/application/src/test/java/dev/falae/api/ApplicationTests.java deleted file mode 100644 index 4ad82ff..0000000 --- a/template/3layer/application/src/test/java/dev/falae/api/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.falae.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/template/3layer/core/pom.xml b/template/3layer/core/pom.xml deleted file mode 100644 index 7b78be2..0000000 --- a/template/3layer/core/pom.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - 4.0.0 - - - {{yourDomain}} - {{appName}} - 1.0.0 - - - core - - - - \ No newline at end of file diff --git a/template/3layer/core/src/main/java/your/domain/core/domain/entities/Admin.java b/template/3layer/core/src/main/java/your/domain/core/domain/entities/Admin.java deleted file mode 100644 index 688085d..0000000 --- a/template/3layer/core/src/main/java/your/domain/core/domain/entities/Admin.java +++ /dev/null @@ -1,98 +0,0 @@ -package {{yourDomain}}.core.domain.entities; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class Admin { - private Long id; - private String email; - private String password; - private String name; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public Admin() {} - - public Admin(String email, String password, String name) { - this.email = email; - this.password = password; - this.name = name; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); - } - - public Admin(Long id, String email, String password, String name, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.email = email; - this.password = password; - this.name = name; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public String getRole() { - return "ADMIN"; - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Admin admin = (Admin) o; - return Objects.equals(id, admin.id) && Objects.equals(email, admin.email); - } - - @Override - public int hashCode() { - return Objects.hash(id, email); - } -} diff --git a/template/3layer/core/src/main/java/your/domain/core/domain/entities/UserEntity.java b/template/3layer/core/src/main/java/your/domain/core/domain/entities/UserEntity.java deleted file mode 100644 index bceec5a..0000000 --- a/template/3layer/core/src/main/java/your/domain/core/domain/entities/UserEntity.java +++ /dev/null @@ -1,98 +0,0 @@ -package {{yourDomain}}.core.domain.entities; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class {{UserEntity}} { - private Long id; - private String email; - private String password; - private String name; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public {{UserEntity}}() {} - - public {{UserEntity}}(String email, String password, String name) { - this.email = email; - this.password = password; - this.name = name; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); - } - - public {{UserEntity}}(Long id, String email, String password, String name, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.email = email; - this.password = password; - this.name = name; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public String getRole() { - return "AUTHOR"; - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - {{UserEntity}} {{UserEntity}} = ({{UserEntity}}) o; - return Objects.equals(id, {{UserEntity}}.id) && Objects.equals(email, {{UserEntity}}.email); - } - - @Override - public int hashCode() { - return Objects.hash(id, email); - } -} diff --git a/template/3layer/core/src/main/java/your/domain/core/domain/valueobjects/AuthenticationResult.java b/template/3layer/core/src/main/java/your/domain/core/domain/valueobjects/AuthenticationResult.java deleted file mode 100644 index 496b8de..0000000 --- a/template/3layer/core/src/main/java/your/domain/core/domain/valueobjects/AuthenticationResult.java +++ /dev/null @@ -1,31 +0,0 @@ -package {{yourDomain}}.core.domain.valueobjects; - -public class AuthenticationResult { - private final String token; - private final String role; - private final String email; - private final String name; - - public AuthenticationResult(String token, String role, String email, String name) { - this.token = token; - this.role = role; - this.email = email; - this.name = name; - } - - public String getToken() { - return token; - } - - public String getRole() { - return role; - } - - public String getEmail() { - return email; - } - - public String getName() { - return name; - } -} diff --git a/template/3layer/core/src/main/resources/application.properties b/template/3layer/core/src/main/resources/application.properties deleted file mode 100644 index de94ecd..0000000 --- a/template/3layer/core/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name={{appName}} diff --git a/template/3layer/core/src/test/java/dev/falae/api/ApplicationTests.java b/template/3layer/core/src/test/java/dev/falae/api/ApplicationTests.java deleted file mode 100644 index 4ad82ff..0000000 --- a/template/3layer/core/src/test/java/dev/falae/api/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.falae.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/template/3layer/infrastructure/pom.xml b/template/3layer/infrastructure/pom.xml deleted file mode 100644 index 6dcdc61..0000000 --- a/template/3layer/infrastructure/pom.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - 4.0.0 - - - {{yourDomain}} - {{appName}} - 1.0.0 - - - infrastructure - - - - {{yourDomain}} - application - 1.0.0 - - - {{yourDomain}} - core - 1.0.0 - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.13 - - - org.apache.commons - commons-lang3 - 3.18.0 - - - - org.postgresql - postgresql - runtime - - - - org.flywaydb - flyway-core - 11.13.2 - - - - org.flywaydb - flyway-database-postgresql - 11.13.2 - runtime - - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - \ No newline at end of file diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/AppNameCleanApplication.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/AppNameCleanApplication.java deleted file mode 100644 index c73b9de..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/AppNameCleanApplication.java +++ /dev/null @@ -1,23 +0,0 @@ -package {{yourDomain}}.infrastructure; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -@SpringBootApplication -@ComponentScan(basePackages = { - "{{yourDomain}}.application", - "{{yourDomain}}.core", - "{{yourDomain}}.infrastructure" -}) -@EnableJpaRepositories(basePackages = "{{yourDomain}}.infrastructure.adapters.repositories.jpa") -@EntityScan(basePackages = "{{yourDomain}}.infrastructure.adapters.repositories.entities") -public class {{AppNameClean}}Application { - - public static void main(String[] args) { - SpringApplication.run({{AppNameClean}}Application.class, args); - } - -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaAdminRepository.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaAdminRepository.java deleted file mode 100644 index ccc6bbd..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaAdminRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories; - -import {{yourDomain}}.application.ports.repositories.AdminRepository; -import {{yourDomain}}.core.domain.entities.Admin; -import {{yourDomain}}.infrastructure.adapters.repositories.entities.AdminEntity; -import {{yourDomain}}.infrastructure.adapters.repositories.jpa.AdminJpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public class JpaAdminRepository implements AdminRepository { - private final AdminJpaRepository jpaRepository; - - public JpaAdminRepository(AdminJpaRepository jpaRepository) { - this.jpaRepository = jpaRepository; - } - - @Override - public Optional findByEmail(String email) { - return jpaRepository.findByEmail(email) - .map(this::toDomain); - } - - @Override - public Admin save(Admin admin) { - AdminEntity entity = toEntity(admin); - AdminEntity savedEntity = jpaRepository.save(entity); - return toDomain(savedEntity); - } - - @Override - public Optional findById(Long id) { - return jpaRepository.findById(id) - .map(this::toDomain); - } - - private Admin toDomain(AdminEntity entity) { - return new Admin( - entity.getId(), - entity.getEmail(), - entity.getPassword(), - entity.getName(), - entity.getCreatedAt(), - entity.getUpdatedAt() - ); - } - - private AdminEntity toEntity(Admin domain) { - AdminEntity entity = new AdminEntity(); - entity.setId(domain.getId()); - entity.setEmail(domain.getEmail()); - entity.setPassword(domain.getPassword()); - entity.setName(domain.getName()); - entity.setCreatedAt(domain.getCreatedAt()); - entity.setUpdatedAt(domain.getUpdatedAt()); - return entity; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaUserEntityRepository.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaUserEntityRepository.java deleted file mode 100644 index 710a386..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/JpaUserEntityRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories; - -import {{yourDomain}}.application.ports.repositories.{{UserEntity}}Repository; -import {{yourDomain}}.core.domain.entities.{{UserEntity}}; -import {{yourDomain}}.infrastructure.adapters.repositories.entities.{{UserEntity}}Entity; -import {{yourDomain}}.infrastructure.adapters.repositories.jpa.{{UserEntity}}JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public class Jpa{{UserEntity}}Repository implements {{UserEntity}}Repository { - private final {{UserEntity}}JpaRepository jpaRepository; - - public Jpa{{UserEntity}}Repository({{UserEntity}}JpaRepository jpaRepository) { - this.jpaRepository = jpaRepository; - } - - @Override - public Optional<{{UserEntity}}> findByEmail(String email) { - return jpaRepository.findByEmail(email) - .map(this::toDomain); - } - - @Override - public {{UserEntity}} save({{UserEntity}} {{userEntity}}) { - {{UserEntity}}Entity entity = toEntity({{userEntity}}); - {{UserEntity}}Entity savedEntity = jpaRepository.save(entity); - return toDomain(savedEntity); - } - - @Override - public Optional<{{UserEntity}}> findById(Long id) { - return jpaRepository.findById(id) - .map(this::toDomain); - } - - private {{UserEntity}} toDomain({{UserEntity}}Entity entity) { - return new {{UserEntity}}( - entity.getId(), - entity.getEmail(), - entity.getPassword(), - entity.getName(), - entity.getCreatedAt(), - entity.getUpdatedAt() - ); - } - - private {{UserEntity}}Entity toEntity({{UserEntity}} domain) { - {{UserEntity}}Entity entity = new {{UserEntity}}Entity(); - entity.setId(domain.getId()); - entity.setEmail(domain.getEmail()); - entity.setPassword(domain.getPassword()); - entity.setName(domain.getName()); - entity.setCreatedAt(domain.getCreatedAt()); - entity.setUpdatedAt(domain.getUpdatedAt()); - return entity; - } -} \ No newline at end of file diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/AdminEntity.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/AdminEntity.java deleted file mode 100644 index a491435..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/AdminEntity.java +++ /dev/null @@ -1,88 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories.entities; - -import jakarta.persistence.*; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "admins") -public class AdminEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = true, nullable = false) - private String email; - - @Column(nullable = false) - private String password; - - @Column(nullable = false) - private String name; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - } - - @PreUpdate - protected void onUpdate() { - updatedAt = LocalDateTime.now(); - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/UserEntityEntity.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/UserEntityEntity.java deleted file mode 100644 index 36b7017..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/entities/UserEntityEntity.java +++ /dev/null @@ -1,88 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories.entities; - -import jakarta.persistence.*; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "{{tableName}}") -public class {{UserEntity}}Entity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = true, nullable = false) - private String email; - - @Column(nullable = false) - private String password; - - @Column(nullable = false) - private String name; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - } - - @PreUpdate - protected void onUpdate() { - updatedAt = LocalDateTime.now(); - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/AdminJpaRepository.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/AdminJpaRepository.java deleted file mode 100644 index 6de9f7e..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/AdminJpaRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories.jpa; - -import {{yourDomain}}.infrastructure.adapters.repositories.entities.AdminEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AdminJpaRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/UserEntityJpaRepository.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/UserEntityJpaRepository.java deleted file mode 100644 index b60152a..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/repositories/jpa/UserEntityJpaRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.repositories.jpa; - -import {{yourDomain}}.infrastructure.adapters.repositories.entities.{{UserEntity}}Entity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface {{UserEntity}}JpaRepository extends JpaRepository<{{UserEntity}}Entity, Long> { - Optional<{{UserEntity}}Entity> findByEmail(String email); -} \ No newline at end of file diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/BCryptPasswordService.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/BCryptPasswordService.java deleted file mode 100644 index 9f6402d..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/BCryptPasswordService.java +++ /dev/null @@ -1,24 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.services; - -import {{yourDomain}}.application.ports.services.PasswordService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; - -@Service -public class BCryptPasswordService implements PasswordService { - private final BCryptPasswordEncoder encoder; - - public BCryptPasswordService() { - this.encoder = new BCryptPasswordEncoder(); - } - - @Override - public String encode(String rawPassword) { - return encoder.encode(rawPassword); - } - - @Override - public boolean matches(String rawPassword, String encodedPassword) { - return encoder.matches(rawPassword, encodedPassword); - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/JwtTokenService.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/JwtTokenService.java deleted file mode 100644 index ad88933..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/services/JwtTokenService.java +++ /dev/null @@ -1,81 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.services; - -import {{yourDomain}}.application.ports.services.TokenService; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import javax.crypto.SecretKey; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtTokenService implements TokenService { - - @Value("${jwt.secret:mySecretKey}") - private String secret; - - @Value("${jwt.expiration:86400000}") - private Long expiration; - - private SecretKey getSigningKey() { - return Keys.hmacShaKeyFor(secret.getBytes()); - } - - @Override - public String generateToken(String email, String role) { - Map claims = new HashMap<>(); - claims.put("role", role); - return createToken(claims, email); - } - - private String createToken(Map claims, String subject) { - return Jwts.builder() - .setClaims(claims) - .setSubject(subject) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + expiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS512) - .compact(); - } - - @Override - public boolean validateToken(String token) { - try { - Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token); - return true; - } catch (Exception e) { - return false; - } - } - - @Override - public String extractEmail(String token) { - return extractClaim(token, Claims::getSubject); - } - - @Override - public String extractRole(String token) { - return extractClaim(token, claims -> claims.get("role", String.class)); - } - - private T extractClaim(String token, java.util.function.Function claimsResolver) { - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); - } - - private Claims extractAllClaims(String token) { - return Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token) - .getBody(); - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/AuthController.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/AuthController.java deleted file mode 100644 index 6e2a92a..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/AuthController.java +++ /dev/null @@ -1,97 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.controllers; - -import {{yourDomain}}.application.usecases.AuthenticateAdminUseCase; -import {{yourDomain}}.application.usecases.Authenticate{{UserEntity}}UseCase; -import {{yourDomain}}.core.domain.valueobjects.AuthenticationResult; -import {{yourDomain}}.infrastructure.adapters.web.dto.LoginRequest; -import {{yourDomain}}.infrastructure.adapters.web.dto.LoginResponse; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Optional; - -@RestController -@RequestMapping("/auth") -public class AuthController { - private final Authenticate{{UserEntity}}UseCase authenticate{{UserEntity}}UseCase; - private final AuthenticateAdminUseCase authenticateAdminUseCase; - - public AuthController(Authenticate{{UserEntity}}UseCase authenticate{{UserEntity}}UseCase, - AuthenticateAdminUseCase authenticateAdminUseCase) { - this.authenticate{{UserEntity}}UseCase = authenticate{{UserEntity}}UseCase; - this.authenticateAdminUseCase = authenticateAdminUseCase; - } - - @PostMapping("/{{userEntity}}/login") - public ResponseEntity login{{UserEntity}}(@RequestBody LoginRequest request, - HttpServletResponse response) { - Optional result = authenticate{{UserEntity}}UseCase.execute( - request.getEmail(), - request.getPassword() - ); - - if (result.isEmpty()) { - return ResponseEntity.badRequest() - .body(new LoginResponse("Invalid credentials", null, null, null)); - } - - AuthenticationResult authResult = result.get(); - setTokenCookie(response, authResult.getToken()); - - return ResponseEntity.ok(new LoginResponse( - "Login successful", - authResult.getRole(), - authResult.getEmail(), - authResult.getName() - )); - } - - @PostMapping("/admin/login") - public ResponseEntity loginAdmin(@RequestBody LoginRequest request, - HttpServletResponse response) { - Optional result = authenticateAdminUseCase.execute( - request.getEmail(), - request.getPassword() - ); - - if (result.isEmpty()) { - return ResponseEntity.badRequest() - .body(new LoginResponse("Invalid credentials", null, null, null)); - } - - AuthenticationResult authResult = result.get(); - setTokenCookie(response, authResult.getToken()); - - return ResponseEntity.ok(new LoginResponse( - "Login successful", - authResult.getRole(), - authResult.getEmail(), - authResult.getName() - )); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - Cookie cookie = new Cookie("token", null); - cookie.setMaxAge(0); - cookie.setHttpOnly(true); - cookie.setPath("/"); - response.addCookie(cookie); - - return ResponseEntity.ok("Logout successful"); - } - - private void setTokenCookie(HttpServletResponse response, String token) { - Cookie cookie = new Cookie("token", token); - cookie.setHttpOnly(true); - cookie.setSecure(false); // Set to true in production with HTTPS - cookie.setPath("/"); - cookie.setMaxAge(24 * 60 * 60); // 24 hours - response.addCookie(cookie); - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/TestController.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/TestController.java deleted file mode 100644 index 21be61b..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/TestController.java +++ /dev/null @@ -1,26 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.controllers; - -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api") -public class TestController { - - @GetMapping("/admin/dashboard") - public String adminDashboard(Authentication authentication) { - return "Welcome to Admin Dashboard, " + authentication.getName() + "!"; - } - - @GetMapping("/{{userEntity}}/profile") - public String authorProfile(Authentication authentication) { - return "Welcome to Author Profile, " + authentication.getName() + "!"; - } - - @GetMapping("/public") - public String publicEndpoint() { - return "This is a public endpoint!"; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/UserEntityController.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/UserEntityController.java deleted file mode 100644 index efe6a2a..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/controllers/UserEntityController.java +++ /dev/null @@ -1,53 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.controllers; - -import {{yourDomain}}.application.usecases.Create{{UserEntity}}UseCase; -import {{yourDomain}}.core.domain.entities.{{UserEntity}}; -import {{yourDomain}}.infrastructure.adapters.web.dto.Create{{UserEntity}}Request; -import {{yourDomain}}.infrastructure.adapters.web.dto.Create{{UserEntity}}Response; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Optional; - -@RestController -@RequestMapping("/api/{{userEntity}}") -public class {{UserEntity}}Controller { - private final Create{{UserEntity}}UseCase create{{UserEntity}}UseCase; - - public {{UserEntity}}Controller(Create{{UserEntity}}UseCase create{{UserEntity}}UseCase) { - this.create{{UserEntity}}UseCase = create{{UserEntity}}UseCase; - } - - @PostMapping - public ResponseEntity create{{UserEntity}}(@Valid @RequestBody Create{{UserEntity}}Request request) { - Optional<{{UserEntity}}> result = create{{UserEntity}}UseCase.execute( - request.getEmail(), - request.getPassword(), - request.getName() - ); - - if (result.isEmpty()) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(new Create{{UserEntity}}Response( - "Email already exists", - null, - null, - null - )); - } - - {{UserEntity}} created{{UserEntity}} = result.get(); - return ResponseEntity.status(HttpStatus.CREATED) - .body(new Create{{UserEntity}}Response( - "{{UserEntity}} created successfully", - created{{UserEntity}}.getId(), - created{{UserEntity}}.getEmail(), - created{{UserEntity}}.getName() - )); - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityRequest.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityRequest.java deleted file mode 100644 index cac0b09..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityRequest.java +++ /dev/null @@ -1,52 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -public class Create{{UserEntity}}Request { - - @NotBlank(message = "Email is required") - @Email(message = "Email should be valid") - private String email; - - @NotBlank(message = "Password is required") - @Size(min = 6, message = "Password must be at least 6 characters") - private String password; - - @NotBlank(message = "Name is required") - @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") - private String name; - - public Create{{UserEntity}}Request() {} - - public Create{{UserEntity}}Request(String email, String password, String name) { - this.email = email; - this.password = password; - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityResponse.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityResponse.java deleted file mode 100644 index 88d0db4..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/CreateUserEntityResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.dto; - -public class Create{{UserEntity}}Response { - private String message; - private Long id; - private String email; - private String name; - - public Create{{UserEntity}}Response() {} - - public Create{{UserEntity}}Response(String message, Long id, String email, String name) { - this.message = message; - this.id = id; - this.email = email; - this.name = name; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginRequest.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginRequest.java deleted file mode 100644 index a4254ae..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.dto; - -public class LoginRequest { - private String email; - private String password; - - public LoginRequest() {} - - public LoginRequest(String email, String password) { - this.email = email; - this.password = password; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginResponse.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginResponse.java deleted file mode 100644 index 1e576f6..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/adapters/web/dto/LoginResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package {{yourDomain}}.infrastructure.adapters.web.dto; - -public class LoginResponse { - private String message; - private String role; - private String email; - private String name; - - public LoginResponse() {} - - public LoginResponse(String message, String role, String email, String name) { - this.message = message; - this.role = role; - this.email = email; - this.name = name; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/BeanConfiguration.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/BeanConfiguration.java deleted file mode 100644 index c6982b8..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/BeanConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -package {{yourDomain}}.infrastructure.config; - -import {{yourDomain}}.application.ports.repositories.AdminRepository; -import {{yourDomain}}.application.ports.repositories.{{UserEntity}}Repository; -import {{yourDomain}}.application.ports.services.PasswordService; -import {{yourDomain}}.application.ports.services.TokenService; -import {{yourDomain}}.application.usecases.AuthenticateAdminUseCase; -import {{yourDomain}}.application.usecases.Authenticate{{UserEntity}}UseCase; -import {{yourDomain}}.application.usecases.Create{{UserEntity}}UseCase; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class BeanConfiguration { - - @Bean - public Authenticate{{UserEntity}}UseCase authenticate{{UserEntity}}UseCase( - {{UserEntity}}Repository authorRepository, - PasswordService passwordService, - TokenService tokenService) { - return new Authenticate{{UserEntity}}UseCase(authorRepository, passwordService, tokenService); - } - - @Bean - public AuthenticateAdminUseCase authenticateAdminUseCase( - AdminRepository adminRepository, - PasswordService passwordService, - TokenService tokenService) { - return new AuthenticateAdminUseCase(adminRepository, passwordService, tokenService); - } - - @Bean - public Create{{UserEntity}}UseCase create{{UserEntity}}UseCase( - {{UserEntity}}Repository authorRepository, - PasswordService passwordService) { - return new Create{{UserEntity}}UseCase(authorRepository, passwordService); - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/doc/OpenAPI30Configuration.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/doc/OpenAPI30Configuration.java deleted file mode 100644 index cdbfb5d..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/doc/OpenAPI30Configuration.java +++ /dev/null @@ -1,45 +0,0 @@ -package {{yourDomain}}.infrastructure.config.doc; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Contact; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.servers.Server; -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@OpenAPIDefinition( - info = @Info(title = "{{developName}} - {{appName}}", version = "1.0", - contact = @Contact(name = "{{developName}}", email = "{{developMail}}", url = "{{developUrl}}"), - description = "{{description}}"), - servers = {@Server(url = "http://localhost:8080/", description = "Development")}) -public class OpenAPI30Configuration { - /** - * Configure the OpenAPI components. - * - * @return Returns fully configure OpenAPI object - * @see OpenAPI - */ - - @Bean - public OpenAPI customizeOpenAPI() { - final String securitySchemeName = "bearerAuth"; - return new OpenAPI() - .addSecurityItem(new SecurityRequirement() - .addList(securitySchemeName)) - .components(new Components() - .addSecuritySchemes(securitySchemeName, new SecurityScheme() - .name(securitySchemeName) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .description( - "Forneça o token JWT. O token JWT pode ser obtido na requisição de Login") - .bearerFormat("JWT"))); - - } - -} \ No newline at end of file diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/JwtAuthenticationFilter.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/JwtAuthenticationFilter.java deleted file mode 100644 index 289e632..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/JwtAuthenticationFilter.java +++ /dev/null @@ -1,60 +0,0 @@ -package {{yourDomain}}.infrastructure.config.security; - -import {{yourDomain}}.application.ports.services.TokenService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Collections; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final TokenService tokenService; - - public JwtAuthenticationFilter(TokenService tokenService) { - this.tokenService = tokenService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException, IOException { - - String token = extractTokenFromCookies(request); - - if (token != null && tokenService.validateToken(token)) { - String email = tokenService.extractEmail(token); - String role = tokenService.extractRole(token); - - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken( - email, - null, - Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)) - ); - - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - filterChain.doFilter(request, response); - } - - private String extractTokenFromCookies(HttpServletRequest request) { - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if ("token".equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - return null; - } -} diff --git a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/SecurityConfig.java b/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/SecurityConfig.java deleted file mode 100644 index 01cbed9..0000000 --- a/template/3layer/infrastructure/src/main/java/your/domain/infrastructure/config/security/SecurityConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package {{yourDomain}}.infrastructure.config.security; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authz -> authz - //Swagger/OpenAPI - .requestMatchers( - "/v3/api-docs/**", - "/swagger-ui.html", - "/swagger-ui/**" - ).permitAll() - .requestMatchers("/auth/**").permitAll() - .requestMatchers("/api/authors/**").permitAll() - .requestMatchers("/api/admin/**").hasRole("ADMIN") - .requestMatchers("/api/author/**").hasAnyRole("AUTHOR", "ADMIN") - .anyRequest().authenticated() - ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } -} diff --git a/template/3layer/infrastructure/src/main/resources/application.properties b/template/3layer/infrastructure/src/main/resources/application.properties deleted file mode 100644 index 7fc7922..0000000 --- a/template/3layer/infrastructure/src/main/resources/application.properties +++ /dev/null @@ -1,25 +0,0 @@ -spring.application.name={{appName}} - -#Datasource -spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:your_db} -spring.datasource.username=${DATABASE_USER:postgres} -spring.datasource.password=${DATABASE_PASSWORD:3memvhyka93v} - -#JPA / Hibernate -spring.jpa.hibernate.ddl-auto=validate -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.properties.hibernate.format_sql=true - -#JWT -jwt.secret=${TOKEN_SECRET_KEY:zT#xVnu_-EL5E188~'FPv,BNNxMMr!ii#8fN#@%$kZQANBD.fuR6@cNQwZe-VXUe;mT}o2g$y;-FBcB~s<.*m,$/} -jwt.expiration=86400000 - -#Logging -logging.level.com.example=DEBUG -logging.level.org.springframework.security=DEBUG - -#flyway -spring.flyway.locations=classpath:db/migration/postgres -spring.flyway.enabled=true diff --git a/template/3layer/infrastructure/src/main/resources/db/migration/postgres/V01_01__insert_data_test.sql b/template/3layer/infrastructure/src/main/resources/db/migration/postgres/V01_01__insert_data_test.sql deleted file mode 100644 index 4a4132c..0000000 --- a/template/3layer/infrastructure/src/main/resources/db/migration/postgres/V01_01__insert_data_test.sql +++ /dev/null @@ -1,32 +0,0 @@ ---default schema: public -CREATE TABLE IF NOT EXISTS admins ( - id BIGSERIAL PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS {{tableName}} ( - id BIGSERIAL PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_admins_email ON admins (email); -CREATE INDEX IF NOT EXISTS idx_authors_email ON {{tableName}} (email); - --- default password: test123456 -INSERT INTO admins (email, password, name, created_at, updated_at) -VALUES - ('admin1@{{yourDomain}}', '$2a$10$XTcZJSBbcIdA7PD2Ta8fmu4EmZ2tasvrPoHM2BdUtd.mYi2I5EBFK', 'Admin One', NOW(), NOW()), - ('admin2@{{yourDomain}}', '$2a$10$HAtgYRWeNHDeyA4kntuq6OrEMN8Qgz86XN0ftyg.wsBWXzTunAmKe', 'Admin Two', NOW(), NOW()); - -INSERT INTO {{tableName}} (email, password, name, created_at, updated_at) -VALUES - ('{{userEntity}}1@{{yourDomain}}', '$2a$10$xi3eengxM5..Sa16AqgRU.cZ7lltDkacVlXLbYRqrzzttDVprHS06', '{{userEntity}} One', NOW(), NOW()), - ('{{userEntity}}2@{{yourDomain}}', '$2a$10$VgpXD/oN91RpM/OH9s/3OO5B/BGrfpOAcV/0FPRPKu0ZJV1ITuZey', '{{userEntity}} Two', NOW(), NOW()); \ No newline at end of file diff --git a/template/3layer/infrastructure/src/test/java/dev/falae/api/ApplicationTests.java b/template/3layer/infrastructure/src/test/java/dev/falae/api/ApplicationTests.java deleted file mode 100644 index 4ad82ff..0000000 --- a/template/3layer/infrastructure/src/test/java/dev/falae/api/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.falae.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/template/3layer/mvnw b/template/3layer/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/template/3layer/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/template/3layer/mvnw.cmd b/template/3layer/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/template/3layer/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/template/3layer/pom.xml b/template/3layer/pom.xml deleted file mode 100644 index 78e531c..0000000 --- a/template/3layer/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - {{yourDomain}} - {{appName}} - 1.0.0 - pom - - - org.springframework.boot - spring-boot-starter-parent - 3.5.6 - - - - - 25 - 25 - UTF-8 - - - - core - application - infrastructure - - - - - - - \ No newline at end of file