diff --git a/README.md b/README.md index 8f70b5a1..4e14aaa8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ In larger organizations, Gradle modules and dependencies are often owned by spec # Identifier for dependencies -> dependency shorthand without the version - identifier: androidx.core:core owner: core-team + +# Wildcard identifier -> matches multiple components (can be both modules or dependencies) +- identifier: :sample:wildcard:* + owner: wildcard-team ``` This ownership file can be maintained manually, but in most cases it will be more practical to generate it during the build. You can point Ruler to your YAML file in the `build.gradle`, you can also configure default owners for components missing from the ownership file: diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfo.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfo.kt index f3356644..9dffbb77 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfo.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfo.kt @@ -25,14 +25,38 @@ import com.spotify.ruler.models.ComponentType * @param defaultOwner Owner which should be used if no explicit owner is defined. */ class OwnershipInfo(entries: List, private val defaultOwner: String) { - private val ownershipEntries = entries.associate { entry -> entry.identifier to entry.owner } + private val explicitOwnershipEntries = mutableMapOf() + private val wildcardOwnershipEntries = mutableMapOf() + + // Differentiate between explicit (full match) entries and wildcard entries + init { + entries.forEach { (identifier, owner) -> + if (identifier.endsWith('*')) { + wildcardOwnershipEntries[identifier.substringBeforeLast('*')] = owner + } else { + explicitOwnershipEntries[identifier] = owner + } + } + } /** * Returns the owner of a given [component]. If the owner has no explicit owner, the [defaultOwner] will be returned * instead. */ - fun getOwner(component: String, componentType: ComponentType): String = when (componentType) { - ComponentType.INTERNAL -> ownershipEntries[component] ?: defaultOwner - ComponentType.EXTERNAL -> ownershipEntries[component.substringBeforeLast(':')] ?: defaultOwner + fun getOwner(component: String, componentType: ComponentType): String { + val owner = when (componentType) { + ComponentType.INTERNAL -> explicitOwnershipEntries[component] + ComponentType.EXTERNAL -> explicitOwnershipEntries[component.substringBeforeLast(':')] + } + return owner ?: getWildcardOwner(component) ?: defaultOwner + } + + /** Tries to find the owner for a component with the given [component] identifier based on all wildcard entries. */ + private fun getWildcardOwner(component: String): String? { + val identifier = wildcardOwnershipEntries.keys + .filter(component::startsWith) // Find all identifiers that match the wildcard + .maxByOrNull(String::length) // Take the longest one because that one is the most specific + + return wildcardOwnershipEntries[identifier] } } diff --git a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfoTest.kt b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfoTest.kt index 0a163286..00efd8d8 100644 --- a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfoTest.kt +++ b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/ownership/OwnershipInfoTest.kt @@ -23,7 +23,13 @@ import org.junit.jupiter.api.Test class OwnershipInfoTest { private val entries = listOf( OwnershipEntry(":foo:bar", "internal-component-owner"), + OwnershipEntry(":wildcard:foo:*", "internal-wildcard-foo-owner"), + OwnershipEntry(":wildcard:*", "internal-wildcard-owner"), + OwnershipEntry(":wildcard:foo:bar", "internal-wildcard-foo-bar-owner"), OwnershipEntry("com.spotify:main", "external-component-owner"), + OwnershipEntry("com.wildcard.spotify:*", "external-wildcard-spotify-owner"), + OwnershipEntry("com.wildcard.*", "external-wildcard-owner"), + OwnershipEntry("com.wildcard.spotify:foo", "external-wildcard-spotify-foo-owner"), ) private val ownershipInfo = OwnershipInfo(entries, "default-owner") @@ -45,6 +51,24 @@ class OwnershipInfoTest { assertThat(owner).isEqualTo("default-owner") } + @Test + fun `Internal component owner is found for wildcard entries`() { + val owner = ownershipInfo.getOwner(":wildcard:test", ComponentType.INTERNAL) + assertThat(owner).isEqualTo("internal-wildcard-owner") + } + + @Test + fun `Internal component owner is found for more specific wildcard entries`() { + val owner = ownershipInfo.getOwner(":wildcard:foo:test", ComponentType.INTERNAL) + assertThat(owner).isEqualTo("internal-wildcard-foo-owner") + } + + @Test + fun `Internal component owner is found for explicit entry when wildcard is present`() { + val owner = ownershipInfo.getOwner(":wildcard:foo:bar", ComponentType.INTERNAL) + assertThat(owner).isEqualTo("internal-wildcard-foo-bar-owner") + } + @Test fun `External component owner is found`() { val owner = ownershipInfo.getOwner("com.spotify:main:1.0.0", ComponentType.EXTERNAL) @@ -62,4 +86,22 @@ class OwnershipInfoTest { val owner = ownershipInfo.getOwner("com.spotify:main:1.0.0", ComponentType.INTERNAL) assertThat(owner).isEqualTo("default-owner") } + + @Test + fun `External component owner is found for wildcard entries`() { + val owner = ownershipInfo.getOwner("com.wildcard.test:test:1.0.0", ComponentType.EXTERNAL) + assertThat(owner).isEqualTo("external-wildcard-owner") + } + + @Test + fun `External component owner is found for more specific wildcard entries`() { + val owner = ownershipInfo.getOwner("com.wildcard.spotify:test:1.0.0", ComponentType.EXTERNAL) + assertThat(owner).isEqualTo("external-wildcard-spotify-owner") + } + + @Test + fun `External component owner is found for explicit entry when wildcard is present`() { + val owner = ownershipInfo.getOwner("com.wildcard.spotify:foo:1.0.0", ComponentType.EXTERNAL) + assertThat(owner).isEqualTo("external-wildcard-spotify-foo-owner") + } }