diff --git a/README.md b/README.md index 3e415d0..80761a8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,68 @@ +# Nebula ArchRules + +[ArchUnit](https://www.archunit.org/) a popular OSS library used to enforce “architectural” code rules as part of a +JUnit suite. However, it is limited by its design to be used as part of a JUnit suite in a single repository. Nebula +ArchRules is a toolkit which gives organizations the ability to share and apply rules across any number of repositories. +Rules can be sourced from OSS libraries or private internal libraries. + +### Authoring Rules + +To author rules, apply the ArchRules Library plugin to a project: + +```kotlin +plugins { + id("com.netflix.nebula.archrules.library") version ("latest.release") +} +``` + +This plugin will create a source set called `archRules`. Create classes in that source set which implement the +`com.netflix.nebula.archrules.core.ArchRulesService` interface. + +#### Example + +```java +package com.example.library; + +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.Priority; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; + +import java.util.Map; + +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; + +public class LibraryArchRules implements ArchRulesService { + private final ArchRule noDeprecated = ArchRuleDefinition.priority(Priority.LOW).noClasses() + .should().accessTargetWhere(targetOwner(annotatedWith(Deprecated.class))) + .orShould().accessTargetWhere(target(annotatedWith(Deprecated.class))) + .orShould().dependOnClassesThat().areAnnotatedWith(Deprecated.class) + .allowEmptyShould(true) + .as("No code should reference deprecated APIs") + .because("usage of deprecated APIs introduces risk that future upgrades and migrations will be blocked"); + + @Override + public Map getRules() { + return Map.of("deprecated", noDeprecated); + } +} +``` + +When authoring rules about the usage of your own library code, it is recommended to colocate your rules library in the +same project as the library code. The ArchRules plugin will publish the rules in a separate Jar, and the Runner plugin +will select that jar for running rules, but these rule classes will not end up up in the runtime classpath. + ## LICENSE + Copyright 2025 Netflix, Inc. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " +AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt index b03d531..2748524 100644 --- a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt @@ -2,8 +2,18 @@ package com.netflix.nebula.archrules.gradle import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.getByType class ArchrulesLibraryPlugin : Plugin { - override fun apply(target: Project) { + override fun apply(project: Project) { + project.plugins.withId("java") { + val ext = project.extensions.getByType() + val archRulesSourceSet = ext.sourceSets.create("archRules") + val version = ArchrulesLibraryPlugin::class.java.`package`.implementationVersion ?: "latest.release" + project.dependencies.add(archRulesSourceSet.implementationConfigurationName, + "com.netflix.nebula:nebula-archrules-core:$version" + ) + } } } \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt new file mode 100644 index 0000000..eb91cfa --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt @@ -0,0 +1,21 @@ +package com.netflix.nebula.archrules.gradle + +import nebula.test.dsl.TestKitAssertions.assertThat +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + +class ArchrulesLibraryPluginTest { + + @Test + fun `plugin registers library dependency`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("java") + project.plugins.apply(ArchrulesLibraryPlugin::class.java) + val configuration = project.configurations.findByName("archRulesImplementation") + assertThat(configuration).isNotNull + val coreLibrary = configuration!!.dependencies + .firstOrNull { it.group == "com.netflix.nebula" && it.name == "nebula-archrules-core" } + assertThat(coreLibrary).isNotNull + assertThat(coreLibrary!!.version).isEqualTo("latest.release") + } +} \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt index 955561f..8b76e3d 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt @@ -20,6 +20,10 @@ internal class IntegrationTest { id("java-library") id("com.netflix.nebula.archrules.library") } + repositories { + maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins") + mavenCentral() + } src { main { java( @@ -34,6 +38,40 @@ public class LibraryClass { """ ) } + sourceSet("archRules") { + java( + "com/example/library/LibraryArchRules.java", + //language=java + """ +package com.example.library; + +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.Priority; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import java.util.Map; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; + +public class LibraryArchRules implements ArchRulesService { + private final ArchRule noDeprecated = ArchRuleDefinition.priority(Priority.LOW) + .noClasses() + .should().accessTargetWhere(targetOwner(annotatedWith(Deprecated.class))) + .orShould().accessTargetWhere(target(annotatedWith(Deprecated.class))) + .orShould().dependOnClassesThat().areAnnotatedWith(Deprecated.class) + .allowEmptyShould(true) + .as("No code should reference deprecated APIs") + .because("usage of deprecated APIs introduces risk that future upgrades and migrations will be blocked"); + + @Override + public Map getRules() { + return Map.of("deprecated", noDeprecated); + } +} +""" + ) + } } } subProject("code-to-check") { @@ -48,8 +86,11 @@ public class LibraryClass { } } - val result = runner.run("check") + val result = runner.run("check", "compileArchRulesJava") + assertThat(result.task(":library-with-rules:compileArchRulesJava")) + .`as`("compile task runs for the archRules source set") + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE) assertThat(result.task(":library-with-rules:check")) .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE) assertThat(result.task(":code-to-check:check"))