diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index c5526fc..2356087 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -44,6 +44,19 @@ Taikai.builder() .check(); ``` +### 3.3 Excluding Classes Globally + +You can globally exclude specific classes from all rule checks by using the `excludeClass` or `excludeClasses` methods in the builder. This ensures that the specified classes are not checked by any rule. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .excludeClass("com.company.yourproject.SomeClassToExclude") + .excludeClasses(Set.of("com.company.yourproject.foo.ClassToExclude", "com.company.yourproject.bar.ClassToExclude")) + .build() + .check(); +``` + ## 4. Rules Overview Taikai's architecture rules cover a wide range of categories to enforce best practices and maintain consistency. @@ -455,13 +468,15 @@ Taikai.builder() ### Custom Configuration for Import Rules -For every rule, you have the flexibility to add a custom configuration. This allows you to specify the namespace and import options tailored to your needs. +For every rule, you have the flexibility to add a custom configuration. This allows you to specify the namespace, import options, and exclude specific classes from the checks. The `Configuration` class offers various static methods to create custom configurations: - `Configuration.of(String namespace)` to set a custom namespace. - `Configuration.of(Namespace.IMPORT namespaceImport)` to specify import options such as `WITHOUT_TESTS`, `WITH_TESTS`, or `ONLY_TESTS`. - `Configuration.of(String namespace, Namespace.IMPORT namespaceImport)` to set both namespace and import options. - `Configuration.of(JavaClasses javaClasses)` to directly provide a set of Java classes. +- `Configuration.of(Set excludedClasses)` to exclude specific classes from the checks. +- Additional overloaded methods to combine these options in various ways. If a `namespaceImport` is not explicitly provided, it defaults to `Namespace.IMPORT.WITHOUT_TESTS`. diff --git a/src/main/java/com/enofex/taikai/Namespace.java b/src/main/java/com/enofex/taikai/Namespace.java index 90616d5..11e6912 100644 --- a/src/main/java/com/enofex/taikai/Namespace.java +++ b/src/main/java/com/enofex/taikai/Namespace.java @@ -1,9 +1,10 @@ package com.enofex.taikai; +import static java.util.Objects.requireNonNull; + import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; -import java.util.Objects; public final class Namespace { @@ -17,8 +18,8 @@ private Namespace() { } public static JavaClasses from(String namespace, IMPORT importOption) { - Objects.requireNonNull(namespace); - Objects.requireNonNull(importOption); + requireNonNull(namespace); + requireNonNull(importOption); return switch (importOption) { case WITH_TESTS -> withTests(namespace); @@ -28,7 +29,7 @@ public static JavaClasses from(String namespace, IMPORT importOption) { } public static JavaClasses withoutTests(String namespace) { - Objects.requireNonNull(namespace); + requireNonNull(namespace); return new ClassFileImporter() .withImportOption(new ImportOption.DoNotIncludeTests()) @@ -37,7 +38,7 @@ public static JavaClasses withoutTests(String namespace) { } public static JavaClasses withTests(String namespace) { - Objects.requireNonNull(namespace); + requireNonNull(namespace); return new ClassFileImporter() .withImportOption(new ImportOption.DoNotIncludeJars()) @@ -45,7 +46,7 @@ public static JavaClasses withTests(String namespace) { } public static JavaClasses onlyTests(String namespace) { - Objects.requireNonNull(namespace); + requireNonNull(namespace); return new ClassFileImporter() .withImportOption(new ImportOption.OnlyIncludeTests()) diff --git a/src/main/java/com/enofex/taikai/Taikai.java b/src/main/java/com/enofex/taikai/Taikai.java index fcae95c..c61bf0c 100644 --- a/src/main/java/com/enofex/taikai/Taikai.java +++ b/src/main/java/com/enofex/taikai/Taikai.java @@ -1,5 +1,8 @@ package com.enofex.taikai; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; + import com.enofex.taikai.configures.Configurer; import com.enofex.taikai.configures.ConfigurerContext; import com.enofex.taikai.configures.Configurers; @@ -12,7 +15,9 @@ import com.tngtech.archunit.core.domain.JavaClasses; import java.util.ArrayList; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; @@ -20,11 +25,13 @@ public final class Taikai { private final boolean failOnEmpty; private final String namespace; + private final Set excludedClasses; private final Collection rules; private Taikai(Builder builder) { this.failOnEmpty = builder.failOnEmpty; this.namespace = builder.namespace; + this.excludedClasses = requireNonNullElse(builder.excludedClasses, Collections.emptySet()); this.rules = Stream.concat( builder.configurers.all() .stream() @@ -52,12 +59,16 @@ public JavaClasses classesWithTests() { return Namespace.withTests(this.namespace); } + public Set excludedClasses() { + return this.excludedClasses; + } + public Collection rules() { return this.rules; } public void check() { - this.rules.forEach(rule -> rule.check(this.namespace)); + this.rules.forEach(rule -> rule.check(this.namespace, this.excludedClasses)); } public static Builder builder() { @@ -68,6 +79,7 @@ public static final class Builder { private final Configurers configurers; private final Collection rules; + private final Set excludedClasses; private boolean failOnEmpty; private String namespace; @@ -75,6 +87,7 @@ public static final class Builder { public Builder() { this.configurers = new Configurers(); this.rules = new ArrayList<>(); + this.excludedClasses = new HashSet<>(); } public Builder addRule(TaikaiRule rule) { @@ -97,6 +110,16 @@ public Builder namespace(String namespace) { return this; } + public Builder excludeClass(String className) { + this.excludedClasses.add(className); + return this; + } + + public Builder excludeClasses(Collection classNames) { + this.excludedClasses.addAll(classNames); + return this; + } + public Builder java(Customizer customizer) { return configure(customizer, JavaConfigurer::new); } @@ -115,8 +138,8 @@ public Builder spring(Customizer customizer) { private Builder configure(Customizer customizer, Function supplier) { - Objects.requireNonNull(customizer); - Objects.requireNonNull(supplier); + requireNonNull(customizer); + requireNonNull(supplier); customizer.customize(this.configurers.getOrApply(supplier.apply( new ConfigurerContext(this.namespace, this.configurers))) diff --git a/src/main/java/com/enofex/taikai/TaikaiRule.java b/src/main/java/com/enofex/taikai/TaikaiRule.java index 3383cf9..e49b551 100644 --- a/src/main/java/com/enofex/taikai/TaikaiRule.java +++ b/src/main/java/com/enofex/taikai/TaikaiRule.java @@ -1,8 +1,16 @@ package com.enofex.taikai; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.lang.ArchRule; -import java.util.Objects; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class TaikaiRule { @@ -10,9 +18,8 @@ public final class TaikaiRule { private final Configuration configuration; private TaikaiRule(ArchRule archRule, Configuration configuration) { - this.archRule = Objects.requireNonNull(archRule); - this.configuration = Objects.requireNonNullElse(configuration, - Configuration.defaultConfiguration()); + this.archRule = requireNonNull(archRule); + this.configuration = requireNonNullElse(configuration, Configuration.defaultConfiguration()); } public ArchRule archRule() { @@ -32,6 +39,10 @@ public static TaikaiRule of(ArchRule archRule, Configuration configuration) { } public void check(String globalNamespace) { + check(globalNamespace, Collections.emptySet()); + } + + public void check(String globalNamespace, Set excludedClasses) { if (this.configuration.javaClasses() != null) { this.archRule.check(this.configuration.javaClasses()); } else { @@ -43,22 +54,44 @@ public void check(String globalNamespace) { throw new TaikaiException("Namespace is not provided"); } - this.archRule.check(Namespace.from(namespace, this.configuration.namespaceImport)); + Set allExcludedClasses = allExcludedClasses(excludedClasses); + + if (allExcludedClasses.isEmpty()) { + this.archRule.check(Namespace.from(namespace, this.configuration.namespaceImport)); + } else { + this.archRule.check(Namespace.from(namespace, this.configuration.namespaceImport) + .that(new DescribedPredicate<>("exclude classes") { + @Override + public boolean test(JavaClass javaClass) { + return !allExcludedClasses.contains(javaClass.getFullName()); + } + })); + } } } + private Set allExcludedClasses(Set excludedClasses) { + return Stream.concat( + this.configuration.excludedClasses != null + ? this.configuration.excludedClasses.stream() : Stream.empty(), + excludedClasses != null + ? excludedClasses.stream() : Stream.empty()).collect(Collectors.toSet()); + } + public static final class Configuration { private final String namespace; private final Namespace.IMPORT namespaceImport; private final JavaClasses javaClasses; + private final Set excludedClasses; private Configuration(String namespace, Namespace.IMPORT namespaceImport, - JavaClasses javaClasses) { + JavaClasses javaClasses, Set excludedClasses) { this.namespace = namespace; - this.namespaceImport = Objects.requireNonNullElse(namespaceImport, + this.namespaceImport = requireNonNullElse(namespaceImport, Namespace.IMPORT.WITHOUT_TESTS); this.javaClasses = javaClasses; + this.excludedClasses = excludedClasses != null ? excludedClasses : Collections.emptySet(); } public String namespace() { @@ -73,24 +106,51 @@ public JavaClasses javaClasses() { return this.javaClasses; } + public Set excludedClasses() { + return this.excludedClasses; + } + public static Configuration defaultConfiguration() { - return new Configuration(null, Namespace.IMPORT.WITHOUT_TESTS, null); + return new Configuration(null, Namespace.IMPORT.WITHOUT_TESTS, null, null); } public static Configuration of(String namespace) { - return new Configuration(namespace, Namespace.IMPORT.WITHOUT_TESTS, null); + return new Configuration(namespace, Namespace.IMPORT.WITHOUT_TESTS, null, null); + } + + public static Configuration of(String namespace, Set excludedClasses) { + return new Configuration(namespace, Namespace.IMPORT.WITHOUT_TESTS, null, excludedClasses); } public static Configuration of(Namespace.IMPORT namespaceImport) { - return new Configuration(null, namespaceImport, null); + return new Configuration(null, namespaceImport, null, null); + } + + public static Configuration of(Namespace.IMPORT namespaceImport, Set excludedClasses) { + return new Configuration(null, namespaceImport, null, excludedClasses); } public static Configuration of(String namespace, Namespace.IMPORT namespaceImport) { - return new Configuration(namespace, namespaceImport, null); + return new Configuration(namespace, namespaceImport, null, null); + } + + public static Configuration of(String namespace, Namespace.IMPORT namespaceImport, + Set excludedClasses) { + return new Configuration(namespace, namespaceImport, null, excludedClasses); } public static Configuration of(JavaClasses javaClasses) { - return new Configuration(null, null, javaClasses); + return new Configuration(null, null, javaClasses, null); + } + + public static Configuration of(JavaClasses javaClasses, Set excludedClasses) { + return new Configuration(null, null, javaClasses, excludedClasses); + } + + public static Configuration of(Set excludedClasses) { + return new Configuration(null, null, null, excludedClasses); } + + } } diff --git a/src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java b/src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java index 039d8d6..9b294df 100644 --- a/src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java +++ b/src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java @@ -1,9 +1,10 @@ package com.enofex.taikai.configures; +import static java.util.Objects.requireNonNull; + import com.enofex.taikai.TaikaiRule; import java.util.ArrayList; import java.util.Collection; -import java.util.Objects; import java.util.function.Supplier; public abstract class AbstractConfigurer implements Configurer { @@ -12,7 +13,7 @@ public abstract class AbstractConfigurer implements Configurer { private final Collection rules; protected AbstractConfigurer(ConfigurerContext configurerContext) { - this.configurerContext = Objects.requireNonNull(configurerContext); + this.configurerContext = requireNonNull(configurerContext); this.rules = new ArrayList<>(); } @@ -37,8 +38,8 @@ protected void disable(Class clazz) { protected C customizer(Customizer customizer, Supplier supplier) { - Objects.requireNonNull(customizer); - Objects.requireNonNull(supplier); + requireNonNull(customizer); + requireNonNull(supplier); customizer.customize(this.configurerContext .configurers() diff --git a/src/main/java/com/enofex/taikai/configures/Configurers.java b/src/main/java/com/enofex/taikai/configures/Configurers.java index e058dbb..789a0e3 100644 --- a/src/main/java/com/enofex/taikai/configures/Configurers.java +++ b/src/main/java/com/enofex/taikai/configures/Configurers.java @@ -1,10 +1,11 @@ package com.enofex.taikai.configures; +import static java.util.Objects.requireNonNull; + import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; public final class Configurers implements Iterable { @@ -15,7 +16,7 @@ public Configurers() { } public C getOrApply(C configurer) { - Objects.requireNonNull(configurer); + requireNonNull(configurer); C existingConfigurer = (C) this.get(configurer.getClass()); return existingConfigurer != null ? existingConfigurer : this.apply(configurer); diff --git a/src/main/java/com/enofex/taikai/spring/BootConfigurer.java b/src/main/java/com/enofex/taikai/spring/BootConfigurer.java index c937d2d..d64f7df 100644 --- a/src/main/java/com/enofex/taikai/spring/BootConfigurer.java +++ b/src/main/java/com/enofex/taikai/spring/BootConfigurer.java @@ -4,12 +4,12 @@ import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithSpringBootApplication; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static java.util.Objects.requireNonNull; import com.enofex.taikai.TaikaiRule; import com.enofex.taikai.TaikaiRule.Configuration; import com.enofex.taikai.configures.AbstractConfigurer; import com.enofex.taikai.configures.ConfigurerContext; -import java.util.Objects; public final class BootConfigurer extends AbstractConfigurer { @@ -18,7 +18,7 @@ public final class BootConfigurer extends AbstractConfigurer { } public BootConfigurer springBootApplicationShouldBeIn(String location) { - Objects.requireNonNull(location); + requireNonNull(location); return springBootApplicationShouldBeIn(location, null); } diff --git a/src/test/java/com/enofex/taikai/TaikaiTest.java b/src/test/java/com/enofex/taikai/TaikaiTest.java index 35cc94b..9caaa20 100644 --- a/src/test/java/com/enofex/taikai/TaikaiTest.java +++ b/src/test/java/com/enofex/taikai/TaikaiTest.java @@ -17,6 +17,7 @@ import com.tngtech.archunit.ArchConfiguration; import java.util.Collection; import java.util.Collections; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +41,7 @@ void shouldBuildTaikaiWithDefaultValues() { assertNotNull(taikai.classes()); assertNotNull(taikai.classesWithTests()); assertTrue(taikai.rules().isEmpty()); + assertTrue(taikai.excludedClasses().isEmpty()); } @Test @@ -49,6 +51,9 @@ void shouldBuildTaikaiWithCustomValues() { Taikai taikai = Taikai.builder() .namespace(VALID_NAMESPACE) + .excludeClass("com.enofex.taikai.SomeClassToExclude") + .excludeClasses( + Set.of("com.enofex.taikai.foo.ClassToExclude", "com.enofex.taikai.bar.ClassToExclude")) .failOnEmpty(true) .addRules(rules) .build(); @@ -59,6 +64,7 @@ void shouldBuildTaikaiWithCustomValues() { assertNotNull(taikai.classesWithTests()); assertEquals(1, taikai.rules().size()); assertTrue(taikai.rules().contains(mockRule)); + assertEquals(3, taikai.excludedClasses().size()); } @Test @@ -127,6 +133,6 @@ void shouldCheckRules() { .build() .check(); - verify(mockRule, times(1)).check(VALID_NAMESPACE); + verify(mockRule, times(1)).check(VALID_NAMESPACE, Collections.emptySet()); } }