diff --git a/src/main/java/pl/commit/craft/controller/CommitTranslateController.java b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java index bf4e3ed..cb5edf2 100644 --- a/src/main/java/pl/commit/craft/controller/CommitTranslateController.java +++ b/src/main/java/pl/commit/craft/controller/CommitTranslateController.java @@ -1,11 +1,17 @@ package pl.commit.craft.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; import pl.commit.craft.service.CommitTranslateService; @RestController @RequestMapping("/api/v1/commit-translate") +@Tag(name = "Commit Translation Controller", description = "Translate commit wit deepl api") public class CommitTranslateController { private final CommitTranslateService commitTranslateService; @@ -18,8 +24,19 @@ public CommitTranslateController(CommitTranslateService commitTranslateService) summary = "Generate commit translation", description = "Generates a translated commit message based on the provided request information." ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Successfully generated commit translation", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "Bad request, invalid input data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PostMapping("/craft") public String generateCommit( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Commit translation request data", + required = true, + content = @Content(schema = @Schema(implementation = CommitTranslateRequest.class))) @RequestBody CommitTranslateRequest commitTranslateRequest) { return commitTranslateService.generateTranslateCommit( commitTranslateRequest.major(), diff --git a/src/main/java/pl/commit/craft/flow/CommitFlowController.java b/src/main/java/pl/commit/craft/flow/CommitFlowController.java index edd024e..9a54082 100644 --- a/src/main/java/pl/commit/craft/flow/CommitFlowController.java +++ b/src/main/java/pl/commit/craft/flow/CommitFlowController.java @@ -1,6 +1,11 @@ package pl.commit.craft.flow; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,6 +14,7 @@ @RestController @RequestMapping("/api/v1/commit-flow") +@Tag(name = "Commit Flow Controller", description = "Flow commit") public class CommitFlowController { private final CommitTranslateService commitTranslateService; @@ -21,8 +27,20 @@ public CommitFlowController(CommitTranslateService commitTranslateService) { summary = "Generate a commit message based on the provided commit flow data", description = "This endpoint receives commit flow details and generates the corresponding commit message." ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Successfully generated commit message", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "400", description = "Bad request, invalid input data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PostMapping("/craft") - public String generateCommit(@RequestBody CommitFlowRequest commitFlowRequest) { + public String generateCommit( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Commit flow request data", + required = true, + content = @Content(schema = @Schema(implementation = CommitFlowRequest.class))) + @RequestBody CommitFlowRequest commitFlowRequest) { return commitTranslateService.generateFlowCommit( commitFlowRequest.major(), commitFlowRequest.type(), diff --git a/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java index d046db0..2a11c52 100644 --- a/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java +++ b/src/main/java/pl/commit/craft/template/CommitCraftTemplateController.java @@ -1,17 +1,25 @@ package pl.commit.craft.template; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/v1/craft-template") @RequiredArgsConstructor +@Tag(name = "Commit Template Controller", description = "Management template commit model") public class CommitCraftTemplateController { private final CommitTemplateService commitTemplateService; @@ -20,6 +28,11 @@ public class CommitCraftTemplateController { summary = "Get all commit templates", description = "Fetches a list of all available commit craft templates." ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Successfully fetched templates", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CommitCraftTemplate.class))) + }) @GetMapping("/all") public ResponseEntity> getAllTemplates() throws IOException { List templates = commitTemplateService.getAllTemplates(); @@ -28,29 +41,40 @@ public ResponseEntity> getAllTemplates() throws IOExce @Operation( summary = "Create a dedicated commit template", - description = "Creates a new dedicated commit template if the pattern and model scope are valid.", - responses = { - @ApiResponse(responseCode = "200", description = "Template added successfully"), - @ApiResponse(responseCode = "400", description = "Template already exists") - } + description = "Creates a new dedicated commit template if the pattern and model scope are valid." ) + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Template created successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", description = "Invalid template format or template already exists", + content = @Content(mediaType = "application/json")) + }) @PostMapping("/dedicated") - public ResponseEntity createDedicatedTemplate(@RequestBody CommitCraftTemplate template) throws IOException { - boolean patternAndModelScope = CommitDedicatedTemplateValidator.validatePatternAndModelScope(template); - if (patternAndModelScope) { - commitTemplateService.createDedicatedTemplate(template); - return ResponseEntity.ok("Template added successfully."); - } - return ResponseEntity.badRequest().body("Template already exists."); + public ResponseEntity> createDedicatedTemplate( + @RequestBody(description = "Commit template data", required = true, + content = @Content(schema = @Schema(implementation = CommitCraftTemplate.class))) + @org.springframework.web.bind.annotation.RequestBody CommitCraftTemplate template) throws IOException { + TemplateOperationResult result = commitTemplateService.createDedicatedTemplate(template); + + if (result.success()) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(Collections.singletonMap("message", result.message())); + } + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(Collections.singletonMap("error", result.message())); } @Operation( summary = "Remove a commit template", - description = "Removes a dedicated commit template by name.", - responses = { - @ApiResponse(responseCode = "200", description = "Template removed successfully") - } + description = "Removes a dedicated commit template by name." ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Template removed successfully", + content = @Content(mediaType = "text/plain")) + }) @DeleteMapping("/removed/{name}") public ResponseEntity addTemplate(@PathVariable("name") String name) throws IOException { commitTemplateService.removeDedicatedTemplate(name); @@ -59,11 +83,12 @@ public ResponseEntity addTemplate(@PathVariable("name") String name) thr @Operation( summary = "Generate commit template JSON", - description = "Generates a JSON representation of the commit template based on its name.", - responses = { - @ApiResponse(responseCode = "200", description = "JSON generated successfully") - } + description = "Generates a JSON representation of the commit template based on its name." ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "JSON generated successfully", + content = @Content(mediaType = "application/json")) + }) @PostMapping("/generate-json/{name}") public Map generateJson(@PathVariable String name) throws IOException { CommitCraftJson commitCraftJson = commitTemplateService.prepareJsonByModel(name); diff --git a/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java index bf80671..b4ba3b4 100644 --- a/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java +++ b/src/main/java/pl/commit/craft/template/CommitDedicatedTemplateValidator.java @@ -1,42 +1,79 @@ package pl.commit.craft.template; import lombok.extern.slf4j.Slf4j; -import java.util.Map; +import java.util.HashSet; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; @Slf4j class CommitDedicatedTemplateValidator { - private static final String PATTERN_VALIDATE_MODEL = "\\{(\\w+)\\}(-\\{(\\w+)\\})*"; + private static final String PLACEHOLDER_REGEX = "\\{(\\w+)\\}"; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_REGEX); - static boolean validatePatternAndModelScope(CommitCraftTemplate template) { - log.info("Validating pattern and model scope starting"); - String pattern = template.getPattern(); - Map model = template.getModel(); + /** + * For backward compatibility + */ + public static boolean validatePatternAndModelScope(CommitCraftTemplate template) { + return validatePatternAndModelScopeDetailed(template).isValid(); + } - Set modelKeys = model.keySet(); + /** + * Validates that all keys in the model exist in the pattern and vice versa. + * + * @param template The commit template to validate + * @return result of validation with details about any mismatches + */ + public static ValidationResult validatePatternAndModelScopeDetailed(CommitCraftTemplate template) { + log.info("Validating pattern and model scope starting for template: {}", template.getName()); - boolean matches = true; - for (String key : modelKeys) { - if (!pattern.contains(key)) { - log.warn("Pattern is missing key: {}", key); - matches = false; - } - } + Set modelKeys = template.getModel().keySet(); + Set patternKeys = extractPlaceholdersFromPattern(template.getPattern()); - 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; - } - } + Set missingInPattern = findMissingKeys(modelKeys, patternKeys); + Set extraInPattern = findMissingKeys(patternKeys, modelKeys); + + boolean isValid = missingInPattern.isEmpty() && extraInPattern.isEmpty(); - if (matches) { + if (isValid) { log.info("Pattern matches the model keys."); + return ValidationResult.valid(); } else { - log.warn("Pattern does not match the model keys."); + if (!missingInPattern.isEmpty()) { + log.warn("Pattern is missing keys: {}", missingInPattern); + } + if (!extraInPattern.isEmpty()) { + log.warn("Pattern contains extra keys not in the model: {}", extraInPattern); + } + return ValidationResult.invalid(missingInPattern, extraInPattern); } + } + + + /** + * Extracts all placeholder keys from the pattern string. + * + * @param pattern The pattern string containing placeholders + * @return A set of placeholder keys + */ + private static Set extractPlaceholdersFromPattern(String pattern) { + Set patternKeys = new HashSet<>(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(pattern); + + while (matcher.find()) { + patternKeys.add(matcher.group(1)); + } + + return patternKeys; + } - return matches; + /** + * Finds keys that are in the first set but not in the second set + */ + private static Set findMissingKeys(Set sourceKeys, Set targetKeys) { + return sourceKeys.stream() + .filter(key -> !targetKeys.contains(key)) + .collect(Collectors.toSet()); } } diff --git a/src/main/java/pl/commit/craft/template/CommitTemplateService.java b/src/main/java/pl/commit/craft/template/CommitTemplateService.java index 3ce7d8e..9e3e1f7 100644 --- a/src/main/java/pl/commit/craft/template/CommitTemplateService.java +++ b/src/main/java/pl/commit/craft/template/CommitTemplateService.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; @@ -14,6 +15,7 @@ import java.util.List; import java.util.Map; +@Slf4j @Service @RequiredArgsConstructor class CommitTemplateService { @@ -78,10 +80,57 @@ public List readTemplates() throws IOException { }); } - public void createDedicatedTemplate(CommitCraftTemplate newTemplate) throws IOException { - List templates = readTemplates(); - templates.add(newTemplate); - saveTemplates(templates); + /** + * Creates a dedicated template after validating it. + * + * @param template The template to create + * @return Result containing success/failure status and a message + * @throws IOException If there's an issue accessing the template storage + */ + public TemplateOperationResult createDedicatedTemplate(CommitCraftTemplate template) throws IOException { + ValidationResult validationResult = + CommitDedicatedTemplateValidator.validatePatternAndModelScopeDetailed(template); + + if (!validationResult.isValid()) { + String errorMessage = validationResult.getErrorMessage(); + log.warn("Template validation failed: {}", errorMessage); + return new TemplateOperationResult(false, errorMessage); + } + + if (templateExists(template.getName())) { + log.warn("Template with name '{}' already exists", template.getName()); + return new TemplateOperationResult(false, "Template with name '" + template.getName() + "' already exists"); + } + + saveTemplate(template); + log.info("Template '{}' created successfully", template.getName()); + return new TemplateOperationResult(true, "Template created successfully"); + } + + /** + * Checks if a template with the given name already exists + * + * @param templateName Name of the template to check + * @return true if the template exists, false otherwise + * @throws IOException If there's an issue accessing the template storage + */ + private boolean templateExists(String templateName) throws IOException { + List existingTemplates = getAllTemplates(); + return existingTemplates.stream() + .anyMatch(template -> template.getName().equals(templateName)); + } + + /** + * Saves a new template to the dedicated templates file + * + * @param template The template to save + * @throws IOException If there's an issue accessing or writing to the template storage + */ + private void saveTemplate(CommitCraftTemplate template) throws IOException { + List existingTemplates = readTemplates(); + existingTemplates.add(template); + saveTemplates(existingTemplates); + log.debug("Template saved successfully: {}", template.getName()); } public void removeDedicatedTemplate(String dedicatedTemplateName) throws IOException { diff --git a/src/main/java/pl/commit/craft/template/TemplateOperationResult.java b/src/main/java/pl/commit/craft/template/TemplateOperationResult.java new file mode 100644 index 0000000..0813ec9 --- /dev/null +++ b/src/main/java/pl/commit/craft/template/TemplateOperationResult.java @@ -0,0 +1,4 @@ +package pl.commit.craft.template; + +record TemplateOperationResult(boolean success, String message) { +} diff --git a/src/main/java/pl/commit/craft/template/ValidationResult.java b/src/main/java/pl/commit/craft/template/ValidationResult.java new file mode 100644 index 0000000..fa5b4aa --- /dev/null +++ b/src/main/java/pl/commit/craft/template/ValidationResult.java @@ -0,0 +1,56 @@ +package pl.commit.craft.template; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import java.util.Set; + +@AllArgsConstructor +class ValidationResult { + @Getter + private final boolean valid; + private final Set missingInPattern; + private final Set extraInPattern; + + /** + * Creates a successful validation result + */ + public static ValidationResult valid() { + return new ValidationResult(true, Set.of(), Set.of()); + } + + /** + * Creates a validation result with errors + */ + public static ValidationResult invalid(Set missingInPattern, Set extraInPattern) { + return new ValidationResult(false, missingInPattern, extraInPattern); + } + + /** + * Generates a detailed error message for invalid templates + */ + public String getErrorMessage() { + if (valid) { + return "Template is valid"; + } + + StringBuilder message = new StringBuilder("Invalid template format: "); + + if (!missingInPattern.isEmpty()) { + message.append("Keys missing in pattern: [") + .append(String.join(", ", missingInPattern)) + .append("]"); + + if (!extraInPattern.isEmpty()) { + message.append("; "); + } + } + + if (!extraInPattern.isEmpty()) { + message.append("Missing keys in model: [") + .append(String.join(", ", extraInPattern)) + .append("]"); + } + + return message.toString(); + } +} diff --git a/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java index 9cc74f2..39dc0fb 100644 --- a/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java +++ b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateController.java @@ -1,6 +1,13 @@ package pl.commit.craft.template.generate; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,12 +17,57 @@ @RestController @RequestMapping("api/v1/craft-template") @RequiredArgsConstructor +@Tag( + name = "Commit Template Generation", + description = "API for generating commit messages based on predefined templates." +) public class CommitTemplateGenerateController { private final CommitTemplateGenerateService service; + @Operation( + summary = "Generate commit message", + description = "Generates a commit message based on a specified template and commit data." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Commit message generated successfully", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", description = "Invalid request data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "Server error during commit generation", + content = @Content(mediaType = "application/json")) + }) @PostMapping("/generate") - public ResponseEntity generateCommit(@RequestParam String templateName, @RequestBody JsonNode commitData) throws IOException { + public ResponseEntity generateCommit( + @Parameter(description = "Name of the commit template", required = true) + @RequestParam String templateName, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Commit data as JSON model", required = true, + content = @Content(schema = @Schema(implementation = JsonNode.class))) + @RequestBody JsonNode commitData) throws IOException { String commitMessage = service.generateCommit(templateName, commitData); return ResponseEntity.ok(commitMessage); } + + + @Operation( + summary = "Generate dedicated commit message", + description = "Generates a commit message using a dedicated template with provided commit data." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Commit message generated successfully", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", description = "Invalid request data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "Server error during commit generation", + content = @Content(mediaType = "application/json")) + }) + @PostMapping("/generate-dedicated") + public ResponseEntity generateDedicatedCommit( + @Parameter(description = "Name of the dedicated commit template", required = true) + @RequestParam String templateName, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Commit data as JSON", required = true, + content = @Content(schema = @Schema(implementation = JsonNode.class))) + @org.springframework.web.bind.annotation.RequestBody JsonNode commitData) throws IOException { + String commitMessage = service.generateDedicatedCommit(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 index a6aa33b..2fd5aca 100644 --- a/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java +++ b/src/main/java/pl/commit/craft/template/generate/CommitTemplateGenerateService.java @@ -10,41 +10,75 @@ import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.stream.StreamSupport; @Service @RequiredArgsConstructor class CommitTemplateGenerateService { + private static final String ROOT_TEMPLATE_NAME = "templates"; + private static final String ROOT_DEDICATED_NAME = "dedicated"; + private static final String TEMPLATES_SCHEMA_PATH = "src/main/resources/templates/meta-schema.json"; + private static final String DEDICATED_SCHEMA_PATH = "src/main/resources/templates/dedicated-meta-schema.json"; private final ObjectMapper objectMapper; - private static final String RESOURCES_TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; + /** + * Generates a commit message using a standard template + */ String generateCommit(String templateName, JsonNode commitData) throws IOException { - JsonNode rootNode = objectMapper.readTree(new File(RESOURCES_TEMPLATES_META_SCHEMA_JSON)); - JsonNode templatesArray = rootNode.path("templates"); + return generateCommitFromSchema( + TEMPLATES_SCHEMA_PATH, + ROOT_TEMPLATE_NAME, + templateName, + commitData + ); + } + + /** + * Generates a commit message using a dedicated template + */ + String generateDedicatedCommit(String templateName, JsonNode commitData) throws IOException { + return generateCommitFromSchema( + DEDICATED_SCHEMA_PATH, + ROOT_DEDICATED_NAME, + templateName, + commitData + ); + } - Optional matchingTemplate = findTemplateByName(templatesArray, templateName); + /** + * Core method for generating commit messages from any schema + */ + private String generateCommitFromSchema( + String schemaPath, + String rootNodeName, + String templateName, + JsonNode commitData) throws IOException { - if (matchingTemplate.isEmpty()) { - throw new IllegalArgumentException("Template with name " + templateName + " not found"); - } + JsonNode rootNode = objectMapper.readTree(new File(schemaPath)); + JsonNode templatesArray = rootNode.path(rootNodeName); + + JsonNode template = findTemplateByName(templatesArray, templateName) + .orElseThrow(() -> 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); } + /** + * Finds a template by name in the templates array + */ 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(); + return StreamSupport.stream(templatesArray.spliterator(), false) + .filter(template -> templateName.equals(template.path("name").asText())) + .findFirst(); } + /** + * Validates the commit data against the template model + */ private void validateCommitData(JsonNode model, JsonNode commitData) { List missingFields = new ArrayList<>(); @@ -60,43 +94,41 @@ private void validateCommitData(JsonNode model, JsonNode commitData) { }); if (!missingFields.isEmpty()) { - throw new IllegalArgumentException("Missing required fields: " + missingFields); + throw new IllegalArgumentException("Missing required fields: " + String.join(", ", missingFields)); } } + /** + * Validates that field values match allowed values in the model + */ private void validateArrayField(String fieldName, JsonNode fieldModel, JsonNode commitData) { JsonNode valueNode = commitData.path(fieldName); - if (valueNode.isTextual()) { - String value = valueNode.asText(); - boolean isValid = false; + if (!valueNode.isTextual()) { + throw new IllegalArgumentException("Field '" + fieldName + "' should be a string"); + } - for (JsonNode allowedValue : fieldModel) { - if (allowedValue.asText().equals(value)) { - isValid = true; - break; - } - } + String value = valueNode.asText(); + boolean isValid = StreamSupport.stream(fieldModel.spliterator(), false) + .anyMatch(allowedValue -> allowedValue.asText().equals(value)); - if (!isValid) { - throw new IllegalArgumentException("Invalid value for field '" + fieldName + "': " + value); - } - } else { - throw new IllegalArgumentException("Field '" + fieldName + "' should be a string"); + if (!isValid) { + throw new IllegalArgumentException("Invalid value for field '" + fieldName + "': " + value); } } + /** + * Fills the template pattern with the commit data + */ 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); - } + String placeholder = "\\{" + fieldName + "\\}"; + String value = commitData.path(fieldName).asText(); + result = result.replaceAll(placeholder, value); } return result.trim(); diff --git a/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java b/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java index 9ab1f2e..b694e29 100644 --- a/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java +++ b/src/test/java/pl/commit/craft/template/CommitDedicatedTemplateValidatorTest.java @@ -1,77 +1,72 @@ 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; + @Test + void shouldValidatePatternAndModelScope_Success() { + Map model = Map.of( + "scope", "core", + "message", "message" + ); + CommitCraftTemplate validTemplate = new CommitCraftTemplate("feat-{scope}-{message}", "Standard commit", "feat-{scope}-{message}", model); - @BeforeEach - void setUp() { - listAppender = new ListAppender<>(); - listAppender.start(); + ValidationResult result = CommitDedicatedTemplateValidator.validatePatternAndModelScopeDetailed(validTemplate); - ((ch.qos.logback.classic.Logger) log).addAppender(listAppender); + assertTrue(result.isValid()); + assertEquals("Template is valid", result.getErrorMessage()); } @Test - void testValidatePatternAndModelScope_success() { + void shouldValidatePatternAndModelScope_MissingModelKey() { Map model = Map.of( "type", "feat", - "scope", "core", - "message", "message" + "message", "message", + "scope", "core" ); - CommitCraftTemplate validTemplate = new CommitCraftTemplate("feat-{scope}", "Standard commit", "feat-{scope}", model); + CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}-{message}", "Invalid commit", "feat-{scope}-{message}", model); - boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(validTemplate); + ValidationResult result = CommitDedicatedTemplateValidator.validatePatternAndModelScopeDetailed(invalidTemplate); - assertFalse(result); - assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern matches the model keys."))); + assertFalse(result.isValid()); + assertEquals("Invalid template format: Keys missing in pattern: [type]", result.getErrorMessage()); } @Test - void testValidatePatternAndModelScope_missingModelKey() { + void shouldValidatePatternAndModelScope_ExtraModelKey() { Map model = Map.of( "type", "feat", - "scope", "core" + "scope", "core", + "extraKey", "extra" ); - CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}-{extraKey}", "Invalid commit", "feat-{scope}-{extraKey}", model); + CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}", "Invalid commit", "feat-{scope}", model); - boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(invalidTemplate); + ValidationResult result = CommitDedicatedTemplateValidator.validatePatternAndModelScopeDetailed(invalidTemplate); - assertFalse(result); - assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern contains an extra key not in the model: extraKey"))); + assertFalse(result.isValid()); + assertEquals("Invalid template format: Keys missing in pattern: [extraKey, type]", result.getErrorMessage()); } @Test - void testValidatePatternAndModelScope_extraModelKey() { + void shouldValidatePatternAndModelScope_BothMissingAndExtraKeys() { Map model = Map.of( "type", "feat", "scope", "core", - "message", "extra" + "extraKey", "extra" ); - CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}", "Invalid commit", "feat-{scope}", model); + CommitCraftTemplate invalidTemplate = new CommitCraftTemplate("feat-{scope}-{message}", "Invalid commit", "feat-{scope}-{message}", model); - boolean result = CommitDedicatedTemplateValidator.validatePatternAndModelScope(invalidTemplate); + ValidationResult result = CommitDedicatedTemplateValidator.validatePatternAndModelScopeDetailed(invalidTemplate); - assertFalse(result); - assertFalse(listAppender.list.stream().anyMatch(event -> event.getMessage().contains("Pattern is missing key: message"))); + assertFalse(result.isValid()); + assertEquals("Invalid template format: Keys missing in pattern: [extraKey, type]; Missing keys in model: [message]", result.getErrorMessage()); } } \ 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 index 128b319..6fec68b 100644 --- a/src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java +++ b/src/test/java/pl/commit/craft/template/generate/CommitTemplateGenerateServiceTest.java @@ -13,10 +13,11 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class CommitTemplateGenerateServiceTest { - public static final String TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; + private static final String TEMPLATES_META_SCHEMA_JSON = "src/main/resources/templates/meta-schema.json"; @InjectMocks private CommitTemplateGenerateService service; @@ -30,9 +31,9 @@ class CommitTemplateGenerateServiceTest { .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) @@ -47,10 +48,8 @@ void shouldGenerateCommitWhenTemplateAndDataAreValid() throws IOException { 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); } @@ -73,7 +72,6 @@ void shouldThrowExceptionWhenTemplateNotFound() throws IOException { @Test void shouldThrowExceptionWhenRequiredFieldsAreMissing() throws IOException { - // Mock JSON template structure with required fields JsonNode modelNode = JsonNodeFactory.instance.objectNode() .put("field1", true) .put("field2", true); @@ -94,12 +92,46 @@ void shouldThrowExceptionWhenRequiredFieldsAreMissing() throws IOException { 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()); + assertEquals("Missing required fields: field2", exception.getMessage()); } + @Test + void shouldGenerateDedicatedCommitWhenTemplateAndDataAreValid() throws IOException { + // Mock JSON template structure + JsonNode templateNode = JsonNodeFactory.instance.objectNode() + .put("name", "test-project") + .put("description", "Dedykowany dla projektu coś tam") + .put("pattern", "{ticket_id} {type}({scope}): {message}") + .set("model", JsonNodeFactory.instance.objectNode() + .put("ticket_id", "Numer powiązanego zadania w JIRA") + .set("type", JsonNodeFactory.instance.arrayNode() + .add("feat") + .add("fix") + .add("junk") + .add("chore") + .add("test")) + ); + + JsonNode templatesNode = JsonNodeFactory.instance.objectNode() + .set("dedicated", JsonNodeFactory.instance.arrayNode().add(templateNode)); + + // Mock ObjectMapper behavior using the same path as in production code + when(objectMapper.readTree(new File("src/main/resources/templates/dedicated-meta-schema.json"))) + .thenReturn(templatesNode); + + JsonNode validCommitData = JsonNodeFactory.instance.objectNode() + .put("ticket_id", "JIRA-123") + .put("type", "feat") + .put("scope", "auth-module") + .put("message", "Dodano obsługę OAuth2"); + + // Execute method + String result = service.generateDedicatedCommit("test-project", validCommitData); + // Verify result + assertEquals("JIRA-123 feat(auth-module): Dodano obsługę OAuth2", result); + } } \ 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 index 02c2457..67339e9 100644 --- a/src/test/resources/test-dedicated-meta-schema.json +++ b/src/test/resources/test-dedicated-meta-schema.json @@ -1 +1,21 @@ -{"dedicated":[]} \ No newline at end of file +{ + "dedicated": [ + { + "name": "test-project", + "description": "Dedykowany dla projektu coś tam", + "pattern": "{ticket_id} {type}({scope}): {message}", + "model": { + "ticket_id": "Numer powiązanego zadania w JIRA", + "type": [ + "feat", + "fix", + "junk", + "chore", + "test" + ], + "scope": "[moduł lub komponent]", + "message": "Krótki opis zmiany" + } + } + ] +} \ No newline at end of file