diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index da6aadf..3d19e49 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -157,26 +157,30 @@ The default mode is `ONLY_TESTS`, which checks only test classes. The default mode is `WITHOUT_TESTS`, which excludes test classes from the import check. -| Category | Method Name | Rule Description | -|----------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| General | `noAutowiredFields` | Fields should not be annotated with `@Autowired`, prefer constructor injection | -| Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | -| Configurations | `namesShouldEndWithConfiguration` | Configuration annotated with `@Configuration` classes should end with `Configuration` | -| Configurations | `namesShouldMatch` | Configuration annotated with `@Configuration` classes should match a regex pattern | -| Controllers | `namesShouldEndWithController` | Controllers annotated with `@Controller` or `@RestController` should end with `Controller` | -| Controllers | `namesShouldMatch` | Controllers annotated with `@Controller` or `@RestController` should match a regex pattern | -| Controllers | `shouldBeAnnotatedWithController` | Controllers should be annotated with `@Controller` | -| Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | -| Controllers | `shouldBePackagePrivate` | Controllers annotated with `@Controller` or `@RestController` should be package-private | -| Controllers | `shouldNotDependOnOtherControllers` | Controllers annotated with `@Controller` or `@RestController` should not depend on other controllers annotated with `@Controller` or `@RestController` | -| Repositories | `namesShouldEndWithRepository` | Repositories annotated with `@Repository` should end with `Repository` | -| Repositories | `namesShouldMatch` | Repositories annotated with `@Repository` should match a regex pattern | -| Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | -| Repositories | `shouldNotDependOnServices` | Repositories annotated with `@Repository` should not depend on service classes annotated with `@Service.` | -| Services | `namesShouldEndWithService` | Services annotated with `@Service.` should end with `Service` | -| Services | `namesShouldMatch` | Services annotated with `@Service.` should match a regex pattern | -| Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | -| Services | `shouldNotDependOnControllers` | Services annotated with `@Service.` should not depend on controllers annotated with `@Controller` or `@RestController` | +| Category | Method Name | Rule Description | +|----------------|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| General | `noAutowiredFields` | Fields should not be annotated with `@Autowired`, prefer constructor injection | +| Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | +| Properties | `namesShouldEndWithProperties` | Properties annotated with `@ConfigurationProperties` should end with `Properties` | +| Properties | `namesShouldMatch` | Properties annotated with `@ConfigurationProperties` should match a regex pattern | +| Properties | `shouldBeAnnotatedWithValidated` | Properties annotated with `@ConfigurationProperties` should be annotated with `@Validated` | +| Properties | `shouldBeAnnotatedWithConfigurationProperties` | Properties should be annotated with `@ConfigurationProperties` | +| Configurations | `namesShouldEndWithConfiguration` | Configuration annotated with `@Configuration` classes should end with `Configuration` | +| Configurations | `namesShouldMatch` | Configuration annotated with `@Configuration` classes should match a regex pattern | +| Controllers | `namesShouldEndWithController` | Controllers annotated with `@Controller` or `@RestController` should end with `Controller` | +| Controllers | `namesShouldMatch` | Controllers annotated with `@Controller` or `@RestController` should match a regex pattern | +| Controllers | `shouldBeAnnotatedWithController` | Controllers should be annotated with `@Controller` | +| Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | +| Controllers | `shouldBePackagePrivate` | Controllers annotated with `@Controller` or `@RestController` should be package-private | +| Controllers | `shouldNotDependOnOtherControllers` | Controllers annotated with `@Controller` or `@RestController` should not depend on other controllers annotated with `@Controller` or `@RestController` | +| Repositories | `namesShouldEndWithRepository` | Repositories annotated with `@Repository` should end with `Repository` | +| Repositories | `namesShouldMatch` | Repositories annotated with `@Repository` should match a regex pattern | +| Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | +| Repositories | `shouldNotDependOnServices` | Repositories annotated with `@Repository` should not depend on service classes annotated with `@Service.` | +| Services | `namesShouldEndWithService` | Services annotated with `@Service.` should end with `Service` | +| Services | `namesShouldMatch` | Services annotated with `@Service.` should match a regex pattern | +| Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | +| Services | `shouldNotDependOnControllers` | Services annotated with `@Service.` should not depend on controllers annotated with `@Controller` or `@RestController` | ## 5. Java Rules @@ -459,6 +463,21 @@ Taikai.builder() .check(); ``` +- **Properties Configuration**: Ensure that configuration property classes end with `Properties` or match a specific regex pattern, are annotated with `@ConfigurationProperties` or annotated with `@Validated`. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .spring(spring -> spring + .properties(properties -> properties + .shouldBeAnnotatedWithConfigurationProperties() + .namesShouldEndWithProperties() + .namesShouldMatch("regex") + .shouldBeAnnotatedWithValidated())) + .build() + .check(); +``` + - **Configurations Configuration**: Ensure that configuration classes end with `Configuration` or match a specific regex pattern. ```java diff --git a/src/main/java/com/enofex/taikai/spring/PropertiesConfigurer.java b/src/main/java/com/enofex/taikai/spring/PropertiesConfigurer.java new file mode 100644 index 0000000..f797859 --- /dev/null +++ b/src/main/java/com/enofex/taikai/spring/PropertiesConfigurer.java @@ -0,0 +1,78 @@ +package com.enofex.taikai.spring; + +import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_CONFIGURATION_PROPERTIES; +import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_VALIDATED; +import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithConfigurationProperties; +import static com.tngtech.archunit.lang.conditions.ArchConditions.be; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +import com.enofex.taikai.TaikaiRule; +import com.enofex.taikai.TaikaiRule.Configuration; +import com.enofex.taikai.configures.AbstractConfigurer; +import com.enofex.taikai.configures.ConfigurerContext; + +public final class PropertiesConfigurer extends AbstractConfigurer { + + private static final String DEFAULT_PROPERTIES_NAME_MATCHING = ".+Properties"; + + PropertiesConfigurer(ConfigurerContext configurerContext) { + super(configurerContext); + } + + public PropertiesConfigurer namesShouldEndWithProperties() { + return namesShouldMatch(DEFAULT_PROPERTIES_NAME_MATCHING, null); + } + + public PropertiesConfigurer namesShouldEndWithProperties(Configuration configuration) { + return namesShouldMatch(DEFAULT_PROPERTIES_NAME_MATCHING, configuration); + } + + public PropertiesConfigurer namesShouldMatch(String regex) { + return namesShouldMatch(regex, null); + } + + public PropertiesConfigurer namesShouldMatch(String regex, Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that(are(annotatedWithConfigurationProperties(true))) + .should().haveNameMatching(regex) + .as("Properties should have name ending %s".formatted(regex)), configuration)); + } + + public PropertiesConfigurer shouldBeAnnotatedWithValidated() { + return shouldBeAnnotatedWithValidated(null); + } + + public PropertiesConfigurer shouldBeAnnotatedWithValidated(Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that(are(annotatedWithConfigurationProperties(true))) + .should().beMetaAnnotatedWith(ANNOTATION_VALIDATED) + .as("Configuration properties annotated with %s should be annotated with %s as well".formatted( + ANNOTATION_VALIDATED)), + configuration)); + } + + public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties() { + return shouldBeAnnotatedWithConfigurationProperties(DEFAULT_PROPERTIES_NAME_MATCHING, null); + } + + public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties( + Configuration configuration) { + return shouldBeAnnotatedWithConfigurationProperties(DEFAULT_PROPERTIES_NAME_MATCHING, + configuration); + } + + public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties(String regex) { + return shouldBeAnnotatedWithConfigurationProperties(regex, null); + } + + public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties(String regex, + Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that().haveNameMatching(regex) + .should(be(annotatedWithConfigurationProperties(true))) + .as("Configuration properties should be annotated with %s".formatted( + ANNOTATION_CONFIGURATION_PROPERTIES)), + configuration)); + } +} diff --git a/src/main/java/com/enofex/taikai/spring/SpringConfigurer.java b/src/main/java/com/enofex/taikai/spring/SpringConfigurer.java index 121ca15..8a38044 100644 --- a/src/main/java/com/enofex/taikai/spring/SpringConfigurer.java +++ b/src/main/java/com/enofex/taikai/spring/SpringConfigurer.java @@ -1,7 +1,7 @@ package com.enofex.taikai.spring; import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_AUTOWIRED; -import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedAutowired; +import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithAutowired; import static com.tngtech.archunit.lang.conditions.ArchConditions.be; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; @@ -17,6 +17,10 @@ public SpringConfigurer(ConfigurerContext configurerContext) { super(configurerContext); } + public SpringConfigurer properties(Customizer customizer) { + return customizer(customizer, () -> new PropertiesConfigurer(configurerContext())); + } + public SpringConfigurer configurations(Customizer customizer) { return customizer(customizer, () -> new ConfigurationsConfigurer(configurerContext())); } @@ -43,13 +47,14 @@ public SpringConfigurer noAutowiredFields() { public SpringConfigurer noAutowiredFields(Configuration configuration) { return addRule(TaikaiRule.of(noFields() - .should(be(annotatedAutowired(true))) + .should(be(annotatedWithAutowired(true))) .as("No fields should be annotated with %s, use constructor injection".formatted( ANNOTATION_AUTOWIRED)), configuration)); } @Override public void disable() { + disable(PropertiesConfigurer.class); disable(ConfigurationsConfigurer.class); disable(ControllersConfigurer.class); disable(ServicesConfigurer.class); diff --git a/src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java b/src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java index dfbafb0..81dd917 100644 --- a/src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java +++ b/src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java @@ -8,12 +8,14 @@ final class SpringDescribedPredicates { static final String ANNOTATION_CONFIGURATION = "org.springframework.context.annotation.Configuration"; + static final String ANNOTATION_CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties"; static final String ANNOTATION_CONTROLLER = "org.springframework.web.bind.annotation.Controller"; static final String ANNOTATION_REST_CONTROLLER = "org.springframework.web.bind.annotation.RestController"; static final String ANNOTATION_SERVICE = "org.springframework.stereotype.Service"; static final String ANNOTATION_REPOSITORY = "org.springframework.stereotype.Repository"; static final String ANNOTATION_SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; static final String ANNOTATION_AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; + static final String ANNOTATION_VALIDATED = "org.springframework.validation.annotation.Validated"; private SpringDescribedPredicates() { } @@ -30,6 +32,11 @@ static DescribedPredicate annotatedWithConfiguration( return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated); } + static DescribedPredicate annotatedWithConfigurationProperties( + boolean isMetaAnnotated) { + return annotatedWith(ANNOTATION_CONFIGURATION_PROPERTIES, isMetaAnnotated); + } + static DescribedPredicate annotatedWithRestController(boolean isMetaAnnotated) { return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated); } @@ -51,7 +58,11 @@ static DescribedPredicate annotatedWithSpringBootApplication( return annotatedWith(ANNOTATION_SPRING_BOOT_APPLICATION, isMetaAnnotated); } - static DescribedPredicate annotatedAutowired(boolean isMetaAnnotated) { + static DescribedPredicate annotatedWithAutowired(boolean isMetaAnnotated) { return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated); } + + static DescribedPredicate annotatedWithValidated(boolean isMetaAnnotated) { + return annotatedWith(ANNOTATION_VALIDATED, isMetaAnnotated); + } } diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index 240dc5f..9472be0 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -53,6 +53,11 @@ public static void main(String[] args) { .noAutowiredFields() .boot(boot -> boot .springBootApplicationShouldBeIn("com.enofex.taikai")) + .properties(properties -> properties + .namesShouldEndWithProperties() + .namesShouldMatch("regex") + .shouldBeAnnotatedWithConfigurationProperties() + .shouldBeAnnotatedWithValidated()) .configurations(configuration -> configuration .namesShouldEndWithConfiguration() .namesShouldMatch("regex"))