Skip to content

Commit 0a1c6a7

Browse files
authored
Add task for generating archrules documentation in library plugin (#57)
1 parent f1ff834 commit 0a1c6a7

File tree

4 files changed

+149
-2
lines changed

4 files changed

+149
-2
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ You may declare dependencies on the `archRulesImplementation` configuration. Thi
7676
1) using a dependency as helper for your rule code
7777
2) transitively depending on a standalone rules library so that its rules are run in any project which runs the current project's rules
7878

79+
#### Automated Documentation
80+
81+
The ArchRules Library plugin provides the `generateRulesDocumentation` task to generate markdown documentation for all of your library's rules authored in the `archRules` source set.
82+
Running `./gradlew generateRulesDocumentation` creates documentation at `build/docs/archrules.md`, which includes all rules, descriptions, and priorities.
83+
7984
### Testing Rules
8085

8186
The ArchRules Library plugin creates a test suite called `archRulesTest`. You can write unit tests for your rules in the `archRulesTest` source set.

nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.netflix.nebula.archrules.gradle
33
import com.netflix.nebula.archrules.gradle.ArchRuleAttribute.ARCH_RULES
44
import org.gradle.api.Plugin
55
import org.gradle.api.Project
6-
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
76
import org.gradle.api.attributes.Usage
87
import org.gradle.api.component.AdhocComponentWithVariants
98
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact
@@ -74,6 +73,15 @@ class ArchrulesLibraryPlugin : Plugin<Project> {
7473
archiveClassifier.set("arch-rules")
7574
dependsOn(generateServicesTask)
7675
}
76+
project.tasks.register<GenerateRulesDocumentationTask>("generateRulesDocumentation") {
77+
description = "Generates documentation for ArchRules"
78+
group = "documentation"
79+
rulesClasspath.from(archRulesSourceSet.output)
80+
outputFile.convention(
81+
project.layout.buildDirectory.file("docs/archrules.md")
82+
)
83+
dependsOn(generateServicesTask)
84+
}
7785
registerRuntimeFeatureForSourceSet(project, archRulesSourceSet, jarTask)
7886
project.pluginManager.withPlugin("jvm-test-suite") {
7987
val ext = project.extensions.getByType<TestingExtension>()
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.netflix.nebula.archrules.gradle
2+
3+
import com.netflix.nebula.archrules.core.ArchRulesService
4+
import com.netflix.nebula.archrules.core.Runner
5+
import com.tngtech.archunit.lang.ArchRule
6+
import com.tngtech.archunit.lang.Priority
7+
import org.gradle.api.DefaultTask
8+
import org.gradle.api.file.ConfigurableFileCollection
9+
import org.gradle.api.file.RegularFileProperty
10+
import org.gradle.api.tasks.CacheableTask
11+
import org.gradle.api.tasks.Classpath
12+
import org.gradle.api.tasks.OutputFile
13+
import org.gradle.api.tasks.TaskAction
14+
import java.net.URLClassLoader
15+
import java.util.ServiceLoader
16+
17+
@CacheableTask
18+
abstract class GenerateRulesDocumentationTask : DefaultTask() {
19+
20+
@get:Classpath
21+
abstract val rulesClasspath: ConfigurableFileCollection
22+
23+
@get:OutputFile
24+
abstract val outputFile: RegularFileProperty
25+
26+
@TaskAction
27+
fun generateDocs() {
28+
val rules = mutableListOf<RuleMetadata>()
29+
val classPathUrls = rulesClasspath.files.map { it.toURI().toURL() }.toTypedArray()
30+
31+
URLClassLoader(classPathUrls, this.javaClass.classLoader).use { classLoader ->
32+
ServiceLoader.load(ArchRulesService::class.java, classLoader).forEach { service ->
33+
val serviceClassName = service.javaClass.name
34+
service.rules.forEach { (ruleName, archRule) ->
35+
rules.add(
36+
RuleMetadata(
37+
ruleClass = serviceClassName,
38+
ruleName = ruleName,
39+
description = archRule.description,
40+
priority = getPriority(archRule)
41+
)
42+
)
43+
}
44+
}
45+
}
46+
47+
val output = outputFile.get().asFile
48+
output.parentFile.mkdirs()
49+
output.writeText(formatMarkdown(rules))
50+
}
51+
52+
private fun formatMarkdown(rules: List<RuleMetadata>): String {
53+
val str = StringBuilder()
54+
str.append("# ArchRules Documentation\n\n")
55+
str.append("List of all archrules defined in this library.\n\n")
56+
57+
val sortedRulesByName = rules.sortedBy { it.ruleName }
58+
sortedRulesByName.forEach { rule ->
59+
str.append("## ${rule.ruleName}\n\n")
60+
str.append("**Description:** ${rule.description}\n\n")
61+
str.append("**Priority:** ${rule.priority}\n\n")
62+
str.append("**Class:** `${rule.ruleClass}`\n\n")
63+
str.append("---\n\n")
64+
}
65+
66+
return str.toString()
67+
}
68+
69+
// workaround since ArchRule priority is private
70+
private fun getPriority(archRule: ArchRule): Priority {
71+
val dummyResult = Runner.check(archRule)
72+
return dummyResult.priority
73+
}
74+
75+
private data class RuleMetadata(
76+
val ruleClass: String,
77+
val ruleName: String,
78+
val description: String,
79+
val priority: Priority
80+
)
81+
}

nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,57 @@ class ArchrulesLibraryPluginTest {
331331
.hasNoMutableStateWarnings()
332332
.hasNoDeprecationWarnings()
333333
}
334-
}
334+
335+
336+
@Test
337+
fun `test generateRulesDocumentation task`() {
338+
val runner = testProject(projectDir) {
339+
properties {
340+
buildCache(true)
341+
}
342+
settings {
343+
name("library-with-rules")
344+
}
345+
rootProject {
346+
plugins {
347+
id("java-library")
348+
id("com.netflix.nebula.archrules.library")
349+
}
350+
repositories {
351+
maven("https://netflixoss.jfrog.io/artifactory/gradle-plugins")
352+
mavenCentral()
353+
}
354+
src {
355+
sourceSet("archRules") {
356+
dontUseRule()
357+
exampleDeprecatedHighArchRule()
358+
}
359+
}
360+
dependencies("""
361+
archRulesImplementation("com.netflix.nebula:archrules-nullability:0.+")
362+
""".trimIndent()
363+
)
364+
}
365+
}
366+
367+
val result = runner.run("generateRulesDocumentation", "--stacktrace") {
368+
forwardOutput()
369+
}
370+
371+
assertThat(result.task(":generateRulesDocumentation"))
372+
.hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE)
373+
374+
val docsFile = projectDir.resolve("build/docs/archrules.md")
375+
assertThat(docsFile).exists()
376+
377+
assertThat(docsFile.readText())
378+
.contains("# ArchRules Documentation")
379+
.contains("## deprecated\n" +
380+
"\n" +
381+
"**Description:** No code should reference deprecated APIs, because usage of deprecated APIs introduces risk that future upgrades and migrations will be blocked\n" +
382+
"\n" +
383+
"**Priority:** HIGH")
384+
.contains("## dont use")
385+
.doesNotContain("## public classes should be @NullMarked")
386+
}
387+
}

0 commit comments

Comments
 (0)