From eed8f85cd77b0ac068afce41303d83f2d4888d78 Mon Sep 17 00:00:00 2001 From: Kamil Sulejewski Date: Tue, 14 Jan 2025 01:00:18 +0100 Subject: [PATCH 1/4] feat: craft, dedicated template, validation pattern as model. --- build.gradle | 1 + settings.gradle | 2 +- .../commit/craft/CommitCraftApplication.java | 13 +++ .../controller/CommitTranslateController.java | 8 +- .../controller/CommitTranslateRequest.java | 2 +- .../error/ErrorResponseCommiting.java | 2 +- .../error/RestExceptionHandler.java | 2 +- .../flow/CommitFlowController.java | 8 +- .../flow/CommitFlowRequest.java | 2 +- .../pattern/BasicModelPattern.java | 2 +- .../pattern/CommitModelPattern.java | 2 +- .../quick/QuickCommitController.java | 6 +- .../quick/QuickCommitRequest.java | 2 +- .../quick/QuickCommitService.java | 2 +- .../{gen => craft}/service/CommitService.java | 10 +- .../{gen => craft}/service/CommitType.java | 2 +- .../{gen => craft}/service/MajorNumber.java | 2 +- .../service/MajorNumberPreparer.java | 2 +- .../craft/template/CommitCraftJson.java | 22 ++++ .../craft/template/CommitCraftTemplate.java | 17 +++ .../CommitCraftTemplateController.java | 52 +++++++++ .../CommitDedicatedTemplateValidator.java | 42 +++++++ .../craft/template/ProjectTemplate.java | 14 +++ .../craft/template/TemplateService.java | 104 ++++++++++++++++++ .../pl/commit/gen/CommitGenApplication.java | 13 --- src/main/resources/application-kam.yml | 2 + .../templates/dedicated-meta-schema.json | 26 +++++ src/main/resources/templates/meta-schema.json | 46 ++++++++ .../CommitCraftApplicationTests.java} | 4 +- .../flow/CommitFlowControllerTest.java | 8 +- .../pattern/CommitModelPatternTest.java | 2 +- .../quick/QuickCommitServiceTest.java | 2 +- .../service/CommitServiceTest.java | 14 +-- .../service/MajorNumberPreparerTest.java | 2 +- .../{ => craft}/translate/DeeplResponse.java | 2 +- .../{ => craft}/translate/DeeplWebClient.java | 4 +- .../craft/translate/TranslateCommitCraft.java | 5 + .../translate/config/WebClientConfig.java | 2 +- .../commit/translate/TranslateCommiting.java | 8 -- translate/src/main/resources/application.yml | 2 +- 40 files changed, 393 insertions(+), 70 deletions(-) create mode 100644 src/main/java/pl/commit/craft/CommitCraftApplication.java rename src/main/java/pl/commit/{gen => craft}/controller/CommitTranslateController.java (83%) rename src/main/java/pl/commit/{gen => craft}/controller/CommitTranslateRequest.java (84%) rename src/main/java/pl/commit/{gen => craft}/error/ErrorResponseCommiting.java (95%) rename src/main/java/pl/commit/{gen => craft}/error/RestExceptionHandler.java (96%) rename src/main/java/pl/commit/{gen => craft}/flow/CommitFlowController.java (86%) rename src/main/java/pl/commit/{gen => craft}/flow/CommitFlowRequest.java (86%) rename src/main/java/pl/commit/{gen => craft}/pattern/BasicModelPattern.java (90%) rename src/main/java/pl/commit/{gen => craft}/pattern/CommitModelPattern.java (99%) rename src/main/java/pl/commit/{gen => craft}/quick/QuickCommitController.java (87%) rename src/main/java/pl/commit/{gen => craft}/quick/QuickCommitRequest.java (74%) rename src/main/java/pl/commit/{gen => craft}/quick/QuickCommitService.java (96%) rename src/main/java/pl/commit/{gen => craft}/service/CommitService.java (87%) rename src/main/java/pl/commit/{gen => craft}/service/CommitType.java (94%) rename src/main/java/pl/commit/{gen => craft}/service/MajorNumber.java (57%) rename src/main/java/pl/commit/{gen => craft}/service/MajorNumberPreparer.java (96%) create mode 100644 src/main/java/pl/commit/craft/template/CommitCraftJson.java create mode 100644 src/main/java/pl/commit/craft/template/CommitCraftTemplate.java create mode 100644 src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java create mode 100644 src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java create mode 100644 src/main/java/pl/commit/craft/template/ProjectTemplate.java create mode 100644 src/main/java/pl/commit/craft/template/TemplateService.java delete mode 100644 src/main/java/pl/commit/gen/CommitGenApplication.java create mode 100644 src/main/resources/application-kam.yml create mode 100644 src/main/resources/templates/dedicated-meta-schema.json create mode 100644 src/main/resources/templates/meta-schema.json rename src/test/java/pl/commit/{gen/CommitGenApplicationTests.java => craft/CommitCraftApplicationTests.java} (71%) rename src/test/java/pl/commit/{gen => craft}/flow/CommitFlowControllerTest.java (91%) rename src/test/java/pl/commit/{gen => craft}/pattern/CommitModelPatternTest.java (98%) rename src/test/java/pl/commit/{gen => craft}/quick/QuickCommitServiceTest.java (98%) rename src/test/java/pl/commit/{gen => craft}/service/CommitServiceTest.java (86%) rename src/test/java/pl/commit/{gen => craft}/service/MajorNumberPreparerTest.java (98%) rename translate/src/main/java/pl/commit/{ => craft}/translate/DeeplResponse.java (89%) rename translate/src/main/java/pl/commit/{ => craft}/translate/DeeplWebClient.java (94%) create mode 100644 translate/src/main/java/pl/commit/craft/translate/TranslateCommitCraft.java rename translate/src/main/java/pl/commit/{ => craft}/translate/config/WebClientConfig.java (91%) delete mode 100644 translate/src/main/java/pl/commit/translate/TranslateCommiting.java diff --git a/build.gradle b/build.gradle index 2673540..5fdc404 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.modelmapper:modelmapper:3.2.2' } dependencyManagement { diff --git a/settings.gradle b/settings.gradle index ed01c7a..b8713be 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ -rootProject.name = 'gen' +rootProject.name = 'commit-craft' include 'translate' diff --git a/src/main/java/pl/commit/craft/CommitCraftApplication.java b/src/main/java/pl/commit/craft/CommitCraftApplication.java new file mode 100644 index 0000000..025cd4b --- /dev/null +++ b/src/main/java/pl/commit/craft/CommitCraftApplication.java @@ -0,0 +1,13 @@ +package pl.commit.craft; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"pl.commit.craft"}) +public class CommitCraftApplication { + + public static void main(String[] args) { + SpringApplication.run(CommitCraftApplication.class, args); + } + +} diff --git a/src/main/java/pl/commit/gen/controller/CommitTranslateController.java b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java similarity index 83% rename from src/main/java/pl/commit/gen/controller/CommitTranslateController.java rename to src/main/java/pl/commit/craft/controller/CommitTranslateController.java index daa073d..4017cec 100644 --- a/src/main/java/pl/commit/gen/controller/CommitTranslateController.java +++ b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java @@ -1,10 +1,10 @@ -package pl.commit.gen.controller; +package pl.commit.craft.controller; import org.springframework.web.bind.annotation.*; -import pl.commit.gen.service.CommitService; +import pl.commit.craft.service.CommitService; @RestController -@RequestMapping("/api/commit-translate") +@RequestMapping("/api/v1/commit-translate") public class CommitTranslateController { private final CommitService commitService; @@ -13,7 +13,7 @@ public CommitTranslateController(CommitService commitService) { this.commitService = commitService; } - @PostMapping("/generate") + @PostMapping("/craft") public String generateCommit(@RequestBody CommitTranslateRequest commitTranslateRequest) { return commitService.generateTranslateCommit( commitTranslateRequest.major(), diff --git a/src/main/java/pl/commit/gen/controller/CommitTranslateRequest.java b/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java similarity index 84% rename from src/main/java/pl/commit/gen/controller/CommitTranslateRequest.java rename to src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java index 672234a..e8a7bff 100644 --- a/src/main/java/pl/commit/gen/controller/CommitTranslateRequest.java +++ b/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.controller; +package pl.commit.craft.controller; public record CommitTranslateRequest( String major, diff --git a/src/main/java/pl/commit/gen/error/ErrorResponseCommiting.java b/src/main/java/pl/commit/craft/error/ErrorResponseCommiting.java similarity index 95% rename from src/main/java/pl/commit/gen/error/ErrorResponseCommiting.java rename to src/main/java/pl/commit/craft/error/ErrorResponseCommiting.java index d3fe0ac..6ba784d 100644 --- a/src/main/java/pl/commit/gen/error/ErrorResponseCommiting.java +++ b/src/main/java/pl/commit/craft/error/ErrorResponseCommiting.java @@ -1,4 +1,4 @@ -package pl.commit.gen.error; +package pl.commit.craft.error; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/pl/commit/gen/error/RestExceptionHandler.java b/src/main/java/pl/commit/craft/error/RestExceptionHandler.java similarity index 96% rename from src/main/java/pl/commit/gen/error/RestExceptionHandler.java rename to src/main/java/pl/commit/craft/error/RestExceptionHandler.java index 40546a8..9255ea9 100644 --- a/src/main/java/pl/commit/gen/error/RestExceptionHandler.java +++ b/src/main/java/pl/commit/craft/error/RestExceptionHandler.java @@ -1,4 +1,4 @@ -package pl.commit.gen.error; +package pl.commit.craft.error; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/pl/commit/gen/flow/CommitFlowController.java b/src/main/java/pl/commit/craft/flow/CommitFlowController.java similarity index 86% rename from src/main/java/pl/commit/gen/flow/CommitFlowController.java rename to src/main/java/pl/commit/craft/flow/CommitFlowController.java index bac1520..9bded34 100644 --- a/src/main/java/pl/commit/gen/flow/CommitFlowController.java +++ b/src/main/java/pl/commit/craft/flow/CommitFlowController.java @@ -1,13 +1,13 @@ -package pl.commit.gen.flow; +package pl.commit.craft.flow; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import pl.commit.gen.service.CommitService; +import pl.commit.craft.service.CommitService; @RestController -@RequestMapping("/api/commit-flow") +@RequestMapping("/api/v1/commit-flow") public class CommitFlowController { private final CommitService commitService; @@ -16,7 +16,7 @@ public CommitFlowController(CommitService commitService) { this.commitService = commitService; } - @PostMapping("/generate") + @PostMapping("/craft") public String generateCommit(@RequestBody CommitFlowRequest commitFlowRequest) { return commitService.generateFlowCommit( commitFlowRequest.major(), diff --git a/src/main/java/pl/commit/gen/flow/CommitFlowRequest.java b/src/main/java/pl/commit/craft/flow/CommitFlowRequest.java similarity index 86% rename from src/main/java/pl/commit/gen/flow/CommitFlowRequest.java rename to src/main/java/pl/commit/craft/flow/CommitFlowRequest.java index 714e2ee..b057a7a 100644 --- a/src/main/java/pl/commit/gen/flow/CommitFlowRequest.java +++ b/src/main/java/pl/commit/craft/flow/CommitFlowRequest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.flow; +package pl.commit.craft.flow; record CommitFlowRequest( String major, diff --git a/src/main/java/pl/commit/gen/pattern/BasicModelPattern.java b/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java similarity index 90% rename from src/main/java/pl/commit/gen/pattern/BasicModelPattern.java rename to src/main/java/pl/commit/craft/pattern/BasicModelPattern.java index 783b5df..f6c6e65 100644 --- a/src/main/java/pl/commit/gen/pattern/BasicModelPattern.java +++ b/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java @@ -1,4 +1,4 @@ -package pl.commit.gen.pattern; +package pl.commit.craft.pattern; sealed class BasicModelPattern permits CommitModelPattern { private static final String TARGET_LANG = "EN"; diff --git a/src/main/java/pl/commit/gen/pattern/CommitModelPattern.java b/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java similarity index 99% rename from src/main/java/pl/commit/gen/pattern/CommitModelPattern.java rename to src/main/java/pl/commit/craft/pattern/CommitModelPattern.java index 971ba42..d2c2169 100644 --- a/src/main/java/pl/commit/gen/pattern/CommitModelPattern.java +++ b/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java @@ -1,4 +1,4 @@ -package pl.commit.gen.pattern; +package pl.commit.craft.pattern; public final class CommitModelPattern extends BasicModelPattern { private static final String GIT_COMMAND = "git commit -m"; diff --git a/src/main/java/pl/commit/gen/quick/QuickCommitController.java b/src/main/java/pl/commit/craft/quick/QuickCommitController.java similarity index 87% rename from src/main/java/pl/commit/gen/quick/QuickCommitController.java rename to src/main/java/pl/commit/craft/quick/QuickCommitController.java index 55a1afa..120b585 100644 --- a/src/main/java/pl/commit/gen/quick/QuickCommitController.java +++ b/src/main/java/pl/commit/craft/quick/QuickCommitController.java @@ -1,4 +1,4 @@ -package pl.commit.gen.quick; +package pl.commit.craft.quick; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; @@ -7,13 +7,13 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/quick-commit") +@RequestMapping("/api/v1/quick-commit") @RequiredArgsConstructor public class QuickCommitController { final QuickCommitService quickCommitService; - @PostMapping("/generate") + @PostMapping("/craft") public String generateCommit(@RequestBody QuickCommitRequest quickCommitRequest) { return quickCommitService.generateQuickCommit( quickCommitRequest.topicScope(), diff --git a/src/main/java/pl/commit/gen/quick/QuickCommitRequest.java b/src/main/java/pl/commit/craft/quick/QuickCommitRequest.java similarity index 74% rename from src/main/java/pl/commit/gen/quick/QuickCommitRequest.java rename to src/main/java/pl/commit/craft/quick/QuickCommitRequest.java index 3e3cf7d..51c197b 100644 --- a/src/main/java/pl/commit/gen/quick/QuickCommitRequest.java +++ b/src/main/java/pl/commit/craft/quick/QuickCommitRequest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.quick; +package pl.commit.craft.quick; record QuickCommitRequest( String topicScope, diff --git a/src/main/java/pl/commit/gen/quick/QuickCommitService.java b/src/main/java/pl/commit/craft/quick/QuickCommitService.java similarity index 96% rename from src/main/java/pl/commit/gen/quick/QuickCommitService.java rename to src/main/java/pl/commit/craft/quick/QuickCommitService.java index 1c4c8fc..dbd3e96 100644 --- a/src/main/java/pl/commit/gen/quick/QuickCommitService.java +++ b/src/main/java/pl/commit/craft/quick/QuickCommitService.java @@ -1,4 +1,4 @@ -package pl.commit.gen.quick; +package pl.commit.craft.quick; import org.springframework.stereotype.Service; diff --git a/src/main/java/pl/commit/gen/service/CommitService.java b/src/main/java/pl/commit/craft/service/CommitService.java similarity index 87% rename from src/main/java/pl/commit/gen/service/CommitService.java rename to src/main/java/pl/commit/craft/service/CommitService.java index 8395912..6f218e2 100644 --- a/src/main/java/pl/commit/gen/service/CommitService.java +++ b/src/main/java/pl/commit/craft/service/CommitService.java @@ -1,15 +1,15 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import pl.commit.gen.pattern.CommitModelPattern; -import pl.commit.translate.TranslateCommiting; +import pl.commit.craft.pattern.CommitModelPattern; +import pl.commit.craft.translate.TranslateCommitCraft; @Service @RequiredArgsConstructor public class CommitService { - private final TranslateCommiting translateCommiting; + private final TranslateCommitCraft translateCommitCraft; public String generateTranslateCommit(String major, String type, String component, String changeDescription, String details, boolean wholeGitCommand) { if (isValidType(type)) { @@ -51,7 +51,7 @@ public String generateFlowCommit(String major, String type, String component, St } private String getChangeDescriptionTranslated(String changeDescription) { - return translateCommiting.translate(changeDescription, CommitModelPattern.getTargetLanguage()); + return translateCommitCraft.translate(changeDescription, CommitModelPattern.getTargetLanguage()); } private boolean isValidType(String type) { diff --git a/src/main/java/pl/commit/gen/service/CommitType.java b/src/main/java/pl/commit/craft/service/CommitType.java similarity index 94% rename from src/main/java/pl/commit/gen/service/CommitType.java rename to src/main/java/pl/commit/craft/service/CommitType.java index 913c2a6..d16e9e0 100644 --- a/src/main/java/pl/commit/gen/service/CommitType.java +++ b/src/main/java/pl/commit/craft/service/CommitType.java @@ -1,4 +1,4 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; import lombok.Getter; diff --git a/src/main/java/pl/commit/gen/service/MajorNumber.java b/src/main/java/pl/commit/craft/service/MajorNumber.java similarity index 57% rename from src/main/java/pl/commit/gen/service/MajorNumber.java rename to src/main/java/pl/commit/craft/service/MajorNumber.java index 7e606d8..d5ab480 100644 --- a/src/main/java/pl/commit/gen/service/MajorNumber.java +++ b/src/main/java/pl/commit/craft/service/MajorNumber.java @@ -1,3 +1,3 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; record MajorNumber(String issueNumber) { } diff --git a/src/main/java/pl/commit/gen/service/MajorNumberPreparer.java b/src/main/java/pl/commit/craft/service/MajorNumberPreparer.java similarity index 96% rename from src/main/java/pl/commit/gen/service/MajorNumberPreparer.java rename to src/main/java/pl/commit/craft/service/MajorNumberPreparer.java index ccb394f..af2742e 100644 --- a/src/main/java/pl/commit/gen/service/MajorNumberPreparer.java +++ b/src/main/java/pl/commit/craft/service/MajorNumberPreparer.java @@ -1,4 +1,4 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; import lombok.Getter; diff --git a/src/main/java/pl/commit/craft/template/CommitCraftJson.java b/src/main/java/pl/commit/craft/template/CommitCraftJson.java new file mode 100644 index 0000000..8cf76c4 --- /dev/null +++ b/src/main/java/pl/commit/craft/template/CommitCraftJson.java @@ -0,0 +1,22 @@ +package pl.commit.craft.template; + +import lombok.Getter; +import java.util.HashMap; +import java.util.Map; + +@Getter +public class CommitCraftJson { + private Map jsonContent; + + public CommitCraftJson() { + jsonContent = new HashMap<>(); + } + + public void addField(String key, Object value) { + jsonContent.put(key, value); + } + + public Map getJsonContent() { + return jsonContent; + } +} diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java new file mode 100644 index 0000000..4539aec --- /dev/null +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java @@ -0,0 +1,17 @@ +package pl.commit.craft.template; + +import lombok.Data; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Data +public class CommitCraftTemplate { + @NotNull + private String name; + @NotNull + private String description; + @NotNull + private String pattern; + @NotNull + private Map model; +} diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java new file mode 100644 index 0000000..a20e618 --- /dev/null +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java @@ -0,0 +1,52 @@ +package pl.commit.craft.template; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/craft-template") +@RequiredArgsConstructor +public class CommitCraftTemplateController { + + private final TemplateService templateService; + + @GetMapping + public ResponseEntity> getAllTemplates() throws IOException { + List templates = templateService.getAllTemplates(); + return ResponseEntity.ok(templates); + } + + @PostMapping("/create") + public ResponseEntity createDedicatedTemplate(@RequestBody CommitCraftTemplate template) { + try { + boolean patternAndModelScope = CommitDedicatedTemplateValidator.validatePatternAndModelScope(template); + if (patternAndModelScope) { + templateService.createDedicatedTemplate(template); + return ResponseEntity.ok("Template added successfully."); + } + return ResponseEntity.badRequest().body("Template already exists."); + } catch (IOException e) { + return ResponseEntity.status(500).body("Error adding template: " + e.getMessage()); + } + } + + @DeleteMapping("/removed/{name}") + public ResponseEntity addTemplate(@PathVariable("name") String name) { + try { + templateService.removeDedicatedTemplate(name); + return ResponseEntity.ok("Template removed successfully."); + } catch (IOException e) { + return ResponseEntity.status(500).body("Error removing template: " + e.getMessage()); + } + } + + @PostMapping("/generate-json/{name}") + public Map generateJson(@PathVariable String name) throws IOException { + CommitCraftJson commitCraftJson = templateService.prepareJsonByModel(name); + return commitCraftJson.getJsonContent(); + } +} diff --git a/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java new file mode 100644 index 0000000..64b18fd --- /dev/null +++ b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java @@ -0,0 +1,42 @@ +package pl.commit.craft.template; + +import lombok.extern.slf4j.Slf4j; +import java.util.Map; +import java.util.Set; + +@Slf4j +public class CommitDedicatedTemplateValidator { + private static final String PATTERN_VALIDATE_MODEL = "\\{(\\w+)\\}(-\\{(\\w+)\\})*"; + + public static boolean validatePatternAndModelScope(CommitCraftTemplate template) { + log.info("Validating pattern and model scope starting"); + String pattern = template.getPattern(); + Map model = template.getModel(); + + Set modelKeys = model.keySet(); + + boolean matches = true; + for (String key : modelKeys) { + if (!pattern.contains(key)) { + log.warn("Pattern is missing key: {}", key); + matches = false; + } + } + + String[] patternWords = pattern.split(PATTERN_VALIDATE_MODEL); + for (String word : patternWords) { + if (!modelKeys.contains(word)) { + log.warn("Pattern contains an extra key not in the model: {}", word); + matches = false; + } + } + + if (matches) { + log.info("Pattern matches the model keys."); + } else { + log.warn("Pattern does not match the model keys."); + } + + return matches; + } +} diff --git a/src/main/java/pl/commit/craft/template/ProjectTemplate.java b/src/main/java/pl/commit/craft/template/ProjectTemplate.java new file mode 100644 index 0000000..2ce8359 --- /dev/null +++ b/src/main/java/pl/commit/craft/template/ProjectTemplate.java @@ -0,0 +1,14 @@ +package pl.commit.craft.template; + +import lombok.Data; + +import java.util.Map; + +@Data +public class ProjectTemplate { + private String name; + private String description; + private String pattern; + private Map model; + +} diff --git a/src/main/java/pl/commit/craft/template/TemplateService.java b/src/main/java/pl/commit/craft/template/TemplateService.java new file mode 100644 index 0000000..24047fd --- /dev/null +++ b/src/main/java/pl/commit/craft/template/TemplateService.java @@ -0,0 +1,104 @@ +package pl.commit.craft.template; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class TemplateService { + public static final String PROPERTY_TEMPLATES = "templates"; + public static final String PROPERTY_DEDICATED_TEMPLATES = "dedicated"; + public static final String PATH_NAME_ELEMENT = "name"; + private static final String RESOURCES_TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; + private static final String RESOURCES_DEDICATED_META_SCHEMA_JSON = "src/main/resources/templates/dedicated-meta-schema.json"; + private final ObjectMapper objectMapper; + + public List getAllTemplates() throws IOException { + JsonNode rootNode = objectMapper.readTree(new File(RESOURCES_TEMPLATES_META_SCHEMA_JSON)); + JsonNode templatesNode = rootNode.path(PROPERTY_TEMPLATES); + List templatesList = objectMapper.readValue( + templatesNode.toString(), + new TypeReference<>() { + } + ); + List templates = new ArrayList<>(templatesList); + + JsonNode rootDedicatedNode = objectMapper.readTree(new File(RESOURCES_DEDICATED_META_SCHEMA_JSON)); + JsonNode dedicatedNode = rootDedicatedNode.path(PROPERTY_DEDICATED_TEMPLATES); + List dedicatedList = objectMapper.readValue( + dedicatedNode.toString(), + new TypeReference<>() { + } + ); + templates.addAll(dedicatedList); + return templates; + } + + public CommitCraftJson prepareJsonByModel(String name) throws IOException { + List templates = getAllTemplates(); + CommitCraftTemplate selectedTemplate = templates.stream() + .filter(template -> template.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Template not found")); + return getCommitCraftJson(selectedTemplate, selectedTemplate.getName()); + } + + private CommitCraftJson getCommitCraftJson(CommitCraftTemplate selectedTemplate, String selectedTemplateName) throws IOException { + Map model = selectedTemplate.getModel(); + CommitCraftJson commitCraftJson = new CommitCraftJson(); + commitCraftJson.addField(PATH_NAME_ELEMENT, selectedTemplateName); + for (Map.Entry entry : model.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof List list) { + commitCraftJson.addField(key, list); + } else { + commitCraftJson.addField(key, value); + } + } + return commitCraftJson; + } + + public List readTemplates() throws IOException { + JsonNode rootNode = objectMapper.readTree(new File(RESOURCES_DEDICATED_META_SCHEMA_JSON)); + JsonNode dedicatedNode = rootNode.path(PROPERTY_DEDICATED_TEMPLATES); + return objectMapper.convertValue(dedicatedNode, new TypeReference>() { + }); + } + + public void createDedicatedTemplate(CommitCraftTemplate newTemplate) throws IOException { + List templates = readTemplates(); + templates.add(newTemplate); + saveTemplates(templates); + } + + public void removeDedicatedTemplate(String dedicatedTemplateName) throws IOException { + JsonNode rootNode = objectMapper.readTree(new File(RESOURCES_DEDICATED_META_SCHEMA_JSON)); + ArrayNode dedicatedArray = (ArrayNode) rootNode.path(PROPERTY_DEDICATED_TEMPLATES); + for (Iterator nodeIterator = dedicatedArray.elements(); nodeIterator.hasNext(); ) { + JsonNode element = nodeIterator.next(); + if (dedicatedTemplateName.equals(element.path(PATH_NAME_ELEMENT).asText())) { + nodeIterator.remove(); + } + } + objectMapper.writeValue(new File(RESOURCES_DEDICATED_META_SCHEMA_JSON), rootNode); + } + + private void saveTemplates(List templates) throws IOException { + JsonNode rootNode = objectMapper.createObjectNode(); + ((ObjectNode) rootNode).putPOJO(PROPERTY_DEDICATED_TEMPLATES, templates); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(RESOURCES_DEDICATED_META_SCHEMA_JSON), rootNode); + } +} diff --git a/src/main/java/pl/commit/gen/CommitGenApplication.java b/src/main/java/pl/commit/gen/CommitGenApplication.java deleted file mode 100644 index 9689ef8..0000000 --- a/src/main/java/pl/commit/gen/CommitGenApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package pl.commit.gen; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication(scanBasePackages = {"pl.commit"}) -public class CommitGenApplication { - - public static void main(String[] args) { - SpringApplication.run(CommitGenApplication.class, args); - } - -} diff --git a/src/main/resources/application-kam.yml b/src/main/resources/application-kam.yml new file mode 100644 index 0000000..58de10c --- /dev/null +++ b/src/main/resources/application-kam.yml @@ -0,0 +1,2 @@ +server: + port: 8090 \ No newline at end of file diff --git a/src/main/resources/templates/dedicated-meta-schema.json b/src/main/resources/templates/dedicated-meta-schema.json new file mode 100644 index 0000000..7cb1979 --- /dev/null +++ b/src/main/resources/templates/dedicated-meta-schema.json @@ -0,0 +1,26 @@ +{ + "dedicated" : [ { + "name" : "apilia-project", + "description" : "Dedykowany dla projektu coś tam", + "pattern" : "{ticket_id}-{type}-{scope}-{message}-{details}", + "model" : { + "ticket_id" : "Numer powiązanego zadania w JIRA", + "type" : [ "feat", "fix", "junk", "chore", "test" ], + "scope" : "[moduł lub komponent]", + "message" : "Krótki opis zmiany", + "details" : "Szczegóły zmian w treści commit message" + } + }, { + "name" : "apilia-project", + "description" : "Dedykowany dla projektu coś tam", + "pattern" : "{ticket_id}-{type}-{scope}-{message}-{details}-{none}", + "model" : { + "ticket_id" : "Numer powiązanego zadania w JIRA", + "type" : [ "feat", "fix", "junk", "chore", "test" ], + "scope" : "[moduł lub komponent]", + "message" : "Krótki opis zmiany", + "details" : "Szczegóły zmian w treści commit message", + "none" : "cos" + } + } ] +} \ No newline at end of file diff --git a/src/main/resources/templates/meta-schema.json b/src/main/resources/templates/meta-schema.json new file mode 100644 index 0000000..2488f72 --- /dev/null +++ b/src/main/resources/templates/meta-schema.json @@ -0,0 +1,46 @@ +{ + "templates": [ + { + "name": "conventional", + "description": "Standardowy format Conventional Commits", + "pattern": "{type}: {scope} - {message}", + "model": { + "type": ["feat", "fix", "chore", "docs", "refactor", "test"], + "scope": "[optional]", + "message": "Zwięzły opis zmiany" + } + }, + { + "name": "detailed", + "description": "Rozszerzone informacje dla commitów", + "pattern": "[{ticket_link_number}] {type}({scope}): {message}\n\n{details}", + "model": { + "ticket_id": "Numer powiązanego zadania w JIRA", + "type": ["feat", "fix", "junk", "chore", "test"], + "scope": "[moduł lub komponent]", + "message": "Krótki opis zmiany", + "details": "Szczegóły zmian w treści commit message" + } + }, + { + "name": "markdown", + "description": "Commit message w stylu markdown", + "pattern": "# {type}: {message}\n\n## Opis zmian\n{details}\n\n### Powiązane zadania\n{related_tasks}", + "model": { + "type": ["feat", "fix", "docs"], + "message": "Krótki opis zmian", + "details": "Szczegółowy opis zmian w commitach", + "related_tasks": "Lista powiązanych zadań (np. #123, #124)" + } + }, + { + "name": "quickly", + "description": "Szybki commit message", + "pattern": "# {type}: {message}\n\n## Opis zmian\n{details}\n\n### Powiązane zadania\n{related_tasks}", + "model": { + "type": ["feat", "fix", "docs"], + "message": "Krótki opis zmian" + } + } + ] +} \ No newline at end of file diff --git a/src/test/java/pl/commit/gen/CommitGenApplicationTests.java b/src/test/java/pl/commit/craft/CommitCraftApplicationTests.java similarity index 71% rename from src/test/java/pl/commit/gen/CommitGenApplicationTests.java rename to src/test/java/pl/commit/craft/CommitCraftApplicationTests.java index e15cae5..0b7cb00 100644 --- a/src/test/java/pl/commit/gen/CommitGenApplicationTests.java +++ b/src/test/java/pl/commit/craft/CommitCraftApplicationTests.java @@ -1,10 +1,10 @@ -package pl.commit.gen; +package pl.commit.craft; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class CommitGenApplicationTests { +class CommitCraftApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/pl/commit/gen/flow/CommitFlowControllerTest.java b/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java similarity index 91% rename from src/test/java/pl/commit/gen/flow/CommitFlowControllerTest.java rename to src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java index b699d80..23beb84 100644 --- a/src/test/java/pl/commit/gen/flow/CommitFlowControllerTest.java +++ b/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.flow; +package pl.commit.craft.flow; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,7 +11,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import pl.commit.gen.service.CommitService; +import pl.commit.craft.service.CommitService; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -50,10 +50,10 @@ void testGenerateCommitSuccess() throws Exception { .thenReturn("git commit -m \"1 bugfix(componentA): Fixed issue\n\nDetailed description\""); //then - mockMvc.perform(post("/api/commit-flow/generate") + mockMvc.perform(post("/api/v1/commit-flow/craft") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(status().isOk()) // Oczekiwany status HTTP 200 + .andExpect(status().isOk()) .andExpect(content().string("git commit -m \"1 bugfix(componentA): Fixed issue\n\nDetailed description\"")); // Oczekiwana odpowiedź } } diff --git a/src/test/java/pl/commit/gen/pattern/CommitModelPatternTest.java b/src/test/java/pl/commit/craft/pattern/CommitModelPatternTest.java similarity index 98% rename from src/test/java/pl/commit/gen/pattern/CommitModelPatternTest.java rename to src/test/java/pl/commit/craft/pattern/CommitModelPatternTest.java index cd018b3..19c3c8c 100644 --- a/src/test/java/pl/commit/gen/pattern/CommitModelPatternTest.java +++ b/src/test/java/pl/commit/craft/pattern/CommitModelPatternTest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.pattern; +package pl.commit.craft.pattern; import org.junit.jupiter.api.Test; diff --git a/src/test/java/pl/commit/gen/quick/QuickCommitServiceTest.java b/src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java similarity index 98% rename from src/test/java/pl/commit/gen/quick/QuickCommitServiceTest.java rename to src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java index f621b24..e08ed58 100644 --- a/src/test/java/pl/commit/gen/quick/QuickCommitServiceTest.java +++ b/src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.quick; +package pl.commit.craft.quick; import org.junit.jupiter.api.Test; diff --git a/src/test/java/pl/commit/gen/service/CommitServiceTest.java b/src/test/java/pl/commit/craft/service/CommitServiceTest.java similarity index 86% rename from src/test/java/pl/commit/gen/service/CommitServiceTest.java rename to src/test/java/pl/commit/craft/service/CommitServiceTest.java index 2aaffe3..50cf1de 100644 --- a/src/test/java/pl/commit/gen/service/CommitServiceTest.java +++ b/src/test/java/pl/commit/craft/service/CommitServiceTest.java @@ -1,11 +1,11 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import pl.commit.translate.TranslateCommiting; +import pl.commit.craft.translate.TranslateCommitCraft; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -14,7 +14,7 @@ class CommitServiceTest { @Mock - private TranslateCommiting translateCommiting; + private TranslateCommitCraft translateCommitCraft; @InjectMocks private CommitService commitService; @@ -35,8 +35,8 @@ void testGenerateTranslateCommitValidType() { boolean wholeGitCommand = true; // when - when(translateCommiting.translate(changeDescription, "EN")).thenReturn("Add new button"); - when(translateCommiting.translate(details, "EN")).thenReturn("Added a new button to the main page."); + when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Add new button"); + when(translateCommitCraft.translate(details, "EN")).thenReturn("Added a new button to the main page."); String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand); @@ -72,7 +72,7 @@ void testGenerateTranslateCommitEmptyDetails() { boolean wholeGitCommand = true; // when - when(translateCommiting.translate(changeDescription, "EN")).thenReturn("Fix bug in payment module"); + when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Fix bug in payment module"); String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand); @@ -91,7 +91,7 @@ void testGenerateTranslateCommitWithTaskNumberAndWholeGitCommandIsFalse() { boolean wholeGitCommand = false; // when - when(translateCommiting.translate(changeDescription, "EN")).thenReturn("Add new feature"); + when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Add new feature"); String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, "", wholeGitCommand); diff --git a/src/test/java/pl/commit/gen/service/MajorNumberPreparerTest.java b/src/test/java/pl/commit/craft/service/MajorNumberPreparerTest.java similarity index 98% rename from src/test/java/pl/commit/gen/service/MajorNumberPreparerTest.java rename to src/test/java/pl/commit/craft/service/MajorNumberPreparerTest.java index 57075c8..1fe2343 100644 --- a/src/test/java/pl/commit/gen/service/MajorNumberPreparerTest.java +++ b/src/test/java/pl/commit/craft/service/MajorNumberPreparerTest.java @@ -1,4 +1,4 @@ -package pl.commit.gen.service; +package pl.commit.craft.service; import org.junit.jupiter.api.Test; diff --git a/translate/src/main/java/pl/commit/translate/DeeplResponse.java b/translate/src/main/java/pl/commit/craft/translate/DeeplResponse.java similarity index 89% rename from translate/src/main/java/pl/commit/translate/DeeplResponse.java rename to translate/src/main/java/pl/commit/craft/translate/DeeplResponse.java index e45a442..3ba3b57 100644 --- a/translate/src/main/java/pl/commit/translate/DeeplResponse.java +++ b/translate/src/main/java/pl/commit/craft/translate/DeeplResponse.java @@ -1,4 +1,4 @@ -package pl.commit.translate; +package pl.commit.craft.translate; import lombok.Getter; import lombok.Setter; diff --git a/translate/src/main/java/pl/commit/translate/DeeplWebClient.java b/translate/src/main/java/pl/commit/craft/translate/DeeplWebClient.java similarity index 94% rename from translate/src/main/java/pl/commit/translate/DeeplWebClient.java rename to translate/src/main/java/pl/commit/craft/translate/DeeplWebClient.java index e14c95c..05ea96d 100644 --- a/translate/src/main/java/pl/commit/translate/DeeplWebClient.java +++ b/translate/src/main/java/pl/commit/craft/translate/DeeplWebClient.java @@ -1,4 +1,4 @@ -package pl.commit.translate; +package pl.commit.craft.translate; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -8,7 +8,7 @@ @Service @RequiredArgsConstructor -class DeeplWebClient implements TranslateCommiting { +class DeeplWebClient implements TranslateCommitCraft { private final WebClient webClient; @Value("${deepl.api.key}") diff --git a/translate/src/main/java/pl/commit/craft/translate/TranslateCommitCraft.java b/translate/src/main/java/pl/commit/craft/translate/TranslateCommitCraft.java new file mode 100644 index 0000000..8c0766f --- /dev/null +++ b/translate/src/main/java/pl/commit/craft/translate/TranslateCommitCraft.java @@ -0,0 +1,5 @@ +package pl.commit.craft.translate; + +public interface TranslateCommitCraft { + String translate(String text, String targetLang); +} diff --git a/translate/src/main/java/pl/commit/translate/config/WebClientConfig.java b/translate/src/main/java/pl/commit/craft/translate/config/WebClientConfig.java similarity index 91% rename from translate/src/main/java/pl/commit/translate/config/WebClientConfig.java rename to translate/src/main/java/pl/commit/craft/translate/config/WebClientConfig.java index 147107a..534be91 100644 --- a/translate/src/main/java/pl/commit/translate/config/WebClientConfig.java +++ b/translate/src/main/java/pl/commit/craft/translate/config/WebClientConfig.java @@ -1,4 +1,4 @@ -package pl.commit.translate.config; +package pl.commit.craft.translate.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/translate/src/main/java/pl/commit/translate/TranslateCommiting.java b/translate/src/main/java/pl/commit/translate/TranslateCommiting.java deleted file mode 100644 index ce3dd76..0000000 --- a/translate/src/main/java/pl/commit/translate/TranslateCommiting.java +++ /dev/null @@ -1,8 +0,0 @@ -package pl.commit.translate; - -import org.springframework.stereotype.Service; - -@Service -public interface TranslateCommiting { - String translate(String text, String targetLang); -} diff --git a/translate/src/main/resources/application.yml b/translate/src/main/resources/application.yml index 28e27f6..057f7d0 100644 --- a/translate/src/main/resources/application.yml +++ b/translate/src/main/resources/application.yml @@ -1,4 +1,4 @@ deepl: api: - key: + key: 92f2c230-abe8-4dc6-b90b-bbc3f7db0210:fx url: https://api-free.deepl.com/v2/translate From bef0918d6d0b5b0e10c79ec5722d79868fe1f27c Mon Sep 17 00:00:00 2001 From: Kamil Sulejewski Date: Tue, 14 Jan 2025 01:39:30 +0100 Subject: [PATCH 2/4] chore: clean dedicated schema --- .../templates/dedicated-meta-schema.json | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/main/resources/templates/dedicated-meta-schema.json b/src/main/resources/templates/dedicated-meta-schema.json index 7cb1979..b726b5f 100644 --- a/src/main/resources/templates/dedicated-meta-schema.json +++ b/src/main/resources/templates/dedicated-meta-schema.json @@ -1,26 +1,3 @@ { - "dedicated" : [ { - "name" : "apilia-project", - "description" : "Dedykowany dla projektu coś tam", - "pattern" : "{ticket_id}-{type}-{scope}-{message}-{details}", - "model" : { - "ticket_id" : "Numer powiązanego zadania w JIRA", - "type" : [ "feat", "fix", "junk", "chore", "test" ], - "scope" : "[moduł lub komponent]", - "message" : "Krótki opis zmiany", - "details" : "Szczegóły zmian w treści commit message" - } - }, { - "name" : "apilia-project", - "description" : "Dedykowany dla projektu coś tam", - "pattern" : "{ticket_id}-{type}-{scope}-{message}-{details}-{none}", - "model" : { - "ticket_id" : "Numer powiązanego zadania w JIRA", - "type" : [ "feat", "fix", "junk", "chore", "test" ], - "scope" : "[moduł lub komponent]", - "message" : "Krótki opis zmiany", - "details" : "Szczegóły zmian w treści commit message", - "none" : "cos" - } - } ] + "dedicated" : [ ] } \ No newline at end of file From 956b117ba42df9d6a108d5040ce663d04031f639 Mon Sep 17 00:00:00 2001 From: Kamil Sulejewski Date: Mon, 27 Jan 2025 23:37:13 +0100 Subject: [PATCH 3/4] feat: Added handler and Added tests --- .../controller/CommitTranslateController.java | 13 ++- .../controller/CommitTranslateRequest.java | 3 +- .../craft/error/RestExceptionHandler.java | 28 +++++ .../craft/flow/CommitFlowController.java | 10 +- .../craft/pattern/BasicModelPattern.java | 6 +- .../craft/pattern/CommitModelPattern.java | 31 +++--- ...roller.java => CommitQuickController.java} | 14 +-- ...itRequest.java => CommitQuickRequest.java} | 2 +- ...itService.java => CommitQuickService.java} | 2 +- ...rvice.java => CommitTranslateService.java} | 14 +-- .../craft/template/CommitCraftJson.java | 9 +- .../craft/template/CommitCraftTemplate.java | 6 +- .../CommitCraftTemplateController.java | 26 ++--- .../CommitDedicatedTemplateValidator.java | 4 +- ...ervice.java => CommitTemplateService.java} | 8 +- .../craft/template/ProjectTemplate.java | 3 +- .../CommitTemplateGenerateController.java | 21 ++++ .../CommitTemplateGenerateService.java | 104 +++++++++++++++++ .../templates/dedicated-meta-schema.json | 4 +- .../craft/flow/CommitFlowControllerTest.java | 6 +- .../quick/CommitQuickControllerTest.java | 68 ++++++++++++ ...eTest.java => CommitQuickServiceTest.java} | 20 ++-- ...t.java => CommitTranslateServiceTest.java} | 14 +-- .../CommitCraftTemplateControllerTest.java | 73 ++++++++++++ .../CommitDedicatedTemplateValidatorTest.java | 77 +++++++++++++ .../template/CommitTemplateServiceTest.java | 9 ++ .../CommitTemplateGenerateServiceTest.java | 105 ++++++++++++++++++ .../resources/test-dedicated-meta-schema.json | 1 + src/test/resources/test-meta-schema.json | 46 ++++++++ 29 files changed, 630 insertions(+), 97 deletions(-) rename src/main/java/pl/commit/craft/quick/{QuickCommitController.java => CommitQuickController.java} (52%) rename src/main/java/pl/commit/craft/quick/{QuickCommitRequest.java => CommitQuickRequest.java} (77%) rename src/main/java/pl/commit/craft/quick/{QuickCommitService.java => CommitQuickService.java} (96%) rename src/main/java/pl/commit/craft/service/{CommitService.java => CommitTranslateService.java} (86%) rename src/main/java/pl/commit/craft/template/{TemplateService.java => CommitTemplateService.java} (95%) create mode 100644 src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java create mode 100644 src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java create mode 100644 src/test/java/pl/commit/craft/quick/CommitQuickControllerTest.java rename src/test/java/pl/commit/craft/quick/{QuickCommitServiceTest.java => CommitQuickServiceTest.java} (69%) rename src/test/java/pl/commit/craft/service/{CommitServiceTest.java => CommitTranslateServiceTest.java} (81%) create mode 100644 src/test/java/pl/commit/craft/template/CommitCraftTemplateControllerTest.java create mode 100644 src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java create mode 100644 src/test/java/pl/commit/craft/template/CommitTemplateServiceTest.java create mode 100644 src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java create mode 100644 src/test/resources/test-dedicated-meta-schema.json create mode 100644 src/test/resources/test-meta-schema.json diff --git a/src/main/java/pl/commit/craft/controller/CommitTranslateController.java b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java index 4017cec..c04a4c2 100644 --- a/src/main/java/pl/commit/craft/controller/CommitTranslateController.java +++ b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java @@ -1,27 +1,28 @@ package pl.commit.craft.controller; import org.springframework.web.bind.annotation.*; -import pl.commit.craft.service.CommitService; +import pl.commit.craft.service.CommitTranslateService; @RestController @RequestMapping("/api/v1/commit-translate") public class CommitTranslateController { - private final CommitService commitService; + private final CommitTranslateService commitTranslateService; - public CommitTranslateController(CommitService commitService) { - this.commitService = commitService; + public CommitTranslateController(CommitTranslateService commitTranslateService) { + this.commitTranslateService = commitTranslateService; } @PostMapping("/craft") public String generateCommit(@RequestBody CommitTranslateRequest commitTranslateRequest) { - return commitService.generateTranslateCommit( + return commitTranslateService.generateTranslateCommit( commitTranslateRequest.major(), commitTranslateRequest.type(), commitTranslateRequest.component(), commitTranslateRequest.changeDescription(), commitTranslateRequest.details(), - commitTranslateRequest.wholeGitCommand() + commitTranslateRequest.wholeGitCommand(), + commitTranslateRequest.language() ); } } diff --git a/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java b/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java index e8a7bff..0a2f0bd 100644 --- a/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java +++ b/src/main/java/pl/commit/craft/controller/CommitTranslateRequest.java @@ -6,5 +6,6 @@ public record CommitTranslateRequest( String component, String changeDescription, String details, - boolean wholeGitCommand + boolean wholeGitCommand, + String language ) {} diff --git a/src/main/java/pl/commit/craft/error/RestExceptionHandler.java b/src/main/java/pl/commit/craft/error/RestExceptionHandler.java index 9255ea9..d194312 100644 --- a/src/main/java/pl/commit/craft/error/RestExceptionHandler.java +++ b/src/main/java/pl/commit/craft/error/RestExceptionHandler.java @@ -2,10 +2,14 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; @ControllerAdvice public class RestExceptionHandler { @@ -15,5 +19,29 @@ public ResponseEntity handleIllegalArgumentException(IllegalArgum ErrorResponseCommiting errorResponseCommiting = new ErrorResponseCommiting("INVALID_ARGUMENT", ex.getMessage()); return new ResponseEntity<>(errorResponseCommiting, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("error", "Invalid JSON request"); + body.put("message", ex.getLocalizedMessage()); + body.put("path", ""); + + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception ex) { + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); + body.put("error", "Internal Server Error"); + body.put("message", ex.getLocalizedMessage()); + body.put("path", ""); + + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } } diff --git a/src/main/java/pl/commit/craft/flow/CommitFlowController.java b/src/main/java/pl/commit/craft/flow/CommitFlowController.java index 9bded34..898e573 100644 --- a/src/main/java/pl/commit/craft/flow/CommitFlowController.java +++ b/src/main/java/pl/commit/craft/flow/CommitFlowController.java @@ -4,21 +4,21 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import pl.commit.craft.service.CommitService; +import pl.commit.craft.service.CommitTranslateService; @RestController @RequestMapping("/api/v1/commit-flow") public class CommitFlowController { - private final CommitService commitService; + private final CommitTranslateService commitTranslateService; - public CommitFlowController(CommitService commitService) { - this.commitService = commitService; + public CommitFlowController(CommitTranslateService commitTranslateService) { + this.commitTranslateService = commitTranslateService; } @PostMapping("/craft") public String generateCommit(@RequestBody CommitFlowRequest commitFlowRequest) { - return commitService.generateFlowCommit( + return commitTranslateService.generateFlowCommit( commitFlowRequest.major(), commitFlowRequest.type(), commitFlowRequest.component(), diff --git a/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java b/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java index f6c6e65..462c6ce 100644 --- a/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java +++ b/src/main/java/pl/commit/craft/pattern/BasicModelPattern.java @@ -1,13 +1,13 @@ package pl.commit.craft.pattern; sealed class BasicModelPattern permits CommitModelPattern { - private static final String TARGET_LANG = "EN"; + private static final String DEFAULT_TARGET_LANG = "EN"; protected BasicModelPattern() { throw new IllegalStateException("Utility class"); } - public static String getTargetLanguage() { - return TARGET_LANG; + public static String getTargetLanguage(String language) { + return language == null ? DEFAULT_TARGET_LANG : language; } } diff --git a/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java b/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java index d2c2169..499572a 100644 --- a/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java +++ b/src/main/java/pl/commit/craft/pattern/CommitModelPattern.java @@ -46,11 +46,12 @@ private static String getCommittingWorkPatternWithoutComponentAndDetails() { } /** - * Główna metoda wybierająca odpowiedni wzorzec na podstawie flagi `wholeGitCommand`, `component` i `details`. - * @param wholeGitCommand - jeśli true, zwraca wzorzec z pełnym git commit. - * @param component - nazwa komponentu, może być pusta. - * @param details - szczegóły, mogą być puste. - * @return odpowiedni wzorzec. + * Main method for selecting the appropriate pattern based on the `wholeGitCommand`, `component`, and `details` flags. + * + * @param wholeGitCommand - if true, returns a pattern with the full Git commit command. + * @param component - the name of the component, may be empty. + * @param details - additional details, may be empty. + * @return the appropriate pattern. */ public static String getPattern(boolean wholeGitCommand, String component, String details) { if (wholeGitCommand) { @@ -61,10 +62,12 @@ public static String getPattern(boolean wholeGitCommand, String component, Strin } /** - * Zwraca odpowiedni wzorzec z pełnym poleceniem Git, w zależności od tego, czy `component` i `details` są puste. - * @param component - nazwa komponentu. - * @param details - szczegóły. - * @return wzorzec z pełnym poleceniem Git. + * Returns the appropriate pattern with the full Git command, + * depending on whether `component` and `details` are empty or not. + * + * @param component - the name of the component. + * @param details - additional details. + * @return a pattern with the full Git commit command. */ private static String getPatternWithGitCommand(String component, String details) { if (component.isEmpty() && details.isEmpty()) { @@ -79,10 +82,12 @@ private static String getPatternWithGitCommand(String component, String details) } /** - * Zwraca odpowiedni wzorzec bez pełnego polecenia Git, w zależności od tego, czy `component` i `details` są puste. - * @param component - nazwa komponentu. - * @param details - szczegóły. - * @return wzorzec bez pełnego polecenia Git. + * Returns the appropriate pattern without the full Git command, + * depending on whether `component` and `details` are empty or not. + * + * @param component - the name of the component. + * @param details - additional details. + * @return a pattern without the full Git commit command. */ private static String getPatternWithoutGitCommand(String component, String details) { if (component.isEmpty() && details.isEmpty()) { diff --git a/src/main/java/pl/commit/craft/quick/QuickCommitController.java b/src/main/java/pl/commit/craft/quick/CommitQuickController.java similarity index 52% rename from src/main/java/pl/commit/craft/quick/QuickCommitController.java rename to src/main/java/pl/commit/craft/quick/CommitQuickController.java index 120b585..4b1d943 100644 --- a/src/main/java/pl/commit/craft/quick/QuickCommitController.java +++ b/src/main/java/pl/commit/craft/quick/CommitQuickController.java @@ -7,17 +7,17 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/quick-commit") +@RequestMapping("/api/v1/commit-quick") @RequiredArgsConstructor -public class QuickCommitController { +public class CommitQuickController { - final QuickCommitService quickCommitService; + final CommitQuickService commitQuickService; @PostMapping("/craft") - public String generateCommit(@RequestBody QuickCommitRequest quickCommitRequest) { - return quickCommitService.generateQuickCommit( - quickCommitRequest.topicScope(), - quickCommitRequest.isGitCommand() + public String generateCommit(@RequestBody CommitQuickRequest commitQuickRequest) { + return commitQuickService.generateQuickCommit( + commitQuickRequest.topicScope(), + commitQuickRequest.isGitCommand() ); } } diff --git a/src/main/java/pl/commit/craft/quick/QuickCommitRequest.java b/src/main/java/pl/commit/craft/quick/CommitQuickRequest.java similarity index 77% rename from src/main/java/pl/commit/craft/quick/QuickCommitRequest.java rename to src/main/java/pl/commit/craft/quick/CommitQuickRequest.java index 51c197b..74b7c68 100644 --- a/src/main/java/pl/commit/craft/quick/QuickCommitRequest.java +++ b/src/main/java/pl/commit/craft/quick/CommitQuickRequest.java @@ -1,6 +1,6 @@ package pl.commit.craft.quick; -record QuickCommitRequest( +record CommitQuickRequest( String topicScope, boolean isGitCommand ) { diff --git a/src/main/java/pl/commit/craft/quick/QuickCommitService.java b/src/main/java/pl/commit/craft/quick/CommitQuickService.java similarity index 96% rename from src/main/java/pl/commit/craft/quick/QuickCommitService.java rename to src/main/java/pl/commit/craft/quick/CommitQuickService.java index dbd3e96..2746b3f 100644 --- a/src/main/java/pl/commit/craft/quick/QuickCommitService.java +++ b/src/main/java/pl/commit/craft/quick/CommitQuickService.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Service; @Service -class QuickCommitService { +class CommitQuickService { private static final String AUDIT_COMMIT = "audit: Audit fix"; private static final String PR_FIX_COMMIT = "fix: Pull request comments improved"; private static final String TEST_FIX_COMMIT = "test: Fixed tests"; diff --git a/src/main/java/pl/commit/craft/service/CommitService.java b/src/main/java/pl/commit/craft/service/CommitTranslateService.java similarity index 86% rename from src/main/java/pl/commit/craft/service/CommitService.java rename to src/main/java/pl/commit/craft/service/CommitTranslateService.java index 6f218e2..c731632 100644 --- a/src/main/java/pl/commit/craft/service/CommitService.java +++ b/src/main/java/pl/commit/craft/service/CommitTranslateService.java @@ -8,17 +8,17 @@ @Service @RequiredArgsConstructor -public class CommitService { +public class CommitTranslateService { private final TranslateCommitCraft translateCommitCraft; - public String generateTranslateCommit(String major, String type, String component, String changeDescription, String details, boolean wholeGitCommand) { + public String generateTranslateCommit(String major, String type, String component, String changeDescription, String details, boolean wholeGitCommand, String language) { if (isValidType(type)) { throw new IllegalArgumentException("Invalid commit type: " + type); } MajorNumber majorNumber = MajorNumberPreparer.of(major).getMajorNumber(); - String changeDescriptionTranslated = getChangeDescriptionTranslated(changeDescription); - String detailsTranslated = !details.isEmpty() ? getChangeDescriptionTranslated(details) : ""; + String changeDescriptionTranslated = getChangeDescriptionTranslated(changeDescription, language); + String detailsTranslated = !details.isEmpty() ? getChangeDescriptionTranslated(details, language) : ""; String pattern = CommitModelPattern.getPattern(wholeGitCommand, component, detailsTranslated); return String.format( @@ -33,7 +33,7 @@ public String generateTranslateCommit(String major, String type, String componen public String generateFlowCommit(String major, String type, String component, String changeDescription, String details, boolean wholeGitCommand) { if (isValidType(type)) { - throw new IllegalArgumentException("Invalid commit type: " + type); + throw new IllegalArgumentException(String.format("Invalid commit type: %s", type)); } MajorNumber majorNumber = MajorNumberPreparer.of(major).getMajorNumber(); @@ -50,8 +50,8 @@ public String generateFlowCommit(String major, String type, String component, St ).trim(); } - private String getChangeDescriptionTranslated(String changeDescription) { - return translateCommitCraft.translate(changeDescription, CommitModelPattern.getTargetLanguage()); + private String getChangeDescriptionTranslated(String changeDescription, String language) { + return translateCommitCraft.translate(changeDescription, CommitModelPattern.getTargetLanguage(language)); } private boolean isValidType(String type) { diff --git a/src/main/java/pl/commit/craft/template/CommitCraftJson.java b/src/main/java/pl/commit/craft/template/CommitCraftJson.java index 8cf76c4..01d5084 100644 --- a/src/main/java/pl/commit/craft/template/CommitCraftJson.java +++ b/src/main/java/pl/commit/craft/template/CommitCraftJson.java @@ -1,12 +1,13 @@ package pl.commit.craft.template; import lombok.Getter; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @Getter -public class CommitCraftJson { - private Map jsonContent; +class CommitCraftJson { + private final Map jsonContent; public CommitCraftJson() { jsonContent = new HashMap<>(); @@ -16,7 +17,7 @@ public void addField(String key, Object value) { jsonContent.put(key, value); } - public Map getJsonContent() { - return jsonContent; + public Map getFields() { + return Collections.unmodifiableMap(jsonContent); } } diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java index 4539aec..e9583b6 100644 --- a/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java @@ -1,11 +1,14 @@ package pl.commit.craft.template; +import lombok.AllArgsConstructor; import lombok.Data; + import javax.validation.constraints.NotNull; import java.util.Map; @Data -public class CommitCraftTemplate { +@AllArgsConstructor +class CommitCraftTemplate { @NotNull private String name; @NotNull @@ -14,4 +17,5 @@ public class CommitCraftTemplate { private String pattern; @NotNull private Map model; + } diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java index a20e618..ea7ea3b 100644 --- a/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java @@ -12,41 +12,33 @@ @RequiredArgsConstructor public class CommitCraftTemplateController { - private final TemplateService templateService; + private final CommitTemplateService commitTemplateService; - @GetMapping + @GetMapping("/all") public ResponseEntity> getAllTemplates() throws IOException { - List templates = templateService.getAllTemplates(); + List templates = commitTemplateService.getAllTemplates(); return ResponseEntity.ok(templates); } - @PostMapping("/create") - public ResponseEntity createDedicatedTemplate(@RequestBody CommitCraftTemplate template) { - try { + @PostMapping("/dedicated") + public ResponseEntity createDedicatedTemplate(@RequestBody CommitCraftTemplate template) throws IOException { boolean patternAndModelScope = CommitDedicatedTemplateValidator.validatePatternAndModelScope(template); if (patternAndModelScope) { - templateService.createDedicatedTemplate(template); + commitTemplateService.createDedicatedTemplate(template); return ResponseEntity.ok("Template added successfully."); } return ResponseEntity.badRequest().body("Template already exists."); - } catch (IOException e) { - return ResponseEntity.status(500).body("Error adding template: " + e.getMessage()); - } } @DeleteMapping("/removed/{name}") - public ResponseEntity addTemplate(@PathVariable("name") String name) { - try { - templateService.removeDedicatedTemplate(name); + public ResponseEntity addTemplate(@PathVariable("name") String name) throws IOException { + commitTemplateService.removeDedicatedTemplate(name); return ResponseEntity.ok("Template removed successfully."); - } catch (IOException e) { - return ResponseEntity.status(500).body("Error removing template: " + e.getMessage()); - } } @PostMapping("/generate-json/{name}") public Map generateJson(@PathVariable String name) throws IOException { - CommitCraftJson commitCraftJson = templateService.prepareJsonByModel(name); + CommitCraftJson commitCraftJson = commitTemplateService.prepareJsonByModel(name); return commitCraftJson.getJsonContent(); } } diff --git a/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java index 64b18fd..bf80671 100644 --- a/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java +++ b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java @@ -5,10 +5,10 @@ import java.util.Set; @Slf4j -public class CommitDedicatedTemplateValidator { +class CommitDedicatedTemplateValidator { private static final String PATTERN_VALIDATE_MODEL = "\\{(\\w+)\\}(-\\{(\\w+)\\})*"; - public static boolean validatePatternAndModelScope(CommitCraftTemplate template) { + static boolean validatePatternAndModelScope(CommitCraftTemplate template) { log.info("Validating pattern and model scope starting"); String pattern = template.getPattern(); Map model = template.getModel(); diff --git a/src/main/java/pl/commit/craft/template/TemplateService.java b/src/main/java/pl/commit/craft/template/CommitTemplateService.java similarity index 95% rename from src/main/java/pl/commit/craft/template/TemplateService.java rename to src/main/java/pl/commit/craft/template/CommitTemplateService.java index 24047fd..9c020f5 100644 --- a/src/main/java/pl/commit/craft/template/TemplateService.java +++ b/src/main/java/pl/commit/craft/template/CommitTemplateService.java @@ -16,10 +16,10 @@ @Service @RequiredArgsConstructor -public class TemplateService { - public static final String PROPERTY_TEMPLATES = "templates"; - public static final String PROPERTY_DEDICATED_TEMPLATES = "dedicated"; - public static final String PATH_NAME_ELEMENT = "name"; +class CommitTemplateService { + private static final String PROPERTY_TEMPLATES = "templates"; + private static final String PROPERTY_DEDICATED_TEMPLATES = "dedicated"; + private static final String PATH_NAME_ELEMENT = "name"; private static final String RESOURCES_TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; private static final String RESOURCES_DEDICATED_META_SCHEMA_JSON = "src/main/resources/templates/dedicated-meta-schema.json"; private final ObjectMapper objectMapper; diff --git a/src/main/java/pl/commit/craft/template/ProjectTemplate.java b/src/main/java/pl/commit/craft/template/ProjectTemplate.java index 2ce8359..3bba9a9 100644 --- a/src/main/java/pl/commit/craft/template/ProjectTemplate.java +++ b/src/main/java/pl/commit/craft/template/ProjectTemplate.java @@ -1,11 +1,10 @@ package pl.commit.craft.template; import lombok.Data; - import java.util.Map; @Data -public class ProjectTemplate { +class ProjectTemplate { private String name; private String description; private String pattern; diff --git a/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java new file mode 100644 index 0000000..9cc74f2 --- /dev/null +++ b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java @@ -0,0 +1,21 @@ +package pl.commit.craft.template.generate; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; + +@RestController +@RequestMapping("api/v1/craft-template") +@RequiredArgsConstructor +public class CommitTemplateGenerateController { + private final CommitTemplateGenerateService service; + + @PostMapping("/generate") + public ResponseEntity generateCommit(@RequestParam String templateName, @RequestBody JsonNode commitData) throws IOException { + String commitMessage = service.generateCommit(templateName, commitData); + return ResponseEntity.ok(commitMessage); + } +} diff --git a/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java new file mode 100644 index 0000000..a6aa33b --- /dev/null +++ b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java @@ -0,0 +1,104 @@ +package pl.commit.craft.template.generate; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +class CommitTemplateGenerateService { + + private final ObjectMapper objectMapper; + private static final String RESOURCES_TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; + + String generateCommit(String templateName, JsonNode commitData) throws IOException { + JsonNode rootNode = objectMapper.readTree(new File(RESOURCES_TEMPLATES_META_SCHEMA_JSON)); + JsonNode templatesArray = rootNode.path("templates"); + + Optional matchingTemplate = findTemplateByName(templatesArray, templateName); + + if (matchingTemplate.isEmpty()) { + throw new IllegalArgumentException("Template with name " + templateName + " not found"); + } + + JsonNode template = matchingTemplate.get(); + validateCommitData(template.path("model"), commitData); + + String pattern = template.path("pattern").asText(); + return fillPatternWithData(pattern, commitData); + } + + private Optional findTemplateByName(JsonNode templatesArray, String templateName) { + for (Iterator it = templatesArray.elements(); it.hasNext(); ) { + JsonNode template = it.next(); + if (templateName.equals(template.path("name").asText())) { + return Optional.of(template); + } + } + return Optional.empty(); + } + + private void validateCommitData(JsonNode model, JsonNode commitData) { + List missingFields = new ArrayList<>(); + + model.fields().forEachRemaining(field -> { + String fieldName = field.getKey(); + JsonNode fieldModel = field.getValue(); + + if (!commitData.has(fieldName)) { + missingFields.add(fieldName); + } else if (fieldModel.isArray()) { + validateArrayField(fieldName, fieldModel, commitData); + } + }); + + if (!missingFields.isEmpty()) { + throw new IllegalArgumentException("Missing required fields: " + missingFields); + } + } + + private void validateArrayField(String fieldName, JsonNode fieldModel, JsonNode commitData) { + JsonNode valueNode = commitData.path(fieldName); + + if (valueNode.isTextual()) { + String value = valueNode.asText(); + boolean isValid = false; + + for (JsonNode allowedValue : fieldModel) { + if (allowedValue.asText().equals(value)) { + isValid = true; + break; + } + } + + if (!isValid) { + throw new IllegalArgumentException("Invalid value for field '" + fieldName + "': " + value); + } + } else { + throw new IllegalArgumentException("Field '" + fieldName + "' should be a string"); + } + } + + private String fillPatternWithData(String pattern, JsonNode commitData) { + String result = pattern; + + Iterator fieldNames = commitData.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + String placeholder = "{" + fieldName + "}"; + if (result.contains(placeholder)) { + String value = commitData.path(fieldName).asText(); + result = result.replace(placeholder, value); + } + } + + return result.trim(); + } +} diff --git a/src/main/resources/templates/dedicated-meta-schema.json b/src/main/resources/templates/dedicated-meta-schema.json index b726b5f..02c2457 100644 --- a/src/main/resources/templates/dedicated-meta-schema.json +++ b/src/main/resources/templates/dedicated-meta-schema.json @@ -1,3 +1 @@ -{ - "dedicated" : [ ] -} \ No newline at end of file +{"dedicated":[]} \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java b/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java index 23beb84..a820aaa 100644 --- a/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java +++ b/src/test/java/pl/commit/craft/flow/CommitFlowControllerTest.java @@ -11,7 +11,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import pl.commit.craft.service.CommitService; +import pl.commit.craft.service.CommitTranslateService; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -22,7 +22,7 @@ class CommitFlowControllerTest { @Mock - private CommitService commitService; + private CommitTranslateService commitTranslateService; @InjectMocks private CommitFlowController commitFlowController; @@ -46,7 +46,7 @@ void testGenerateCommitSuccess() throws Exception { + "}"; // when - when(commitService.generateFlowCommit("1", "bugfix", "componentA", "Fixed issue", "Detailed description", true)) + when(commitTranslateService.generateFlowCommit("1", "bugfix", "componentA", "Fixed issue", "Detailed description", true)) .thenReturn("git commit -m \"1 bugfix(componentA): Fixed issue\n\nDetailed description\""); //then diff --git a/src/test/java/pl/commit/craft/quick/CommitQuickControllerTest.java b/src/test/java/pl/commit/craft/quick/CommitQuickControllerTest.java new file mode 100644 index 0000000..1a246cc --- /dev/null +++ b/src/test/java/pl/commit/craft/quick/CommitQuickControllerTest.java @@ -0,0 +1,68 @@ +package pl.commit.craft.quick; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(CommitQuickController.class) +class CommitQuickControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private CommitQuickService commitQuickService; + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + void testGenerateCommitSuccess() throws Exception { + String topicScope = "fix"; + boolean isGitCommand = true; + + CommitQuickRequest commitQuickRequest = new CommitQuickRequest(topicScope, isGitCommand); + String expectedCommitMessage = "git commit -m \"fix: fixed a bug\""; + + when(commitQuickService.generateQuickCommit(topicScope, isGitCommand)).thenReturn(expectedCommitMessage); + + mockMvc.perform(post("/api/v1/commit-quick/craft") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(commitQuickRequest))) + .andExpect(status().isOk()) + .andExpect(content().string(expectedCommitMessage)); + + verify(commitQuickService, times(1)).generateQuickCommit(topicScope, isGitCommand); + } + + @Test + void testGenerateCommitFailure() throws Exception { + String topicScope = "invalidTopic"; + boolean isGitCommand = false; + + CommitQuickRequest commitQuickRequest = new CommitQuickRequest(topicScope, isGitCommand); + + when(commitQuickService.generateQuickCommit(topicScope, isGitCommand)).thenReturn("Invalid commit"); + + mockMvc.perform(post("/api/v1/commit-quick/craft") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(commitQuickRequest))) + .andExpect(status().isOk()) + .andExpect(content().string("Invalid commit")); + + verify(commitQuickService, times(1)).generateQuickCommit(topicScope, isGitCommand); + } +} \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java b/src/test/java/pl/commit/craft/quick/CommitQuickServiceTest.java similarity index 69% rename from src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java rename to src/test/java/pl/commit/craft/quick/CommitQuickServiceTest.java index e08ed58..181e6c2 100644 --- a/src/test/java/pl/commit/craft/quick/QuickCommitServiceTest.java +++ b/src/test/java/pl/commit/craft/quick/CommitQuickServiceTest.java @@ -4,55 +4,55 @@ import static org.junit.jupiter.api.Assertions.*; -class QuickCommitServiceTest { - private final QuickCommitService quickCommitService = new QuickCommitService(); +class CommitQuickServiceTest { + private final CommitQuickService commitQuickService = new CommitQuickService(); @Test void testGenerateQuickCommit_Audit_NoGitCommand() { - String result = quickCommitService.generateQuickCommit("audit", false); + String result = commitQuickService.generateQuickCommit("audit", false); assertEquals("audit: Audit fix", result); } @Test void testGenerateQuickCommit_Fix_NoGitCommand() { - String result = quickCommitService.generateQuickCommit("fix", false); + String result = commitQuickService.generateQuickCommit("fix", false); assertEquals("fix: Pull request comments improved", result); } @Test void testGenerateQuickCommit_Test_NoGitCommand() { - String result = quickCommitService.generateQuickCommit("test", false); + String result = commitQuickService.generateQuickCommit("test", false); assertEquals("test: Fixed tests", result); } @Test void testGenerateQuickCommit_UnknownTopic_NoGitCommand() { - String result = quickCommitService.generateQuickCommit("unknown", false); + String result = commitQuickService.generateQuickCommit("unknown", false); assertEquals("Unknown commit type", result); } @Test void testGenerateQuickCommit_Audit_WithGitCommand() { - String result = quickCommitService.generateQuickCommit("audit", true); + String result = commitQuickService.generateQuickCommit("audit", true); assertEquals("git commit --no-verify -m \"audit: Audit fix\"", result); } @Test void testGenerateQuickCommit_Fix_WithGitCommand() { - String result = quickCommitService.generateQuickCommit("fix", true); + String result = commitQuickService.generateQuickCommit("fix", true); assertEquals("git commit --no-verify -m \"fix: Pull request comments improved\"", result); } @Test void testGenerateQuickCommit_Test_WithGitCommand() { - String result = quickCommitService.generateQuickCommit("test", true); + String result = commitQuickService.generateQuickCommit("test", true); assertEquals("git commit --no-verify -m \"test: Fixed tests\"", result); } @Test void testGenerateQuickCommit_UnknownTopic_WithGitCommand() { - String result = quickCommitService.generateQuickCommit("unknown", true); + String result = commitQuickService.generateQuickCommit("unknown", true); assertEquals("git commit --no-verify -m \"Unknown commit type\"", result); } } \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/service/CommitServiceTest.java b/src/test/java/pl/commit/craft/service/CommitTranslateServiceTest.java similarity index 81% rename from src/test/java/pl/commit/craft/service/CommitServiceTest.java rename to src/test/java/pl/commit/craft/service/CommitTranslateServiceTest.java index 50cf1de..afd3be1 100644 --- a/src/test/java/pl/commit/craft/service/CommitServiceTest.java +++ b/src/test/java/pl/commit/craft/service/CommitTranslateServiceTest.java @@ -11,13 +11,13 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; -class CommitServiceTest { +class CommitTranslateServiceTest { @Mock private TranslateCommitCraft translateCommitCraft; @InjectMocks - private CommitService commitService; + private CommitTranslateService commitTranslateService; @BeforeEach void setUp() { @@ -38,7 +38,7 @@ void testGenerateTranslateCommitValidType() { when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Add new button"); when(translateCommitCraft.translate(details, "EN")).thenReturn("Added a new button to the main page."); - String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand); + String commitMessage = commitTranslateService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand, "EN"); // then assertNotNull(commitMessage); @@ -54,7 +54,7 @@ void testGenerateTranslateCommitInvalidType() { String type = "invalidType"; IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - commitService.generateTranslateCommit(null, type, "UI", "Description", "Details", false); + commitTranslateService.generateTranslateCommit(null, type, "UI", "Description", "Details", false, "EN"); }); // then @@ -74,7 +74,7 @@ void testGenerateTranslateCommitEmptyDetails() { // when when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Fix bug in payment module"); - String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand); + String commitMessage = commitTranslateService.generateTranslateCommit(major, type, component, changeDescription, details, wholeGitCommand, "EN"); // then assertNotNull(commitMessage); @@ -93,7 +93,7 @@ void testGenerateTranslateCommitWithTaskNumberAndWholeGitCommandIsFalse() { // when when(translateCommitCraft.translate(changeDescription, "EN")).thenReturn("Add new feature"); - String commitMessage = commitService.generateTranslateCommit(major, type, component, changeDescription, "", wholeGitCommand); + String commitMessage = commitTranslateService.generateTranslateCommit(major, type, component, changeDescription, "", wholeGitCommand, "EN"); // then assertNotNull(commitMessage); @@ -111,7 +111,7 @@ void testGenerateFlowCommitWithTaskNumber() { String details = ""; boolean wholeGitCommand = true; - String commitMessage = commitService.generateFlowCommit(major, type, component, changeDescription, details, wholeGitCommand); + String commitMessage = commitTranslateService.generateFlowCommit(major, type, component, changeDescription, details, wholeGitCommand); // then assertNotNull(commitMessage); diff --git a/src/test/java/pl/commit/craft/template/CommitCraftTemplateControllerTest.java b/src/test/java/pl/commit/craft/template/CommitCraftTemplateControllerTest.java new file mode 100644 index 0000000..dbcdb21 --- /dev/null +++ b/src/test/java/pl/commit/craft/template/CommitCraftTemplateControllerTest.java @@ -0,0 +1,73 @@ +package pl.commit.craft.template; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import java.util.List; +import static org.mockito.Mockito.*; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(CommitCraftTemplateController.class) +class CommitCraftTemplateControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private CommitTemplateService commitTemplateService; + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + void testGetAllTemplates() throws Exception { + CommitCraftTemplate template1 = new CommitCraftTemplate("template1", "Description 1", "format1", null); + CommitCraftTemplate template2 = new CommitCraftTemplate("template2", "Description 2", "format2", null); + List mockTemplates = List.of(template1, template2); + + when(commitTemplateService.getAllTemplates()).thenReturn(mockTemplates); + + mockMvc.perform(get("/api/v1/craft-template/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("template1")) + .andExpect(jsonPath("$[1].name").value("template2")); + + verify(commitTemplateService, times(1)).getAllTemplates(); + } + + @Test + void testRemoveTemplate() throws Exception { + String templateName = "templateToRemove"; + + mockMvc.perform(delete("/api/v1/craft-template/removed/{name}", templateName)) + .andExpect(status().isOk()) + .andExpect(content().string("Template removed successfully.")); + + verify(commitTemplateService, times(1)).removeDedicatedTemplate(templateName); + } + + @Test + void testGenerateJson() throws Exception { + String templateName = "template1"; + CommitCraftJson mockJson = new CommitCraftJson(); + mockJson.addField("name", templateName); + mockJson.addField("description", "Template Description"); + when(commitTemplateService.prepareJsonByModel(templateName)).thenReturn(mockJson); + + mockMvc.perform(post("/api/v1/craft-template/generate-json/{name}", templateName)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value(templateName)) + .andExpect(jsonPath("$.description").value("Template Description")); + + verify(commitTemplateService, times(1)).prepareJsonByModel(templateName); + } +} \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java b/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java new file mode 100644 index 0000000..9ab1f2e --- /dev/null +++ b/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java @@ -0,0 +1,77 @@ +package pl.commit.craft.template; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CommitDedicatedTemplateValidatorTest { + private static final Logger log = LoggerFactory.getLogger(CommitDedicatedTemplateValidator.class); + + @InjectMocks + private CommitDedicatedTemplateValidator validator; + + private ListAppender listAppender; + + + @BeforeEach + void setUp() { + listAppender = new ListAppender<>(); + listAppender.start(); + + ((ch.qos.logback.classic.Logger) log).addAppender(listAppender); + } + + @Test + void testValidatePatternAndModelScope_success() { + Map model = Map.of( + "type", "feat", + "scope", "core", + "message", "message" + ); + + CommitCraftTemplate validTemplate = new CommitCraftTemplate("feat-{scope}", "Standard commit", "feat-{scope}", model); + + boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(validTemplate); + + assertFalse(result); + assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern matches the model keys."))); + } + + @Test + void testValidatePatternAndModelScope_missingModelKey() { + Map model = Map.of( + "type", "feat", + "scope", "core" + ); + + CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}-{extraKey}", "Invalid commit", "feat-{scope}-{extraKey}", model); + + boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(invalidTemplate); + + assertFalse(result); + assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern contains an extra key not in the model: extraKey"))); + } + + @Test + void testValidatePatternAndModelScope_extraModelKey() { + Map model = Map.of( + "type", "feat", + "scope", "core", + "message", "extra" + ); + + CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}", "Invalid commit", "feat-{scope}", model); + + boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(invalidTemplate); + + assertFalse(result); + assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern is missing key: message"))); + } +} \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/template/CommitTemplateServiceTest.java b/src/test/java/pl/commit/craft/template/CommitTemplateServiceTest.java new file mode 100644 index 0000000..24ca2cf --- /dev/null +++ b/src/test/java/pl/commit/craft/template/CommitTemplateServiceTest.java @@ -0,0 +1,9 @@ +package pl.commit.craft.template; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + + +class CommitTemplateServiceTest { + +} \ No newline at end of file diff --git a/src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java b/src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java new file mode 100644 index 0000000..128b319 --- /dev/null +++ b/src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java @@ -0,0 +1,105 @@ +package pl.commit.craft.template.generate; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class CommitTemplateGenerateServiceTest { + public static final String TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; + + @InjectMocks + private CommitTemplateGenerateService service; + + @Mock + private ObjectMapper objectMapper; + + private static final String TEMPLATE_NAME = "testTemplate"; + private static final String VALID_PATTERN = "Commit: {field1}, {field2}"; + private static final JsonNode VALID_COMMIT_DATA = JsonNodeFactory.instance.objectNode() + .put("field1", "value1") + .put("field2", "value2"); + + @Test + void shouldGenerateCommitWhenTemplateAndDataAreValid() throws IOException { + // Mock JSON template structure + JsonNode templateNode = JsonNodeFactory.instance.objectNode() + .put("name", TEMPLATE_NAME) + .put("pattern", VALID_PATTERN) + .set("model", JsonNodeFactory.instance.objectNode()); + + JsonNode templatesNode = JsonNodeFactory.instance.arrayNode().add(templateNode); + + JsonNode rootNode = JsonNodeFactory.instance.objectNode() + .set("templates", templatesNode); + + // Mock ObjectMapper behavior + Mockito.when(objectMapper.readTree(new File(TEMPLATES_META_SCHEMA_JSON))) + .thenReturn(rootNode); + + // Execute method + String result = service.generateCommit(TEMPLATE_NAME, VALID_COMMIT_DATA); + + // Verify result + assertEquals("Commit: value1, value2", result); + } + + @Test + void shouldThrowExceptionWhenTemplateNotFound() throws IOException { + // Mock empty templates array + JsonNode rootNode = JsonNodeFactory.instance.objectNode() + .set("templates", JsonNodeFactory.instance.arrayNode()); + + // Mock ObjectMapper behavior + Mockito.when(objectMapper.readTree(new File(TEMPLATES_META_SCHEMA_JSON))) + .thenReturn(rootNode); + + // Execute method and expect exception + Exception exception = assertThrows(IllegalArgumentException.class, () -> + service.generateCommit(TEMPLATE_NAME, VALID_COMMIT_DATA)); + + assertEquals("Template with name " + TEMPLATE_NAME + " not found", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenRequiredFieldsAreMissing() throws IOException { + // Mock JSON template structure with required fields + JsonNode modelNode = JsonNodeFactory.instance.objectNode() + .put("field1", true) + .put("field2", true); + + JsonNode templateNode = JsonNodeFactory.instance.objectNode() + .put("name", TEMPLATE_NAME) + .put("pattern", VALID_PATTERN) + .set("model", modelNode); + + JsonNode templatesNode = JsonNodeFactory.instance.arrayNode().add(templateNode); + + JsonNode rootNode = JsonNodeFactory.instance.objectNode() + .set("templates", templatesNode); + + // Mock ObjectMapper behavior + Mockito.when(objectMapper.readTree(new File(TEMPLATES_META_SCHEMA_JSON))) + .thenReturn(rootNode); + + JsonNode incompleteCommitData = JsonNodeFactory.instance.objectNode().put("field1", "value1"); + + // Execute method and expect exception + Exception exception = assertThrows(IllegalArgumentException.class, () -> + service.generateCommit(TEMPLATE_NAME, incompleteCommitData)); + + assertEquals("Missing required fields: [field2]", exception.getMessage()); + } + + +} \ No newline at end of file diff --git a/src/test/resources/test-dedicated-meta-schema.json b/src/test/resources/test-dedicated-meta-schema.json new file mode 100644 index 0000000..02c2457 --- /dev/null +++ b/src/test/resources/test-dedicated-meta-schema.json @@ -0,0 +1 @@ +{"dedicated":[]} \ No newline at end of file diff --git a/src/test/resources/test-meta-schema.json b/src/test/resources/test-meta-schema.json new file mode 100644 index 0000000..2488f72 --- /dev/null +++ b/src/test/resources/test-meta-schema.json @@ -0,0 +1,46 @@ +{ + "templates": [ + { + "name": "conventional", + "description": "Standardowy format Conventional Commits", + "pattern": "{type}: {scope} - {message}", + "model": { + "type": ["feat", "fix", "chore", "docs", "refactor", "test"], + "scope": "[optional]", + "message": "Zwięzły opis zmiany" + } + }, + { + "name": "detailed", + "description": "Rozszerzone informacje dla commitów", + "pattern": "[{ticket_link_number}] {type}({scope}): {message}\n\n{details}", + "model": { + "ticket_id": "Numer powiązanego zadania w JIRA", + "type": ["feat", "fix", "junk", "chore", "test"], + "scope": "[moduł lub komponent]", + "message": "Krótki opis zmiany", + "details": "Szczegóły zmian w treści commit message" + } + }, + { + "name": "markdown", + "description": "Commit message w stylu markdown", + "pattern": "# {type}: {message}\n\n## Opis zmian\n{details}\n\n### Powiązane zadania\n{related_tasks}", + "model": { + "type": ["feat", "fix", "docs"], + "message": "Krótki opis zmian", + "details": "Szczegółowy opis zmian w commitach", + "related_tasks": "Lista powiązanych zadań (np. #123, #124)" + } + }, + { + "name": "quickly", + "description": "Szybki commit message", + "pattern": "# {type}: {message}\n\n## Opis zmian\n{details}\n\n### Powiązane zadania\n{related_tasks}", + "model": { + "type": ["feat", "fix", "docs"], + "message": "Krótki opis zmian" + } + } + ] +} \ No newline at end of file From 7ed19ca3167ad5853c8f470abb7ea81524b7733a Mon Sep 17 00:00:00 2001 From: Kamil Sulejewski Date: Tue, 28 Jan 2025 21:54:23 +0100 Subject: [PATCH 4/4] test: added more test up to 70 % coverage. Modifying README.md. --- README.md | 10 +- build.gradle | 5 + .../craft/template/CommitCraftTemplate.java | 17 +- .../craft/template/CommitTemplateService.java | 2 +- src/main/resources/images/craft.jpg | Bin 0 -> 117733 bytes .../craft/error/RestExceptionHandlerTest.java | 72 ++++++ .../CommitCraftTemplateControllerTest.java | 10 +- .../template/CommitTemplateServiceTest.java | 218 ++++++++++++++++++ 8 files changed, 320 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/images/craft.jpg create mode 100644 src/test/java/pl/commit/craft/error/RestExceptionHandlerTest.java diff --git a/README.md b/README.md index b2f24c4..3675370 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# Gen Commiting Application +# Commmit Craft API + +Craft ## Requirements - Java 17+ @@ -29,12 +31,12 @@ 3. If you want to build the Docker image, use the following command: ```bash - docker build -t gen-commiting . + docker build -t commmit-craft . ``` 4. Run the Docker container: ```bash - docker run -d -p 9000:9000 --name gen-commiting gen-commiting + docker run -d -p 8090:8090 --name commmit-craft commmit-craft ``` 5. The application will be available at `http://localhost:8090`. @@ -56,7 +58,7 @@ The `translate` module integrates with DeepL for machine translation. To use thi ## Configuration -You can specify different profiles for the application. For example, to use the `kam` profile: +You can specify different profiles for the application. For example, to use the `dev` profile: 1. In `application.yml`: ```properties diff --git a/build.gradle b/build.gradle index 5fdc404..2da365c 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.modelmapper:modelmapper:3.2.2' + implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' + implementation 'org.glassfish:jakarta.el:4.0.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + testImplementation 'org.mockito:mockito-core:4.5.1' + testImplementation 'org.mockito:mockito-junit-jupiter:4.5.1' } dependencyManagement { diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java index e9583b6..e88ea00 100644 --- a/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplate.java @@ -2,20 +2,23 @@ import lombok.AllArgsConstructor; import lombok.Data; - +import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import java.util.Map; @Data +@NoArgsConstructor @AllArgsConstructor -class CommitCraftTemplate { - @NotNull +public class CommitCraftTemplate { + @NotNull(message = "Name cannot be null") private String name; - @NotNull + + @NotNull(message = "Description cannot be null") private String description; - @NotNull + + @NotNull(message = "Pattern cannot be null") private String pattern; - @NotNull - private Map model; + @NotNull(message = "Model cannot be null") + private Map model; } diff --git a/src/main/java/pl/commit/craft/template/CommitTemplateService.java b/src/main/java/pl/commit/craft/template/CommitTemplateService.java index 9c020f5..3ce7d8e 100644 --- a/src/main/java/pl/commit/craft/template/CommitTemplateService.java +++ b/src/main/java/pl/commit/craft/template/CommitTemplateService.java @@ -54,7 +54,7 @@ public CommitCraftJson prepareJsonByModel(String name) throws IOException { return getCommitCraftJson(selectedTemplate, selectedTemplate.getName()); } - private CommitCraftJson getCommitCraftJson(CommitCraftTemplate selectedTemplate, String selectedTemplateName) throws IOException { + CommitCraftJson getCommitCraftJson(CommitCraftTemplate selectedTemplate, String selectedTemplateName) throws IOException { Map model = selectedTemplate.getModel(); CommitCraftJson commitCraftJson = new CommitCraftJson(); commitCraftJson.addField(PATH_NAME_ELEMENT, selectedTemplateName); diff --git a/src/main/resources/images/craft.jpg b/src/main/resources/images/craft.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f92030a7716099e20b5a24dfd40d0a4796b73ce8 GIT binary patch literal 117733 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 j?_}l+g{*RDAZe57)7$bGLB~lDn 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 model = new HashMap<>(); + model.put("type", "feat"); + model.put("scope", Arrays.asList("auth", "api")); + + CommitCraftTemplate template = new CommitCraftTemplate( + "conventional", + "Conventional commit template", + "type(scope): description", + model + ); + + Set> violations = validator.validate(template); + + assertTrue(violations.isEmpty()); + } + + @Test + void getAllTemplates_ShouldCombineTemplatesFromBothFiles() throws IOException { + // given + ObjectNode rootNode = mock(ObjectNode.class); + ObjectNode rootDedicatedNode = mock(ObjectNode.class); + JsonNode templatesNode = mock(JsonNode.class); + JsonNode dedicatedNode = mock(JsonNode.class); + + CommitCraftTemplate template1 = new CommitCraftTemplate( + "template1", + "Regular template", + "feat: description", + Map.of("type", "feat") + ); + + CommitCraftTemplate template2 = new CommitCraftTemplate( + "template2", + "Dedicated template", + "fix: description", + Map.of("type", "fix") + ); + + // when + when(objectMapper.readTree(any(File.class))) + .thenReturn(rootNode) + .thenReturn(rootDedicatedNode); + + when(rootNode.path("templates")).thenReturn(templatesNode); + when(rootDedicatedNode.path("dedicated")).thenReturn(dedicatedNode); + + when(templatesNode.toString()).thenReturn("[{\"regular\":\"template\"}]"); + when(dedicatedNode.toString()).thenReturn("[{\"dedicated\":\"template\"}]"); + + when(objectMapper.readValue(eq(templatesNode.toString()), any(TypeReference.class))) + .thenReturn(List.of(template1)); + when(objectMapper.readValue(eq(dedicatedNode.toString()), any(TypeReference.class))) + .thenReturn(List.of(template2)); + + List result = commitTemplateService.getAllTemplates(); + + assertEquals(2, result.size()); + assertEquals("template1", result.get(0).getName()); + assertEquals("template2", result.get(1).getName()); + + // Verify file reads happened exactly once for each file + verify(objectMapper, times(2)).readTree(any(File.class)); + verify(objectMapper, times(2)).readValue(anyString(), any(TypeReference.class)); + + // Verify correct paths were accessed + verify(rootNode).path("templates"); + verify(rootDedicatedNode).path("dedicated"); + } + + @Test + void getAllTemplates_ShouldHandleEmptyFiles() throws IOException { + // given + ObjectNode rootNode = mock(ObjectNode.class); + ObjectNode rootDedicatedNode = mock(ObjectNode.class); + JsonNode templatesNode = mock(JsonNode.class); + JsonNode dedicatedNode = mock(JsonNode.class); + + // Configure mock behavior + when(objectMapper.readTree(any(File.class))) + .thenReturn(rootNode) + .thenReturn(rootDedicatedNode); + + when(rootNode.path("templates")).thenReturn(templatesNode); + when(rootDedicatedNode.path("dedicated")).thenReturn(dedicatedNode); + + when(templatesNode.toString()).thenReturn("[]"); + when(dedicatedNode.toString()).thenReturn("[]"); + + when(objectMapper.readValue(anyString(), any(TypeReference.class))) + .thenReturn(Collections.emptyList()); + + // when + List result = commitTemplateService.getAllTemplates(); + + // then + assertTrue(result.isEmpty()); + } + + @Test + void getAllTemplates_ShouldHandleIOException() throws IOException { + // given + when(objectMapper.readTree(any(File.class))) + .thenThrow(new IOException("File not found")); + + // when/then + assertThrows(IOException.class, () -> commitTemplateService.getAllTemplates()); + } + + @Test + void getCommitCraftJson_ShouldCreateValidCommitCraftJson() throws IOException { + // given + Map model = new HashMap<>(); + model.put("type", "feat"); + model.put("scope", Arrays.asList("auth", "api")); + + CommitCraftTemplate selectedTemplate = new CommitCraftTemplate( + "conventional", + "Conventional commit template", + "type(scope): description", + model + ); + String selectedTemplateName = "conventional"; + + // when + CommitCraftJson result = commitTemplateService.getCommitCraftJson(selectedTemplate, selectedTemplateName); + + // then + assertNotNull(result); + assertEquals(selectedTemplateName, result.getFields().get("name")); + } + + @Test + void prepareJsonByModel_ShouldReturnValidCommitCraftJson() throws IOException { + // given + ObjectNode rootNode = mock(ObjectNode.class); + ObjectNode rootDedicatedNode = mock(ObjectNode.class); + JsonNode templatesNode = mock(JsonNode.class); + JsonNode dedicatedNode = mock(JsonNode.class); + + CommitCraftTemplate template1 = new CommitCraftTemplate( + "template1", + "Regular template", + "feat: description", + Map.of("type", "feat") + ); + + CommitCraftTemplate template2 = new CommitCraftTemplate( + "template2", + "Dedicated template", + "fix: description", + Map.of("type", "fix", "scope", Arrays.asList("auth", "api")) + ); + + // Configure mock behavior + when(objectMapper.readTree(any(File.class))) + .thenReturn(rootNode) + .thenReturn(rootDedicatedNode); + + when(rootNode.path("templates")).thenReturn(templatesNode); + when(rootDedicatedNode.path("dedicated")).thenReturn(dedicatedNode); + + when(templatesNode.toString()).thenReturn("[{\"name\":\"template1\",\"description\":\"Regular template\",\"commitMessage\":\"feat: description\",\"model\":{\"type\":\"feat\"}}]"); + when(dedicatedNode.toString()).thenReturn("[{\"name\":\"template2\",\"description\":\"Dedicated template\",\"commitMessage\":\"fix: description\",\"model\":{\"type\":\"fix\",\"scope\":[\"auth\",\"api\"]}}]"); + + when(objectMapper.readValue(eq(templatesNode.toString()), any(TypeReference.class))) + .thenReturn(List.of(template1)); + when(objectMapper.readValue(eq(dedicatedNode.toString()), any(TypeReference.class))) + .thenReturn(List.of(template2)); + + // when + CommitCraftJson result = commitTemplateService.prepareJsonByModel("template2"); + + // then + assertNotNull(result, "The result should not be null"); + + // Verify interactions + verify(objectMapper, times(2)).readTree(any(File.class)); + verify(objectMapper, times(2)).readValue(anyString(), any(TypeReference.class)); + verify(rootNode).path("templates"); + verify(rootDedicatedNode).path("dedicated"); + } } \ No newline at end of file