diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d021614..68ce4a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,13 @@ on: branches: [ main ] pull_request: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -17,6 +21,18 @@ jobs: with: java-version: '21' distribution: 'temurin' + cache: maven - - name: Build with Maven - run: mvn -B clean verify \ No newline at end of file + - name: Build with Maven (tests + coverage) + run: mvn -B -q -ntp clean verify + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: | + target/site/jacoco/jacoco.xml + target/site/jacoco-it/jacoco.xml + flags: codegen-blueprint + name: codegen-blueprint + fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6882da4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: CodeQL + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '18 3 * * 1' + +permissions: + contents: read + security-events: write + actions: read + +jobs: + analyze: + name: Analyze (CodeQL) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: java + queries: +security-and-quality + + - name: Build (codegen-blueprint) + run: mvn -q -ntp -DskipTests=true clean package + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:java" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2107c55..9eb1bba 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ buildNumber.properties generated-sources/ generated-classes/ /HELP.md +*.iml \ No newline at end of file diff --git a/LICENSE b/LICENSE index 09364ee..2ad710b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2025 bsayli +Copyright (c) 2025 blueprint-platform +Maintained by Barış Saylı (@bsayli) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8d3a97c..03c116d 100644 --- a/README.md +++ b/README.md @@ -1,216 +1,326 @@ -# Codegen Spring Boot Initializr +# Codegen Blueprint — Profile‑Driven Project Generator with Architecture Options -![Build](https://github.com/bsayli/spring-boot-openapi-generics-clients/actions/workflows/build.yml/badge.svg) -![Java](https://img.shields.io/badge/Java-21-red) -![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5-green) -![Maven](https://img.shields.io/badge/Maven-3.9-blue) -![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) +[![Build](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/build.yml/badge.svg)](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/build.yml) +[![Release](https://img.shields.io/github/v/release/blueprint-platform/codegen-blueprint?logo=github\&label=release)](https://github.com/blueprint-platform/codegen-blueprint/releases/latest) +[![CodeQL](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/codeql.yml/badge.svg)](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/codeql.yml) +[![codecov](https://codecov.io/gh/blueprint-platform/codegen-blueprint/branch/refactor/hexagonal-architecture/graph/badge.svg)](https://codecov.io/gh/blueprint-platform/codegen-blueprint/tree/refactor/hexagonal-architecture) +[![Java](https://img.shields.io/badge/Java-21-red?logo=openjdk)](https://openjdk.org/projects/jdk/21/) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5.7-green?logo=springboot)](https://spring.io/projects/spring-boot) +[![Maven](https://img.shields.io/badge/Maven-3.9-blue?logo=apachemaven)](https://maven.apache.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

- Social preview -
- Social preview banner for GitHub and sharing + Executable Architecture — From Day Zero

-**A customizable project generator for Spring Boot.** -Quickly scaffold a new Java application with predefined structure, configuration, and tests — no repetitive setup -required. - --- -## 🚧 Active Development Notice +## 🧠 Why Codegen Blueprint Exists -This repository is currently undergoing a full **Hexagonal Architecture refactor**. +Modern engineering teams don’t struggle to **start** new services — +they struggle to keep them **architecturally consistent** as they scale. -➡️ All active development continues on the branch: +Most generators create a folder layout and walk away. +Codegen Blueprint enforces **architectural integrity**: -🔗 **https://github.com/blueprint-platform/codegen-blueprint/tree/refactor/hexagonal-architecture** +* Starts clean — no framework leaks into the domain +* Stays clean — structure guides every evolution +* Prevents silent architecture drifts over time -The `main` branch currently reflects **the older pre-refactor version** and will be updated once the refactor reaches **1.0.0-RC**. +Not just scaffolding. +Not just templates. ---- +> **Architecture embedded into the software delivery pipeline.** -## 🚀 Problem Statement +
-Bootstrapping a new Spring Boot project often involves: +

+ Value Proposition: Why Codegen Blueprint Exists +
+ Who benefits ➜ What the engine delivers ➜ Generated services +

+ +--- -* Manually creating Maven folders -* Writing boilerplate `pom.xml` -* Copying `.gitignore`, `application.yml`, and test classes -* Setting up wrapper scripts +### 🎯 Who is this for? -❌ Time wasted on repetitive setup -❌ Risk of inconsistencies between projects -❌ Slower onboarding for new developers +| Role | What you gain | +| -------------------- | --------------------------------- | +| Platform Engineering | Organization‑wide standardization | +| Lead Architect | Governance as Code | +| Developers | Clean architecture from day zero | +| New Team Members | Instant productivity | --- -## 💡 Solution +### 🥇 What makes it different? + +> **This is not a competitor comparison.** +> Spring Initializr and JHipster are fantastic and widely used tools with different missions. +> Codegen Blueprint focuses specifically on keeping architecture **intentional** from day zero. -This project automates all of that: +| Capability Focus | Spring Initializr & JHipster | Codegen Blueprint | +| ---------------------------------- | ---------------------------- | ----------------- | +| Generates folder layout | ✔ | ✔ | +| Opinionated architecture defaults | ⚠️ | **✔** | +| Domain isolation by design | ❌ | **✔** | +| Profile-driven evolution paths | ⚠️ | **✔** | +| Anti-drift architecture support | ❌ | **✔** | -* Generates a **ready-to-run Spring Boot project** with Maven -* Adds `.gitignore`, `application.yml`, starter class, and test class -* Supports **custom package and project naming** -* Ships with **CLI runner** for one-liner project generation -* Produces a **zip archive** you can immediately extract and use +> 🚀 Same starting point → different long-term destination +> 🧭 Codegen Blueprint helps teams **preserve architectural integrity** as services evolve --- -## ⚡ Quick Start +## 📑 Table of Contents -### 1. Clone the Repository +* ⚡ [What is Codegen Blueprint (Today)?](#-what-is-codegen-blueprint-today) +* 🧭 [1.0.0 Scope & Status](#-100-scope--status) +* 💡 [Why This Project Matters](#-why-this-project-matters) +* 🔌 [Inbound Adapter](#-inbound-adapter-delivery) +* ⚙️ [Outbound Adapters & Artifacts](#-outbound-adapters--artifacts) +* 🧪 [Testing & CI](#-testing--ci) +* 🔄 [CLI Usage Example](#-cli-usage-example) +* 🚀 [Vision & Roadmap](#-vision--roadmap-beyond-100) +* 🤝 [Contributing](#-contributing) +* ⭐ [Support & Community](#-support--community) +* 🛡 [License](#-license) -```bash -git clone https://github.com/bsayli/codegen-springboot-initializr.git -cd codegen-springboot-initializr -``` +--- -### 2. Build the Project +## ⚡ What is Codegen Blueprint (Today)? -```bash -mvn clean install -``` +A **CLI‑driven**, **architecture‑aware** project generator. -### 3. Run in CLI Mode +📌 Current primary profile: +**springboot‑maven‑java** +(Spring Boot 3 + Maven + Java 21) -```bash -mvn spring-boot:run -Dspring-boot.run.profiles=cli \ - -Dspring-boot.run.arguments="--groupId=com.example --artifactId=demo-app --packageName=com.example.demo --outputDir=./target/generated-projects --overwrite=true" -``` +Generates a **clean**, **ready‑to‑extend** Spring Boot project structure — similar to Spring Initializr, but with: + +* Clear and predictable layout +* Standardized project metadata (groupId, name, package) +* Built‑in test entry points from day zero +* Consistency without dependency overload -✅ This generates a new project as a zip archive under the specified output directory (default: -`./target/generated-projects`): +### Optional Architecture Layout + +📌 Hexagonal is an evolution path — not a barrier. + +For teams embracing Clean/Hexagonal architecture: ``` -[OK] Project archive generated at: /.../target/generated-projects/demo-app/demo-app.zip +domain // business rules (no Spring dependencies) +application // orchestrates ports +adapters // inbound & outbound adapters +bootstrap // Spring wiring & configuration ``` -ℹ️ Tips: +> "Spring Initializr — but **with architecture options built‑in**, not bolted on later." -* If you don’t provide `--outputDir`, the project will be created under the default path `target/generated-projects`. -* If the target directory already exists: +
- * By default, the generator will **fail-fast** with a clear error. - * Add `--overwrite=true` to **delete and regenerate** the project in the same directory. +

+ Codegen Blueprint — Hexagonal Architecture Overview +
+ + Engine flow: CLI input ➜ Use case orchestration ➜ Domain constraints ➜ Artifact generation ➜ Spring Boot project output + +

--- -## 🧑‍💻 Programmatic Usage - -```java -import java.nio.file.Path; -import java.util.List; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; -import io.github.bsayli.codegen.initializr.projectgeneration.service.ProjectGenerationService; - -// Assume ProjectGenerationService is injected or obtained from Spring context -ProjectGenerationService service = /* @Autowired or ApplicationContext.getBean(...) */; - - var depWeb = new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); - - var depTest = new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); - - var metadata = new SpringBootJavaProjectMetadataBuilder() - .springBootVersion("3.5.5") - .javaVersion("21") - .groupId("com.example") - .artifactId("demo-app") - .name("demo-app") - .description("Generated by codegen-initializr-core") - .packageName("com.example.demo") - .dependencies(List.of(depWeb, depTest)) - .build(); - - var type = new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); - - Path zip = service.generateProject(type, metadata); -System.out. - - println("Archive generated at: "+zip.toAbsolutePath()); -``` +## 🧭 1.0.0 Scope & Status ---- +### What is included (1.0.0) -## 🖼 Demo Output +| Capability | Status | +|-------------------------------------------------|-------------------| +| CLI-based generation | ✔ Production-ready | +| Standard Spring Boot skeleton | ✔ Stable | +| Hexagonal layout option (opt-in) | ✔ Available | +| Spring Boot 3 / Java 21 / Maven support | ✔ | +| Main + test entrypoints generated | ✔ | +| Required artifacts generated (pom, YAML, etc.) | ✔ | +| Open-source licensing | ✔ MIT License | -Example of the generated project structure: +### What is planned next -```text -demo-app/ - ├── pom.xml - ├── .gitignore - ├── src/ - │ ├── main/java/com/example/demo/DemoAppApplication.java - │ ├── main/resources/application.yml - │ ├── test/java/com/example/demo/DemoAppApplicationTests.java - │ └── gen/java/... (for codegen output) -``` +| Feature | Status | +|----------------------------------------------|------------| +| REST inbound adapter | Planned | +| Advanced hexagonal variations (ports, CQRS) | Planned | +| Additional profiles (Gradle, Kotlin, Quarkus)| Planned | +| Multi-module generation | Planned | +| Foundation libraries (`blueprint-*`) | Planned | +| Developer UI / web console | Evaluating | + +> Strategy: **Deep quality in one profile** → then expand ecosystem. + +📌 For more details: +- [Engine Enforcement Scope (1.0.0)](docs/architecture/engine-scope.md) +- [Generated Project Scope (1.0.0)](docs/architecture/project-scope.md) + +## 💡 Why This Project Matters + +Modern services deserve more than a bare `/src/main/java`. + +You get: + +* ✔ Predictable structure +* ✔ Testability from day zero +* ✔ Architecture as a **standard**, not an afterthought +* ✔ Faster onboarding + +You avoid: + +* ❌ Copy-paste architecture +* ❌ Every repo looks different +* ❌ Best practices lost over time +* ❌ Silent architecture drift + +📘 Explore the architecture: +👉 [How to Explore This Project (Hexagonal Architecture Guide)](./docs/guides/how-to-explore-hexagonal-architecture.md) + +### 🧩 Strategic Impact + +Architecture stays **intentional — not accidental**. + +Teams benefit from: + +* Standardized setup across services +* Clear responsibility boundaries +* Faster developer onboarding +* Future enforcement ready (ArchUnit, boundaries) + +Result: +**Every new service starts aligned — and scales without losing its architecture.** --- -## 🛠 Tech Stack & Features +## 🔌 Inbound Adapter (Delivery) -* 🚀 **Java 21** — modern baseline -* 🍃 **Spring Boot 3.5** -* 📦 **Maven 3.9+** — build and dependency management -* 🧩 **FreeMarker templates** — for generator extensibility -* 📂 **Automatic directory structure** — `src/main/java`, `src/test/java`, etc. -* 🧪 **JUnit 5** — generated test classes +| Adapter | Status | +| ------- | ---------------- | +| CLI | ✔ Primary driver | +| REST | Planned | --- -## 🧩 Architecture +## ⚙️ Outbound Adapters & Artifacts + +Active profile: -This project follows a **hexagonal (ports & adapters) architecture**: +``` +springboot‑maven‑java +``` -* **Ports** — abstract interfaces like `ProjectBuildGenerator`, `ApplicationYamlGenerator`, `ProjectArchiver` -* **Adapters** — framework-specific implementations (Spring Boot, Maven, FreeMarker) -* **Core** — generation service depends only on ports, making it extensible and testable +Generated artifacts (1.0.0): -This design allows the generator to evolve independently of specific tools while staying highly testable. +| Category | Includes | +|------------------|-------------------------------------------------------------------------| +| Build files | `pom.xml`, Maven Wrapper (`mvnw`, `.mvn/`) | +| Runtime config | `src/main/resources/application.yml` | +| Source entrypoints | Main application & test bootstrap classes | +| Git hygiene | Standard `.gitignore` | +| Documentation | Minimal `README.md` inside the generated project | + +> Everything required to **build, run, test and evolve** a clean Spring Boot service from day zero. --- -## 🧪 Testing +## 🧪 Testing & CI + +```bash +mvn verify +``` + +Includes: + +* ✔ Unit + integration tests +* ✔ JaCoCo coverage +* ✔ CodeQL security scan +* ✔ Codecov reporting + +--- -Run the test suite: +## 🔄 CLI Usage Example ```bash -mvn test +java -jar codegen-blueprint-1.0.0.jar \ + --cli \ + springboot \ + --group-id com.acme \ + --artifact-id demo \ + --name "Demo App" \ + --description "Demo application for Acme" \ + --package-name com.acme.demo \ + --layout hexagonal \ # optional architecture flag + --dependency web \ + --dependency data_jpa \ + --dependency validation ``` -The generator components (`pom.xml`, `.gitignore`, `application.yml`, layout, archiver) are fully covered with unit & -integration tests. +**Output (simplified)** + +``` +demo/ + ├── pom.xml + ├── src/main/java/com/example/demo/DemoApplication.java + ├── src/test/java/com/example/demo/DemoApplicationTests.java + ├── src/main/resources/application.yml + └── .gitignore +``` --- -## 📖 Related Work +## 🚀 Vision & Roadmap (Beyond 1.0.0) + +> Best practices should **execute**, not just be documented. -This tool is inspired by the need to automate repetitive **Spring Boot project initialization** tasks. -It works well alongside other repositories -like [spring-boot-openapi-generics-clients](https://github.com/bsayli/spring-boot-openapi-generics-clients). +Roadmap themes: + +* 🧱 Hexagonal evolution kit (ports + adapters + CQRS) +* 📈 Observability acceleration (metrics + tracing defaults) +* 🔐 Enterprise-grade security (OAuth2 / Keycloak) +* 🧩 Multi-module service composition +* 🎯 Broader profile ecosystem (Gradle / Kotlin / Quarkus) +* 💻 Developer UI → configure → generate → download + +> **Executable Architecture** for modern service development. --- -## 🛡 License +## 🤝 Contributing + +Discussions: +[https://github.com/blueprint-platform/codegen-blueprint/discussions](https://github.com/blueprint-platform/codegen-blueprint/discussions) -MIT +Issues: +[https://github.com/blueprint-platform/codegen-blueprint/issues](https://github.com/blueprint-platform/codegen-blueprint/issues) --- -**Author:** Barış Saylı -**GitHub:** [bsayli](https://github.com/bsayli) +## ⭐ Support & Community + +If Codegen Blueprint helps you: +👉 Please star the repo — it really matters. + +**Barış Saylı** + +GitHub — [bsayli](https://github.com/bsayli) +LinkedIn — [linkedin.com/in/bsayli](https://www.linkedin.com/in/bsayli) +Medium — [@baris.sayli](https://medium.com/@baris.sayli) + +--- + +## 🛡 License + +Licensed under MIT — free for personal and commercial use. +See: [LICENSE](LICENSE) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..75edc97 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,25 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: 70% + +comment: + layout: "reach, diff, flags, files" + behavior: default + +ignore: + - "target/**" + - "**/target/**" + - "**/generated/**" + - "**/generated-sources/**" + +flags: + codegen-blueprint: + paths: + - "src/main/java/" + - "src/test/java/" \ No newline at end of file diff --git a/docs/architecture/engine-scope.md b/docs/architecture/engine-scope.md new file mode 100644 index 0000000..ed9274f --- /dev/null +++ b/docs/architecture/engine-scope.md @@ -0,0 +1,222 @@ +# Engine Scope — Codegen Blueprint 1.0.0 + +> **What this document is:** +> The **formal contract** describing what Codegen Blueprint **guarantees** in 1.0.0. +> +> **What this document is not:** +> A list of future intentions or marketing claims. + +--- + +## 📑 Table of Contents + +* [1. Purpose](#1-purpose) +* [2. Core Mental Model](#2-core-mental-model) +* [3. Enforcement Guarantees (1.0.0)](#3-enforcement-guarantees-100) + + * [3.1 Deterministic Project Layout](#31-deterministic-project-layout) + * [3.2 Naming & Identity Enforcement](#32-naming--identity-enforcement) + * [3.3 Spring Boot Minimal Runtime Baseline](#33-spring-boot-minimal-runtime-baseline) + * [3.4 Test-Ready Project](#34-test-ready-project) + * [3.5 Separation of Engine & Templates](#35-separation-of-engine--templates) + * [3.6 Profile-Driven Execution](#36-profile-driven-execution) +* [4. Explicitly Not Enforced (Yet)](#4-explicitly-not-enforced-yet) +* [5. Intentional Scope Constraints](#5-intentional-scope-constraints) +* [6. Path Toward Executable Architecture](#6-path-toward-executable-architecture) +* [7. Review Guidance](#7-review-guidance) + +--- + +## 1. Purpose + +This document ensures: + +* ✔ Alignment between README promises and actual engine behavior +* ✔ Predictability for early adopters +* ✔ A stable baseline for future architectural enforcement + +> If the README **claims** it, the engine **must** guarantee it — and it must be reflected here. + +--- + +## 2. Core Mental Model + +| Concept | Definition | +| -------------- | ---------------------------------------------------------------------------- | +| **Platform** | Long-term home for engines, enforcement & governance (`blueprint-platform`) | +| **Engine** | CLI-based generator that applies profiles (`codegen-blueprint`) | +| **Profile** | Defines build tool + language + technology stack (`springboot-maven-java`) | +| **Blueprints** | Template artifacts selected by profile (e.g., `POM_XML`, `APPLICATION_YAML`) | + +The engine’s job in 1.0.0: + +> **Generate a clean, production-ready Spring Boot skeleton** +> that is prepared — but not yet forced — to follow architectural constraints. + +--- + +## 3. Enforcement Guarantees (1.0.0) + +These behaviors are **strictly required** and validated through tests. + +### 3.1 Deterministic Project Layout + +Generated structure must follow: + +``` +/ + ├─ pom.xml + ├─ src/main/java// + ├─ src/test/java// + ├─ src/main/resources/application.yml + ├─ .gitignore + └─ README.md (minimal) +``` + +*always single-module* + +--- + +### 3.2 Naming & Identity Enforcement + +Engine normalizes and applies: + +* ✔ `groupId` +* ✔ `artifactId` +* ✔ project name +* ✔ Java package namespace + +Main class naming rule: + +``` +Application +``` + +Misformatted identifiers → **fail early** +(no silently invalid layout) + +--- + +### 3.3 Spring Boot Minimal Runtime Baseline + +Generated project must: + +* ✔ Build successfully using Maven +* ✔ Run `SpringApplication.run(...)` out-of-the-box +* ✔ Include **only** core starters specified by dependencies + +No demo controllers +No accidental architectural noise + +--- + +### 3.4 Test-Ready Project + +Generated project must: + +* ✔ Include `@SpringBootTest` entrypoint +* ✔ Mirror source layout in test tree +* ✔ Pass `mvn verify` immediately after generation + +Testing is not optional. + +--- + +### 3.5 Separation of Engine & Templates + +Engine **never** imports: + +* 🚫 Spring +* 🚫 File system logic +* 🚫 Maven implementation details + +All stack-specific concerns are owned by: + +* Profiles +* Template layer + +This enables future: + +> Gradle, Kotlin, Quarkus… without touching core engine. + +--- + +### 3.6 Profile-Driven Execution + +Generation always activated via profile: + +```bash +java -jar codegen-blueprint.jar \ + --cli \ + springboot \ + --group-id com.acme \ + --artifact-id order-service \ + --name "Order Service" \ + --package-name com.acme.order \ + --layout hexagonal \ # optional + --dependency web \ + --dependency validation +``` + +Profiles decide: + +* ✔ structure +* ✔ templates +* ✔ technology capabilities + +--- + +## 4. Explicitly Not Enforced (Yet) + +| Not Included | Reason | +| --------------------------------- | ------------------------------------------- | +| Hexagonal structure by default | Optional for now to avoid adoption friction | +| Compile-time boundary enforcement | Requires future policy engine | +| ArchUnit generation | Milestone after hexagonal scaffolding | +| Governance across repos | Will arrive with org-level profiles | + +> We are **architecture-aware**, not yet **architecture-policing**. + +--- + +## 5. Intentional Scope Constraints + +* 1️⃣ Trust first — enforce later +* 2️⃣ Focused quality > bloated footprint +* 3️⃣ Evolution without rewriting core engine + +> Narrow now → scalable tomorrow + +--- + +## 6. Path Toward Executable Architecture + +| Stage | Capability | Effect | +| --------- | ---------------------------------- | ----------------------------- | +| **v1.1+** | Layout-aware hexagonal scaffolding | Real boundaries in generation | +| **v1.2+** | Auto-arch tests | Prevent silent drift | +| **v1.3+** | Policy DSL + CI enforcement | Architecture as quality gates | +| **v2.0** | Org profiles | Governance at scale | + +This roadmap exists **outside** enforcement scope for 1.0.0. + +--- + +## 7. Review Guidance + +Every PR must validate: + +> Does this change claim enforcement behavior? + +If **yes** → update this document +If **not** → ensure claims remain in README roadmap + +> 🔑 This document is the **source of truth** for what users can rely on **today**. + +--- + +### Final statement + +**Codegen Blueprint 1.0.0**: +A clean, testable, profile-driven Spring Boot starting point +built for **real enforcement** in the versions that follow. diff --git a/docs/architecture/project-scope.md b/docs/architecture/project-scope.md new file mode 100644 index 0000000..d196bf1 --- /dev/null +++ b/docs/architecture/project-scope.md @@ -0,0 +1,173 @@ +# Project Scope — Generated Output for 1.0.0 GA + +> This document defines what a generated project **must include** for the 1.0.0 GA release — a checklist for architectural consistency. + +--- + +## 🎯 Goal + +Ensure every new generated Spring Boot service starts **clean**, **testable**, and **architecture-aligned** — not just a folder dump. + +Current target profile: + +``` +springboot-maven-java +``` + +--- + +## 🏗 Standard Project Structure (Required) + +The generated project **must include**: + +``` +/ + ├── pom.xml + ├── src/main/java// + │ └── Application.java (Main class) + ├── src/test/java// + │ └── ApplicationTests.java (Basic test entrypoint) + ├── src/main/resources/ + │ └── application.yml + ├── .gitignore + └── README.md (minimal usage notes) +``` + +--- + +## 🔌 Technology Baseline + +| Component | Target Version | Required | +| ----------- | -------------- | -------- | +| Java | 21 | ✔ | +| Spring Boot | 3.5.x | ✔ | +| Maven | 3.9+ | ✔ | + +--- + +## 🧩 Architecture Option (Opt-In) + +If user selects: + +```bash +--layout hexagonal +``` + +then structure becomes: + +``` +/ + ├── domain/ // pure business rules + ├── application/ // orchestrates ports + ├── adapters/ // inbound & outbound + └── bootstrap/ // wiring & config +``` + +Requirements: + +* No Spring APIs inside **domain** +* Basic unit tests scaffolding provided +* Naming templates consistent with conventions + +--- + +## 📦 Application Metadata Generation + +| Artifact | Status | +| ------------ | ----------------- | +| groupId | ✔ mandatory param | +| artifactId | ✔ mandatory param | +| package name | ✔ enforced format | +| project name | ✔ mandatory param | + +Rules: + +* `basePackage` **must** reflect provided groupId / artifactId +* Naming must be validated and normalized (no invalid characters) + +--- + +## 🧪 Testing Guarantees + +Generated project must include: + +* A working test pipeline via `mvn verify` +* `@SpringBootTest` example test +* Structure that encourages future unit testing + +> Future releases → architecture rule enforcement (ArchUnit) + +--- + +## 📙 Minimal Documentation (Included) + +`README.md` must contain: + +* Build & run instructions +* Version badges (Java / Spring Boot) +* CLI usage example of Codegen Blueprint: + +```bash +java -jar codegen-blueprint-1.0.0.jar \ + --cli \ + springboot \ + --group-id com.acme \ + --artifact-id demo \ + --name "Demo App" \ + --description "Demo application for Acme" \ + --package-name com.acme.demo \ + --layout hexagonal \ # optional architecture flag + --dependency web \ + --dependency data_jpa \ + --dependency validation +``` + +Optional architecture-aware generation must be documented: + +```bash +--layout hexagonal +``` + +> If omitted → standard Spring Boot layout + +--- + +## ❌ Explicitly Out of Scope (1.0.0 GA) + +| Not Included | Reason | +| ----------------------- | -------------------------------------------------- | +| REST inbound adapter | Not part of 1.0.0 GA; planned follow-up after CLI | +| Security defaults | Avoid opinionated coupling (later) | +| Observability setup | Future profile variation | +| Multi-module generation | Larger iteration required | + +These remain out of GA scope **to keep the release focused**. +If REST lands earlier than planned, this document will be updated accordingly. + +--- + +## ✔ Definition of Done (DoD) + +A generated project must: + +* Compile & run immediately +* Contain correct package namespace +* Include minimal test scaffolding +* Apply optional hexagonal layout **when selected** +* Be releasable as a **production‑starter template** + +--- + +## 📊 Status Tracking — GA Confidence + +| Requirement Area | Status | Notes | +| ---------------- | ------------------ | ------------------------------------------------ | +| Standard layout | ✔ GA-ready | ZIP output naming validated via CLI tests | +| Hexagonal layout | ✔ Opt-in (Limited) | Structure generates correctly — enforcement next | +| App metadata | ✔ Complete | Rules apply consistently | +| Tests | ✔ Minimal Complete | Test entrypoints verified | +| Documentation | ✔ Ready | README usage aligned | + +--- + +> This document evolves with each milestone and defines the quality bar for **1.0.0 GA**. diff --git a/docs/design/social-preview.html b/docs/design/social-preview.html new file mode 100644 index 0000000..d3a63a9 --- /dev/null +++ b/docs/design/social-preview.html @@ -0,0 +1,445 @@ + + + + + Codegen Blueprint – Social Preview + + + + +
+
+ +
+
+
+
+
+ Blueprint Platform +
+
Profile-Driven · Architecture-Aware
+
+ +

+ Codegen Blueprint +

+ +
+ Profile-driven project generator with architecture options — + standardized Spring Boot scaffolding plus hexagonal-ready layout paths, + built for scalable reuse across teams. +
+ +
+
+
+
Standardized, enterprise-ready Spring Boot services from day zero.
+
+
+
+
Hexagonal-ready layout for domain-centric, framework-independent design.
+
+
+
+
Profiles capture your stack — Spring Boot, Maven, Java 21 and beyond.
+
+
+
+
Architecture as a Product — automation that enforces best practices.
+
+
+ +
+
+ Runtime + Java 21 +
+
+ Framework + Spring Boot 3.5+ +
+
+ Build Tool + Maven +
+
+ Architecture + Hexagonal-ready +
+
+
+ + +
+ + +
+
+
+
+ Generate a new service +
+ CLI profile · springboot-maven-java +
+ +
+
// Architecture-aware scaffolding
+ +
java -jar codegen-blueprint-1.0.0.jar \
+
--cli \
+
springboot \
+
--group-id com.example \
+
--artifact-id demo-service \
+
--name "Demo Service" \
+
--description "Demo service for Acme" \
+
--package-name com.example.demo \
+ +
// Optional flags
+
--layout hexagonal \
+
--dependency web \
+
--dependency data_jpa \
+
--dependency validation
+ +
// Defaults → maven · java · Java 21 · Spring Boot 3.5.x
+
// Generated structure (simplified)
+ +
demo-service/
+
├─ pom.xml
+
├─ src/main/java/com/example/demo/DemoServiceApplication.java
+
├─ src/test/java/com/example/demo/DemoServiceApplicationTests.java
+
└─ src/main/resources/application.yml
+
+
+
+
+ + \ No newline at end of file diff --git a/docs/diagrams/architecture-overview.drawio b/docs/diagrams/architecture-overview.drawio new file mode 100644 index 0000000..4b92cb0 --- /dev/null +++ b/docs/diagrams/architecture-overview.drawio @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/diagrams/value-proposition.drawio b/docs/diagrams/value-proposition.drawio new file mode 100644 index 0000000..3e0b1a2 --- /dev/null +++ b/docs/diagrams/value-proposition.drawio @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guides/how-to-explore-hexagonal-architecture.md b/docs/guides/how-to-explore-hexagonal-architecture.md new file mode 100644 index 0000000..a15d98e --- /dev/null +++ b/docs/guides/how-to-explore-hexagonal-architecture.md @@ -0,0 +1,111 @@ +## 👀 How to Explore This Project (Hexagonal Architecture Guide) + +If you're here to understand **how Hexagonal Architecture (Ports & Adapters)** is applied in a **real, fully-tested, production‑grade Java project**, this section will guide your exploration. + +The repository demonstrates how to build a **framework‑agnostic, testable, cleanly layered architecture** — while still generating real output (project scaffolding). + +--- + +### 🧱 Core Architectural Structure + +**Layered by strict responsibilities:** + +* **`domain`** → Pure business rules: aggregate, value objects, naming policies, dependency rules +* **`application`** → Executes generation pipelines using defined ports +* **`adapter`** → Technology-specific implementations (CLI, REST, FreeMarker, filesystem, Maven, docs) +* **`bootstrap`** → Spring wiring: profile → adapters → renderer binding + +Each package enforces **one direction** of dependency: toward the domain. + +--- + +### 🔌 Ports & Adapters (Decoupled Delivery) + +Generation behavior is defined by **ports**: + +* `ArtifactPort` → Generates a single artifact +* `ProjectArtifactsPort` → Orchestrates ordered artifact pipeline + +Concrete behavior is in **outbound adapters**, mapped via keys: + +* `BUILD_CONFIG` → Maven POM generator +* `IGNORE_RULES` → .gitignore generator +* `APP_CONFIG` → application.yml generator +* `MAIN_SOURCE_ENTRY_POINT` → Main class scaffolder +* `TEST_SOURCE_ENTRY_POINT` → Test scaffolder +* `PROJECT_DOCUMENTATION` → README generator + +Adding support for a new tech stack (e.g., Gradle) requires **only new adapters + templates** — no core changes. + +--- + +### 🧩 Profile‑Driven Architecture + +Profiles define the generation rules: + +* Template namespacing +* Which artifacts are generated +* The exact processing order + +These stay externalized in configuration (`application.yml`), keeping the engine **evolution‑friendly**. + +--- + +### 🧲 Inbound Adapters (CLI currently implemented) + +Inbound adapters trigger **use cases** from external channels. + +Currently implemented: + +* **CLI Adapter (active)** → Powered by Picocli + Spring Context + +📌 Usage example: + +```bash +java -jar codegen-blueprint.jar \ + --spring.profiles.active=cli \ + springboot \ + --group-id com.example \ + --artifact-id demo-app \ + --name "Demo App" \ + --package-name com.example.demo \ + --dependency WEB \ + --dependency DATA_JPA +``` + +The CLI maps arguments → domain commands → artifact pipeline → project zip output. + +Planned inbound adapter: + +* REST API (HTTP-driven generation service) + +--- + +### 🧪 Testing Strategy (CI‑Ready) + +* **Unit tests** → Domain rules + adapter behavior +* **Integration tests** → Full Spring Context + end‑to‑end artifact generation +* JaCoCo + Codecov coverage reporting +* CodeQL for static security scanning + +Every major component is validated **without mocking core logic**. + +--- + +### 🎯 Why This Repo Matters + +This project serves as a concrete reference for: + +| Learning Goal | How this repo helps | +| ---------------------------- | ------------------------------------------------- | +| Apply Hexagonal Architecture | Clean separation of domain, application, adapters | +| Reduce framework coupling | Domain has zero Spring dependencies | +| Improve maintainability | Technology swaps don’t cause refactors | +| Ensure high testability | Full integration test pipeline + CI validation | +| Build generation engines | Profile‑driven artifact pipeline architecture | + +If you're evaluating engineering skills or searching for a scalable architecture pattern — this repository is designed to showcase **the real thing**, not a toy example. + +--- + +📌 *Tip:* Begin with `ProjectBlueprint` (domain), then follow how it flows into `ProjectArtifactsPort`, down to each registered adapter in the `springboot-maven-java` profile. diff --git a/docs/images/architecture/architecture-overview.png b/docs/images/architecture/architecture-overview.png new file mode 100644 index 0000000..a5bea7e Binary files /dev/null and b/docs/images/architecture/architecture-overview.png differ diff --git a/docs/images/architecture/value-proposition.png b/docs/images/architecture/value-proposition.png new file mode 100644 index 0000000..988a0c8 Binary files /dev/null and b/docs/images/architecture/value-proposition.png differ diff --git a/docs/images/cover/cover-mini.png b/docs/images/cover/cover-mini.png new file mode 100644 index 0000000..aa9c3f5 Binary files /dev/null and b/docs/images/cover/cover-mini.png differ diff --git a/docs/images/cover/cover.png b/docs/images/cover/cover.png new file mode 100644 index 0000000..f5e1f70 Binary files /dev/null and b/docs/images/cover/cover.png differ diff --git a/docs/images/social-preview.png b/docs/images/social-preview.png deleted file mode 100644 index c4e56f8..0000000 Binary files a/docs/images/social-preview.png and /dev/null differ diff --git a/pom.xml b/pom.xml index 025a124..c16e43a 100644 --- a/pom.xml +++ b/pom.xml @@ -7,16 +7,18 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 3.5.8 - io.github.bsayli - codegen-springboot-initializr - 0.2.0 - codegen-springboot-initializr - Spring Boot project generator (CLI & programmatic) - https://github.com/bsayli/codegen-springboot-initializr + io.github.blueprint-platform + codegen-blueprint + 1.0.0 + codegen-blueprint + Hexagonal, profile-driven blueprint engine for generating production-ready project scaffolding across + frameworks and languages + + https://github.com/blueprint-platform/codegen-blueprint @@ -26,9 +28,9 @@ - https://github.com/bsayli/codegen-springboot-initializr - scm:git:https://github.com/bsayli/codegen-springboot-initializr.git - scm:git:ssh://git@github.com/bsayli/codegen-springboot-initializr.git + https://github.com/blueprint-platform/codegen-blueprint + scm:git:https://github.com/blueprint-platform/codegen-blueprint.git + scm:git:ssh://git@github.com/blueprint-platform/codegen-blueprint.git HEAD @@ -47,7 +49,8 @@ 3.9.11 2.20.0 3.18.0 - 1.28.0 + 0.8.13 + 4.7.7 @@ -56,12 +59,23 @@ spring-boot-starter + + org.springframework.boot + spring-boot-starter-validation + + org.apache.maven maven-model ${maven-model.version} + + info.picocli + picocli + ${picocli.version} + + commons-io commons-io @@ -74,11 +88,6 @@ ${commons-lang3.version} - - org.apache.commons - commons-compress - ${commons-compress.version} - org.freemarker @@ -106,7 +115,7 @@ - io.github.bsayli.codegen.initializr + io.github.blueprintplatform.codegen @@ -125,6 +134,77 @@ ${project.build.sourceEncoding} + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + it-tests + + integration-test + verify + + + + **/*IT.java + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + + prepare-agent-integration + + prepare-agent-integration + + + + + report + verify + + report + + + + + report-integration + verify + + report-integration + + + + + + + it + + \ No newline at end of file diff --git a/src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java b/src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java similarity index 51% rename from src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java rename to src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java index db93952..155e75e 100644 --- a/src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java +++ b/src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java @@ -1,14 +1,14 @@ -package io.github.bsayli.codegen.initializr; +package io.github.blueprintplatform.codegen; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication -@ConfigurationPropertiesScan(basePackages = "io.github.bsayli.codegen.initializr") -public class CodegenSpringbootInitializrApplication { +@ConfigurationPropertiesScan(basePackages = "io.github.blueprintplatform.codegen") +public class CodegenBlueprintApplication { public static void main(String[] args) { - SpringApplication.run(CodegenSpringbootInitializrApplication.class, args); + SpringApplication.run(CodegenBlueprintApplication.class, args); } } diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java new file mode 100644 index 0000000..a28fadc --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import io.github.blueprintplatform.codegen.bootstrap.error.exception.InfrastructureException; + +public abstract class AdapterException extends InfrastructureException { + protected AdapterException(String messageKey, Object... args) { + super(messageKey, args); + } + + protected AdapterException(String messageKey, Throwable cause, Object... args) { + super(messageKey, cause, args); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java new file mode 100644 index 0000000..efccb04 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java @@ -0,0 +1,12 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; + +@SuppressWarnings("java:S110") +public final class ArtifactKeyMismatchException extends AdapterException { + private static final String KEY = "adapter.generator.key.mismatch"; + + public ArtifactKeyMismatchException(ArtifactKey expected, ArtifactKey actual) { + super(KEY, expected.key(), actual.key()); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java new file mode 100644 index 0000000..217b6b8 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java @@ -0,0 +1,19 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType; + +@SuppressWarnings("java:S110") +public final class ArtifactsPortNotFoundException extends AdapterException { + + private static final String KEY = "adapter.artifacts.port.not.found"; + private final ProfileType profileType; + + public ArtifactsPortNotFoundException(ProfileType profileType) { + super(KEY, profileType.name()); + this.profileType = profileType; + } + + public ProfileType getProfileType() { + return profileType; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java new file mode 100644 index 0000000..4070066 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java @@ -0,0 +1,11 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +@SuppressWarnings("java:S110") +public class InvalidDependencyAliasException extends AdapterException { + + private static final String KEY = "adapter.dependency.alias.unknown"; + + public InvalidDependencyAliasException(String alias) { + super(KEY, alias); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java new file mode 100644 index 0000000..a612603 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectArchiveIOException extends AdapterException { + + private static final String KEY = "adapter.project.archive.io"; + + public ProjectArchiveIOException(Path root, Throwable cause) { + super(KEY, cause, root); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java new file mode 100644 index 0000000..5b1fb69 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectArchiveInvalidRootException extends AdapterException { + + private static final String KEY = "adapter.project.archive.invalid.root"; + + public ProjectArchiveInvalidRootException(Path root) { + super(KEY, root != null ? root : ""); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java new file mode 100644 index 0000000..6882e37 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectRootAlreadyExistsException extends AdapterException { + + private static final String KEY = "adapter.project-root.already-exists"; + + public ProjectRootAlreadyExistsException(Path path) { + super(KEY, path); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java new file mode 100644 index 0000000..f282ee8 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectRootIOException extends AdapterException { + + private static final String KEY = "adapter.project-root.io.failed"; + + public ProjectRootIOException(Path path, Throwable cause) { + super(KEY, cause, path); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java new file mode 100644 index 0000000..9d4ce11 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectRootNotDirectoryException extends AdapterException { + + private static final String KEY = "adapter.project-root.not-directory"; + + public ProjectRootNotDirectoryException(Path path) { + super(KEY, path); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java new file mode 100644 index 0000000..ab1197c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import java.nio.file.Path; + +@SuppressWarnings("java:S110") +public final class ProjectWriteException extends AdapterException { + + private static final String KEY = "adapter.project.write.failed"; + + public ProjectWriteException(Path path, Throwable cause) { + super(KEY, cause, path); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java new file mode 100644 index 0000000..8fce891 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java @@ -0,0 +1,21 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +@SuppressWarnings("java:S110") +public final class TemplateRenderingException extends AdapterException { + private static final String KEY = "adapter.template.render.failed"; + private final String templateName; + + public TemplateRenderingException(String templateName, Object... args) { + super(KEY, prepend(templateName, args)); + this.templateName = templateName; + } + + public TemplateRenderingException(String templateName, Throwable cause, Object... args) { + super(KEY, cause, prepend(templateName, args)); + this.templateName = templateName; + } + + public String getTemplateName() { + return templateName; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java new file mode 100644 index 0000000..d84d990 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java @@ -0,0 +1,12 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; + +@SuppressWarnings("java:S110") +public final class UnsupportedProfileTypeException extends AdapterException { + private static final String KEY = "adapter.profile.unsupported"; + + public UnsupportedProfileTypeException(TechStack options) { + super(KEY, options.framework(), options.buildTool(), options.language()); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java new file mode 100644 index 0000000..3834a67 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java @@ -0,0 +1,15 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli; + +import java.nio.file.Path; +import java.util.List; + +public record CliProjectRequest( + String groupId, + String artifactId, + String name, + String description, + String packageName, + String profile, + String layoutKey, + List dependencies, + Path targetDirectory) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java new file mode 100644 index 0000000..7e4318f --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java @@ -0,0 +1,76 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli; + +import io.github.blueprintplatform.codegen.adapter.error.exception.AdapterException; +import io.github.blueprintplatform.codegen.application.error.exception.ApplicationException; +import io.github.blueprintplatform.codegen.bootstrap.error.exception.InfrastructureException; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainException; +import java.io.IOException; +import java.util.Locale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import picocli.CommandLine; +import picocli.CommandLine.IExecutionExceptionHandler; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.ParseResult; + +public class CodegenCliExceptionHandler implements IExecutionExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(CodegenCliExceptionHandler.class); + + private final MessageSource messageSource; + + public CodegenCliExceptionHandler(MessageSource messageSource) { + this.messageSource = messageSource; + } + + @Override + public int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) { + + if (ex instanceof ParameterException parameterException) { + cmd.getErr().println("codegen: usage error: " + parameterException.getMessage()); + cmd.usage(cmd.getErr()); + return 1; + } + + Throwable cause = (ex.getCause() != null) ? ex.getCause() : ex; + + return switch (cause) { + case DomainException domainException -> { + printLocalizedError(cmd, domainException.getMessageKey(), domainException.getArgs()); + yield 1; + } + case ApplicationException applicationException -> { + printLocalizedError( + cmd, applicationException.getMessageKey(), applicationException.getArgs()); + yield 2; + } + case AdapterException adapterException -> { + printLocalizedError(cmd, adapterException.getMessageKey(), adapterException.getArgs()); + yield 3; + } + case InfrastructureException infrastructureException -> { + printLocalizedError( + cmd, infrastructureException.getMessageKey(), infrastructureException.getArgs()); + yield 3; + } + case IOException ioException -> { + cmd.getErr().println("codegen: error: I/O error occurred: " + ioException.getMessage()); + log.error("I/O error in CLI execution", ioException); + yield 3; + } + default -> { + log.error("Unexpected CLI error", cause); + cmd.getErr() + .println("codegen: error: Unexpected failure. Please try again or open an issue."); + yield 99; + } + }; + } + + private void printLocalizedError(CommandLine cmd, String key, Object[] args) { + String resolved = messageSource.getMessage(key, args, key, Locale.getDefault()); + cmd.getErr().println("codegen: error: " + resolved); + cmd.getErr().println("(code: " + key + ")"); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java new file mode 100644 index 0000000..704dafe --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java @@ -0,0 +1,12 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli; + +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.SpringBootGenerateCommand; +import picocli.CommandLine.Command; + +@Command( + name = "codegen", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Hexagonal project code generator CLI", + subcommands = {SpringBootGenerateCommand.class}) +public class CodegenCommand {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java new file mode 100644 index 0000000..3af833f --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java @@ -0,0 +1,19 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli.shared; + +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; +import java.util.function.Function; +import picocli.CommandLine.ITypeConverter; + +public final class KeyedEnumConverter & KeyedEnum> implements ITypeConverter { + + private final Function delegate; + + public KeyedEnumConverter(Function delegate) { + this.delegate = delegate; + } + + @Override + public E convert(String value) { + return delegate.apply(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java new file mode 100644 index 0000000..874bac6 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java @@ -0,0 +1,65 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli.springboot; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest; +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectCommand; +import io.github.blueprintplatform.codegen.application.usecase.project.DependencyInput; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.ArrayList; +import java.util.List; + +public class CreateProjectCommandMapper { + + public CreateProjectCommand from( + CliProjectRequest request, + BuildTool buildTool, + Language language, + JavaVersion javaVersion, + SpringBootVersion bootVersion) { + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, buildTool, language); + PlatformTarget platformTarget = new SpringBootJvmTarget(javaVersion, bootVersion); + ProjectLayout layout = ProjectLayout.fromKey(request.layoutKey()); + List inputs = toDependencyInputs(request.dependencies()); + + return new CreateProjectCommand( + request.groupId(), + request.artifactId(), + request.name(), + request.description(), + request.packageName(), + techStack, + layout, + platformTarget, + inputs, + request.targetDirectory()); + } + + private List toDependencyInputs(List aliases) { + if (aliases == null || aliases.isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(); + + for (String raw : aliases) { + if (raw == null || raw.isBlank()) { + continue; + } + + SpringBootDependencyAlias alias = SpringBootDependencyAlias.fromKey(raw); + + result.add(new DependencyInput(alias.groupId(), alias.artifactId(), null, null)); + } + + return List.copyOf(result); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java new file mode 100644 index 0000000..ed5dc88 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java @@ -0,0 +1,147 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli.springboot; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest; +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectResult; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Callable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command( + name = "springboot", + mixinStandardHelpOptions = true, + description = "Generate a Spring Boot project scaffold (standard or hexagonal layout)") +public class SpringBootGenerateCommand implements Callable { + + private static final Logger log = LoggerFactory.getLogger(SpringBootGenerateCommand.class); + + private final CreateProjectCommandMapper mapper; + private final CreateProjectUseCase createProjectUseCase; + + @Option( + names = {"--group-id"}, + required = true, + description = "Maven groupId, for example: com.example") + String groupId; + + @Option( + names = {"--artifact-id"}, + required = true, + description = "Maven artifactId, for example: demo-app") + String artifactId; + + @Option( + names = {"--name"}, + required = true, + description = "Human-readable project name") + String name; + + @Option( + names = {"--description"}, + required = true, + description = "Project description (min 10 characters)") + String description; + + @Option( + names = {"--package-name"}, + required = true, + description = "Base package name, for example: com.example.demo") + String packageName; + + @Option( + names = {"--build-tool"}, + required = false, + description = "Build tool. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "maven") + BuildTool buildTool; + + @Option( + names = {"--language"}, + required = false, + description = "Programming language. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "java") + Language language; + + @Option( + names = {"--java"}, + required = false, + description = "Java version. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "21") + JavaVersion javaVersion; + + @Option( + names = {"--boot"}, + required = false, + description = "Spring Boot version. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "3.5") + SpringBootVersion bootVersion; + + @Option( + names = {"--layout"}, + required = false, + description = "Project layout. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "standard") + ProjectLayout layout; + + @Option( + names = {"--dependency"}, + required = false, + description = "Dependency alias, can be repeated. Available: ${COMPLETION-CANDIDATES}") + List dependencies; + + @Option( + names = {"--target-dir"}, + required = false, + description = "Target directory for the generated project", + defaultValue = ".") + Path targetDirectory; + + public SpringBootGenerateCommand( + CreateProjectCommandMapper mapper, CreateProjectUseCase createProjectUseCase) { + this.mapper = mapper; + this.createProjectUseCase = createProjectUseCase; + } + + @Override + public Integer call() { + String profile = buildProfileKey(buildTool, language); + + List dependencyAliases = + dependencies == null ? List.of() : dependencies.stream().map(Enum::name).toList(); + + CliProjectRequest request = + new CliProjectRequest( + groupId, + artifactId, + name, + description, + packageName, + profile, + layout.key(), + dependencyAliases, + targetDirectory); + + var command = mapper.from(request, buildTool, language, javaVersion, bootVersion); + + CreateProjectResult result = createProjectUseCase.handle(command); + + log.info("Spring Boot project generated successfully."); + log.info("Archive path: {}", result.archivePath()); + + return 0; + } + + private String buildProfileKey(BuildTool buildTool, Language language) { + return "springboot-" + buildTool.key() + "-" + language.key(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java new file mode 100644 index 0000000..300a05b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java @@ -0,0 +1,52 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency; + +import io.github.blueprintplatform.codegen.adapter.error.exception.InvalidDependencyAliasException; + +public enum SpringBootDependencyAlias { + WEB(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-web"), + DATA_JPA(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-data-jpa"), + VALIDATION(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-validation"), + ACTUATOR(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-actuator"), + SECURITY(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-security"), + DEVTOOLS(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-devtools"); + + private final String groupId; + private final String artifactId; + + SpringBootDependencyAlias(String groupId, String artifactId) { + this.groupId = groupId; + this.artifactId = artifactId; + } + + public static SpringBootDependencyAlias fromKey(String raw) { + if (raw == null || raw.isBlank()) { + throw new InvalidDependencyAliasException(String.valueOf(raw)); + } + + String normalized = raw.trim(); + + for (SpringBootDependencyAlias alias : values()) { + if (alias.name().equalsIgnoreCase(normalized)) { + return alias; + } + } + throw new InvalidDependencyAliasException(raw); + } + + public String groupId() { + return groupId; + } + + public String artifactId() { + return artifactId; + } + + @Override + public String toString() { + return name().toLowerCase(); + } + + private static class Constants { + public static final String ORG_SPRINGFRAMEWORK_BOOT = "org.springframework.boot"; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java new file mode 100644 index 0000000..80b464f --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java @@ -0,0 +1,11 @@ +package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared; + +public record PomDependency(String groupId, String artifactId, String version, String scope) { + public static PomDependency of(String groupId, String artifactId) { + return new PomDependency(groupId, artifactId, null, null); + } + + public static PomDependency of(String groupId, String artifactId, String version, String scope) { + return new PomDependency(groupId, artifactId, version, scope); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java new file mode 100644 index 0000000..27bcd2a --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java @@ -0,0 +1,23 @@ +package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared; + +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import java.util.ArrayList; +import java.util.List; + +public class PomDependencyMapper { + + public List from(Dependencies dependencies) { + if (dependencies == null || dependencies.isEmpty()) return List.of(); + var list = new ArrayList(dependencies.asList().size()); + for (Dependency d : dependencies.asList()) list.add(from(d)); + return list; + } + + public PomDependency from(Dependency d) { + var v = (d.version() == null || d.version().value().isBlank()) ? null : d.version().value(); + var s = (d.scope() == null || d.scope().value().isBlank()) ? null : d.scope().value(); + return PomDependency.of( + d.coordinates().groupId().value(), d.coordinates().artifactId().value(), v, s); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java new file mode 100644 index 0000000..127b624 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java @@ -0,0 +1,97 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveIOException; +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveInvalidRootException; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class FileSystemProjectArchiverAdapter implements ProjectArchiverPort { + + private static final String ZIP_EXTENSION = ".zip"; + private static final char ZIP_SEPARATOR = '/'; + + @Override + public Path archive(Path projectRoot, String artifactId) { + if (projectRoot == null) { + throw new ProjectArchiveInvalidRootException(null); + } + + Path parent = projectRoot.getParent(); + if (parent == null) { + throw new ProjectArchiveInvalidRootException(projectRoot); + } + + if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) { + throw new ProjectArchiveInvalidRootException(projectRoot); + } + + String baseName = + (artifactId == null || artifactId.isBlank()) + ? projectRoot.getFileName().toString() + : artifactId; + + Path archivePath = parent.resolve(baseName + ZIP_EXTENSION); + + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(archivePath))) { + writeDirectoryToZip(projectRoot, baseName, zipOut); + return archivePath; + } catch (IOException e) { + throw new ProjectArchiveIOException(projectRoot, e); + } + } + + private void writeDirectoryToZip(Path root, String rootName, ZipOutputStream zos) + throws IOException { + Path normalizedRoot = root.toAbsolutePath().normalize(); + + try (Stream paths = Files.walk(normalizedRoot)) { + paths.forEachOrdered( + path -> { + try { + writeEntry(normalizedRoot, rootName, path, zos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + private void writeEntry(Path root, String rootName, Path current, ZipOutputStream zos) + throws IOException { + + Path relative = root.relativize(current); + StringBuilder entryName = new StringBuilder(); + entryName.append(rootName).append(ZIP_SEPARATOR); + + String rel = relative.toString(); + if (!rel.isEmpty()) { + String fsSep = root.getFileSystem().getSeparator(); + if (!fsSep.equals(String.valueOf(ZIP_SEPARATOR))) { + rel = rel.replace(fsSep, String.valueOf(ZIP_SEPARATOR)); + } + entryName.append(rel); + } + + boolean directory = Files.isDirectory(current); + if (directory && entryName.charAt(entryName.length() - 1) != ZIP_SEPARATOR) { + entryName.append(ZIP_SEPARATOR); + } + + ZipEntry entry = new ZipEntry(entryName.toString()); + zos.putNextEntry(entry); + + if (!directory) { + Files.copy(current, zos); + } + + zos.closeEntry(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java new file mode 100644 index 0000000..db6f0ea --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java @@ -0,0 +1,39 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootAlreadyExistsException; +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootIOException; +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootNotDirectoryException; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class FileSystemProjectRootAdapter implements ProjectRootPort { + + @Override + public Path prepareRoot(Path targetDir, String artifactId, ProjectRootExistencePolicy policy) { + Path projectRoot = targetDir.resolve(artifactId); + + try { + if (Files.exists(projectRoot)) { + + if (!Files.isDirectory(projectRoot)) { + throw new ProjectRootNotDirectoryException(projectRoot); + } + + if (policy == ProjectRootExistencePolicy.FAIL_IF_EXISTS) { + throw new ProjectRootAlreadyExistsException(projectRoot); + } + + return projectRoot; + } + + Files.createDirectories(projectRoot); + return projectRoot; + + } catch (IOException e) { + throw new ProjectRootIOException(projectRoot, e); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java new file mode 100644 index 0000000..ab1619f --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java @@ -0,0 +1,37 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectWriteException; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +public class FileSystemProjectWriterAdapter implements ProjectWriterPort { + + @Override + public void writeBytes(Path projectRoot, Path relativePath, byte[] content) { + Path target = projectRoot.resolve(relativePath); + try { + Path parent = target.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.write( + target, + content, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + } catch (IOException e) { + throw new ProjectWriteException(target, e); + } + } + + @Override + public void writeText(Path projectRoot, Path relativePath, String content, Charset charset) { + byte[] bytes = content.getBytes(charset); + writeBytes(projectRoot, relativePath, bytes); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java new file mode 100644 index 0000000..56db0b7 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java @@ -0,0 +1,32 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactsPortNotFoundException; +import io.github.blueprintplatform.codegen.adapter.error.exception.UnsupportedProfileTypeException; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.Map; + +public class ProfileBasedArtifactsSelector implements ProjectArtifactsSelector { + + private final Map registry; + + public ProfileBasedArtifactsSelector(Map registry) { + this.registry = registry; + } + + @Override + public ProjectArtifactsPort select(TechStack options) { + ProfileType type = ProfileType.from(options); + if (type == null) { + throw new UnsupportedProfileTypeException(options); + } + + ProjectArtifactsPort port = registry.get(type); + if (port == null) { + throw new ArtifactsPortNotFoundException(type); + } + + return port; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java new file mode 100644 index 0000000..c6e590a --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java @@ -0,0 +1,39 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile; + +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; + +public enum ProfileType { + SPRINGBOOT_MAVEN_JAVA(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + private final Framework framework; + private final BuildTool buildTool; + private final Language language; + + ProfileType(Framework framework, BuildTool buildTool, Language language) { + this.framework = framework; + this.buildTool = buildTool; + this.language = language; + } + + public static ProfileType from(TechStack o) { + for (ProfileType p : values()) { + if (p.framework == o.framework() + && p.buildTool == o.buildTool() + && p.language == o.language()) { + return p; + } + } + return null; + } + + private static String slug(Enum e) { + return e.name().toLowerCase().replace("_", ""); + } + + public String key() { + return slug(framework) + "-" + slug(buildTool) + "-" + slug(language); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java new file mode 100644 index 0000000..60cdb0e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java @@ -0,0 +1,24 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java; + +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.util.List; +import java.util.stream.StreamSupport; + +public class SpringBootMavenJavaArtifactsAdapter implements ProjectArtifactsPort { + + private final List artifacts; + + public SpringBootMavenJavaArtifactsAdapter(List artifacts) { + this.artifacts = artifacts; + } + + @Override + public Iterable generate(ProjectBlueprint blueprint) { + return artifacts.stream() + .flatMap(p -> StreamSupport.stream(p.generate(blueprint).spliterator(), false)) + .toList(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java new file mode 100644 index 0000000..54acd0c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java @@ -0,0 +1,70 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build; + +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency; +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.BuildConfigurationPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class MavenPomBuildConfigurationAdapter extends AbstractSingleTemplateArtifactAdapter + implements BuildConfigurationPort { + + private static final String KEY_GROUP_ID = "groupId"; + private static final String KEY_ARTIFACT_ID = "artifactId"; + private static final String KEY_JAVA_VERSION = "javaVersion"; + private static final String KEY_SPRING_BOOT_VER = "springBootVersion"; + private static final String KEY_DEPENDENCIES = "dependencies"; + private static final String KEY_PROJECT_NAME = "projectName"; + private static final String KEY_PROJECT_DESCRIPTION = "projectDescription"; + + private static final PomDependency CORE_STARTER = + PomDependency.of("org.springframework.boot", "spring-boot-starter"); + + private static final PomDependency TEST_STARTER = + PomDependency.of("org.springframework.boot", "spring-boot-starter-test", null, "test"); + + private final PomDependencyMapper pomDependencyMapper; + + public MavenPomBuildConfigurationAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + PomDependencyMapper pomDependencyMapper) { + super(renderer, artifactDefinition); + this.pomDependencyMapper = pomDependencyMapper; + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.BUILD_CONFIG; + } + + @Override + protected Map buildModel(ProjectBlueprint bp) { + ProjectIdentity id = bp.getIdentity(); + SpringBootJvmTarget pt = (SpringBootJvmTarget) bp.getPlatformTarget(); + + List dependencies = new ArrayList<>(); + dependencies.add(CORE_STARTER); + dependencies.addAll(pomDependencyMapper.from(bp.getDependencies())); + dependencies.add(TEST_STARTER); + + return Map.ofEntries( + entry(KEY_GROUP_ID, id.groupId().value()), + entry(KEY_ARTIFACT_ID, id.artifactId().value()), + entry(KEY_JAVA_VERSION, pt.java().asString()), + entry(KEY_SPRING_BOOT_VER, pt.springBoot().defaultVersion()), + entry(KEY_PROJECT_NAME, bp.getName().value()), + entry(KEY_PROJECT_DESCRIPTION, bp.getDescription().value()), + entry(KEY_DEPENDENCIES, dependencies)); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java new file mode 100644 index 0000000..3e7ead5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config; + +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ApplicationConfigurationPort; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import java.util.Map; + +public class ApplicationYamlAdapter extends AbstractSingleTemplateArtifactAdapter + implements ApplicationConfigurationPort { + + private static final String KEY_PROJECT_NAME = "projectName"; + + public ApplicationYamlAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + super(renderer, artifactDefinition); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.APP_CONFIG; + } + + @Override + protected Map buildModel(ProjectBlueprint blueprint) { + return Map.ofEntries(entry(KEY_PROJECT_NAME, blueprint.getName().value())); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapter.java new file mode 100644 index 0000000..7b91883 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapter.java @@ -0,0 +1,78 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.docs; + +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency; +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ProjectDocumentationPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.List; +import java.util.Map; + +public class ProjectDocumentationAdapter extends AbstractSingleTemplateArtifactAdapter + implements ProjectDocumentationPort { + + private static final String KEY_PROJECT_NAME = "projectName"; + private static final String KEY_PROJECT_DESCRIPTION = "projectDescription"; + private static final String KEY_GROUP_ID = "groupId"; + private static final String KEY_ARTIFACT_ID = "artifactId"; + private static final String KEY_PACKAGE_NAME = "packageName"; + private static final String KEY_BUILD_TOOL = "buildTool"; + private static final String KEY_LANGUAGE = "language"; + private static final String KEY_FRAMEWORK = "framework"; + private static final String KEY_JAVA_VERSION = "javaVersion"; + private static final String KEY_SPRING_BOOT_VERSION = "springBootVersion"; + private static final String KEY_DEPENDENCIES = "dependencies"; + + private final PomDependencyMapper pomDependencyMapper; + + public ProjectDocumentationAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + PomDependencyMapper pomDependencyMapper) { + super(renderer, artifactDefinition); + this.pomDependencyMapper = pomDependencyMapper; + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.PROJECT_DOCUMENTATION; + } + + @Override + protected Map buildModel(ProjectBlueprint bp) { + ProjectIdentity id = bp.getIdentity(); + TechStack stack = bp.getTechStack(); + SpringBootJvmTarget pt = (SpringBootJvmTarget) bp.getPlatformTarget(); + + PackageName pkg = bp.getPackageName(); + Dependencies deps = bp.getDependencies(); + + List mappedDeps = pomDependencyMapper.from(deps); + + boolean hex = bp.getLayout().isHexagonal(); + + return Map.ofEntries( + entry(KEY_PROJECT_NAME, bp.getName().value()), + entry(KEY_PROJECT_DESCRIPTION, bp.getDescription().value()), + entry(KEY_GROUP_ID, id.groupId().value()), + entry(KEY_ARTIFACT_ID, id.artifactId().value()), + entry(KEY_PACKAGE_NAME, pkg.value()), + entry(KEY_BUILD_TOOL, stack.buildTool().key()), + entry(KEY_LANGUAGE, stack.language().key()), + entry(KEY_FRAMEWORK, stack.framework().key()), + entry(KEY_JAVA_VERSION, pt.java().asString()), + entry(KEY_SPRING_BOOT_VERSION, pt.springBoot().defaultVersion()), + entry(KEY_DEPENDENCIES, mappedDeps), + entry("hasHexSample", hex)); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java new file mode 100644 index 0000000..f693a94 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java @@ -0,0 +1,30 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore; + +import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.IgnoreRulesPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import java.util.List; +import java.util.Map; + +public class GitIgnoreAdapter extends AbstractSingleTemplateArtifactAdapter + implements IgnoreRulesPort { + + private static final String KEY_IGNORE_LIST = "ignoreList"; + + public GitIgnoreAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + super(renderer, artifactDefinition); + } + + @Override + protected Map buildModel(ProjectBlueprint blueprint) { + return Map.of(KEY_IGNORE_LIST, List.of()); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.IGNORE_RULES; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java new file mode 100644 index 0000000..fc249d8 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java @@ -0,0 +1,64 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared; + +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public abstract class AbstractJavaSourceFileAdapter implements ArtifactPort { + + private static final String KEY_PROJECT_PACKAGE = "projectPackageName"; + private static final String KEY_CLASS_NAME = "className"; + private static final String JAVA_FILE_EXTENSION = ".java"; + + private static final String PACKAGE_PATH_DELIMITER = "."; + private static final String FILE_PATH_DELIMITER = "/"; + + private final TemplateRenderer renderer; + private final ArtifactDefinition artifactDefinition; + private final StringCaseFormatter stringCaseFormatter; + + protected AbstractJavaSourceFileAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + StringCaseFormatter stringCaseFormatter) { + this.renderer = renderer; + this.artifactDefinition = artifactDefinition; + this.stringCaseFormatter = stringCaseFormatter; + } + + @Override + public final Iterable generate(ProjectBlueprint blueprint) { + String className = buildClassName(blueprint); + PackageName packageName = blueprint.getPackageName(); + + Map model = + Map.ofEntries( + entry(KEY_PROJECT_PACKAGE, packageName.value()), entry(KEY_CLASS_NAME, className)); + + TemplateDefinition templateDefinition = artifactDefinition.templates().getFirst(); + Path baseDir = Path.of(templateDefinition.outputPath()); + String templateName = artifactDefinition.basePath() + templateDefinition.template(); + + String packagePath = packageName.value().replace(PACKAGE_PATH_DELIMITER, FILE_PATH_DELIMITER); + Path outPath = baseDir.resolve(packagePath).resolve(className + JAVA_FILE_EXTENSION); + + GeneratedFile file = renderer.renderUtf8(outPath, templateName, model); + return List.of(file); + } + + protected String pascal(String value) { + return stringCaseFormatter.toPascalCase(value); + } + + protected abstract String buildClassName(ProjectBlueprint blueprint); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java new file mode 100644 index 0000000..ce28c40 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java @@ -0,0 +1,34 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source; + +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared.AbstractJavaSourceFileAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.MainSourceEntrypointPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; + +public class MainSourceEntrypointAdapter extends AbstractJavaSourceFileAdapter + implements MainSourceEntrypointPort { + + public static final String POSTFIX_APPLICATION = "Application"; + + public MainSourceEntrypointAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + StringCaseFormatter stringCaseFormatter) { + super(renderer, artifactDefinition, stringCaseFormatter); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.MAIN_SOURCE_ENTRY_POINT; + } + + @Override + protected String buildClassName(ProjectBlueprint blueprint) { + ProjectIdentity id = blueprint.getIdentity(); + return pascal(id.artifactId().value()) + POSTFIX_APPLICATION; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapter.java new file mode 100644 index 0000000..2c93a5d --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapter.java @@ -0,0 +1,34 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.test; + +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared.AbstractJavaSourceFileAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.TestSourceEntrypointPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; + +public class TestSourceEntrypointAdapter extends AbstractJavaSourceFileAdapter + implements TestSourceEntrypointPort { + + public static final String POSTFIX_APPLICATION_TESTS = "ApplicationTests"; + + public TestSourceEntrypointAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + StringCaseFormatter stringCaseFormatter) { + super(renderer, artifactDefinition, stringCaseFormatter); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.TEST_SOURCE_ENTRY_POINT; + } + + @Override + protected String buildClassName(ProjectBlueprint blueprint) { + ProjectIdentity id = blueprint.getIdentity(); + return pascal(id.artifactId().value()) + POSTFIX_APPLICATION_TESTS; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java new file mode 100644 index 0000000..c7cf61b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java @@ -0,0 +1,38 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.wrapper; + +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.BuildToolFilesPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import java.util.Map; + +public class MavenWrapperBuildToolFilesAdapter extends AbstractSingleTemplateArtifactAdapter + implements BuildToolFilesPort { + + private static final String KEY_WRAPPER_VERSION = "wrapperVersion"; + private static final String KEY_MAVEN_VERSION = "mavenVersion"; + + private static final String DEFAULT_WRAPPER_VERSION = "3.3.4"; + private static final String DEFAULT_MAVEN_VERSION = "3.9.11"; + + public MavenWrapperBuildToolFilesAdapter( + TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + super(renderer, artifactDefinition); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.BUILD_TOOL_METADATA; + } + + @Override + protected Map buildModel(ProjectBlueprint blueprint) { + return Map.ofEntries( + entry(KEY_WRAPPER_VERSION, DEFAULT_WRAPPER_VERSION), + entry(KEY_MAVEN_VERSION, DEFAULT_MAVEN_VERSION)); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java new file mode 100644 index 0000000..8bdcdf7 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java @@ -0,0 +1,38 @@ +package io.github.blueprintplatform.codegen.adapter.out.shared.artifact; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public abstract class AbstractSingleTemplateArtifactAdapter implements ArtifactPort { + + private final TemplateRenderer renderer; + private final ArtifactDefinition artifactDefinition; + + protected AbstractSingleTemplateArtifactAdapter( + TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + this.renderer = renderer; + this.artifactDefinition = artifactDefinition; + } + + @Override + public final Iterable generate(ProjectBlueprint blueprint) { + TemplateDefinition templateDefinition = artifactDefinition.templates().getFirst(); + + Path outPath = Path.of(templateDefinition.outputPath()); + String templateName = artifactDefinition.basePath() + templateDefinition.template(); + + Map model = buildModel(blueprint); + GeneratedFile file = renderer.renderUtf8(outPath, templateName, model); + + return List.of(file); + } + + protected abstract Map buildModel(ProjectBlueprint blueprint); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java new file mode 100644 index 0000000..73535bf --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java @@ -0,0 +1,30 @@ +package io.github.blueprintplatform.codegen.adapter.out.templating; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import io.github.blueprintplatform.codegen.adapter.error.exception.TemplateRenderingException; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; + +public class FreeMarkerTemplateRenderer implements TemplateRenderer { + + private final Configuration cfg; + + public FreeMarkerTemplateRenderer(Configuration cfg) { + this.cfg = cfg; + } + + @Override + public GeneratedFile renderUtf8(Path outPath, String templateName, Map model) { + try (StringWriter sw = new StringWriter()) { + Template tpl = cfg.getTemplate(templateName); + tpl.process(model, sw); + return new GeneratedFile.Text(outPath, sw.toString(), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new TemplateRenderingException(templateName, e); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java new file mode 100644 index 0000000..8c3f5f5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.adapter.out.templating; + +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.file.Path; +import java.util.Map; + +public interface TemplateRenderer { + GeneratedFile renderUtf8(Path outPath, String templateName, Map model); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java new file mode 100644 index 0000000..edc7805 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java @@ -0,0 +1,24 @@ +package io.github.blueprintplatform.codegen.adapter.shared.naming; + +import java.util.regex.Pattern; + +public class StringCaseFormatter { + + private static final String EMPTY = ""; + private static final Pattern NON_ALPHANUMERIC_DELIMITER = Pattern.compile("[^A-Za-z0-9]+"); + + public String toPascalCase(String raw) { + if (raw == null || raw.isBlank()) return EMPTY; + + String[] parts = NON_ALPHANUMERIC_DELIMITER.split(raw.trim()); + StringBuilder sb = new StringBuilder(parts.length * 8); + + for (String part : parts) { + if (part.isEmpty()) continue; + sb.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) sb.append(part.substring(1).toLowerCase()); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java new file mode 100644 index 0000000..7b6e4e9 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java @@ -0,0 +1,41 @@ +package io.github.blueprintplatform.codegen.application.error.exception; + +import java.io.Serial; + +public abstract class ApplicationException extends RuntimeException { + + @Serial private static final long serialVersionUID = 1L; + + private final String messageKey; + private final transient Object[] args; + + protected ApplicationException(String messageKey, Object... args) { + super(messageKey); + this.messageKey = messageKey; + this.args = args; + } + + protected ApplicationException(String messageKey, Throwable cause, Object... args) { + super(messageKey, cause); + this.messageKey = messageKey; + this.args = args; + } + + protected static Object[] prepend(Object first, Object... rest) { + int extra = rest == null ? 0 : rest.length; + Object[] merged = new Object[1 + extra]; + merged[0] = first; + if (extra > 0) { + System.arraycopy(rest, 0, merged, 1, extra); + } + return merged; + } + + public String getMessageKey() { + return messageKey; + } + + public Object[] getArgs() { + return args; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java new file mode 100644 index 0000000..c75bb7c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java @@ -0,0 +1,10 @@ +package io.github.blueprintplatform.codegen.application.error.exception; + +public final class UnknownArtifactKeyException extends ApplicationException { + + private static final String KEY = "application.artifact.key.unknown"; + + public UnknownArtifactKeyException(String key) { + super(KEY, key); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java new file mode 100644 index 0000000..67649d9 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.application.port.out; + +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; + +public interface ProjectArtifactsPort { + + Iterable generate(ProjectBlueprint blueprint); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java new file mode 100644 index 0000000..069e4d7 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java @@ -0,0 +1,7 @@ +package io.github.blueprintplatform.codegen.application.port.out; + +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; + +public interface ProjectArtifactsSelector { + ProjectArtifactsPort select(TechStack options); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java new file mode 100644 index 0000000..68d68cf --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java @@ -0,0 +1,7 @@ +package io.github.blueprintplatform.codegen.application.port.out.archive; + +import java.nio.file.Path; + +public interface ProjectArchiverPort { + Path archive(Path projectRoot, String artifactId); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java new file mode 100644 index 0000000..ca71b53 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface ApplicationConfigurationPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java new file mode 100644 index 0000000..de051db --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java @@ -0,0 +1,36 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +import io.github.blueprintplatform.codegen.application.error.exception.UnknownArtifactKeyException; +import java.util.Arrays; + +public enum ArtifactKey { + BUILD_CONFIG("build-config"), + BUILD_TOOL_METADATA("build-tool-metadata"), + IGNORE_RULES("ignore-rules"), + APP_CONFIG("app-config"), + MAIN_SOURCE_ENTRY_POINT("main-source-entrypoint"), + TEST_SOURCE_ENTRY_POINT("test-source-entrypoint"), + PROJECT_DOCUMENTATION("project-documentation"); + + private final String key; + + ArtifactKey(String key) { + this.key = key; + } + + public static ArtifactKey fromKey(String key) { + return Arrays.stream(values()) + .filter(a -> a.key.equals(key)) + .findFirst() + .orElseThrow(() -> new UnknownArtifactKeyException(key)); + } + + public String key() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java new file mode 100644 index 0000000..0b6cb09 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java @@ -0,0 +1,10 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; + +public interface ArtifactPort { + ArtifactKey artifactKey(); + + Iterable generate(ProjectBlueprint blueprint); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java new file mode 100644 index 0000000..b51fa70 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface BuildConfigurationPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java new file mode 100644 index 0000000..63a792b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface BuildToolFilesPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java new file mode 100644 index 0000000..acd2d23 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface IgnoreRulesPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java new file mode 100644 index 0000000..844adc7 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface MainSourceEntrypointPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java new file mode 100644 index 0000000..fc68a9e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface ProjectDocumentationPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java new file mode 100644 index 0000000..adf6ac1 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface TestSourceEntrypointPort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java new file mode 100644 index 0000000..8a0ee46 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java @@ -0,0 +1,19 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.nio.file.Path; +import java.util.List; + +public record CreateProjectCommand( + String groupId, + String artifactId, + String projectName, + String projectDescription, + String packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + List dependencies, + Path targetDirectory) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java new file mode 100644 index 0000000..d7ff6bb --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java @@ -0,0 +1,52 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import static io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy.FAIL_IF_EXISTS; + +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import java.nio.file.Path; + +public class CreateProjectHandler implements CreateProjectUseCase { + + private final ProjectBlueprintMapper mapper; + private final ProjectRootPort rootPort; + private final ProjectArtifactsSelector artifactsSelector; + private final ProjectWriterPort writerPort; + private final ProjectArchiverPort archiverPort; + + public CreateProjectHandler( + ProjectBlueprintMapper mapper, + ProjectRootPort rootPort, + ProjectArtifactsSelector artifactsSelector, + ProjectWriterPort writerPort, + ProjectArchiverPort archiverPort) { + this.mapper = mapper; + this.rootPort = rootPort; + this.artifactsSelector = artifactsSelector; + this.writerPort = writerPort; + this.archiverPort = archiverPort; + } + + @Override + public CreateProjectResult handle(CreateProjectCommand command) { + ProjectBlueprint bp = mapper.from(command); + + Path projectRoot = + rootPort.prepareRoot( + command.targetDirectory(), bp.getIdentity().artifactId().value(), FAIL_IF_EXISTS); + + ProjectArtifactsPort port = artifactsSelector.select(bp.getTechStack()); + var files = port.generate(bp); + + writerPort.write(projectRoot, files); + + String baseName = bp.getIdentity().artifactId().value(); + Path archive = archiverPort.archive(projectRoot, baseName); + + return new CreateProjectResult(archive); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java new file mode 100644 index 0000000..04c0224 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java @@ -0,0 +1,5 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import java.nio.file.Path; + +public record CreateProjectResult(Path archivePath) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java new file mode 100644 index 0000000..ca16e97 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java @@ -0,0 +1,5 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +public interface CreateProjectUseCase { + CreateProjectResult handle(CreateProjectCommand command); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java new file mode 100644 index 0000000..20faf80 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +public record DependencyInput(String groupId, String artifactId, String version, String scope) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java new file mode 100644 index 0000000..7dbb54d --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java @@ -0,0 +1,65 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import io.github.blueprintplatform.codegen.domain.factory.ProjectBlueprintFactory; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import java.util.ArrayList; +import java.util.List; + +public class ProjectBlueprintMapper { + + public ProjectBlueprint from(CreateProjectCommand c) { + ProjectIdentity identity = + new ProjectIdentity(new GroupId(c.groupId()), new ArtifactId(c.artifactId())); + + ProjectName name = new ProjectName(c.projectName()); + ProjectDescription description = new ProjectDescription(c.projectDescription()); + PackageName pkg = new PackageName(c.packageName()); + + PlatformTarget target = c.platformTarget(); + ProjectLayout layout = c.layout(); + + Dependencies deps = mapDependencies(c.dependencies()); + + return ProjectBlueprintFactory.of( + identity, name, description, pkg, c.techStack(), layout, target, deps); + } + + private Dependencies mapDependencies(List raw) { + if (raw == null || raw.isEmpty()) { + return Dependencies.of(List.of()); + } + + List items = new ArrayList<>(raw.size()); + for (DependencyInput d : raw) { + DependencyVersion version = + (d.version() == null || d.version().isBlank()) + ? null + : new DependencyVersion(d.version()); + + DependencyScope scope = + (d.scope() == null || d.scope().isBlank()) + ? null + : DependencyScope.valueOf(d.scope().trim().toUpperCase()); + + items.add( + new Dependency( + new DependencyCoordinates(new GroupId(d.groupId()), new ArtifactId(d.artifactId())), + version, + scope)); + } + return Dependencies.of(items); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java new file mode 100644 index 0000000..a97e6af --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java @@ -0,0 +1,8 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record ArtifactDefinition( + String basePath, @Valid @NotNull List templates) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java new file mode 100644 index 0000000..26fada8 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java @@ -0,0 +1,16 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationPropertiesBinding +public class ArtifactKeyConverter implements Converter { + @Override + public ArtifactKey convert(@NonNull String source) { + return ArtifactKey.fromKey(source); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java new file mode 100644 index 0000000..8ab0944 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java @@ -0,0 +1,49 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.error.exception.ProfileConfigurationException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties(prefix = "codegen") +public record CodegenProfilesProperties(@Valid @NotNull Map profiles) { + + public ArtifactDefinition artifact(ProfileType profile, ArtifactKey artifactKey) { + var profileProps = requireProfile(profile); + return requireArtifact(profile, profileProps, artifactKey); + } + + public ProfileProperties requireProfile(ProfileType profile) { + var key = profile.key(); + var profileProps = profiles.get(key); + if (profileProps == null) { + throw new ProfileConfigurationException( + ProfileConfigurationException.KEY_PROFILE_NOT_FOUND, key); + } + return profileProps; + } + + ArtifactDefinition requireArtifact( + ProfileType profile, ProfileProperties profileProps, ArtifactKey artifactKey) { + + ArtifactDefinition artifact = profileProps.artifacts().get(artifactKey.key()); + if (artifact == null) { + throw new ProfileConfigurationException( + ProfileConfigurationException.KEY_ARTIFACT_NOT_FOUND, artifactKey.key(), profile.key()); + } + + String basePath = profileProps.templateBasePath(); + + if (basePath == null || basePath.isBlank()) { + throw new ProfileConfigurationException( + ProfileConfigurationException.KEY_TEMPLATE_BASE_MISSING, profile.key()); + } + + return new ArtifactDefinition(basePath, artifact.templates()); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java new file mode 100644 index 0000000..00db4c5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +public record ProfileProperties( + @NotBlank String templateBasePath, + @Valid @NotNull List orderedArtifactKeys, + @Valid @NotNull Map artifacts) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java new file mode 100644 index 0000000..74c93ef --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java @@ -0,0 +1,5 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import jakarta.validation.constraints.NotBlank; + +public record TemplateDefinition(@NotBlank String template, @NotBlank String outputPath) {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java new file mode 100644 index 0000000..9681ddd --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java @@ -0,0 +1,38 @@ +package io.github.blueprintplatform.codegen.bootstrap.error.exception; + +import java.io.Serial; + +public abstract class InfrastructureException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; + + private final String messageKey; + private final transient Object[] args; + + protected InfrastructureException(String messageKey, Object... args) { + super(messageKey); + this.messageKey = messageKey; + this.args = args; + } + + protected InfrastructureException(String messageKey, Throwable cause, Object... args) { + super(messageKey, cause); + this.messageKey = messageKey; + this.args = args; + } + + protected static Object[] prepend(Object first, Object... rest) { + int extra = (rest == null) ? 0 : rest.length; + Object[] merged = new Object[1 + extra]; + merged[0] = first; + if (extra > 0) System.arraycopy(rest, 0, merged, 1, extra); + return merged; + } + + public String getMessageKey() { + return messageKey; + } + + public Object[] getArgs() { + return args; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java new file mode 100644 index 0000000..68162e6 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java @@ -0,0 +1,15 @@ +package io.github.blueprintplatform.codegen.bootstrap.error.exception; + +public final class ProfileConfigurationException extends InfrastructureException { + public static final String KEY_PROFILE_NOT_FOUND = "bootstrap.profile.not.found"; + public static final String KEY_ARTIFACT_NOT_FOUND = "bootstrap.artifact.not.found"; + public static final String KEY_TEMPLATE_BASE_MISSING = "bootstrap.template.base.missing"; + + public ProfileConfigurationException(String key, Object... args) { + super(key, args); + } + + public ProfileConfigurationException(String key, Throwable cause, Object... args) { + super(key, cause, args); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java new file mode 100644 index 0000000..0be67b5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java @@ -0,0 +1,58 @@ +package io.github.blueprintplatform.codegen.bootstrap.templating; + +import static freemarker.template.Configuration.VERSION_2_3_34; + +import freemarker.template.Configuration; +import freemarker.template.TemplateExceptionHandler; +import freemarker.template.Version; +import io.github.blueprintplatform.codegen.adapter.out.templating.FreeMarkerTemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@org.springframework.context.annotation.Configuration +@EnableConfigurationProperties(FreeMarkerTemplatingProperties.class) +public class FreeMarkerTemplatingConfig { + + public static final String NUMBER_FORMAT_COMPUTER = "computer"; + + private static final Version FM_VER = VERSION_2_3_34; + + private final FreeMarkerTemplatingProperties props; + + public FreeMarkerTemplatingConfig(FreeMarkerTemplatingProperties props) { + this.props = props; + } + + @Bean + Configuration freemarkerConfiguration() { + Configuration cfg = new Configuration(FM_VER); + cfg.setDefaultEncoding(props.encoding()); + cfg.setOutputEncoding(props.encoding()); + cfg.setClassForTemplateLoading(getClass(), props.templatePath()); + cfg.setTemplateExceptionHandler(toHandler(props.handler())); + cfg.setLogTemplateExceptions(false); + cfg.setWrapUncheckedExceptions(true); + cfg.setLocalizedLookup(false); + cfg.setNumberFormat(NUMBER_FORMAT_COMPUTER); + cfg.setFallbackOnNullLoopVariable(false); + + cfg.setTemplateUpdateDelayMilliseconds(props.cacheEnabled() ? props.cacheUpdateDelayMs() : 0L); + + return cfg; + } + + @Bean + TemplateRenderer templateRenderer(Configuration freemarkerConfiguration) { + return new FreeMarkerTemplateRenderer(freemarkerConfiguration); + } + + private TemplateExceptionHandler toHandler(FreeMarkerTemplatingProperties.Handler h) { + return switch (h) { + case RETHROW -> TemplateExceptionHandler.RETHROW_HANDLER; + case DEBUG -> TemplateExceptionHandler.DEBUG_HANDLER; + case HTML_DEBUG -> TemplateExceptionHandler.HTML_DEBUG_HANDLER; + case IGNORE -> TemplateExceptionHandler.IGNORE_HANDLER; + }; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java new file mode 100644 index 0000000..0b74e6b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java @@ -0,0 +1,22 @@ +package io.github.blueprintplatform.codegen.bootstrap.templating; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties(prefix = "templating") +public record FreeMarkerTemplatingProperties( + @NotBlank String encoding, + @NotNull Handler handler, + @NotBlank String templatePath, + boolean cacheEnabled, + long cacheUpdateDelayMs) { + public enum Handler { + RETHROW, + DEBUG, + HTML_DEBUG, + IGNORE + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java new file mode 100644 index 0000000..e13a448 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.application.project; + +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectHandler; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase; +import io.github.blueprintplatform.codegen.application.usecase.project.ProjectBlueprintMapper; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProjectUseCaseConfig { + + @Bean + public ProjectBlueprintMapper projectBlueprintMapper() { + return new ProjectBlueprintMapper(); + } + + @Bean + public CreateProjectUseCase createProjectHandler( + ProjectBlueprintMapper mapper, + ProjectRootPort rootPort, + ProjectArtifactsSelector artifactsSelector, + ProjectWriterPort writerPort, + ProjectArchiverPort archiverPort) { + + return new CreateProjectHandler(mapper, rootPort, artifactsSelector, writerPort, archiverPort); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java new file mode 100644 index 0000000..c325117 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java @@ -0,0 +1,20 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.common; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CodegenCommonConfig { + + @Bean + public StringCaseFormatter stringCaseFormatter() { + return new StringCaseFormatter(); + } + + @Bean + public PomDependencyMapper pomDependencyMapper() { + return new PomDependencyMapper(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java new file mode 100644 index 0000000..3000109 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java @@ -0,0 +1,21 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCliExceptionHandler; +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CliCommonConfig { + + @Bean + public CodegenCommand codegenCommand() { + return new CodegenCommand(); + } + + @Bean + public CodegenCliExceptionHandler codegenCliExceptionHandler(MessageSource messageSource) { + return new CodegenCliExceptionHandler(messageSource); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java new file mode 100644 index 0000000..a3f90b9 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java @@ -0,0 +1,104 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCliExceptionHandler; +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand; +import io.github.blueprintplatform.codegen.adapter.in.cli.shared.KeyedEnumConverter; +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import picocli.CommandLine; + +@Component +public class CodegenCliRunner implements ApplicationRunner { + + private static final String CLI_OPTION_NAME = "cli"; + private static final String CLI_FLAG = "--" + CLI_OPTION_NAME; + private static final String LONG_OPTION_PREFIX = "--"; + + private static final List FILTERED_PREFIXES = List.of("--spring."); + + private final CodegenCommand codegenCommand; + private final CommandLine.IFactory factory; + private final CodegenCliExceptionHandler exceptionHandler; + + public CodegenCliRunner( + CodegenCommand codegenCommand, + CommandLine.IFactory factory, + CodegenCliExceptionHandler exceptionHandler) { + this.codegenCommand = codegenCommand; + this.factory = factory; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void run(ApplicationArguments args) { + if (!args.containsOption(CLI_OPTION_NAME)) { + return; + } + + var cliArgs = extractCliArgs(args.getSourceArgs()); + + var cmd = + new CommandLine(codegenCommand, factory) + .registerConverter(BuildTool.class, new KeyedEnumConverter<>(BuildTool::fromKey)) + .registerConverter(Language.class, new KeyedEnumConverter<>(Language::fromKey)) + .registerConverter( + ProjectLayout.class, new KeyedEnumConverter<>(ProjectLayout::fromKey)) + .registerConverter(JavaVersion.class, new KeyedEnumConverter<>(JavaVersion::fromKey)) + .registerConverter( + SpringBootVersion.class, new KeyedEnumConverter<>(SpringBootVersion::fromKey)) + .registerConverter(SpringBootDependencyAlias.class, SpringBootDependencyAlias::fromKey); + + cmd.setExecutionExceptionHandler(exceptionHandler); + + System.exit(cmd.execute(cliArgs)); + } + + @SuppressWarnings("java:S135") + private String[] extractCliArgs(String[] source) { + var cli = new ArrayList(source.length); + boolean skipNextValue = false; + + for (int i = 0; i < source.length; i++) { + var arg = source[i]; + + if (CLI_FLAG.equals(arg)) { + continue; + } + if (skipNextValue) { + skipNextValue = false; + continue; + } + + if (shouldFilter(arg)) { + if (requiresValueSkip(arg, source, i)) { + skipNextValue = true; + } + continue; + } + + cli.add(arg); + } + + return cli.toArray(String[]::new); + } + + private boolean shouldFilter(String arg) { + return FILTERED_PREFIXES.stream().anyMatch(arg::startsWith); + } + + private boolean requiresValueSkip(String arg, String[] source, int index) { + var nextIndex = index + 1; + return !arg.contains("=") + && nextIndex < source.length + && !source[nextIndex].startsWith(LONG_OPTION_PREFIX); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java new file mode 100644 index 0000000..bdfe390 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java @@ -0,0 +1,33 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import picocli.CommandLine; + +@Component +public class PicocliSpringFactory implements CommandLine.IFactory { + + private static final String CLI_COMMAND_BASE_PACKAGE = + CodegenCommand.class.getPackage().getName(); + + private final ApplicationContext applicationContext; + private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory(); + + public PicocliSpringFactory(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public K create(Class cls) throws Exception { + if (isCliCommandType(cls)) { + return applicationContext.getBean(cls); + } + return defaultFactory.create(cls); + } + + private boolean isCliCommandType(Class cls) { + String pkg = cls.getPackageName(); + return pkg.startsWith(CLI_COMMAND_BASE_PACKAGE); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java new file mode 100644 index 0000000..45536eb --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java @@ -0,0 +1,23 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli; + +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.CreateProjectCommandMapper; +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.SpringBootGenerateCommand; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringBootCliConfig { + + @Bean + public CreateProjectCommandMapper springBootCreateProjectCommandMapper() { + return new CreateProjectCommandMapper(); + } + + @Bean + public SpringBootGenerateCommand springBootGenerateCommand( + CreateProjectCommandMapper mapper, CreateProjectUseCase createProjectUseCase) { + + return new SpringBootGenerateCommand(mapper, createProjectUseCase); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java new file mode 100644 index 0000000..8336fce --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java @@ -0,0 +1,29 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.out.filesystem; + +import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectArchiverAdapter; +import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectRootAdapter; +import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectWriterAdapter; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProjectFilesystemConfig { + + @Bean + public ProjectRootPort fileSystemProjectRootAdapter() { + return new FileSystemProjectRootAdapter(); + } + + @Bean + public ProjectWriterPort fileSystemProjectWriterAdapter() { + return new FileSystemProjectWriterAdapter(); + } + + @Bean + public ProjectArchiverPort fileSystemProjectArchiverAdapter() { + return new FileSystemProjectArchiverAdapter(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java new file mode 100644 index 0000000..bb70256 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.out.profile; + +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileBasedArtifactsSelector; +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProjectArtifactsSelectorConfig { + + @Bean + public Map projectArtifactsPortRegistry( + ProjectArtifactsPort springBootMavenJavaArtifactsAdapter) { + + Map registry = new EnumMap<>(ProfileType.class); + registry.put(ProfileType.SPRINGBOOT_MAVEN_JAVA, springBootMavenJavaArtifactsAdapter); + return Collections.unmodifiableMap(registry); + } + + @Bean + public ProjectArtifactsSelector projectArtifactsSelector( + Map projectArtifactsPortRegistry) { + + return new ProfileBasedArtifactsSelector(projectArtifactsPortRegistry); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java new file mode 100644 index 0000000..365548c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java @@ -0,0 +1,148 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.out.profile; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactKeyMismatchException; +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.SpringBootMavenJavaArtifactsAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build.MavenPomBuildConfigurationAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config.ApplicationYamlAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.docs.ProjectDocumentationAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore.GitIgnoreAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.MainSourceEntrypointAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.test.TestSourceEntrypointAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.wrapper.MavenWrapperBuildToolFilesAdapter; +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.CodegenProfilesProperties; +import io.github.blueprintplatform.codegen.bootstrap.error.exception.ProfileConfigurationException; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringBootMavenJavaConfig { + + @Bean + MavenPomBuildConfigurationAdapter springBootMavenJavaMavenPomBuildConfigurationAdapter( + TemplateRenderer renderer, + CodegenProfilesProperties profiles, + PomDependencyMapper pomDependencyMapper) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.BUILD_CONFIG); + return new MavenPomBuildConfigurationAdapter(renderer, props, pomDependencyMapper); + } + + @Bean + MavenWrapperBuildToolFilesAdapter springBootMavenJavaMavenWrapperBuildToolFilesAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.BUILD_TOOL_METADATA); + return new MavenWrapperBuildToolFilesAdapter(renderer, props); + } + + @Bean + GitIgnoreAdapter springBootMavenJavaGitIgnoreAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.IGNORE_RULES); + return new GitIgnoreAdapter(renderer, props); + } + + @Bean + ApplicationYamlAdapter springBootMavenJavaApplicationYamlAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.APP_CONFIG); + return new ApplicationYamlAdapter(renderer, props); + } + + @Bean + MainSourceEntrypointAdapter springBootMavenJavaMainSourceEntrypointAdapter( + TemplateRenderer renderer, + CodegenProfilesProperties profiles, + StringCaseFormatter stringCaseFormatter) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.MAIN_SOURCE_ENTRY_POINT); + return new MainSourceEntrypointAdapter(renderer, props, stringCaseFormatter); + } + + @Bean + TestSourceEntrypointAdapter springBootMavenJavaTestSourceEntrypointAdapter( + TemplateRenderer renderer, + CodegenProfilesProperties profiles, + StringCaseFormatter stringCaseFormatter) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.TEST_SOURCE_ENTRY_POINT); + return new TestSourceEntrypointAdapter(renderer, props, stringCaseFormatter); + } + + @Bean + ProjectDocumentationAdapter springBootMavenJavaProjectDocumentationAdapter( + TemplateRenderer renderer, + CodegenProfilesProperties profiles, + PomDependencyMapper pomDependencyMapper) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.PROJECT_DOCUMENTATION); + return new ProjectDocumentationAdapter(renderer, props, pomDependencyMapper); + } + + @Bean + Map springBootMavenJavaArtifactRegistry( + MavenPomBuildConfigurationAdapter springBootMavenJavaMavenPomBuildConfigurationAdapter, + MavenWrapperBuildToolFilesAdapter springBootMavenJavaMavenWrapperBuildToolFilesAdapter, + GitIgnoreAdapter springBootMavenJavaGitIgnoreAdapter, + ApplicationYamlAdapter springBootMavenJavaApplicationYamlAdapter, + MainSourceEntrypointAdapter springBootMavenJavaMainSourceEntrypointAdapter, + TestSourceEntrypointAdapter springBootMavenJavaTestSourceEntrypointAdapter, + ProjectDocumentationAdapter springBootMavenJavaProjectDocumentationAdapter) { + + Map registry = new EnumMap<>(ArtifactKey.class); + registry.put(ArtifactKey.BUILD_CONFIG, springBootMavenJavaMavenPomBuildConfigurationAdapter); + registry.put( + ArtifactKey.BUILD_TOOL_METADATA, springBootMavenJavaMavenWrapperBuildToolFilesAdapter); + registry.put(ArtifactKey.IGNORE_RULES, springBootMavenJavaGitIgnoreAdapter); + registry.put(ArtifactKey.APP_CONFIG, springBootMavenJavaApplicationYamlAdapter); + registry.put( + ArtifactKey.MAIN_SOURCE_ENTRY_POINT, springBootMavenJavaMainSourceEntrypointAdapter); + registry.put( + ArtifactKey.TEST_SOURCE_ENTRY_POINT, springBootMavenJavaTestSourceEntrypointAdapter); + registry.put(ArtifactKey.PROJECT_DOCUMENTATION, springBootMavenJavaProjectDocumentationAdapter); + return Collections.unmodifiableMap(registry); + } + + @Bean + ProjectArtifactsPort springBootMavenJavaArtifactsAdapter( + CodegenProfilesProperties codegenProfilesProperties, + Map springBootMavenJavaArtifactRegistry) { + + var profile = codegenProfilesProperties.requireProfile(ProfileType.SPRINGBOOT_MAVEN_JAVA); + var orderedArtifactKeys = profile.orderedArtifactKeys(); + + List ordered = + orderedArtifactKeys.stream() + .map( + key -> { + ArtifactPort port = springBootMavenJavaArtifactRegistry.get(key); + if (port == null) { + throw new ProfileConfigurationException( + "bootstrap.artifact.not.found", + key.key(), + ProfileType.SPRINGBOOT_MAVEN_JAVA.key()); + } + if (!port.artifactKey().equals(key)) { + throw new ArtifactKeyMismatchException(key, port.artifactKey()); + } + return port; + }) + .toList(); + + return new SpringBootMavenJavaArtifactsAdapter(ordered); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java new file mode 100644 index 0000000..4d38eb7 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java @@ -0,0 +1,5 @@ +package io.github.blueprintplatform.codegen.domain.error.code; + +public interface ErrorCode { + String key(); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java new file mode 100644 index 0000000..fb8a771 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.error.code; + +public final class ErrorKeys { + private ErrorKeys() {} + + public static ErrorCode compose(Field field, Violation v) { + return () -> field.key() + v.suffix; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java new file mode 100644 index 0000000..ba14d8e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java @@ -0,0 +1,29 @@ +package io.github.blueprintplatform.codegen.domain.error.code; + +public enum Field implements ErrorCode { + PROJECT_NAME(project("name")), + PROJECT_DESCRIPTION(project("description")), + GROUP_ID(project("group-id")), + ARTIFACT_ID(project("artifact-id")), + PACKAGE_NAME(project("package-name")), + DEPENDENCY_VERSION(dependency("version")); + + private final String key; + + Field(String key) { + this.key = key; + } + + private static String project(String suffix) { + return "project." + suffix; + } + + private static String dependency(String suffix) { + return "dependency." + suffix; + } + + @Override + public String key() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java new file mode 100644 index 0000000..b04eb8c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java @@ -0,0 +1,21 @@ +package io.github.blueprintplatform.codegen.domain.error.code; + +public enum Violation { + NOT_BLANK(".not.blank"), + LENGTH(".length"), + INVALID_CHARS(".invalid.chars"), + RESERVED(".reserved"), + + STARTS_WITH_LETTER(".starts.with.letter"), + EDGE_CHAR(".edge.char"), + CONSECUTIVE_CHAR(".consecutive.char"), + SEGMENT_FORMAT(".segment.format"), + RESERVED_PREFIX(".reserved.prefix"), + CONTROL_CHARS(".control.chars"); + + public final String suffix; + + Violation(String s) { + this.suffix = s; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java new file mode 100644 index 0000000..4eb5a51 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java @@ -0,0 +1,26 @@ +package io.github.blueprintplatform.codegen.domain.error.exception; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; + +public abstract class DomainException extends RuntimeException { + private final transient ErrorCode code; + private final transient Object[] args; + + protected DomainException(ErrorCode code, Object... args) { + super(code.key()); + this.code = code; + this.args = args; + } + + public ErrorCode getCode() { + return code; + } + + public String getMessageKey() { + return code.key(); + } + + public Object[] getArgs() { + return args; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java new file mode 100644 index 0000000..6c27bff --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.error.exception; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; + +public class DomainViolationException extends DomainException { + public DomainViolationException(ErrorCode code, Object... args) { + super(code, args); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java b/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java new file mode 100644 index 0000000..65c9930 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java @@ -0,0 +1,78 @@ +package io.github.blueprintplatform.codegen.domain.factory; + +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.policy.tech.CompatibilityPolicy; +import java.util.Arrays; +import java.util.List; + +public final class ProjectBlueprintFactory { + + private ProjectBlueprintFactory() {} + + public static ProjectBlueprint of( + ProjectIdentity identity, + ProjectName name, + ProjectDescription description, + PackageName packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + Dependencies dependencies) { + + CompatibilityPolicy.ensureCompatible(techStack, platformTarget); + + return new ProjectBlueprint( + identity, name, description, packageName, techStack, layout, platformTarget, dependencies); + } + + public static ProjectBlueprint of( + ProjectIdentity identity, + ProjectName name, + ProjectDescription description, + PackageName packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + List dependencies) { + + return of( + identity, + name, + description, + packageName, + techStack, + layout, + platformTarget, + Dependencies.of(dependencies)); + } + + public static ProjectBlueprint of( + ProjectIdentity identity, + ProjectName name, + ProjectDescription description, + PackageName packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + Dependency... deps) { + + return of( + identity, + name, + description, + packageName, + techStack, + layout, + platformTarget, + Dependencies.of(Arrays.asList(deps))); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java new file mode 100644 index 0000000..e187273 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java @@ -0,0 +1,73 @@ +package io.github.blueprintplatform.codegen.domain.model; + +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; + +public class ProjectBlueprint { + + private final ProjectIdentity identity; + private final ProjectName name; + private final ProjectDescription description; + private final PackageName packageName; + private final TechStack techStack; + private final ProjectLayout layout; + private final PlatformTarget platformTarget; + private final Dependencies dependencies; + + public ProjectBlueprint( + ProjectIdentity identity, + ProjectName name, + ProjectDescription description, + PackageName packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + Dependencies dependencies) { + this.identity = identity; + this.name = name; + this.description = description; + this.packageName = packageName; + this.techStack = techStack; + this.layout = layout; + this.platformTarget = platformTarget; + this.dependencies = dependencies; + } + + public ProjectIdentity getIdentity() { + return identity; + } + + public ProjectName getName() { + return name; + } + + public ProjectDescription getDescription() { + return description; + } + + public PackageName getPackageName() { + return packageName; + } + + public TechStack getTechStack() { + return techStack; + } + + public ProjectLayout getLayout() { + return layout; + } + + public PlatformTarget getPlatformTarget() { + return platformTarget; + } + + public Dependencies getDependencies() { + return dependencies; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java new file mode 100644 index 0000000..575ba82 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java @@ -0,0 +1,24 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import io.github.blueprintplatform.codegen.domain.policy.dependency.DependenciesPolicy; +import java.util.List; + +public final class Dependencies { + private final List items; + + private Dependencies(List items) { + this.items = List.copyOf(items); + } + + public static Dependencies of(List raw) { + return new Dependencies(DependenciesPolicy.enforce(raw)); + } + + public List asList() { + return List.copyOf(items); + } + + public boolean isEmpty() { + return items.isEmpty(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java new file mode 100644 index 0000000..cafc143 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java @@ -0,0 +1,20 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; + +public record Dependency( + DependencyCoordinates coordinates, DependencyVersion version, DependencyScope scope) { + + private static final ErrorCode COORDINATES_REQUIRED = () -> "dependency.coordinates.not.blank"; + + public Dependency { + if (coordinates == null) { + throw new DomainViolationException(COORDINATES_REQUIRED); + } + } + + public boolean isDefaultScope() { + return scope == null || scope == DependencyScope.COMPILE; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java new file mode 100644 index 0000000..8d528c5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java @@ -0,0 +1,17 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; + +public record DependencyCoordinates(GroupId groupId, ArtifactId artifactId) { + + private static final ErrorCode COORDINATES_REQUIRED = () -> "dependency.coordinates.not.blank"; + + public DependencyCoordinates { + if (groupId == null || artifactId == null) { + throw new DomainViolationException(COORDINATES_REQUIRED); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java new file mode 100644 index 0000000..f2ab4b0 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java @@ -0,0 +1,25 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +public enum DependencyScope { + COMPILE("compile"), + PROVIDED("provided"), + RUNTIME("runtime"), + TEST("test"), + SYSTEM("system"), + IMPORT("import"); + + private final String value; + + DependencyScope(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java new file mode 100644 index 0000000..80ed2b5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import io.github.blueprintplatform.codegen.domain.policy.dependency.DependencyVersionPolicy; + +public record DependencyVersion(String value) { + public DependencyVersion { + value = DependencyVersionPolicy.enforce(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java new file mode 100644 index 0000000..fc01b39 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import io.github.blueprintplatform.codegen.domain.policy.identity.ArtifactIdPolicy; + +public record ArtifactId(String value) { + public ArtifactId { + value = ArtifactIdPolicy.enforce(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java new file mode 100644 index 0000000..e199227 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import io.github.blueprintplatform.codegen.domain.policy.identity.GroupIdPolicy; + +public record GroupId(String value) { + public GroupId { + value = GroupIdPolicy.enforce(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java new file mode 100644 index 0000000..3fb8006 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java @@ -0,0 +1,15 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; + +public record ProjectIdentity(GroupId groupId, ArtifactId artifactId) { + + private static final ErrorCode IDENTITY_REQUIRED = () -> "project.identity.not.blank"; + + public ProjectIdentity { + if (groupId == null || artifactId == null) { + throw new DomainViolationException(IDENTITY_REQUIRED); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java new file mode 100644 index 0000000..0394340 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java @@ -0,0 +1,40 @@ +package io.github.blueprintplatform.codegen.domain.model.value.layout; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum ProjectLayout implements KeyedEnum { + STANDARD("standard"), + HEXAGONAL("hexagonal"); + + private static final ErrorCode UNKNOWN = () -> "project.layout.unknown"; + + private final String key; + + ProjectLayout(String key) { + this.key = key; + } + + public static ProjectLayout fromKey(String rawKey) { + return KeyEnumParser.parse(ProjectLayout.class, rawKey, UNKNOWN); + } + + public boolean isHexagonal() { + return this == HEXAGONAL; + } + + public boolean isStandard() { + return this == STANDARD; + } + + @Override + public String key() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java new file mode 100644 index 0000000..82117be --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java @@ -0,0 +1,13 @@ +package io.github.blueprintplatform.codegen.domain.model.value.naming; + +import io.github.blueprintplatform.codegen.domain.policy.naming.ProjectDescriptionPolicy; + +public record ProjectDescription(String value) { + public ProjectDescription { + value = ProjectDescriptionPolicy.enforce(value); + } + + public boolean isEmpty() { + return value.isEmpty(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java new file mode 100644 index 0000000..adf068c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.naming; + +import io.github.blueprintplatform.codegen.domain.policy.naming.ProjectNamePolicy; + +public record ProjectName(String value) { + public ProjectName { + value = ProjectNamePolicy.enforce(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java new file mode 100644 index 0000000..545da64 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.pkg; + +import io.github.blueprintplatform.codegen.domain.policy.pkg.PackageNamePolicy; + +public record PackageName(String value) { + public PackageName { + value = PackageNamePolicy.enforce(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java new file mode 100644 index 0000000..5fc5c55 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java @@ -0,0 +1,42 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.platform; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum JavaVersion implements KeyedEnum { + JAVA_21("21", 21), + JAVA_25("25", 25); + + private static final ErrorCode UNKNOWN = () -> "platform.java-version.unknown"; + + private final String key; + private final int major; + + JavaVersion(String key, int major) { + this.key = key; + this.major = major; + } + + public static JavaVersion fromKey(String raw) { + return KeyEnumParser.parse(JavaVersion.class, raw, UNKNOWN); + } + + @Override + public String key() { + return key; + } + + public int major() { + return major; + } + + public String asString() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java new file mode 100644 index 0000000..6a81938 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.platform; + +public sealed interface PlatformTarget permits SpringBootJvmTarget {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java new file mode 100644 index 0000000..8f8c476 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java @@ -0,0 +1,16 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.platform; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; + +public record SpringBootJvmTarget(JavaVersion java, SpringBootVersion springBoot) + implements PlatformTarget { + + private static final ErrorCode TARGET_REQUIRED = () -> "platform.target.not.blank"; + + public SpringBootJvmTarget { + if (java == null || springBoot == null) { + throw new DomainViolationException(TARGET_REQUIRED); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java new file mode 100644 index 0000000..d6356a4 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java @@ -0,0 +1,44 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.platform; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum SpringBootVersion implements KeyedEnum { + V3_5("3.5", "3.5.8"), // Latest known stable patch for 3.5.x + V3_4("3.4", "3.4.12"); // Latest known stable patch for 3.4.x + + private static final ErrorCode UNKNOWN = () -> "platform.springboot-version.unknown"; + + private final String key; // major.minor + private final String defaultPatch; // full version, e.g. 3.5.8 + + SpringBootVersion(String key, String defaultPatch) { + this.key = key; + this.defaultPatch = defaultPatch; + } + + public static SpringBootVersion fromKey(String raw) { + return KeyEnumParser.parse(SpringBootVersion.class, raw, UNKNOWN); + } + + @Override + public String key() { + return key; + } + + /** Major.minor representation, e.g. 3.5 */ + public String majorMinor() { + return key; + } + + /** Full default version, e.g. 3.5.8 */ + public String defaultVersion() { + return defaultPatch; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java new file mode 100644 index 0000000..329f377 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.stack; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum BuildTool implements KeyedEnum { + MAVEN("maven"); + + private static final ErrorCode UNKNOWN = () -> "project.tech-stack.build-tool.unknown"; + + private final String key; + + BuildTool(String key) { + this.key = key; + } + + public static BuildTool fromKey(String raw) { + return KeyEnumParser.parse(BuildTool.class, raw, UNKNOWN); + } + + @Override + public String key() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java new file mode 100644 index 0000000..292be4c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.stack; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum Framework implements KeyedEnum { + SPRING_BOOT("spring-boot"); + + private static final ErrorCode UNKNOWN = () -> "project.tech-stack.framework.unknown"; + + private final String key; + + Framework(String key) { + this.key = key; + } + + public static Framework fromKey(String raw) { + return KeyEnumParser.parse(Framework.class, raw, UNKNOWN); + } + + @Override + public String key() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java new file mode 100644 index 0000000..87e3caf --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.stack; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser; +import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum; + +public enum Language implements KeyedEnum { + JAVA("java"); + + private static final ErrorCode UNKNOWN = () -> "project.tech-stack.language.unknown"; + + private final String key; + + Language(String key) { + this.key = key; + } + + public static Language fromKey(String raw) { + return KeyEnumParser.parse(Language.class, raw, UNKNOWN); + } + + @Override + public String key() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java new file mode 100644 index 0000000..d4692c6 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.stack; + +import io.github.blueprintplatform.codegen.domain.policy.tech.TechStackPolicy; + +public record TechStack(Framework framework, BuildTool buildTool, Language language) { + public TechStack { + TechStackPolicy.requireNonNull(framework, buildTool, language); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java new file mode 100644 index 0000000..c456c18 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java @@ -0,0 +1,51 @@ +package io.github.blueprintplatform.codegen.domain.policy.dependency; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import java.util.*; + +public final class DependenciesPolicy { + + private static final ErrorCode LIST_REQUIRED = () -> "dependency.list.not.blank"; + private static final ErrorCode ITEM_REQUIRED = () -> "dependency.item.not.blank"; + private static final ErrorCode DUPLICATE_COORDS = () -> "dependency.duplicate.coordinates"; + + private DependenciesPolicy() {} + + public static List enforce(List raw) { + if (raw == null) { + throw new DomainViolationException(LIST_REQUIRED); + } + if (raw.isEmpty()) { + return List.of(); + } + + Map byCoords = getDependencyMap(raw); + + List list = new ArrayList<>(byCoords.values()); + list.sort( + Comparator.comparing( + dep -> + dep.coordinates().groupId().value() + + ":" + + dep.coordinates().artifactId().value())); + return List.copyOf(list); + } + + private static Map getDependencyMap(List raw) { + Map byCoords = new LinkedHashMap<>(); + for (Dependency d : raw) { + if (d == null) { + throw new DomainViolationException(ITEM_REQUIRED); + } + var coords = d.coordinates(); + var key = coords.groupId().value() + ":" + coords.artifactId().value(); + if (byCoords.containsKey(key)) { + throw new DomainViolationException(DUPLICATE_COORDS, key); + } + byCoords.put(key, d); + } + return byCoords; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java new file mode 100644 index 0000000..e5e2ab3 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java @@ -0,0 +1,44 @@ +package io.github.blueprintplatform.codegen.domain.policy.dependency; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.DEPENDENCY_VERSION; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.INVALID_CHARS; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.RegexMatchRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.regex.Pattern; + +public final class DependencyVersionPolicy { + + private static final Pattern ALLOWED = Pattern.compile("^[A-Za-z0-9._\\-+\\[\\](),:{}$\\s]+$"); + + private static final int MIN = 1; + private static final int MAX = 100; + + private DependencyVersionPolicy() {} + + public static String enforce(String raw) { + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + if (raw == null || raw.isBlank()) { + throw new DomainViolationException(compose(DEPENDENCY_VERSION, NOT_BLANK)); + } + return raw.trim(); + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new LengthBetweenRule(MIN, MAX, DEPENDENCY_VERSION), + new RegexMatchRule(ALLOWED, DEPENDENCY_VERSION, INVALID_CHARS)); + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java new file mode 100644 index 0000000..5f11ff9 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.domain.policy.file; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import java.nio.charset.Charset; +import java.nio.file.Path; + +public final class GeneratedFilePolicy { + private GeneratedFilePolicy() {} + + public static void requireRelativePath(Path path) { + if (path == null) throw new DomainViolationException(() -> "file.path.not.blank"); + if (path.isAbsolute()) + throw new DomainViolationException(() -> "file.path.absolute.not.allowed"); + if (path.getNameCount() == 0) throw new DomainViolationException(() -> "file.path.not.blank"); + for (Path part : path) { + String s = part.toString(); + if (s.isEmpty() || ".".equals(s) || "..".equals(s)) { + throw new DomainViolationException(() -> "file.path.traversal.not.allowed"); + } + } + } + + public static void requireTextContent(CharSequence content, Charset charset) { + if (content == null) throw new DomainViolationException(() -> "file.content.not.blank"); + if (charset == null) throw new DomainViolationException(() -> "file.charset.not.blank"); + } + + public static void requireBinaryContent(byte[] bytes) { + if (bytes == null) throw new DomainViolationException(() -> "file.content.not.blank"); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java new file mode 100644 index 0000000..211e9da --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java @@ -0,0 +1,49 @@ +package io.github.blueprintplatform.codegen.domain.policy.identity; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.ARTIFACT_ID; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.AllowedCharsRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NoEdgeCharRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.StartsWithLetterRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Locale; + +public final class ArtifactIdPolicy { + + private static final int MIN = 3; + private static final int MAX = 50; + + private ArtifactIdPolicy() {} + + public static String enforce(String raw) { + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + if (raw == null) throw new DomainViolationException(compose(ARTIFACT_ID, NOT_BLANK)); + return raw.trim() + .replaceAll("\\s+", "-") + .replace('_', '-') + .toLowerCase(Locale.ROOT) + .replaceAll("-{2,}", "-"); + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new NotBlankRule(ARTIFACT_ID), + new LengthBetweenRule(MIN, MAX, ARTIFACT_ID), + new AllowedCharsRule("[a-z0-9-]", ARTIFACT_ID, INVALID_CHARS), + new StartsWithLetterRule(ARTIFACT_ID, STARTS_WITH_LETTER), + new NoEdgeCharRule('-', ARTIFACT_ID, EDGE_CHAR)); + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java new file mode 100644 index 0000000..a3f93a4 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java @@ -0,0 +1,44 @@ +package io.github.blueprintplatform.codegen.domain.policy.identity; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.GROUP_ID; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.DotSeparatedSegmentsRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Locale; +import java.util.regex.Pattern; + +public final class GroupIdPolicy { + + private static final int MIN = 3; + private static final int MAX = 100; + + private static final Pattern SEGMENT = Pattern.compile("^[a-z][a-z0-9]*$"); + + private GroupIdPolicy() {} + + public static String enforce(String raw) { + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + if (raw == null) throw new DomainViolationException(compose(GROUP_ID, NOT_BLANK)); + return raw.trim().replaceAll("\\s+", "").toLowerCase(Locale.ROOT); + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new NotBlankRule(GROUP_ID), + new LengthBetweenRule(MIN, MAX, GROUP_ID), + new DotSeparatedSegmentsRule(SEGMENT, GROUP_ID, SEGMENT_FORMAT)); + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java new file mode 100644 index 0000000..d428e57 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java @@ -0,0 +1,47 @@ +package io.github.blueprintplatform.codegen.domain.policy.naming; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.PROJECT_DESCRIPTION; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.CONTROL_CHARS; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.RegexMatchRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.regex.Pattern; + +public final class ProjectDescriptionPolicy { + + private static final int MIN = 10; + private static final int MAX = 280; + + private static final Pattern NO_CONTROL_CHARS = Pattern.compile("^\\P{Cntrl}*$"); + + private ProjectDescriptionPolicy() {} + + public static String enforce(String raw) { + if (raw == null) { + throw new DomainViolationException(compose(PROJECT_DESCRIPTION, NOT_BLANK)); + } + + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + return raw.trim().replaceAll("\\s+", " "); + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new NotBlankRule(PROJECT_DESCRIPTION), + new LengthBetweenRule(MIN, MAX, PROJECT_DESCRIPTION), + new RegexMatchRule(NO_CONTROL_CHARS, PROJECT_DESCRIPTION, CONTROL_CHARS)); + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java new file mode 100644 index 0000000..b43b21d --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java @@ -0,0 +1,46 @@ +package io.github.blueprintplatform.codegen.domain.policy.naming; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.PROJECT_NAME; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.INVALID_CHARS; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.AllowedCharsRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class ProjectNamePolicy { + + private static final int MIN = 3; + private static final int MAX = 60; + + private static final String ALLOWED_CHARS = "[A-Za-z0-9 .,_'()\\-]"; + + private ProjectNamePolicy() {} + + public static String enforce(String raw) { + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + if (raw == null) { + throw new DomainViolationException(compose(PROJECT_NAME, NOT_BLANK)); + } + return raw.trim(); + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new NotBlankRule(PROJECT_NAME), + new LengthBetweenRule(MIN, MAX, PROJECT_NAME), + new AllowedCharsRule(ALLOWED_CHARS, PROJECT_NAME, INVALID_CHARS)); + + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java new file mode 100644 index 0000000..f92d7b4 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java @@ -0,0 +1,61 @@ +package io.github.blueprintplatform.codegen.domain.policy.pkg; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Field.PACKAGE_NAME; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.DotSeparatedSegmentsRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.ReservedPrefixRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +public final class PackageNamePolicy { + + private static final int MIN = 3; + private static final int MAX = 255; + + private static final Pattern SEGMENT = Pattern.compile("^[a-z][a-z0-9]*$"); + + private static final Pattern SEP_CHARS = Pattern.compile("[\\s_\\-]+"); + private static final Pattern MULTI_DOTS = Pattern.compile("\\.{2,}"); + private static final Pattern LEADING_DOTS = Pattern.compile("^\\.+"); + private static final Pattern TRAILING_DOTS = Pattern.compile("\\.+$"); + + private static final Set RESERVED_PREFIXES = Set.of("java", "javax", "sun", "com.sun"); + + private PackageNamePolicy() {} + + public static String enforce(String raw) { + String n = normalize(raw); + validate(n); + return n; + } + + private static String normalize(String raw) { + if (raw == null) throw new DomainViolationException(compose(PACKAGE_NAME, NOT_BLANK)); + + String s = raw.trim(); + s = SEP_CHARS.matcher(s).replaceAll("."); + s = MULTI_DOTS.matcher(s).replaceAll("."); + s = LEADING_DOTS.matcher(s).replaceAll(""); + s = TRAILING_DOTS.matcher(s).replaceAll(""); + s = s.toLowerCase(Locale.ROOT); + return s; + } + + private static void validate(String value) { + Rule rule = + CompositeRule.of( + new NotBlankRule(PACKAGE_NAME), + new LengthBetweenRule(MIN, MAX, PACKAGE_NAME), + new DotSeparatedSegmentsRule(SEGMENT, PACKAGE_NAME, SEGMENT_FORMAT), + new ReservedPrefixRule(RESERVED_PREFIXES, PACKAGE_NAME, RESERVED_PREFIX)); + rule.check(value); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java new file mode 100644 index 0000000..98f3244 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java @@ -0,0 +1,30 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class AllowedCharsRule implements Rule { + private final Pattern allowed; + private final Field field; + private final Violation violation; + + public AllowedCharsRule(String allowedCharClassRegex, Field field, Violation violation) { + Objects.requireNonNull(allowedCharClassRegex); + this.allowed = Pattern.compile("^(?:" + allowedCharClassRegex + ")+$"); + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null || !allowed.matcher(value).matches()) { + throw new DomainViolationException(compose(field, violation)); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java new file mode 100644 index 0000000..5d46a47 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java @@ -0,0 +1,37 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.regex.Pattern; + +public final class DotSeparatedSegmentsRule implements Rule { + private final Pattern segmentPattern; + private final Field field; + private final Violation violation; + + public DotSeparatedSegmentsRule(Pattern segmentPattern, Field field, Violation violation) { + this.segmentPattern = segmentPattern; + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null) { + throw new DomainViolationException(compose(field, violation)); + } + String[] parts = value.split("\\.", -1); + if (parts.length == 0) { + throw new DomainViolationException(compose(field, violation)); + } + for (String p : parts) { + if (p.isEmpty() || !segmentPattern.matcher(p).matches()) { + throw new DomainViolationException(compose(field, violation), p); + } + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java new file mode 100644 index 0000000..55d3aee --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java @@ -0,0 +1,26 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.LENGTH; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class LengthBetweenRule implements Rule { + private final int min; + private final int max; + private final Field field; + + public LengthBetweenRule(int min, int max, Field field) { + this.min = min; + this.max = max; + this.field = field; + } + + @Override + public void check(String v) { + if (v == null || v.length() < min || v.length() > max) + throw new DomainViolationException(compose(field, LENGTH), min, max); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java new file mode 100644 index 0000000..ac17b80 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java @@ -0,0 +1,32 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class NoConsecutiveCharRule implements Rule { + private final char ch; + private final Field field; + private final Violation violation; + + public NoConsecutiveCharRule(char ch, Field field, Violation violation) { + this.ch = ch; + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null) { + throw new DomainViolationException(compose(field, violation)); + } + for (int i = 1; i < value.length(); i++) { + if (value.charAt(i) == ch && value.charAt(i - 1) == ch) { + throw new DomainViolationException(compose(field, violation)); + } + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java new file mode 100644 index 0000000..d6b8740 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java @@ -0,0 +1,30 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class NoEdgeCharRule implements Rule { + private final char edgeChar; + private final Field field; + private final Violation violation; + + public NoEdgeCharRule(char edgeChar, Field field, Violation violation) { + this.edgeChar = edgeChar; + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null || value.isEmpty()) { + throw new DomainViolationException(compose(field, violation)); + } + if (value.charAt(0) == edgeChar || value.charAt(value.length() - 1) == edgeChar) { + throw new DomainViolationException(compose(field, violation)); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java new file mode 100644 index 0000000..f3e74c8 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java @@ -0,0 +1,22 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; +import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class NotBlankRule implements Rule { + private final Field field; + + public NotBlankRule(Field field) { + this.field = field; + } + + @Override + public void check(String value) { + if (value == null || value.isBlank()) + throw new DomainViolationException(compose(field, NOT_BLANK)); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java new file mode 100644 index 0000000..37ee043 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java @@ -0,0 +1,28 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.regex.Pattern; + +public final class RegexMatchRule implements Rule { + private final Pattern pattern; + private final Field field; + private final Violation violation; + + public RegexMatchRule(Pattern pattern, Field field, Violation violation) { + this.pattern = pattern; + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null || !pattern.matcher(value).matches()) { + throw new DomainViolationException(compose(field, violation)); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java new file mode 100644 index 0000000..cf4d2e0 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java @@ -0,0 +1,33 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +public final class ReservedNamesRule implements Rule { + private final Set reservedLower; + private final Field field; + + public ReservedNamesRule(Set reserved, Field field) { + this.reservedLower = + reserved.stream() + .map(s -> s.toLowerCase(Locale.ROOT)) + .collect(Collectors.toUnmodifiableSet()); + this.field = field; + } + + @Override + public void check(String value) { + if (value == null) throw new DomainViolationException(compose(field, Violation.RESERVED)); + String lower = value.toLowerCase(Locale.ROOT); + if (reservedLower.contains(lower)) { + throw new DomainViolationException(compose(field, Violation.RESERVED), value); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java new file mode 100644 index 0000000..640ae60 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java @@ -0,0 +1,36 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; +import java.util.Locale; +import java.util.Set; + +public final class ReservedPrefixRule implements Rule { + private final Set reservedPrefixesLower; + private final Field field; + private final Violation violation; + + public ReservedPrefixRule(Set reservedPrefixes, Field field, Violation violation) { + this.reservedPrefixesLower = + reservedPrefixes.stream() + .map(s -> s.toLowerCase(Locale.ROOT)) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null) throw new DomainViolationException(compose(field, violation)); + String lower = value.toLowerCase(Locale.ROOT); + for (String p : reservedPrefixesLower) { + if (lower.equals(p) || lower.startsWith(p + ".")) { + throw new DomainViolationException(compose(field, violation), p); + } + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java new file mode 100644 index 0000000..958a99e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java @@ -0,0 +1,29 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule; + +import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose; + +import io.github.blueprintplatform.codegen.domain.error.code.Field; +import io.github.blueprintplatform.codegen.domain.error.code.Violation; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule; + +public final class StartsWithLetterRule implements Rule { + private final Field field; + private final Violation violation; + + public StartsWithLetterRule(Field field, Violation violation) { + this.field = field; + this.violation = violation; + } + + @Override + public void check(String value) { + if (value == null || value.isEmpty()) { + throw new DomainViolationException(compose(field, violation)); + } + char c = value.charAt(0); + if (c < 'a' || c > 'z') { + throw new DomainViolationException(compose(field, violation)); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java new file mode 100644 index 0000000..aa0f97c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java @@ -0,0 +1,31 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule.base; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class CompositeRule implements Rule { + + private final List> rules; + + private CompositeRule(List> rules) { + this.rules = List.copyOf(rules); + } + + @SafeVarargs + public static CompositeRule of(Rule... rules) { + List> list = Arrays.asList(rules); + return new CompositeRule<>(list); + } + + public static CompositeRule of(List> rules) { + return new CompositeRule<>(new ArrayList<>(rules)); + } + + @Override + public void check(T value) { + for (Rule r : rules) { + r.check(value); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java new file mode 100644 index 0000000..7e7cc2e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java @@ -0,0 +1,6 @@ +package io.github.blueprintplatform.codegen.domain.policy.rule.base; + +@FunctionalInterface +public interface Rule { + void check(T value); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java new file mode 100644 index 0000000..f9d1e3b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java @@ -0,0 +1,69 @@ +package io.github.blueprintplatform.codegen.domain.policy.tech; + +import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion.JAVA_21; +import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion.JAVA_25; +import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion.V3_4; +import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion.V3_5; +import static java.util.Map.entry; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class CompatibilityPolicy { + + private static final ErrorCode TARGET_MISSING = () -> "platform.target.missing"; + private static final ErrorCode OPTIONS_UNSUPPORTED = () -> "platform.target.unsupported.options"; + private static final ErrorCode TARGET_INCOMPATIBLE = () -> "platform.target.incompatible"; + + private static final Map> SPRINGBOOT_JAVA_SUPPORT = + Map.ofEntries(entry(V3_5, EnumSet.of(JAVA_21, JAVA_25)), entry(V3_4, EnumSet.of(JAVA_21))); + + private CompatibilityPolicy() {} + + public static void ensureCompatible(TechStack techStack, PlatformTarget target) { + if (techStack == null || target == null) { + throw new DomainViolationException(TARGET_MISSING); + } + + if (techStack.framework() != Framework.SPRING_BOOT + || techStack.language() != Language.JAVA + || techStack.buildTool() != BuildTool.MAVEN) { + throw new DomainViolationException( + OPTIONS_UNSUPPORTED, techStack.framework(), techStack.language(), techStack.buildTool()); + } + + if (!(target instanceof SpringBootJvmTarget(JavaVersion java, SpringBootVersion springBoot))) { + throw new DomainViolationException( + TARGET_INCOMPATIBLE, "SPRING_BOOT", target.getClass().getSimpleName()); + } + + var allowed = SPRINGBOOT_JAVA_SUPPORT.getOrDefault(springBoot, Set.of()); + if (!allowed.contains(java)) { + throw new DomainViolationException( + TARGET_INCOMPATIBLE, springBoot.defaultVersion(), java.asString()); + } + } + + public static List allSupportedTargets() { + List list = new ArrayList<>(); + for (var e : SPRINGBOOT_JAVA_SUPPORT.entrySet()) { + for (var j : e.getValue()) { + list.add(new SpringBootJvmTarget(j, e.getKey())); + } + } + return List.copyOf(list); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java new file mode 100644 index 0000000..3cc0660 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java @@ -0,0 +1,25 @@ +package io.github.blueprintplatform.codegen.domain.policy.tech; + +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.List; + +public final class PlatformTargetSelector { + + private PlatformTargetSelector() {} + + public static PlatformTarget select( + TechStack techStack, JavaVersion preferredJava, SpringBootVersion preferredBoot) { + + var requested = new SpringBootJvmTarget(preferredJava, preferredBoot); + CompatibilityPolicy.ensureCompatible(techStack, requested); + return requested; + } + + public static List supportedTargetsFor() { + return CompatibilityPolicy.allSupportedTargets(); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java new file mode 100644 index 0000000..8d482a4 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java @@ -0,0 +1,28 @@ +package io.github.blueprintplatform.codegen.domain.policy.tech; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; + +public final class TechStackPolicy { + + private TechStackPolicy() {} + + public static TechStack enforce(TechStack techStack) { + if (techStack == null + || techStack.framework() == null + || techStack.buildTool() == null + || techStack.language() == null) { + throw new DomainViolationException(() -> "project.tech-stack.not.blank"); + } + return techStack; + } + + public static void requireNonNull(Framework framework, BuildTool buildTool, Language language) { + if (framework == null || buildTool == null || language == null) { + throw new DomainViolationException(() -> "project.tech-stack.not.blank"); + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFile.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFile.java new file mode 100644 index 0000000..fea488e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFile.java @@ -0,0 +1,59 @@ +package io.github.blueprintplatform.codegen.domain.port.out.artifact; + +import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Arrays; + +public sealed interface GeneratedFile permits GeneratedFile.Text, GeneratedFile.Binary { + + Path relativePath(); + + record Text(Path relativePath, String content, Charset charset) implements GeneratedFile { + public Text { + requireRelativePath(relativePath); + requireTextContent(content, charset); + } + } + + @SuppressWarnings("java:S2384") // Representation exposure false-positive; defensive copy applied + record Binary(Path relativePath, byte[] bytes) implements GeneratedFile { + + public Binary(Path relativePath, byte[] bytes) { + requireRelativePath(relativePath); + requireBinaryContent(bytes); + this.relativePath = relativePath; + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + @Override + public byte[] bytes() { + return Arrays.copyOf(bytes, bytes.length); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Binary(Path path, byte[] bytes1))) { + return false; + } + return relativePath.equals(path) && Arrays.equals(bytes, bytes1); + } + + @Override + public int hashCode() { + int result = relativePath.hashCode(); + result = 31 * result + Arrays.hashCode(bytes); + return result; + } + + @SuppressWarnings("NullableProblems") + @Override + public String toString() { + return "GeneratedFile.Binary[" + relativePath + ", size=" + bytes.length + "]"; + } + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java new file mode 100644 index 0000000..b8ed61a --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java @@ -0,0 +1,6 @@ +package io.github.blueprintplatform.codegen.domain.port.out.filesystem; + +public enum ProjectRootExistencePolicy { + FAIL_IF_EXISTS, + OVERWRITE +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java new file mode 100644 index 0000000..3b3d13f --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java @@ -0,0 +1,8 @@ +package io.github.blueprintplatform.codegen.domain.port.out.filesystem; + +import java.nio.file.Path; + +public interface ProjectRootPort { + + Path prepareRoot(Path targetDir, String artifactId, ProjectRootExistencePolicy policy); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java new file mode 100644 index 0000000..30d15dd --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java @@ -0,0 +1,34 @@ +package io.github.blueprintplatform.codegen.domain.port.out.filesystem; + +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.charset.Charset; +import java.nio.file.Path; + +public interface ProjectWriterPort { + void writeBytes(Path projectRoot, Path relativePath, byte[] content); + + void writeText(Path projectRoot, Path relativePath, String content, Charset charset); + + default void writeText(Path root, Path relative, String content) { + writeText(root, relative, content, java.nio.charset.StandardCharsets.UTF_8); + } + + default void write(Path projectRoot, GeneratedFile file) { + switch (file) { + case GeneratedFile.Text(Path p, String c, Charset cs) -> writeText(projectRoot, p, c, cs); + case GeneratedFile.Binary(Path p, byte[] b) -> writeBytes(projectRoot, p, b); + } + } + + default void write(Path projectRoot, Iterable files) { + for (GeneratedFile f : files) write(projectRoot, f); + } + + default void write(Path projectRoot, GeneratedFile... files) { + for (GeneratedFile f : files) write(projectRoot, f); + } + + default void write(Path projectRoot, java.util.stream.Stream files) { + files.forEach(f -> write(projectRoot, f)); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java new file mode 100644 index 0000000..d7c87d0 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java @@ -0,0 +1,27 @@ +package io.github.blueprintplatform.codegen.domain.shared; + +import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode; +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; + +public final class KeyEnumParser { + + private KeyEnumParser() {} + + public static & KeyedEnum> E parse( + Class type, String raw, ErrorCode unknownCode) { + + if (raw == null || raw.isBlank()) { + throw new DomainViolationException(unknownCode, raw); + } + + String normalized = raw.trim().toLowerCase(); + + for (E e : type.getEnumConstants()) { + if (e.key().equalsIgnoreCase(normalized)) { + return e; + } + } + + throw new DomainViolationException(unknownCode, normalized); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java new file mode 100644 index 0000000..4083e46 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java @@ -0,0 +1,5 @@ +package io.github.blueprintplatform.codegen.domain.shared; + +public interface KeyedEnum { + String key(); +} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java b/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java deleted file mode 100644 index a3241c2..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java +++ /dev/null @@ -1,162 +0,0 @@ -package io.github.bsayli.codegen.initializr.cli; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; -import io.github.bsayli.codegen.initializr.projectgeneration.service.ProjectGenerationService; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.*; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component -@Profile("cli") -public class CliRunner implements ApplicationRunner { - - private static final Logger log = LoggerFactory.getLogger(CliRunner.class); - - private static final String ARG_GROUP_ID = "groupId"; - private static final String ARG_ARTIFACT_ID = "artifactId"; - private static final String ARG_NAME = "name"; - private static final String ARG_PACKAGE_NAME = "packageName"; - private static final String ARG_JAVA_VERSION = "javaVersion"; - private static final String ARG_BOOT_VERSION = "springBootVersion"; - private static final String ARG_OUTPUT_DIR = "outputDir"; - private static final String ARG_OVERWRITE = "overwrite"; - - private static final String DEF_GROUP_ID = "com.example"; - private static final String DEF_ARTIFACT_ID = "demo-app"; - private static final String DEF_PACKAGE_NAME = "com.example.demo"; - private static final String DEF_JAVA_VERSION = "21"; - private static final String DEF_BOOT_VERSION = "3.5.5"; - private static final String DEF_DESCRIPTION = "Generated by codegen-initializr-core"; - private static final boolean DEF_OVERWRITE = false; - - private static final Path DEFAULT_OUTPUT_ROOT = - Paths.get(System.getProperty("user.dir"), "target", "generated-projects"); - - private final ProjectGenerationService service; - - public CliRunner(ProjectGenerationService service) { - this.service = service; - } - - @Override - public void run(ApplicationArguments args) throws Exception { - String groupId = argOrDefault(args, ARG_GROUP_ID, DEF_GROUP_ID); - String artifact = argOrDefault(args, ARG_ARTIFACT_ID, DEF_ARTIFACT_ID); - String name = argOrDefault(args, ARG_NAME, artifact); - String pkg = argOrDefault(args, ARG_PACKAGE_NAME, DEF_PACKAGE_NAME); - String javaVer = argOrDefault(args, ARG_JAVA_VERSION, DEF_JAVA_VERSION); - String bootVer = argOrDefault(args, ARG_BOOT_VERSION, DEF_BOOT_VERSION); - Path outputRoot = argPath(args, ARG_OUTPUT_DIR).orElse(DEFAULT_OUTPUT_ROOT); - boolean overwrite = argBoolean(args, ARG_OVERWRITE, DEF_OVERWRITE); - - Path projectDir = outputRoot.resolve(artifact); - - if (Files.exists(projectDir)) { - if (overwrite) { - log.info( - "♻️ Existing directory found. Deleting before regeneration: {}", - projectDir.toAbsolutePath()); - deleteRecursively(projectDir); - } else { - throw new IllegalStateException( - "Target directory already exists: " - + projectDir.toAbsolutePath() - + " (use --overwrite=true to replace it)"); - } - } - - var depWeb = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); - - var depTest = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); - - var metadata = - new SpringBootJavaProjectMetadataBuilder() - .springBootVersion(bootVer) - .javaVersion(javaVer) - .groupId(groupId) - .artifactId(artifact) - .name(name) - .description(DEF_DESCRIPTION) - .packageName(pkg) - .projectLocation(outputRoot) - .dependencies(List.of(depWeb, depTest)) - .build(); - - var type = new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); - - Path zip = service.generateProject(type, metadata); - - log.info("✅ Project archive generated at: {}", zip.toAbsolutePath()); - if (outputRoot.equals(DEFAULT_OUTPUT_ROOT)) { - log.info("ℹ️ Tip: Use --{}=/absolute/path to control the output location.", ARG_OUTPUT_DIR); - } - } - - private String argOrDefault(ApplicationArguments args, String name, String def) { - var values = args.getOptionValues(name); - return (values != null && !values.isEmpty()) ? values.getFirst() : def; - } - - private Optional argPath(ApplicationArguments args, String name) { - var values = args.getOptionValues(name); - if (values == null || values.isEmpty()) return Optional.empty(); - String raw = values.getFirst(); - if (raw == null || raw.isBlank()) return Optional.empty(); - return Optional.of(Paths.get(raw)); - } - - private boolean argBoolean(ApplicationArguments args, String name, boolean def) { - var values = args.getOptionValues(name); - if (values == null || values.isEmpty()) return def; - String raw = values.getFirst(); - if (raw == null) return def; - return switch (raw.trim().toLowerCase()) { - case "true", "1", "yes", "y" -> true; - case "false", "0", "no", "n" -> false; - default -> def; - }; - } - - private void deleteRecursively(Path dir) { - if (!Files.exists(dir)) return; - - try (Stream paths = Files.walk(dir)) { - paths - .sorted(Comparator.reverseOrder()) - .forEach( - p -> { - try { - Files.deleteIfExists(p); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } catch (IOException e) { - throw new UncheckedIOException("Failed to clean directory: " + dir.toAbsolutePath(), e); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java deleted file mode 100644 index 6a8d26d..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("gitIgnoreFileGenerator") -public class GitIgnoreFileGenerator implements GitIgnoreGenerator { - - private final TemplateEngine freeMarkerTemplateEngine; - - public GitIgnoreFileGenerator(TemplateEngine freeMarkerTemplateEngine) { - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - } - - @Override - public void generateGitIgnoreContent(File projectDestination, List ignoreList) - throws IOException { - - Map gitIgnoreData = new HashMap<>(); - gitIgnoreData.put("ignoreList", ignoreList != null ? ignoreList : Collections.emptyList()); - - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.GITIGNORE, gitIgnoreData, projectDestination); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java deleted file mode 100644 index 05c5c20..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer; -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.springframework.stereotype.Component; - -@Component("projectRootDirectoryInitializer") -public class ProjectRootDirectoryInitializer implements ProjectDirectoryInitializer { - - @Override - public Path initializeProjectDirectory(String projectName) throws IOException { - Path tempPath = Files.createTempDirectory(projectName); - Path projectPath = Paths.get(tempPath.toString(), projectName); - Files.createDirectories(projectPath); - return projectPath; - } - - @Override - public Path initializeProjectDirectory(String projectName, Path projectLocation) - throws IOException { - if (projectLocation != null) { - Path projectDir = projectLocation.resolve(projectName); - if (Files.exists(projectDir)) { - throw new FileAlreadyExistsException(projectDir.toString(), null, "File already exists!"); - } - Files.createDirectories(projectDir); // Create directories recursively - return projectDir; - } else { - return initializeProjectDirectory(projectName); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java deleted file mode 100644 index be4c8f5..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import org.springframework.stereotype.Component; - -@Component("zipProjectArchiver") -public class ZipProjectArchiver implements ProjectArchiver { - - @Override - public Path archiveProject(File projectDestination, String projectName) throws IOException { - projectName = sanitizeFilename(projectName); - String archiveFilename = projectName + ".zip"; - File archiveFile = new File(projectDestination.getParent(), archiveFilename); - - try (FileOutputStream fos = new FileOutputStream(archiveFile); - ZipOutputStream zipOut = new ZipOutputStream(fos)) { - - zipOut.setLevel(ZipOutputStream.DEFLATED); - - Path projectPath = Paths.get(projectDestination.getAbsolutePath()); - - addFilesToZip(projectPath, zipOut); - } - - return archiveFile.toPath(); - } - - private void addFilesToZip(Path projectPath, ZipOutputStream zipOut) throws IOException { - String rootFileName = sanitizeRootFileName(projectPath.getFileName().toString()); - List processingErrors = new ArrayList<>(); - try (Stream walkStream = Files.walk(projectPath)) { - walkStream.forEach( - filePath -> { - String entryName = getEntryName(projectPath, filePath, rootFileName); - try { - addFileToZip(zipOut, filePath, entryName); - } catch (IOException e) { - processingErrors.add( - String.format("Error processing file: %s. Reason: %s", filePath, e.getMessage())); - } - }); - } - - if (!processingErrors.isEmpty()) { - throw new IOException( - String.format( - "Error encountered during archive creation for %s: %s", - projectPath.getFileName(), String.join(", ", processingErrors))); - } - } - - private String getEntryName(Path projectPath, Path filePath, String rootFileName) { - if (projectPath.equals(filePath)) { - return rootFileName; - } else { - String entryName = sanitizeEntryName(projectPath.relativize(filePath).toString()); - return "/" + rootFileName + "/" + entryName; - } - } - - private void addFileToZip(ZipOutputStream zipOut, Path filePath, String entryName) - throws IOException { - if (Files.isDirectory(filePath)) { - zipOut.putNextEntry(new ZipEntry(entryName + "/")); - } else { - zipOut.putNextEntry(new ZipEntry(entryName)); - try (FileInputStream fis = new FileInputStream(filePath.toFile())) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = fis.read(buffer)) != -1) { - zipOut.write(buffer, 0, bytesRead); - } - } - } - } - - private String sanitizeFilename(String filename) { - return filename.replaceAll("[/:*?<>|\\\\.^]", "").replaceAll("--+", "-"); - } - - private String sanitizeEntryName(String filename) { - return filename.replaceAll("[:*?<>|\\\\^]", "").replaceAll("-+", "_"); - } - - private String sanitizeRootFileName(String filename) { - return filename.replaceAll("[:*?<>|\\\\.^]", "").replaceAll("--+", "-"); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java deleted file mode 100644 index 0551f74..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("springBootApplicationYamlGenerator") -public class SpringBootApplicationYamlGenerator implements ApplicationYamlGenerator { - - private static final String SRC_MAIN_RESOURCES = "src/main/resources"; - private final TemplateEngine freeMarkerTemplateEngine; - - public SpringBootApplicationYamlGenerator(TemplateEngine freeMarkerTemplateEngine) { - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - } - - @Override - public void generateApplicationYaml(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - Map appPropertiesModel = new HashMap<>(); - appPropertiesModel.put("projectName", projectMetadata.getName()); - - File srcMainResourcesFile = new File(projectDestination, SRC_MAIN_RESOURCES); - - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.SPRING_BOOT_APPLICATION_YAML, appPropertiesModel, srcMainResourcesFile); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java deleted file mode 100644 index 4341ee0..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.FILE_NAME_EXTENSION; -import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.FILE_NAME_POSTFIX; -import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.TEMPLATE_NAME; - -import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.naming.NameConverter; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("springBootJavaMainClassGenerator") -public class SpringBootJavaMainClassGenerator implements FrameworkProjectStarterClassGenerator { - - private final TemplateEngine freeMarkerTemplateEngine; - private final MavenJavaSourceFolderProperties sourceFolder; - private final NameConverter nameConverter; - - public SpringBootJavaMainClassGenerator( - TemplateEngine freeMarkerTemplateEngine, - MavenJavaSourceFolderProperties sourceFolder, - NameConverter nameConverter) { - this.sourceFolder = sourceFolder; - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - this.nameConverter = nameConverter; - } - - @Override - public void generateProjectStarterClass(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - - Map mainClassModel = new HashMap<>(); - mainClassModel.put("projectPackageName", projectMetadata.getPackageName()); - - // e.g. "codegen-demo" -> "CodegenDemo" + "Application" - String classBase = nameConverter.toPascalCase(projectMetadata.getName()); - String className = classBase + FILE_NAME_POSTFIX; - mainClassModel.put("className", className); - - String basePackagePath = projectMetadata.getPackageName().replace(".", "/"); - File srcMainJavaFile = new File(projectDestination, sourceFolder.srcMainJava()); - - File mainClassFileDestination = new File(srcMainJavaFile, basePackagePath); - String fileName = className + FILE_NAME_EXTENSION; - - freeMarkerTemplateEngine.generateFileFromTemplate( - TEMPLATE_NAME, fileName, mainClassModel, mainClassFileDestination); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java deleted file mode 100644 index 8008858..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaTestClassGeneratorConstants.*; - -import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.naming.NameConverter; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("springBootJavaTestClassGenerator") -public class SpringBootJavaTestClassGenerator implements FrameworkSpecificTestUnitGenerator { - - private final TemplateEngine freeMarkerTemplateEngine; - private final MavenJavaSourceFolderProperties sourceFolder; - private final NameConverter nameConverter; - - public SpringBootJavaTestClassGenerator( - TemplateEngine freeMarkerTemplateEngine, - MavenJavaSourceFolderProperties sourceFolder, - NameConverter nameConverter) { - this.sourceFolder = sourceFolder; - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - this.nameConverter = nameConverter; - } - - @Override - public void generateTestClass(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - - Map testClassModel = new HashMap<>(); - testClassModel.put("projectPackageName", projectMetadata.getPackageName()); - - String classBase = nameConverter.toPascalCase(projectMetadata.getName()); - String className = classBase + FILE_NAME_POSTFIX; - testClassModel.put("className", className); - - String basePackagePath = projectMetadata.getPackageName().replace(".", "/"); - File srcTestJavaFile = new File(projectDestination, sourceFolder.srcTestJava()); - - File testClassFileDestination = new File(srcTestJavaFile, basePackagePath); - String fileName = className + FILE_NAME_EXTENSION; - - freeMarkerTemplateEngine.generateFileFromTemplate( - TEMPLATE_NAME, fileName, testClassModel, testClassFileDestination); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java deleted file mode 100644 index 817a7fd..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPlugin; -import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPom; -import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPom.MavenPomBuilder; -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildWrapperGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("springBootMavenJavaProjectBuildGenerator") -public class SpringBootMavenJavaProjectBuildGenerator implements ProjectBuildGenerator { - - private static final String MAVEN_MODEL_VERSION = "4.0.0"; - private static final String MAVEN_PROJECT_VERSION = "0.0.1-SNAPSHOT"; - - private final TemplateEngine freeMarkerTemplateEngine; - private final ProjectBuildWrapperGenerator mavenBuildWrapperGenerator; - private final List springBootMavenJavaPlugins; - - public SpringBootMavenJavaProjectBuildGenerator( - TemplateEngine freeMarkerTemplateEngine, - ProjectBuildWrapperGenerator mavenBuildWrapperGenerator, - List springBootMavenJavaPlugins) { - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - this.springBootMavenJavaPlugins = springBootMavenJavaPlugins; - this.mavenBuildWrapperGenerator = mavenBuildWrapperGenerator; - } - - @Override - public void generateBuildConfiguration(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - MavenPomBuilder mavenPomBuilder = new MavenPom.MavenPomBuilder(); - MavenPom mavenPom = - mavenPomBuilder - .modelVersion(MAVEN_MODEL_VERSION) - .version(MAVEN_PROJECT_VERSION) - .projectMetadata(projectMetadata) - .addDependencies(projectMetadata.getDependencies()) - .addPlugins(springBootMavenJavaPlugins) - .build(); - - Map pomModel = new HashMap<>(); - pomModel.put("pom", mavenPom); - - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.SPRING_BOOT_JAVA_POM, pomModel, projectDestination); - - mavenBuildWrapperGenerator.generateBuildWrapper(projectDestination, projectMetadata); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java deleted file mode 100644 index 6961c3f..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("springBootMavenJavaReadMeGenerator") -public class SpringBootMavenJavaReadMeGenerator implements ProjectDocumentationGenerator { - - private final TemplateEngine freeMarkerTemplateEngine; - - public SpringBootMavenJavaReadMeGenerator(TemplateEngine freeMarkerTemplateEngine) { - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - } - - @Override - public void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - Map readMeModel = new HashMap<>(); - readMeModel.put("projectName", projectMetadata.getName()); - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.SPRING_BOOT_MAVEN_JAVA_README, readMeModel, projectDestination); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java deleted file mode 100644 index 610d1d5..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants; - -public class SpringBootJavaMainClassGeneratorConstants { - - public static final String TEMPLATE_NAME = "springBootMainClass.java.ftl"; - public static final String FILE_NAME_POSTFIX = "Application"; - public static final String FILE_NAME_EXTENSION = ".java"; - - private SpringBootJavaMainClassGeneratorConstants() {} -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java deleted file mode 100644 index c703031..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants; - -public class SpringBootJavaTestClassGeneratorConstants { - - public static final String TEMPLATE_NAME = "springBootTestClass.java.ftl"; - public static final String FILE_NAME_POSTFIX = "ApplicationTests"; - public static final String FILE_NAME_EXTENSION = ".java"; - - private SpringBootJavaTestClassGeneratorConstants() {} -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java deleted file mode 100644 index 493c3d3..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildWrapperGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("mavenBuildWrapperGenerator") -public class MavenBuildWrapperGenerator implements ProjectBuildWrapperGenerator { - - private static final String MAVEN_VERSION = "3.9.11"; - private static final String WRAPPER_VERSION = "3.3.3"; - private static final String WRAPPER_FILE_DIR = ".mvn/wrapper"; - - private final TemplateEngine freeMarkerTemplateEngine; - - public MavenBuildWrapperGenerator(TemplateEngine freeMarkerTemplateEngine) { - this.freeMarkerTemplateEngine = freeMarkerTemplateEngine; - } - - @Override - public void generateBuildWrapper(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - Map wrapperModel = new HashMap<>(); - wrapperModel.put("wrapperVersion", WRAPPER_VERSION); - wrapperModel.put("mavenVersion", MAVEN_VERSION); - File wrapperFileDestination = new File(projectDestination, WRAPPER_FILE_DIR); - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.MAVEN_WRAPPER, wrapperModel, wrapperFileDestination); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java deleted file mode 100644 index 9098da3..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven; - -import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator; -import java.io.File; -import java.io.IOException; -import java.util.List; -import org.springframework.stereotype.Component; - -@Component("mavenJavaProjectLayoutGenerator") -public class MavenJavaProjectLayoutGenerator implements ProjectLayoutGenerator { - - private final MavenJavaSourceFolderProperties sourceFolder; - - public MavenJavaProjectLayoutGenerator(MavenJavaSourceFolderProperties sourceFolder) { - this.sourceFolder = sourceFolder; - } - - @Override - public void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - generateSourceFolders(projectDestination); - generatePackages(projectDestination, projectMetadata.getPackageName()); - } - - private void generateSourceFolders(File projectDestination) { - sourceFolder - .getSourceFolders() - .forEach( - s -> { - File sourceDir = new File(projectDestination, s); - if (!sourceDir.exists()) { - sourceDir.mkdirs(); - } - }); - } - - private void generatePackages(File projectDestination, String packageName) { - String packageNamePath = packageName.replace(".", "/"); - packageNamePath = sanitizePackageName(packageNamePath); - - File sourceFolderMainJavaFile = new File(projectDestination, sourceFolder.srcMainJava()); - File packageJavaFile = new File(sourceFolderMainJavaFile, packageNamePath); - - File sourceFolderTestJavaFile = new File(projectDestination, sourceFolder.srcTestJava()); - File packageTestFile = new File(sourceFolderTestJavaFile, packageNamePath); - - String packageGenPath = packageNamePath + "/codegen"; - File sourceFolderGenJavaFile = new File(projectDestination, sourceFolder.srcGenJava()); - File packageGenFile = new File(sourceFolderGenJavaFile, packageGenPath); - - List projectPackages = List.of(packageJavaFile, packageTestFile, packageGenFile); - - projectPackages.forEach( - p -> { - if (!p.exists()) { - p.mkdirs(); - } - }); - } - - private String sanitizePackageName(String packageName) { - return packageName.replaceAll("[\\:*?<>|\\\\\\^]", "").replaceAll("-+", "_"); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java deleted file mode 100644 index 55f2c1d..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating; - -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.util.Map; -import org.springframework.stereotype.Component; - -@Component("freeMarkerTemplateEngine") -public class FreeMarkerTemplateEngine implements TemplateEngine { - - private final Configuration freemarkerTemplateConfiguration; - - public FreeMarkerTemplateEngine(Configuration freemarkerTemplateConfiguration) { - this.freemarkerTemplateConfiguration = freemarkerTemplateConfiguration; - } - - @Override - public void generateFileFromTemplate( - TemplateType templateType, Map data, File destination) throws IOException { - - String templateFileName = templateType.getTemplateFileName(); - String fileName = templateType.getFileName(); - - generateFileFromTemplate(templateFileName, fileName, data, destination); - } - - public void generateFileFromTemplate( - String templateFileName, String fileName, Map data, File destination) - throws IOException { - Template template = freemarkerTemplateConfiguration.getTemplate(templateFileName); - - if (!destination.exists()) { - destination.mkdirs(); - } - - try (Writer writer = new FileWriter(new File(destination, fileName))) { - template.process(data, writer); - } catch (TemplateException e) { - throw new IOException("Error processing template: " + e.getMessage(), e); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java deleted file mode 100644 index fa79066..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.configuration; - -import freemarker.template.TemplateExceptionHandler; -import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.FreeMarkerTemplateProperties; -import java.io.Serial; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties(FreeMarkerTemplateProperties.class) -public class FreeMarkerTemplateConfiguration { - - private final FreeMarkerTemplateProperties freeMarkerProperties; - - public FreeMarkerTemplateConfiguration(FreeMarkerTemplateProperties freeMarkerProperties) { - this.freeMarkerProperties = freeMarkerProperties; - } - - @Bean - freemarker.template.Configuration freemarkerTemplateConfiguration() - throws FreeMarkerConfigurationException { - return initializeFreeMarkerTemplateConfiguration(); - } - - public freemarker.template.Configuration initializeFreeMarkerTemplateConfiguration() { - freemarker.template.Configuration configuration = - new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_32); - configuration.setDefaultEncoding(freeMarkerProperties.encoding()); - setTemplateExceptionHandler(configuration); - configuration.setClassForTemplateLoading(this.getClass(), freeMarkerProperties.templatePath()); - return configuration; - } - - private void setTemplateExceptionHandler(freemarker.template.Configuration configuration) { - try { - TemplateExceptionHandler exceptionHandler = - switch (freeMarkerProperties.templateExceptionHandler()) { - case "RETHROW_HANDLER" -> TemplateExceptionHandler.RETHROW_HANDLER; - case "DEBUG_HANDLER" -> TemplateExceptionHandler.DEBUG_HANDLER; - case "HTML_DEBUG_HANDLER" -> TemplateExceptionHandler.HTML_DEBUG_HANDLER; - case "IGNORE_HANDLER" -> TemplateExceptionHandler.IGNORE_HANDLER; - default -> - throw new IllegalArgumentException( - "Invalid exception handler name: " - + freeMarkerProperties.templateExceptionHandler()); - }; - configuration.setTemplateExceptionHandler(exceptionHandler); - } catch (IllegalArgumentException e) { - throw new FreeMarkerConfigurationException( - "Invalid templateExceptionHandler value: " - + freeMarkerProperties.templateExceptionHandler(), - e); - } - } -} - -class FreeMarkerConfigurationException extends RuntimeException { - - @Serial private static final long serialVersionUID = 4482627787641879716L; - - public FreeMarkerConfigurationException(String message, Exception e) { - super(message, e); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java deleted file mode 100644 index 6c20a3a..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.configuration; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPlugin; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class SpringBootMavenJavaProjectConfiguration { - - @Bean - List springBootMavenJavaPlugins() { - List springBootMavenPlugins = new ArrayList<>(); - - MavenPlugin springBootMavenPlugin = - new MavenPlugin.MavenPluginBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-maven-plugin") - .build(); - - MavenPlugin mavenCompilerPlugin = - new MavenPlugin.MavenPluginBuilder() - .groupId("org.apache.maven.plugins") - .artifactId("maven-compiler-plugin") - .addConfigurationElement("generatedSourcesDirectory", "src/gen/java") - .addConfigurationElement("compileSourceRoots", List.of("src/main/java", "src/gen/java")) - .build(); - - springBootMavenPlugins.add(springBootMavenPlugin); - springBootMavenPlugins.add(mavenCompilerPlugin); - - return Collections.unmodifiableList(springBootMavenPlugins); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java deleted file mode 100644 index 0e625d9..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "freemarker") -public record FreeMarkerTemplateProperties( - String encoding, String templateExceptionHandler, String templatePath) {} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java deleted file mode 100644 index cccaffd..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties; - -import java.util.List; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "maven.source-folder") -public record MavenJavaSourceFolderProperties( - String srcMainJava, - String srcMainResources, - String srcTestJava, - String srcTestResources, - String srcGenJava) { - - public List getSourceFolders() { - return List.of(srcMainJava, srcMainResources, srcTestJava, srcTestResources, srcGenJava); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java deleted file mode 100644 index c528b51..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.generator; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.IOException; -import java.nio.file.Path; - -public interface ProjectGenerator { - - /** - * Generates a project based on the provided project metadata. This method delegates the specific - * generation tasks to appropriate collaborators (ports) based on the project type. - * - * @param projectMetadata The project metadata object containing information about the desired - * project type. - * @throws IOException If an error occurs during project generation. - */ - Path generateProject(ProjectMetadata projectMetadata) throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java deleted file mode 100644 index 15e6fc3..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java +++ /dev/null @@ -1,118 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.generator.base; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; - -public abstract class AbstractProjectGenerator implements ProjectGenerator { - - private final ProjectDirectoryInitializer projectDirectoryInitializer; - private final GitIgnoreGenerator gitIgnoreGenerator; - private final ProjectArchiver projectArchiver; - private final ProjectLayoutGenerator projectLayoutGenerator; - private final ProjectBuildGenerator projectBuildGenerator; - private final ApplicationYamlGenerator applicationYamlGenerator; - private final FrameworkProjectStarterClassGenerator frameworkProjectStarterClassGenerator; - private final FrameworkSpecificTestUnitGenerator frameworkSpecificTestUnitGenerator; - private final ProjectDocumentationGenerator projectDocumentationGenerator; - - protected AbstractProjectGenerator( - ProjectDirectoryInitializer projectDirectoryInitializer, - GitIgnoreGenerator gitIgnoreGenerator, - ProjectArchiver projectArchiver, - ProjectLayoutGenerator projectLayoutGenerator, - ProjectBuildGenerator projectBuildGenerator, - ApplicationYamlGenerator applicationYamlGenerator, - FrameworkProjectStarterClassGenerator frameworkProjectStarterClassGenerator, - FrameworkSpecificTestUnitGenerator frameworkSpecificTestUnitGenerator, - ProjectDocumentationGenerator projectDocumentationGenerator) { - this.projectDirectoryInitializer = projectDirectoryInitializer; - this.gitIgnoreGenerator = gitIgnoreGenerator; - this.projectArchiver = projectArchiver; - this.projectLayoutGenerator = projectLayoutGenerator; - this.projectBuildGenerator = projectBuildGenerator; - this.applicationYamlGenerator = applicationYamlGenerator; - this.frameworkProjectStarterClassGenerator = frameworkProjectStarterClassGenerator; - this.frameworkSpecificTestUnitGenerator = frameworkSpecificTestUnitGenerator; - this.projectDocumentationGenerator = projectDocumentationGenerator; - } - - @Override - public final Path generateProject(ProjectMetadata projectMetadata) throws IOException { - Path projectDestinationPath = initializeProjectDirectory(projectMetadata); - File projectDestination = projectDestinationPath.toFile(); - - generateGitIgnoreContent(projectDestination); - generateProjectLayout(projectDestination, projectMetadata); - generateBuildConfiguration(projectDestination, projectMetadata); - generateApplicationProperties(projectDestination, projectMetadata); - generateProjectStarterClass(projectDestination, projectMetadata); - generateTestClass(projectDestination, projectMetadata); - generateProjectDocument(projectDestination, projectMetadata); - return archiveProject(projectDestination, projectMetadata); - } - - protected Path initializeProjectDirectory(ProjectMetadata projectMetadata) throws IOException { - if (projectMetadata.getProjectLocation() != null) { - return projectDirectoryInitializer.initializeProjectDirectory( - projectMetadata.getArtifactId(), projectMetadata.getProjectLocation()); - } else { - return projectDirectoryInitializer.initializeProjectDirectory( - projectMetadata.getArtifactId()); - } - } - - protected void generateGitIgnoreContent(File projectDestination) throws IOException { - List ignoreList = Collections.emptyList(); - gitIgnoreGenerator.generateGitIgnoreContent(projectDestination, ignoreList); - } - - protected void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - projectLayoutGenerator.generateProjectLayout(projectDestination, projectMetadata); - } - - protected void generateBuildConfiguration( - File projectDestination, ProjectMetadata projectMetadata) throws IOException { - projectBuildGenerator.generateBuildConfiguration(projectDestination, projectMetadata); - } - - protected void generateApplicationProperties( - File projectDestination, ProjectMetadata projectMetadata) throws IOException { - applicationYamlGenerator.generateApplicationYaml(projectDestination, projectMetadata); - } - - protected void generateProjectStarterClass( - File projectDestination, ProjectMetadata projectMetadata) throws IOException { - frameworkProjectStarterClassGenerator.generateProjectStarterClass( - projectDestination, projectMetadata); - } - - protected void generateTestClass(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - frameworkSpecificTestUnitGenerator.generateTestClass(projectDestination, projectMetadata); - } - - protected void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - projectDocumentationGenerator.generateProjectDocument(projectDestination, projectMetadata); - } - - protected Path archiveProject(File projectDestination, ProjectMetadata projectMetadata) - throws IOException { - return projectArchiver.archiveProject(projectDestination, projectMetadata.getName()); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java deleted file mode 100644 index 41a2022..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.generator.springboot.maven; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.base.AbstractProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator; -import org.springframework.stereotype.Component; - -@Component("springBootMavenJavaProjectGenerator") -public class SpringBootMavenJavaProjectGenerator extends AbstractProjectGenerator { - - public SpringBootMavenJavaProjectGenerator( - ProjectDirectoryInitializer projectRootDirectoryInitializer, - GitIgnoreGenerator gitIgnoreFileGenerator, - ProjectArchiver projectZipArchiver, - ProjectLayoutGenerator mavenJavaProjectLayoutGenerator, - ProjectBuildGenerator springBootMavenJavaProjectBuildGenerator, - ApplicationYamlGenerator springBootApplicationYamlGenerator, - FrameworkProjectStarterClassGenerator springBootJavaMainClassGenerator, - FrameworkSpecificTestUnitGenerator springBootJavaTestClassGenerator, - ProjectDocumentationGenerator springBootMavenJavaReadMeGenerator) { - super( - projectRootDirectoryInitializer, - gitIgnoreFileGenerator, - projectZipArchiver, - mavenJavaProjectLayoutGenerator, - springBootMavenJavaProjectBuildGenerator, - springBootApplicationYamlGenerator, - springBootJavaMainClassGenerator, - springBootJavaTestClassGenerator, - springBootMavenJavaReadMeGenerator); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java deleted file mode 100644 index 2327d8d..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model; - -public class Dependency { - private final String groupId; - private final String artifactId; - private final String version; - private final String scope; - - private Dependency(DependencyBuilder builder) { - this.groupId = builder.groupId; - this.artifactId = builder.artifactId; - this.version = builder.version; - this.scope = builder.scope; - } - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getVersion() { - return version; - } - - public String getScope() { - return scope; - } - - @Override - public String toString() { - return toShortDefinition(); - } - - public String toShortDefinition() { - return "Dependency [artifactId=" + artifactId + "]"; - } - - public String toLongDefinition() { - return "Dependency [groupId=" - + groupId - + ", artifactId=" - + artifactId - + ", version=" - + version - + ", scope=" - + scope - + "]"; - } - - public static class DependencyBuilder { - - private String groupId; - private String artifactId; - private String version; - private String scope; - - public DependencyBuilder groupId(String groupId) { - this.groupId = groupId; - return this; - } - - public DependencyBuilder artifactId(String artifactId) { - this.artifactId = artifactId; - return this; - } - - public DependencyBuilder version(String version) { - this.version = version; - return this; - } - - public DependencyBuilder scope(String scope) { - this.scope = scope; - return this; - } - - public Dependency build() { - if (groupId == null || artifactId == null) { - throw new IllegalStateException("groupId and artifactId are required fields"); - } - return new Dependency(this); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java deleted file mode 100644 index d09c2fa..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model; - -import java.nio.file.Path; -import java.util.List; - -public class ProjectMetadata { - - private final String name; - private final String description; - private final String groupId; - private final String artifactId; - private final String packageName; - private final List dependencies; - private final Path projectLocation; - - protected ProjectMetadata(ProjectMetadataBuilder builder) { - this.name = builder.name; - this.description = builder.description; - this.groupId = builder.groupId; - this.artifactId = builder.artifactId; - this.packageName = builder.packageName; - this.dependencies = builder.dependencies; - this.projectLocation = builder.projectLocation; - } - - public String getName() { - return name; - } - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getPackageName() { - return packageName; - } - - public String getDescription() { - return description; - } - - public List getDependencies() { - return dependencies; - } - - public Path getProjectLocation() { - return projectLocation; - } - - public static class ProjectMetadataBuilder { - - private String name; - private String description; - private String groupId; - private String artifactId; - private String packageName; - private List dependencies; - private Path projectLocation; - - public ProjectMetadataBuilder name(String name) { - this.name = name; - return this; - } - - public ProjectMetadataBuilder description(String description) { - this.description = description; - return this; - } - - public ProjectMetadataBuilder groupId(String groupId) { - this.groupId = groupId; - return this; - } - - public ProjectMetadataBuilder artifactId(String artifactId) { - this.artifactId = artifactId; - return this; - } - - public ProjectMetadataBuilder packageName(String packageName) { - this.packageName = packageName; - return this; - } - - public ProjectMetadataBuilder dependencies(List dependencies) { - this.dependencies = dependencies; - return this; - } - - public ProjectMetadataBuilder projectLocation(Path projectLocation) { - this.projectLocation = projectLocation; - return this; - } - - public ProjectMetadata build() { - return new ProjectMetadata(this); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java deleted file mode 100644 index b95ddfa..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; - -public record ProjectType(Framework framework, BuildTool buildTool, Language language) {} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java deleted file mode 100644 index 0299129..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.maven; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class MavenPlugin { - - private final String groupId; - private final String artifactId; - private final String version; - private final Map configuration; - - private MavenPlugin(MavenPluginBuilder builder) { - this.groupId = builder.groupId; - this.artifactId = builder.artifactId; - this.version = builder.version; - this.configuration = builder.configuration; - } - - public String getGroupId() { - return groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public String getVersion() { - return version; - } - - public Map getConfiguration() { - return configuration; - } - - public static class MavenPluginBuilder { - private final Map configuration = new LinkedHashMap<>(); - private String groupId; - private String artifactId; - private String version; - - public MavenPluginBuilder groupId(String groupId) { - this.groupId = groupId; - return this; - } - - public MavenPluginBuilder artifactId(String artifactId) { - this.artifactId = artifactId; - return this; - } - - public MavenPluginBuilder version(String version) { - this.version = version; - return this; - } - - public MavenPluginBuilder addConfigurationElement(String key, Object value) { - configuration.put(key, value); - return this; - } - - public MavenPlugin build() { - if (groupId == null || artifactId == null) { - throw new IllegalStateException("groupId, artifactId are required fields"); - } - return new MavenPlugin(this); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java deleted file mode 100644 index facc982..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.maven; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.util.ArrayList; -import java.util.List; - -public class MavenPom { - - private final String modelVersion; - private final String version; - private final ProjectMetadata projectMetadata; - private final List dependencies; - private final List plugins; - - private MavenPom(MavenPomBuilder builder) { - this.modelVersion = builder.modelVersion; - this.version = builder.version; - this.projectMetadata = builder.projectMetadata; - this.dependencies = builder.dependencies; - this.plugins = builder.plugins; - } - - public String getModelVersion() { - return modelVersion; - } - - public String getVersion() { - return version; - } - - public ProjectMetadata getProjectMetadata() { - return projectMetadata; - } - - public List getDependencies() { - return dependencies; - } - - public List getPlugins() { - return plugins; - } - - public static class MavenPomBuilder { - private String modelVersion; - private String version; - private ProjectMetadata projectMetadata; - private List dependencies = new ArrayList<>(); - private List plugins = new ArrayList<>(); - - public MavenPomBuilder modelVersion(String modelVersion) { - this.modelVersion = modelVersion; - return this; - } - - public MavenPomBuilder version(String version) { - this.version = version; - return this; - } - - public MavenPomBuilder projectMetadata(ProjectMetadata projectMetadata) { - this.projectMetadata = projectMetadata; - return this; - } - - public MavenPomBuilder addDependencies(List dependencies) { - this.dependencies = new ArrayList<>(dependencies); - return this; - } - - public MavenPomBuilder addPlugins(List plugins) { - this.plugins = new ArrayList<>(plugins); - return this; - } - - public MavenPom build() { - return new MavenPom(this); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java deleted file mode 100644 index 29f5cb4..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.spring; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; - -public class SpringBootJavaProjectMetadata extends ProjectMetadata { - - private String springBootVersion; - private String javaVersion; - - protected SpringBootJavaProjectMetadata( - ProjectMetadataBuilder builder, String springBootVersion, String javaVersion) { - super(builder); - this.springBootVersion = springBootVersion; - this.javaVersion = javaVersion; - } - - public String getSpringBootVersion() { - return springBootVersion; - } - - public void setSpringBootVersion(String springBootVersion) { - this.springBootVersion = springBootVersion; - } - - public String getJavaVersion() { - return javaVersion; - } - - public void setJavaVersion(String javaVersion) { - this.javaVersion = javaVersion; - } - - public static class SpringBootJavaProjectMetadataBuilder extends ProjectMetadataBuilder { - private String springBootVersion; - private String javaVersion; - - public SpringBootJavaProjectMetadataBuilder springBootVersion(String springBootVersion) { - this.springBootVersion = springBootVersion; - return this; - } - - public SpringBootJavaProjectMetadataBuilder javaVersion(String javaVersion) { - this.javaVersion = javaVersion; - return this; - } - - @Override - public SpringBootJavaProjectMetadata build() { - return new SpringBootJavaProjectMetadata(this, springBootVersion, javaVersion); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java deleted file mode 100644 index f0570a0..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack; - -public enum BuildTool { - MAVEN, - GRADLE -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java deleted file mode 100644 index def9f0b..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack; - -public enum Framework { - SPRING_BOOT, - QUARKUS -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java deleted file mode 100644 index bf619c3..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack; - -public enum Language { - JAVA, - KOTLIN -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java deleted file mode 100644 index 54b973d..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.model.templating; - -public enum TemplateType { - GITIGNORE(".gitignore", "gitignore.ftl"), - SPRING_BOOT_JAVA_POM("pom.xml", "springBootJavaPom.xml.ftl"), - SPRING_BOOT_MAVEN_JAVA_README("README.md", "springBootMavenJavaReadMe.ftl"), - MAVEN_WRAPPER("maven-wrapper.properties", "maven-wrapper.properties.ftl"), - SPRING_BOOT_APPLICATION_YAML("application.yml", "springBootApplication.yml.ftl"); - - private final String fileName; - private final String templateFileName; - - TemplateType(String fileName, String templateFileName) { - this.fileName = fileName; - this.templateFileName = templateFileName; - } - - public String getFileName() { - return fileName; - } - - public String getTemplateFileName() { - return templateFileName; - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java deleted file mode 100644 index d70e1a2..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.naming; - -import org.springframework.stereotype.Component; - -@Component -public class NameConverter { - - /** - * Converts a raw project name into Java-friendly PascalCase. Examples: "codegen-demo" -> - * "CodegenDemo" "my_service.core" -> "MyServiceCore" "123-metric" -> "App123Metric" - */ - public String toPascalCase(String raw) { - if (raw == null || raw.isBlank()) return ""; - String[] parts = raw.split("[^A-Za-z0-9]+"); - StringBuilder sb = new StringBuilder(); - for (String p : parts) { - if (p.isBlank()) continue; - String lower = p.toLowerCase(); - sb.append(Character.toUpperCase(lower.charAt(0))); - if (lower.length() > 1) { - sb.append(lower.substring(1)); - } - } - String result = sb.toString(); - if (!result.isEmpty() && Character.isDigit(result.charAt(0))) { - result = "App" + result; - } - return result; - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java deleted file mode 100644 index 5a0b9e9..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface ApplicationYamlGenerator { - - void generateApplicationYaml(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java deleted file mode 100644 index 7494230..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface FrameworkProjectStarterClassGenerator { - - void generateProjectStarterClass(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java deleted file mode 100644 index d1fe0e2..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface FrameworkSpecificTestUnitGenerator { - - void generateTestClass(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java deleted file mode 100644 index 44a5906..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -public interface GitIgnoreGenerator { - - void generateGitIgnoreContent(File projectDestination, List ignoreList) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java deleted file mode 100644 index 92c679d..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; - -public interface ProjectArchiver { - - Path archiveProject(File projectDestination, String projectName) throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java deleted file mode 100644 index c9c5ada..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface ProjectBuildGenerator { - - void generateBuildConfiguration(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java deleted file mode 100644 index 3e470c1..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface ProjectBuildWrapperGenerator { - - void generateBuildWrapper(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java deleted file mode 100644 index bf3df58..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import java.io.IOException; -import java.nio.file.Path; - -public interface ProjectDirectoryInitializer { - - Path initializeProjectDirectory(String projectName) throws IOException; - - Path initializeProjectDirectory(String projectName, Path projectLocation) throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java deleted file mode 100644 index 3de2d4e..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface ProjectDocumentationGenerator { - - void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java deleted file mode 100644 index 5262192..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; - -public interface ProjectLayoutGenerator { - - void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java deleted file mode 100644 index 29b7b8b..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.ports; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import java.io.File; -import java.io.IOException; -import java.util.Map; - -public interface TemplateEngine { - - void generateFileFromTemplate( - TemplateType templateType, Map data, File destination) throws IOException; - - void generateFileFromTemplate( - String templateFileName, String fileName, Map data, File destination) - throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java deleted file mode 100644 index 8f57415..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.registry; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import java.util.Optional; - -public interface ProjectGeneratorRegistry { - - Optional getProjectGenerator(ProjectType projectType); -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java deleted file mode 100644 index 3e8527f..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.registry; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import java.util.Map; -import java.util.Optional; -import org.springframework.stereotype.Component; - -@Component -public class SimpleProjectGeneratorRegistry implements ProjectGeneratorRegistry { - - private final Map registeredProjectGenerators; - - public SimpleProjectGeneratorRegistry( - Map registeredProjectGenerators) { - this.registeredProjectGenerators = registeredProjectGenerators; - } - - @Override - public Optional getProjectGenerator(ProjectType projectType) { - return Optional.ofNullable(registeredProjectGenerators.get(projectType)); - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java deleted file mode 100644 index aeea919..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.registry.configuration; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; -import java.util.HashMap; -import java.util.Map; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ProjectGeneratorRegistryConfiguration { - - private final ProjectGenerator springBootMavenJavaProjectGenerator; - - public ProjectGeneratorRegistryConfiguration( - ProjectGenerator springBootMavenJavaProjectGenerator) { - this.springBootMavenJavaProjectGenerator = springBootMavenJavaProjectGenerator; - } - - @Bean - Map registeredProjectGenerators() { - Map generatorFactories = new HashMap<>(); - - ProjectType springBootMavenJavaProjectType = - new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); - - generatorFactories.put(springBootMavenJavaProjectType, springBootMavenJavaProjectGenerator); - - return generatorFactories; - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java deleted file mode 100644 index eee7d9a..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.service; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import java.io.IOException; -import java.nio.file.Path; - -public interface ProjectGenerationService { - - Path generateProject(ProjectType projectType, ProjectMetadata projectMetadata) throws IOException; -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java deleted file mode 100644 index d9256ed..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.service; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.registry.ProjectGeneratorRegistry; -import io.github.bsayli.codegen.initializr.projectgeneration.service.exception.ProjectGenerationException; -import java.io.IOException; -import java.nio.file.Path; -import org.springframework.stereotype.Service; - -@Service -public class ProjectGenerationServiceImpl implements ProjectGenerationService { - - private final ProjectGeneratorRegistry registry; - - public ProjectGenerationServiceImpl(ProjectGeneratorRegistry registry) { - this.registry = registry; - } - - @Override - public Path generateProject(ProjectType projectType, ProjectMetadata projectMetadata) { - ProjectGenerator projectGenerator = - registry - .getProjectGenerator(projectType) - .orElseThrow( - () -> new IllegalArgumentException("Unsupported project type: " + projectType)); - try { - return projectGenerator.generateProject(projectMetadata); - } catch (IOException e) { - throw new ProjectGenerationException("Error generating project: " + e.getMessage(), e); - } - } -} diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java deleted file mode 100644 index 4808e8a..0000000 --- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.service.exception; - -public class ProjectGenerationException extends RuntimeException { - - public ProjectGenerationException(String message) { - super(message); - } - - public ProjectGenerationException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0171193..80934eb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,16 +1,60 @@ spring: application: - name: codegen-springboot-initializr + name: codegen-blueprint -freemarker: +codegen: + profiles: + springboot-maven-java: + template-base-path: springboot/maven/java/ + + ordered-artifact-keys: + - build-config + - build-tool-metadata + - ignore-rules + - app-config + - main-source-entrypoint + - test-source-entrypoint + - project-documentation + + artifacts: + build-config: + templates: + - template: pom.xml.ftl + output-path: pom.xml + + build-tool-metadata: + templates: + - template: maven-wrapper.properties.ftl + output-path: .mvn/wrapper/maven-wrapper.properties + + ignore-rules: + templates: + - template: gitignore.ftl + output-path: .gitignore + + app-config: + templates: + - template: application.yml.ftl + output-path: src/main/resources/application.yml + + main-source-entrypoint: + templates: + - template: MainClass.java.ftl + output-path: src/main/java + + test-source-entrypoint: + templates: + - template: MainClassTests.java.ftl + output-path: src/test/java + + project-documentation: + templates: + - template: README.md.ftl + output-path: README.md + +templating: encoding: UTF-8 - template-exception-handler: RETHROW_HANDLER + handler: RETHROW template-path: /templates - -maven: - source-folder: - src-main-java: src/main/java - src-main-resources: src/main/resources - src-test-java: src/test/java - src-test-resources: src/test/resources - src-gen-java: src/gen/java \ No newline at end of file + cache-enabled: true + cache-update-delay-ms: 60000 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..7c83765 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,73 @@ +# ================================ +# === DOMAIN LAYER MESSAGES === +# ================================ +# --- GROUP ID --- +project.group-id.not.blank=GroupId is required +project.group-id.length=GroupId must be between {0} and {1} characters +project.group-id.segment.format=GroupId must be dot-separated segments like 'com.example' (segment: [a-z][a-z0-9]*) +# --- PROJECT DESCRIPTION --- +project.description.not.blank=Description must not be blank +project.description.length=Description must be between {0} and {1} characters +project.description.control.chars=Description contains invalid control characters +# --- PROJECT NAME --- +project.name.not.blank=Project name is required +project.name.length=Project name must be between {0} and {1} characters +project.name.invalid.chars=Project name contains invalid characters +# --- ARTIFACT ID --- +project.artifact-id.not.blank=ArtifactId is required +project.artifact-id.length=ArtifactId must be between {0} and {1} characters +project.artifact-id.invalid.chars=Only lowercase letters, digits and '-' are allowed +project.artifact-id.starts.with.letter=ArtifactId must start with a letter +project.artifact-id.edge.char=ArtifactId must not start or end with '-' +# --- PACKAGE NAME --- +project.package-name.not.blank=Package name is required +project.package-name.length=Package name must be between {0} and {1} characters +project.package-name.segment.format=Package name must be dot-separated segments like 'com.example' (segment: [a-z][a-z0-9]*) +project.package-name.reserved.prefix=Reserved package prefixes are not allowed (java, javax, sun, com.sun) +# --- PROJECT IDENTITY --- +project.identity.not.blank=Project identity requires both GroupId and ArtifactId +# --- TECHNOLOGY STACK --- +project.tech-stack.not.blank=Technology stack requires Framework, Build Tool, and Language +project.tech-stack.framework.unknown=Unknown framework: {0}. Supported values: spring-boot +project.tech-stack.build-tool.unknown=Unknown build tool: {0}. Supported values: maven +project.tech-stack.language.unknown=Unknown language: {0}. Supported values: java +# --- PROJECT LAYOUT --- +project.layout.unknown=Unknown project layout: {0}. Supported layouts: standard, hexagonal +# --- PLATFORM TARGET --- +platform.target.not.blank=Platform target requires both Java version and Spring Boot version +platform.target.missing=Platform target and technology stack must be provided +platform.target.unsupported.options=Unsupported options: framework={0}, language={1}, buildTool={2} +platform.target.incompatible=Selected platform is incompatible (springBoot={0}, java={1}) +platform.java-version.unknown=Unknown Java version ''{0}''. +platform.springboot-version.unknown=Unknown Spring Boot version ''{0}''. +# --- DEPENDENCY --- +dependency.version.not.blank=Dependency version is required +dependency.version.invalid.chars=Dependency version contains invalid characters +dependency.coordinates.not.blank=Dependency coordinates require both GroupId and ArtifactId +dependency.list.not.blank=Dependency list is required +dependency.item.not.blank=Dependency entry must not be null +dependency.duplicate.coordinates=Duplicate dependency coordinates: {0} +# ================================ +# === APPLICATION LAYER MESSAGES === +# ================================ +application.artifact.key.unknown=Unknown artifact key ''{0}''. +# ================================ +# === ADAPTER / BOOTSTRAP MESSAGES === +# ================================ +# --- TEMPLATE / PROFILE / ARTIFACT --- +adapter.template.render.failed=Failed to render template ''{0}''. +bootstrap.profile.not.found=Unknown profile: {0} +bootstrap.artifact.not.found=Unknown artifact ''{0}'' for profile: {1} +bootstrap.template.base.missing=template-base-path must be set for profile: {0} +adapter.artifacts.port.not.found=No artifact generator adapter registered for profile ''{0}''. +adapter.profile.unsupported=Unsupported profile combination: framework={0}, buildTool={1}, language={2} +adapter.generator.key.mismatch=Generator artifact key mismatch (expected ''{0}'', actual ''{1}''). +# --- CLI / INPUT VALIDATION --- +adapter.dependency.alias.unknown=Unknown dependency alias ''{0}''. Use --help to see supported values. +# --- PROJECT WRITE / ROOT / ARCHIVE --- +adapter.project.write.failed=Failed to write generated file ''{0}''. +adapter.project-root.not-directory=Project root ''{0}'' exists but is not a directory. +adapter.project-root.already-exists=Project root ''{0}'' already exists. +adapter.project-root.io.failed=Failed to prepare project root at ''{0}''. +adapter.project.archive.invalid.root=Invalid project root ''{0}''. +adapter.project.archive.io=I/O error while archiving project ''{0}''. \ No newline at end of file diff --git a/src/main/resources/templates/springBootApplication.yml.ftl b/src/main/resources/templates/springBootApplication.yml.ftl deleted file mode 100644 index 3525bb9..0000000 --- a/src/main/resources/templates/springBootApplication.yml.ftl +++ /dev/null @@ -1,8 +0,0 @@ -spring: -application: -name: ${projectName} -# server: -# port: 8080 -# logging: -# level: -# root: INFO \ No newline at end of file diff --git a/src/main/resources/templates/springBootJavaPom.xml.ftl b/src/main/resources/templates/springBootJavaPom.xml.ftl deleted file mode 100644 index 4eef2c8..0000000 --- a/src/main/resources/templates/springBootJavaPom.xml.ftl +++ /dev/null @@ -1,67 +0,0 @@ - - - - ${pom.modelVersion} - - - org.springframework.boot - spring-boot-starter-parent - ${pom.projectMetadata.springBootVersion} - - - - ${pom.projectMetadata.groupId} - ${pom.projectMetadata.artifactId} - ${pom.version} - - - ${pom.projectMetadata.javaVersion} - - - - <#list pom.dependencies as dependency> - - ${dependency.groupId} - ${dependency.artifactId} - <#if dependency.version??> - ${dependency.version} - - <#if dependency.scope??> - ${dependency.scope} - - - - - - - - <#list pom.plugins as plugin> - - ${plugin.groupId} - ${plugin.artifactId} - <#if plugin.version??> - ${plugin.version} - - <#if plugin.configuration?has_content && plugin.configuration?size gt 0> - - <#list plugin.configuration! {} as key, value> - <#if key != 'compileSourceRoots'> - <${key}>${value} - - - <#if plugin.configuration.compileSourceRoots?has_content && plugin.configuration.compileSourceRoots?size gt 0> - - <#list plugin.configuration.compileSourceRoots! {} as sourceDirectory> - ${sourceDirectory} - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/templates/springBootMavenJavaReadMe.ftl b/src/main/resources/templates/springBootMavenJavaReadMe.ftl deleted file mode 100644 index ff10dfb..0000000 --- a/src/main/resources/templates/springBootMavenJavaReadMe.ftl +++ /dev/null @@ -1,53 +0,0 @@ -Project Initialization - -Extract the downloaded archive: Use a tool like WinZip, 7-Zip, or the unzip command to extract the downloaded archive file (e.g., ${projectName}.zip). - -Navigate to the project directory: Open your terminal or command prompt and navigate to the extracted project directory using the cd command (e.g., cd ${projectName}). - -Running the project: - -Option 1: With Maven (Recommended): - -If you already have Maven installed, you can directly use standard Maven commands like mvn package or mvn test to build and potentially run the project (refer to the project documentation for specific commands on how to run the application). - -Option 2: Without Maven: - -Pre-built Version (if available): The project might offer pre-built versions that include the mvnw and mvnw.cmd scripts for running the project without requiring Maven installation. Check the project website or documentation for download instructions for a pre-built version (if available). - -Build Scripts and Run the Project (if source code available): - -1. Download a minimal Apache Maven version from the official website: https://maven.apache.org/download.cgi -2. Extract the downloaded Maven archive into a temporary directory. -3. Open a terminal window (command prompt on Windows). -4. Navigate to the extracted project's root directory using the cd command. -5. (Optional) Run mvn package to see the build process and downloaded dependencies. -6. Generate the wrapper scripts using a specific Maven command (check project documentation for the exact command, a common example might be: mvn wrapper:wrapper). -7. After running the script generation command, check the project's root directory for the newly generated scripts: mvnw (Linux/macOS) and mvnw.cmd (Windows). -8. Run the Project: -- Linux/macOS: With the mvnw script generated, execute the following command in the terminal (assuming the script is executable): ./mvnw - - - Windows: With the mvnw.cmd script generated, double-click the script or run it from the command prompt: mvnw.cmd - - - Replace - with the desired action you want to perform. Here are some common examples: - - ./mvnw package: Builds the project and creates a package (JAR file). - ./mvnw test: Runs unit tests for the project. - - Dependencies - - This project uses the following dependencies based on your selections during generation: - - (List generated dependencies here. You can access this information from the project's pom.xml file) - Project Structure - - pom.xml: This file defines the project's metadata (groupId, artifactId, version) and dependencies. - wrapper/: This directory stores the configuration for the Maven Wrapper. - wrapper.conf: Configuration file for the wrapper executable. - src/main/java/: Source code directory for your application's Java classes. - src/main/resources/: Configuration files and resources used by your application. - src/test/java/: Source code directory for your application's unit tests (optional). - src/gen/java/: Generated code directory for the Codegen's Java classes. - - Additional Notes \ No newline at end of file diff --git a/src/main/resources/templates/springBootMainClass.java.ftl b/src/main/resources/templates/springboot/maven/java/MainClass.java.ftl similarity index 66% rename from src/main/resources/templates/springBootMainClass.java.ftl rename to src/main/resources/templates/springboot/maven/java/MainClass.java.ftl index 4b7565b..3016fe2 100644 --- a/src/main/resources/templates/springBootMainClass.java.ftl +++ b/src/main/resources/templates/springboot/maven/java/MainClass.java.ftl @@ -6,8 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ${className} { -public static void main(String[] args) { -SpringApplication.run(${className}.class, args); -} - -} + public static void main(String[] args) { + SpringApplication.run(${className}.class, args); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springBootTestClass.java.ftl b/src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl similarity index 80% rename from src/main/resources/templates/springBootTestClass.java.ftl rename to src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl index dcc1b84..e6362a9 100644 --- a/src/main/resources/templates/springBootTestClass.java.ftl +++ b/src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl @@ -6,9 +6,9 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ${className} { -@Test -void contextLoads() { -} + @Test + void contextLoads() { + } } diff --git a/src/main/resources/templates/springboot/maven/java/README.md.ftl b/src/main/resources/templates/springboot/maven/java/README.md.ftl new file mode 100644 index 0000000..6fee796 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/README.md.ftl @@ -0,0 +1,93 @@ +<#-- README Generator Template --> + +# ${projectName} + +${projectDescription} + +--- + +## 🔧 Tech Stack + +| Category | Value | +|---------|------| +| **Framework** | ${framework} | +| **Language** | ${language} | +| **Build Tool** | ${buildTool?upper_case} | +| **Java** | ${javaVersion} | +| **Spring Boot** | ${springBootVersion} | + +--- + +## 📦 Coordinates + +| Key | Value | +|-----|------| +| `groupId` | `${groupId}` | +| `artifactId` | `${artifactId}` | +| `package` | `${packageName}` | + +--- + +## 🚀 Quick Start + +```bash +./mvnw clean package # Build (using wrapper) +./mvnw spring-boot:run # Run the application +``` + +> If Maven is installed globally, you may also use `mvn` instead of `./mvnw`. + +--- + +## 📁 Project Layout + +``` +src +├─ main +│ ├─ java/${packageName?replace('.', '/')} +│ └─ resources/ +└─ test +└─ java/${packageName?replace('.', '/')} +``` + +<#-- Optional hexagonal showcase --> +<#if hasHexSample?? && hasHexSample> + + --- + + ## 🧱 Hexagonal Architecture Example + + This project includes an example **domain → application → adapter** structure: + * Domain models and rules + * Application use cases + * CLI / Web / Persistence adapters + + Modular & clean expansion ready! ✨ + + +--- + +## 📚 Selected Dependencies + +<#if dependencies?has_content> + | Dependency | Scope | + |-----------|-------| + <#list dependencies as d> + | `${d.groupId}:${d.artifactId}`<#if d.version?? && d.version?has_content>:`${d.version}` | <#if d.scope?? && d.scope?has_content>${d.scope}<#else>default | + +<#else> + > No additional dependencies were selected. + + +--- + +## 🧩 Next Steps + +✔ Structure your domain and use case logic +✔ Add CI/CD pipelines or Docker support +✔ Configure profiles in `application.yml` +✔ Add more Spring Boot starters if needed + +--- + +🏗 Generated by **Blueprint Platform — Codegen Blueprint CLI** \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/application.yml.ftl b/src/main/resources/templates/springboot/maven/java/application.yml.ftl new file mode 100644 index 0000000..6a1b7b6 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/application.yml.ftl @@ -0,0 +1,8 @@ +spring: + application: + name: ${projectName} + # server: + # port: 8080 + # logging: + # level: + # root: INFO \ No newline at end of file diff --git a/src/main/resources/templates/gitignore.ftl b/src/main/resources/templates/springboot/maven/java/gitignore.ftl similarity index 100% rename from src/main/resources/templates/gitignore.ftl rename to src/main/resources/templates/springboot/maven/java/gitignore.ftl diff --git a/src/main/resources/templates/maven-wrapper.properties.ftl b/src/main/resources/templates/springboot/maven/java/maven-wrapper.properties.ftl similarity index 100% rename from src/main/resources/templates/maven-wrapper.properties.ftl rename to src/main/resources/templates/springboot/maven/java/maven-wrapper.properties.ftl diff --git a/src/main/resources/templates/springboot/maven/java/pom.xml.ftl b/src/main/resources/templates/springboot/maven/java/pom.xml.ftl new file mode 100644 index 0000000..50ad8eb --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/pom.xml.ftl @@ -0,0 +1,50 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + ${springBootVersion} + + + + ${groupId} + ${artifactId} + 0.0.1-SNAPSHOT + + ${projectName} + ${projectDescription} + + + ${javaVersion} + + + + <#list dependencies as d> + + ${d.groupId} + ${d.artifactId} + <#if d.version?? && d.version?has_content> + ${d.version} + + <#if d.scope?? && d.scope?has_content> + ${d.scope} + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java b/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java new file mode 100644 index 0000000..80c50a0 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java @@ -0,0 +1,18 @@ +package io.github.blueprintplatform.codegen; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@Tag("integration") +@DisplayName("Integration Test: Spring Context Bootstrapping") +class CodegenBlueprintApplicationIT { + + @Test + @DisplayName("Spring context should load successfully") + void contextLoads() { + // If context fails to load, the test will fail automatically. + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java new file mode 100644 index 0000000..e7b6d45 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java @@ -0,0 +1,112 @@ +package io.github.blueprintplatform.codegen.adapter.in.cli.springboot; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest; +import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectCommand; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectResult; +import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class SpringBootGenerateCommandTest { + + @Test + @DisplayName("call() should build profile, map layout & dependencies and invoke use case") + void call_shouldBuildProfileAndInvokeUseCase() { + var mapper = new RecordingMapper(); + var useCase = new StubCreateProjectUseCase(); + + var cmd = new SpringBootGenerateCommand(mapper, useCase); + + cmd.groupId = "com.acme"; + cmd.artifactId = "demo-app"; + cmd.name = "Demo App"; + cmd.description = "Demo application for Acme"; + cmd.packageName = "com.acme.demo"; + cmd.buildTool = BuildTool.MAVEN; + cmd.language = Language.JAVA; + cmd.javaVersion = JavaVersion.JAVA_21; + cmd.bootVersion = SpringBootVersion.V3_5; + + cmd.layout = ProjectLayout.STANDARD; + cmd.dependencies = List.of(SpringBootDependencyAlias.WEB); + Path expected = Path.of("."); + cmd.targetDirectory = expected; + + Integer exitCode = cmd.call(); + + assertThat(exitCode).isZero(); + + assertThat(mapper.lastRequest).isNotNull(); + assertThat(mapper.lastBuildTool).isEqualTo(BuildTool.MAVEN); + assertThat(mapper.lastLanguage).isEqualTo(Language.JAVA); + assertThat(mapper.lastJavaVersion).isEqualTo(JavaVersion.JAVA_21); + assertThat(mapper.lastBootVersion).isEqualTo(SpringBootVersion.V3_5); + + assertThat(mapper.lastRequest.groupId()).isEqualTo("com.acme"); + assertThat(mapper.lastRequest.artifactId()).isEqualTo("demo-app"); + assertThat(mapper.lastRequest.name()).isEqualTo("Demo App"); + assertThat(mapper.lastRequest.description()).isEqualTo("Demo application for Acme"); + assertThat(mapper.lastRequest.packageName()).isEqualTo("com.acme.demo"); + assertThat(mapper.lastRequest.targetDirectory()).isEqualTo(expected); + + assertThat(mapper.lastRequest.profile()).isEqualTo("springboot-maven-java"); + + assertThat(mapper.lastRequest.layoutKey()).isEqualTo(ProjectLayout.STANDARD.key()); + + assertThat(mapper.lastRequest.dependencies()) + .containsExactly(SpringBootDependencyAlias.WEB.name()); + + assertThat(useCase.lastCommand).isSameAs(mapper.returnedCommand); + } + + static class RecordingMapper extends CreateProjectCommandMapper { + + final CreateProjectCommand returnedCommand = null; + CliProjectRequest lastRequest; + BuildTool lastBuildTool; + Language lastLanguage; + JavaVersion lastJavaVersion; + SpringBootVersion lastBootVersion; + + @Override + public CreateProjectCommand from( + CliProjectRequest request, + BuildTool buildTool, + Language language, + JavaVersion javaVersion, + SpringBootVersion bootVersion) { + + this.lastRequest = request; + this.lastBuildTool = buildTool; + this.lastLanguage = language; + this.lastJavaVersion = javaVersion; + this.lastBootVersion = bootVersion; + + return returnedCommand; + } + } + + static class StubCreateProjectUseCase implements CreateProjectUseCase { + + CreateProjectCommand lastCommand; + + @Override + public CreateProjectResult handle(CreateProjectCommand command) { + this.lastCommand = command; + return new CreateProjectResult(Path.of("demo-app.zip")); + } + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java new file mode 100644 index 0000000..fa1f4c4 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java @@ -0,0 +1,87 @@ +package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class PomDependencyMapperTest { + + private final PomDependencyMapper mapper = new PomDependencyMapper(); + + private static Dependency dep( + String groupId, String artifactId, String version, DependencyScope scope) { + DependencyVersion v = (version == null) ? null : new DependencyVersion(version); + return new Dependency( + new DependencyCoordinates(new GroupId(groupId), new ArtifactId(artifactId)), v, scope); + } + + @Test + @DisplayName("from(empty Dependencies) should return empty list") + void from_emptyDependencies_shouldReturnEmptyList() { + Dependencies deps = Dependencies.of(List.of()); + + List result = mapper.from(deps); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("from(Dependencies) should map coordinates, optional version and scope correctly") + void from_dependencies_shouldMapFieldsCorrectly() { + Dependency d1 = dep("org.acme", "alpha", null, null); + Dependency d2 = dep("org.acme", "beta", "1.2.3", DependencyScope.RUNTIME); + Dependency d3 = dep("org.acme", "gamma", "2.0.0-RC1", DependencyScope.TEST); + + Dependencies deps = Dependencies.of(List.of(d1, d2, d3)); + + List result = mapper.from(deps); + + assertThat(result).hasSize(3); + + Map byArtifactId = + result.stream().collect(Collectors.toMap(PomDependency::artifactId, p -> p)); + + PomDependency alpha = byArtifactId.get("alpha"); + assertThat(alpha.groupId()).isEqualTo("org.acme"); + assertThat(alpha.version()).isNull(); + assertThat(alpha.scope()).isNull(); + + PomDependency beta = byArtifactId.get("beta"); + assertThat(beta.groupId()).isEqualTo("org.acme"); + assertThat(beta.version()).isEqualTo("1.2.3"); + assertThat(beta.scope()).isEqualTo("runtime"); + + PomDependency gamma = byArtifactId.get("gamma"); + assertThat(gamma.groupId()).isEqualTo("org.acme"); + assertThat(gamma.version()).isEqualTo("2.0.0-RC1"); + assertThat(gamma.scope()).isEqualTo("test"); + } + + @Test + @DisplayName("from(Dependency) should map single dependency to PomDependency") + void from_singleDependency_shouldMapToPomDependency() { + Dependency domainDep = + dep("com.example", "demo-lib", "0.9.0-SNAPSHOT", DependencyScope.PROVIDED); + + PomDependency pomDep = mapper.from(domainDep); + + assertThat(pomDep.groupId()).isEqualTo("com.example"); + assertThat(pomDep.artifactId()).isEqualTo("demo-lib"); + assertThat(pomDep.version()).isEqualTo("0.9.0-SNAPSHOT"); + assertThat(pomDep.scope()).isEqualTo("provided"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java new file mode 100644 index 0000000..52129cd --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java @@ -0,0 +1,107 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveInvalidRootException; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +@Tag("unit") +@Tag("adapter") +class FileSystemProjectArchiverAdapterTest { + + private final ProjectArchiverPort archiver = new FileSystemProjectArchiverAdapter(); + + private static List zipEntries(ZipFile zipFile) { + Enumeration entries = zipFile.entries(); + List names = new java.util.ArrayList<>(); + while (entries.hasMoreElements()) { + names.add(entries.nextElement().getName()); + } + return names; + } + + @Test + @DisplayName("archive() should create zip with artifactId as root directory and include files") + void archive_shouldCreateZipWithArtifactIdRoot(@TempDir Path tempDir) throws IOException { + Path projectRoot = Files.createDirectory(tempDir.resolve("demo-app")); + + Path mainJava = projectRoot.resolve("src/main/java"); + Files.createDirectories(mainJava); + Files.writeString(mainJava.resolve("App.java"), "class App {}", StandardCharsets.UTF_8); + + Path testJava = projectRoot.resolve("src/test/java"); + Files.createDirectories(testJava); + Files.writeString(testJava.resolve("AppTest.java"), "class AppTest {}", StandardCharsets.UTF_8); + + Path archivePath = archiver.archive(projectRoot, "my-artifact"); + + assertThat(archivePath).isEqualTo(tempDir.resolve("my-artifact.zip")); + assertThat(Files.exists(archivePath)).isTrue(); + + try (ZipFile zipFile = new ZipFile(archivePath.toFile())) { + List entryNames = zipEntries(zipFile); + assertThat(entryNames) + .contains("my-artifact/") + .anySatisfy(name -> assertThat(name).startsWith("my-artifact/src/")) + .contains("my-artifact/src/main/java/App.java") + .contains("my-artifact/src/test/java/AppTest.java"); + } + } + + @Test + @DisplayName("archive() should fall back to directory name when artifactId is null") + void archive_shouldUseDirectoryNameWhenArtifactIdNull(@TempDir Path tempDir) throws IOException { + Path projectRoot = Files.createDirectory(tempDir.resolve("demo-app")); + + Files.writeString(projectRoot.resolve("README.md"), "# Demo", StandardCharsets.UTF_8); + + Path archivePath = archiver.archive(projectRoot, null); + + assertThat(archivePath).isEqualTo(tempDir.resolve("demo-app.zip")); + assertThat(Files.exists(archivePath)).isTrue(); + + try (ZipFile zipFile = new ZipFile(archivePath.toFile())) { + List entryNames = zipEntries(zipFile); + assertThat(entryNames).contains("demo-app/").contains("demo-app/README.md"); + } + } + + @Test + @DisplayName("archive() should throw ProjectArchiveInvalidRootException when root is null") + void archive_shouldThrowWhenRootIsNull() { + assertThatThrownBy(() -> archiver.archive(null, "anything")) + .isInstanceOf(ProjectArchiveInvalidRootException.class); + } + + @Test + @DisplayName("archive() should throw ProjectArchiveInvalidRootException when root has no parent") + void archive_shouldThrowWhenRootHasNoParent(@TempDir Path tempDir) { + Path rootWithoutParent = tempDir.getRoot(); + + assertThatThrownBy(() -> archiver.archive(rootWithoutParent, "artifact")) + .isInstanceOf(ProjectArchiveInvalidRootException.class); + } + + @Test + @DisplayName( + "archive() should throw ProjectArchiveInvalidRootException when path is not a directory") + void archive_shouldThrowWhenNotDirectory(@TempDir Path tempDir) throws IOException { + Path fileAsRoot = Files.createFile(tempDir.resolve("not-a-directory.txt")); + + assertThatThrownBy(() -> archiver.archive(fileAsRoot, "artifact")) + .isInstanceOf(ProjectArchiveInvalidRootException.class); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java new file mode 100644 index 0000000..316a3b0 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java @@ -0,0 +1,86 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootAlreadyExistsException; +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootIOException; +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootNotDirectoryException; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +@Tag("unit") +@Tag("adapter") +class FileSystemProjectRootAdapterTest { + + private final FileSystemProjectRootAdapter adapter = new FileSystemProjectRootAdapter(); + @TempDir Path tempDir; + + @Test + @DisplayName("Should create directory when project root does not exist") + void shouldCreateDirectoryWhenNotExists() { + Path result = + adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS); + + assertThat(result).exists().isDirectory(); + assertThat(result.getFileName().toString()).hasToString("demo-app"); + } + + @Test + @DisplayName( + "Should throw ProjectRootAlreadyExistsException when directory exists and policy=FAIL_IF_EXISTS") + void shouldFailIfExists() throws IOException { + Path existing = tempDir.resolve("demo-app"); + Files.createDirectories(existing); + + assertThatThrownBy( + () -> + adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS)) + .isInstanceOf(ProjectRootAlreadyExistsException.class); + } + + @Test + @DisplayName("Should return directory when exists and policy=OVERWRITE") + void shouldReturnExistingDirWhenOverwrite() throws IOException { + Path existing = tempDir.resolve("demo-app"); + Files.createDirectories(existing); + + Path result = adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.OVERWRITE); + + assertThat(result).isEqualTo(existing); + } + + @Test + @DisplayName("Should throw ProjectRootNotDirectoryException when exists and is a file") + void shouldThrowIfExistsButNotDirectory() throws IOException { + Path file = tempDir.resolve("demo-app"); + Files.writeString(file, "not a directory"); + + assertThatThrownBy( + () -> + adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS)) + .isInstanceOf(ProjectRootNotDirectoryException.class); + } + + @Test + @DisplayName("Should wrap IO errors in ProjectRootIOException") + void shouldWrapIOException() throws IOException { + Path targetDir = Files.createTempDirectory("locked"); + File dir = targetDir.toFile(); + + assertThat(dir.setReadable(true)).isTrue(); + assertThat(dir.setWritable(false)).isTrue(); + assertThat(dir.setExecutable(true)).isTrue(); + + assertThatThrownBy( + () -> adapter.prepareRoot(targetDir, "demo-app", ProjectRootExistencePolicy.OVERWRITE)) + .isInstanceOf(ProjectRootIOException.class); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java new file mode 100644 index 0000000..fb2eda5 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java @@ -0,0 +1,81 @@ +package io.github.blueprintplatform.codegen.adapter.out.filesystem; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectWriteException; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class FileSystemProjectWriterAdapterTest { + + private final ProjectWriterPort writer = new FileSystemProjectWriterAdapter(); + + @Test + @DisplayName("writeBytes() should create parent directories and write file") + void writeBytes_shouldCreateDirsAndWrite() throws IOException { + Path temp = Files.createTempDirectory("writer-test"); + Path relative = Path.of("a/b/c.txt"); + + byte[] content = "hello-bytes".getBytes(StandardCharsets.UTF_8); + + writer.writeBytes(temp, relative, content); + + Path target = temp.resolve(relative); + assertThat(Files.exists(target)).isTrue(); + assertThat(Files.readString(target)).isEqualTo("hello-bytes"); + } + + @Test + @DisplayName("writeBytes() should overwrite existing file") + void writeBytes_shouldOverwriteExistingFile() throws IOException { + Path temp = Files.createTempDirectory("writer-test2"); + Path relative = Path.of("file.txt"); + + Files.writeString(temp.resolve(relative), "old"); + + writer.writeBytes(temp, relative, "new".getBytes(StandardCharsets.UTF_8)); + + assertThat(Files.readString(temp.resolve(relative))).isEqualTo("new"); + } + + @Test + @DisplayName("writeText() should write file with provided charset") + void writeText_shouldWriteWithCharset() throws IOException { + Path temp = Files.createTempDirectory("writer-test3"); + Path relative = Path.of("utf16.txt"); + + writer.writeText(temp, relative, "Merhaba Dünya", StandardCharsets.UTF_16); + + assertThat(Files.readString(temp.resolve(relative), StandardCharsets.UTF_16)) + .isEqualTo("Merhaba Dünya"); + } + + @Test + @DisplayName("writeBytes() should wrap IOExceptions in ProjectWriteException") + void writeBytes_shouldWrapIOException() throws IOException { + Path temp = Files.createTempDirectory("writer-test4"); + + // Make directory read-only to force IOException + File root = temp.toFile(); + assertThat(root.setWritable(false)).isTrue(); + + Path relative = Path.of("fail/here.txt"); + byte[] content = "boom".getBytes(StandardCharsets.UTF_8); + + assertThatThrownBy(() -> writer.writeBytes(temp, relative, content)) + .isInstanceOf(ProjectWriteException.class); + + // Restore permission for cleanup + assertThat(root.setWritable(true)).isTrue(); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java new file mode 100644 index 0000000..a624ce8 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java @@ -0,0 +1,63 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactsPortNotFoundException; +import io.github.blueprintplatform.codegen.adapter.error.exception.UnsupportedProfileTypeException; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class ProfileBasedArtifactsSelectorTest { + + @Test + @DisplayName("Should throw UnsupportedProfileTypeException when ProfileType.from() returns null") + void shouldThrowWhenProfileUnsupported() { + TechStack options = mock(TechStack.class); + + ProfileBasedArtifactsSelector selector = new ProfileBasedArtifactsSelector(Map.of()); + + assertThatThrownBy(() -> selector.select(options)) + .isInstanceOf(UnsupportedProfileTypeException.class); + } + + @Test + @DisplayName("Should throw ArtifactsPortNotFoundException when no port registered for type") + void shouldThrowWhenPortMissing() { + TechStack opts = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + ProfileType type = ProfileType.from(opts); + assertThat(type).isNotNull(); + + ProfileBasedArtifactsSelector selector = + new ProfileBasedArtifactsSelector(Map.of()); // empty registry + + assertThatThrownBy(() -> selector.select(opts)) + .isInstanceOf(ArtifactsPortNotFoundException.class); + } + + @Test + @DisplayName("Should return registered ProjectArtifactsPort for matching profile") + void shouldReturnMatchingPort() { + TechStack opts = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + ProfileType type = ProfileType.from(opts); + + ProjectArtifactsPort port = mock(ProjectArtifactsPort.class); + + ProfileBasedArtifactsSelector selector = new ProfileBasedArtifactsSelector(Map.of(type, port)); + + ProjectArtifactsPort result = selector.select(opts); + + assertThat(result).isSameAs(port); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java new file mode 100644 index 0000000..096d0d6 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java @@ -0,0 +1,80 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.domain.factory.ProjectBlueprintFactory; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.util.List; +import java.util.stream.StreamSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@Tag("integration") +class SpringBootMavenJavaArtifactsAdapterIT { + + @Autowired private SpringBootMavenJavaArtifactsAdapter adapter; + + @Test + @DisplayName( + "generate() should produce artifacts for a valid Spring Boot + Maven + Java blueprint") + void generate_shouldProduceArtifactsForValidBlueprint() { + ProjectBlueprint blueprint = blueprint(); + + Iterable files = adapter.generate(blueprint); + + var list = StreamSupport.stream(files.spliterator(), false).toList(); + + assertThat(list).isNotEmpty(); + } + + private ProjectBlueprint blueprint() { + ProjectIdentity identity = + new ProjectIdentity(new GroupId("com.example"), new ArtifactId("demo-app")); + + ProjectName name = new ProjectName("demo-app"); + ProjectDescription description = new ProjectDescription("Integration test blueprint"); + PackageName packageName = new PackageName("com.example.demo"); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + PlatformTarget platformTarget = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependency webStarter = + new Dependency( + new DependencyCoordinates( + new GroupId("org.springframework.boot"), new ArtifactId("spring-boot-starter")), + null, + null); + + Dependencies dependencies = Dependencies.of(List.of(webStarter)); + + ProjectLayout layout = ProjectLayout.STANDARD; + + return ProjectBlueprintFactory.of( + identity, name, description, packageName, techStack, layout, platformTarget, dependencies); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java new file mode 100644 index 0000000..bc68347 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java @@ -0,0 +1,54 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.util.Collections; +import java.util.List; +import java.util.stream.StreamSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class SpringBootMavenJavaArtifactsAdapterTest { + + @Test + @DisplayName("Should return empty list when no artifact ports are configured") + void shouldReturnEmptyWhenNoPorts() { + SpringBootMavenJavaArtifactsAdapter adapter = + new SpringBootMavenJavaArtifactsAdapter(List.of()); + + ProjectBlueprint blueprint = + new ProjectBlueprint(null, null, null, null, null, null, null, null); + + List result = + StreamSupport.stream(adapter.generate(blueprint).spliterator(), false).toList(); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should delegate generate() exactly once to each ArtifactPort") + void shouldDelegateToEachArtifactPort() { + ProjectBlueprint blueprint = + new ProjectBlueprint(null, null, null, null, null, null, null, null); + + ArtifactPort p1 = mock(ArtifactPort.class); + ArtifactPort p2 = mock(ArtifactPort.class); + + when(p1.generate(blueprint)).thenReturn(Collections.emptyList()); + when(p2.generate(blueprint)).thenReturn(Collections.emptyList()); + + SpringBootMavenJavaArtifactsAdapter adapter = + new SpringBootMavenJavaArtifactsAdapter(List.of(p1, p2)); + + adapter.generate(blueprint); + + verify(p1, times(1)).generate(blueprint); + verify(p2, times(1)).generate(blueprint); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java new file mode 100644 index 0000000..f5720be --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java @@ -0,0 +1,148 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.build.RecordingPomDependencyMapper; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class MavenPomBuildConfigurationAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + private static ProjectBlueprint blueprintWithDependencies() { + ProjectIdentity identity = + new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app")); + + ProjectName name = new ProjectName("Demo App"); + ProjectDescription description = new ProjectDescription("Sample Project"); + PackageName pkg = new PackageName("com.acme.demo"); + ProjectLayout layout = ProjectLayout.STANDARD; + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + PlatformTarget target = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependency dep = + new Dependency( + new DependencyCoordinates(new GroupId("org.acme"), new ArtifactId("custom-dep")), + new DependencyVersion("1.0.0"), + DependencyScope.RUNTIME); + + Dependencies dependencies = Dependencies.of(List.of(dep)); + + return new ProjectBlueprint( + identity, name, description, pkg, techStack, layout, target, dependencies); + } + + @Test + @DisplayName("artifactKey() should return POM") + void artifactKey_shouldReturnPom() { + MavenPomBuildConfigurationAdapter adapter = + new MavenPomBuildConfigurationAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, List.of(new TemplateDefinition("pom.ftl", "pom.xml"))), + new RecordingPomDependencyMapper(List.of())); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.BUILD_CONFIG); + } + + @Test + @DisplayName("buildModel via generate() should populate all POM fields and dependencies") + void generate_shouldBuildCorrectModelForPom() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + RecordingPomDependencyMapper mapper = + new RecordingPomDependencyMapper( + List.of(PomDependency.of("org.acme", "custom-dep", "1.0.0", "runtime"))); + + TemplateDefinition templateDefinition = new TemplateDefinition("pom.ftl", "pom.xml"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + MavenPomBuildConfigurationAdapter adapter = + new MavenPomBuildConfigurationAdapter(renderer, artifactDefinition, mapper); + + ProjectBlueprint blueprint = blueprintWithDependencies(); + + Path relativePath = Path.of("pom.xml"); + GeneratedFile.Text dummyFile = + new GeneratedFile.Text(relativePath, "", StandardCharsets.UTF_8); + renderer.nextFile = dummyFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(dummyFile); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "pom.ftl"); + assertThat(renderer.capturedModel).isNotNull(); + + Map model = renderer.capturedModel; + + assertThat(model) + .containsEntry("groupId", "com.acme") + .containsEntry("artifactId", "demo-app") + .containsEntry("javaVersion", "21") + .containsEntry("springBootVersion", "3.5.8") + .containsEntry("projectName", "Demo App") + .containsEntry("projectDescription", "Sample Project"); + + assertThat(mapper.capturedDependencies).isSameAs(blueprint.getDependencies()); + + @SuppressWarnings("unchecked") + List deps = (List) model.get("dependencies"); + assertThat(deps).hasSize(3); + + PomDependency core = deps.getFirst(); + assertThat(core.groupId()).isEqualTo("org.springframework.boot"); + assertThat(core.artifactId()).isEqualTo("spring-boot-starter"); + assertThat(core.version()).isNull(); + assertThat(core.scope()).isNull(); + + PomDependency mapped = deps.get(1); + assertThat(mapped.groupId()).isEqualTo("org.acme"); + assertThat(mapped.artifactId()).isEqualTo("custom-dep"); + assertThat(mapped.version()).isEqualTo("1.0.0"); + assertThat(mapped.scope()).isEqualTo("runtime"); + + PomDependency testStarter = deps.get(2); + assertThat(testStarter.groupId()).isEqualTo("org.springframework.boot"); + assertThat(testStarter.artifactId()).isEqualTo("spring-boot-starter-test"); + assertThat(testStarter.scope()).isEqualTo("test"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java new file mode 100644 index 0000000..e38b281 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java @@ -0,0 +1,77 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class ApplicationYamlAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + @Test + @DisplayName("artifactKey() should return APPLICATION_YAML") + void artifactKey_shouldReturnApplicationYaml() { + ApplicationYamlAdapter adapter = + new ApplicationYamlAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, + List.of(new TemplateDefinition("application-yaml.ftl", "application.yml")))); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.APP_CONFIG); + } + + @Test + @DisplayName("generate() should build model with normalized projectName and render single file") + void generate_shouldBuildModelAndRenderFile() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = + new TemplateDefinition("application-yaml.ftl", "src/main/resources/application.yml"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + ApplicationYamlAdapter adapter = new ApplicationYamlAdapter(renderer, artifactDefinition); + + ProjectBlueprint blueprint = + new ProjectBlueprint( + null, + new ProjectName("Demo App"), + new ProjectDescription("Sample Project"), + null, + null, + null, + null, + null); + + Path relativePath = Path.of("src/main/resources/application.yml"); + GeneratedFile.Text expectedFile = + new GeneratedFile.Text( + relativePath, "spring.application.name=demo-app", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "application-yaml.ftl"); + assertThat(renderer.capturedModel).isNotNull().containsEntry("projectName", "Demo App"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapterTest.java new file mode 100644 index 0000000..6788474 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/docs/ProjectDocumentationAdapterTest.java @@ -0,0 +1,146 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.docs; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency; +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.build.RecordingPomDependencyMapper; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class ProjectDocumentationAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + private static ProjectBlueprint blueprintWithDependencies() { + ProjectIdentity identity = + new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app")); + + ProjectName name = new ProjectName("Demo App"); + ProjectDescription description = new ProjectDescription("Sample Project"); + PackageName pkg = new PackageName("com.acme.demo"); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + ProjectLayout layout = ProjectLayout.STANDARD; + PlatformTarget target = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependency dep = + new Dependency( + new DependencyCoordinates(new GroupId("org.acme"), new ArtifactId("custom-dep")), + new DependencyVersion("1.0.0"), + DependencyScope.RUNTIME); + + Dependencies dependencies = Dependencies.of(List.of(dep)); + + return new ProjectBlueprint( + identity, name, description, pkg, techStack, layout, target, dependencies); + } + + @Test + @DisplayName("artifactKey() should return PROJECT_DOCUMENTATION") + void artifactKey_shouldReturnProjectDocumentation() { + ProjectDocumentationAdapter adapter = + new ProjectDocumentationAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, List.of(new TemplateDefinition("README.ftl", "README.md"))), + new PomDependencyMapper()); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.PROJECT_DOCUMENTATION); + } + + @Test + @DisplayName( + "generate() should build correct project documentation model and delegate dependencies mapping") + void generate_shouldBuildCorrectModelForProjectDocumentation() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + List mappedDeps = + List.of(PomDependency.of("org.acme", "custom-dep", "1.0.0", "runtime")); + RecordingPomDependencyMapper mapper = new RecordingPomDependencyMapper(mappedDeps); + + TemplateDefinition templateDefinition = new TemplateDefinition("README.ftl", "README.md"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + ProjectDocumentationAdapter adapter = + new ProjectDocumentationAdapter(renderer, artifactDefinition, mapper); + + ProjectBlueprint blueprint = blueprintWithDependencies(); + + Path relativePath = Path.of("README.md"); + GeneratedFile.Text dummyFile = + new GeneratedFile.Text(relativePath, "# Readme", StandardCharsets.UTF_8); + renderer.nextFile = dummyFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(dummyFile); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "README.ftl"); + assertThat(renderer.capturedModel).isNotNull(); + + Map model = renderer.capturedModel; + + assertThat(model) + .containsEntry("projectName", "Demo App") + .containsEntry("projectDescription", "Sample Project") + .containsEntry("groupId", "com.acme") + .containsEntry("artifactId", "demo-app") + .containsEntry("packageName", "com.acme.demo") + .containsEntry("buildTool", "maven") + .containsEntry("language", "java") + .containsEntry("framework", "spring-boot") + .containsEntry("javaVersion", "21") + .containsEntry("springBootVersion", "3.5.8") + .containsEntry("hasHexSample", false); + + assertThat(mapper.capturedDependencies).isSameAs(blueprint.getDependencies()); + + @SuppressWarnings("unchecked") + List deps = (List) model.get("dependencies"); + assertThat(deps).isSameAs(mappedDeps).hasSize(1); + + PomDependency d = deps.getFirst(); + assertThat(d.groupId()).isEqualTo("org.acme"); + assertThat(d.artifactId()).isEqualTo("custom-dep"); + assertThat(d.version()).isEqualTo("1.0.0"); + assertThat(d.scope()).isEqualTo("runtime"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java new file mode 100644 index 0000000..a749927 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java @@ -0,0 +1,67 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class GitIgnoreAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + @Test + @DisplayName("artifactKey() should return IGNORE_RULES") + void artifactKey_shouldReturnIgnoreRules() { + GitIgnoreAdapter adapter = + new GitIgnoreAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, List.of(new TemplateDefinition("gitignore.ftl", ".gitignore")))); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.IGNORE_RULES); + } + + @Test + @DisplayName("generate() should render ignore rules with an empty ignoreList model") + void generate_shouldRenderGitignoreWithEmptyIgnoreList() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = new TemplateDefinition("gitignore.ftl", ".gitignore"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + GitIgnoreAdapter adapter = new GitIgnoreAdapter(renderer, artifactDefinition); + + ProjectBlueprint blueprint = + new ProjectBlueprint(null, null, null, null, null, null, null, null); + + Path relativePath = Path.of(".gitignore"); + GeneratedFile.Text expectedFile = + new GeneratedFile.Text(relativePath, "# gitignore", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "gitignore.ftl"); + + Map model = renderer.capturedModel; + assertThat(model).isNotNull().containsEntry("ignoreList", List.of()); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapterTest.java new file mode 100644 index 0000000..ace739b --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapterTest.java @@ -0,0 +1,85 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class AbstractJavaSourceFileAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + @Test + @DisplayName("generate() should build correct path, model and return single file") + void generate_shouldBuildOutPathAndModelAndReturnFile() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = + new TemplateDefinition("java-class.ftl", "src/main/java"); + + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + StringCaseFormatter formatter = new StringCaseFormatter(); + + TestJavaSourceFileAdapter adapter = + new TestJavaSourceFileAdapter(renderer, artifactDefinition, formatter); + + ProjectBlueprint blueprint = + new ProjectBlueprint( + null, null, null, new PackageName("com.acme.demo"), null, null, null, null); + + Path expectedPath = Path.of("src/main/java/com/acme/demo/DemoApplication.java"); + + GeneratedFile.Text expectedFile = + new GeneratedFile.Text(expectedPath, "class DemoApplication {}", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(expectedPath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "java-class.ftl"); + + assertThat(renderer.capturedModel) + .isNotNull() + .containsEntry("projectPackageName", "com.acme.demo") + .containsEntry("className", "DemoApplication"); + } + + private static final class TestJavaSourceFileAdapter extends AbstractJavaSourceFileAdapter { + + TestJavaSourceFileAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + StringCaseFormatter stringCaseFormatter) { + super(renderer, artifactDefinition, stringCaseFormatter); + } + + @Override + protected String buildClassName(ProjectBlueprint blueprint) { + return "DemoApplication"; + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.MAIN_SOURCE_ENTRY_POINT; + } + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapterTest.java new file mode 100644 index 0000000..cd59432 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapterTest.java @@ -0,0 +1,108 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class MainSourceEntrypointAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + private static ProjectBlueprint blueprint() { + ProjectIdentity identity = + new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app")); + + ProjectName name = new ProjectName("Demo App"); + ProjectDescription description = new ProjectDescription("Sample Project"); + PackageName pkg = new PackageName("com.acme.demo"); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + ProjectLayout layout = ProjectLayout.STANDARD; + PlatformTarget platformTarget = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + Dependencies dependencies = Dependencies.of(List.of()); + + return new ProjectBlueprint( + identity, name, description, pkg, techStack, layout, platformTarget, dependencies); + } + + @Test + @DisplayName("artifactKey() should return MAIN_SOURCE_ENTRY_POINT") + void artifactKey_shouldReturnMainSourceEntrypoint() { + MainSourceEntrypointAdapter adapter = + new MainSourceEntrypointAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, List.of(new TemplateDefinition("source.ftl", "src/main/java"))), + new StringCaseFormatter()); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.MAIN_SOURCE_ENTRY_POINT); + } + + @Test + @DisplayName( + "generate() should build class name from artifactId (PascalCase + Application) and render file under package path") + void generate_shouldBuildClassNameFromArtifactIdAndRenderFile() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = new TemplateDefinition("source.ftl", "src/main/java"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + MainSourceEntrypointAdapter adapter = + new MainSourceEntrypointAdapter(renderer, artifactDefinition, new StringCaseFormatter()); + + ProjectBlueprint blueprint = blueprint(); + + Path expectedPath = Path.of("src/main/java/com/acme/demo/DemoAppApplication.java"); + + GeneratedFile.Text expectedFile = + new GeneratedFile.Text(expectedPath, "class DemoAppApplication {}", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(expectedPath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "source.ftl"); + + Map model = renderer.capturedModel; + assertThat(model) + .isNotNull() + .containsEntry("projectPackageName", "com.acme.demo") + .containsEntry("className", "DemoAppApplication"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapterTest.java new file mode 100644 index 0000000..77dd90d --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/test/TestSourceEntrypointAdapterTest.java @@ -0,0 +1,110 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class TestSourceEntrypointAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + private static ProjectBlueprint blueprint() { + ProjectIdentity identity = + new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app")); + + ProjectName name = new ProjectName("Demo App"); + ProjectDescription description = new ProjectDescription("Sample Project"); + PackageName pkg = new PackageName("com.acme.demo"); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + ProjectLayout layout = ProjectLayout.STANDARD; + PlatformTarget platformTarget = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependencies dependencies = Dependencies.of(List.of()); + + return new ProjectBlueprint( + identity, name, description, pkg, techStack, layout, platformTarget, dependencies); + } + + @Test + @DisplayName("artifactKey() should return TEST_SOURCE_ENTRY_POINT") + void artifactKey_shouldReturnTestSourceEntrypoint() { + TestSourceEntrypointAdapter adapter = + new TestSourceEntrypointAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, List.of(new TemplateDefinition("test.ftl", "src/test/java"))), + new StringCaseFormatter()); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.TEST_SOURCE_ENTRY_POINT); + } + + @Test + @DisplayName( + "generate() should build className = PascalCase(artifactId) + ApplicationTests and render file") + void generate_shouldBuildClassNameAndRenderFile() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = new TemplateDefinition("test.ftl", "src/test/java"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + TestSourceEntrypointAdapter adapter = + new TestSourceEntrypointAdapter(renderer, artifactDefinition, new StringCaseFormatter()); + + ProjectBlueprint blueprint = blueprint(); + + Path expectedPath = Path.of("src/test/java/com/acme/demo/DemoAppApplicationTests.java"); + + GeneratedFile.Text expectedFile = + new GeneratedFile.Text( + expectedPath, "class DemoAppApplicationTests {}", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(expectedPath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "test.ftl"); + + Map model = renderer.capturedModel; + assertThat(model) + .isNotNull() + .containsEntry("projectPackageName", "com.acme.demo") + .containsEntry("className", "DemoAppApplicationTests"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapterTest.java new file mode 100644 index 0000000..f872977 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapterTest.java @@ -0,0 +1,75 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.wrapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class MavenWrapperBuildToolFilesAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + @Test + @DisplayName("artifactKey() should return BUILD_TOOL_METADATA") + void artifactKey_shouldReturnBuildToolMetadata() { + MavenWrapperBuildToolFilesAdapter adapter = + new MavenWrapperBuildToolFilesAdapter( + new NoopTemplateRenderer(), + new ArtifactDefinition( + BASE_PATH, + List.of( + new TemplateDefinition( + "maven-wrapper.ftl", ".mvn/wrapper/maven-wrapper.properties")))); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.BUILD_TOOL_METADATA); + } + + @Test + @DisplayName("generate() should build model with default wrapper and maven versions") + void generate_shouldBuildModelWithDefaultVersions() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = + new TemplateDefinition("maven-wrapper.ftl", ".mvn/wrapper/maven-wrapper.properties"); + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + MavenWrapperBuildToolFilesAdapter adapter = + new MavenWrapperBuildToolFilesAdapter(renderer, artifactDefinition); + + ProjectBlueprint blueprint = + new ProjectBlueprint(null, null, null, null, null, null, null, null); + + Path relativePath = Path.of(".mvn/wrapper/maven-wrapper.properties"); + GeneratedFile.Text expectedFile = + new GeneratedFile.Text(relativePath, "distributionUrl=...", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(result).singleElement().isSameAs(expectedFile); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "maven-wrapper.ftl"); + + Map model = renderer.capturedModel; + assertThat(model) + .isNotNull() + .containsEntry("wrapperVersion", "3.3.4") + .containsEntry("mavenVersion", "3.9.11"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapterTest.java new file mode 100644 index 0000000..bb2cc2c --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapterTest.java @@ -0,0 +1,73 @@ +package io.github.blueprintplatform.codegen.adapter.out.shared.artifact; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("adapter") +class AbstractSingleTemplateArtifactAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + + @Test + @DisplayName("generate() should use first template, render with model, and return single file") + void generate_shouldRenderSingleTemplateAndReturnFile() { + CapturingTemplateRenderer renderer = new CapturingTemplateRenderer(); + + TemplateDefinition templateDefinition = + new TemplateDefinition("test-template.ftl", "output/test.txt"); + + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(BASE_PATH, List.of(templateDefinition)); + + TestSingleTemplateAdapter adapter = new TestSingleTemplateAdapter(renderer, artifactDefinition); + + ProjectBlueprint blueprint = + new ProjectBlueprint(null, null, null, null, null, null, null, null); + + Path relativePath = Path.of("output/test.txt"); + GeneratedFile.Text expectedFile = + new GeneratedFile.Text(relativePath, "rendered-content", StandardCharsets.UTF_8); + renderer.nextFile = expectedFile; + + Iterable result = adapter.generate(blueprint); + + assertThat(renderer.capturedOutPath).isEqualTo(relativePath); + assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "test-template.ftl"); + assertThat(renderer.capturedModel).isEqualTo(Map.of("key", "value")); + + assertThat(result).singleElement().isSameAs(expectedFile); + } + + private static final class TestSingleTemplateAdapter + extends AbstractSingleTemplateArtifactAdapter { + + TestSingleTemplateAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + super(renderer, artifactDefinition); + } + + @Override + protected Map buildModel(ProjectBlueprint blueprint) { + return Map.of("key", "value"); + } + + @Override + public ArtifactKey artifactKey() { + return null; + } + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatterTest.java new file mode 100644 index 0000000..a46e29a --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatterTest.java @@ -0,0 +1,99 @@ +package io.github.blueprintplatform.codegen.adapter.shared.naming; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +@Tag("unit") +@Tag("adapter") +class StringCaseFormatterTest { + + private final StringCaseFormatter formatter = new StringCaseFormatter(); + + @ParameterizedTest + @NullSource + @ValueSource(strings = {" ", "---___*** ###"}) + @DisplayName("null, blank or delimiters-only input should return empty string") + void nullBlankOrDelimitersOnly_shouldReturnEmpty(String input) { + String result = formatter.toPascalCase(input); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("single word should be capitalized") + void singleWord_shouldBeCapitalized() { + String result = formatter.toPascalCase("hello"); + + assertThat(result).isEqualTo("Hello"); + } + + @Test + @DisplayName("single word with mixed case should be normalized to PascalCase") + void singleWord_mixedCase_shouldBeNormalized() { + String result = formatter.toPascalCase("hELLo"); + + assertThat(result).isEqualTo("Hello"); + } + + @Test + @DisplayName("already PascalCase word should be normalized to first upper and rest lower") + void pascalCaseWord_shouldNormalizeRestToLower() { + String result = formatter.toPascalCase("MyValue"); + + assertThat(result).isEqualTo("Myvalue"); + } + + @Test + @DisplayName("space separated words should become concatenated PascalCase") + void spaceSeparatedWords_shouldBecomePascalCase() { + String result = formatter.toPascalCase("hello world example"); + + assertThat(result).isEqualTo("HelloWorldExample"); + } + + @Test + @DisplayName("multiple spaces and leading/trailing spaces should be ignored") + void multipleSpaces_shouldBeHandled() { + String result = formatter.toPascalCase(" hello world "); + + assertThat(result).isEqualTo("HelloWorld"); + } + + @Test + @DisplayName("kebab-case should become PascalCase") + void kebabCase_shouldBecomePascalCase() { + String result = formatter.toPascalCase("my-service-name"); + + assertThat(result).isEqualTo("MyServiceName"); + } + + @Test + @DisplayName("snake_case should become PascalCase") + void snakeCase_shouldBecomePascalCase() { + String result = formatter.toPascalCase("my_service_name"); + + assertThat(result).isEqualTo("MyServiceName"); + } + + @Test + @DisplayName("mixed delimiters and digits should be handled correctly") + void mixedDelimiters_andDigits_shouldBeHandled() { + String result = formatter.toPascalCase(" my-service_42 NAME "); + + assertThat(result).isEqualTo("MyService42Name"); + } + + @Test + @DisplayName("non-alphanumeric delimiters should be treated as separators") + void nonAlphanumericDelimiters_shouldBeSeparators() { + String result = formatter.toPascalCase("app@core#service!"); + + assertThat(result).isEqualTo("AppCoreService"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandlerTest.java b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandlerTest.java new file mode 100644 index 0000000..fe2c8f3 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandlerTest.java @@ -0,0 +1,168 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import static io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy.FAIL_IF_EXISTS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort; +import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector; +import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort; +import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +@Tag("unit") +@Tag("application") +class CreateProjectHandlerTest { + + @TempDir Path tempDir; + + @Test + @DisplayName("handle() prepares project root, writes artifacts, and returns archive path") + void handle_prepares_root_writes_artifacts_and_archives() { + var mapper = new ProjectBlueprintMapper(); + var fakeRootPort = new FakeRootPort(); + var fakeArtifacts = new FakeArtifactsPort(); + var fakeSelector = new FakeSelector(fakeArtifacts); + var fakeWriter = new FakeWriterPort(); + var fakeArchiver = new FakeArchiverPort(); + + var handler = + new CreateProjectHandler(mapper, fakeRootPort, fakeSelector, fakeWriter, fakeArchiver); + + var cmd = getCreateProjectCommand(); + + var result = handler.handle(cmd); + + assertThat(result.archivePath()).hasFileName("demo-app.zip"); + assertThat(fakeArchiver.lastProjectRoot).isEqualTo(fakeRootPort.lastPreparedRoot); + assertThat(fakeArchiver.lastArtifactId).isEqualTo("demo-app"); + + assertThat(fakeRootPort.lastPreparedRoot).isEqualTo(tempDir.resolve("demo-app")); + assertThat(fakeRootPort.lastPolicy).isEqualTo(FAIL_IF_EXISTS); + + assertThat(fakeWriter.writtenFiles) + .containsExactlyInAnyOrderElementsOf(fakeArtifacts.lastEmittedRelativePaths) + .hasSize(fakeArtifacts.lastEmittedRelativePaths.size()); + } + + private CreateProjectCommand getCreateProjectCommand() { + var techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + var platformTarget = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + return new CreateProjectCommand( + "com.acme", + "demo-app", + "Demo App", + "Demo project", + "com.acme.demo", + techStack, + ProjectLayout.STANDARD, // NEW: layout eklendi + platformTarget, + List.of(), + tempDir); + } + + static class FakeRootPort implements ProjectRootPort { + Path lastPreparedRoot; + ProjectRootExistencePolicy lastPolicy; + + @Override + public Path prepareRoot(Path targetDir, String artifactId, ProjectRootExistencePolicy policy) { + this.lastPolicy = policy; + this.lastPreparedRoot = targetDir.resolve(artifactId); + return lastPreparedRoot; + } + } + + static class FakeSelector implements ProjectArtifactsSelector { + private final ProjectArtifactsPort delegate; + + FakeSelector(ProjectArtifactsPort delegate) { + this.delegate = delegate; + } + + @Override + public ProjectArtifactsPort select(TechStack options) { + return delegate; + } + } + + static class FakeArtifactsPort implements ProjectArtifactsPort { + final List lastEmittedRelativePaths = new ArrayList<>(); + + @Override + public Iterable generate(ProjectBlueprint bp) { + var files = + List.of( + new GeneratedFile.Text(Path.of("pom.xml"), "", StandardCharsets.UTF_8), + new GeneratedFile.Text(Path.of(".gitignore"), "*.class", StandardCharsets.UTF_8), + new GeneratedFile.Text( + Path.of("src/main/java/com/acme/demo/DemoApplication.java"), + "class DemoApplication {}", + StandardCharsets.UTF_8), + new GeneratedFile.Text( + Path.of("src/test/java/com/acme/demo/DemoApplicationTests.java"), + "class DemoApplicationTests {}", + StandardCharsets.UTF_8), + new GeneratedFile.Text( + Path.of("src/main/resources/application.yml"), + "spring:\n application:\n name: demo-app", + StandardCharsets.UTF_8), + new GeneratedFile.Text( + Path.of("README.md"), + "# Demo App\n\nThis project was generated by Codegen Initializr.", + StandardCharsets.UTF_8)); + + lastEmittedRelativePaths.clear(); + for (var gf : files) { + lastEmittedRelativePaths.add(gf.relativePath()); + } + return files; + } + } + + static class FakeWriterPort implements ProjectWriterPort { + final List writtenFiles = new ArrayList<>(); + + @Override + public void writeBytes(Path projectRoot, Path relativePath, byte[] content) { + throw new UnsupportedOperationException("bytes not expected"); + } + + @Override + public void writeText(Path projectRoot, Path relativePath, String content, Charset charset) { + writtenFiles.add(relativePath); + } + } + + static class FakeArchiverPort implements ProjectArchiverPort { + Path lastProjectRoot; + String lastArtifactId; + + @Override + public Path archive(Path projectRoot, String artifactId) { + this.lastProjectRoot = projectRoot; + this.lastArtifactId = artifactId; + return projectRoot.getParent().resolve(artifactId + ".zip"); + } + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapperTest.java b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapperTest.java new file mode 100644 index 0000000..8495f1b --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapperTest.java @@ -0,0 +1,90 @@ +package io.github.blueprintplatform.codegen.application.usecase.project; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("application") +class ProjectBlueprintMapperTest { + + private static ProjectBlueprint getProjectBlueprint() { + var mapper = new ProjectBlueprintMapper(); + + var inputs = + List.of( + new DependencyInput("org.acme", "alpha", "", ""), + new DependencyInput("org.acme", "beta", "1.2.3", "runtime"), + new DependencyInput("org.acme", "gamma", " ", " "), + new DependencyInput("org.acme", "delta", "2.0.0-RC1", "TeSt")); + + var cmd = getCreateProjectCommand(inputs); + + return mapper.from(cmd); + } + + private static CreateProjectCommand getCreateProjectCommand(List inputs) { + var techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + var platformTarget = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + return new CreateProjectCommand( + "com.acme", + "demo-app", + "Demo App", + "Demo project", + "com.acme.demo", + techStack, + ProjectLayout.STANDARD, + platformTarget, + inputs, + Path.of(".")); + } + + @Test + @DisplayName("maps dependencies; handles blank version/scope and case-insensitive scope") + void maps_dependencies_and_handles_blank_version_and_scope() { + ProjectBlueprint bp = getProjectBlueprint(); + + assertThat(bp.getDependencies().asList()).hasSize(4); + + Map byArtifact = + bp.getDependencies().asList().stream() + .collect(Collectors.toMap(d -> d.coordinates().artifactId().value(), d -> d)); + + var alpha = byArtifact.get("alpha"); + assertThat(alpha.coordinates().groupId().value()).isEqualTo("org.acme"); + assertThat(alpha.version()).isNull(); + assertThat(alpha.scope()).isNull(); + assertThat(alpha.isDefaultScope()).isTrue(); + + var beta = byArtifact.get("beta"); + assertThat(beta.version().value()).isEqualTo("1.2.3"); + assertThat(beta.scope()).isEqualTo(DependencyScope.RUNTIME); + + var gamma = byArtifact.get("gamma"); + assertThat(gamma.version()).isNull(); + assertThat(gamma.scope()).isNull(); + assertThat(gamma.isDefaultScope()).isTrue(); + + var delta = byArtifact.get("delta"); + assertThat(delta.version().value()).isEqualTo("2.0.0-RC1"); + assertThat(delta.scope()).isEqualTo(DependencyScope.TEST); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesPropertiesTest.java b/src/test/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesPropertiesTest.java new file mode 100644 index 0000000..0eb5ce1 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesPropertiesTest.java @@ -0,0 +1,112 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType; +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.bootstrap.error.exception.ProfileConfigurationException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("bootstrap") +class CodegenProfilesPropertiesTest { + + private static final ProfileType PROFILE = ProfileType.SPRINGBOOT_MAVEN_JAVA; + private static final ArtifactKey ARTIFACT_KEY = ArtifactKey.BUILD_CONFIG; + private static final String PROFILE_KEY = PROFILE.key(); + private static final String ARTIFACT_MAP_KEY = ARTIFACT_KEY.key(); + private static final String TEMPLATE_BASE_PATH = "springboot/maven/java/"; + + private static CodegenProfilesProperties getCodegenProfilesProperties() { + TemplateDefinition templateDefinition = new TemplateDefinition("pom.ftl", "pom.xml"); + + ArtifactDefinition artifactDefinition = + new ArtifactDefinition(null, List.of(templateDefinition)); + + ProfileProperties profileProperties = + new ProfileProperties( + " ", List.of(ARTIFACT_KEY), Map.of(ARTIFACT_MAP_KEY, artifactDefinition)); + + return new CodegenProfilesProperties(Map.of(PROFILE_KEY, profileProperties)); + } + + @Test + @DisplayName( + "artifact() should return ArtifactDefinition with profile basePath and artifact templates") + void artifact_shouldReturnDefinitionWithProfileBasePathAndArtifactTemplates() { + TemplateDefinition templateDefinition = new TemplateDefinition("pom.ftl", "pom.xml"); + + ArtifactDefinition artifactDefinition = + new ArtifactDefinition("artifact-specific/", List.of(templateDefinition)); + + ProfileProperties profileProperties = + new ProfileProperties( + TEMPLATE_BASE_PATH, + List.of(ARTIFACT_KEY), + Map.of(ARTIFACT_MAP_KEY, artifactDefinition)); + + CodegenProfilesProperties properties = + new CodegenProfilesProperties(Map.of(PROFILE_KEY, profileProperties)); + + ArtifactDefinition result = properties.artifact(PROFILE, ARTIFACT_KEY); + + assertThat(result.basePath()).isEqualTo(TEMPLATE_BASE_PATH); + assertThat(result.templates()).isSameAs(artifactDefinition.templates()); + } + + @Test + @DisplayName( + "requireProfile() should throw ProfileConfigurationException when profile is missing") + void requireProfile_shouldThrowWhenProfileMissing() { + CodegenProfilesProperties properties = new CodegenProfilesProperties(Map.of()); + + assertThatThrownBy(() -> properties.requireProfile(PROFILE)) + .isInstanceOfSatisfying( + ProfileConfigurationException.class, + ex -> { + assertThat(ex.getMessageKey()) + .isEqualTo(ProfileConfigurationException.KEY_PROFILE_NOT_FOUND); + assertThat(ex.getArgs()).containsExactly(PROFILE_KEY); + }); + } + + @Test + @DisplayName("artifact() should throw ProfileConfigurationException when artifact is missing") + void artifact_shouldThrowWhenArtifactMissing() { + ProfileProperties profileProperties = + new ProfileProperties(TEMPLATE_BASE_PATH, List.of(ARTIFACT_KEY), Map.of()); + + CodegenProfilesProperties properties = + new CodegenProfilesProperties(Map.of(PROFILE_KEY, profileProperties)); + + assertThatThrownBy(() -> properties.artifact(PROFILE, ARTIFACT_KEY)) + .isInstanceOfSatisfying( + ProfileConfigurationException.class, + ex -> { + assertThat(ex.getMessageKey()) + .isEqualTo(ProfileConfigurationException.KEY_ARTIFACT_NOT_FOUND); + assertThat(ex.getArgs()).containsExactly(ARTIFACT_MAP_KEY, PROFILE_KEY); + }); + } + + @Test + @DisplayName( + "artifact() should throw ProfileConfigurationException when templateBasePath is blank") + void artifact_shouldThrowWhenTemplateBasePathBlank() { + CodegenProfilesProperties properties = getCodegenProfilesProperties(); + + assertThatThrownBy(() -> properties.artifact(PROFILE, ARTIFACT_KEY)) + .isInstanceOfSatisfying( + ProfileConfigurationException.class, + ex -> { + assertThat(ex.getMessageKey()) + .isEqualTo(ProfileConfigurationException.KEY_TEMPLATE_BASE_MISSING); + assertThat(ex.getArgs()).containsExactly(PROFILE_KEY); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunnerTest.java b/src/test/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunnerTest.java new file mode 100644 index 0000000..b9f1594 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunnerTest.java @@ -0,0 +1,94 @@ +package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCliExceptionHandler; +import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand; +import java.lang.reflect.Method; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +@Tag("unit") +@Tag("bootstrap") +class CodegenCliRunnerTest { + + @Test + @DisplayName("--cli should be removed and remaining arguments preserved") + void extractCliArgs_shouldRemoveCliFlag() throws Exception { + var runner = new CodegenCliRunner(dummyCommand(), dummyFactory(), dummyHandler()); + + String[] source = {"--cli", "springboot", "--group-id", "com.acme"}; + + String[] result = invokeExtractCliArgs(runner, source); + + assertThat(result).containsExactly("springboot", "--group-id", "com.acme"); + } + + @Test + @DisplayName("--spring.* option with inline value should be filtered out") + void extractCliArgs_shouldFilterSpringOptionWithInlineValue() throws Exception { + var runner = new CodegenCliRunner(dummyCommand(), dummyFactory(), dummyHandler()); + + String[] source = { + "--cli", "--spring.profiles.active=cli", "springboot", "--artifact-id", "demo-app" + }; + + String[] result = invokeExtractCliArgs(runner, source); + + assertThat(result).containsExactly("springboot", "--artifact-id", "demo-app"); + } + + @Test + @DisplayName("--spring.* option with separate value should skip both option and value") + void extractCliArgs_shouldFilterSpringOptionWithSeparateValue() throws Exception { + var runner = new CodegenCliRunner(dummyCommand(), dummyFactory(), dummyHandler()); + + String[] source = { + "--cli", + "--spring.config.location", + "application-test.yml", + "springboot", + "--group-id", + "com.acme" + }; + + String[] result = invokeExtractCliArgs(runner, source); + + assertThat(result).containsExactly("springboot", "--group-id", "com.acme"); + } + + @Test + @DisplayName("Non-filtered options should pass through unchanged") + void extractCliArgs_shouldKeepNonFilteredOptions() throws Exception { + var runner = new CodegenCliRunner(dummyCommand(), dummyFactory(), dummyHandler()); + + String[] source = { + "--cli", "springboot", "--group-id", "com.acme", "--artifact-id", "demo-app" + }; + + String[] result = invokeExtractCliArgs(runner, source); + + assertThat(result) + .containsExactly("springboot", "--group-id", "com.acme", "--artifact-id", "demo-app"); + } + + private String[] invokeExtractCliArgs(CodegenCliRunner runner, String[] source) throws Exception { + Method m = CodegenCliRunner.class.getDeclaredMethod("extractCliArgs", String[].class); + m.setAccessible(true); + return (String[]) m.invoke(runner, new Object[] {source}); + } + + private CodegenCommand dummyCommand() { + return new CodegenCommand(); + } + + private CommandLine.IFactory dummyFactory() { + return CommandLine.defaultFactory(); + } + + private CodegenCliExceptionHandler dummyHandler() { + return null; + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactoryTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactoryTest.java new file mode 100644 index 0000000..b2f7e0f --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactoryTest.java @@ -0,0 +1,196 @@ +package io.github.blueprintplatform.codegen.domain.factory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription; +import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName; +import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class ProjectBlueprintFactoryTest { + + private static ProjectIdentity identity() { + return new ProjectIdentity(new GroupId("com.example"), new ArtifactId("demo-app")); + } + + private static ProjectName name() { + return new ProjectName("demo-app"); + } + + private static ProjectDescription description() { + return new ProjectDescription("simple demo project"); + } + + private static PackageName pkg() { + return new PackageName("com.example.demo"); + } + + private static TechStack techStack() { + return new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + } + + private static PlatformTarget target() { + return new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + } + + private static ProjectLayout layout() { + return ProjectLayout.STANDARD; + } + + private static Dependencies dependencies() { + Dependency d = dep("org.acme", "alpha"); + return Dependencies.of(List.of(d)); + } + + private static Dependency dep(String groupId, String artifactId) { + return new Dependency( + new DependencyCoordinates(new GroupId(groupId), new ArtifactId(artifactId)), null, null); + } + + @Test + @DisplayName( + "of(identity, name, desc, package, stack, layout, target, dependencies) should create blueprint with same references") + void of_withDependenciesObject_shouldCreateBlueprint() { + ProjectIdentity identity = identity(); + ProjectName name = name(); + ProjectDescription description = description(); + PackageName packageName = pkg(); + TechStack stack = techStack(); + PlatformTarget target = target(); + ProjectLayout layout = layout(); + Dependencies dependencies = dependencies(); + + ProjectBlueprint bp = + ProjectBlueprintFactory.of( + identity, name, description, packageName, stack, layout, target, dependencies); + + assertThat(bp.getIdentity()).isSameAs(identity); + assertThat(bp.getName()).isSameAs(name); + assertThat(bp.getDescription()).isSameAs(description); + assertThat(bp.getPackageName()).isSameAs(packageName); + assertThat(bp.getTechStack()).isSameAs(stack); + assertThat(bp.getLayout()).isSameAs(layout); + assertThat(bp.getPlatformTarget()).isSameAs(target); + assertThat(bp.getDependencies()).isSameAs(dependencies); + } + + @Test + @DisplayName("of(..., List) should wrap list into Dependencies") + void of_withDependencyList_shouldWrapIntoDependencies() { + var d1 = dep("org.acme", "alpha"); + var d2 = dep("org.example", "beta"); + + ProjectBlueprint bp = + ProjectBlueprintFactory.of( + identity(), + name(), + description(), + pkg(), + techStack(), + layout(), + target(), + List.of(d1, d2)); + + assertThat(bp.getDependencies()).isNotNull(); + assertThat(bp.getDependencies().asList()).hasSize(2); + } + + @Test + @DisplayName("of(..., Dependency...) should wrap varargs into Dependencies") + void of_withVarargs_shouldWrapIntoDependencies() { + var d1 = dep("org.acme", "alpha"); + var d2 = dep("org.example", "beta"); + + ProjectBlueprint bp = + ProjectBlueprintFactory.of( + identity(), name(), description(), pkg(), techStack(), layout(), target(), d1, d2); + + assertThat(bp.getDependencies()).isNotNull(); + assertThat(bp.getDependencies().asList()).hasSize(2); + } + + @Test + @DisplayName("null tech stack should fail via CompatibilityPolicy with platform.target.missing") + void nullTechStack_shouldFailPlatformTargetMissing() { + assertThatThrownBy( + () -> + ProjectBlueprintFactory.of( + identity(), + name(), + description(), + pkg(), + null, // techStack + layout(), + target(), + dependencies())) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.missing")); + } + + @Test + @DisplayName( + "null platform target should fail via CompatibilityPolicy with platform.target.missing") + void nullPlatformTarget_shouldFailPlatformTargetMissing() { + assertThatThrownBy( + () -> + ProjectBlueprintFactory.of( + identity(), + name(), + description(), + pkg(), + techStack(), + layout(), + null, // platformTarget + dependencies())) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.missing")); + } + + @Test + @DisplayName("incompatible platform target should delegate to CompatibilityPolicy and fail") + void incompatiblePlatformTarget_shouldFailCompatibility() { + TechStack stack = techStack(); + PlatformTarget incompatible = + new SpringBootJvmTarget(JavaVersion.JAVA_25, SpringBootVersion.V3_4); + + assertThatThrownBy( + () -> + ProjectBlueprintFactory.of( + identity(), + name(), + description(), + pkg(), + stack, + layout(), + incompatible, + dependencies())) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.incompatible")); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependenciesTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependenciesTest.java new file mode 100644 index 0000000..9ac6d96 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependenciesTest.java @@ -0,0 +1,109 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class DependenciesTest { + + private static Dependency dep(String groupId, String artifactId) { + return new Dependency( + new DependencyCoordinates(new GroupId(groupId), new ArtifactId(artifactId)), null, null); + } + + @Test + @DisplayName("of(null) should fail with LIST_REQUIRED") + void of_nullList_shouldFailListRequired() { + assertThatThrownBy(() -> Dependencies.of(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.list.not.blank"); + }); + } + + @Test + @DisplayName("of(emptyList) should return empty Dependencies") + void of_emptyList_shouldReturnEmptyDependencies() { + Dependencies deps = Dependencies.of(List.of()); + + assertThat(deps.asList()).isEmpty(); + assertThat(deps.isEmpty()).isTrue(); + } + + @Test + @DisplayName("of(non-empty list) should sort by groupId:artifactId and be immutable") + void of_nonEmptyList_shouldSortAndBeImmutable() { + Dependency depZ = dep("org.zeta", "beta"); + Dependency depA = dep("org.alpha", "alpha"); + + List raw = new ArrayList<>(); + raw.add(depZ); + raw.add(depA); + + Dependencies deps = Dependencies.of(raw); + + var list = deps.asList(); + assertThat(list).hasSize(2); + + assertThat(list.get(0).coordinates().groupId().value()).isEqualTo("org.alpha"); + assertThat(list.get(0).coordinates().artifactId().value()).isEqualTo("alpha"); + assertThat(list.get(1).coordinates().groupId().value()).isEqualTo("org.zeta"); + assertThat(list.get(1).coordinates().artifactId().value()).isEqualTo("beta"); + + raw.add(dep("org.extra", "extra")); + + var snapshot = deps.asList(); + assertThat(snapshot).hasSize(2); + + Dependency extraDep = dep("org.any", "any"); + assertThatThrownBy(() -> snapshot.add(extraDep)) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + @DisplayName("of(list with null item) should fail ITEM_REQUIRED") + void of_listWithNullItem_shouldFailItemRequired() { + Dependency d1 = dep("org.acme", "alpha"); + List raw = new ArrayList<>(); + raw.add(d1); + raw.add(null); + + assertThatThrownBy(() -> Dependencies.of(raw)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.item.not.blank"); + }); + } + + @Test + @DisplayName("of(list with duplicate coordinates) should fail DUPLICATE_COORDS") + void of_listWithDuplicateCoords_shouldFailDuplicateCoords() { + Dependency d1 = dep("org.acme", "common"); + Dependency d2 = dep("org.acme", "common"); + + List raw = List.of(d1, d2); + + assertThatThrownBy(() -> Dependencies.of(raw)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.duplicate.coordinates"); + assertThat(dve.getArgs()).containsExactly("org.acme:common"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinatesTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinatesTest.java new file mode 100644 index 0000000..675f06d --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinatesTest.java @@ -0,0 +1,56 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class DependencyCoordinatesTest { + + @Test + @DisplayName("valid groupId and artifactId should be accepted") + void validCoordinates_shouldBeAccepted() { + GroupId g = new GroupId("org.acme"); + ArtifactId a = new ArtifactId("demo-artifact"); + + DependencyCoordinates coords = new DependencyCoordinates(g, a); + + assertThat(coords.groupId()).isSameAs(g); + assertThat(coords.artifactId()).isSameAs(a); + } + + @Test + @DisplayName("null groupId should fail COORDINATES_REQUIRED") + void nullGroupId_shouldFailCoordinatesRequired() { + ArtifactId a = new ArtifactId("demo-artifact"); + + assertThatThrownBy(() -> new DependencyCoordinates(null, a)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.coordinates.not.blank"); + }); + } + + @Test + @DisplayName("null artifactId should fail COORDINATES_REQUIRED") + void nullArtifactId_shouldFailCoordinatesRequired() { + GroupId g = new GroupId("org.acme"); + + assertThatThrownBy(() -> new DependencyCoordinates(g, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.coordinates.not.blank"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyTest.java new file mode 100644 index 0000000..22c5ea3 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyTest.java @@ -0,0 +1,71 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId; +import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class DependencyTest { + + private static DependencyCoordinates coords(String groupId, String artifactId) { + return new DependencyCoordinates(new GroupId(groupId), new ArtifactId(artifactId)); + } + + @Test + @DisplayName("non-null coordinates should be accepted") + void nonNullCoordinates_shouldBeAccepted() { + DependencyCoordinates coords = coords("org.acme", "demo"); + Dependency d = new Dependency(coords, new DependencyVersion("1.0.0"), DependencyScope.RUNTIME); + + assertThat(d.coordinates()).isSameAs(coords); + assertThat(d.version().value()).isEqualTo("1.0.0"); + assertThat(d.scope()).isEqualTo(DependencyScope.RUNTIME); + } + + @Test + @DisplayName("null coordinates should fail COORDINATES_REQUIRED") + void nullCoordinates_shouldFailCoordinatesRequired() { + assertThatThrownBy(() -> new Dependency(null, null, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.coordinates.not.blank"); + }); + } + + @Test + @DisplayName("isDefaultScope should be true when scope is null") + void isDefaultScope_shouldBeTrueWhenScopeIsNull() { + Dependency d = new Dependency(coords("org.acme", "demo"), new DependencyVersion("1.0.0"), null); + + assertThat(d.isDefaultScope()).isTrue(); + } + + @Test + @DisplayName("isDefaultScope should be true when scope is COMPILE") + void isDefaultScope_shouldBeTrueWhenScopeIsCompile() { + Dependency d = + new Dependency( + coords("org.acme", "demo"), new DependencyVersion("1.0.0"), DependencyScope.COMPILE); + + assertThat(d.isDefaultScope()).isTrue(); + } + + @Test + @DisplayName("isDefaultScope should be false when scope is not COMPILE") + void isDefaultScope_shouldBeFalseWhenScopeIsNotCompile() { + Dependency d = + new Dependency( + coords("org.acme", "demo"), new DependencyVersion("1.0.0"), DependencyScope.RUNTIME); + + assertThat(d.isDefaultScope()).isFalse(); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersionTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersionTest.java new file mode 100644 index 0000000..34a5f10 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersionTest.java @@ -0,0 +1,87 @@ +package io.github.blueprintplatform.codegen.domain.model.value.dependency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class DependencyVersionTest { + + @Test + @DisplayName("valid version should be accepted as-is") + void validVersion_shouldBeAccepted() { + DependencyVersion v = new DependencyVersion("1.2.3"); + assertThat(v.value()).isEqualTo("1.2.3"); + } + + @Test + @DisplayName("version with surrounding spaces should be trimmed") + void versionWithSpaces_shouldBeTrimmed() { + DependencyVersion v = new DependencyVersion(" 1.2.3-SNAPSHOT "); + assertThat(v.value()).isEqualTo("1.2.3-SNAPSHOT"); + } + + @Test + @DisplayName("null version should fail NOT_BLANK rule") + void nullVersion_shouldFailNotBlankRule() { + assertThatThrownBy(() -> new DependencyVersion(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.version.not.blank"); + }); + } + + @Test + @DisplayName("blank version should fail NOT_BLANK rule") + void blankVersion_shouldFailNotBlankRule() { + assertThatThrownBy(() -> new DependencyVersion(" ")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.version.not.blank"); + }); + } + + @Test + @DisplayName("too long version should fail LENGTH rule") + void tooLongVersion_shouldFailLengthRule() { + String longValue = "a".repeat(101); + + assertThatThrownBy(() -> new DependencyVersion(longValue)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.version.length"); + }); + } + + @Test + @DisplayName("version with invalid chars should fail INVALID_CHARS rule") + void invalidChars_shouldFailInvalidCharsRule() { + String bad = "1.0.0!final"; + + assertThatThrownBy(() -> new DependencyVersion(bad)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("dependency.version.invalid.chars"); + }); + } + + @Test + @DisplayName("version with allowed special chars should be accepted") + void allowedSpecialChars_shouldBeAccepted() { + DependencyVersion v = new DependencyVersion("1.0.0-RC1+[classifier]_extra(1),{meta}:$var"); + assertThat(v.value()).isEqualTo("1.0.0-RC1+[classifier]_extra(1),{meta}:$var"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactIdTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactIdTest.java new file mode 100644 index 0000000..44534e3 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactIdTest.java @@ -0,0 +1,102 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class ArtifactIdTest { + + @Test + @DisplayName("valid raw value should be normalized and accepted") + void validValue_shouldNormalizeAndAccept() { + ArtifactId id = new ArtifactId(" My_Artifact Id "); + + assertThat(id.value()).isEqualTo("my-artifact-id"); + } + + @Test + @DisplayName("null should throw NOT_BLANK violation with correct message key") + void nullValue_shouldThrowNotBlank() { + assertThatThrownBy(() -> new ArtifactId(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.not.blank"); + }); + } + + @Test + @DisplayName("too short value should fail LENGTH rule") + void tooShort_shouldFailLengthRule() { + assertThatThrownBy(() -> new ArtifactId("ab")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.length"); + }); + } + + @Test + @DisplayName("value with invalid characters should fail INVALID_CHARS rule") + void invalidCharacters_shouldFailInvalidCharsRule() { + assertThatThrownBy(() -> new ArtifactId("my$app")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.invalid.chars"); + }); + } + + @Test + @DisplayName("value starting with non letter should fail STARTS_WITH_LETTER rule") + void startsWithNonLetter_shouldFailStartsWithLetterRule() { + assertThatThrownBy(() -> new ArtifactId("1artifact")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.starts.with.letter"); + }); + } + + @Test + @DisplayName("leading dash should fail STARTS_WITH_LETTER rule") + void leadingDash_shouldFailStartsWithLetterRule() { + assertThatThrownBy(() -> new ArtifactId("-artifact")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.starts.with.letter"); + }); + } + + @Test + @DisplayName("trailing dash should fail EDGE_CHAR rule") + void trailingDash_shouldFailEdgeCharRule() { + assertThatThrownBy(() -> new ArtifactId("artifact-")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.artifact-id.edge.char"); + }); + } + + @Test + @DisplayName("multiple consecutive dashes are normalized to a single dash") + void consecutiveDashes_areNormalizedToSingleDash() { + ArtifactId id = new ArtifactId("my--artifact---id"); + + assertThat(id.value()).isEqualTo("my-artifact-id"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupIdTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupIdTest.java new file mode 100644 index 0000000..df0780c --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupIdTest.java @@ -0,0 +1,70 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class GroupIdTest { + + @Test + @DisplayName("valid raw value should be normalized and accepted") + void validValue_shouldNormalizeAndAccept() { + GroupId groupId = new GroupId(" Com.Example.App "); + + assertThat(groupId.value()).isEqualTo("com.example.app"); + } + + @Test + @DisplayName("null should throw NOT_BLANK violation with correct message key") + void nullValue_shouldThrowNotBlank() { + assertThatThrownBy(() -> new GroupId(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.group-id.not.blank"); + }); + } + + @Test + @DisplayName("too short value should fail LENGTH rule") + void tooShort_shouldFailLengthRule() { + assertThatThrownBy(() -> new GroupId("ab")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.group-id.length"); + }); + } + + @Test + @DisplayName("value with invalid segment format should fail SEGMENT_FORMAT rule") + void invalidSegmentFormat_shouldFailSegmentFormatRule() { + assertThatThrownBy(() -> new GroupId("com..example")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.group-id.segment.format"); + }); + } + + @Test + @DisplayName("segment starting with non-letter should fail SEGMENT_FORMAT rule") + void segmentStartingWithNonLetter_shouldFailSegmentFormatRule() { + assertThatThrownBy(() -> new GroupId("com.1example")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.group-id.segment.format"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentityTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentityTest.java new file mode 100644 index 0000000..6b670dc --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentityTest.java @@ -0,0 +1,54 @@ +package io.github.blueprintplatform.codegen.domain.model.value.identity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class ProjectIdentityTest { + + @Test + @DisplayName("valid groupId and artifactId should be accepted") + void validIdentity_shouldBeAccepted() { + GroupId groupId = new GroupId("io.github.blueprintplatform"); + ArtifactId artifactId = new ArtifactId("demo-app"); + + ProjectIdentity identity = new ProjectIdentity(groupId, artifactId); + + assertThat(identity.groupId()).isSameAs(groupId); + assertThat(identity.artifactId()).isSameAs(artifactId); + } + + @Test + @DisplayName("null groupId should fail IDENTITY_REQUIRED") + void nullGroupId_shouldFailIdentityRequired() { + ArtifactId artifactId = new ArtifactId("demo-app"); + + assertThatThrownBy(() -> new ProjectIdentity(null, artifactId)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.identity.not.blank"); + }); + } + + @Test + @DisplayName("null artifactId should fail IDENTITY_REQUIRED") + void nullArtifactId_shouldFailIdentityRequired() { + GroupId groupId = new GroupId("io.github.blueprintplatform"); + + assertThatThrownBy(() -> new ProjectIdentity(groupId, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.identity.not.blank"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescriptionTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescriptionTest.java new file mode 100644 index 0000000..27495e5 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescriptionTest.java @@ -0,0 +1,90 @@ +package io.github.blueprintplatform.codegen.domain.model.value.naming; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class ProjectDescriptionTest { + + @Test + @DisplayName("valid description should be normalized and accepted") + void validDescription_shouldNormalizeAndAccept() { + ProjectDescription desc = new ProjectDescription(" This is a test "); + + assertThat(desc.value()).isEqualTo("This is a test"); + assertThat(desc.isEmpty()).isFalse(); + } + + @Test + @DisplayName("null description should fail NOT_BLANK rule") + void nullDescription_shouldFailNotBlankRule() { + assertThatThrownBy(() -> new ProjectDescription(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.description.not.blank"); + }); + } + + @Test + @DisplayName("blank description should fail NOT_BLANK rule") + void blankDescription_shouldFailNotBlankRule() { + assertThatThrownBy(() -> new ProjectDescription(" ")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.description.not.blank"); + }); + } + + @Test + @DisplayName("too short description should fail LENGTH rule") + void tooShortDescription_shouldFailLengthRule() { + // 9 karakter, MIN = 10 altında + String shortText = "too short"; + + assertThatThrownBy(() -> new ProjectDescription(shortText)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.description.length"); + }); + } + + @Test + @DisplayName("too long description should fail LENGTH rule") + void tooLongDescription_shouldFailLengthRule() { + String longText = "a".repeat(281); + + assertThatThrownBy(() -> new ProjectDescription(longText)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.description.length"); + }); + } + + @Test + @DisplayName("description with control chars should fail CONTROL_CHARS rule") + void controlChars_shouldFailInvalidCharsRule() { + String bad = "valid" + '\u0001' + "text"; + + assertThatThrownBy(() -> new ProjectDescription(bad)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.description.control.chars"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectNameTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectNameTest.java new file mode 100644 index 0000000..8661895 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectNameTest.java @@ -0,0 +1,80 @@ +package io.github.blueprintplatform.codegen.domain.model.value.naming; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class ProjectNameTest { + + @Test + @DisplayName("valid raw value should be trimmed and accepted as-is") + void validValue_shouldTrimAndAccept() { + ProjectName name = new ProjectName(" My Project_Name "); + + assertThat(name.value()).isEqualTo("My Project_Name"); + } + + @Test + @DisplayName("null should throw NOT_BLANK violation with correct message key") + void nullValue_shouldThrowNotBlank() { + assertThatThrownBy(() -> new ProjectName(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.name.not.blank"); + }); + } + + @Test + @DisplayName("too short value should fail LENGTH rule") + void tooShort_shouldFailLengthRule() { + assertThatThrownBy(() -> new ProjectName("ab")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.name.length"); + }); + } + + @Test + @DisplayName("too long value should fail LENGTH rule") + void tooLong_shouldFailLengthRule() { + String longName = "A".repeat(61); // MAX = 60 + + assertThatThrownBy(() -> new ProjectName(longName)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.name.length"); + }); + } + + @Test + @DisplayName("value with invalid characters should fail INVALID_CHARS rule") + void invalidCharacters_shouldFailInvalidCharsRule() { + assertThatThrownBy(() -> new ProjectName("my$app")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.name.invalid.chars"); + }); + } + + @Test + @DisplayName("value with allowed punctuation should be accepted") + void allowedPunctuation_shouldBeAccepted() { + ProjectName name = new ProjectName("My Project, v1.0 (LTS)_alpha"); + + assertThat(name.value()).isEqualTo("My Project, v1.0 (LTS)_alpha"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageNameTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageNameTest.java new file mode 100644 index 0000000..7933a06 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageNameTest.java @@ -0,0 +1,106 @@ +package io.github.blueprintplatform.codegen.domain.model.value.pkg; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class PackageNameTest { + + @Test + @DisplayName("valid raw value should be normalized and accepted") + void validValue_shouldNormalizeAndAccept() { + PackageName pkg = new PackageName(" Com_Example-Api "); + + assertThat(pkg.value()).isEqualTo("com.example.api"); + } + + @Test + @DisplayName("null should throw NOT_BLANK violation with correct message key") + void nullValue_shouldThrowNotBlank() { + assertThatThrownBy(() -> new PackageName(null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.not.blank"); + }); + } + + @Test + @DisplayName("blank or only separators should throw NOT_BLANK after normalization") + void blankOrOnlySeparators_shouldThrowNotBlankAfterNormalization() { + assertThatThrownBy(() -> new PackageName(" ")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.not.blank"); + }); + + assertThatThrownBy(() -> new PackageName(" - _ - ")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.not.blank"); + }); + } + + @Test + @DisplayName("too short normalized value should fail LENGTH rule") + void tooShort_shouldFailLengthRule() { + assertThatThrownBy(() -> new PackageName("ab")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.length"); + }); + } + + @Test + @DisplayName("segment with invalid format should fail SEGMENT_FORMAT rule") + void invalidSegmentFormat_shouldFailSegmentFormatRule() { + assertThatThrownBy(() -> new PackageName("com.1example")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.segment.format"); + }); + } + + @Test + @DisplayName("reserved prefixes (java, javax, sun, com.sun) should fail RESERVED_PREFIX rule") + void reservedPrefix_shouldFailReservedPrefixRule() { + assertThatThrownBy(() -> new PackageName("java.util")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.reserved.prefix"); + }); + + assertThatThrownBy(() -> new PackageName("javax.mail")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.reserved.prefix"); + }); + + assertThatThrownBy(() -> new PackageName("com.sun.tools")) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.package-name.reserved.prefix"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTargetTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTargetTest.java new file mode 100644 index 0000000..82b53d8 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTargetTest.java @@ -0,0 +1,62 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.platform; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class PlatformTargetTest { + + @Test + @DisplayName("valid java and springBoot should be accepted") + void validTarget_shouldBeAccepted() { + SpringBootJvmTarget target = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_4); + + assertThat(target.java()).isEqualTo(JavaVersion.JAVA_21); + assertThat(target.springBoot()).isEqualTo(SpringBootVersion.V3_4); + assertThat(target.java().asString()).isEqualTo("21"); + assertThat(target.springBoot().defaultVersion()).isEqualTo("3.4.12"); + } + + @Test + @DisplayName("null java should fail TARGET_REQUIRED") + void nullJava_shouldFailTargetRequired() { + assertThatThrownBy(() -> new SpringBootJvmTarget(null, SpringBootVersion.V3_4)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("platform.target.not.blank"); + }); + } + + @Test + @DisplayName("null springBoot should fail TARGET_REQUIRED") + void nullSpringBoot_shouldFailTargetRequired() { + assertThatThrownBy(() -> new SpringBootJvmTarget(JavaVersion.JAVA_21, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("platform.target.not.blank"); + }); + } + + @Test + @DisplayName("null java and springBoot should fail TARGET_REQUIRED") + void nullJavaAndSpringBoot_shouldFailTargetRequired() { + assertThatThrownBy(() -> new SpringBootJvmTarget(null, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("platform.target.not.blank"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStackTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStackTest.java new file mode 100644 index 0000000..77d56f7 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStackTest.java @@ -0,0 +1,60 @@ +package io.github.blueprintplatform.codegen.domain.model.value.tech.stack; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class TechStackTest { + + @Test + @DisplayName("valid framework, buildTool and language should be accepted") + void validOptions_shouldBeAccepted() { + TechStack options = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + + assertThat(options.framework()).isEqualTo(Framework.SPRING_BOOT); + assertThat(options.buildTool()).isEqualTo(BuildTool.MAVEN); + assertThat(options.language()).isEqualTo(Language.JAVA); + } + + @Test + @DisplayName("null framework should fail TECH_STACK_REQUIRED") + void nullFramework_shouldFailTechStackRequired() { + assertThatThrownBy(() -> new TechStack(null, BuildTool.MAVEN, Language.JAVA)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.tech-stack.not.blank"); + }); + } + + @Test + @DisplayName("null buildTool should fail TECH_STACK_REQUIRED") + void nullBuildTool_shouldFailTechStackRequired() { + assertThatThrownBy(() -> new TechStack(Framework.SPRING_BOOT, null, Language.JAVA)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.tech-stack.not.blank"); + }); + } + + @Test + @DisplayName("null language should fail TECH_STACK_REQUIRED") + void nullLanguage_shouldFailTechStackRequired() { + assertThatThrownBy(() -> new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, null)) + .isInstanceOf(DomainViolationException.class) + .satisfies( + ex -> { + DomainViolationException dve = (DomainViolationException) ex; + assertThat(dve.getMessageKey()).isEqualTo("project.tech-stack.not.blank"); + }); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicyTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicyTest.java new file mode 100644 index 0000000..f71586a --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicyTest.java @@ -0,0 +1,108 @@ +package io.github.blueprintplatform.codegen.domain.policy.tech; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class CompatibilityPolicyTest { + + private static TechStack techStack() { + return new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + } + + private static PlatformTarget target(JavaVersion java, SpringBootVersion boot) { + return new SpringBootJvmTarget(java, boot); + } + + @Test + @DisplayName("ensureCompatible should fail when techStack or target is null") + @SuppressWarnings("DataFlowIssue") + void ensureCompatible_nullTechStackOrTarget_shouldFailTargetMissing() { + TechStack stack = techStack(); + PlatformTarget target = target(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + assertThatThrownBy(() -> CompatibilityPolicy.ensureCompatible(null, target)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.missing")); + + assertThatThrownBy(() -> CompatibilityPolicy.ensureCompatible(stack, null)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.missing")); + + assertThatThrownBy(() -> CompatibilityPolicy.ensureCompatible(null, null)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.missing")); + } + + @Test + @DisplayName("ensureCompatible should accept all supported Spring Boot / Java combinations") + void ensureCompatible_supportedTargets_shouldPass() { + TechStack stack = techStack(); + + assertThatCode( + () -> + CompatibilityPolicy.ensureCompatible( + stack, target(JavaVersion.JAVA_21, SpringBootVersion.V3_5))) + .doesNotThrowAnyException(); + + assertThatCode( + () -> + CompatibilityPolicy.ensureCompatible( + stack, target(JavaVersion.JAVA_25, SpringBootVersion.V3_5))) + .doesNotThrowAnyException(); + + assertThatCode( + () -> + CompatibilityPolicy.ensureCompatible( + stack, target(JavaVersion.JAVA_21, SpringBootVersion.V3_4))) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("ensureCompatible should fail for incompatible Spring Boot / Java combinations") + void ensureCompatible_incompatibleTarget_shouldFail() { + TechStack stack = techStack(); + PlatformTarget incompatible = target(JavaVersion.JAVA_25, SpringBootVersion.V3_4); + + assertThatThrownBy(() -> CompatibilityPolicy.ensureCompatible(stack, incompatible)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> { + assertThat(dve.getMessageKey()).isEqualTo("platform.target.incompatible"); + assertThat(dve.getArgs()) + .containsExactly( + SpringBootVersion.V3_4.defaultVersion(), JavaVersion.JAVA_25.asString()); + }); + } + + @Test + @DisplayName("allSupportedTargets should return all combinations defined in the matrix") + void allSupportedTargets_shouldReturnAllMatrixCombinations() { + List targets = CompatibilityPolicy.allSupportedTargets(); + + assertThat(targets) + .containsExactlyInAnyOrder( + target(JavaVersion.JAVA_21, SpringBootVersion.V3_4), + target(JavaVersion.JAVA_21, SpringBootVersion.V3_5), + target(JavaVersion.JAVA_25, SpringBootVersion.V3_5)); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelectorTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelectorTest.java new file mode 100644 index 0000000..0bbff21 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelectorTest.java @@ -0,0 +1,65 @@ +package io.github.blueprintplatform.codegen.domain.policy.tech; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget; +import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language; +import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class PlatformTargetSelectorTest { + + private static TechStack techStack() { + return new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + } + + @Test + @DisplayName("select with compatible target should return PlatformTarget") + void select_compatibleTarget_shouldReturnRequestedTarget() { + TechStack stack = techStack(); + + PlatformTarget result = + PlatformTargetSelector.select(stack, JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + assertThat(result) + .isInstanceOf(SpringBootJvmTarget.class) + .extracting("java") + .isEqualTo(JavaVersion.JAVA_21); + + assertThat(result).extracting("springBoot").isEqualTo(SpringBootVersion.V3_5); + } + + @Test + @DisplayName("select with incompatible target should delegate to CompatibilityPolicy and fail") + void select_incompatibleTarget_shouldFailCompatibility() { + TechStack stack = techStack(); + + assertThatThrownBy( + () -> PlatformTargetSelector.select(stack, JavaVersion.JAVA_25, SpringBootVersion.V3_4)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("platform.target.incompatible")); + } + + @Test + @DisplayName("supportedTargetsFor should return all supported targets from CompatibilityPolicy") + void supportedTargetsFor_shouldReturnAllSupportedTargets() { + List targets = PlatformTargetSelector.supportedTargetsFor(); + + assertThat(targets) + .isNotEmpty() + .contains(new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5)); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFileTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFileTest.java new file mode 100644 index 0000000..4ad8774 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedFileTest.java @@ -0,0 +1,89 @@ +package io.github.blueprintplatform.codegen.domain.port.out.artifact; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException; +import java.nio.file.Path; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +@Tag("domain") +class GeneratedFileTest { + + @Test + @DisplayName("Text with valid args should be created successfully") + void text_validArgs_shouldCreateInstance() { + GeneratedFile.Text text = new GeneratedFile.Text(Path.of("pom.xml"), "", UTF_8); + + assertThat(text.relativePath()).isEqualTo(Path.of("pom.xml")); + assertThat(text.content()).isEqualTo(""); + assertThat(text.charset()).isEqualTo(UTF_8); + } + + @Test + @DisplayName("Text with null path should fail with file.path.not.blank") + void text_nullPath_shouldFailPathNotBlank() { + assertThatThrownBy(() -> new GeneratedFile.Text(null, "x", UTF_8)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("file.path.not.blank")); + } + + @Test + @DisplayName("Text with null content should fail with file.content.not.blank") + void text_nullContent_shouldFailContentNotBlank() { + assertThatThrownBy(() -> new GeneratedFile.Text(Path.of("pom.xml"), null, UTF_8)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("file.content.not.blank")); + } + + @Test + @DisplayName("Binary should defensively copy bytes in ctor and accessor") + void binary_shouldDefensivelyCopyBytes() { + byte[] original = new byte[] {1, 2, 3}; + GeneratedFile.Binary binary = new GeneratedFile.Binary(Path.of("bin.dat"), original); + + original[0] = 9; + + byte[] fromGetter = binary.bytes(); + assertThat(fromGetter).containsExactly(1, 2, 3); + + fromGetter[1] = 8; + + byte[] fromGetterAgain = binary.bytes(); + assertThat(fromGetterAgain).containsExactly(1, 2, 3); + } + + @Test + @DisplayName("Binary with null path should fail with file.path.not.blank") + void binary_nullPath_shouldFailPathNotBlank() { + assertThatThrownBy(() -> new GeneratedFile.Binary(null, new byte[] {1})) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("file.path.not.blank")); + } + + @Test + @DisplayName("Binary with null bytes should fail with file.content.not.blank") + void binary_nullBytes_shouldFailContentNotBlank() { + assertThatThrownBy(() -> new GeneratedFile.Binary(Path.of("bin.dat"), null)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("file.content.not.blank")); + } + + @Test + @DisplayName("Binary equals/hashCode should depend on path and bytes") + void binary_equalsAndHashCode_shouldDependOnPathAndBytes() { + GeneratedFile.Binary b1 = new GeneratedFile.Binary(Path.of("bin.dat"), new byte[] {1, 2, 3}); + GeneratedFile.Binary b2 = new GeneratedFile.Binary(Path.of("bin.dat"), new byte[] {1, 2, 3}); + GeneratedFile.Binary b3 = new GeneratedFile.Binary(Path.of("other.bin"), new byte[] {1, 2, 3}); + + assertThat(b1).isEqualTo(b2).hasSameHashCodeAs(b2).isNotEqualTo(b3); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/testsupport/build/RecordingPomDependencyMapper.java b/src/test/java/io/github/blueprintplatform/codegen/testsupport/build/RecordingPomDependencyMapper.java new file mode 100644 index 0000000..dfcdac5 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/testsupport/build/RecordingPomDependencyMapper.java @@ -0,0 +1,22 @@ +package io.github.blueprintplatform.codegen.testsupport.build; + +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency; +import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper; +import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies; +import java.util.List; + +public final class RecordingPomDependencyMapper extends PomDependencyMapper { + + private final List toReturn; + public Dependencies capturedDependencies; + + public RecordingPomDependencyMapper(List toReturn) { + this.toReturn = toReturn; + } + + @Override + public List from(Dependencies dependencies) { + this.capturedDependencies = dependencies; + return toReturn; + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/CapturingTemplateRenderer.java b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/CapturingTemplateRenderer.java new file mode 100644 index 0000000..bc0d079 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/CapturingTemplateRenderer.java @@ -0,0 +1,22 @@ +package io.github.blueprintplatform.codegen.testsupport.templating; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.file.Path; +import java.util.Map; + +public final class CapturingTemplateRenderer implements TemplateRenderer { + + public Path capturedOutPath; + public String capturedTemplateName; + public Map capturedModel; + public GeneratedFile nextFile; + + @Override + public GeneratedFile renderUtf8(Path outPath, String templateName, Map model) { + this.capturedOutPath = outPath; + this.capturedTemplateName = templateName; + this.capturedModel = model; + return nextFile; + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/NoopTemplateRenderer.java b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/NoopTemplateRenderer.java new file mode 100644 index 0000000..61a639d --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/NoopTemplateRenderer.java @@ -0,0 +1,15 @@ +package io.github.blueprintplatform.codegen.testsupport.templating; + +import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedFile; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; + +public final class NoopTemplateRenderer implements TemplateRenderer { + + @Override + public GeneratedFile renderUtf8(Path outPath, String templateName, Map model) { + return new GeneratedFile.Text(outPath, "", StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGeneratorTest.java deleted file mode 100644 index e644e8d..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGeneratorTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; - -import io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating.FreeMarkerTemplateEngine; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class GitIgnoreFileGeneratorTest { - - private static final String GITIGNORE_FILE_NAME = ".gitignore"; - - @Autowired private GitIgnoreFileGenerator gitIgnoreFileGenerator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateGitIgnoreContent_CreatesCorrectFileWithEmptyIgnoreList() throws IOException { - gitIgnoreFileGenerator.generateGitIgnoreContent(tempFolder.toFile(), Collections.emptyList()); - - Path generatedFile = tempFolder.resolve(GITIGNORE_FILE_NAME); - assertTrue(Files.exists(generatedFile)); - String content = Files.readString(generatedFile); - assertTrue(content.length() > 10); - } - - @Test - void testGenerateGitIgnoreContent_CreatesCorrectFileWithSomeIgnoreList() throws IOException { - List ignoreList = Arrays.asList("*.pyc", "__pycache__"); - gitIgnoreFileGenerator.generateGitIgnoreContent(tempFolder.toFile(), ignoreList); - - Path generatedFile = tempFolder.resolve(GITIGNORE_FILE_NAME); - assertTrue(Files.exists(generatedFile)); - String content = Files.readString(generatedFile); - assertTrue(content.contains("*.py")); - assertTrue(content.contains("__pycache__")); - } - - @Test - void testGenerateGitIgnoreContent_CreatesFileAndVerifiesContent() throws IOException { - File projectDestination = tempFolder.toFile(); - - List additionalIgnorePatterns = List.of("*.md"); - gitIgnoreFileGenerator.generateGitIgnoreContent(projectDestination, additionalIgnorePatterns); - - File generatedFile = new File(projectDestination, GITIGNORE_FILE_NAME); - assertTrue(generatedFile.exists(), "Generated file should be created!"); - - List gitIgnoreList = - Files.readAllLines(generatedFile.toPath()).stream() - .map(String::trim) - .filter(s -> !s.isBlank()) - .toList(); - - assertThat(gitIgnoreList, hasItems("*.com", "*.md", ".idea/", "target/", "generated-sources/")); - } - - @Test - void testGenerateGitIgnoreContent_ThrowsExceptionOnTemplateEngineError() throws Exception { - FreeMarkerTemplateEngine mockEngine = Mockito.mock(FreeMarkerTemplateEngine.class); - GitIgnoreFileGenerator gitIgnoreFileGeneratorLocal = new GitIgnoreFileGenerator(mockEngine); - - Mockito.doThrow(new IOException("Template processing error")) - .when(mockEngine) - .generateFileFromTemplate(any(), any(), any()); - - assertThrows( - IOException.class, - () -> gitIgnoreFileGeneratorLocal.generateGitIgnoreContent(tempFolder.toFile(), null)); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializerTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializerTest.java deleted file mode 100644 index e13631e..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializerTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ProjectRootDirectoryInitializerTest { - - @TempDir public Path tempFolder; - @Autowired private ProjectRootDirectoryInitializer initializer; - private Path projectDirectory; - - @Test - void testInitializeProjectDirectory_CreatesCorrectDirectoryWithProjectName() throws IOException { - String projectName = "springBootTest"; - - projectDirectory = initializer.initializeProjectDirectory(projectName); - - assertTrue(Files.isDirectory(projectDirectory), "Project directory should be created"); - - assertTrue( - projectDirectory.getFileName().toString().startsWith(projectName), - "Directory name should start with " + projectName); - } - - @Test - void testInitializeProjectDirectory_CreatesCorrectDirectoryWithProjectNameAndLocation() - throws IOException { - String projectName = "springBootTest"; - Path projectLocation = Path.of(tempFolder.toFile().toPath().toString() + "/resources/app"); - - Path projectFullPath = projectLocation.resolve(projectName); - - Path createdDir = initializer.initializeProjectDirectory(projectName, projectLocation); - - assertTrue(Files.isDirectory(createdDir), "Project directory should be created"); - - assertEquals(projectFullPath, createdDir, "Directory should be created at specified location"); - - String directoryName = createdDir.getFileName().toString(); - assertEquals(projectName, directoryName, "Directory name should include project name"); - } - - @Test - void testInitializeProjectDirectory_CreatesCorrectDirectoryWithNullProjectLocation() - throws IOException { - String projectName = "null-location-app"; - - projectDirectory = initializer.initializeProjectDirectory(projectName, null); - - assertTrue(Files.isDirectory(projectDirectory), "Project directory should be created"); - assertTrue( - projectDirectory.getFileName().toString().startsWith(projectName), - "Directory name should start with project name"); - } - - @Test - void testInitializeProjectDirectory_ShouldFailWhenDirectoryAlreadyExists() throws IOException { - String projectName = "codegen-demo"; - Path projectLocation = tempFolder.toFile().toPath(); - - Path projectDir = projectLocation.resolve(projectName); - Files.createDirectories(projectDir); // Create directories recursively - - try { - initializer.initializeProjectDirectory(projectName, projectLocation); - fail("Expected an exception for existing directory"); - } catch (FileAlreadyExistsException e) { - assertTrue(e.getMessage().contains("File already exists")); - } - } - - @AfterEach - void cleanup() throws IOException { - if (projectDirectory != null && Files.exists(projectDirectory)) { - FileUtils.deleteDirectory(projectDirectory.toFile()); - File parentFile = projectDirectory.getParent().toFile(); - if (parentFile.exists() && FileUtils.isEmptyDirectory(parentFile)) { - FileUtils.deleteDirectory(parentFile); - } - } - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiverTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiverTest.java deleted file mode 100644 index 8a73fa4..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiverTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.ArchiveInputStream; -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ZipProjectArchiverTest { - - @TempDir public Path tempFolder; - @Autowired private ZipProjectArchiver zipProjectArchiver; - - @Test - void testArchiveProject_CreatesArchiveFile() throws IOException { - String projectName = "codegen-demo"; - Path projectDir = tempFolder.resolve(projectName); - Files.createDirectories(projectDir); - - File testFile1 = tempFolder.resolve(projectDir.toString() + "/demo-01.txt").toFile(); - boolean created1 = testFile1.createNewFile(); - assertTrue(created1 || testFile1.exists(), "demo-01.txt could not be created"); - - File testFile2 = tempFolder.resolve(projectDir.toString() + "/folder-04/file-02.txt").toFile(); - File parent2 = testFile2.getParentFile(); - boolean mkParent2 = parent2.mkdirs(); - assertTrue(mkParent2 || parent2.exists(), "folder-04 could not be created"); - boolean created2 = testFile2.createNewFile(); - assertTrue(created2 || testFile2.exists(), "file-02.txt could not be created"); - - Path archivedProjectPath = zipProjectArchiver.archiveProject(projectDir.toFile(), projectName); - File archivedProjectFile = archivedProjectPath.toFile(); - assertTrue(archivedProjectFile.exists(), "Archive project file should be created"); - } - - @Test - void testArchiveProject_CreatesArchiveAndExtractsContent() throws IOException { - String projectName = "codegen-enterprise"; - Path projectDir = tempFolder.resolve(projectName); - Files.createDirectories(projectDir); - - String enterprise01FileName = "enterprise-01.txt"; - File testFile1 = - tempFolder.resolve(projectDir.toString() + "/" + enterprise01FileName).toFile(); - boolean created1 = testFile1.createNewFile(); - assertTrue(created1 || testFile1.exists(), enterprise01FileName + " could not be created"); - - File testFile2 = tempFolder.resolve(projectDir.toString() + "/folder-01/file-01.txt").toFile(); - File parent2 = testFile2.getParentFile(); - boolean mkParent2 = parent2.mkdirs(); - assertTrue(mkParent2 || parent2.exists(), "folder-01 could not be created"); - boolean created2 = testFile2.createNewFile(); - assertTrue(created2 || testFile2.exists(), "file-01.txt could not be created"); - - Path archivedProjectPath = zipProjectArchiver.archiveProject(projectDir.toFile(), projectName); - File archivedProjectFile = archivedProjectPath.toFile(); - assertTrue(archivedProjectFile.exists(), "Archive project file should be created"); - - File extractedDir = createExtractedProject(projectDir, archivedProjectFile); - assertTrue( - extractedDir.exists() && !FileUtils.isEmptyDirectory(extractedDir), - "Archived file was corrupted!"); - - String archivedProjectName = archivedProjectFile.getName().replace(".zip", ""); - File extractedProjectDir = new File(extractedDir, archivedProjectName); - String enterprise01FileNameFromArchived = "enterprise_01.txt"; - File enterprise01FileFromUnarchived = - new File(extractedProjectDir, enterprise01FileNameFromArchived); - assertTrue( - enterprise01FileFromUnarchived.exists(), - "Archived project file does not contain " + enterprise01FileNameFromArchived); - } - - @Test - void testArchiveProject_SuccessInEmptyDirectoryCreation() throws IOException { - String projectName = "codegen-demo-empty"; - Path projectDir = tempFolder.resolve(projectName); - Files.createDirectories(projectDir); - - Path archivedProjectPath = zipProjectArchiver.archiveProject(projectDir.toFile(), projectName); - File archivedProjectFile = archivedProjectPath.toFile(); - - assertTrue( - archivedProjectFile.exists(), - "Archived project file should be created even for empty directory"); - } - - @Test - void testArchiveProject_InvalidProjectFilePath_IOException() throws IOException { - String projectName = "codegen-demo"; - Path invalidProjectDir = Paths.get(tempFolder.toString(), "invalid/path"); - - try { - zipProjectArchiver.archiveProject(invalidProjectDir.toFile(), projectName); - fail("Expected an IOException for invalid project directory"); - } catch (IOException e) { - assertTrue( - e.getMessage() != null && e.getMessage().contains("No such file"), - "Unexpected exception message: " + e.getMessage()); - } - } - - @Test - void testArchiveProject_CorruptedFile_IOException() throws IOException { - String projectName = "codegen-demo"; - Path projectDir = tempFolder.resolve(projectName); - Files.createDirectories(projectDir); - - File testFile1 = tempFolder.resolve(projectDir.toString() + "/demo-01.txt").toFile(); - boolean created1 = testFile1.createNewFile(); - assertTrue(created1 || testFile1.exists(), "demo-01.txt could not be created"); - - File testFile2 = tempFolder.resolve(projectDir.toString() + "/folder-04/file-02.txt").toFile(); - File parent2 = testFile2.getParentFile(); - boolean mkParent2 = parent2.mkdirs(); - assertTrue(mkParent2 || parent2.exists(), "folder-04 could not be created"); - boolean created2 = testFile2.createNewFile(); - assertTrue(created2 || testFile2.exists(), "file-02.txt could not be created"); - - File corruptedFile = tempFolder.resolve(projectDir.toString() + "/corrupted-file.txt").toFile(); - boolean createdCorrupted = corruptedFile.createNewFile(); - assertTrue( - createdCorrupted || corruptedFile.exists(), "corrupted-file.txt could not be created"); - try (OutputStream corruptedOutputStream = new FileOutputStream(corruptedFile)) { - corruptedOutputStream.write("This is a corrupted file!".getBytes()); - corruptedOutputStream.write(new byte[] {1, 2, 3, -12}); - } - - // Try to remove permissions; accept success OR already-in-effect states. - boolean execUnset = corruptedFile.setExecutable(false, false); - assertTrue( - execUnset || !corruptedFile.canExecute(), - "Failed to unset execute permission on corrupted file"); - - boolean readUnset = corruptedFile.setReadable(false, false); - assertTrue( - readUnset || !corruptedFile.canRead(), "Failed to unset read permission on corrupted file"); - - boolean writeUnset = corruptedFile.setWritable(false, false); - assertTrue( - writeUnset || !corruptedFile.canWrite(), - "Failed to unset write permission on corrupted file"); - - try { - zipProjectArchiver.archiveProject(projectDir.toFile(), projectName); - fail("Expected an IOException for archiving corrupted file"); - } catch (IOException e) { - assertTrue( - e.getMessage() != null && e.getMessage().contains("Error processing file"), - "Unexpected exception message: " + e.getMessage()); - } - } - - private File createExtractedProject(Path projectDir, File archivedFile) throws IOException { - File extractedDir = tempFolder.resolve(projectDir.getParent() + "/unarchived").toFile(); - boolean mkExtracted = extractedDir.mkdirs(); - assertTrue(mkExtracted || extractedDir.exists(), "unarchived directory could not be created"); - - try (ArchiveInputStream inputStream = - new ZipArchiveInputStream(new FileInputStream(archivedFile))) { - ArchiveEntry entry; - while ((entry = inputStream.getNextEntry()) != null) { - if (entry.isDirectory()) { - File directory = new File(extractedDir, entry.getName()); - boolean mk = directory.mkdirs(); - assertTrue(mk || directory.exists(), "Failed to create directory: " + directory); - } else { - File file = new File(extractedDir, entry.getName()); - File parent = file.getParentFile(); - boolean mk = parent.mkdirs(); - assertTrue(mk || parent.exists(), "Failed to create parent directory: " + parent); - try (OutputStream outputStream = new FileOutputStream(file)) { - IOUtils.copy(inputStream, outputStream); - } - } - } - } - - return extractedDir; - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGeneratorTest.java deleted file mode 100644 index a4c770b..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGeneratorTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; - -import io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating.FreeMarkerTemplateEngine; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringBootApplicationYamlGeneratorTest { - - private static final String APPLICATION_YAML_FILE_NAME = "application.yml"; - private static final String EXPECTED_APPLICATION_NAME = "codegen-demo"; - private static final String SRC_MAIN_RESOURCES = "src/main/resources"; - - @Autowired private SpringBootApplicationYamlGenerator applicationYamlGenerator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateApplicationYaml_CreatesCorrectFileStructureAndFileName() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - applicationYamlGenerator.generateApplicationYaml(projectDestination, projectMetadata); - - File srcMainResourcesFileDestination = new File(projectDestination, SRC_MAIN_RESOURCES); - File generatedFile = new File(srcMainResourcesFileDestination, APPLICATION_YAML_FILE_NAME); - assertTrue(generatedFile.exists()); - - String content = Files.readString(generatedFile.toPath()); - assertTrue(content.length() > 10); - } - - @Test - void testGenerateApplicationYaml_CreatesFileAndVerifiesContent() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - applicationYamlGenerator.generateApplicationYaml(projectDestination, projectMetadata); - - File srcMainResourcesFileDestination = new File(projectDestination, SRC_MAIN_RESOURCES); - File generatedFile = new File(srcMainResourcesFileDestination, APPLICATION_YAML_FILE_NAME); - assertTrue(generatedFile.exists()); - - String yml = Files.readString(generatedFile.toPath()); - - assertTrue(yml.contains("spring:"), "YAML should contain 'spring:'"); - assertTrue(yml.contains("application:"), "YAML should contain 'application:'"); - assertTrue( - yml.contains("name: " + EXPECTED_APPLICATION_NAME), "YAML should set application name"); - } - - @Test - void testGenerateApplicationYaml_ThrowsExceptionOnTemplateEngineError() throws Exception { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - FreeMarkerTemplateEngine mockEngine = Mockito.mock(FreeMarkerTemplateEngine.class); - SpringBootApplicationYamlGenerator generatorLocal = - new SpringBootApplicationYamlGenerator(mockEngine); - - Mockito.doThrow(new IOException("Template processing error")) - .when(mockEngine) - .generateFileFromTemplate(any(), any(), any()); - - assertThrows( - IOException.class, - () -> generatorLocal.generateApplicationYaml(projectDestination, projectMetadata)); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGeneratorTest.java deleted file mode 100644 index af2622d..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGeneratorTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringBootJavaMainClassGeneratorTest { - - @Autowired private SpringBootJavaMainClassGenerator generator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateMainClass_CreatesMainClassFile() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - generator.generateProjectStarterClass(tempFolder.toFile(), projectMetadata); - - File expectedMainClassFile = - new File(tempFolder.toFile(), "src/main/java/com/codegen/core/CodegenDemoApplication.java"); - - assertTrue(expectedMainClassFile.exists(), "Main class file should be created"); - assertEquals( - "CodegenDemoApplication.java", - expectedMainClassFile.getName(), - "Main class file name should be CodegenDemoApplication.java"); - } - - @Test - void testGenerateMainClass_CreatesMainClassAndVerifiesContent() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - generator.generateProjectStarterClass(tempFolder.toFile(), projectMetadata); - - String generatedContent = - Files.readString( - new File( - tempFolder.toString(), - "src/main/java/com/codegen/core/CodegenDemoApplication.java") - .toPath()); - - assertTrue( - generatedContent.contains("@SpringBootApplication"), - "Generated content should contain @SpringBootApplication"); - - String expectedMainClassName = "CodegenDemoApplication"; - assertTrue( - generatedContent.contains(expectedMainClassName), - "Generated content should contain CodegenDemoApplication"); - - String expectedRunClassDefinition = "SpringApplication.run(CodegenDemoApplication.class, args)"; - assertTrue( - generatedContent.contains(expectedRunClassDefinition), - "Generated content should contain this line " + expectedRunClassDefinition); - } - - @Test - void testGenerateMainClass_CreatesMainClassWithSpecialCharsAndVerifiesContent() - throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo?*") - .packageName("com.codegen.core") - .build(); - - generator.generateProjectStarterClass(tempFolder.toFile(), projectMetadata); - - String generatedContent = - Files.readString( - new File( - tempFolder.toString(), - "src/main/java/com/codegen/core/CodegenDemoApplication.java") - .toPath()); - - assertTrue( - generatedContent.contains("@SpringBootApplication"), - "Generated content should contain @SpringBootApplication"); - - String expectedMainClassName = "CodegenDemoApplication"; - assertTrue( - generatedContent.contains(expectedMainClassName), - "Generated content should contain CodegenDemoApplication"); - - String expectedRunClassDefinition = "SpringApplication.run(CodegenDemoApplication.class, args)"; - assertTrue( - generatedContent.contains(expectedRunClassDefinition), - "Generated content should contain this line " + expectedRunClassDefinition); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGeneratorTest.java deleted file mode 100644 index 7ab42dc..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGeneratorTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringBootJavaTestClassGeneratorTest { - - @Autowired private SpringBootJavaTestClassGenerator generator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateTestClass_CreatesTestClassFile() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - generator.generateTestClass(tempFolder.toFile(), projectMetadata); - - File expectedTestClassFile = - new File( - tempFolder.toFile(), "src/test/java/com/codegen/core/CodegenDemoApplicationTests.java"); - - assertTrue(expectedTestClassFile.exists(), "Test class file should be created"); - assertEquals( - "CodegenDemoApplicationTests.java", - expectedTestClassFile.getName(), - "Test class file name should be CodegenDemoApplicationTests.java"); - } - - @Test - void testGenerateTestClass_CreatesTestClassAndVerifiesContent() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - generator.generateTestClass(tempFolder.toFile(), projectMetadata); - - String generatedContent = - Files.readString( - new File( - tempFolder.toString(), - "src/test/java/com/codegen/core/CodegenDemoApplicationTests.java") - .toPath()); - - assertTrue( - generatedContent.contains("@SpringBootTest"), - "Generated content should contain @SpringBootTest"); - - String expectedTestClassName = "CodegenDemoApplicationTests"; - assertTrue( - generatedContent.contains(expectedTestClassName), - "Generated content should contain CodegenDemoApplicationTests"); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGeneratorTest.java deleted file mode 100644 index 6afbb32..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGeneratorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringBootMavenJavaProjectBuildGeneratorTest { - - private static final String POM_FILE_NAME = "pom.xml"; - private static final String WRAPPER_FILE_DIR = ".mvn/wrapper"; - private static final String WRAPPER_FILE_NAME = "maven-wrapper.properties"; - private static final String WRAPPER_VERSION = "3.3.3"; - - @Autowired private SpringBootMavenJavaProjectBuildGenerator generator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateBuildConfiguration_CreatesPomFileAndWrapper() throws IOException { - Dependency dependencySpringBootStarterWeb = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); - - Dependency dependencySpringBootStarterTest = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); - - SpringBootJavaProjectMetadataBuilder projectMetadataBuilder = - new SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder(); - projectMetadataBuilder - .groupId("com.codegen") - .artifactId("codegen-initialzr") - .name("codegen-initialzr") - .description("Codegen Initialzr") - .packageName("com.codegen.initialzr") - .dependencies(List.of(dependencySpringBootStarterWeb, dependencySpringBootStarterTest)); - - SpringBootJavaProjectMetadata springBootJavaProjectMetadata = - projectMetadataBuilder.springBootVersion("3.5.5").javaVersion("21").build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateBuildConfiguration(projectDestination, springBootJavaProjectMetadata); - - File pomFile = new File(projectDestination, POM_FILE_NAME); - assertTrue(pomFile.exists(), "pom.xml file should be created"); - - File wrapperFileDir = new File(projectDestination, WRAPPER_FILE_DIR); - assertTrue(wrapperFileDir.exists(), "Wrapper file directory should be created!"); - - File wrapperFile = new File(wrapperFileDir, WRAPPER_FILE_NAME); - assertTrue(wrapperFile.exists(), "Wrapper file maven-wrapper.properties should be created!"); - } - - @Test - void testGenerateBuildConfiguration_CreatesFileAndVerifiesContent() throws IOException { - Dependency dependencySpringBootStarterWeb = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); - - Dependency dependencySpringBootStarterTest = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); - - SpringBootJavaProjectMetadataBuilder projectMetadataBuilder = - new SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder(); - projectMetadataBuilder - .groupId("com.codegen") - .artifactId("codegen-initialzr") - .name("codegen-initialzr") - .description("Codegen Initialzr") - .packageName("com.codegen.initialzr") - .dependencies(List.of(dependencySpringBootStarterWeb, dependencySpringBootStarterTest)); - - String javaVersion = "21"; - String springBootVersion = "3.5.5"; - SpringBootJavaProjectMetadata springBootJavaProjectMetadata = - projectMetadataBuilder - .springBootVersion(springBootVersion) - .javaVersion(javaVersion) - .build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateBuildConfiguration(projectDestination, springBootJavaProjectMetadata); - - File pomFile = new File(projectDestination, POM_FILE_NAME); - String pomContent = Files.readString(pomFile.toPath()); - pomContent = pomContent.trim().replaceAll("\\s*", ""); - - String expectedJavaVersionLine = "" + javaVersion + ""; - assertTrue( - pomContent.contains(expectedJavaVersionLine), - "Generated content should contain " + expectedJavaVersionLine); - - String parentContent = - """ - - org.springframework.boot - spring-boot-starter-parent - 3.5.5 - - - """; - - parentContent = parentContent.trim().replaceAll("\\s*", ""); - assertTrue( - pomContent.contains(parentContent), "Generated content should contain " + parentContent); - - File wrapperFileDir = new File(projectDestination, WRAPPER_FILE_DIR); - File wrapperFile = new File(wrapperFileDir, WRAPPER_FILE_NAME); - assertTrue(wrapperFile.exists(), "Wrapper file should be created!"); - - List wrapperFileList = Files.readAllLines(wrapperFile.toPath()); - assertThat(wrapperFileList, hasItem("wrapperVersion=" + WRAPPER_VERSION)); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGeneratorTest.java deleted file mode 100644 index 2ba0e51..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGeneratorTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; - -import io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating.FreeMarkerTemplateEngine; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringBootMavenJavaReadMeGeneratorTest { - - private static final String README_FILE_NAME = "README.md"; - - @Autowired private SpringBootMavenJavaReadMeGenerator readMeGenerator; - - @TempDir private Path tempFolder; - - @Test - void testGenerateReadMe_CreatesCorrectFileStructureAndFileName() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - readMeGenerator.generateProjectDocument(projectDestination, projectMetadata); - - File generatedFile = new File(projectDestination, README_FILE_NAME); - assertTrue(generatedFile.exists()); - - String content = Files.readString(generatedFile.toPath()); - assertTrue(content.length() > 10); - } - - @Test - void testGenerateReadMe_CreatesFileAndVerifiesContent() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - readMeGenerator.generateProjectDocument(projectDestination, projectMetadata); - - File generatedFile = new File(projectDestination, README_FILE_NAME); - assertTrue(generatedFile.exists()); - - String generatedContent = - Files.readString(new File(tempFolder.toString(), README_FILE_NAME).toPath()); - - String expectedLineProjectInitialization = "Project Initialization"; - assertTrue( - generatedContent.contains(expectedLineProjectInitialization), - "Generated content should contain" + expectedLineProjectInitialization); - - String expectedLineProjectName = "codegen-demo"; - assertTrue( - generatedContent.contains(expectedLineProjectName), - "Generated content should contain" + expectedLineProjectName); - - String expectedLineDependencies = "Dependencies"; - assertTrue( - generatedContent.contains(expectedLineDependencies), - "Generated content should contain" + expectedLineDependencies); - } - - @Test - void testGenerateReadMe_ThrowsExceptionOnTemplateEngineError() throws Exception { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").build(); - - File projectDestination = tempFolder.toFile(); - - FreeMarkerTemplateEngine mockEngine = Mockito.mock(FreeMarkerTemplateEngine.class); - SpringBootMavenJavaReadMeGenerator generatorLocal = - new SpringBootMavenJavaReadMeGenerator(mockEngine); - - Mockito.doThrow(new IOException("Template processing error")) - .when(mockEngine) - .generateFileFromTemplate(any(), any(), any()); - - assertThrows( - IOException.class, - () -> generatorLocal.generateProjectDocument(projectDestination, projectMetadata)); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGeneratorTest.java deleted file mode 100644 index 9d4d134..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGeneratorTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MavenBuildWrapperGeneratorTest { - - private static final String WRAPPER_FILE_DIR = ".mvn/wrapper"; - private static final String WRAPPER_FILE_NAME = "maven-wrapper.properties"; - private static final String WRAPPER_VERSION = "3.3.3"; - @TempDir public Path tempFolder; - @Autowired private MavenBuildWrapperGenerator generator; - - @Test - void testGenerateBuildWrapper_CreatesWrapperFile() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateBuildWrapper(projectDestination, projectMetadata); - - File wrapperFileDir = new File(projectDestination, WRAPPER_FILE_DIR); - assertTrue(wrapperFileDir.exists(), "Wrapper file directory should be created!"); - - File wrapperFile = new File(wrapperFileDir, WRAPPER_FILE_NAME); - assertTrue(wrapperFile.exists(), "Wrapper file should be created!"); - } - - @Test - void testGenerateBuildWrapper_CreatesFileAndVerifiesContent() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.codegen.core") - .build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateBuildWrapper(projectDestination, projectMetadata); - - File wrapperFileDir = new File(projectDestination, WRAPPER_FILE_DIR); - File wrapperFile = new File(wrapperFileDir, WRAPPER_FILE_NAME); - assertTrue(wrapperFile.exists(), "Wrapper file should be created!"); - - List wrapperFileList = Files.readAllLines(wrapperFile.toPath()); - assertThat(wrapperFileList, hasItem("wrapperVersion=" + WRAPPER_VERSION)); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGeneratorTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGeneratorTest.java deleted file mode 100644 index 8dded14..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGeneratorTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MavenJavaProjectLayoutGeneratorTest { - - @TempDir public Path tempFolder; - @Autowired private MavenJavaProjectLayoutGenerator generator; - - @Test - void testGenerateProjectLayout_CreatesCorrectDirectoryAndPackageNames() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.example.demo") - .build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateProjectLayout(projectDestination, projectMetadata); - - List expectedDirectories = - List.of( - "src/main/java/com/example/demo", - "src/main/resources", - "src/test/java/com/example/demo", - "src/test/resources", - "src/gen/java/com/example/demo/codegen"); - - expectedDirectories.forEach( - dir -> { - File expectedDir = new File(projectDestination, dir); - assertTrue(expectedDir.exists(), "Directory " + dir + " was not created!"); - }); - } - - @Test - void testGenerateProjectLayout_CreatesProjectLayoutWithEmptyPackageName() throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder().name("codegen-demo").packageName("").build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateProjectLayout(projectDestination, projectMetadata); - - File expectedDir = new File(projectDestination, "src/main/java"); - assertTrue(expectedDir.exists(), "Main Java source directory was not created!"); - } - - @Test - void testGenerateProjectLayout_CreatesProjectLayoutWithSpecialCharInPackageName() - throws IOException { - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .name("codegen-demo") - .packageName("com.example-demo") - .build(); - - File projectDestination = tempFolder.toFile(); - - generator.generateProjectLayout(projectDestination, projectMetadata); - - String expectedDirName = "src/main/java/com/example_demo"; - File expectedDir = new File(projectDestination, expectedDirName); - assertTrue(expectedDir.exists(), "Directory with special character not created!"); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngineTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngineTest.java deleted file mode 100644 index 4a04d0f..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngineTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class FreeMarkerTemplateEngineTest { - - private static final String GITIGNORE_FILE_NAME = ".gitignore"; - - @Autowired FreeMarkerTemplateEngine freeMarkerTemplateEngine; - - @TempDir private Path tempFolder; - - @Test - void testGenerateFileFromTemplateWithTemplateType() throws IOException { - List emptyList = Collections.emptyList(); - Map gitIgnoreData = new HashMap<>(); - gitIgnoreData.put("ignoreList", emptyList); - - freeMarkerTemplateEngine.generateFileFromTemplate( - TemplateType.GITIGNORE, gitIgnoreData, tempFolder.toFile()); - Path generatedFile = tempFolder.resolve(GITIGNORE_FILE_NAME); - assertTrue(Files.exists(generatedFile)); - String content = Files.readString(generatedFile); - assertTrue(content.length() > 10); - } - - @Test - void testGenerateFileFromTemplate() throws IOException { - List emptyList = Collections.emptyList(); - Map gitIgnoreData = new HashMap<>(); - gitIgnoreData.put("ignoreList", emptyList); - - String templateFileName = TemplateType.GITIGNORE.getTemplateFileName(); - String fileName = TemplateType.GITIGNORE.getFileName(); - freeMarkerTemplateEngine.generateFileFromTemplate( - templateFileName, fileName, gitIgnoreData, tempFolder.toFile()); - Path generatedFile = tempFolder.resolve(GITIGNORE_FILE_NAME); - assertTrue(Files.exists(generatedFile)); - String content = Files.readString(generatedFile); - assertTrue(content.length() > 10); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfigurationTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfigurationTest.java deleted file mode 100644 index fb71f2c..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfigurationTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import freemarker.template.Configuration; -import freemarker.template.TemplateExceptionHandler; -import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.FreeMarkerTemplateProperties; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class FreeMarkerTemplateConfigurationTest { - - @Autowired private FreeMarkerTemplateConfiguration configuration; - - @Test - void testFreemarkerTemplateConfiguration_withValidProperties() - throws FreeMarkerConfigurationException { - Configuration freeMarkerConfiguration = - configuration.initializeFreeMarkerTemplateConfiguration(); - - assertThat(freeMarkerConfiguration.getTemplateExceptionHandler()) - .isEqualTo(TemplateExceptionHandler.RETHROW_HANDLER); - - assertThat(freeMarkerConfiguration.getDefaultEncoding()).isEqualTo("UTF-8"); - } - - @Test - void testFreemarkerTemplateConfiguration_withInvalidExceptionHandler_throwsException() { - FreeMarkerTemplateProperties testMockProperties = - new FreeMarkerTemplateProperties("UTF-8", "INVALID_HANDLER", "/templates"); - - FreeMarkerTemplateConfiguration freeMarkerTemplateConfigurationLocal = - new FreeMarkerTemplateConfiguration(testMockProperties); - - assertThrows( - FreeMarkerConfigurationException.class, - freeMarkerTemplateConfigurationLocal::initializeFreeMarkerTemplateConfiguration); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistryTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistryTest.java deleted file mode 100644 index 5fa72a7..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistryTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.registry; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.generator.springboot.maven.SpringBootMavenJavaProjectGenerator; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SimpleProjectGeneratorRegistryTest { - - @Autowired private SimpleProjectGeneratorRegistry registry; - - @Test - void testGetProjectGenerator_ExistingProjectType_ReturnsGenerator() { - ProjectType expectedProjectType = - new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); - - Optional generatorOptional = - registry.getProjectGenerator(expectedProjectType); - - assertTrue(generatorOptional.isPresent()); - - ProjectGenerator projectGenerator = generatorOptional.get(); - - assertSame(SpringBootMavenJavaProjectGenerator.class, projectGenerator.getClass()); - } - - @Test - void testGetProjectGenerator_NonExistingProjectType_ReturnsEmptyOptional() { - ProjectType unsupportedProjectType = - new ProjectType(Framework.QUARKUS, BuildTool.MAVEN, Language.JAVA); - - Optional generatorOptional = - registry.getProjectGenerator(unsupportedProjectType); - - assertFalse(generatorOptional.isPresent()); - } -} diff --git a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImplTest.java b/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImplTest.java deleted file mode 100644 index ada2506..0000000 --- a/src/test/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImplTest.java +++ /dev/null @@ -1,205 +0,0 @@ -package io.github.bsayli.codegen.initializr.projectgeneration.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata; -import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework; -import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.ArchiveInputStream; -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.util.CollectionUtils; - -@SpringBootTest -class ProjectGenerationServiceImplTest { - - private static final String DIRECTORY_NAME_UNARCHIVED = "unarchived"; - - @Autowired private ProjectGenerationService projectGenerationService; - - private Path archivedProjectPath; - - @Test - void testGenerateProject_SupportedProjectType_GeneratesProjectAndVerifiesContent() - throws IOException { - ProjectType springBootMavenJavaProjectType = - new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); - - Dependency dependencySpringBootStarterWeb = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); - - Dependency dependencySpringBootStarterTest = - new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); - - SpringBootJavaProjectMetadataBuilder projectMetadataBuilder = - new SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder(); - projectMetadataBuilder - .groupId("com.codegen") - .artifactId("codegen-demo") - .name("codegen-demo") - .description("Codegen Demo Project") - .packageName("com.codegen.demo") - .dependencies(List.of(dependencySpringBootStarterWeb, dependencySpringBootStarterTest)); - - SpringBootJavaProjectMetadata springBootJavaProjectMetadata = - projectMetadataBuilder.springBootVersion("3.5.5").javaVersion("21").build(); - - archivedProjectPath = - projectGenerationService.generateProject( - springBootMavenJavaProjectType, springBootJavaProjectMetadata); - File archivedProjectFile = archivedProjectPath.toFile(); - - assertTrue(archivedProjectFile.exists(), "Archive project file should be created"); - - File extractedDir = createExtractedProject(archivedProjectFile); - assertTrue( - extractedDir.exists() && !FileUtils.isEmptyDirectory(extractedDir), - "Archived file was corrupted!"); - - String archived = archivedProjectFile.getName().replace(".zip", ""); - File extractedProjectDir = new File(extractedDir, archived); - - String gitIgnoreFileName = ".gitignore"; - File gitIgnoreFileFromUnarchived = new File(extractedProjectDir, gitIgnoreFileName); - assertTrue( - gitIgnoreFileFromUnarchived.exists(), - "Archived project file does not contain " + gitIgnoreFileName + " file"); - - String pomFileName = "pom.xml"; - File pomFileFromUnarchived = new File(extractedProjectDir, pomFileName); - assertTrue( - pomFileFromUnarchived.exists(), - "Archived project file does not contain " + pomFileName + " file"); - } - - @Test - void testGenerateProject_UnsupportedProjectType_ThrowsException() throws IOException { - ProjectType quarkusMavenJavaProjectType = - new ProjectType(Framework.QUARKUS, BuildTool.MAVEN, Language.JAVA); - - ProjectMetadata projectMetadata = - new ProjectMetadata.ProjectMetadataBuilder() - .groupId("com.codegen") - .artifactId("codegen-demo") - .name("codegen-demo") - .description("Codegen Demo Project") - .packageName("com.codegen.demo") - .dependencies(Collections.emptyList()) - .build(); - - try { - archivedProjectPath = - projectGenerationService.generateProject(quarkusMavenJavaProjectType, projectMetadata); - fail("Expected exception for unsupported project type"); - } catch (IllegalArgumentException e) { - assertEquals("Unsupported project type: " + quarkusMavenJavaProjectType, e.getMessage()); - } - } - - @AfterEach - void cleanup() throws IOException { - if (archivedProjectPath != null && archivedProjectPath.toFile().exists()) { - Path parentPath = archivedProjectPath.getParent(); - if (parentPath != null && Files.exists(parentPath)) { - Collection files = FileUtils.listFiles(parentPath.toFile(), null, false); - List subfolderNames = getSubfolderNames(parentPath); - if (!CollectionUtils.isEmpty(files) && !CollectionUtils.isEmpty(subfolderNames)) { - boolean countSizeOk = files.size() == 1 && subfolderNames.size() == 2; - if (countSizeOk && subfolderNames.contains(DIRECTORY_NAME_UNARCHIVED)) { - FileUtils.deleteDirectory(parentPath.toFile()); - } - } - } - } - } - - private File createExtractedProject(File archivedProjectFile) throws IOException { - File extractedDir = new File(archivedProjectFile.getParentFile(), DIRECTORY_NAME_UNARCHIVED); - ensureDir(extractedDir, "Failed to create extracted dir: " + extractedDir); - - try (ArchiveInputStream inputStream = - new ZipArchiveInputStream(new FileInputStream(archivedProjectFile))) { - ArchiveEntry entry; - while ((entry = inputStream.getNextEntry()) != null) { - if (entry.isDirectory()) { - File directory = new File(extractedDir, entry.getName()); - ensureDir(directory, "Failed to create directory: " + directory); - } else { - File file = new File(extractedDir, entry.getName()); - File parent = file.getParentFile(); - ensureDir(parent, "Failed to create parent directory: " + parent); - try (OutputStream outputStream = new FileOutputStream(file)) { - IOUtils.copy(inputStream, outputStream); - } - } - } - } - - return extractedDir; - } - - private void ensureDir(File dir, String errorMessage) throws IOException { - if (dir == null) { - throw new IOException(errorMessage + " (null)"); - } - if (dir.exists()) { - if (!dir.isDirectory()) { - throw new IOException(errorMessage + " (exists but not a directory)"); - } - return; - } - boolean created = dir.mkdirs(); - if (!created && !dir.exists()) { - throw new IOException(errorMessage); - } - } - - private List getSubfolderNames(Path folderPath) { - List subfolderNames = new ArrayList<>(); - File folder = new File(folderPath.toString()); - if (!folder.isDirectory()) { - return subfolderNames; - } - File[] files = folder.listFiles(); - if (files == null) { - return subfolderNames; - } - for (File file : files) { - if (file.isDirectory()) { - subfolderNames.add(file.getName()); - } - } - return subfolderNames; - } -}