Skip to content

Commit

Permalink
Refactor Api extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfeo committed Jan 3, 2023
1 parent 93c1bb4 commit b3bcf0f
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.gabrielfeo.gradle.enterprise.api

import com.gabrielfeo.gradle.enterprise.api.internal.API_MAX_BUILDS
import com.gabrielfeo.gradle.enterprise.api.internal.operator.pagedUntilLastBuild
import com.gabrielfeo.gradle.enterprise.api.internal.operator.withGradleAttributes
import com.gabrielfeo.gradle.enterprise.api.model.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
import retrofit2.await

private const val API_MAX_BUILDS = 1000

/**
* Gets builds on demand from the API, in as many requests as necessary. It allows
* for queries of any size, as opposed to [GradleEnterpriseApi.getBuilds] which is limited by the
Expand All @@ -24,43 +25,17 @@ fun GradleEnterpriseApi.getBuildsFlow(
fromInstant: Long? = null,
fromBuild: String? = null,
): Flow<Build> = flow {
var lastBuildId: String? = null
while (true) {
val call = when (lastBuildId) {
null -> getBuilds(
since = since, sinceBuild = sinceBuild,
fromInstant = fromInstant, fromBuild = fromBuild,
maxBuilds = API_MAX_BUILDS,
)
else -> getBuilds(fromBuild = lastBuildId, maxBuilds = API_MAX_BUILDS)
}
val builds = call.await()
emitAll(builds.asFlow())
when {
builds.isEmpty() || builds.size < API_MAX_BUILDS -> break
else -> lastBuildId = builds.last().id
}
}
val firstBuilds = getBuilds(
since = since,
sinceBuild = sinceBuild,
fromInstant = fromInstant,
fromBuild = fromBuild,
maxBuilds = API_MAX_BUILDS,
).await()
val pagedBuilds = firstBuilds.asFlow().pagedUntilLastBuild(maxPerRequest = API_MAX_BUILDS)
emitAll(pagedBuilds)
}

/**
* Joins builds with their [GradleAttributes], which comes from a different endpoint
* ([GradleEnterpriseApi.getGradleAttributes]).
*
* Don't expect client-side filtering to be efficient. Does as many concurrent calls
* as it can, requesting attributes in an eager coroutine, in [scope].
*/
fun Flow<Build>.withGradleAttributes(
scope: CoroutineScope = GlobalScope,
): Flow<Pair<Build, GradleAttributes>> =
map { build ->
build to scope.async {
api.getGradleAttributes(build.id).await()
}
}.buffer(Int.MAX_VALUE).map { (build, attrs) ->
build to attrs.await()
}

/**
* Gets [GradleAttributes] of all builds from a given date. Queries [GradleEnterpriseApi.getBuilds]
* first, since it's the only endpoint providing a timeline of builds, then maps each to
Expand All @@ -69,7 +44,10 @@ fun Flow<Build>.withGradleAttributes(
* Don't expect client-side filtering to be efficient. Does as many concurrent calls
* as it can, requesting attributes in an eager coroutine, in [scope]. For other params,
* see [getBuildsFlow] and [GradleEnterpriseApi.getBuilds].
*
* @param scope CoroutineScope in which to create coroutines. Defaults to [GlobalScope].
*/
@OptIn(DelicateCoroutinesApi::class)
fun GradleEnterpriseApi.getGradleAttributesFlow(
since: Long = 0,
sinceBuild: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gabrielfeo.gradle.enterprise.api.internal

import com.gabrielfeo.gradle.enterprise.api.GradleEnterpriseApi

/**
* Undocumented max value of `/api/builds?maxBuilds`. Last checked in 2022.4.
*/
internal const val API_MAX_BUILDS = 1000
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.gabrielfeo.gradle.enterprise.api.internal.operator

import com.gabrielfeo.gradle.enterprise.api.*
import com.gabrielfeo.gradle.enterprise.api.internal.API_MAX_BUILDS
import com.gabrielfeo.gradle.enterprise.api.model.*
import kotlinx.coroutines.flow.*
import retrofit2.await

/**
* Emits all available builds starting from the upstream Flow builds until the last build available.
* Makes paged requests to the API using `fromBuild`, [maxPerRequest] at a time.
*/
internal fun Flow<Build>.pagedUntilLastBuild(
maxPerRequest: Int,
): Flow<Build> {
val firstBuilds = this
return flow {
var lastBuildId = ""
firstBuilds.collect {
lastBuildId = it.id
emit(it)
}
if (lastBuildId.isEmpty()) {
return@flow
} else while (true) {
val builds = api.getBuilds(fromBuild = lastBuildId, maxBuilds = maxPerRequest).await()
emitAll(builds.asFlow())
when {
builds.isEmpty() || builds.size < API_MAX_BUILDS -> break
else -> lastBuildId = builds.last().id
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.gabrielfeo.gradle.enterprise.api.internal.operator

import com.gabrielfeo.gradle.enterprise.api.*
import com.gabrielfeo.gradle.enterprise.api.model.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import retrofit2.await

/**
* Joins builds with their [GradleAttributes], which comes from a different endpoint
* ([GradleEnterpriseApi.getGradleAttributes]).
*
* Don't expect client-side filtering to be efficient. Does as many concurrent calls
* as it can, requesting attributes in an eager coroutine, in [scope].
*/
internal fun Flow<Build>.withGradleAttributes(
scope: CoroutineScope,
): Flow<Pair<Build, GradleAttributes>> =
map { build ->
build to scope.async {
api.getGradleAttributes(build.id).await()
}
}.buffer(Int.MAX_VALUE).map { (build, attrs) ->
build to attrs.await()
}

0 comments on commit b3bcf0f

Please sign in to comment.