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..0a53552 100644 --- a/README.md +++ b/README.md @@ -1,216 +1,329 @@ -# 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 produce a folder structure and walk away. +Codegen Blueprint safeguards **architectural integrity**: -🔗 **https://github.com/blueprint-platform/codegen-blueprint/tree/refactor/hexagonal-architecture** +* Starts clean — no framework dependencies in the domain +* Stays clean — structure guides every evolution +* Prevents silent architecture drift -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. ---- +> **Executable Architecture — baked into the delivery pipeline.** + +
-## 🚀 Problem Statement +

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

-Bootstrapping a new Spring Boot project often involves: +--- -* 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 | Benefit | +| -------------------- | -------------------------------- | +| Platform Engineering | Org‑wide standardization | +| Lead Architect | Governance as Code | +| Developers | Clean architecture from day zero | +| New Team Members | Instant productivity | --- -## 💡 Solution +### 🥇 What makes it different? -This project automates all of that: +> **Initializr‑like simplicity** ➜ **Architecture‑first consistency** -* 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 +| Capability Focus | Spring Initializr & JHipster | Codegen Blueprint | +| --------------------------------- | ---------------------------- | ----------------- | +| Generates folder layout | ✔ | ✔ | +| Opinionated architecture defaults | ⚠️ | **✔** | +| Domain isolation by design | ❌ | **✔** | +| Profile‑driven evolution paths | ⚠️ | **✔** | +| Anti‑drift support (future‑ready) | ❌ | **✔** | + +> 🧭 Same starting point — **better long‑term alignment** --- -## ⚡ Quick Start +## 📑 Table of Contents + +* ⚡ [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) +* 🔔 [Sample Code & Greeting Example](#-sample-code--greeting-example) +* 🔌 [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) -### 1. Clone the Repository +--- -```bash -git clone https://github.com/bsayli/codegen-springboot-initializr.git -cd codegen-springboot-initializr -``` +## ⚡ What is Codegen Blueprint (Today)? -### 2. Build the Project +A **CLI‑driven**, **architecture‑aware** project generator. -```bash -mvn clean install -``` +📌 Current primary profile: **springboot‑maven‑java** +(✔ Spring Boot 3.5.x · ✔ Java 21 · ✔ Maven) -### 3. Run in CLI Mode +Produces a clean and predictable structure with: -```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" -``` +* Standardized identifiers (groupId, artifactId, package) +* Clear boundaries for maintainability +* Tests ready from day zero +* No 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 **opt‑in structured evolution path**. ``` -[OK] Project archive generated at: /.../target/generated-projects/demo-app/demo-app.zip +domain // business logic only (no Spring) +application // orchestrates ports +adapters // inbound & outbound +bootstrap // configuration & wiring ``` -ℹ️ Tips: +> “Framework‑free domain — intentional architecture from day zero.” -* 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. +

+ Hexagonal Architecture Overview +
+ Flow: Inputs ➜ Use Cases ➜ Domain Rules ➜ Artifacts ➜ Executable Service +

--- -## 🧑‍💻 Programmatic Usage +## 🧭 1.0.0 Scope & Status -```java -import java.nio.file.Path; -import java.util.List; +### Included in 1.0.0 -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; +| Feature | Status | +| -------------------------------------- | ------------------ | +| CLI‑based generation | ✔ Production‑ready | +| Hexagonal architecture layout (opt‑in) | ✔ Available | +| Spring Boot 3 / Java 21 / Maven | ✔ Supported | +| Main + test entrypoints | ✔ Provided | +| Required build + config artifacts | ✔ Generated | +| Greeting sample (optional sample‑code) | ✔ Included | +| MIT License | ✔ Open‑source | -// Assume ProjectGenerationService is injected or obtained from Spring context -ProjectGenerationService service = /* @Autowired or ApplicationContext.getBean(...) */; +### Up Next - var depWeb = new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-web") - .build(); +| Feature | Status | +|------------------------------------------------| ---------- | +| REST inbound adapter | Planned | +| Hexagonal evolution kit (ports + CQRS) | Planned | +| Additional profiles (Gradle, Kotlin, Quarkus) | Planned | +| Foundation libraries (`blueprint-*`) | Planned | +| Multi‑module services | Planned | +| Developer UI | Evaluating | - var depTest = new Dependency.DependencyBuilder() - .groupId("org.springframework.boot") - .artifactId("spring-boot-starter-test") - .scope("test") - .build(); +> ✔ Deep quality first → expand ecosystem next - 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(); +📌 For more details: +- [Executable Architecture Scope (1.0.0)](docs/architecture/executable-architecture-scope.md) - var type = new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); +--- - Path zip = service.generateProject(type, metadata); -System.out. +## 💡 Why This Project Matters - println("Archive generated at: "+zip.toAbsolutePath()); -``` +Clean architecture shouldn’t be optional. + +You gain: + +* ✔ Predictable structure & boundaries +* ✔ Testability from day zero +* ✔ Faster onboarding & team scaling +* ✔ Architectural governance without friction + +You avoid: + +* ❌ Copy‑paste architecture +* ❌ Each repo reinventing patterns +* ❌ Best‑practice rot over time +* ❌ Architecture drift + +📘 Explore design: +👉 [How to Explore This Project (Hexagonal Architecture Guide)](./docs/guides/how-to-explore-hexagonal-architecture.md) + +### Strategic Impact + +Architecture becomes **intentional — enforceable — repeatable**. --- -## 🖼 Demo Output +## 🔔 Sample Code & Greeting Example -Example of the generated project structure: +📌 Minimal but meaningful reference sample: -```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) +* Domain model: **Greeting** +* Use case: generate greeting text +* Inbound REST adapter: `/api/v1/sample/greetings/default` +* Hexagonal structure illustrates **port‑driven design** + +Enabled when flags include: + +``` +--layout hexagonal \ +--sample-code basic ``` +> Designed as a **teaching reference** and a **quick productivity boost** + --- -## 🛠 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: -This design allows the generator to evolve independently of specific tools while staying highly testable. +| Category | Includes | +| --------------- | -------------------------------------------------------- | +| Build system | `pom.xml`, Maven Wrapper | +| Runtime config | `src/main/resources/application.yml` | +| Source skeleton | Main application & test bootstraps | +| Sample code | Optional greeting sample (domain + ports + REST adapter) | +| Git hygiene | `.gitignore` | +| Docs (minimal) | `README.md` inside generated project | + +> Everything required to **build ▸ run ▸ extend** a clean service --- -## 🧪 Testing +## 🧪 Testing & CI -Run the test suite: +```bash +mvn verify +``` + +* Unit + integration tests ✔ +* JaCoCo coverage ✔ +* CodeQL security ✔ +* Codecov reporting ✔ + +--- + +## 🔄 CLI Usage Example ```bash -mvn test +java -jar codegen-blueprint-1.0.0.jar \ + --cli \ + springboot \ + --group-id io.github.blueprintplatform.samples \ + --artifact-id greeting-service \ + --name "Greeting Service" \ + --description "Hexagonal greeting sample powered by Blueprint" \ + --package-name io.github.blueprintplatform.samples.greeting \ + --layout hexagonal \ + --sample-code basic \ + --dependency web \ + --dependency data_jpa +``` + +**Output (simplified)** + +``` +greeting-service/ + ├── pom.xml + ├── src/main/java/io/github/blueprintplatform/samples/greeting/GreetingServiceApplication.java + ├── src/test/java/io/github/blueprintplatform/samples/greeting/GreetingServiceApplicationTests.java + ├── src/main/resources/application.yml + └── .gitignore ``` -The generator components (`pom.xml`, `.gitignore`, `application.yml`, layout, archiver) are fully covered with unit & -integration tests. +> Hexagonal + sample code = ready‑to‑run REST greeting service --- -## 📖 Related Work +## 🚀 Vision & Roadmap (Beyond 1.0.0) + +> Best practices should **execute**, not merely 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). +* 🧱 Hexagonal evolution kits (ports / adapters / CQRS) +* 📈 Observability defaults (tracing / metrics) +* 🔐 Security (OAuth2 / Keycloak patterns) +* 🧩 Multi‑module service generation +* 🎯 Broader ecosystem: Gradle / Kotlin / Quarkus +* 💻 Developer UI → configure → generate → download + +> **Executable Architecture for modern delivery** --- -## 🛡 License +## 🤝 Contributing -MIT +Discussions: +[https://github.com/blueprint-platform/codegen-blueprint/discussions](https://github.com/blueprint-platform/codegen-blueprint/discussions) + +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/executable-architecture-scope.md b/docs/architecture/executable-architecture-scope.md new file mode 100644 index 0000000..a107d3d --- /dev/null +++ b/docs/architecture/executable-architecture-scope.md @@ -0,0 +1,265 @@ +# Architecture Enforcement Scope — Codegen Blueprint 1.0.0 GA + +> This unified document defines what the **Codegen Blueprint engine enforces today (1.0.0 GA)** and what the **generated project guarantees at output** — a single reference point for architectural truth. + +--- + +## 📚 Table of Contents + +* [1 Purpose](#1-purpose) +* [2 Core Mental Model](#2-core-mental-model) +* [3 Engine Enforcement Guarantees (1.0.0 GA)](#3-engine-enforcement-guarantees-100-ga) + * [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 Generated Project Scope (Output Contract)](#4-generated-project-scope-output-contract) +* [5 Explicitly Not Enforced (Yet)](#5-explicitly-not-enforced-yet) +* [6 Intentional Scope Constraints](#6-intentional-scope-constraints) +* [7 Path Toward Executable Architecture](#7-path-toward-executable-architecture) +* [8 Review Guidance](#8-review-guidance) + +--- + +## 1️⃣ Purpose + +Ensure that: + +* README **claims** match **actual engine guarantees** +* Teams get a **predictable**, **testable**, **clean** project every time +* Foundations are in place for **strict boundary enforcement** later + +> 🧠 If we promise it, we enforce it. + +--- + +## 2️⃣ Core Mental Model + +| Concept | Description | +| -------------- | ---------------------------------------------------- | +| **Engine** | CLI‑driven generator applying architectural profiles | +| **Profile** | Defines language + build tool + architecture layout | +| **Blueprints** | Artifact templates (POM, YAML, sources, docs) | + +📌 The engine today: + +> Generates clean, production‑viable Spring Boot services — with architecture *prepared* for enforcement. + +--- + +## 3️⃣ Engine Enforcement Guarantees (1.0.0 GA) + +These are **strict contracts** validated through automated 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 +``` + +Always **single‑module**. + +--- + +### 3.2 Naming & Identity Enforcement + +Engine **normalizes + validates**: + +* groupId +* artifactId +* package name +* application name + +Main class rule: + +``` +Application +``` + +> ❌ Invalid identifiers → **fail fast** + +--- + +### 3.3 Spring Boot Minimal Runtime Baseline + +Project must: + +* ✔ compile + run instantly +* ✔ use explicitly provided dependencies only +* ✔ bootstrap through SpringApplication.run(...) + +📌 No accidental demo code. + +--- + +### 3.4 Test Ready Project + +Generated project must: + +* contain test execution entrypoint via `@SpringBootTest` +* pass `mvn verify` right after generation + +Testing is not optional. + +--- + +### 3.5 Separation of Engine & Templates + +Engine **does not depend on**: + +* Spring +* File system +* Maven internals + +All tech‑specific logic lives in **adapters + profiles**. + +> Enables Gradle, Kotlin, Quarkus… with **zero** engine refactor. + +--- + +### 3.6 Profile Driven Execution + +```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 \ + --dependency web +``` + +Profile determines: + +* templates +* structure +* tech behavior + +--- + +## 4️⃣ Generated Project Scope (Output Contract) + +### Standard Profile + +``` +springboot-maven-java +``` + +### Output requirements + +``` +/ + ├── pom.xml + ├── src/main/java//Application.java + ├── src/test/java//ApplicationTests.java + ├── src/main/resources/application.yml + ├── .gitignore + └── README.md +``` + +### Architecture Option (OPT‑IN) + +```bash +--layout hexagonal +``` + +Produces structured boundaries: + +``` +├── domain/ # business rules only +├── application/ # orchestrates ports +├── adapters/ # inbound + outbound +└── bootstrap/ # wiring + config +``` + +### Sample Code Option (OPT‑IN) + +```bash +--sample-code basic +``` + +Provides ready‑to‑run teaching example: + +```bash +GET /api/v1/sample/greetings/default +→ 200 OK +{ + "text": "Hello from hexagonal sample!" +} +``` + +Run instantly: + +```bash +./mvnw spring-boot:run +``` + +--- + +## 5️⃣ Explicitly Not Enforced (Yet) + +| Item | Reason | +| ------------------------- | --------------------------------- | +| Hexagonal by default | Avoid adoption friction | +| Policy engine | Requires architectural DSL | +| ArchUnit rules generation | Depends on next milestone | +| Org‑wide governance | Future platform-level enforcement | + +> Architecture‑aware today → architecture‑policing tomorrow + +--- + +## 6️⃣ Intentional Scope Constraints + +* 🚫 No bloated features +* 🚫 No silent opinionated defaults +* 🎯 Precision > volume +* ♻️ Upgrade without core rewrites + +> Narrow now → **massively scalable later** + +--- + +## 7️⃣ Path Toward Executable Architecture + +| Stage | Capability | Benefit | +| ----- | ---------------------------- | -------------------------------- | +| v1.1+ | Layout‑aware hex scaffolding | Real boundaries in code output | +| v1.2+ | Auto‑architecture tests | Prevent drift at compile/CI time | +| v1.3+ | Policy DSL | Architecture as CI quality gate | +| v2.0 | Org profiles | Governance at organization scale | + +--- + +## 8️⃣ Review Guidance + +Every change touching architectural behavior must answer: + +> ❓ Does this change **claim** enforcement? + +If YES → update this document. +If NO → update README roadmap (only). + +--- + +### Final Statement + +**Codegen Blueprint 1.0.0 GA** generates: + +* Clean & testable projects +* Architecture‑aware structure +* Predictable foundations for future enforcement + +> **Executable Architecture begins here.** 🚀 diff --git a/docs/design/social-preview.html b/docs/design/social-preview.html new file mode 100644 index 0000000..71b8863 --- /dev/null +++ b/docs/design/social-preview.html @@ -0,0 +1,437 @@ + + + + + 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 io.github.blueprintplatform.samples \
+
--artifact-id greeting-service \
+
--name "Greeting Service" \
+
--package-name io.github.blueprintplatform.samples.greeting \
+ +
// Optional flags
+
--layout hexagonal \
+
--sample-code basic \
+
--dependency web \
+
--dependency data_jpa
+ +
// Defaults → maven · java · Java 21 · Spring Boot 3.5.x
+
+
+
+
+ + \ 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..8ef93de --- /dev/null +++ b/docs/guides/how-to-explore-hexagonal-architecture.md @@ -0,0 +1,319 @@ +# 🚀 Codegen-Blueprint — Hexagonal Architecture Deep Dive + +Welcome! This guide shows how **Hexagonal Architecture (Ports & Adapters)** is applied to a **real, production-grade project generation engine** — with strict boundaries and full test coverage. + +This repository demonstrates **Executable Architecture** in action: + +* Architecture rules **enforced by the engine**, not left to individuals +* Domain remains **pure and framework-agnostic** +* Technology choices are **plug-replaceable** — without core changes + +> Build a scalable ecosystem of services — +> **without losing architectural consistency over time**. + +*Hexagonal Architecture — not just documented, but executed.* + +--- + +## 📚 Table of Contents + +* [🧱 Architectural Overview](#-architectural-overview) +* [🔌 Ports & Adapters](#-ports--adapters) + * [💼 Domain → Outbound Ports](#-domain--outbound-ports) + * [🧩 Application → Artifact Generation Ports](#-application--artifact-generation-ports) + * [🛠️ Technology Adapters](#-technology-adapters) +* [📦 Profiles: Externalized Architecture Rules](#-profiles-externalized-architecture-rules) +* [🧱 Source Layout Generation](#-source-layout-generation) +* [📄 Resource Model — Stronger than “Files”](#-resource-model--stronger-than-files) +* [🧪 Testing Strategy](#-testing-strategy) +* [🎯 What You Can Learn Here](#-what-you-can-learn-here) +* [🎮 Try It — CLI Adapter](#-try-it--cli-adapter) +* [🔍 Start Here](#-start-here) +* [⭐ Final Thoughts](#-final-thoughts) + +--- + +## 🧱 Architectural Overview + +Codegen Blueprint applies **strict inward dependency flow** — ensuring the **domain stays pure** and fully independent of frameworks: + +``` +bootstrap // Spring & runtime wiring only +↓ +adapter // technology-specific implementations (CLI, File, Templating…) +↓ +application // orchestration, profiles, generation rules +↓ +domain // core business rules — no external dependencies +``` + +### Key Principles + +* **Domain-centric** — business logic remains framework-free +* **Replaceable adapters** — switch technology with no core changes +* **Independent testing** — every layer testable on its own +* **Evolution-ready** — new profiles or stacks plug in without refactor + +> Architecture is not a *guideline* here — +> **it is enforced by design** + +--- + +## 🔌 Ports & Adapters + +The engine is **driven by ports (interfaces)** — fully decoupled from frameworks. + +--- + +### 💼 Domain → Outbound Ports + +These ports allow the **application layer** to perform external actions +**without** depending on external technology: + +| Port | Responsibility | +| --------------------- | ----------------------------------------------------- | +| `ProjectRootPort` | Resolve and prepare the output project directory | +| `ProjectWriterPort` | Persist generated resources (text / binary / folders) | +| `ProjectArchiverPort` | Bundle project for delivery (e.g., ZIP packaging) | + +> Same domain → multiple tech stacks → zero changes to business rules + +--- + +### 🧩 Application → Artifact Generation Ports + +Each artifact in the produced project has a **dedicated generation port**: + +| Port | Output artifact | +| ------------------------------ | ------------------------------------------------------ | +| `SourceLayoutPort` | Java package structure & source folders | +| `MainSourceEntrypointPort` | Main application class | +| `TestSourceEntrypointPort` | Test bootstrap | +| `ApplicationConfigurationPort` | Runtime configuration (`application.yml`) | +| `BuildConfigurationPort` | Build descriptor (`pom.xml`) | +| `BuildToolFilesPort` | Wrapper + tool metadata (`mvnw`, `.mvn/`) | +| `IgnoreRulesPort` | `.gitignore` + VCS hygiene | +| `ProjectDocumentationPort` | Generated project README | +| `SampleCodePort` | Optional greeting sample (domain + ports + REST demo) | + +Supporting the pipeline: + +| Component | Role | +| -------------------------- | --------------------------------------------------- | +| `ProjectArtifactsPort` | Executes artifacts in correct architectural order | +| `ProjectArtifactsSelector` | Chooses implementation based on selected TechStack | + +> Every artifact is intentional → nothing accidental is generated + +--- + +### 🛠️ Technology Adapters + +Adapters **implement ports using real world tooling**: + +* File system access +* FreeMarker-based resource templating +* Maven build metadata + +Designed for evolution: + +* Gradle +* Kotlin +* Quarkus +* REST delivery + +⬆ All can be added **without touching domain or application code** + +--- + +> **Ports define the architecture** +> **Adapters only enable execution** + +--- + +## 📦 Profiles: Externalized Architecture Rules + +Profiles define what artifacts are generated, in what order, and under which architecture rules: + +* Template namespace (profile defines rendering folders) +* Enabled artifacts per stack +* Strict generation ordering — architecture enforcement + +📍 Example — `springboot-maven-java` 1.0.0 pipeline + +``` +build-config → build-tool-files → ignore-rules +→ source-layout → app-config +→ main-source-entrypoint → test-source-entrypoint +→ sample-code (optional) +→ project-documentation +``` + +> Profiles ensure **hexagonal evolution** does not require code changes — only configuration. +
+ +--- + +## 🧱 Source Layout Generation + +`SOURCE_LAYOUT` adapter now generates: + +### Standard Layout + +``` +src/main/java// +src/main/resources/ +src/test/java// +src/test/resources/ +``` + +### Hexagonal layout (opt-in an evolution path) + +``` +src/main/java// +├─ domain/ +├─ application/ +├─ adapter/ +│ ├─ in/ +│ └─ out/ +└─ bootstrap/ +``` + +If `--sample-code basic` is enabled: + +``` +adapter/in/rest/ +adapter/out/ (future) +domain/greeting/ +application/greeting/ +``` + +> Directories are **intentional artifacts** → not side effects. + +--- + +## 📄 Resource Model — Stronger than “Files” + +Generated assets are modeled as first-class domain concepts: + +| Type | Model | Purpose | +| --------- | -------------------------- | ------------------------------ | +| Directory | `GeneratedDirectory` | Ensure structural correctness | +| Text | `GeneratedTextResource` | Java, YAML, README, etc. | +| Binary | `GeneratedBinaryResource` | Maven wrapper, future assets | + +Capability highlights: + +* Template-driven & template-less generation +* Supports future binary artifacts (zip, images) +* Perfect fit for multi-artifact pipelines + + +--- + +## 🧪 Testing Strategy + +| Test Type | Validates | +| ------------------------ | ------------------------------------------------- | +| **Unit Tests** | Domain rules + adapter logic | +| **Integration Tests** | Spring wiring + ordered artifact pipeline | +| **E2E CLI Tests** | Full generation → ZIP structure correctness | +| **Template Coverage** | Sample code, structure, placeholders, UTF-8 model | + +CI includes: + +* 🧩 Contract tests for every port + adapter pair +* 📊 Codecov tracking — full pipeline validation +* 🔐 CodeQL security scanning +* ✔ Architectural test gates planned (`ArchUnit`) + +### Summary + +* Profiles externalize **architecture rules** +* Layout generation enforces **predictability** +* Resource model prevents **accidental drift** +* Tests safeguard **contract integrity** + +--- + +## 🎯 What You Can Learn Here + +| Capability You’ll Gain | How This Repo Enables It | +|---------------------------|-----------------------------------------------------------| +| Hexagonal architecture | Strict boundaries, port-driven domain isolation | +| Code generation engines | Profile-driven, ordered artifact pipelines | +| Enterprise maintainability| Add new stacks w/o modifying core engine | +| CI-First delivery | Coverage, contract tests, secure pipelines | +| Architecture automation | Enforce structure from day zero — “Executable Architecture” | + +This is a **real production reference**, not a conceptual demo. + +--- + +## 🎮 Try It — CLI Adapter + +Here’s the **springboot-maven-java** profile with **hexagonal** layout and **sample greeting** included: + +```bash +java -jar codegen-blueprint-1.0.0.jar \ + --cli \ + springboot \ + --group-id io.github.blueprintplatform.samples \ + --artifact-id greeting-service \ + --name "Greeting Service" \ + --description "Hexagonal greeting sample powered by Blueprint" \ + --package-name io.github.blueprintplatform.samples.greeting \ + --layout hexagonal \ + --sample-code basic \ + --dependency web \ + --dependency data_jpa +``` + +This produces a ready-to-run service with a REST greeting endpoint: + +```bash +GET /api/v1/sample/greetings/default + +→ 200 OK: "Hello from hexagonal sample!" +``` + + +Run instantly: +cd greeting-service +./mvnw spring-boot:run + +--- + +## 🔍 Start Here + +Follow the architecture execution path: + +``` +[ CLI input ] + ↓ +ProjectBlueprint + ↓ +ProjectArtifactsSelector // chooses profile implementation + ↓ +ProjectArtifactsPort // executes ordered ports + ↓ +ProjectWriterPort // writes physical output (FS/ZIP) +``` + +You are watching architecture → compiled and executed. + +--- + +## ⭐ Final Thoughts + +Executable Architecture means: +* 🚫 No framework leaking into domain logic +* 🧠 Architecture intent is automated, not “documented & forgotten” +* ♻️ Adaptable tech stacks w/o core rewrites +* 🧪 Full test enforcement from pipeline to template + +Built for teams who believe: + +“Architecture isn’t a diagram — it’s a behavior that must execute.” + +Happy generating! 🚀✨ \ No newline at end of file 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..a7727c1 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..d3837df 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/SampleCodeLevelNotSupportedException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeLevelNotSupportedException.java new file mode 100644 index 0000000..250cf61 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeLevelNotSupportedException.java @@ -0,0 +1,11 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +@SuppressWarnings("java:S110") +public final class SampleCodeLevelNotSupportedException extends AdapterException { + + private static final String KEY = "adapter.sample-code.level.unsupported"; + + public SampleCodeLevelNotSupportedException(String levelKey) { + super(KEY, levelKey); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java new file mode 100644 index 0000000..3f839bc --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java @@ -0,0 +1,12 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +@SuppressWarnings("java:S110") +public final class SampleCodeTemplatesNotFoundException extends AdapterException { + + private static final String KEY = "adapter.sample-code.templates.not-found"; + + public SampleCodeTemplatesNotFoundException( + String templateRoot, String layoutKey, String levelKey) { + super(KEY, templateRoot, layoutKey, levelKey); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java new file mode 100644 index 0000000..26db9ca --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java @@ -0,0 +1,11 @@ +package io.github.blueprintplatform.codegen.adapter.error.exception; + +@SuppressWarnings("java:S110") +public final class SampleCodeTemplatesScanException extends AdapterException { + + private static final String KEY = "adapter.sample-code.templates.scan.failed"; + + public SampleCodeTemplatesScanException(String templateRoot, Throwable cause) { + super(KEY, cause, templateRoot); + } +} 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..96d34f2 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java @@ -0,0 +1,16 @@ +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, + String sampleCodeLevelKey, + 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..7909863 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java @@ -0,0 +1,70 @@ +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.sample.SampleCodeLevel; +import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions; +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()); + SampleCodeLevel sampleCodeLevel = SampleCodeLevel.fromKey(request.sampleCodeLevelKey()); + List dependencies = toDependencyInputs(request.dependencies()); + SampleCodeOptions sampleCodeOptions = new SampleCodeOptions(sampleCodeLevel); + + return new CreateProjectCommand( + request.groupId(), + request.artifactId(), + request.name(), + request.description(), + request.packageName(), + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions, + 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..4db0a9a --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java @@ -0,0 +1,156 @@ +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.sample.SampleCodeLevel; +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 = {"--sample-code"}, + required = false, + description = "Sample code level. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "none") + SampleCodeLevel sampleCode; + + @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, + sampleCode.key(), + 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..ab76b13 --- /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"; + } +} 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..e0f44bd --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java @@ -0,0 +1,47 @@ +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); + } + + @Override + public void createDirectories(Path projectRoot, Path relativeDir) { + Path target = projectRoot.resolve(relativeDir); + try { + Files.createDirectories(target); + } catch (IOException e) { + throw new ProjectWriteException(target, e); + } + } +} 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..27ab73e --- /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.GeneratedResource; +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..582b69e --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java @@ -0,0 +1,33 @@ +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 io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity; +import java.util.Map; + +public class ApplicationYamlAdapter extends AbstractSingleTemplateArtifactAdapter + implements ApplicationConfigurationPort { + + private static final String KEY_APP_NAME = "applicationName"; + + public ApplicationYamlAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) { + super(renderer, artifactDefinition); + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.APP_CONFIG; + } + + @Override + protected Map buildModel(ProjectBlueprint blueprint) { + ProjectIdentity id = blueprint.getIdentity(); + return Map.ofEntries(entry(KEY_APP_NAME, id.artifactId().value())); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java new file mode 100644 index 0000000..542f1fb --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java @@ -0,0 +1,78 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.doc; + +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/sample/SampleCodeAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapter.java new file mode 100644 index 0000000..f1ec48a --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapter.java @@ -0,0 +1,174 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.sample; + +import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeLevelNotSupportedException; +import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeTemplatesNotFoundException; +import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeTemplatesScanException; +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.SampleCodePort; +import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition; +import io.github.blueprintplatform.codegen.bootstrap.config.SamplesProperties; +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.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel; +import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class SampleCodeAdapter implements SampleCodePort { + + private static final String TEMPLATES_ROOT_DIR = "templates"; + private static final String SRC_MAIN_JAVA = "src/main/java"; + private static final String PATH_SEPARATOR = "/"; + private static final String JAVA_FTL_SUFFIX = ".java.ftl"; + private static final String FTL_SUFFIX = ".ftl"; + private static final String MODEL_KEY_PROJECT_PACKAGE_NAME = "projectPackageName"; + + private final TemplateRenderer renderer; + private final ArtifactDefinition artifactDefinition; + private final SamplesProperties samplesProperties; + + public SampleCodeAdapter( + TemplateRenderer renderer, + ArtifactDefinition artifactDefinition, + SamplesProperties samplesProperties) { + this.renderer = renderer; + this.artifactDefinition = artifactDefinition; + this.samplesProperties = samplesProperties; + } + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.SAMPLE_CODE; + } + + @Override + public Iterable generate(ProjectBlueprint blueprint) { + SampleCodeOptions options = blueprint.getSampleCodeOptions(); + SampleCodeLevel level = options == null ? SampleCodeLevel.NONE : options.level(); + + boolean levelInvalid = (level == null) || !level.isEnabled() || SampleCodeLevel.RICH == level; + if (levelInvalid || !blueprint.getLayout().isHexagonal()) { + return List.of(); + } + + ProjectLayout layout = blueprint.getLayout(); + String templateBasePath = artifactDefinition.basePath(); + String samplesRootRelative = resolveSamplesRoot(layout, level); + String templateRoot = normalizeTemplateRoot(templateBasePath, samplesRootRelative); + + List templatePaths = resolveTemplatePaths(templateRoot); + if (templatePaths.isEmpty()) { + throw new SampleCodeTemplatesNotFoundException(templateRoot, layout.key(), level.key()); + } + + PackageName pkg = blueprint.getPackageName(); + String packagePath = pkg.value().replace('.', '/'); + + Map model = buildModel(blueprint); + List generated = new ArrayList<>(); + + for (String fullTemplatePath : templatePaths) { + if (!fullTemplatePath.endsWith(JAVA_FTL_SUFFIX)) { + continue; + } + + String relativeUnderRoot = fullTemplatePath.substring(templateRoot.length() + 1); + String javaRelative = stripSuffix(relativeUnderRoot); + + Path outPath = Paths.get(SRC_MAIN_JAVA).resolve(packagePath).resolve(javaRelative); + + GeneratedResource resource = renderer.renderUtf8(outPath, fullTemplatePath, model); + + generated.add(resource); + } + + return List.copyOf(generated); + } + + private String resolveSamplesRoot(ProjectLayout layout, SampleCodeLevel level) { + String layoutRoot = + layout != null && layout.isHexagonal() + ? samplesProperties.hexagonal() + : samplesProperties.standard(); + + String levelDir = + switch (level) { + case BASIC -> samplesProperties.basicDirName(); + case RICH -> samplesProperties.richDirName(); + case NONE -> throw new SampleCodeLevelNotSupportedException(level.key()); + }; + + String normalizedLayoutRoot = + layoutRoot.endsWith(PATH_SEPARATOR) + ? layoutRoot.substring(0, layoutRoot.length() - 1) + : layoutRoot; + + return normalizedLayoutRoot + PATH_SEPARATOR + levelDir; + } + + private String normalizeTemplateRoot(String basePath, String samplesRootRelative) { + String normalizedBase = + basePath.endsWith(PATH_SEPARATOR) ? basePath.substring(0, basePath.length() - 1) : basePath; + return normalizedBase + PATH_SEPARATOR + samplesRootRelative; + } + + private Map buildModel(ProjectBlueprint blueprint) { + PackageName pkg = blueprint.getPackageName(); + return Map.of(MODEL_KEY_PROJECT_PACKAGE_NAME, pkg.value()); + } + + private List resolveTemplatePaths(String templateRoot) { + List result = new ArrayList<>(); + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + String rootWithPrefix = TEMPLATES_ROOT_DIR + PATH_SEPARATOR + templateRoot; + + URL rootUrl = cl.getResource(rootWithPrefix); + if (rootUrl == null) { + return result; + } + + try { + Path rootPath = Paths.get(rootUrl.toURI()); + + try (var paths = java.nio.file.Files.walk(rootPath)) { + paths + .filter(java.nio.file.Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(FTL_SUFFIX)) + .forEach( + file -> { + Path relativeToRoot = rootPath.relativize(file); + String normalizedRelativeToRoot = + relativeToRoot + .toString() + .replace(File.separatorChar, PATH_SEPARATOR.charAt(0)); + + String fullTemplatePath = + templateRoot + PATH_SEPARATOR + normalizedRelativeToRoot; + + result.add(fullTemplatePath); + }); + } + + return result; + } catch (URISyntaxException | java.io.IOException e) { + throw new SampleCodeTemplatesScanException(templateRoot, e); + } + } + + private String stripSuffix(String value) { + if (value.endsWith(FTL_SUFFIX)) { + return value.substring(0, value.length() - FTL_SUFFIX.length()); + } + return value; + } +} 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..3e7ebde --- /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.GeneratedResource; +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); + + GeneratedResource 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/source/SourceLayoutAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapter.java new file mode 100644 index 0000000..58a670c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapter.java @@ -0,0 +1,59 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source; + +import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey; +import io.github.blueprintplatform.codegen.application.port.out.artifact.SourceLayoutPort; +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.pkg.PackageName; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedDirectory; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class SourceLayoutAdapter implements SourceLayoutPort { + + private static final Path SRC_MAIN_JAVA = Path.of("src/main/java"); + private static final Path SRC_TEST_JAVA = Path.of("src/test/java"); + private static final Path SRC_MAIN_RESOURCES = Path.of("src/main/resources"); + private static final Path SRC_TEST_RESOURCES = Path.of("src/test/resources"); + + private static final String SEGMENT_ADAPTER = "adapter"; + private static final String SEGMENT_APPLICATION = "application"; + private static final String SEGMENT_BOOTSTRAP = "bootstrap"; + private static final String SEGMENT_DOMAIN = "domain"; + + @Override + public ArtifactKey artifactKey() { + return ArtifactKey.SOURCE_LAYOUT; + } + + @Override + public Iterable generate(ProjectBlueprint blueprint) { + List resources = new ArrayList<>(); + + resources.add(new GeneratedDirectory(SRC_MAIN_JAVA)); + resources.add(new GeneratedDirectory(SRC_TEST_JAVA)); + resources.add(new GeneratedDirectory(SRC_MAIN_RESOURCES)); + resources.add(new GeneratedDirectory(SRC_TEST_RESOURCES)); + + PackageName packageName = blueprint.getPackageName(); + String packagePath = packageName.value().replace('.', '/'); + + Path mainBasePackageDir = SRC_MAIN_JAVA.resolve(packagePath); + Path testBasePackageDir = SRC_TEST_JAVA.resolve(packagePath); + + resources.add(new GeneratedDirectory(mainBasePackageDir)); + resources.add(new GeneratedDirectory(testBasePackageDir)); + + ProjectLayout layout = blueprint.getLayout(); + if (layout.isHexagonal()) { + resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_ADAPTER))); + resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_APPLICATION))); + resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_BOOTSTRAP))); + resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_DOMAIN))); + } + + return List.copyOf(resources); + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.java new file mode 100644 index 0000000..18e77b9 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.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.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..eafadb1 --- /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.GeneratedResource; +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); + GeneratedResource 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..653ce60 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java @@ -0,0 +1,32 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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 GeneratedResource renderUtf8( + Path outPath, String templateName, Map model) { + try (StringWriter sw = new StringWriter()) { + Template tpl = cfg.getTemplate(templateName); + tpl.process(model, sw); + return new GeneratedTextResource(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..3a55016 --- /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.GeneratedResource; +import java.nio.file.Path; +import java.util.Map; + +public interface TemplateRenderer { + GeneratedResource 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..3452697 --- /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.GeneratedResource; + +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..285093c --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java @@ -0,0 +1,38 @@ +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"), + SOURCE_LAYOUT("source-layout"), + APP_CONFIG("app-config"), + MAIN_SOURCE_ENTRY_POINT("main-source-entrypoint"), + TEST_SOURCE_ENTRY_POINT("test-source-entrypoint"), + SAMPLE_CODE("sample-code"), + 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..97d03e7 --- /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.GeneratedResource; + +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/SampleCodePort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SampleCodePort.java new file mode 100644 index 0000000..259d833 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SampleCodePort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface SampleCodePort extends ArtifactPort {} diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java new file mode 100644 index 0000000..bf4fede --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java @@ -0,0 +1,3 @@ +package io.github.blueprintplatform.codegen.application.port.out.artifact; + +public interface SourceLayoutPort 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..1d8cc8b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java @@ -0,0 +1,21 @@ +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.sample.SampleCodeOptions; +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, + SampleCodeOptions sampleCodeOptions, + 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..85a8da3 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java @@ -0,0 +1,54 @@ +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 projectBlueprint = mapper.from(command); + + Path projectRoot = + rootPort.prepareRoot( + command.targetDirectory(), + projectBlueprint.getIdentity().artifactId().value(), + FAIL_IF_EXISTS); + + ProjectArtifactsPort port = artifactsSelector.select(projectBlueprint.getTechStack()); + var files = port.generate(projectBlueprint); + + writerPort.write(projectRoot, files); + + String baseName = projectBlueprint.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..254be00 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java @@ -0,0 +1,67 @@ +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.sample.SampleCodeOptions; +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(); + SampleCodeOptions sampleCodeOptions = c.sampleCodeOptions(); + + Dependencies deps = mapDependencies(c.dependencies()); + + return ProjectBlueprintFactory.of( + identity, name, description, pkg, c.techStack(), layout, target, deps, sampleCodeOptions); + } + + 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..f29f43b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java @@ -0,0 +1,51 @@ +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, + @Valid @NotNull SamplesProperties samples) { + + 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/SamplesProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/SamplesProperties.java new file mode 100644 index 0000000..ecda988 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/SamplesProperties.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.bootstrap.config; + +import jakarta.validation.constraints.NotBlank; + +public record SamplesProperties( + @NotBlank String standard, + @NotBlank String hexagonal, + @NotBlank String basicDirName, + @NotBlank String richDirName) {} 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..c13a7af --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java @@ -0,0 +1,107 @@ +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.sample.SampleCodeLevel; +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( + SampleCodeLevel.class, new KeyedEnumConverter<>(SampleCodeLevel::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..67a50f5 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java @@ -0,0 +1,166 @@ +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.doc.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.sample.SampleCodeAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.MainSourceEntrypointAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.SourceLayoutAdapter; +import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.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.*; +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 + BuildConfigurationPort 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 + BuildToolFilesPort springBootMavenJavaMavenWrapperBuildToolFilesAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.BUILD_TOOL_METADATA); + return new MavenWrapperBuildToolFilesAdapter(renderer, props); + } + + @Bean + IgnoreRulesPort springBootMavenJavaGitIgnoreAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.IGNORE_RULES); + return new GitIgnoreAdapter(renderer, props); + } + + @Bean + SourceLayoutPort springBootMavenJavaSourceLayoutAdapter() { + return new SourceLayoutAdapter(); + } + + @Bean + ApplicationConfigurationPort springBootMavenJavaApplicationYamlAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.APP_CONFIG); + return new ApplicationYamlAdapter(renderer, props); + } + + @Bean + MainSourceEntrypointPort 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 + TestSourceEntrypointPort 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 + SampleCodePort springBootMavenJavaSampleCodeAdapter( + TemplateRenderer renderer, CodegenProfilesProperties profiles) { + ArtifactDefinition props = + profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.PROJECT_DOCUMENTATION); + return new SampleCodeAdapter(renderer, props, profiles.samples()); + } + + @Bean + ProjectDocumentationPort 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( + BuildConfigurationPort springBootMavenJavaMavenPomBuildConfigurationAdapter, + BuildToolFilesPort springBootMavenJavaMavenWrapperBuildToolFilesAdapter, + IgnoreRulesPort springBootMavenJavaGitIgnoreAdapter, + SourceLayoutPort springBootMavenJavaSourceLayoutAdapter, + ApplicationConfigurationPort springBootMavenJavaApplicationYamlAdapter, + MainSourceEntrypointPort springBootMavenJavaMainSourceEntrypointAdapter, + TestSourceEntrypointPort springBootMavenJavaTestSourceEntrypointAdapter, + SampleCodePort springBootMavenJavaSampleCodeAdapter, + ProjectDocumentationPort 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.SOURCE_LAYOUT, springBootMavenJavaSourceLayoutAdapter); + 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.SAMPLE_CODE, springBootMavenJavaSampleCodeAdapter); + 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..2dbdd71 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java @@ -0,0 +1,90 @@ +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.sample.SampleCodeOptions; +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, + SampleCodeOptions sampleCodeOptions) { + + CompatibilityPolicy.ensureCompatible(techStack, platformTarget); + + return new ProjectBlueprint( + identity, + name, + description, + packageName, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + 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), + SampleCodeOptions.none()); + } + + 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)), + SampleCodeOptions.none()); + } +} 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..2ec8762 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java @@ -0,0 +1,81 @@ +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.sample.SampleCodeOptions; +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; + private final SampleCodeOptions sampleCodeOptions; + + public ProjectBlueprint( + ProjectIdentity identity, + ProjectName name, + ProjectDescription description, + PackageName packageName, + TechStack techStack, + ProjectLayout layout, + PlatformTarget platformTarget, + Dependencies dependencies, + SampleCodeOptions sampleCodeOptions) { + this.identity = identity; + this.name = name; + this.description = description; + this.packageName = packageName; + this.techStack = techStack; + this.layout = layout; + this.platformTarget = platformTarget; + this.dependencies = dependencies; + this.sampleCodeOptions = sampleCodeOptions; + } + + 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; + } + + public SampleCodeOptions getSampleCodeOptions() { + return sampleCodeOptions; + } +} 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/sample/SampleCodeLevel.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeLevel.java new file mode 100644 index 0000000..bd6dc24 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeLevel.java @@ -0,0 +1,37 @@ +package io.github.blueprintplatform.codegen.domain.model.value.sample; + +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 SampleCodeLevel implements KeyedEnum { + NONE("none"), + BASIC("basic"), + RICH("rich"); + + private static final ErrorCode UNKNOWN = () -> "project.sample.level.unknown"; + + private final String key; + + SampleCodeLevel(String key) { + this.key = key; + } + + public static SampleCodeLevel fromKey(String rawKey) { + return KeyEnumParser.parse(SampleCodeLevel.class, rawKey, UNKNOWN); + } + + public boolean isEnabled() { + return this != NONE; + } + + @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/sample/SampleCodeOptions.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptions.java new file mode 100644 index 0000000..3a8b588 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptions.java @@ -0,0 +1,26 @@ +package io.github.blueprintplatform.codegen.domain.model.value.sample; + +public record SampleCodeOptions(SampleCodeLevel level) { + + public SampleCodeOptions { + if (level == null) { + level = SampleCodeLevel.NONE; + } + } + + public static SampleCodeOptions none() { + return new SampleCodeOptions(SampleCodeLevel.NONE); + } + + public static SampleCodeOptions basic() { + return new SampleCodeOptions(SampleCodeLevel.BASIC); + } + + public static SampleCodeOptions rich() { + return new SampleCodeOptions(SampleCodeLevel.RICH); + } + + public boolean isEnabled() { + return level.isEnabled(); + } +} 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/GeneratedBinaryResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedBinaryResource.java new file mode 100644 index 0000000..5e5b7da --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedBinaryResource.java @@ -0,0 +1,46 @@ +package io.github.blueprintplatform.codegen.domain.port.out.artifact; + +import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*; + +import java.nio.file.Path; +import java.util.Arrays; + +public record GeneratedBinaryResource(Path relativePath, byte[] bytes) + implements GeneratedResource { + + public GeneratedBinaryResource(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 GeneratedBinaryResource(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 "GeneratedBinaryFile[" + relativePath + ", size=" + bytes.length + "]"; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java new file mode 100644 index 0000000..22ed6db --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java @@ -0,0 +1,18 @@ +package io.github.blueprintplatform.codegen.domain.port.out.artifact; + +import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*; + +import java.nio.file.Path; + +public record GeneratedDirectory(Path relativePath) implements GeneratedResource { + + public GeneratedDirectory { + requireRelativePath(relativePath); + } + + @SuppressWarnings("NullableProblems") + @Override + public String toString() { + return "GeneratedDirectory[" + relativePath + "]"; + } +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java new file mode 100644 index 0000000..a7d83bc --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java @@ -0,0 +1,9 @@ +package io.github.blueprintplatform.codegen.domain.port.out.artifact; + +import java.nio.file.Path; + +public sealed interface GeneratedResource + permits GeneratedTextResource, GeneratedBinaryResource, GeneratedDirectory { + + Path relativePath(); +} diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java new file mode 100644 index 0000000..da78fa2 --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java @@ -0,0 +1,15 @@ +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; + +public record GeneratedTextResource(Path relativePath, String content, Charset charset) + implements GeneratedResource { + + public GeneratedTextResource { + requireRelativePath(relativePath); + requireTextContent(content, charset); + } +} 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..4f9445b --- /dev/null +++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java @@ -0,0 +1,41 @@ +package io.github.blueprintplatform.codegen.domain.port.out.filesystem; + +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedBinaryResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedDirectory; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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); + + void createDirectories(Path projectRoot, Path relativeDir); + + default void writeText(Path root, Path relative, String content) { + writeText(root, relative, content, java.nio.charset.StandardCharsets.UTF_8); + } + + default void write(Path projectRoot, GeneratedResource resource) { + switch (resource) { + case GeneratedTextResource(Path p, String c, Charset cs) -> writeText(projectRoot, p, c, cs); + case GeneratedBinaryResource(Path p, byte[] b) -> writeBytes(projectRoot, p, b); + case GeneratedDirectory(Path p) -> createDirectories(projectRoot, p); + } + } + + default void write(Path projectRoot, Iterable resources) { + for (GeneratedResource f : resources) write(projectRoot, f); + } + + default void write(Path projectRoot, GeneratedResource... files) { + for (GeneratedResource 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..15a2052 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,16 +1,73 @@ spring: application: - name: codegen-springboot-initializr + name: codegen-blueprint -freemarker: +codegen: + samples: + standard: sample/standard + hexagonal: sample/hexagonal + basic-dir-name: basic + rich-dir-name: rich + profiles: + springboot-maven-java: + template-base-path: springboot/maven/java/ + + ordered-artifact-keys: + - build-config + - build-tool-metadata + - ignore-rules + - source-layout + - app-config + - main-source-entrypoint + - test-source-entrypoint + - sample-code + - 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 + + source-layout: + templates: [] + + 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 + + sample-code: + templates: [ ] + + 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 \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..7f7e545 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,79 @@ +# ================================ +# === 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} +# --- PROJECT SAMPLE LEVEL --- +project.sample.level.unknown=Unknown sample code level: {0}. Supported values: none, basic, rich +# ================================ +# === 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}''. +# Sample code adapter +adapter.sample-code.templates.not-found=No sample code templates found under ''{0}'' for layout ''{1}'' and level ''{2}''. +adapter.sample-code.templates.scan.failed=Failed to scan sample code templates under ''{0}''. +adapter.sample-code.level.unsupported=Sample code level ''{0}'' is not supported by the current adapter. \ 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..6bfaf95 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/README.md.ftl @@ -0,0 +1,124 @@ +<#-- README Generator Template --> + +# ${projectName} + +${projectDescription} + +--- + +## 🔧 Tech Stack + +| Category | Value | +|---------|------| +| **Framework** | ${framework} | +| **Language** | ${language} | +| **Build Tool** | ${buildTool} | +| **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 was generated with the optional **hexagonal layout**: + +```bash +--layout hexagonal +``` + +With this flag, your `src/main/java/${packageName?replace('.', '/')}` tree is structured for: + +* `domain` – core business rules (no Spring dependencies) +* `application` – use cases orchestrating ports +* `adapter` – inbound & outbound adapters +* `bootstrap` – configuration and wiring + +If you also enabled **sample code**: + +```bash +--sample-code basic +``` + +then the project includes a minimal but complete **greeting flow** wired end-to-end: + +* **Domain & ports** – greeting model and port contracts +* **Application** – greeting use case orchestration +* **REST adapter** – sample controller exposing: + +```bash +GET /api/v1/sample/greetings/default +→ 200 OK +{ +"text": "Hello from hexagonal sample!" +} +``` + +You can use this sample in two ways: + +* As a **teaching reference** for hexagonal structure in this codebase +* As a **starting slice** to evolve into your real business modules + + + +--- + +## 📚 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** 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..31e2c48 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/application.yml.ftl @@ -0,0 +1,8 @@ +spring: + application: + name: ${applicationName} + # 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/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl new file mode 100644 index 0000000..5731012 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl @@ -0,0 +1,61 @@ +package ${projectPackageName}.adapter.sample.greeting.in.rest; + +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingQuery; +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingResult; +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingUseCase; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Inbound REST adapter for Greeting sample. + * Responsibilities: + * - translate HTTP requests into queries + * - invoke the application layer (use cases) + * - translate results into HTTP responses + */ +@RestController +@RequestMapping( + path = "/api/v1/sample/greetings", + produces = MediaType.APPLICATION_JSON_VALUE +) +public class GreetingController { + + private final GetGreetingUseCase getGreetingUseCase; + private final GreetingResponseMapper responseMapper; + + public GreetingController( + GetGreetingUseCase getGreetingUseCase, + GreetingResponseMapper responseMapper) { + this.getGreetingUseCase = getGreetingUseCase; + this.responseMapper = responseMapper; + } + + /** + * GET /api/v1/sample/greetings/default + * + * Returns a default greeting from the domain. + */ + @GetMapping("/default") + public ResponseEntity getDefaultGreeting() { + GetGreetingResult result = getGreetingUseCase.getDefault(); + return ResponseEntity.ok(responseMapper.from(result)); + } + + /** + * GET /api/v1/sample/greetings + * Example: /api/v1/sample/greetings?name=John + * + * Creates a greeting personalized with a given name. + */ + @GetMapping + public ResponseEntity getPersonalGreeting( + @RequestParam(name = "name", required = false) String name) { + GetGreetingResult result = + getGreetingUseCase.getPersonal(new GetGreetingQuery(name)); + return ResponseEntity.ok(responseMapper.from(result)); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl new file mode 100644 index 0000000..42adc03 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl @@ -0,0 +1,11 @@ +package ${projectPackageName}.adapter.sample.greeting.in.rest; + +/** + * REST response DTO for the Greeting sample. + * Kept transport-friendly: + * - id is a String (UUID.toString()) + */ +public record GreetingResponse( + String id, + String text +) {} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl new file mode 100644 index 0000000..9022b4f --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl @@ -0,0 +1,22 @@ +package ${projectPackageName}.adapter.sample.greeting.in.rest; + +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingResult; + +/** + * Maps application-layer result to REST response DTO. + * GetGreetingResult → GreetingResponse + * Kept free of Spring annotations to stay framework-agnostic. + * It will be wired via a bootstrap @Configuration class. + */ +public class GreetingResponseMapper { + + public GreetingResponse from(GetGreetingResult result) { + if (result == null) { + return null; + } + return new GreetingResponse( + result.id() != null ? result.id().toString() : null, + result.text() + ); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl new file mode 100644 index 0000000..5bb714f --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl @@ -0,0 +1,34 @@ +package ${projectPackageName}.adapter.sample.greeting.out.logging; + +import ${projectPackageName}.domain.sample.greeting.model.Greeting; +import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Outbound adapter that implements {@link GreetingAuditPort} by logging events. + * Hexagonal intent: + * - Domain only knows about GreetingAuditPort + * - Infrastructure decides how to implement it (here: logging) + * This class is intentionally kept free of Spring annotations. + * It will be registered as a bean from a bootstrap @Configuration class. + */ +public class LoggingGreetingAuditAdapter implements GreetingAuditPort { + + private static final Logger log = LoggerFactory.getLogger(LoggingGreetingAuditAdapter.class); + + @Override + public void auditCreated(Greeting greeting) { + // Very simple implementation for the BASIC sample: + // just log that a greeting was created. + if (greeting == null) { + log.warn("GreetingAuditPort.auditCreated invoked with null Greeting"); + return; + } + + log.info("Greeting created: id={}, text={}", + greeting.id(), + greeting.text().value() + ); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl new file mode 100644 index 0000000..ba3f629 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl @@ -0,0 +1,50 @@ +package ${projectPackageName}.application.sample.greeting.usecase; + +import ${projectPackageName}.domain.sample.greeting.model.Greeting; +import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort; +import ${projectPackageName}.domain.sample.greeting.service.GreetingService; + +/** + * Application-layer handler for the GetGreeting use case. + * Responsibilities: + * - orchestrate domain services + * - call outbound ports (e.g. audit) + * - map domain objects to use case result DTOs + */ +public class GetGreetingHandler implements GetGreetingUseCase { + + private final GreetingService greetingService; + private final GreetingAuditPort auditPort; + + public GetGreetingHandler(GreetingService greetingService, + GreetingAuditPort auditPort) { + this.greetingService = greetingService; + this.auditPort = auditPort; + } + + @Override + public GetGreetingResult getDefault() { + Greeting greeting = greetingService.defaultGreeting(); + auditPort.auditCreated(greeting); + return map(greeting); + } + + @Override + public GetGreetingResult getPersonal(GetGreetingQuery query) { + Greeting greeting; + if (query == null || query.name() == null) { + greeting = greetingService.defaultGreeting(); + } else { + greeting = greetingService.createPersonalGreeting(query.name()); + } + auditPort.auditCreated(greeting); + return map(greeting); + } + + private GetGreetingResult map(Greeting greeting) { + return GetGreetingResult.of( + greeting.id().value(), + greeting.text().value() + ); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl new file mode 100644 index 0000000..8f5b989 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl @@ -0,0 +1,14 @@ +package ${projectPackageName}.application.sample.greeting.usecase; + +/** + * Input model for the GetGreeting use case. + * Intentionally tiny: + * - shows how even simple use cases use a dedicated input type + * - can evolve (e.g. locale, channel, tenant) without breaking the interface + */ +public record GetGreetingQuery(String name) { + + public static GetGreetingQuery anonymous() { + return new GetGreetingQuery(null); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl new file mode 100644 index 0000000..0ceee92 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl @@ -0,0 +1,16 @@ +package ${projectPackageName}.application.sample.greeting.usecase; + +import java.util.UUID; + +/** + * Output model for the GetGreeting use case. + * Hexagonal intent: + * - application returns a simple, serializable shape + * - adapters (REST, CLI, etc.) can map this to their own DTOs + */ +public record GetGreetingResult(UUID id, String text) { + + public static GetGreetingResult of(UUID id, String text) { + return new GetGreetingResult(id, text); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl new file mode 100644 index 0000000..4bba29a --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl @@ -0,0 +1,14 @@ +package ${projectPackageName}.application.sample.greeting.usecase; + +/** + * Application-level use case for retrieving greetings. + * Kept small and focused on intent: + * - default greeting + * - personalized greeting + */ +public interface GetGreetingUseCase { + + GetGreetingResult getDefault(); + + GetGreetingResult getPersonal(GetGreetingQuery query); +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl new file mode 100644 index 0000000..4344f9f --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl @@ -0,0 +1,57 @@ +package ${projectPackageName}.bootstrap.sample.greeting; + +import ${projectPackageName}.adapter.sample.greeting.in.rest.GreetingResponseMapper; +import ${projectPackageName}.adapter.sample.greeting.out.logging.LoggingGreetingAuditAdapter; +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingHandler; +import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingUseCase; +import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort; +import ${projectPackageName}.domain.sample.greeting.service.GreetingService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Bootstrap wiring for the Greeting BASIC sample. + * Principles: + * - Beans are exposed by their *port* types (interfaces) + * - Implementation classes stay framework-agnostic + * - Bean names reflect concrete roles + */ +@Configuration +public class GreetingSampleConfig { + + @Bean + GreetingService greetingService() { + return new GreetingService(); + } + + /** + * Outbound audit port wiring. + * Return type = port interface (Hexagonal rule) + * Bean name = concrete role (LoggingGreetingAuditAdapter) + */ + @Bean + GreetingAuditPort loggingGreetingAuditAdapter() { + return new LoggingGreetingAuditAdapter(); + } + + /** + * Use case wiring. + * Return type = use case interface + * Bean name = concrete handler implementation + */ + @Bean + GetGreetingUseCase getGreetingHandler( + GreetingService greetingService, + GreetingAuditPort loggingGreetingAuditAdapter + ) { + return new GetGreetingHandler(greetingService, loggingGreetingAuditAdapter); + } + + /** + * REST adapter helper (mapper), kept free of Spring annotations. + */ + @Bean + GreetingResponseMapper greetingResponseMapper() { + return new GreetingResponseMapper(); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl new file mode 100644 index 0000000..f365433 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl @@ -0,0 +1,46 @@ +package ${projectPackageName}.domain.sample.greeting.model; + +import ${projectPackageName}.domain.sample.greeting.model.value.GreetingId; +import ${projectPackageName}.domain.sample.greeting.model.value.GreetingText; + +/** + * Aggregate root for the Greeting sample. + * Intent: + * - show a small, self-contained aggregate + * - keep invariants inside value objects + * - keep this class as a simple composition of those values + */ +public class Greeting { + + private final GreetingId id; + private final GreetingText text; + + private Greeting(GreetingId id, GreetingText text) { + this.id = id; + this.text = text; + } + + public static Greeting of(GreetingId id, GreetingText text) { + // Invariants are already enforced in value objects + return new Greeting(id, text); + } + + public static Greeting createDefault() { + return new Greeting( + GreetingId.newId(), + GreetingText.of("Hello from hexagonal sample!") + ); + } + + public GreetingId id() { + return id; + } + + public GreetingText text() { + return text; + } + + public String render() { + return text.value(); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl new file mode 100644 index 0000000..7a2b655 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl @@ -0,0 +1,27 @@ +package ${projectPackageName}.domain.sample.greeting.model.value; + +import java.util.Objects; +import java.util.UUID; + +/** + * Strongly-typed identifier for the Greeting aggregate. + */ +public record GreetingId(UUID value) { + + public GreetingId { + Objects.requireNonNull(value, "value must not be null"); + } + + public static GreetingId newId() { + return new GreetingId(UUID.randomUUID()); + } + + public static GreetingId fromString(String raw) { + return new GreetingId(UUID.fromString(raw)); + } + + @Override + public String toString() { + return value.toString(); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl new file mode 100644 index 0000000..0d51842 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl @@ -0,0 +1,34 @@ +package ${projectPackageName}.domain.sample.greeting.model.value; + +import java.util.Objects; + +/** + * Value Object capturing the greeting text with basic invariants. + * Kept intentionally small and self-contained, to show: + * - trimming + * - length invariant + * - not-blank constraint + */ +public record GreetingText(String value) { + + private static final int MAX_LENGTH = 140; + + public GreetingText { + Objects.requireNonNull(value, "value must not be null"); + + String trimmed = value.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("Greeting text must not be blank"); + } + if (trimmed.length() > MAX_LENGTH) { + throw new IllegalArgumentException( + "Greeting text must not exceed " + MAX_LENGTH + " characters" + ); + } + value = trimmed; + } + + public static GreetingText of(String raw) { + return new GreetingText(raw); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl new file mode 100644 index 0000000..0f49097 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl @@ -0,0 +1,18 @@ +package ${projectPackageName}.domain.sample.greeting.port.out; + +import ${projectPackageName}.domain.sample.greeting.model.Greeting; + +/** + * Outbound port for auditing greeting creations. + * Hexagonal intent: + * - Domain expresses the need for persistence/logging + * - Without depending on any technology (DB, File, Kafka, etc.) + */ +public interface GreetingAuditPort { + + /** + * Publish or persist that a Greeting was created. + * Infrastructure (adapter) decides how (log, file, DB, etc.) + */ + void auditCreated(Greeting greeting); +} \ No newline at end of file diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl new file mode 100644 index 0000000..a55c378 --- /dev/null +++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl @@ -0,0 +1,47 @@ +package ${projectPackageName}.domain.sample.greeting.service; + +import ${projectPackageName}.domain.sample.greeting.model.Greeting; +import ${projectPackageName}.domain.sample.greeting.model.value.GreetingId; +import ${projectPackageName}.domain.sample.greeting.model.value.GreetingText; + +/** + * Domain service for the Greeting aggregate. + * Intent: + * - keep aggregate + value objects small and focused + * - provide simple creation/use flows that can evolve over time + */ +public class GreetingService { + + /** + * Returns a default greeting using the aggregate factory. + */ + public Greeting defaultGreeting() { + return Greeting.createDefault(); + } + + /** + * Creates a Greeting from a raw text value. + * All invariants are enforced by {@link GreetingText}. + */ + public Greeting createGreeting(String rawText) { + return Greeting.of( + GreetingId.newId(), + GreetingText.of(rawText) + ); + } + + /** + * Creates a simple "Hello, !" style greeting. + * Shows a tiny bit of orchestration logic in the domain layer. + */ + public Greeting createPersonalGreeting(String name) { + String safeName = (name == null) ? "there" : name.trim(); + if (safeName.isEmpty()) { + safeName = "there"; + } + return Greeting.of( + GreetingId.newId(), + GreetingText.of("Hello, " + safeName + "!") + ); + } +} \ 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..5b9b41e --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java @@ -0,0 +1,115 @@ +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.sample.SampleCodeLevel; +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.sampleCode = SampleCodeLevel.NONE; + 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.sampleCodeLevelKey()).isEqualTo(SampleCodeLevel.NONE.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..3ea0e58 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java @@ -0,0 +1,90 @@ +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.sample.SampleCodeOptions; +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.GeneratedResource; +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; + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return ProjectBlueprintFactory.of( + identity, + name, + description, + packageName, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } +} 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..859f8ed --- /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.GeneratedResource; +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, 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, 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..b357916 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java @@ -0,0 +1,160 @@ +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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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)); + + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + target, + dependencies, + sampleCodeOptions); + } + + @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"); + GeneratedTextResource dummyFile = + new GeneratedTextResource(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..561db16 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java @@ -0,0 +1,117 @@ +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.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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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 ApplicationYamlAdapterTest { + + 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()); + + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + @Test + @DisplayName("artifactKey() should return APP_CONFIG") + 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 applicationName from artifactId 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 = blueprint(); + + Path relativePath = Path.of("src/main/resources/application.yml"); + GeneratedTextResource expectedFile = + new GeneratedTextResource( + relativePath, "spring:\n application:\n name: demo-app\n", 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"); + + Map model = renderer.capturedModel; + assertThat(model).isNotNull().containsEntry("applicationName", "demo-app"); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java new file mode 100644 index 0000000..702816d --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java @@ -0,0 +1,158 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.doc; + +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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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)); + + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + target, + dependencies, + sampleCodeOptions); + } + + @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"); + GeneratedTextResource dummyFile = + new GeneratedTextResource(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..d6ec3d0 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java @@ -0,0 +1,68 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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, null); + + Path relativePath = Path.of(".gitignore"); + GeneratedTextResource expectedFile = + new GeneratedTextResource(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/sample/SampleCodeAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapterTest.java new file mode 100644 index 0000000..a3090e2 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapterTest.java @@ -0,0 +1,179 @@ +package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.sample; + +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.SamplesProperties; +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.sample.SampleCodeLevel; +import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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") +@Tag("adapter") +class SampleCodeAdapterTest { + + private static final String BASE_PATH = "springboot/maven/java/"; + private static final String STANDARD_SAMPLES_ROOT = "sample/standard"; + private static final String HEXAGONAL_SAMPLES_ROOT = "sample/hexagonal"; + private static final String BASIC_DIR_NAME = "basic"; + private static final String RICH_DIR_NAME = "rich"; + + private static final String BASE_PACKAGE = "com.acme.demo"; + private static final String BASE_PACKAGE_PATH = "com/acme/demo"; + + private static final ArtifactDefinition DUMMY_ARTIFACT_DEFINITION = + new ArtifactDefinition(BASE_PATH, List.of()); + + private static final SamplesProperties SAMPLES_PROPERTIES = + new SamplesProperties( + STANDARD_SAMPLES_ROOT, HEXAGONAL_SAMPLES_ROOT, BASIC_DIR_NAME, RICH_DIR_NAME); + + private static ProjectBlueprint blueprint(ProjectLayout layout, SampleCodeLevel level) { + + 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(BASE_PACKAGE); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + PlatformTarget platformTarget = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependencies dependencies = Dependencies.of(List.of()); + SampleCodeOptions sampleCodeOptions = new SampleCodeOptions(level); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + private static List toRelativePaths(Iterable resources) { + List result = new ArrayList<>(); + StreamSupport.stream(resources.spliterator(), false).forEach(r -> result.add(r.relativePath())); + return result; + } + + @Test + @DisplayName("artifactKey() should return SAMPLE_CODE") + void artifactKey_shouldReturnSampleCode() { + SampleCodeAdapter adapter = + new SampleCodeAdapter( + new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.SAMPLE_CODE); + } + + @Test + @DisplayName("generate() should return empty when sample level is NONE") + void generate_noneLevel_shouldReturnEmpty() { + SampleCodeAdapter adapter = + new SampleCodeAdapter( + new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES); + + ProjectBlueprint blueprint = blueprint(ProjectLayout.HEXAGONAL, SampleCodeLevel.NONE); + + Iterable resources = adapter.generate(blueprint); + + assertThat(resources).isEmpty(); + } + + @Test + @DisplayName("generate() should return empty when layout is STANDARD even if level is BASIC") + void generate_standardLayoutBasicLevel_shouldReturnEmpty() { + SampleCodeAdapter adapter = + new SampleCodeAdapter( + new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES); + + ProjectBlueprint blueprint = blueprint(ProjectLayout.STANDARD, SampleCodeLevel.BASIC); + + Iterable resources = adapter.generate(blueprint); + + assertThat(resources).isEmpty(); + } + + @Test + @DisplayName("generate() should resolve hexagonal BASIC templates and map them to Java sources") + void generate_hexagonalBasic_shouldGenerateSampleSources() { + RecordingTemplateRenderer renderer = new RecordingTemplateRenderer(); + + SampleCodeAdapter adapter = + new SampleCodeAdapter(renderer, DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES); + + ProjectBlueprint blueprint = blueprint(ProjectLayout.HEXAGONAL, SampleCodeLevel.BASIC); + + Iterable resources = adapter.generate(blueprint); + List paths = toRelativePaths(resources); + + assertThat(paths).isNotEmpty(); + + Path expectedGreetingControllerPath = + Path.of("src/main/java") + .resolve(BASE_PACKAGE_PATH) + .resolve("adapter/sample/greeting/in/rest/GreetingController.java"); + + assertThat(paths).contains(expectedGreetingControllerPath); + + String expectedTemplateName = + "springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl"; + + assertThat(renderer.capturedTemplateNames).contains(expectedTemplateName); + + assertThat(renderer.capturedModels) + .allMatch(model -> BASE_PACKAGE.equals(model.get("projectPackageName"))); + } + + private static final class RecordingTemplateRenderer implements TemplateRenderer { + + private final List capturedTemplateNames = new ArrayList<>(); + private final List> capturedModels = new ArrayList<>(); + + @Override + public GeneratedResource renderUtf8( + Path outPath, String templateName, Map model) { + + capturedTemplateNames.add(templateName); + capturedModels.add(model); + + return new GeneratedTextResource(outPath, "", StandardCharsets.UTF_8); + } + } +} 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..c18496b --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapterTest.java @@ -0,0 +1,86 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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, null); + + Path expectedPath = Path.of("src/main/java/com/acme/demo/DemoApplication.java"); + + GeneratedTextResource expectedFile = + new GeneratedTextResource(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..e351dcc --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapterTest.java @@ -0,0 +1,121 @@ +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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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()); + + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + @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"); + + GeneratedTextResource expectedFile = + new GeneratedTextResource( + 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/source/SourceLayoutAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapterTest.java new file mode 100644 index 0000000..a11359f --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapterTest.java @@ -0,0 +1,148 @@ +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.application.port.out.artifact.ArtifactKey; +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.sample.SampleCodeOptions; +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.GeneratedDirectory; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource; +import java.nio.file.Path; +import java.util.ArrayList; +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") +@Tag("adapter") +class SourceLayoutAdapterTest { + + private static final String BASE_PACKAGE = "com.acme.demo"; + private static final String BASE_PACKAGE_PATH = "com/acme/demo"; + + private static ProjectBlueprint blueprint(ProjectLayout layout) { + 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(BASE_PACKAGE); + + TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA); + PlatformTarget platformTarget = + new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5); + + Dependencies dependencies = Dependencies.of(List.of()); + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + private static List toRelativePaths(Iterable resources) { + List result = new ArrayList<>(); + StreamSupport.stream(resources.spliterator(), false) + .forEach( + r -> { + assertThat(r).isInstanceOf(GeneratedDirectory.class); + result.add(r.relativePath()); + }); + return result; + } + + @Test + @DisplayName("artifactKey() should return SOURCE_LAYOUT") + void artifactKey_shouldReturnSourceLayout() { + SourceLayoutAdapter adapter = new SourceLayoutAdapter(); + + assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.SOURCE_LAYOUT); + } + + @Test + @DisplayName("generate() with STANDARD layout should create base and package directories only") + void generate_standardLayout_shouldCreateBaseAndPackageDirsOnly() { + SourceLayoutAdapter adapter = new SourceLayoutAdapter(); + ProjectBlueprint blueprint = blueprint(ProjectLayout.STANDARD); + + Iterable resources = adapter.generate(blueprint); + List paths = toRelativePaths(resources); + + List expected = + List.of( + Path.of("src/main/java"), + Path.of("src/test/java"), + Path.of("src/main/resources"), + Path.of("src/test/resources"), + Path.of("src/main/java").resolve(BASE_PACKAGE_PATH), + Path.of("src/test/java").resolve(BASE_PACKAGE_PATH)); + + assertThat(paths).containsExactlyInAnyOrderElementsOf(expected).hasSize(expected.size()); + + // Hexagonal’a özel alt dizinler olmamalı + assertThat(paths) + .noneMatch( + p -> + p.toString().endsWith("/adapter") + || p.toString().endsWith("/application") + || p.toString().endsWith("/bootstrap") + || p.toString().endsWith("/domain")); + } + + @Test + @DisplayName( + "generate() with HEXAGONAL layout should create base/package directories and hexagonal sub-packages") + void generate_hexagonalLayout_shouldCreateHexagonalSubPackages() { + SourceLayoutAdapter adapter = new SourceLayoutAdapter(); + ProjectBlueprint blueprint = blueprint(ProjectLayout.HEXAGONAL); + + Iterable resources = adapter.generate(blueprint); + List paths = toRelativePaths(resources); + + Path mainBase = Path.of("src/main/java").resolve(BASE_PACKAGE_PATH); + Path testBase = Path.of("src/test/java").resolve(BASE_PACKAGE_PATH); + + List expected = + List.of( + // base source/resources dizinleri + Path.of("src/main/java"), + Path.of("src/test/java"), + Path.of("src/main/resources"), + Path.of("src/test/resources"), + // base package dizinleri + mainBase, + testBase, + // hexagonal alt paketleri + mainBase.resolve("adapter"), + mainBase.resolve("application"), + mainBase.resolve("bootstrap"), + mainBase.resolve("domain")); + + assertThat(paths).containsExactlyInAnyOrderElementsOf(expected).hasSize(expected.size()); + } +} diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapterTest.java new file mode 100644 index 0000000..b3d7dd6 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapterTest.java @@ -0,0 +1,122 @@ +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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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()); + + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + + return new ProjectBlueprint( + identity, + name, + description, + pkg, + techStack, + layout, + platformTarget, + dependencies, + sampleCodeOptions); + } + + @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"); + + GeneratedTextResource expectedFile = + new GeneratedTextResource( + 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..1ab4093 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapterTest.java @@ -0,0 +1,76 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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, null); + + Path relativePath = Path.of(".mvn/wrapper/maven-wrapper.properties"); + GeneratedTextResource expectedFile = + new GeneratedTextResource(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..ec172ce --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapterTest.java @@ -0,0 +1,74 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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, null); + + Path relativePath = Path.of("output/test.txt"); + GeneratedTextResource expectedFile = + new GeneratedTextResource(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..8cd46b8 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandlerTest.java @@ -0,0 +1,176 @@ +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.sample.SampleCodeOptions; +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +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, + platformTarget, + List.of(), + SampleCodeOptions.none(), + 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 GeneratedTextResource(Path.of("pom.xml"), "", StandardCharsets.UTF_8), + new GeneratedTextResource(Path.of(".gitignore"), "*.class", StandardCharsets.UTF_8), + new GeneratedTextResource( + Path.of("src/main/java/com/acme/demo/DemoApplication.java"), + "class DemoApplication {}", + StandardCharsets.UTF_8), + new GeneratedTextResource( + Path.of("src/test/java/com/acme/demo/DemoApplicationTests.java"), + "class DemoApplicationTests {}", + StandardCharsets.UTF_8), + new GeneratedTextResource( + Path.of("src/main/resources/application.yml"), + "spring:\n application:\n name: demo-app", + StandardCharsets.UTF_8), + new GeneratedTextResource( + 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) { + writtenFiles.add(relativePath); + } + + @Override + public void writeText(Path projectRoot, Path relativePath, String content, Charset charset) { + writtenFiles.add(relativePath); + } + + @Override + public void createDirectories(Path projectRoot, Path relativeDir) { + // no directory creation + } + } + + 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..c4980af --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapperTest.java @@ -0,0 +1,92 @@ +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.sample.SampleCodeOptions; +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 dependencies = + 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(dependencies); + + return mapper.from(cmd); + } + + private static CreateProjectCommand getCreateProjectCommand(List dependencies) { + 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, + dependencies, + SampleCodeOptions.none(), + 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..a13de4a --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesPropertiesTest.java @@ -0,0 +1,120 @@ +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), defaultSamplesProperties()); + } + + private static SamplesProperties defaultSamplesProperties() { + return new SamplesProperties("standard/sample", "hexagonal/sample", "basic", "rich"); + } + + @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), defaultSamplesProperties()); + + 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(), defaultSamplesProperties()); + + 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), defaultSamplesProperties()); + + 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..7476bcb --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactoryTest.java @@ -0,0 +1,212 @@ +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.sample.SampleCodeOptions; +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 SampleCodeOptions sampleCodeOptions() { + return SampleCodeOptions.none(); + } + + 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(); + SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none(); + ProjectBlueprint bp = + ProjectBlueprintFactory.of( + identity, + name, + description, + packageName, + stack, + layout, + target, + dependencies, + sampleCodeOptions); + + 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(), + sampleCodeOptions())) + .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, + dependencies(), + sampleCodeOptions())) + .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(), + sampleCodeOptions())) + .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/sample/SampleCodeOptionsTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptionsTest.java new file mode 100644 index 0000000..0b573b3 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptionsTest.java @@ -0,0 +1,56 @@ +package io.github.blueprintplatform.codegen.domain.model.value.sample; + +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; + +@Tag("unit") +@Tag("domain") +class SampleCodeOptionsTest { + + @Test + @DisplayName("null level should default to NONE and be disabled") + void nullLevel_shouldDefaultToNoneAndBeDisabled() { + SampleCodeOptions options = new SampleCodeOptions(null); + + assertThat(options.level()).isEqualTo(SampleCodeLevel.NONE); + assertThat(options.isEnabled()).isFalse(); + } + + @Test + @DisplayName("none() factory should create disabled options with NONE level") + void noneFactory_shouldCreateDisabledNoneLevel() { + SampleCodeOptions options = SampleCodeOptions.none(); + + assertThat(options.level()).isEqualTo(SampleCodeLevel.NONE); + assertThat(options.isEnabled()).isFalse(); + } + + @Test + @DisplayName("basic() factory should create enabled options with BASIC level") + void basicFactory_shouldCreateEnabledBasicLevel() { + SampleCodeOptions options = SampleCodeOptions.basic(); + + assertThat(options.level()).isEqualTo(SampleCodeLevel.BASIC); + assertThat(options.isEnabled()).isTrue(); + } + + @Test + @DisplayName("rich() factory should create enabled options with RICH level") + void richFactory_shouldCreateEnabledRichLevel() { + SampleCodeOptions options = SampleCodeOptions.rich(); + + assertThat(options.level()).isEqualTo(SampleCodeLevel.RICH); + assertThat(options.isEnabled()).isTrue(); + } + + @Test + @DisplayName("isEnabled should be false only for NONE") + void isEnabled_shouldBeFalseOnlyForNone() { + assertThat(SampleCodeOptions.none().isEnabled()).isFalse(); + assertThat(SampleCodeOptions.basic().isEnabled()).isTrue(); + assertThat(SampleCodeOptions.rich().isEnabled()).isTrue(); + } +} 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/GeneratedResourceTest.java b/src/test/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResourceTest.java new file mode 100644 index 0000000..a4c07ee --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResourceTest.java @@ -0,0 +1,112 @@ +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 GeneratedResourceTest { + + @Test + @DisplayName("Text with valid args should be created successfully") + void text_validArgs_shouldCreateInstance() { + GeneratedTextResource text = new GeneratedTextResource(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 GeneratedTextResource(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 GeneratedTextResource(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}; + GeneratedBinaryResource binary = new GeneratedBinaryResource(Path.of("bin.dat"), original); + + // ctor defensive copy + original[0] = 9; + + byte[] fromGetter = binary.bytes(); + assertThat(fromGetter).containsExactly(1, 2, 3); + + // accessor defensive copy + 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 GeneratedBinaryResource(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 GeneratedBinaryResource(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() { + GeneratedBinaryResource b1 = + new GeneratedBinaryResource(Path.of("bin.dat"), new byte[] {1, 2, 3}); + GeneratedBinaryResource b2 = + new GeneratedBinaryResource(Path.of("bin.dat"), new byte[] {1, 2, 3}); + GeneratedBinaryResource b3 = + new GeneratedBinaryResource(Path.of("other.bin"), new byte[] {1, 2, 3}); + + assertThat(b1).isEqualTo(b2).hasSameHashCodeAs(b2).isNotEqualTo(b3); + } + + @Test + @DisplayName("Directory with valid path should be created successfully") + void directory_validPath_shouldCreateInstance() { + GeneratedDirectory dir = new GeneratedDirectory(Path.of("src/main/java")); + + assertThat(dir.relativePath()).isEqualTo(Path.of("src/main/java")); + assertThat(dir.toString()).contains("GeneratedDirectory"); + } + + @Test + @DisplayName("Directory with null path should fail with file.path.not.blank") + void directory_nullPath_shouldFailPathNotBlank() { + assertThatThrownBy(() -> new GeneratedDirectory(null)) + .isInstanceOfSatisfying( + DomainViolationException.class, + dve -> assertThat(dve.getMessageKey()).isEqualTo("file.path.not.blank")); + } +} 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..b81d3e8 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/CapturingTemplateRenderer.java @@ -0,0 +1,23 @@ +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.GeneratedResource; +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 GeneratedResource nextFile; + + @Override + public GeneratedResource 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..8240941 --- /dev/null +++ b/src/test/java/io/github/blueprintplatform/codegen/testsupport/templating/NoopTemplateRenderer.java @@ -0,0 +1,17 @@ +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.GeneratedResource; +import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; + +public final class NoopTemplateRenderer implements TemplateRenderer { + + @Override + public GeneratedResource renderUtf8( + Path outPath, String templateName, Map model) { + return new GeneratedTextResource(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; - } -}