diff --git a/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt b/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt index efa458c4..039ed6a4 100644 --- a/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt +++ b/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt @@ -88,8 +88,8 @@ class ReleaseReportTest { downloadSize += component.downloadSize installSize += component.installSize } - assertThat(downloadSize).isEqualTo(report.downloadSize) - assertThat(installSize).isEqualTo(report.installSize) + assertThat(downloadSize).isEqualTo(report.insights.appDownloadSize) + assertThat(installSize).isEqualTo(report.insights.appInstallSize) } @Test diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt index 4b315323..6b388113 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt @@ -52,8 +52,12 @@ fun RBuilder.containerList(containers: List, sizeType: Measurable @Suppress("UNUSED_PARAMETER") fun RBuilder.containerListItem(id: Int, container: FileContainer, sizeType: Measurable.SizeType, @RKey key: String) { div(classes = "accordion-item") { - containerListItemHeader(id, container, sizeType) - containerListItemBody(id, container, sizeType) + if (container.files.isEmpty()) { + containerWithoutFilesListItemHeader(container, sizeType) + } else { + containerListItemHeader(id, container, sizeType) + containerListItemBody(id, container, sizeType) + } } } @@ -63,15 +67,27 @@ fun RBuilder.containerListItemHeader(id: Int, container: FileContainer, sizeType button(classes = "accordion-button collapsed") { attrs["data-bs-toggle"] = "collapse" attrs["data-bs-target"] = "#module-$id-body" - span(classes = "font-monospace text-truncate me-3") { +container.name } - container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } - span(classes = "ms-auto me-3 text-nowrap") { - +formatSize(container, sizeType) - } + containerHeader(container, sizeType) } } } +@RFunction +fun RBuilder.containerWithoutFilesListItemHeader(container: FileContainer, sizeType: Measurable.SizeType) { + div(classes = "list-group-item d-flex border-0") { + containerHeader(container, sizeType) + } +} + +@RFunction +fun RBuilder.containerHeader(container: FileContainer, sizeType: Measurable.SizeType) { + span(classes = "font-monospace text-truncate me-3") { +container.name } + container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner.name } } + span(classes = "ms-auto me-3 text-nowrap") { + +formatSize(container, sizeType) + } +} + @RFunction fun RBuilder.containerListItemBody(id: Int, container: FileContainer, sizeType: Measurable.SizeType) { div(classes = "accordion-collapse collapse") { diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt index 2659d173..fd201301 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt @@ -44,13 +44,13 @@ import react.useState fun RBuilder.report(report: AppReport) { val (sizeType, setSizeType) = useState(Measurable.SizeType.DOWNLOAD) - val hasOwnershipInfo = report.components.any { component -> component.owner != null } + val hasOwnershipInfo = report.ownershipOverview?.isNotEmpty() == true val hasDynamicFeatures = report.dynamicFeatures.isNotEmpty() val tabs = listOf( Tab("/", "Breakdown") { breakdown(report.components, sizeType) }, - Tab("/insights", "Insights") { insights(report.components) }, - Tab("/ownership", "Ownership", hasOwnershipInfo) { ownership(report.components, sizeType) }, + Tab("/insights", "Insights") { insights(report.insights) }, + Tab("/ownership", "Ownership", hasOwnershipInfo) { ownership(report.components, sizeType, report.ownershipOverview ?: emptyMap()) }, Tab("/dynamic", "Dynamic features", hasDynamicFeatures) { dynamicFeatures(report.dynamicFeatures, sizeType) }, ) @@ -74,8 +74,8 @@ fun RBuilder.header(report: AppReport) { h3 { +report.name } span(classes = "text-muted") { +"Version ${report.version} (${report.variant})" } } - headerSizeItem(report.downloadSize, "Download size") - headerSizeItem(report.installSize, "Install size") + headerSizeItem(report.insights.appDownloadSize, "Download size") + headerSizeItem(report.insights.appInstallSize, "Install size") } } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt index dd82e7f3..5df98eb0 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt @@ -22,8 +22,11 @@ import com.spotify.ruler.frontend.chart.ChartConfig import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.frontend.chart.seriesOf -import com.spotify.ruler.models.AppComponent -import com.spotify.ruler.models.Measurable +import com.spotify.ruler.models.ComponentType +import com.spotify.ruler.models.FileType +import com.spotify.ruler.models.Insights +import com.spotify.ruler.models.TypeInsights +import com.spotify.ruler.models.ResourceType import kotlinx.browser.document import kotlinx.html.id import react.RBuilder @@ -33,30 +36,30 @@ import react.dom.p import react.useEffect @RFunction -fun RBuilder.insights(components: List) { +fun RBuilder.insights(insights: Insights) { div(classes = "row mb-3") { - fileTypeGraphs(components) + fileTypeGraphs(insights.fileTypeInsights) } div(classes = "row") { - componentTypeGraphs(components) + componentTypeGraphs(insights.componentTypeInsights) } div(classes = "row") { - resourcesTypeGraphs(components) + resourcesTypeGraphs(insights.resourcesTypeInsights) } } @RFunction -fun RBuilder.fileTypeGraphs(components: List) { - val labels = arrayOf("Classes", "Resources", "Assets", "Native libraries", "Other") +fun RBuilder.fileTypeGraphs(fileTypeInsights: Map) { + val labels = FileType.values().map { it.label }.toTypedArray() val downloadSizes = LongArray(labels.size) val installSizes = LongArray(labels.size) val fileCounts = LongArray(labels.size) - components.flatMap(AppComponent::files).forEach { file -> - val index = file.type.ordinal - downloadSizes[index] += file.getSize(Measurable.SizeType.DOWNLOAD) - installSizes[index] += file.getSize(Measurable.SizeType.INSTALL) - fileCounts[index]++ + fileTypeInsights.forEach { (fileType, insights) -> + val index = fileType.ordinal + downloadSizes[index] = insights.downloadSize + installSizes[index] = insights.installSize + fileCounts[index] = insights.count } chart( @@ -83,17 +86,17 @@ fun RBuilder.fileTypeGraphs(components: List) { } @RFunction -fun RBuilder.componentTypeGraphs(components: List) { - val labels = arrayOf("Internal", "External") +fun RBuilder.componentTypeGraphs(componentTypeInsights: Map) { + val labels = ComponentType.values().map { it.label }.toTypedArray() val downloadSizes = LongArray(labels.size) val installSizes = LongArray(labels.size) val fileCounts = LongArray(labels.size) - components.forEach { component -> - val index = component.type.ordinal - downloadSizes[index] += component.getSize(Measurable.SizeType.DOWNLOAD) - installSizes[index] += component.getSize(Measurable.SizeType.INSTALL) - fileCounts[index]++ + componentTypeInsights.forEach { (componentType, insights) -> + val index = componentType.ordinal + downloadSizes[index] = insights.downloadSize + installSizes[index] = insights.installSize + fileCounts[index] = insights.count } chart( @@ -122,17 +125,17 @@ fun RBuilder.componentTypeGraphs(components: List) { } @RFunction -fun RBuilder.resourcesTypeGraphs(components: List) { - val labels = arrayOf("Drawable", "Layout", "Raw", "Values", "Font", "Other") +fun RBuilder.resourcesTypeGraphs(resourcesTypeInsights: Map) { + val labels = ResourceType.values().map { it.label }.toTypedArray() val downloadSizes = LongArray(labels.size) val installSizes = LongArray(labels.size) val fileCounts = LongArray(labels.size) - components.flatMap(AppComponent::files).filter { it.resourceType != null }.forEach { file -> - val index = file.resourceType!!.ordinal - downloadSizes[index] += file.getSize(Measurable.SizeType.DOWNLOAD) - installSizes[index] += file.getSize(Measurable.SizeType.INSTALL) - fileCounts[index]++ + resourcesTypeInsights.forEach { (resourceType, insights) -> + val index = resourceType.ordinal + downloadSizes[index] = insights.downloadSize + installSizes[index] = insights.installSize + fileCounts[index] = insights.count } chart( diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt index 5bab9a4a..59723e7b 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt @@ -22,9 +22,11 @@ import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.chart.seriesOf import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.models.AppComponent -import com.spotify.ruler.models.AppFile import com.spotify.ruler.models.ComponentType import com.spotify.ruler.models.Measurable +import com.spotify.ruler.models.OwnedComponentSize +import com.spotify.ruler.models.ComponentOwner +import com.spotify.ruler.models.OwnershipOverview import react.RBuilder import react.dom.div import react.dom.h4 @@ -34,25 +36,21 @@ import react.useState const val PAGE_SIZE = 10 @RFunction -fun RBuilder.ownership(components: List, sizeType: Measurable.SizeType) { - componentOwnershipOverview(components) - componentOwnershipPerTeam(components, sizeType) +fun RBuilder.ownership( + components: List, + sizeType: Measurable.SizeType, + ownershipOverview: Map, +) { + componentOwnershipOverview(ownershipOverview) + componentOwnershipPerTeam(components, sizeType, ownershipOverview) } @RFunction -fun RBuilder.componentOwnershipOverview(components: List) { - val sizes = mutableMapOf() - components.flatMap(AppComponent::files).forEach { file -> - val owner = file.owner ?: return@forEach - val current = sizes.getOrPut(owner) { Measurable.Mutable(0, 0) } - current.downloadSize += file.downloadSize - current.installSize += file.installSize - } - - val sorted = sizes.entries.sortedByDescending { (_, measurable) -> measurable.downloadSize } +fun RBuilder.componentOwnershipOverview(ownershipOverview: Map) { + val sorted = ownershipOverview.entries.sortedByDescending { (_, ownership) -> ownership.totalInstallSize } val owners = sorted.map { (owner, _) -> owner } - val downloadSizes = sorted.map { (_, measurable) -> measurable.downloadSize } - val installSizes = sorted.map { (_, measurable) -> measurable.installSize } + val downloadSizes = sorted.map { (_, ownership) -> ownership.totalDownloadSize } + val installSizes = sorted.map { (_, ownership) -> ownership.totalInstallSize } pagedContent(owners.size, PAGE_SIZE) { pageStartIndex, pageEndIndex -> chart( @@ -74,34 +72,50 @@ fun RBuilder.componentOwnershipOverview(components: List) { } @RFunction -fun RBuilder.componentOwnershipPerTeam(components: List, sizeType: Measurable.SizeType) { +fun RBuilder.componentOwnershipPerTeam( + components: List, + sizeType: Measurable.SizeType, + ownershipOverview: Map, +) { val files = components.flatMap(AppComponent::files) - val owners = files.mapNotNull(AppFile::owner).distinct().sorted() + val owners = ownershipOverview.keys.sorted() var selectedOwner by useState(owners.first()) - val ownedComponents = components.filter { component -> component.owner == selectedOwner } + val ownedComponents = components.filter { component -> component.owner?.name == selectedOwner } val ownedFiles = files.filter { file -> file.owner == selectedOwner } + val ownedFilesCount = ownershipOverview[selectedOwner]?.filesCount ?: 0 + + val totalOwnedDownloadSize = ownershipOverview[selectedOwner]?.totalDownloadSize ?: 0 + val totalOwnedInstallSize = ownershipOverview[selectedOwner]?.totalInstallSize ?: 0 + val remainingOwnedDownloadSize = ownershipOverview[selectedOwner]?.filesFromNotOwnedComponentsDownloadSize ?: 0 + val remainingOwnedInstallSize = ownershipOverview[selectedOwner]?.filesFromNotOwnedComponentsInstallSize ?: 0 val remainingOwnedFiles = ownedFiles.toMutableSet() val processedComponents = ownedComponents.map { component -> val ownedFilesFromComponent = component.files.filter { file -> file.owner == selectedOwner } remainingOwnedFiles.removeAll(ownedFilesFromComponent) component.copy( - downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), - installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), + downloadSize = component.owner?.ownedSize?.downloadSize ?: 0, + installSize = component.owner?.ownedSize?.installSize ?: 0, files = ownedFilesFromComponent, ) }.toMutableList() // Group together all owned files which belong to components not owned by the currently selected owner - if (remainingOwnedFiles.isNotEmpty()) { + if (remainingOwnedDownloadSize > 0 || remainingOwnedInstallSize > 0) { processedComponents += AppComponent( name = "Other owned files", type = ComponentType.INTERNAL, - downloadSize = remainingOwnedFiles.sumOf(AppFile::downloadSize), - installSize = remainingOwnedFiles.sumOf(AppFile::installSize), + downloadSize = remainingOwnedDownloadSize, + installSize = remainingOwnedInstallSize, files = remainingOwnedFiles.toList(), - owner = selectedOwner, + owner = ComponentOwner( + name = selectedOwner, + ownedSize = OwnedComponentSize( + downloadSize = remainingOwnedDownloadSize, + installSize = remainingOwnedInstallSize, + ), + ), ) } @@ -109,9 +123,9 @@ fun RBuilder.componentOwnershipPerTeam(components: List, sizeType: dropdown(owners, "owner-dropdown") { owner -> selectedOwner = owner } div(classes = "row mt-4 mb-4") { highlightedValue(ownedComponents.size, "Component(s)") - highlightedValue(ownedFiles.size, "File(s)") - highlightedValue(ownedFiles.sumOf(AppFile::downloadSize), "Download size", ::formatSize) - highlightedValue(ownedFiles.sumOf(AppFile::installSize), "Install size", ::formatSize) + highlightedValue(ownedFilesCount, "File(s)") + highlightedValue(totalOwnedDownloadSize, "Download size", ::formatSize) + highlightedValue(totalOwnedInstallSize, "Install size", ::formatSize) } containerList(processedComponents, sizeType) } diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt index 9b45f940..82bc95a8 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt @@ -29,8 +29,11 @@ open class RulerExtension(objects: ObjectFactory) { val ownershipFile: RegularFileProperty = objects.fileProperty() val defaultOwner: Property = objects.property(String::class.java) + val shouldExcludeFileListing: Property = objects.property(Boolean::class.java) + // Set up default values init { defaultOwner.convention("unknown") + shouldExcludeFileListing.convention(false) } } diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt index 05d7b27e..0201b3e3 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt @@ -53,6 +53,8 @@ class RulerPlugin : Plugin { task.workingDir.set(project.layout.buildDirectory.dir("intermediates/ruler/${variant.name}")) task.reportDir.set(project.layout.buildDirectory.dir("reports/ruler/${variant.name}")) + task.shouldExcludeFileListing.set(rulerExtension.shouldExcludeFileListing) + // Add explicit dependency to support DexGuard task.dependsOn("bundle$variantName") } @@ -64,7 +66,7 @@ class RulerPlugin : Plugin { private fun getAppInfo(project: Project, variant: ApplicationVariant) = project.provider { AppInfo( applicationId = variant.applicationId.get(), - versionName = variant.outputs.first().versionName.get() ?: "-", + versionName = variant.outputs.first().versionName.orNull ?: "-", variantName = variant.name, ) } diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt index 161565f2..e4f40668 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt @@ -69,6 +69,9 @@ abstract class RulerTask : DefaultTask() { @get:Input abstract val defaultOwner: Property + @get:Input + abstract val shouldExcludeFileListing: Property + @get:OutputDirectory abstract val workingDir: DirectoryProperty @@ -132,7 +135,14 @@ abstract class RulerTask : DefaultTask() { val reportDir = reportDir.asFile.get() val jsonReporter = JsonReporter() - val jsonReport = jsonReporter.generateReport(appInfo.get(), components, features, ownershipInfo, reportDir) + val jsonReport = jsonReporter.generateReport( + appInfo.get(), + components, + features, + ownershipInfo, + shouldExcludeFileListing.get(), + reportDir, + ) project.logger.lifecycle("Wrote JSON report to ${jsonReport.toPath().toUri()}") val htmlReporter = HtmlReporter() diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt index a487a6b5..4c6d993f 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt @@ -19,8 +19,18 @@ package com.spotify.ruler.plugin.report import com.spotify.ruler.models.AppComponent import com.spotify.ruler.models.AppFile import com.spotify.ruler.models.AppReport +import com.spotify.ruler.models.ComponentType import com.spotify.ruler.models.DynamicFeature +import com.spotify.ruler.models.FileType +import com.spotify.ruler.models.Insights import com.spotify.ruler.models.Measurable +import com.spotify.ruler.models.MutableTypeInsights +import com.spotify.ruler.models.OwnedComponentSize +import com.spotify.ruler.models.ComponentOwner +import com.spotify.ruler.models.MutableOwnershipOverview +import com.spotify.ruler.models.OwnershipOverview +import com.spotify.ruler.models.ResourceType +import com.spotify.ruler.models.TypeInsights import com.spotify.ruler.plugin.dependency.DependencyComponent import com.spotify.ruler.plugin.models.AppInfo import com.spotify.ruler.plugin.ownership.OwnershipInfo @@ -38,6 +48,7 @@ class JsonReporter { * @param appInfo General info about the analyzed app. * @param components Map of app component names to their respective files * @param ownershipInfo Optional info about the owners of components. + * @param shouldExcludeFileListing Flag to determine if file listing should be included in the report * @param targetDir Directory where the generated report will be located * @return Generated JSON report file */ @@ -46,51 +57,19 @@ class JsonReporter { components: Map>, features: Map>, ownershipInfo: OwnershipInfo?, + shouldExcludeFileListing: Boolean, targetDir: File ): File { + val appComponents = getAppComponents(components, ownershipInfo) val report = AppReport( name = appInfo.applicationId, version = appInfo.versionName, variant = appInfo.variantName, - downloadSize = components.values.flatten().sumOf(AppFile::downloadSize), - installSize = components.values.flatten().sumOf(AppFile::installSize), - components = components.map { (component, files) -> - AppComponent( - name = component.name, - type = component.type, - downloadSize = files.sumOf(AppFile::downloadSize), - installSize = files.sumOf(AppFile::installSize), - owner = ownershipInfo?.getOwner(component.name, component.type), - files = files.map { file -> - AppFile( - name = file.name, - type = file.type, - downloadSize = file.downloadSize, - installSize = file.installSize, - owner = ownershipInfo?.getOwner(file.name, component.name, component.type), - resourceType = file.resourceType, - ) - }.sortedWith(comparator.reversed()) - ) - }.sortedWith(comparator.reversed()), - dynamicFeatures = features.map { (feature, files) -> - DynamicFeature( - name = feature, - downloadSize = files.sumOf(AppFile::downloadSize), - installSize = files.sumOf(AppFile::installSize), - owner = ownershipInfo?.getOwner(feature), - files = files.map { file -> - AppFile( - name = file.name, - type = file.type, - downloadSize = file.downloadSize, - installSize = file.installSize, - owner = ownershipInfo?.getOwner(file.name, feature), - resourceType = file.resourceType, - ) - }.sortedWith(comparator.reversed()) - ) - }.sortedWith(comparator.reversed()), + components = appComponents.excludeComponentFilesIfNeeded(shouldExcludeFileListing), + dynamicFeatures = getDynamicFeatures(features, ownershipInfo) + .excludeDyamicFeatureFilesIfNeeded(shouldExcludeFileListing), + insights = getInsights(components, appComponents), + ownershipOverview = ownershipInfo?.let { getOwnershipOverview(appComponents) }, ) val format = Json { prettyPrint = true } @@ -98,4 +77,192 @@ class JsonReporter { reportFile.writeText(format.encodeToString(report)) return reportFile } + + private fun getAppComponents( + components: Map>, + ownershipInfo: OwnershipInfo? + ): List = + components.map { (component, files) -> + val componentFiles = files.map { file -> + AppFile( + name = file.name, + type = file.type, + downloadSize = file.downloadSize, + installSize = file.installSize, + owner = ownershipInfo?.getOwner(file.name, component.name, component.type), + resourceType = file.resourceType, + ) + }.sortedWith(comparator.reversed()) + AppComponent( + name = component.name, + type = component.type, + downloadSize = files.sumOf(AppFile::downloadSize), + installSize = files.sumOf(AppFile::installSize), + owner = ownershipInfo?.let { + getOwner(ownershipInfo.getOwner(component.name, component.type), componentFiles) + }, + files = componentFiles + ) + }.sortedWith(comparator.reversed()) + + private fun getDynamicFeatures( + features: Map>, + ownershipInfo: OwnershipInfo? + ): List = + features.map { (feature, files) -> + val dynamicFeatureFiles = files.map { file -> + AppFile( + name = file.name, + type = file.type, + downloadSize = file.downloadSize, + installSize = file.installSize, + owner = ownershipInfo?.getOwner(file.name, feature), + resourceType = file.resourceType, + ) + }.sortedWith(comparator.reversed()) + DynamicFeature( + name = feature, + downloadSize = files.sumOf(AppFile::downloadSize), + installSize = files.sumOf(AppFile::installSize), + owner = ownershipInfo?.let { getOwner(ownershipInfo.getOwner(feature), dynamicFeatureFiles) }, + files = dynamicFeatureFiles + ) + }.sortedWith(comparator.reversed()) + + private fun getOwner( + componentOwnerName: String, + files: List, + ): ComponentOwner { + var ownedDownloadSize = 0L + var ownedInstallSize = 0L + files.filter { it.owner == componentOwnerName }.forEach { ownedFile -> + ownedDownloadSize += ownedFile.downloadSize + ownedInstallSize += ownedFile.installSize + } + + return ComponentOwner( + name = componentOwnerName, + ownedSize = OwnedComponentSize( + downloadSize = ownedDownloadSize, + installSize = ownedInstallSize, + ) + ) + } + + private fun getInsights( + components: Map>, + appComponents: List, + ): Insights = + Insights( + appDownloadSize = components.values.flatten().sumOf(AppFile::downloadSize), + appInstallSize = components.values.flatten().sumOf(AppFile::installSize), + fileTypeInsights = getFileTypeInsights(appComponents), + componentTypeInsights = getComponentTypeInsights(appComponents), + resourcesTypeInsights = getResourcesTypeInsights(appComponents), + ) + + private fun getFileTypeInsights(components: List): Map = + getTypeInsights( + items = components.flatMap(AppComponent::files), + getKey = { type } + ) + + private fun getComponentTypeInsights(components: List): Map = + getTypeInsights( + items = components, + getKey = { type } + ) + + private fun getResourcesTypeInsights(components: List): Map = + getTypeInsights( + items = components.flatMap(AppComponent::files).filter { it.resourceType != null }, + getKey = { resourceType!! } + ) + + private inline fun getTypeInsights( + items: List, + getKey: T.() -> K + ): Map { + val typeInsights = mutableMapOf() + + items.forEach { item -> + val key = item.getKey() + val currentInsights = typeInsights.getOrPut(key) { + MutableTypeInsights( + downloadSize = 0L, + installSize = 0L, + count = 0L, + ) + } + currentInsights.downloadSize += item.getSize(Measurable.SizeType.DOWNLOAD) + currentInsights.installSize += item.getSize(Measurable.SizeType.INSTALL) + currentInsights.count++ + } + + return typeInsights.asSequence() + .map { + val (type, insights) = it + type to TypeInsights( + downloadSize = insights.downloadSize, + installSize = insights.installSize, + count = insights.count + ) + }.toMap() + } + + private fun getOwnershipOverview(appComponents: List): Map { + val overview = mutableMapOf() + + appComponents.forEach { component -> + component.files.forEach { file -> + file.owner?.let { fileOwner -> + val current = overview.getOrPut(fileOwner) { + MutableOwnershipOverview( + totalDownloadSize = 0, + totalInstallSize = 0, + filesCount = 0, + filesFromNotOwnedComponentsDownloadSize = 0, + filesFromNotOwnedComponentsInstallSize = 0, + ) + } + current.totalDownloadSize += file.downloadSize + current.totalInstallSize += file.installSize + current.filesCount++ + if (fileOwner != component.owner?.name) { + current.filesFromNotOwnedComponentsDownloadSize += file.downloadSize + current.filesFromNotOwnedComponentsInstallSize += file.installSize + } + } + } + } + + return overview.map { + val (owner, ownershipOverview) = it + owner to OwnershipOverview( + totalDownloadSize = ownershipOverview.totalDownloadSize, + totalInstallSize = ownershipOverview.totalInstallSize, + filesCount = ownershipOverview.filesCount, + filesFromNotOwnedComponentsDownloadSize = ownershipOverview.filesFromNotOwnedComponentsDownloadSize, + filesFromNotOwnedComponentsInstallSize = ownershipOverview.filesFromNotOwnedComponentsInstallSize, + ) + }.toMap() + } + + private fun List.excludeComponentFilesIfNeeded( + shouldExcludeFileListing: Boolean + ): List = + if (shouldExcludeFileListing) { + map { it.copy(files = emptyList()) } + } else { + this + } + + private fun List.excludeDyamicFeatureFilesIfNeeded( + shouldExcludeFileListing: Boolean + ): List = + if (shouldExcludeFileListing) { + map { it.copy(files = emptyList()) } + } else { + this + } } diff --git a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/RulerIntegrationTest.kt b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/RulerIntegrationTest.kt index faa2ff16..04658fcd 100644 --- a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/RulerIntegrationTest.kt +++ b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/RulerIntegrationTest.kt @@ -77,7 +77,7 @@ class RulerIntegrationTest { gradlew(task) val reportDir = projectDir.resolve("app/build/reports/ruler/$variant") - assertThat(reportDir.resolve("report.json").readText()).doesNotContain("owner") + assertThat(reportDir.resolve("report.json").readText()).contains("\"ownershipOverview\": null") } @ParameterizedTest diff --git a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt index 43d628e3..e4a70078 100644 --- a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt +++ b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt @@ -23,7 +23,12 @@ import com.spotify.ruler.models.AppReport import com.spotify.ruler.models.ComponentType import com.spotify.ruler.models.DynamicFeature import com.spotify.ruler.models.FileType +import com.spotify.ruler.models.Insights +import com.spotify.ruler.models.OwnedComponentSize +import com.spotify.ruler.models.ComponentOwner +import com.spotify.ruler.models.OwnershipOverview import com.spotify.ruler.models.ResourceType +import com.spotify.ruler.models.TypeInsights import com.spotify.ruler.plugin.dependency.DependencyComponent import com.spotify.ruler.plugin.models.AppInfo import com.spotify.ruler.plugin.ownership.OwnershipEntry @@ -42,6 +47,7 @@ class JsonReporterTest { DependencyComponent(":app", ComponentType.INTERNAL) to listOf( AppFile("com.spotify.MainActivity", FileType.CLASS, 100, 200), AppFile("/res/layout/activity_main.xml", FileType.RESOURCE, 150, 250, resourceType = ResourceType.LAYOUT), + AppFile("com.spotify.LoginActivity", FileType.CLASS, 50, 100, "login-team"), ), DependencyComponent(":lib", ComponentType.INTERNAL) to listOf( AppFile("/assets/license.html", FileType.ASSET, 500, 600), @@ -60,49 +66,167 @@ class JsonReporterTest { ), ) - private val ownershipEntries = listOf(OwnershipEntry(":app", "app-team"), OwnershipEntry("dynamic", "dynamic-team")) + private val ownershipEntries = listOf( + OwnershipEntry(":app", "app-team"), + OwnershipEntry("com.spotify.LoginActivity", "login-team"), + OwnershipEntry("dynamic", "dynamic-team") + ) private val ownershipInfo = OwnershipInfo(ownershipEntries, "default-team") @Test fun `JSON report is generated`(@TempDir targetDir: File) { - val reportFile = reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir) + val reportFile = reporter.generateReport(appInfo, components, features, ownershipInfo, false, targetDir) val report = Json.decodeFromString(reportFile.readText()) - val expected = AppReport("com.spotify.music", "1.2.3", "release", 750, 1050, listOf( - AppComponent(":lib", ComponentType.INTERNAL, 500, 600, listOf( - AppFile("/assets/license.html", FileType.ASSET, 500, 600, "default-team"), - ), "default-team"), - AppComponent(":app", ComponentType.INTERNAL, 250, 450, listOf( - AppFile("/res/layout/activity_main.xml", FileType.RESOURCE, 150, 250, "app-team", ResourceType.LAYOUT), - AppFile("com.spotify.MainActivity", FileType.CLASS, 100, 200, "app-team"), - ), "app-team"), - ), listOf( - DynamicFeature("dynamic", 300, 550, listOf( - AppFile("com.spotify.DynamicActivity", FileType.CLASS, 200, 300, "dynamic-team"), - AppFile( - "/res/layout/activity_dynamic.xml", - FileType.RESOURCE, - 100, - 250, - "dynamic-team", - ResourceType.LAYOUT + val expected = AppReport( + name = "com.spotify.music", + version = "1.2.3", + variant = "release", + components = listOf( + AppComponent( + name =":lib", + type = ComponentType.INTERNAL, + downloadSize = 500, + installSize = 600, + files = listOf( + AppFile("/assets/license.html", FileType.ASSET, 500, 600, "default-team"), + ), + owner = ComponentOwner(name = "default-team", ownedSize = OwnedComponentSize(downloadSize = 500, installSize = 600)) + ), + AppComponent( + name = ":app", + type = ComponentType.INTERNAL, + downloadSize = 300, + installSize = 550, + files = listOf( + AppFile("/res/layout/activity_main.xml", FileType.RESOURCE, 150, 250, "app-team", ResourceType.LAYOUT), + AppFile("com.spotify.MainActivity", FileType.CLASS, 100, 200, "app-team"), + AppFile("com.spotify.LoginActivity", FileType.CLASS, 50, 100, "login-team"), + ), + owner = ComponentOwner(name = "app-team", ownedSize = OwnedComponentSize(downloadSize = 250, installSize = 450)) + ), + ), + dynamicFeatures = listOf( + DynamicFeature( + name = "dynamic", + downloadSize = 300, + installSize = 550, + files = listOf( + AppFile("com.spotify.DynamicActivity", FileType.CLASS, 200, 300, "dynamic-team"), + AppFile("/res/layout/activity_dynamic.xml", FileType.RESOURCE, 100, 250, "dynamic-team", ResourceType.LAYOUT), + ), + owner = ComponentOwner(name = "dynamic-team", ownedSize = OwnedComponentSize(downloadSize = 300, installSize = 550)) + ), + ), + insights = Insights( + appDownloadSize = 800, + appInstallSize = 1150, + fileTypeInsights = mapOf( + FileType.CLASS to TypeInsights(downloadSize = 150, installSize = 300, count = 2), + FileType.RESOURCE to TypeInsights(downloadSize = 150, installSize = 250, count = 1), + FileType.ASSET to TypeInsights(downloadSize = 500, installSize = 600, count = 1) + ), + componentTypeInsights = mapOf( + ComponentType.INTERNAL to TypeInsights(downloadSize = 800, installSize = 1150, count = 2) ), - ), "dynamic-team"), - )) + resourcesTypeInsights = mapOf( + ResourceType.LAYOUT to TypeInsights(downloadSize = 150, installSize = 250, count = 1) + ), + ), + ownershipOverview = mapOf( + "default-team" to OwnershipOverview( + totalDownloadSize = 500, + totalInstallSize = 600, + filesCount = 1, + filesFromNotOwnedComponentsDownloadSize = 0, + filesFromNotOwnedComponentsInstallSize = 0, + ), + "app-team" to OwnershipOverview( + totalDownloadSize = 250, + totalInstallSize = 450, + filesCount = 2, + filesFromNotOwnedComponentsDownloadSize = 0, + filesFromNotOwnedComponentsInstallSize = 0, + ), + "login-team" to OwnershipOverview( + totalDownloadSize = 50, + totalInstallSize = 100, + filesCount = 1, + filesFromNotOwnedComponentsDownloadSize = 50, + filesFromNotOwnedComponentsInstallSize = 100, + ) + ) + ) assertThat(report).isEqualTo(expected) } + @Test + fun `JSON report is generated without file listing`(@TempDir targetDir: File) { + val components = mapOf( + DependencyComponent(":app", ComponentType.INTERNAL) to listOf( + AppFile("com.spotify.MainActivity", FileType.CLASS, 100, 200), + ), + ) + val features = mapOf( + "dynamic" to listOf( + AppFile("com.spotify.DynamicActivity", FileType.CLASS, 200, 300) + ) + ) + val reportFile = reporter.generateReport(appInfo, components, features, null, true, targetDir) + val report = Json.decodeFromString(reportFile.readText()) + + val expected = AppReport( + name = "com.spotify.music", + version = "1.2.3", + variant = "release", + components = listOf( + AppComponent( + name =":app", + type = ComponentType.INTERNAL, + downloadSize = 100, + installSize = 200, + files = emptyList(), + owner = null + ), + ), + dynamicFeatures = listOf( + DynamicFeature( + name = "dynamic", + downloadSize = 200, + installSize = 300, + files = emptyList(), + owner = null + ), + ), + insights = Insights( + appDownloadSize = 100, + appInstallSize = 200, + fileTypeInsights = mapOf( + FileType.CLASS to TypeInsights(downloadSize = 100, installSize = 200, count = 1), + ), + componentTypeInsights = mapOf( + ComponentType.INTERNAL to TypeInsights(downloadSize = 100, installSize = 200, count = 1) + ), + resourcesTypeInsights = emptyMap(), + ), + ownershipOverview = null + ) + + assertThat(expected).isEqualTo(report) + } + @Test fun `Ownership info is omitted from report if it is null`(@TempDir targetDir: File) { - val reportFile = reporter.generateReport(appInfo, components, features, null, targetDir) + val reportFile = reporter.generateReport(appInfo, components, features, null, false, targetDir) val reportContent = reportFile.readText() - assertThat(reportContent).doesNotContain("owner") + assertThat(reportContent).doesNotContain("\"owner\"") + assertThat(reportContent).contains("\"ownershipOverview\": null") } @Test fun `Existing reports are overwritten`(@TempDir targetDir: File) { repeat(2) { - reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir) + reporter.generateReport(appInfo, components, features, ownershipInfo, false, targetDir) } } } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt index 6feeb290..7cfd64e4 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt @@ -26,5 +26,5 @@ data class AppComponent( override val downloadSize: Long, override val installSize: Long, override val files: List, - override val owner: String? = null, + override val owner: ComponentOwner? = null, ) : FileContainer diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppReport.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppReport.kt index d4603e77..0e0717cc 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppReport.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppReport.kt @@ -24,8 +24,8 @@ data class AppReport( val name: String, val version: String, val variant: String, - override val downloadSize: Long, - override val installSize: Long, val components: List, val dynamicFeatures: List, -) : Measurable + val insights: Insights, + val ownershipOverview: Map?, +) diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentOwner.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentOwner.kt new file mode 100644 index 00000000..be4bfb33 --- /dev/null +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentOwner.kt @@ -0,0 +1,28 @@ +/* +* Copyright 2021 Spotify AB +* +* 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. +*/ + +package com.spotify.ruler.models + +import kotlinx.serialization.Serializable + +/** Representation of the relation between the owner of a component + * and the corresponding OwnedComponentSize + * */ +@Serializable +data class ComponentOwner( + val name: String, + val ownedSize: OwnedComponentSize, +) diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentType.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentType.kt index 1ba9155f..cf8e6bca 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentType.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ComponentType.kt @@ -17,7 +17,7 @@ package com.spotify.ruler.models /** Type of an [AppComponent]. */ -enum class ComponentType { - INTERNAL, - EXTERNAL, -} +enum class ComponentType(val label: String) { + INTERNAL("Internal"), + EXTERNAL("External"), +} \ No newline at end of file diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt index 4b27a730..2760bb59 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt @@ -25,5 +25,5 @@ data class DynamicFeature( override val downloadSize: Long, override val installSize: Long, override val files: List, - override val owner: String? = null, + override val owner: ComponentOwner? = null, ) : FileContainer diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt index 6b66876c..cbd50164 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt @@ -19,6 +19,6 @@ package com.spotify.ruler.models /** Piece of an app that can contain files. */ interface FileContainer : Measurable { val name: String - val owner: String? + val owner: ComponentOwner? val files: List } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileType.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileType.kt index 55a7795d..39355632 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileType.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileType.kt @@ -17,10 +17,10 @@ package com.spotify.ruler.models /** Type of an [AppFile]. */ -enum class FileType { - CLASS, - RESOURCE, - ASSET, - NATIVE_LIB, - OTHER, +enum class FileType(val label: String) { + CLASS("Classes"), + RESOURCE("Resources"), + ASSET("Assets"), + NATIVE_LIB("Native libraries"), + OTHER("Other"), } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Insights.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Insights.kt new file mode 100644 index 00000000..bdbe0f3f --- /dev/null +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Insights.kt @@ -0,0 +1,29 @@ +/* +* Copyright 2021 Spotify AB +* +* 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. +*/ + +package com.spotify.ruler.models + +import kotlinx.serialization.Serializable + +/** Global Insights about the app size */ +@Serializable +data class Insights( + val appDownloadSize: Long, + val appInstallSize: Long, + val fileTypeInsights: Map, + val resourcesTypeInsights: Map, + val componentTypeInsights: Map, +) diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Measurable.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Measurable.kt index 60c6769c..7d45884b 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Measurable.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/Measurable.kt @@ -33,7 +33,4 @@ interface Measurable { SizeType.DOWNLOAD -> downloadSize SizeType.INSTALL -> installSize } - - /** A mutable [Measurable] implementation. */ - data class Mutable(override var downloadSize: Long, override var installSize: Long) : Measurable } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnedComponentSize.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnedComponentSize.kt new file mode 100644 index 00000000..72a98b50 --- /dev/null +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnedComponentSize.kt @@ -0,0 +1,26 @@ +/* +* Copyright 2021 Spotify AB +* +* 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. +*/ + +package com.spotify.ruler.models + +import kotlinx.serialization.Serializable + +/** Total size of a component that belongs to a specific owner */ +@Serializable +data class OwnedComponentSize( + override val downloadSize: Long, + override val installSize: Long +) : Measurable diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnershipOverview.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnershipOverview.kt new file mode 100644 index 00000000..0dabadf1 --- /dev/null +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/OwnershipOverview.kt @@ -0,0 +1,38 @@ +/* +* Copyright 2021 Spotify AB +* +* 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. +*/ + +package com.spotify.ruler.models + +import kotlinx.serialization.Serializable + +/** Overview with global size details obout the components/files owned by some owner */ +@Serializable +data class OwnershipOverview( + val totalDownloadSize: Long, + val totalInstallSize: Long, + val filesCount: Long, + val filesFromNotOwnedComponentsDownloadSize: Long, + val filesFromNotOwnedComponentsInstallSize: Long, +) + +/** Helper and mutable class used in intermediate calculation of OwnershipOverview */ +data class MutableOwnershipOverview( + var totalDownloadSize: Long, + var totalInstallSize: Long, + var filesCount: Long, + var filesFromNotOwnedComponentsDownloadSize: Long, + var filesFromNotOwnedComponentsInstallSize: Long, +) \ No newline at end of file diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ResourceType.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ResourceType.kt index b97957de..3123ea0e 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ResourceType.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/ResourceType.kt @@ -17,11 +17,11 @@ package com.spotify.ruler.models /** Resource type for [FileType.RESOURCE]. */ -enum class ResourceType { - DRAWABLE, - LAYOUT, - FONT, - RAW, - VALUES, - OTHER, +enum class ResourceType(val label: String) { + DRAWABLE("Drawable"), + LAYOUT("Layout"), + RAW("Raw"), + VALUES("Values"), + FONT("Font"), + OTHER("Other"), } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/TypeInsights.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/TypeInsights.kt new file mode 100644 index 00000000..78942a8f --- /dev/null +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/TypeInsights.kt @@ -0,0 +1,34 @@ +/* +* Copyright 2021 Spotify AB +* +* 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. +*/ + +package com.spotify.ruler.models + +import kotlinx.serialization.Serializable + +/** Insights about some type of measurable item (e.g: a file) */ +@Serializable +data class TypeInsights( + val downloadSize: Long, + val installSize: Long, + val count: Long, +) + +/** Helper and mutable class used in intermediate calculation of TypeInsights */ +data class MutableTypeInsights( + var downloadSize: Long, + var installSize: Long, + var count: Long, +)