diff --git a/build.gradle.kts b/build.gradle.kts index 45cd495..6ebdfa8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ -import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask import nl.littlerobots.vcu.plugin.versionCatalogUpdate +import nl.littlerobots.vcu.plugin.versionSelector import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -7,10 +7,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.detekt) alias(libs.plugins.gradleDoctor) - alias(libs.plugins.versions) alias(libs.plugins.version.catalog.update) alias(libs.plugins.dokka) - alias(libs.plugins.dependencyAnalysis) + alias(libs.plugins.atomicfu) // plugins already on a classpath (conventions) // alias(libs.plugins.androidApplication) apply false // alias(libs.plugins.androidLibrary) apply false @@ -61,12 +60,6 @@ doctor { } } -dependencyAnalysis { - structure { - ignoreKtx(true) - } -} - dependencies { detektPlugins(rootProject.libs.detekt.formatting) detektPlugins(rootProject.libs.detekt.compose) @@ -74,21 +67,23 @@ dependencies { } versionCatalogUpdate { - sortByKey.set(true) + sortByKey = true + + versionSelector { stabilityLevel(it.candidate.version) >= Config.minStabilityLevel } keep { - keepUnusedVersions.set(true) - keepUnusedLibraries.set(true) - keepUnusedPlugins.set(true) + keepUnusedVersions = true + keepUnusedLibraries = true + keepUnusedPlugins = true } } -// atomicfu { -// dependenciesVersion = libs.versions.kotlinx.atomicfu.get() -// transformJvm = false -// jvmVariant = "VH" -// transformJs = false -// } +atomicfu { + dependenciesVersion = libs.versions.kotlinx.atomicfu.get() + transformJvm = false + jvmVariant = "VH" + transformJs = false +} tasks { withType().configureEach { @@ -118,22 +113,6 @@ tasks { description = "Run detekt on whole project" autoCorrect = false } - - withType().configureEach { - outputFormatter = "json" - - fun stabilityLevel(version: String): Int { - Config.stabilityLevels.forEachIndexed { index, postfix -> - val regex = """.*[.\-]$postfix[.\-\d]*""".toRegex(RegexOption.IGNORE_CASE) - if (version.matches(regex)) return index - } - return Config.stabilityLevels.size - } - - rejectVersionIf { - stabilityLevel(currentVersion) > stabilityLevel(candidate.version) - } - } } extensions.findByType()?.run { diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index a3aa6e1..01605ad 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -17,7 +17,7 @@ object Config { const val majorRelease = 1 const val minorRelease = 0 - const val patch = 3 + const val patch = 4 const val postfix = "" const val versionName = "$majorRelease.$minorRelease.$patch$postfix" const val url = "https://github.com/respawn-app/ApiResult" @@ -68,6 +68,8 @@ feature-rich. const val consumerProguardFile = "consumer-rules.pro" val stabilityLevels = listOf("preview", "eap", "alpha", "beta", "m", "cr", "rc") + val minStabilityLevel = stabilityLevels.indexOf("beta") + object Detekt { const val configFile = "detekt.yml" diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index a5a336d..294ba00 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -4,7 +4,10 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.getting import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +@OptIn(ExperimentalWasmDsl::class) +@Suppress("LongParameterList", "CyclomaticComplexMethod") fun Project.configureMultiplatform( ext: KotlinMultiplatformExtension, jvm: Boolean = true, @@ -14,7 +17,8 @@ fun Project.configureMultiplatform( js: Boolean = true, tvOs: Boolean = true, macOs: Boolean = true, - watchOs: Boolean = true + watchOs: Boolean = true, + wasm: Boolean = true, ) = ext.apply { val libs by versionCatalog explicitApi() @@ -27,6 +31,13 @@ fun Project.configureMultiplatform( mingwX64() } + if (wasm) wasmJs { + moduleName = this@configureMultiplatform.name + nodejs() + browser() + binaries.library() + } + if (js) { js(IR) { browser() diff --git a/buildSrc/src/main/kotlin/Util.kt b/buildSrc/src/main/kotlin/Util.kt index 1b2eb1e..3956d07 100644 --- a/buildSrc/src/main/kotlin/Util.kt +++ b/buildSrc/src/main/kotlin/Util.kt @@ -60,3 +60,11 @@ val Project.localProperties load(FileInputStream(File(rootProject.rootDir, "local.properties"))) } } + +fun stabilityLevel(version: String): Int { + Config.stabilityLevels.forEachIndexed { index, postfix -> + val regex = """.*[.\-]$postfix[.\-\d]*""".toRegex(RegexOption.IGNORE_CASE) + if (version.matches(regex)) return index + } + return Config.stabilityLevels.size +} diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt index 90d54ca..a63dd12 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt @@ -627,3 +627,16 @@ public inline val T.asResult: ApiResult get() = ApiResult(this) * Alias for [map] that takes [this] as a parameter */ public inline infix fun ApiResult.apply(block: T.() -> R): ApiResult = map(block) + +/** + * @return if [this] result value is [R], then returns it. If not, returns an [ApiResult.Error] + */ +public inline fun ApiResult.requireIs( + exception: (T) -> Exception = { value -> + "Result value is of type ${value?.let { it::class.simpleName }} but expected ${R::class}" + .let(::ConditionNotSatisfiedException) + }, +): ApiResult = tryMap { value -> + if (value !is R) throw exception(value) + value +} diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index 78c4a87..3f6bed4 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -206,3 +206,37 @@ public inline fun Iterable>.firstSuccessOrThrow(): T = firstSuc * @see firstSuccessOrThrow */ public inline fun Iterable>.firstSuccessOrNull(): T? = firstSuccess().orNull() + +/** + * Maps each value in the collection, wrapping each map operation in an [ApiResult] + */ +public inline fun Sequence.mapResulting( + crossinline map: (T) -> R +): Sequence> = map { ApiResult { map(it) } } + +/** + * Accumulates all errors from this collection and splits them into two lists: + * - First is the [ApiResult.Success] results + * - Seconds is [ApiResult.Error] or errors produced by [ApiResult.Loading] (see [ApiResult.errorOnLoading] + */ +public fun Sequence>.accumulate(): Pair, List> { + val (success, other) = partition { it.isSuccess } + return Pair( + success.map { (it as Success).result }, + other.mapNotNull { it.errorOnLoading().exceptionOrNull() } + ) +} + +/** + * Maps each value in the collection, wrapping each map operation in an [ApiResult] + */ +public inline fun Iterable.mapResulting( + crossinline map: (T) -> R +): List> = map { ApiResult { map(it) } } + +/** + * Accumulates all errors from this collection and splits them into two lists: + * - First is the [ApiResult.Success] results + * - Seconds is [ApiResult.Error] or errors produced by [ApiResult.Loading] (see [ApiResult.errorOnLoading] + */ +public fun Iterable>.accumulate(): Pair, List> = asSequence().accumulate() diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt index dbff883..e7cb513 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt @@ -112,3 +112,13 @@ public inline fun Flow>.onEachResult( public inline fun Flow>.onEachSuccess( crossinline block: suspend (T) -> Unit ): Flow> = onEachResult(block) + +/** + * Maps this flow to an [ApiResult.Success] value, otherwise throws the resulting error + */ +public fun Flow>.orThrow(): Flow = map { it.orThrow() } + +/** + * Maps this flow to an [ApiResult.Success] value, otherwise the value is `null` + */ +public fun Flow>.orNull(): Flow = map { it.orNull() } diff --git a/gradle.properties b/gradle.properties index 1d8dda8..5a85fc9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,4 @@ android.disableResourceValidation=true android.nonFinalResIds=true kotlinx.atomicfu.enableJvmIrTransformation=true org.gradle.configuration-cache.problems=warn -nl.littlerobots.vcu.resolver=false +nl.littlerobots.vcu.resolver=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c46a5ac..1a69b3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,27 +1,26 @@ [versions] -compose = "1.5.4" -compose-activity = "1.8.2" -compose-compiler = "1.5.7" -compose-material3 = "1.2.0-beta01" +compose = "1.6.5" +compose-activity = "1.9.0-rc01" +compose-compiler = "1.5.11" +compose-material3 = "1.2.1" composeDetektPlugin = "1.3.0" -core-ktx = "1.12.0" -coroutines = "1.7.3" -dependencyAnalysisPlugin = "1.28.0" -detekt = "1.23.4" -detektFormattingPlugin = "1.23.4" -dokka = "1.9.10" -gradleAndroid = "8.3.0-alpha18" -gradleDoctorPlugin = "0.9.1" -kotest = "5.8.0" -kotest-plugin = "5.8.0" +core-ktx = "1.13.0-rc01" +coroutines = "1.8.1-Beta" +dependencyAnalysisPlugin = "1.31.0" +detekt = "1.23.6" +detektFormattingPlugin = "1.23.6" +dokka = "1.9.20" +gradleAndroid = "8.4.0-rc02" +gradleDoctorPlugin = "0.9.2" +kotest = "5.8.1" +kotest-plugin = "5.8.1" # @pin -kotlin = "1.9.22" +kotlin = "1.9.23" kotlinx-atomicfu = "0.23.1" -lifecycle = "2.6.2" -lifecycle-runtime-ktx = "2.6.2" +lifecycle = "2.7.0" +lifecycle-runtime-ktx = "2.7.0" turbine = "1.0.0" -versionCatalogUpdatePlugin = "0.8.2" -versionsPlugin = "0.50.0" +versionCatalogUpdatePlugin = "0.8.4" [libraries] android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradleAndroid" } @@ -39,7 +38,6 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } dokka-android = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } -gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versionsPlugin" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } kotest-junit = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } @@ -51,7 +49,6 @@ kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } -version-gradle = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versionsPlugin" } [bundles] unittest = [ @@ -71,4 +68,3 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } gradleDoctor = { id = "com.osacky.doctor", version.ref = "gradleDoctorPlugin" } kotest = { id = "io.kotest.multiplatform", version.ref = "kotest-plugin" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdatePlugin" } -versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e09..b82aa23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail