From 48419db6f1f103a78d06602a2bd24f8e7f289e67 Mon Sep 17 00:00:00 2001 From: Sebastien Vermeille Date: Fri, 23 Aug 2024 11:14:45 +0200 Subject: [PATCH] Refactoring (#21) Global review/refactoring of the existing code to improve maintainability, testability, maintenance and evolution in the future Fix Sonar issues --- docs/docs/getting-started.md | 16 +- lombok.config | 1 + pom.xml | 108 ++++-- stylesniffer-annotation-processor/pom.xml | 53 ++- .../processor/CaseStyleElementsCollector.java | 53 +++ .../annotation/processor/FileWriter.java | 64 ++++ .../processor/ProcessorTemplateEngine.java | 54 +++ .../RegisterCaseStyleAnnotationProcessor.java | 143 +++----- .../processor/TemplateRenderer.java | 106 ++++++ .../src/test/java/.gitkeep | 0 .../processor/AnnotatedClassTest.java | 35 ++ .../CaseStyleElementsCollectorTest.java | 87 +++++ .../annotation/processor/FileWriterTest.java | 93 +++++ ...isterCaseStyleAnnotationProcessorTest.java | 166 +++++++++ .../processor/TemplateRendererTest.java | 128 +++++++ stylesniffer-api/pom.xml | 34 +- .../cookiecode/stylesniffer/StyleSniffer.java | 82 +++++ .../stylesniffer/api/BaseCaseStyle.java | 53 +++ .../stylesniffer/api/CaseStyle.java | 26 +- stylesniffer-api/src/test/java/.gitkeep | 0 .../stylesniffer/api/BaseCaseStyleTest.java | 101 ++++++ .../CaseStyleHavingNoVariantNamesImpl.java | 44 +++ .../CaseStyleHavingTwoVariantNamesImpl.java | 48 +++ .../stylesniffer/api/CaseStyleTest.java | 60 ++++ stylesniffer-impl/pom.xml | 67 ++-- .../cookiecode/stylesniffer/StyleSniffer.java | 136 -------- .../stylesniffer/StyleSnifferFactory.java | 9 +- .../stylesniffer/StyleSnifferImpl.java | 160 +++++++++ .../impl/{ => casestyle}/KebabCaseStyle.java | 5 +- .../{ => casestyle}/LowerCamelCaseStyle.java | 5 +- .../impl/{ => casestyle}/PascalCaseStyle.java | 50 +-- .../ScreamingSnakeCaseStyle.java | 5 +- .../impl/{ => casestyle}/SnakeCaseStyle.java | 5 +- .../stylesniffer/AbstractCaseStyle.java | 32 ++ .../FaultyCaseStyleWithRuntimeException.java | 48 +++ ...ultyCaseStyleWithoutPublicConstructor.java | 46 +++ ...seStyleWithoutPublicNoArgsConstructor.java | 46 +++ .../stylesniffer/StyleSnifferFactoryTest.java | 61 ++++ .../stylesniffer/StyleSnifferImplTest.java | 322 ++++++++++++++++++ .../{ => casestyle}/KebabCaseStyleTest.java | 6 +- .../LowerCamelCaseStyleTest.java | 6 +- .../{ => casestyle}/PascalCaseStyleTest.java | 18 +- .../ScreamingSnakeCaseStyleTest.java | 6 +- .../{ => casestyle}/SnakeCaseStyleTest.java | 6 +- stylesniffer-report-aggregate/pom.xml | 86 +++++ stylesniffer-testkit/pom.xml | 9 +- ...seStyleTest.java => CaseStyleTestKit.java} | 4 +- stylesniffer-testkit/src/test/java/.gitkeep | 0 .../testkit/CaseStyleTestKitTest.java | 48 +++ .../stylesniffer/testkit/DummyCaseStyle.java | 43 +++ 50 files changed, 2398 insertions(+), 386 deletions(-) create mode 100644 lombok.config create mode 100644 stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollector.java create mode 100644 stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriter.java create mode 100644 stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/ProcessorTemplateEngine.java create mode 100644 stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRenderer.java delete mode 100644 stylesniffer-annotation-processor/src/test/java/.gitkeep create mode 100644 stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/AnnotatedClassTest.java create mode 100644 stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollectorTest.java create mode 100644 stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriterTest.java create mode 100644 stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessorTest.java create mode 100644 stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRendererTest.java create mode 100644 stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java create mode 100644 stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/BaseCaseStyle.java delete mode 100644 stylesniffer-api/src/test/java/.gitkeep create mode 100644 stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/BaseCaseStyleTest.java create mode 100644 stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingNoVariantNamesImpl.java create mode 100644 stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingTwoVariantNamesImpl.java create mode 100644 stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleTest.java delete mode 100644 stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java create mode 100644 stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferImpl.java rename stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/KebabCaseStyle.java (94%) rename stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/LowerCamelCaseStyle.java (95%) rename stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/PascalCaseStyle.java (72%) rename stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/ScreamingSnakeCaseStyle.java (95%) rename stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/SnakeCaseStyle.java (94%) create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/AbstractCaseStyle.java create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithRuntimeException.java create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicConstructor.java create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicNoArgsConstructor.java create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferFactoryTest.java create mode 100644 stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferImplTest.java rename stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/KebabCaseStyleTest.java (90%) rename stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/LowerCamelCaseStyleTest.java (90%) rename stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/PascalCaseStyleTest.java (76%) rename stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/ScreamingSnakeCaseStyleTest.java (90%) rename stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/{ => casestyle}/SnakeCaseStyleTest.java (90%) create mode 100644 stylesniffer-report-aggregate/pom.xml rename stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/{BaseCaseStyleTest.java => CaseStyleTestKit.java} (97%) delete mode 100644 stylesniffer-testkit/src/test/java/.gitkeep create mode 100644 stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKitTest.java create mode 100644 stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/DummyCaseStyle.java diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 9166c52..6da175c 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -50,11 +50,14 @@ values={[ ```java +// Create an instance of StyleSniffer var styleSniffer = StyleSnifferFactory.createStyleSniffer(); +// Retrieve a CaseStyle and handle the result Optional caseStyle = styleSniffer.getCaseStyle("myVariableName"); caseStyle.ifPresent(style -> System.out.println("Matched style: " + style.getName())); +// Retrieve and print supported case styles Set supportedStyles = styleSniffer.getSupportedCaseStyles(); System.out.println("Supported styles: " + supportedStyles); ``` @@ -64,7 +67,18 @@ System.out.println("Supported styles: " + supportedStyles); **gradle.properties** ```kotlin -// TODO: document this code for Kotlin +// Create an instance of StyleSniffer +val styleSniffer = StyleSnifferFactory.createStyleSniffer() + +// Retrieve a CaseStyle and handle the result +val caseStyle: CaseStyle? = styleSniffer.getCaseStyle("myVariableName") +caseStyle?.let { + println("Matched style: ${it.getName()}") +} + +// Retrieve and print supported case styles +val supportedStyles: Set = styleSniffer.getSupportedCaseStyles() +println("Supported styles: $supportedStyles") ``` diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/pom.xml b/pom.xml index 5f78fa9..b96ea14 100644 --- a/pom.xml +++ b/pom.xml @@ -33,22 +33,20 @@ pom + stylesniffer-api stylesniffer-impl stylesniffer-annotation-processor - stylesniffer-api stylesniffer-testkit + stylesniffer-report-aggregate + 21 UTF-8 ${basedir} - ${java.sdk.version} - ${java.sdk.version} - ${source.encoding} - - + sebastienvermeille https://sonarcloud.io java @@ -58,22 +56,25 @@ src/test/java https://stylesniffer.cookiecode.dev/ https://github.com/sebastienvermeille/StyleSniffer + ${project.basedir}/report-aggregate/target/site/jacoco-aggregate/jacoco.xml - + 0.8 1.1.1 1.5.7 2.0.16 3.1.2.RELEASE + 3.0.0 - + 3.26.3 5.11.0 + 5.12.0 - + 1.18.34 - + 3.13.0 3.4.0 3.3.1 @@ -93,6 +94,12 @@ ${lombok.version} provided + + jakarta.annotation + jakarta.annotation-api + ${jakarta.annotation.version} + compile + org.thymeleaf thymeleaf @@ -127,7 +134,7 @@ ch.qos.logback logback-classic ${logback.version} - compile + runtime org.assertj @@ -141,6 +148,18 @@ ${junit.version} test + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + @@ -155,26 +174,66 @@ ${java.sdk.version} + org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} + org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin.version} + org.jacoco jacoco-maven-plugin ${maven-jacoco-plugin.version} + + + agent + + prepare-agent + + + + jacoco-report + + report + + test + + + check + + check + + + + + CLASS + + + LINE + COVEREDRATIO + 0.80 + + + + + + + + com.spotify.fmt fmt-maven-plugin @@ -225,6 +284,7 @@ + se.ayoy.maven-plugins ayoy-license-verifier-maven-plugin @@ -246,32 +306,6 @@ - - org.jacoco - jacoco-maven-plugin - ${maven-jacoco-plugin.version} - - - HTML - XML - - - - - - prepare-agent - - agent - - - - report - - jacoco-report - test - - - org.sonarsource.scanner.maven sonar-maven-plugin diff --git a/stylesniffer-annotation-processor/pom.xml b/stylesniffer-annotation-processor/pom.xml index a71eb6a..ae11e4d 100644 --- a/stylesniffer-annotation-processor/pom.xml +++ b/stylesniffer-annotation-processor/pom.xml @@ -29,6 +29,7 @@ 4.0.0 stylesniffer-annotation-processor + 1.0.0-SNAPSHOT jar @@ -38,15 +39,24 @@ - ${java.sdk.version} - ${java.sdk.version} - ${source.encoding} ${basedir}/.. - ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + org.thymeleaf + thymeleaf + + + dev.cookiecode + stylesniffer-api + 1.0.0-SNAPSHOT + compile + + + org.projectlombok lombok @@ -55,32 +65,45 @@ com.google.auto.service auto-service + + - org.thymeleaf - thymeleaf - - - flogger com.google.flogger + flogger - flogger-slf4j-backend com.google.flogger + flogger-slf4j-backend - slf4j-api org.slf4j + slf4j-api ch.qos.logback logback-classic + - dev.cookiecode - stylesniffer-api - 1.0.0-SNAPSHOT - compile + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test diff --git a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollector.java b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollector.java new file mode 100644 index 0000000..cb8b487 --- /dev/null +++ b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollector.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import java.lang.annotation.Annotation; +import java.util.List; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; +import lombok.NonNull; + +/** + * Collects elements annotated with {@link + * dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle}. + * + *

This class is responsible for scanning the {@link RoundEnvironment} and extracting the fully + * qualified class names of elements annotated with {@code @RegisterCaseStyle}. + * + *

The collected class names are used in the template rendering process. + * + * @author Sebastien Vermeille + * @see dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle + */ +public class CaseStyleElementsCollector { + + static final Class ANNOTATION_CLASS = RegisterCaseStyle.class; + + public List collectElements(@NonNull RoundEnvironment roundEnv) { + return roundEnv.getElementsAnnotatedWith(ANNOTATION_CLASS).stream() + .map(element -> ((TypeElement) element).getQualifiedName().toString()) + .toList(); + } +} diff --git a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriter.java b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriter.java new file mode 100644 index 0000000..5d2bd59 --- /dev/null +++ b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriter.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_NAME; +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_PACKAGE_NAME; + +import java.io.IOException; +import javax.annotation.processing.ProcessingEnvironment; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Handles the writing of the generated source code to a file. + * + *

This class is responsible for creating a new Java source file and writing the generated code + * into it. + * + *

The file is created in the package {@code dev.cookiecode.stylesniffer.generated}. + * + * @author Sebastien Vermeille + */ +@RequiredArgsConstructor +public class FileWriter { + + static final String DOT = "."; + private final ProcessingEnvironment processingEnv; + + /** + * Writes the generated code to a new Java source file. + * + * @param generatedCode The code to write into the file. + * @throws IOException If an I/O error occurs while writing the file. + */ + public void writeToFile(@NonNull String generatedCode) throws IOException { + try (var writer = + processingEnv + .getFiler() + .createSourceFile(GENERATED_CLASS_PACKAGE_NAME + DOT + GENERATED_CLASS_NAME) + .openWriter()) { + writer.write(generatedCode); + } + } +} diff --git a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/ProcessorTemplateEngine.java b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/ProcessorTemplateEngine.java new file mode 100644 index 0000000..264cd6a --- /dev/null +++ b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/ProcessorTemplateEngine.java @@ -0,0 +1,54 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import lombok.experimental.Delegate; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +/** + * Wrapper for Thymeleaf template engine pre-configured for the project + * + * @author Sebastien Vermeille + * @see TemplateEngine + */ +public class ProcessorTemplateEngine { + + private static final String TEMPLATES_DIR = "templates/"; + private static final String TEMPLATE_EXTENSION = ".tpl"; + private static final String TEMPLATE_MODE = "TEXT"; + private static final String TEMPLATE_ENCODING = "UTF-8"; + + @Delegate(types = TemplateEngine.class) + private final TemplateEngine templateEngine = new TemplateEngine(); + + public ProcessorTemplateEngine() { + final var templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setPrefix(TEMPLATES_DIR); + templateResolver.setSuffix(TEMPLATE_EXTENSION); + templateResolver.setTemplateMode(TEMPLATE_MODE); + templateResolver.setCharacterEncoding(TEMPLATE_ENCODING); + + templateEngine.setTemplateResolver(templateResolver); + } +} diff --git a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessor.java b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessor.java index 8372ce7..cca3453 100644 --- a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessor.java +++ b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessor.java @@ -22,135 +22,72 @@ */ package dev.cookiecode.stylesniffer.annotation.processor; -import static java.time.LocalDateTime.now; -import static java.time.ZoneOffset.UTC; -import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static lombok.AccessLevel.*; -import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; -import dev.cookiecode.stylesniffer.api.CaseStyle; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; import java.util.Set; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Generated; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; +import javax.annotation.processing.*; import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import lombok.*; import lombok.extern.flogger.Flogger; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.Context; -import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; /** - * Processes the dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle annotation, generating a - * CaseStyleInjector class that registers all dev.cookiecode.stylesniffer.api.CaseStyle - * implementations. + * Annotation processor for {@link dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle}. * - *

Uses Thymeleaf for code generation and Flogger for logging. Supports Java - * SourceVersion.RELEASE_21. + *

This processor scans for classes annotated with {@code @RegisterCaseStyle}, collects them, and + * generates a {@code CaseStyleInjector} class using a template. + * + *

The actual processing tasks are delegated to {@link CaseStyleElementsCollector}, {@link + * TemplateRenderer}, and {@link FileWriter}, promoting the Single Responsibility Principle. * * @author Sebastien Vermeille * @see dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle * @see dev.cookiecode.stylesniffer.api.CaseStyle */ +@NoArgsConstructor(access = PUBLIC) +@AllArgsConstructor( + access = PACKAGE, + onConstructor_ = {@VisibleForTesting}) +@Getter( + value = PACKAGE, + onMethod_ = {@VisibleForTesting}) @SupportedAnnotationTypes("dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle") @SupportedSourceVersion(SourceVersion.RELEASE_21) @Flogger public class RegisterCaseStyleAnnotationProcessor extends AbstractProcessor { - private static final String GENERATED_CLASS_PACKAGE_NAME = - "dev.cookiecode.stylesniffer.generated"; - private static final String GENERATED_CLASS_NAME = "CaseStyleInjector"; - private static final String TEMPLATE_EXTENSION = ".tpl"; - private static final String TEMPLATE_MODE = "TEXT"; - private static final String UTF_8 = "UTF-8"; - private static final String TEMPLATES_DIR = "templates/"; - private static final String TEMPLATE_VARIABLE_PACKAGE_NAME = "packageName"; - private static final String TEMPLATE_VARIABLE_IMPORTS = "imports"; - private static final String TEMPLATE_VARIABLE_CLASS_NAME = "className"; - private static final String TEMPLATE_VARIABLE_ELEMENTS = "elements"; - private static final String TEMPLATE_VARIABLE_GENERATED_AT = "generatedAt"; - private static final String TEMPLATE_FILE_NAME = "case_style_injector"; - private static final String POINT = "."; - private static final Class ANNOTATION_CLASS = RegisterCaseStyle.class; - private static final Class IMPLEMENTED_INTERFACE_CLASS = CaseStyle.class; - - @Override - public boolean process( - final Set annotations, final RoundEnvironment roundEnv) { - final var annotatedElements = roundEnv.getElementsAnnotatedWith(ANNOTATION_CLASS); + // Common constants shared across classes + public static final String GENERATED_CLASS_PACKAGE_NAME = "dev.cookiecode.stylesniffer.generated"; + public static final String GENERATED_CLASS_NAME = "CaseStyleInjector"; - if (!annotatedElements.isEmpty()) { - this.generateCaseStyleInjector(annotatedElements); - } + private TemplateRenderer templateRenderer; + private FileWriter fileWriter; + private CaseStyleElementsCollector elementsCollector; - return true; // Indicates that annotations are claimed + @Override + public synchronized void init(@NonNull ProcessingEnvironment processingEnv) { + super.init(processingEnv); + final var templateEngine = new ProcessorTemplateEngine(); + this.templateRenderer = new TemplateRenderer(templateEngine); + this.fileWriter = new FileWriter(processingEnv); + this.elementsCollector = new CaseStyleElementsCollector(); } - private void generateCaseStyleInjector(final Set elements) { + @Override + public boolean process( + @NonNull Set annotations, @NonNull RoundEnvironment roundEnv) { try { - final var templateEngine = this.configureTemplateEngine(); - final var context = this.prepareTemplateContext(elements); - - final var generatedCode = templateEngine.process(TEMPLATE_FILE_NAME, context); - this.writeGeneratedClassToFile(generatedCode); - - } catch (final IOException e) { + final var elements = elementsCollector.collectElements(roundEnv); + if (!elements.isEmpty()) { + final var generatedCode = templateRenderer.renderTemplate(elements); + fileWriter.writeToFile(generatedCode); + } + } catch (IOException e) { log.atSevere().withCause(e).log("Failed to generate the %s class", GENERATED_CLASS_NAME); } - } - - private TemplateEngine configureTemplateEngine() { - final var templateResolver = new ClassLoaderTemplateResolver(); - templateResolver.setPrefix(TEMPLATES_DIR); - templateResolver.setSuffix(TEMPLATE_EXTENSION); - templateResolver.setTemplateMode(TEMPLATE_MODE); - templateResolver.setCharacterEncoding(UTF_8); - final var templateEngine = new TemplateEngine(); - templateEngine.setTemplateResolver(templateResolver); - return templateEngine; - } - - private Context prepareTemplateContext(final Set elements) { - final var context = new Context(); - context.setVariable(TEMPLATE_VARIABLE_PACKAGE_NAME, GENERATED_CLASS_PACKAGE_NAME); - - final var imports = - Set.of( - Generated.class.getCanonicalName(), - List.class.getCanonicalName(), - ArrayList.class.getCanonicalName(), - IMPLEMENTED_INTERFACE_CLASS.getCanonicalName()); - - context.setVariable(TEMPLATE_VARIABLE_IMPORTS, imports.stream().sorted().toList()); - context.setVariable(TEMPLATE_VARIABLE_CLASS_NAME, GENERATED_CLASS_NAME); - - final var classesList = - elements.stream() - .map(element -> ((TypeElement) element).getQualifiedName().toString()) - .toList(); - - context.setVariable(TEMPLATE_VARIABLE_ELEMENTS, classesList); - - final var moment = now(UTC).format(ISO_LOCAL_DATE_TIME); - context.setVariable(TEMPLATE_VARIABLE_GENERATED_AT, moment); - - return context; - } - - private void writeGeneratedClassToFile(final String generatedCode) throws IOException { - final var file = - this.processingEnv - .getFiler() - .createSourceFile(GENERATED_CLASS_PACKAGE_NAME + POINT + GENERATED_CLASS_NAME); - try (final var writer = file.openWriter()) { - writer.write(generatedCode); - } + return true; } } diff --git a/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRenderer.java b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRenderer.java new file mode 100644 index 0000000..40bae69 --- /dev/null +++ b/stylesniffer-annotation-processor/src/main/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRenderer.java @@ -0,0 +1,106 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_NAME; +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_PACKAGE_NAME; +import static java.time.LocalDateTime.now; +import static java.time.ZoneOffset.UTC; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; + +import com.google.common.annotations.VisibleForTesting; +import dev.cookiecode.stylesniffer.api.CaseStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.Generated; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.thymeleaf.context.Context; + +/** + * Handles the rendering of templates using Thymeleaf. + * + *

This class is responsible for setting up the template engine, preparing the context with + * necessary variables, and rendering the final template into a string. + * + *

It uses the {@code case_style_injector.tpl} template to generate the source code. + * + * @author Sebastien Vermeille + * @see org.thymeleaf.context.Context + */ +@RequiredArgsConstructor(onConstructor_ = {@VisibleForTesting}) +public class TemplateRenderer { + + static final String TEMPLATE_FILE_NAME = "case_style_injector"; + + // Template variable constants + static final String PACKAGE_NAME = "packageName"; + static final String CLASS_NAME = "className"; + static final String ELEMENTS = "elements"; + static final String IMPORTS = "imports"; + static final String GENERATED_AT = "generatedAt"; + + private final ProcessorTemplateEngine templateEngine; + + /** + * Renders the template with the given list of elements. + * + * @param elements List of fully qualified class names to include in the generated class. + * @return Rendered template as a string. + */ + public String renderTemplate(@NonNull List elements) { + if (elements.isEmpty()) { + throw new IllegalStateException( + "Cannot render empty elements, upper layer should have prevented this to occurs."); + } + + final var context = prepareTemplateContext(elements); + return templateEngine.process(TEMPLATE_FILE_NAME, context); + } + + @VisibleForTesting + Context prepareTemplateContext(@NonNull List elements) { + if (elements.isEmpty()) { + throw new IllegalStateException( + "Cannot build context for empty elements, upper layer should have prevented this to occurs."); + } + + final var context = new Context(); + context.setVariable(PACKAGE_NAME, GENERATED_CLASS_PACKAGE_NAME); + context.setVariable(CLASS_NAME, GENERATED_CLASS_NAME); + context.setVariable(ELEMENTS, elements); + context.setVariable( + IMPORTS, + Set.of( + Generated.class.getCanonicalName(), + List.class.getCanonicalName(), + ArrayList.class.getCanonicalName(), + CaseStyle.class.getCanonicalName()) + .stream() + .sorted() + .toList()); + context.setVariable(GENERATED_AT, now(UTC).format(ISO_LOCAL_DATE_TIME)); + return context; + } +} diff --git a/stylesniffer-annotation-processor/src/test/java/.gitkeep b/stylesniffer-annotation-processor/src/test/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/AnnotatedClassTest.java b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/AnnotatedClassTest.java new file mode 100644 index 0000000..8a735ff --- /dev/null +++ b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/AnnotatedClassTest.java @@ -0,0 +1,35 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; + +/** + * Test class This class ensure that the annotation can be applied on the class (breaks compilation + * in case of changes in @RegisterCaseStyle) and is used in different mocks + * + * @author Sebastien Vermeille + */ +@RegisterCaseStyle +@SuppressWarnings("java:S2187") // This is a compile time test +public class AnnotatedClassTest {} diff --git a/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollectorTest.java b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollectorTest.java new file mode 100644 index 0000000..9070ffc --- /dev/null +++ b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/CaseStyleElementsCollectorTest.java @@ -0,0 +1,87 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static dev.cookiecode.stylesniffer.annotation.processor.CaseStyleElementsCollector.ANNOTATION_CLASS; +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.util.Set; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class CaseStyleElementsCollectorTest { + + @Mock private RoundEnvironment roundEnvironment; + + private CaseStyleElementsCollector caseStyleElementsCollector; + + @BeforeEach + void setUp() { + caseStyleElementsCollector = new CaseStyleElementsCollector(); + } + + @Test + void collectElementsShouldReturnCaseStyleElementsGivenSomeWhereMocked() { + + // GIVEN + final var name = mock(Name.class); + doReturn("some").when(name).toString(); + final var mockedElement = mock(TypeElement.class); + doReturn(name).when(mockedElement).getQualifiedName(); + final var mockedClasses = Set.of(mockedElement); + doReturn(mockedClasses).when(roundEnvironment).getElementsAnnotatedWith(ANNOTATION_CLASS); + + // WHEN + var result = caseStyleElementsCollector.collectElements(roundEnvironment); + + // THEN + assertThat(result).isNotEmpty(); + } + + @Test + void collectElementsShouldReturnAnEmptyListGivenNoAnnotatedElementsArePresent() { + + // GIVEN + doReturn(emptySet()).when(roundEnvironment).getElementsAnnotatedWith(ANNOTATION_CLASS); + + // WHEN + var result = caseStyleElementsCollector.collectElements(roundEnvironment); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } +} diff --git a/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriterTest.java b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriterTest.java new file mode 100644 index 0000000..5a8cb9f --- /dev/null +++ b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/FileWriterTest.java @@ -0,0 +1,93 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static dev.cookiecode.stylesniffer.annotation.processor.FileWriter.DOT; +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_NAME; +import static dev.cookiecode.stylesniffer.annotation.processor.RegisterCaseStyleAnnotationProcessor.GENERATED_CLASS_PACKAGE_NAME; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.io.Writer; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class FileWriterTest { + + @Mock private ProcessingEnvironment processingEnvironment; + + @InjectMocks private FileWriter fileWriter; + + @Test + void writeToFileShouldThrowAnIOExceptionGivenThereIsAnIssue() throws Exception { + // GIVEN + final var filerMock = mock(Filer.class); + doThrow(new IOException("IO error")).when(filerMock).createSourceFile(anyString()); + when(processingEnvironment.getFiler()).thenReturn(filerMock); + + final var someValidCode = "some code that is not generating any error."; + + assertThrows( + IOException.class, + () -> { + // WHEN + fileWriter.writeToFile(someValidCode); + }); + } + + @Test + void writeToFileShouldPassGeneratedCodeToWriteMethodGivenThereIsNoIssue() throws Exception { + // GIVEN + final var filerMock = mock(Filer.class); + when(processingEnvironment.getFiler()).thenReturn(filerMock); + + final var javaFileObjectMock = mock(JavaFileObject.class); + doReturn(javaFileObjectMock) + .when(filerMock) + .createSourceFile(GENERATED_CLASS_PACKAGE_NAME + DOT + GENERATED_CLASS_NAME); + + final var writerMock = mock(Writer.class); + when(javaFileObjectMock.openWriter()).thenReturn(writerMock); + + final var someValidCode = "some code that is not generating any error."; + + // WHEN + fileWriter.writeToFile(someValidCode); + + // THEN + verify(writerMock, times(1)).write(someValidCode); + } +} diff --git a/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessorTest.java b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessorTest.java new file mode 100644 index 0000000..ec2bc76 --- /dev/null +++ b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/RegisterCaseStyleAnnotationProcessorTest.java @@ -0,0 +1,166 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import lombok.NonNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class RegisterCaseStyleAnnotationProcessorTest { + + @Mock private TemplateRenderer templateRenderer; + + @Mock private FileWriter fileWriter; + + @Mock private CaseStyleElementsCollector caseStyleElementsCollector; + + @InjectMocks private RegisterCaseStyleAnnotationProcessor processor; + + @Test + void processShouldReturnTrueEvenIfNoElementsWereRegistered() { + // GIVEN + final var roundEnv = noCaseStyleAnnotatedElements(); + + // WHEN + final var actualResult = processor.process(emptySet(), roundEnv); + + // THEN + assertThat(actualResult).isTrue(); + } + + @Test + void processShouldReturnTrueEvenIfAnIOExceptionOccurs() throws Exception { + // GIVEN + final var elements = List.of("DummyStyle", "SuperDummyPlusStyle"); + final var roundEnv = caseStyleAnnotatedElements(elements); + + final var generatedCode = "some code;"; + doReturn(generatedCode).when(templateRenderer).renderTemplate(elements); + + doThrow(new IOException("File writer error")).when(fileWriter).writeToFile(generatedCode); + + // WHEN + final var actualResult = processor.process(emptySet(), roundEnv); + + // THEN + assertThat(actualResult).isTrue(); + } + + @Test + void processShouldNotInteractWithTemplateRendererGivenNoElementsAreCollected() { + // GIVEN + final var mockedRoundEnv = noCaseStyleAnnotatedElements(); + + // WHEN + processor.process(emptySet(), mockedRoundEnv); + + // THEN + verifyNoInteractions(templateRenderer); + } + + @Test + void processShouldNotInteractWithFileWriterGivenNoElementsAreCollected() { + // GIVEN + final var mockedRoundEnv = noCaseStyleAnnotatedElements(); + + // WHEN + processor.process(emptySet(), mockedRoundEnv); + + // THEN + verifyNoInteractions(fileWriter); + } + + @Test + void processShouldProvideRetrievedElementsToTemplateRendererGivenThereAreElements() { + // GIVEN + final var elements = List.of("DummyStyle", "SuperDummyPlusStyle"); + final var roundEnv = caseStyleAnnotatedElements(elements); + + // WHEN + processor.process(emptySet(), roundEnv); + + // THEN + verify(templateRenderer, times(1)).renderTemplate(elements); + } + + @Test + void processShouldProvideGeneratedCodeFileWriterGivenSomeCodeWereGenerated() throws Exception { + // GIVEN + final var elements = List.of("DummyStyle", "SuperDummyPlusStyle"); + final var roundEnv = caseStyleAnnotatedElements(elements); + final var generatedCode = "Generated code;"; + doReturn(generatedCode).when(templateRenderer).renderTemplate(elements); + + // WHEN + processor.process(emptySet(), roundEnv); + + // THEN + verify(fileWriter, times(1)).writeToFile(generatedCode); + } + + @Test + void initShouldInitAllRequiredFields() { + // GIVEN + final var processingEnv = mock(ProcessingEnvironment.class); + final var processorWithoutInjectedMocks = new RegisterCaseStyleAnnotationProcessor(); + + // WHEN + processorWithoutInjectedMocks.init(processingEnv); + + // THEN + assertThat(processorWithoutInjectedMocks.getTemplateRenderer()).isNotNull(); + assertThat(processorWithoutInjectedMocks.getFileWriter()).isNotNull(); + assertThat(processorWithoutInjectedMocks.getElementsCollector()).isNotNull(); + } + + private RoundEnvironment noCaseStyleAnnotatedElements() { + final var roundEnv = mock(RoundEnvironment.class); + final var noElements = emptyList(); + doReturn(noElements).when(caseStyleElementsCollector).collectElements(roundEnv); + return roundEnv; + } + + private RoundEnvironment caseStyleAnnotatedElements(@NonNull List caseStyleNames) { + final var roundEnv = mock(RoundEnvironment.class); + doReturn(caseStyleNames).when(caseStyleElementsCollector).collectElements(roundEnv); + return roundEnv; + } +} diff --git a/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRendererTest.java b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRendererTest.java new file mode 100644 index 0000000..badfb5b --- /dev/null +++ b/stylesniffer-annotation-processor/src/test/java/dev/cookiecode/stylesniffer/annotation/processor/TemplateRendererTest.java @@ -0,0 +1,128 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.annotation.processor; + +import static dev.cookiecode.stylesniffer.annotation.processor.TemplateRenderer.*; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thymeleaf.context.Context; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +@ExtendWith(MockitoExtension.class) +class TemplateRendererTest { + + @Mock private ProcessorTemplateEngine templateEngine; + + private TemplateRenderer templateRenderer; + + @BeforeEach + void setUp() { + templateRenderer = spy(new TemplateRenderer(templateEngine)); + } + + @Test + void renderTemplateShouldThrowAnIllegalStateExceptionGivenItReceivesAnEmptyListOfElements() { + // GIVEN + final List emptyElements = emptyList(); + + assertThrows( + IllegalStateException.class, + () -> { + // WHEN + templateRenderer.renderTemplate(emptyElements); + }); + } + + @Test + void renderTemplateShouldInvokePrepareTemplateContextGivenItContainsElements() { + // GIVEN + final var elements = List.of("firstElement", "secondElement"); + + // WHEN + templateRenderer.renderTemplate(elements); + + // THEN + verify(templateRenderer, times(1)).prepareTemplateContext(elements); + } + + @Test + void + renderTemplateShouldInvokeTemplateEngineProcessWithPreviouslyCreatedContextAndTemplateFileNameGivenItContainsElements() { + // GIVEN + final var elements = List.of("firstElement", "secondElement"); + final var templateContext = mock(Context.class); + doReturn(templateContext).when(templateRenderer).prepareTemplateContext(elements); + + // WHEN + templateRenderer.renderTemplate(elements); + + // THEN + verify(templateEngine, times(1)).process(TEMPLATE_FILE_NAME, templateContext); + } + + @Test + void + prepareTemplateContextShouldThrowAnIllegalStateExceptionGivenItReceivesAnEmptyListOfElements() { + // GIVEN + final List emptyElements = emptyList(); + + assertThrows( + IllegalStateException.class, + () -> { + // WHEN + templateRenderer.prepareTemplateContext(emptyElements); + }); + } + + @Test + void prepareTemplateContextShouldPopulateAllContextVariablesProperly() { + // GIVEN + final var elements = List.of("firstElement", "secondElement"); + final var contextVariableNames = + Set.of(PACKAGE_NAME, CLASS_NAME, ELEMENTS, IMPORTS, GENERATED_AT); + // WHEN + var actualContext = templateRenderer.prepareTemplateContext(elements); + + // THEN + assertThat(actualContext).isNotNull(); + assertThat(actualContext.getVariableNames()).containsAll(contextVariableNames); + for (final var contextVariableName : contextVariableNames) { + assertThat(actualContext.getVariable(contextVariableName)).isNotNull().isNotEqualTo(""); + } + assertThat(actualContext.getVariable(ELEMENTS)).isEqualTo(elements); + } +} diff --git a/stylesniffer-api/pom.xml b/stylesniffer-api/pom.xml index 974ecce..ac9568b 100644 --- a/stylesniffer-api/pom.xml +++ b/stylesniffer-api/pom.xml @@ -29,6 +29,7 @@ 4.0.0 stylesniffer-api + 1.0.0-SNAPSHOT jar @@ -38,19 +39,44 @@ - ${java.sdk.version} - ${java.sdk.version} - ${source.encoding} ${basedir}/.. - ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + + + jakarta.annotation + jakarta.annotation-api + + + lombok org.projectlombok + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + diff --git a/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java new file mode 100644 index 0000000..b071414 --- /dev/null +++ b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java @@ -0,0 +1,82 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import dev.cookiecode.stylesniffer.api.CaseStyle; +import jakarta.annotation.Nullable; +import java.util.*; + +/** + * Interface for a service that provides functionality for sniffing and managing case styles. + * + *

This interface allows for querying case styles by name, retrieving supported case style names, + * and accessing case style names including their variants. + * + *

The implementation of this interface should handle the registration and retrieval of case + * styles as well as manage the variations of case styles. + * + * @author Sebastien Vermeille + */ +public interface StyleSniffer { + + /** + * Retrieves a {@link CaseStyle} that matches the given name. + * + *

This method searches through the registered case styles and returns the first style that + * matches the provided name. + * + * @param name the name to match against case styles + * @return an {@link Optional} containing the matching {@code CaseStyle} if found, or an empty + * {@code Optional} if no match is found + */ + Optional getCaseStyle(@Nullable final String name); + + /** + * Retrieves a {@link CaseStyle} based on either its variant name or its primary name. + * + * @param variantOrName the variant or primary name to match against case styles + * @return an {@link Optional} containing the matching {@code CaseStyle} if found, or an empty + * {@code Optional} if no match is found + */ + Optional getCaseStyleWithVariantOrName(@Nullable final String variantOrName); + + /** + * Returns a set of names for all supported case styles. + * + *

This method provides a set of unique names for the case styles currently recognized by this + * {@code StyleSniffer}. + * + * @return a {@link Set} of names for supported case styles + */ + Set getSupportedCaseStyles(); + + /** + * Returns a set of names for all supported case styles, including all variants. + * + *

This method provides a comprehensive list of all case style names recognized by this {@code + * StyleSniffer}, including both primary names and their variants. + * + * @return a {@link Set} of names for all supported case styles, including variants + */ + Set getSupportedCaseStylesIncludingVariants(); +} diff --git a/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/BaseCaseStyle.java b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/BaseCaseStyle.java new file mode 100644 index 0000000..fe472e9 --- /dev/null +++ b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/BaseCaseStyle.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.api; + +import jakarta.annotation.Nullable; + +/** + * Base implementation of the {@link CaseStyle} interface that provides default implementations for + * {@code equals} and {@code hashCode} based on the case style's name. + * + * @author Sebastien Vermeille + */ +public abstract class BaseCaseStyle implements CaseStyle { + + @Override + public boolean equals(@Nullable Object obj) { + + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + CaseStyle caseStyle = (CaseStyle) obj; + return getName().equals(caseStyle.getName()); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } +} diff --git a/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/CaseStyle.java b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/CaseStyle.java index 02dc322..3aae360 100644 --- a/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/CaseStyle.java +++ b/stylesniffer-api/src/main/java/dev/cookiecode/stylesniffer/api/CaseStyle.java @@ -22,18 +22,28 @@ */ package dev.cookiecode.stylesniffer.api; +import static java.util.Collections.unmodifiableSet; import static java.util.Set.of; +import jakarta.annotation.Nullable; import java.util.Set; import lombok.NonNull; /** * Represents a naming convention (case style) that can be matched against a given string. * + * @implNote Implementations should define specific case styles such as camelCase, snake_case, etc. + * Each case style must have a unique name and may have variant names. * @author Sebastien Vermeille */ public interface CaseStyle { + /** + * Determines if the given string matches this case style. + * + * @param name the string to be checked, must not be null + * @return true if the string matches this case style, false otherwise + */ boolean matches(@NonNull String name); /** @@ -44,12 +54,20 @@ public interface CaseStyle { String getName(); /** - * Returns a set of variant names for the case style (e.g., "PascalCase" and "UpperCamelCase"). - * Implementations should override this method to provide variant names if applicable. + * Returns a set of variant names for the case style (e.g., "PascalCase" and "UpperCamelCase"). By + * default, only the primary name is returned. * - * @return a set of variant names + * @return an immutable set of variant names */ default Set getVariantNames() { - return of(this.getName()); // By default, return only the primary name + return unmodifiableSet(of(getName())); } + + /** + * Determines equality based on the {@link #getName()} method. + * + * @param object the object to compare with + * @return true if the given object is a {@code CaseStyle} with the same name, false otherwise + */ + boolean equals(@Nullable Object object); } diff --git a/stylesniffer-api/src/test/java/.gitkeep b/stylesniffer-api/src/test/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/BaseCaseStyleTest.java b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/BaseCaseStyleTest.java new file mode 100644 index 0000000..cb95b55 --- /dev/null +++ b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/BaseCaseStyleTest.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class BaseCaseStyleTest { + + @Test + void equalsShouldReturnFalseGivenTwoDifferentCaseStylesAreProvided() { + + // GIVEN + final var firstCaseStyle = new CaseStyleHavingNoVariantNamesImpl(); + final var secondCaseStyle = new CaseStyleHavingTwoVariantNamesImpl(); + + // WHEN + final var actualResult = firstCaseStyle.equals(secondCaseStyle); + + // THEN + assertThat(actualResult).isFalse(); + } + + @Test + void equalsShouldReturnFalseGivenNullIsProvided() { + + // GIVEN + final var firstCaseStyle = new CaseStyleHavingNoVariantNamesImpl(); + + // WHEN + final var actualResult = firstCaseStyle.equals(null); + + // THEN + assertThat(actualResult).isFalse(); + } + + @Test + void equalsShouldReturnTrueGivenTheSameInstanceIsProvidedTwice() { + + // GIVEN + final var firstCaseStyle = new CaseStyleHavingNoVariantNamesImpl(); + + // WHEN + final var actualResult = firstCaseStyle.equals(firstCaseStyle); + + // THEN + assertThat(actualResult).isTrue(); + } + + @Test + void equalsShouldReturnTrueGivenTwoInstancesOfTheSameCaseStyleAreCompared() { + + // GIVEN + final var firstCaseStyle = new CaseStyleHavingNoVariantNamesImpl(); + final var secondCaseStyle = new CaseStyleHavingNoVariantNamesImpl(); + + // WHEN + final var actualResult = firstCaseStyle.equals(secondCaseStyle); + + // THEN + assertThat(actualResult).isTrue(); + } + + @Test + void hashCodeShouldReturnAUniqueValueBasedOnTheNameGivenACaseStyleIsProvided() { + // GIVEN + final var caseStyleInstance = new CaseStyleHavingNoVariantNamesImpl(); + + // WHEN + final var actualResult = caseStyleInstance.hashCode(); + + // THEN + assertThat(actualResult).isEqualTo(caseStyleInstance.getName().hashCode()); + } +} diff --git a/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingNoVariantNamesImpl.java b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingNoVariantNamesImpl.java new file mode 100644 index 0000000..1e663b0 --- /dev/null +++ b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingNoVariantNamesImpl.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.api; + +import lombok.NonNull; + +/** + * Dummy implementation of CaseStyle having no variant names + * + * @author Sebastien Vermeille + */ +public class CaseStyleHavingNoVariantNamesImpl extends BaseCaseStyle { + @Override + public boolean matches(@NonNull String name) { + return false; + } + + @Override + public String getName() { + return "CaseStyleHavingNoVariantName"; + } + + // intentionally do not override getVariantNames(); (used in a test) +} diff --git a/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingTwoVariantNamesImpl.java b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingTwoVariantNamesImpl.java new file mode 100644 index 0000000..f00d630 --- /dev/null +++ b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleHavingTwoVariantNamesImpl.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.api; + +import java.util.Set; +import lombok.NonNull; + +/** + * Dummy implementation of CaseStyle having some variant names + * + * @author Sebastien Vermeille + */ +public class CaseStyleHavingTwoVariantNamesImpl extends BaseCaseStyle { + @Override + public boolean matches(@NonNull String name) { + return false; + } + + @Override + public String getName() { + return "ManyVariantsNamesCaseStyle"; + } + + @Override + public Set getVariantNames() { + return Set.of(getName(), "AnotherVariant", "andAlsoThatOne"); + } +} diff --git a/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleTest.java b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleTest.java new file mode 100644 index 0000000..78d8ddc --- /dev/null +++ b/stylesniffer-api/src/test/java/dev/cookiecode/stylesniffer/api/CaseStyleTest.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class CaseStyleTest { + + @Test + void getVariantNamesShouldReturnByDefaultOnlyTheCaseStyleName() { + // GIVEN + final CaseStyle caseStyle = new CaseStyleHavingNoVariantNamesImpl(); + + // WHEN + final var actualVariantNames = caseStyle.getVariantNames(); + + // THEN + assertThat(actualVariantNames).hasSize(1).containsOnly(caseStyle.getName()); + } + + @Test + void getVariantNamesShouldReturnTheCompleteListOfVariantNamesWhenOverride() { + // GIVEN + final CaseStyle caseStyle = new CaseStyleHavingTwoVariantNamesImpl(); + + // WHEN + final var actualVariantNames = caseStyle.getVariantNames(); + + // THEN + assertThat(actualVariantNames).hasSizeGreaterThan(1).isNotSameAs(Set.of(caseStyle.getName())); + } +} diff --git a/stylesniffer-impl/pom.xml b/stylesniffer-impl/pom.xml index cd3e98d..bdd6e83 100644 --- a/stylesniffer-impl/pom.xml +++ b/stylesniffer-impl/pom.xml @@ -38,48 +38,74 @@ - ${java.sdk.version} - ${java.sdk.version} - ${source.encoding} ${basedir}/.. - ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} + org.projectlombok lombok + + jakarta.annotation + jakarta.annotation-api + dev.cookiecode stylesniffer-annotation-processor + 1.0.0-SNAPSHOT provided - ${project.version} stylesniffer-api dev.cookiecode - compile 1.0.0-SNAPSHOT + compile - + dev.cookiecode stylesniffer-testkit - test 1.0.0-SNAPSHOT + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test - + org.apache.maven.plugins maven-compiler-plugin + default-compile + compile + + compile + @@ -89,31 +115,8 @@ ${java.sdk.version} ${java.sdk.version} - - compile - - default-compile - compile - - - org.apache.maven.plugins - 3.13.0 - - - jacoco-maven-plugin - - - - true - - - report-aggregate - - report-aggregate - verify - org.jacoco diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java deleted file mode 100644 index 54ea600..0000000 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSniffer.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * The MIT License - * Copyright © 2024 Sebastien Vermeille - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package dev.cookiecode.stylesniffer; - -import static java.lang.reflect.Modifier.isAbstract; -import static java.util.stream.Collectors.toSet; - -import dev.cookiecode.stylesniffer.api.CaseStyle; -import dev.cookiecode.stylesniffer.api.exception.StyleSnifferException; -import dev.cookiecode.stylesniffer.generated.CaseStyleInjector; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import javax.annotation.Nullable; - -/** - * The {@code StyleSniffer} class is responsible for managing and detecting various {@link - * CaseStyle} implementations. - * - *

It registers all case style classes that are annotated with {@link - * dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle}, instantiates them, and stores them in - * a list. It also provides methods to retrieve case styles based on names and to list all supported - * case styles. - * - * @author Sebastien Vermeille - */ -public class StyleSniffer { - - private final List caseStyles = new ArrayList<>(); - - /** - * Constructs a {@code StyleSniffer} instance and initializes case style recognizers. - * - *

This constructor attempts to register all case style classes that are annotated with - * {@code @RegisterCaseStyle}. - * - * @throws RuntimeException if there is an error during initialization - */ - StyleSniffer() { - try { - this.registerAllAnnotatedCaseStyles(); - } catch (final Exception e) { - throw new StyleSnifferException("Failed to initialize CaseStyleRecognizer", e); - } - } - - /** - * Registers all case style classes that are annotated with {@code @RegisterCaseStyle}. - * - *

This method retrieves annotated case style classes using the {@link CaseStyleInjector} and - * instantiates them, adding them to the internal list of case styles. Any instantiation failures - * are wrapped in a {@code RuntimeException}. - * - * @throws RuntimeException if there is an error instantiating case style classes - */ - private void registerAllAnnotatedCaseStyles() { - - final var annotatedClasses = new CaseStyleInjector().getAnnotatedCaseStyles(); - - for (final var clazz : annotatedClasses) { - if (CaseStyle.class.isAssignableFrom(clazz) && !isAbstract(clazz.getModifiers())) { - try { - this.caseStyles.add(clazz.getDeclaredConstructor().newInstance()); - } catch (final Exception e) { - throw new StyleSnifferException( - String.format("Failed to instantiate case style: %s", clazz.getName()), e); - } - } - } - } - - /** - * Retrieves a {@link CaseStyle} that matches the given name. - * - *

This method searches through the registered case styles and returns the first style that - * matches the provided name. - * - * @param name the name to match against case styles - * @return an {@link Optional} containing the matching {@code CaseStyle} if found, or an empty - * {@code Optional} if no match is found - */ - public Optional getCaseStyle(@Nullable final String name) { - if (name == null || name.isEmpty()) { - return Optional.empty(); - } - - return this.caseStyles.stream().filter(style -> style.matches(name)).findFirst(); - } - - /** - * Returns a set of names for all supported case styles. - * - *

This method provides a set of unique names for the case styles currently recognized by this - * {@code StyleSniffer}. - * - * @return a {@link Set} of names for supported case styles - */ - public Set getSupportedCaseStyles() { - return this.caseStyles.stream().map(CaseStyle::getName).collect(toSet()); - } - - /** - * Returns a set of names for all supported case styles, including all variants. - * - *

This method provides a comprehensive list of all case style names recognized by this {@code - * StyleSniffer}, including both primary names and their variants. - * - * @return a {@link Set} of names for all supported case styles, including variants - */ - public Set getSupportedCaseStylesIncludingVariants() { - return this.caseStyles.stream() - .flatMap(style -> style.getVariantNames().stream()) - .collect(toSet()); - } -} diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferFactory.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferFactory.java index 8d44266..99cd344 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferFactory.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferFactory.java @@ -22,15 +22,15 @@ */ package dev.cookiecode.stylesniffer; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import dev.cookiecode.stylesniffer.generated.CaseStyleInjector; +import lombok.experimental.UtilityClass; /** * Utility class for creating instances of {@link StyleSniffer}. * * @author Sebastien Vermeille */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@UtilityClass public final class StyleSnifferFactory { /** @@ -39,6 +39,7 @@ public final class StyleSnifferFactory { * @return a new {@link StyleSniffer} instance */ public static StyleSniffer createStyleSniffer() { - return new StyleSniffer(); + final var caseStyleClasses = new CaseStyleInjector().getAnnotatedCaseStyles(); + return new StyleSnifferImpl(caseStyleClasses); } } diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferImpl.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferImpl.java new file mode 100644 index 0000000..6c61b06 --- /dev/null +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/StyleSnifferImpl.java @@ -0,0 +1,160 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import static java.lang.reflect.Modifier.isAbstract; +import static java.util.Collections.unmodifiableList; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toSet; + +import com.google.common.annotations.VisibleForTesting; +import dev.cookiecode.stylesniffer.api.CaseStyle; +import dev.cookiecode.stylesniffer.api.exception.StyleSnifferException; +import jakarta.annotation.Nullable; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +/** + * The {@code StyleSniffer} class is responsible for managing and detecting various {@link + * CaseStyle} implementations. + * + *

It registers all case style classes that are annotated with {@link + * dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle}, instantiates them, and stores them in + * a list. It also provides methods to retrieve case styles based on names and to list all supported + * case styles. + * + * @author Sebastien Vermeille + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StyleSnifferImpl implements StyleSniffer { + + private List caseStyles = new ArrayList<>(); + + StyleSnifferImpl(@NonNull List> caseStyleClasses) { + registerCaseStyleClasses(caseStyleClasses); + } + + @VisibleForTesting + void registerCaseStyleClass(@NonNull Class caseStyleClass) { + registerCaseStyleClasses(Collections.singletonList(caseStyleClass)); + } + + @VisibleForTesting + void registerCaseStyleClasses(@NonNull List> caseStyleClasses) { + List newCaseStyles = new ArrayList<>(caseStyles); + newCaseStyles.addAll( + caseStyleClasses.stream() + .filter(this::isValidCaseStyleClass) + .map(this::instantiateCaseStyle) + .toList()); + + caseStyles = + unmodifiableList( + newCaseStyles); // ensure only this method can modify caseStyles items (no add nor + // delete) + } + + /** + * Checks if the given class is a valid {@link CaseStyle} implementation. + * + * @param clazz the class to check + * @return {@code true} if the class is a valid {@code CaseStyle} implementation; {@code false} + * otherwise + */ + @VisibleForTesting + boolean isValidCaseStyleClass(Class clazz) { + return CaseStyle.class.isAssignableFrom(clazz) && !isAbstract(clazz.getModifiers()); + } + + /** + * Instantiates a {@link CaseStyle} class. + * + * @param clazz the class to instantiate + * @return the instantiated {@code CaseStyle} + * @throws StyleSnifferException if instantiation fails + */ + @VisibleForTesting + CaseStyle instantiateCaseStyle(@NonNull Class clazz) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException e) { + throw new StyleSnifferException( + String.format("Cannot instantiate case style class %s. Is it abstract?", clazz.getName()), + e); + } catch (IllegalAccessException | NoSuchMethodException e) { + throw new StyleSnifferException( + String.format( + "Access violation while instantiating case style class %s. Does it has a public no args constructor?", + clazz.getName()), + e); + } catch (InvocationTargetException e) { + throw new StyleSnifferException( + "Exception thrown by constructor for case style class: " + clazz.getName(), e); + } + } + + @Override + public Optional getCaseStyle(@Nullable final String name) { + return sanitizeInput(name) + .flatMap( + sanitizedName -> + caseStyles.stream().filter(style -> style.matches(sanitizedName)).findFirst()); + } + + @Override + public Optional getCaseStyleWithVariantOrName(@Nullable final String variantOrName) { + return sanitizeInput(variantOrName) + .flatMap( + sanitizedName -> + caseStyles.stream() + .filter( + style -> + style.getName().equals(sanitizedName) + || style.getVariantNames().contains(sanitizedName)) + .findFirst()); + } + + /** + * Performs basic sanity checks on the input, such as trimming whitespace. + * + * @param name the input name to sanitize + * @return an {@link Optional} containing the sanitized name, or an empty {@link Optional} if the + * input is invalid + */ + private Optional sanitizeInput(@Nullable final String name) { + return ofNullable(name).map(String::trim).filter(sanitizedName -> !sanitizedName.isEmpty()); + } + + @Override + public Set getSupportedCaseStyles() { + return caseStyles.stream().map(CaseStyle::getName).collect(toSet()); + } + + @Override + public Set getSupportedCaseStylesIncludingVariants() { + return caseStyles.stream().flatMap(style -> style.getVariantNames().stream()).collect(toSet()); + } +} diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyle.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyle.java similarity index 94% rename from stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyle.java rename to stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyle.java index eac8547..c5a99cd 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyle.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyle.java @@ -20,12 +20,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.lang.Character.isLowerCase; import static java.util.Set.of; import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; import dev.cookiecode.stylesniffer.api.CaseStyle; import java.util.Set; import lombok.NonNull; @@ -38,7 +39,7 @@ * @author Sebastien Vermeille */ @RegisterCaseStyle -public class KebabCaseStyle implements CaseStyle { +public class KebabCaseStyle extends BaseCaseStyle { private static final char SEPARATOR = '-'; diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyle.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyle.java similarity index 95% rename from stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyle.java rename to stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyle.java index ad8d722..de71f40 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyle.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyle.java @@ -20,13 +20,14 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.lang.Character.isLowerCase; import static java.lang.Character.isUpperCase; import static java.util.Set.of; import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; import dev.cookiecode.stylesniffer.api.CaseStyle; import java.util.Set; import lombok.NonNull; @@ -40,7 +41,7 @@ * @author Sebastien Vermeille */ @RegisterCaseStyle -public class LowerCamelCaseStyle implements CaseStyle { +public class LowerCamelCaseStyle extends BaseCaseStyle { /** * Checks if the given name matches the CamelCase style. diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyle.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyle.java similarity index 72% rename from stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyle.java rename to stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyle.java index 92b9800..566910d 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyle.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyle.java @@ -20,12 +20,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; -import static java.lang.Character.isLowerCase; import static java.lang.Character.isUpperCase; import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; import dev.cookiecode.stylesniffer.api.CaseStyle; import java.util.Set; import lombok.NonNull; @@ -39,7 +39,7 @@ * @author Sebastien Vermeille */ @RegisterCaseStyle -public class PascalCaseStyle implements CaseStyle { +public class PascalCaseStyle extends BaseCaseStyle { /** * Checks if the given name matches the PascalCase style. @@ -62,6 +62,10 @@ public Set getVariantNames() { return Set.of(this.getName(), "UpperCamelCase", "CamelCase"); } + private Set invalidCharacters() { + return Set.of('_', ' ', '-', '*', ',', '\"', '\'', '#', '$', '@'); + } + /** * Determines if the given name is in PascalCase style. * @@ -74,26 +78,34 @@ public Set getVariantNames() { * @return {@code true} if the name is in PascalCase, {@code false} otherwise */ private boolean isPascalCase(@NonNull final String name) { - if (isUpperCase(name.charAt(0)) && !name.contains("_") && !name.contains(" ")) { - boolean seenLowercase = false; + return !containsInvalidCharacters(name) + && startsWithUpperCase(name) + && followsPascalCasePattern(name); + } + + private boolean startsWithUpperCase(@NonNull final String name) { + return isUpperCase(name.charAt(0)); + } + + private boolean followsPascalCasePattern(@NonNull final String name) { + boolean previousIsUpperCase = false; - for (int i = 1; i < name.length(); i++) { - final char currentChar = name.charAt(i); - final char previousChar = name.charAt(i - 1); + for (int i = 1; i < name.length(); i++) { + char currentChar = name.charAt(i); - if (isLowerCase(currentChar)) { - seenLowercase = true; - } else if (isUpperCase(currentChar)) { - if (seenLowercase && isUpperCase(previousChar)) { - continue; - } - if (!seenLowercase || isLowerCase(previousChar)) { - continue; - } + if (isUpperCase(currentChar)) { + if (previousIsUpperCase) { + continue; } + previousIsUpperCase = true; + } else { + previousIsUpperCase = false; } - return true; } - return false; + return true; + } + + private boolean containsInvalidCharacters(@NonNull String name) { + return name.chars().anyMatch(c -> invalidCharacters().contains((char) c)); } } diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyle.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyle.java similarity index 95% rename from stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyle.java rename to stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyle.java index 87ebfe1..44f84cd 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyle.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyle.java @@ -20,9 +20,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; import dev.cookiecode.stylesniffer.api.CaseStyle; import java.util.Set; import lombok.NonNull; @@ -36,7 +37,7 @@ * @author Sebastien Vermeille */ @RegisterCaseStyle -public class ScreamingSnakeCaseStyle implements CaseStyle { +public class ScreamingSnakeCaseStyle extends BaseCaseStyle { private static final char UNDERSCORE = '_'; diff --git a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyle.java b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyle.java similarity index 94% rename from stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyle.java rename to stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyle.java index 1fae7cc..84ca20f 100644 --- a/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyle.java +++ b/stylesniffer-impl/src/main/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyle.java @@ -20,11 +20,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.lang.Character.isLowerCase; import dev.cookiecode.stylesniffer.annotation.RegisterCaseStyle; +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; import dev.cookiecode.stylesniffer.api.CaseStyle; import java.util.Set; import lombok.NonNull; @@ -37,7 +38,7 @@ * @author Sebastien Vermeille */ @RegisterCaseStyle -public class SnakeCaseStyle implements CaseStyle { +public class SnakeCaseStyle extends BaseCaseStyle { private static final char UNDERSCORE = '_'; diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/AbstractCaseStyle.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/AbstractCaseStyle.java new file mode 100644 index 0000000..e2a7e78 --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/AbstractCaseStyle.java @@ -0,0 +1,32 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import dev.cookiecode.stylesniffer.api.CaseStyle; + +/** + * Test class used by {@link StyleSnifferImplTest} + * + * @author Sebastien Vermeille + */ +public abstract class AbstractCaseStyle implements CaseStyle {} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithRuntimeException.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithRuntimeException.java new file mode 100644 index 0000000..0166316 --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithRuntimeException.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; +import lombok.NonNull; + +/** + * Test class used by {@link StyleSnifferImplTest} + * + * @author Sebastien Vermeille + */ +public class FaultyCaseStyleWithRuntimeException extends BaseCaseStyle { + + public FaultyCaseStyleWithRuntimeException() { + throw new RuntimeException("Runtime exception"); + } + + @Override + public boolean matches(@NonNull String name) { + return false; + } + + @Override + public String getName() { + return "DummyCaseStyle"; + } +} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicConstructor.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicConstructor.java new file mode 100644 index 0000000..9d0d88c --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicConstructor.java @@ -0,0 +1,46 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; +import lombok.NonNull; + +/** + * Test class used by {@link StyleSnifferImplTest} + * + * @author Sebastien Vermeille + */ +public class FaultyCaseStyleWithoutPublicConstructor extends BaseCaseStyle { + + private FaultyCaseStyleWithoutPublicConstructor() {} + + @Override + public boolean matches(@NonNull String name) { + return false; + } + + @Override + public String getName() { + return "DummyCaseStyle"; + } +} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicNoArgsConstructor.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicNoArgsConstructor.java new file mode 100644 index 0000000..0606ac2 --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/FaultyCaseStyleWithoutPublicNoArgsConstructor.java @@ -0,0 +1,46 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import dev.cookiecode.stylesniffer.api.BaseCaseStyle; +import lombok.NonNull; + +/** + * Test class used by {@link StyleSnifferImplTest} + * + * @author Sebastien Vermeille + */ +public class FaultyCaseStyleWithoutPublicNoArgsConstructor extends BaseCaseStyle { + + public FaultyCaseStyleWithoutPublicNoArgsConstructor(String someParam) {} + + @Override + public boolean matches(@NonNull String name) { + return false; + } + + @Override + public String getName() { + return "DummyCaseStyle"; + } +} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferFactoryTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferFactoryTest.java new file mode 100644 index 0000000..b9aaf14 --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferFactoryTest.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Constructor; +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class StyleSnifferFactoryTest { + + @Test + void createStyleSnifferShouldNotGenerateAnyExceptions() { + // THEN + assertDoesNotThrow( + () -> { + // WHEN + final var instance = StyleSnifferFactory.createStyleSniffer(); + instance.getCaseStyle("PascalCaseInput"); + }); + } + + @Test + void instantiateStyleSnifferShouldNotThrowExceptions() { + assertDoesNotThrow( + () -> { + // GIVEN + Constructor constructor = + StyleSnifferFactory.class.getDeclaredConstructor(); + constructor.setAccessible(true); + + // WHEN + constructor.newInstance(); + }); + } +} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferImplTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferImplTest.java new file mode 100644 index 0000000..d5ac044 --- /dev/null +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/StyleSnifferImplTest.java @@ -0,0 +1,322 @@ +/* + * The MIT License + * Copyright © 2024 Sebastien Vermeille + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.cookiecode.stylesniffer; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.cookiecode.stylesniffer.api.exception.StyleSnifferException; +import dev.cookiecode.stylesniffer.generated.CaseStyleInjector; +import dev.cookiecode.stylesniffer.impl.casestyle.PascalCaseStyle; +import java.util.ArrayList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class + * + * @author Sebastien Vermeille + */ +class StyleSnifferImplTest { + + private StyleSnifferImpl styleSniffer; + + @BeforeEach + void setUp() { + styleSniffer = new StyleSnifferImpl(emptyList()); + } + + @Test + void isValidCaseStyleClassShouldReturnTrueGivenTheClassImplementsCaseStyleAndIsNotAbstract() { + // GIVEN + final var clazz = PascalCaseStyle.class; + + // WHEN + final var actualResult = styleSniffer.isValidCaseStyleClass(clazz); + + // THEN + assertThat(actualResult).isTrue(); + } + + @Test + void isValidCaseStyleClassShouldReturnFalseGivenTheClassImplementsCaseStyleButIsAbstract() { + // GIVEN + final var clazz = AbstractCaseStyle.class; + + // WHEN + final var actualResult = styleSniffer.isValidCaseStyleClass(clazz); + + // THEN + assertThat(actualResult).isFalse(); + } + + @Test + void isValidCaseStyleClassShouldReturnFalseGivenTheClassDoNotImplementsCaseStyle() { + // GIVEN + final var clazz = ArrayList.class; + + // WHEN + final var actualResult = styleSniffer.isValidCaseStyleClass(clazz); + + // THEN + assertThat(actualResult).isFalse(); + } + + @Test + void instantiateCaseStyleShouldThrowStyleSnifferExceptionInCaseOfAccessViolation() { + + var thrown = + assertThrows( + StyleSnifferException.class, + () -> { + // GIVEN + final var clazz = FaultyCaseStyleWithoutPublicConstructor.class; + + // WHEN + styleSniffer.instantiateCaseStyle(clazz); + }); + assertThat(thrown.getMessage()).contains("Does it has a public no args constructor?"); + } + + @Test + void instantiateCaseStyleShouldThrowStyleSnifferExceptionInCaseOfNoEmptyConstructor() { + + var thrown = + assertThrows( + StyleSnifferException.class, + () -> { + // GIVEN + final var clazz = FaultyCaseStyleWithoutPublicNoArgsConstructor.class; + + // WHEN + styleSniffer.instantiateCaseStyle(clazz); + }); + assertThat(thrown.getMessage()).contains("Does it has a public no args constructor?"); + } + + @Test + void instantiateCaseStyleShouldThrowStyleSnifferExceptionInCaseOfAbstractClass() { + + var thrown = + assertThrows( + StyleSnifferException.class, + () -> { + // GIVEN + final var clazz = AbstractCaseStyle.class; + + // WHEN + styleSniffer.instantiateCaseStyle(clazz); + }); + assertThat(thrown.getMessage()).contains("Is it abstract?"); + } + + @Test + void + instantiateCaseStyleShouldThrowStyleSnifferExceptionInCaseTheConstructorGeneratesAnException() { + + assertThrows( + StyleSnifferException.class, + () -> { + // GIVEN + final var clazz = FaultyCaseStyleWithRuntimeException.class; + + // WHEN + styleSniffer.instantiateCaseStyle(clazz); + }); + } + + @Test + void instantiateCaseStyleShouldReturnANewInstanceInCaseOfSuccess() { + + // GIVEN + final var clazz = PascalCaseStyle.class; + + // WHEN + final var actualResult = styleSniffer.instantiateCaseStyle(clazz); + + // THEN + assertThat(actualResult).isInstanceOf(PascalCaseStyle.class); + } + + @Test + void getCaseStyleShouldReturnAnEmptyResultGivenNullInput() { + // GIVEN + final String nullInput = null; + + // WHEN + final var result = styleSniffer.getCaseStyle(nullInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void getCaseStyleShouldReturnAnEmptyResultGivenEmptyInput() { + // GIVEN + final var emptyInput = ""; + + // WHEN + final var result = styleSniffer.getCaseStyle(emptyInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void getCaseStyleShouldReturnAnEmptyResultGivenSpaceInput() { + // GIVEN + final var spaceInput = " "; + + // WHEN + final var result = styleSniffer.getCaseStyle(spaceInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void getCaseStyleShouldReturnAPascalCaseCaseStyleGivenInputIsWrittenInPascalCase() { + // GIVEN + final var pascalCaseInput = "SomePascalCase"; + + final var pascalCaseImplClass = PascalCaseStyle.class; + styleSniffer.registerCaseStyleClass(pascalCaseImplClass); + + // WHEN + final var result = styleSniffer.getCaseStyle(pascalCaseInput); + + // THEN + assertThat(result).isPresent().get().isInstanceOf(pascalCaseImplClass); + } + + @Test + void ggetCaseStyleWithVariantOrNameShouldReturnAnEmptyResultGivenNullInput() { + // GIVEN + final String nullInput = null; + + // WHEN + final var result = styleSniffer.getCaseStyleWithVariantOrName(nullInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void getCaseStyleWithVariantOrNameShouldReturnAnEmptyResultGivenEmptyInput() { + // GIVEN + final var emptyInput = ""; + + // WHEN + final var result = styleSniffer.getCaseStyleWithVariantOrName(emptyInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void getCaseStyleWithVariantOrNameShouldReturnAnEmptyResultGivenSpaceInput() { + // GIVEN + final var spaceInput = " "; + + // WHEN + final var result = styleSniffer.getCaseStyleWithVariantOrName(spaceInput); + + // THEN + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void + getCaseStyleWithVariantOrNameShouldReturnPascalCaseCaseStyleGivenInputIsUpperCamelCaseVariant() { + // GIVEN + final String variantName = "UpperCamelCase"; + styleSniffer = new StyleSnifferImpl(new CaseStyleInjector().getAnnotatedCaseStyles()); + + // WHEN + final var result = styleSniffer.getCaseStyleWithVariantOrName(variantName); + + // THEN + assertThat(result).isNotNull().isPresent().get().isInstanceOf(PascalCaseStyle.class); + } + + @Test + void getCaseStyleWithVariantOrNameShouldReturnPascalCaseCaseStyleGivenInputIsUpperCamelCase() { + // GIVEN + final String name = "PascalCase"; + styleSniffer = new StyleSnifferImpl(new CaseStyleInjector().getAnnotatedCaseStyles()); + + // WHEN + final var result = styleSniffer.getCaseStyleWithVariantOrName(name); + + // THEN + assertThat(result).isNotNull().isPresent().get().isInstanceOf(PascalCaseStyle.class); + } + + @Test + void + getSupportedCaseStylesShouldReturnANonEmptyListOfSupportedCaseStylesGivenSomeImplementationExists() { + // GIVEN + // case embedded styles implementations + styleSniffer.registerCaseStyleClasses(new CaseStyleInjector().getAnnotatedCaseStyles()); + + // WHEN + final var actuallySupportedCaseStyle = styleSniffer.getSupportedCaseStyles(); + + // THEN + assertThat(actuallySupportedCaseStyle).isNotEmpty(); + } + + @Test + void getSupportedCaseStylesShouldContainsPascalCaseGivenPascalCaseImplementationExist() { + // GIVEN + // case embedded styles implementations + styleSniffer.registerCaseStyleClasses(new CaseStyleInjector().getAnnotatedCaseStyles()); + + final var pascalCaseStyleName = new PascalCaseStyle().getName(); + + // WHEN + final var actuallySupportedCaseStyle = styleSniffer.getSupportedCaseStyles(); + + // THEN + assertThat(actuallySupportedCaseStyle).contains(pascalCaseStyleName); + } + + @Test + void + getSupportedCaseStylesIncludingVariantsShouldContainsPascalCaseAndAllItsVariantsGivenPascalCaseImplementationExist() { + // GIVEN + // case embedded styles implementations + styleSniffer.registerCaseStyleClasses(new CaseStyleInjector().getAnnotatedCaseStyles()); + + final var pascalCaseStyle = new PascalCaseStyle(); + + // WHEN + final var actuallySupportedCaseStyle = styleSniffer.getSupportedCaseStylesIncludingVariants(); + + // THEN + assertThat(actuallySupportedCaseStyle) + .containsAll(pascalCaseStyle.getVariantNames()) + .contains(pascalCaseStyle.getName()); + } +} diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyleTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyleTest.java similarity index 90% rename from stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyleTest.java rename to stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyleTest.java index f5bac9e..b6631e5 100644 --- a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/KebabCaseStyleTest.java +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/KebabCaseStyleTest.java @@ -20,11 +20,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.util.List.of; -import dev.cookiecode.stylesniffer.testkit.BaseCaseStyleTest; +import dev.cookiecode.stylesniffer.testkit.CaseStyleTestKit; import java.util.List; /** @@ -34,7 +34,7 @@ */ @SuppressWarnings( "java:S2187") // sonar is not able to detect that BaseCaseStyleTest interface generates test -class KebabCaseStyleTest implements BaseCaseStyleTest { +class KebabCaseStyleTest implements CaseStyleTestKit { @Override public KebabCaseStyle createCaseStyle() { diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyleTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyleTest.java similarity index 90% rename from stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyleTest.java rename to stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyleTest.java index 7c7c444..20a9305 100644 --- a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/LowerCamelCaseStyleTest.java +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/LowerCamelCaseStyleTest.java @@ -20,11 +20,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.util.List.of; -import dev.cookiecode.stylesniffer.testkit.BaseCaseStyleTest; +import dev.cookiecode.stylesniffer.testkit.CaseStyleTestKit; import java.util.List; /** @@ -34,7 +34,7 @@ */ @SuppressWarnings( "java:S2187") // sonar is not able to detect that BaseCaseStyleTest interface generates test -class LowerCamelCaseStyleTest implements BaseCaseStyleTest { +class LowerCamelCaseStyleTest implements CaseStyleTestKit { @Override public LowerCamelCaseStyle createCaseStyle() { diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyleTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyleTest.java similarity index 76% rename from stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyleTest.java rename to stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyleTest.java index 45e17e7..202805e 100644 --- a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/PascalCaseStyleTest.java +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/PascalCaseStyleTest.java @@ -20,11 +20,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.util.List.of; -import dev.cookiecode.stylesniffer.testkit.BaseCaseStyleTest; +import dev.cookiecode.stylesniffer.testkit.CaseStyleTestKit; import java.util.List; /** @@ -34,7 +34,7 @@ */ @SuppressWarnings( "java:S2187") // sonar is not able to detect that BaseCaseStyleTest interface generates test -class PascalCaseStyleTest implements BaseCaseStyleTest { +class PascalCaseStyleTest implements CaseStyleTestKit { @Override public PascalCaseStyle createCaseStyle() { @@ -43,11 +43,19 @@ public PascalCaseStyle createCaseStyle() { @Override public List nonMatchingInputs() { - return of("some_snake_case", "someCamelCase", "some-kebab-case"); + return of( + "some_snake_case", + "someCamelCase", + "some-kebab-case", + "S ome space", + "InvalidPascalCase_", + "Invalid-PascalCase", + "123Pascal"); } @Override public List matchingInputs() { - return of("SomePascalCase", "Pascal", "PascalIsAName"); + return of( + "SomePascalCase", "Pascal", "PascalIsAName", "PascalCaseMQTT", "PascalCaseUsing123Digits"); } } diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyleTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyleTest.java similarity index 90% rename from stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyleTest.java rename to stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyleTest.java index 1a2953a..49e4d68 100644 --- a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/ScreamingSnakeCaseStyleTest.java +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/ScreamingSnakeCaseStyleTest.java @@ -20,11 +20,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; import static java.util.List.of; -import dev.cookiecode.stylesniffer.testkit.BaseCaseStyleTest; +import dev.cookiecode.stylesniffer.testkit.CaseStyleTestKit; import java.util.List; /** @@ -34,7 +34,7 @@ */ @SuppressWarnings( "java:S2187") // sonar is not able to detect that BaseCaseStyleTest interface generates test -class ScreamingSnakeCaseStyleTest implements BaseCaseStyleTest { +class ScreamingSnakeCaseStyleTest implements CaseStyleTestKit { @Override public ScreamingSnakeCaseStyle createCaseStyle() { diff --git a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyleTest.java b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyleTest.java similarity index 90% rename from stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyleTest.java rename to stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyleTest.java index cc34351..d1a9411 100644 --- a/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/SnakeCaseStyleTest.java +++ b/stylesniffer-impl/src/test/java/dev/cookiecode/stylesniffer/impl/casestyle/SnakeCaseStyleTest.java @@ -20,9 +20,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package dev.cookiecode.stylesniffer.impl; +package dev.cookiecode.stylesniffer.impl.casestyle; -import dev.cookiecode.stylesniffer.testkit.BaseCaseStyleTest; +import dev.cookiecode.stylesniffer.testkit.CaseStyleTestKit; import java.util.List; /** @@ -32,7 +32,7 @@ */ @SuppressWarnings( "java:S2187") // sonar is not able to detect that BaseCaseStyleTest interface generates test -class SnakeCaseStyleTest implements BaseCaseStyleTest { +class SnakeCaseStyleTest implements CaseStyleTestKit { @Override public SnakeCaseStyle createCaseStyle() { diff --git a/stylesniffer-report-aggregate/pom.xml b/stylesniffer-report-aggregate/pom.xml new file mode 100644 index 0000000..2d33cf8 --- /dev/null +++ b/stylesniffer-report-aggregate/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + stylesniffer-report-aggregate + 1.0.0-SNAPSHOT + pom + Aggregate Jacoco Coverage Report + + + dev.cookiecode + stylesniffer-parent + 1.0.0-SNAPSHOT + + + + ${basedir}/.. + + + + + + dev.cookiecode + stylesniffer-annotation-processor + 1.0.0-SNAPSHOT + + + dev.cookiecode + stylesniffer-api + 1.0.0-SNAPSHOT + + + dev.cookiecode + stylesniffer-impl + 1.0.0-SNAPSHOT + + + dev.cookiecode + stylesniffer-testkit + 1.0.0-SNAPSHOT + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + diff --git a/stylesniffer-testkit/pom.xml b/stylesniffer-testkit/pom.xml index 64a467a..cbd294a 100644 --- a/stylesniffer-testkit/pom.xml +++ b/stylesniffer-testkit/pom.xml @@ -30,6 +30,7 @@ 4.0.0 stylesniffer-testkit + 1.0.0-SNAPSHOT jar @@ -39,11 +40,7 @@ - ${java.sdk.version} - ${java.sdk.version} - ${source.encoding} ${basedir}/.. - ${project.sonar.root.projectKey}-${project.groupId}-${project.artifactId} @@ -53,6 +50,10 @@ stylesniffer-api 1.0.0-SNAPSHOT + + org.projectlombok + lombok + org.assertj assertj-core diff --git a/stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/BaseCaseStyleTest.java b/stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKit.java similarity index 97% rename from stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/BaseCaseStyleTest.java rename to stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKit.java index c87e2d8..efe3f04 100644 --- a/stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/BaseCaseStyleTest.java +++ b/stylesniffer-testkit/src/main/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKit.java @@ -71,7 +71,7 @@ *

Usage Example

* *
{@code
- * public class CamelCaseStyleTest implements BaseCaseStyleTest {
+ * public class CamelCaseStyleTest implements CaseStyleTestKit {
  *
  *     @Override
  *     public CamelCaseStyle createCaseStyle() {
@@ -99,7 +99,7 @@
  */
 @TestInstance(PER_CLASS)
 @SuppressWarnings("unused") // Used by implementers in other Maven modules
-public interface BaseCaseStyleTest {
+public interface CaseStyleTestKit {
 
   T createCaseStyle();
 
diff --git a/stylesniffer-testkit/src/test/java/.gitkeep b/stylesniffer-testkit/src/test/java/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKitTest.java b/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKitTest.java
new file mode 100644
index 0000000..edb0a3c
--- /dev/null
+++ b/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/CaseStyleTestKitTest.java
@@ -0,0 +1,48 @@
+/*
+ * The MIT License
+ * Copyright © 2024 Sebastien Vermeille
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package dev.cookiecode.stylesniffer.testkit;
+
+import java.util.List;
+
+/**
+ * Test class
+ *
+ * @author Sebastien Vermeille
+ */
+public class CaseStyleTestKitTest implements CaseStyleTestKit {
+
+  @Override
+  public DummyCaseStyle createCaseStyle() {
+    return new DummyCaseStyle();
+  }
+
+  @Override
+  public List matchingInputs() {
+    return List.of("dummy42", "dummy", "dummySomething", "dummy-some-thing");
+  }
+
+  @Override
+  public List nonMatchingInputs() {
+    return List.of("something", "not-duMmy");
+  }
+}
diff --git a/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/DummyCaseStyle.java b/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/DummyCaseStyle.java
new file mode 100644
index 0000000..bf7859b
--- /dev/null
+++ b/stylesniffer-testkit/src/test/java/dev/cookiecode/stylesniffer/testkit/DummyCaseStyle.java
@@ -0,0 +1,43 @@
+/*
+ * The MIT License
+ * Copyright © 2024 Sebastien Vermeille
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package dev.cookiecode.stylesniffer.testkit;
+
+import dev.cookiecode.stylesniffer.api.BaseCaseStyle;
+import lombok.NonNull;
+
+/**
+ * Test class A dummy implementation of CaseStyle used for testing purpose
+ *
+ * @author Sebastien Vermeille
+ */
+public class DummyCaseStyle extends BaseCaseStyle {
+  @Override
+  public boolean matches(@NonNull String name) {
+    return name.contains("dummy");
+  }
+
+  @Override
+  public String getName() {
+    return "dummyStyle";
+  }
+}