diff --git a/.github/changelog_config.json b/.github/changelog_config.json index c53db87..2d8f82d 100644 --- a/.github/changelog_config.json +++ b/.github/changelog_config.json @@ -50,7 +50,7 @@ ], "label_extractor" : [ { - "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", + "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|feat!|breaking|api){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", "target" : "$1" } ] diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index 6489d6c..b517966 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -1,5 +1,6 @@ -org.gradle.jvmargs=-Xmx3g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -kotlin.daemon.jvmargs=-Xmx3g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=1g +# suppress inspection "UnusedProperty" for whole file +org.gradle.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=2g android.useAndroidX=true kotlin.code.style=official org.gradle.caching=true @@ -8,21 +9,37 @@ android.enableR8.fullMode=true org.gradle.configureondemand=true android.enableJetifier=false kotlin.incremental.usePreciseJavaTracking=true +org.gradle.configuration-cache.problems=warn android.nonTransitiveRClass=true android.experimental.enableSourceSetPathsMap=true android.experimental.cacheCompileLibResources=true kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true -kotlin.mpp.androidGradlePluginCompatibility.nowarn=true org.gradle.unsafe.configuration-cache=true kotlin.mpp.androidSourceSetLayoutVersion=2 android.disableResourceValidation=false -org.gradle.daemon=true +org.gradle.daemon=false android.nonFinalResIds=true -kotlin.native.ignoreIncorrectDependencies=true kotlinx.atomicfu.enableJvmIrTransformation=true -org.jetbrains.compose.experimental.macos.enabled=true -org.gradle.configuration-cache.problems=warn +android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true -org.gradle.console=plain +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true +# Do not garbage collect on timeout on native when appExtensions are used and app is in bacground +kotlin.native.binary.appStateTracking=enabled +# Lift main thread suspending function invocation restriction +kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none +# Native incremental compilation +kotlin.incremental.native=true +android.experimental.additionalArtifactsInModel=true +kotlin.apple.xcodeCompatibility.nowarn=true +# Enable new k/n GC +kotlin.native.binary.gc=cms +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +org.gradle.configuration-cache.parallel=true release=true +#kotlin.kmp.isolated-projects.support=enable +kotlin.incremental.wasm=true +#org.gradle.unsafe.isolated-projects=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf42574..fc20b40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 230acc8..a2e02d4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper @@ -39,10 +39,13 @@ jobs: run: cp ./README.md ./docs/README.md - name: Generate docs - run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache + run: ./gradlew dokkaGenerate + + - name: Make javadoc dir + run: mkdir -p ./docs/javadocs - name: Move docs to the parent docs dir - run: cp -r ./build/dokka/htmlMultiModule/ ./docs/javadocs/ + run: cp -r ./build/dokka/html/ ./docs/javadocs - name: Setup Pages uses: actions/configure-pages@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 518a010..272c959 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,17 +21,18 @@ jobs: environment: publishing steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: set up JDK + - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper @@ -41,6 +42,22 @@ jobs: with: xcode-version: latest + - name: Create local properties + env: + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + run: echo "$LOCAL_PROPERTIES" | base64 --decode > local.properties + + - name: Cache konan directory + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('*.gradle.kts', 'buildSrc/*') }} + restore-keys: | + ${{ runner.os }}-konan- + + - name: Assemble android sample + run: ./gradlew :app:assembleRelease + - name: Publish to sonatype env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} @@ -64,6 +81,7 @@ jobs: draft: true artifactErrorsFailBuild: true prerelease: false + artifacts: app/build/outputs/apk/release/* body: ${{steps.build_changelog.outputs.changelog}} tag: ${{ inputs.tag != '' && inputs.tag || github.ref_name }} env: diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index de0f9d2..ce82506 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,25 @@ ApiResult is [Railway Programming](https://proandroiddev.com/railway-oriented-pr functional error handling **on steroids**. -## Features +## Why use a library instead of try/catch? -* ApiResult is **lightweight**. The library creates no objects, makes no allocations or virtual function resolutions. - Most of the code is inlined. -* ApiResult offers 90+ operators covering most of possible use cases to turn your +Exceptions in Kotlin are **unchecked**. +Each time you call a function, it can throw and crash you app. +With ApiResult, you will never have this problem again. + +* ApiResult **forces** your code users to handle errors. Forget about unhandled exceptions and unexpected crashes. +* ApiResult is **lightweight**. The library creates no objects and has ~0 performance impact. +* Use 90+ operators covering most of possible use cases to turn your code from imperative and procedural to declarative and functional, which is more readable and extensible. -* ApiResult defines a contract that you can use in your code. No one will be able to obtain the result of a computation - without being forced to handle errors at compilation time. -* The library has 129 tests for 92% operator coverage. +* Core library has **no dependencies**. No need to worry about unexpected junk in your codebase. +* This isn't like Arrow, where with a monad you get a bunch of extra black magic. This framework focuses on **error handling** only. +* ApiResult is fully compatible with Exceptions and Coroutines. Just wrap a call and it will work. +* The library has 140+ tests for 92% operator coverage. Expect long-term support and stability. -## Preview +## How do I use it? ```kotlin -// wrap a result of a computation and expose the result +// wrap a result of any computation and expose the result class BillingRepository(private val api: RestApi) { suspend fun getSubscriptions() = ApiResult { @@ -38,18 +43,14 @@ class BillingRepository(private val api: RestApi) { } // -> ApiResult?> } -// ----- - // obtain and handle the result in the client code -val repo = BillingRepository( /* ... */) - fun onClickVerify() { - val state: SubscriptionState = repo.getSubscriptions() + val state: SubscriptionState = billingRepository.getSubscriptions() .errorOnNull() // map nulls to error states with compile-time safety .recover { emptyList() } // recover from some or all errors .require { securityRepository.isDeviceTrusted() } // conditionally fail the chain .mapValues(::SubscriptionModel) // map list items - .filter { it.isPurchased } // filter values + .filter { it.isPurchased } // filter .mapError { e -> BillingException(cause = e) } // map exceptions .then { validateSubscriptions(it) } // execute a computation and continue with its result, propagating errors .chain { updateGracePeriod(it) } // execute another computation, and if it fails, stop the chain @@ -92,7 +93,7 @@ Ready to try? Start with reading the [Quickstart Guide](https://opensource.respa ## License ``` - Copyright 2022-2024 Respawn Team and contributors + Copyright 2022-2025 Respawn Team and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle.kts b/build.gradle.kts index 3e4b967..eea104b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,28 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.MavenPublishBasePlugin import com.vanniktech.maven.publish.SonatypeHost import nl.littlerobots.vcu.plugin.versionCatalogUpdate import nl.littlerobots.vcu.plugin.versionSelector +import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradleSubplugin +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.detekt) alias(libs.plugins.gradleDoctor) alias(libs.plugins.version.catalog.update) - alias(libs.plugins.dokka) - alias(libs.plugins.atomicfu) + // alias(libs.plugins.atomicfu) alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.maven.publish) apply false + dokkaDocumentation // plugins already on a classpath (conventions) + // alias(libs.plugins.dokka) apply false // alias(libs.plugins.androidApplication) apply false // alias(libs.plugins.androidLibrary) apply false // alias(libs.plugins.kotlinMultiplatform) apply false @@ -31,10 +36,8 @@ allprojects { subprojects { plugins.withType().configureEach { the().apply { - enableIntrinsicRemember = true - enableNonSkippingGroupOptimization = true - enableStrongSkippingMode = true - stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") + featureFlags.addAll(ComposeFeatureFlag.OptimizeNonSkippingGroups) + stabilityConfigurationFiles.add(rootProject.layout.projectDirectory.file("stability_definitions.txt")) if (properties["enableComposeCompilerReports"] == "true") { val metricsDir = layout.buildDirectory.dir("compose_metrics") metricsDestination = metricsDir @@ -42,9 +45,16 @@ subprojects { } } } - afterEvaluate { - extensions.findByType()?.run { + plugins.withType { + the().apply { val isReleaseBuild = properties["release"]?.toString().toBoolean() + configure( + KotlinMultiplatform( + javadocJar = JavadocJar.Empty(), + sourcesJar = true, + androidVariantsToPublish = listOf("release"), + ) + ) publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, false) if (isReleaseBuild) signAllPublications() coordinates(Config.artifactId, name, Config.version(isReleaseBuild)) @@ -79,21 +89,6 @@ subprojects { useJUnitPlatform() filter { isFailOnNoMatchingTests = true } } - withType().configureEach { - compilerOptions { - jvmTarget.set(Config.jvmTarget) - freeCompilerArgs.apply { addAll(Config.jvmCompilerArgs) } - optIn.addAll(Config.optIns.map { "-opt-in=$it" }) - } - } - } - - if (name == "app") return@subprojects - - apply(plugin = rootProject.libs.plugins.dokka.id) - - dependencies { - dokkaPlugin(rootProject.libs.dokka.android) } } @@ -121,12 +116,10 @@ versionCatalogUpdate { } } -atomicfu { - dependenciesVersion = libs.versions.atomicfu.get() - transformJvm = false - jvmVariant = "VH" - transformJs = false -} +// atomicfu { +// dependenciesVersion = libs.versions.atomicfu.get() +// jvmVariant = "VH" +// } tasks { withType().configureEach { @@ -165,3 +158,14 @@ rootProject.plugins.withType().configureEach { yarnLockAutoReplace = true } } + +dependencies { + detektPlugins(rootProject.libs.detekt.formatting) + detektPlugins(rootProject.libs.detekt.compose) + detektPlugins(rootProject.libs.detekt.libraries) + projects.run { + listOf( + core, + ).forEach { dokka(it) } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a88ee7a..4909b97 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -6,4 +6,5 @@ plugins { dependencies { implementation(libs.android.gradle) implementation(libs.kotlin.gradle) + implementation(libs.dokka.gradle) } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 982c20e..0b897e7 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -16,7 +16,7 @@ object Config { const val artifactId = "$group.$artifact" const val majorRelease = 2 - const val minorRelease = 0 + const val minorRelease = 1 const val patch = 0 const val postfix = "" // include dash const val versionName = "$majorRelease.$minorRelease.$patch$postfix" @@ -25,10 +25,9 @@ object Config { const val licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" const val scmUrl = "https://github.com/respawn-app/ApiResult.git" const val name = "ApiResult" - const val description = """ -ApiResult is a Kotlin Multiplatform declarative error handling framework that is performant, easy to use and -feature-rich. - """ + + @Suppress("MaxLineLength") + const val description = """ApiResult is a Kotlin Multiplatform declarative error handling library. Just like Arrow's Either, but without the complexity.""" const val supportEmail = "hello@respawn.pro" const val vendorName = "Respawn Open Source Team" const val vendorId = "respawn-app" @@ -36,32 +35,40 @@ feature-rich. // kotlin + val jvmTarget = JvmTarget.JVM_11 + val javaVersion = JavaVersion.VERSION_11 + const val compileSdk = 35 + const val targetSdk = compileSdk + const val minSdk = 21 + const val appMinSdk = 26 + const val publishingVariant = "release" + val optIns = listOf( "kotlinx.coroutines.ExperimentalCoroutinesApi", "kotlinx.coroutines.FlowPreview", "kotlin.RequiresOptIn", "kotlin.experimental.ExperimentalTypeInference", - "kotlin.contracts.ExperimentalContracts" + "kotlin.uuid.ExperimentalUuidApi", + "kotlin.contracts.ExperimentalContracts", ) val compilerArgs = listOf( "-Xbackend-threads=0", // parallel IR compilation + "-Xexpect-actual-classes", + "-Xwasm-use-new-exception-proposal", + "-Xconsistent-data-class-copy-visibility", + "-Xsuppress-warning=NOTHING_TO_INLINE", + "-Xsuppress-warning=UNUSED_ANONYMOUS_PARAMETER", + "-Xwasm-debugger-custom-formatters" ) val jvmCompilerArgs = buildList { addAll(compilerArgs) add("-Xjvm-default=all") // enable all jvm optimizations add("-Xcontext-receivers") add("-Xstring-concat=inline") - addAll(optIns.map { "-opt-in=$it" }) + add("-Xlambdas=indy") + add("-Xjdk-release=${jvmTarget.target}") } - val jvmTarget = JvmTarget.JVM_11 - val javaVersion = JavaVersion.VERSION_11 - const val compileSdk = 34 - const val targetSdk = compileSdk - const val minSdk = 21 - const val appMinSdk = 26 - const val publishingVariant = "release" - // android const val namespace = artifactId const val testRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index 19ae47e..51de264 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -1,14 +1,14 @@ -@file:Suppress("MissingPackageDeclaration", "unused", "UndocumentedPublicFunction", "LongMethod") +@file:Suppress("MissingPackageDeclaration", "unused", "UndocumentedPublicFunction", "LongMethod", "UnusedImports") import org.gradle.api.Project import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.getting import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -@OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) +@OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class) fun Project.configureMultiplatform( ext: KotlinMultiplatformExtension, jvm: Boolean = true, @@ -22,12 +22,19 @@ fun Project.configureMultiplatform( windows: Boolean = true, wasmJs: Boolean = true, wasmWasi: Boolean = true, + explicitApi: Boolean = true, configure: KotlinHierarchyBuilder.Root.() -> Unit = {}, ) = ext.apply { val libs by versionCatalog - explicitApi() + if (explicitApi) explicitApi() applyDefaultHierarchyTemplate(configure) withSourcesJar(true) + compilerOptions { + extraWarnings.set(true) + freeCompilerArgs.addAll(Config.compilerArgs) + optIn.addAll(Config.optIns) + progressiveMode.set(true) + } if (linux) { linuxX64() @@ -54,10 +61,19 @@ fun Project.configureMultiplatform( } if (android) androidTarget { - publishLibraryVariants("release") + publishLibraryVariants(Config.publishingVariant) + compilerOptions { + jvmTarget.set(Config.jvmTarget) + freeCompilerArgs.addAll(Config.jvmCompilerArgs) + } } - if (jvm) jvm() + if (jvm) jvm { + compilerOptions { + jvmTarget.set(Config.jvmTarget) + freeCompilerArgs.addAll(Config.jvmCompilerArgs) + } + } sequence { if (iOs) { diff --git a/buildSrc/src/main/kotlin/Util.kt b/buildSrc/src/main/kotlin/Util.kt index 9e93d23..08154cc 100644 --- a/buildSrc/src/main/kotlin/Util.kt +++ b/buildSrc/src/main/kotlin/Util.kt @@ -5,7 +5,10 @@ import org.gradle.api.artifacts.VersionCatalog import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.getByType import org.gradle.plugin.use.PluginDependency +import java.io.File +import java.io.FileInputStream import java.util.Base64 +import java.util.Properties /** * Load version catalog for usage in places where it is not available yet with gradle 7.x. @@ -46,6 +49,17 @@ fun List.toJavaArrayString() = buildString { fun String.toBase64() = Base64.getEncoder().encodeToString(toByteArray()) +fun Project.localProperties() = lazy { + Properties().apply { + val file = File(rootProject.rootDir.absolutePath, "local.properties") + if (!file.exists()) { + println("w: Local.properties file does not exist. You may be missing some publishing keys") + return@apply + } + load(FileInputStream(file)) + } +} + fun stabilityLevel(version: String): Int { Config.stabilityLevels.forEachIndexed { index, postfix -> val regex = """.*[.\-]$postfix[.\-\d]*""".toRegex(RegexOption.IGNORE_CASE) @@ -53,8 +67,9 @@ fun stabilityLevel(version: String): Int { } return Config.stabilityLevels.size } - fun Config.version(isRelease: Boolean) = buildString { append(versionName) if (!isRelease) append("-SNAPSHOT") } + +fun Project.namespaceByPath() = "${Config.namespace}.${path.replace(":", ".").removePrefix(".")}" diff --git a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts new file mode 100644 index 0000000..e2905fa --- /dev/null +++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier + +plugins { + id("org.jetbrains.dokka") + // id("org.jetbrains.dokka-javadoc") +} + +val libs by versionCatalog + +dokka { + dokkaGeneratorIsolation = ClassLoaderIsolation() + moduleName = project.name + moduleVersion = project.version.toString() + pluginsConfiguration.html { + footerMessage = "© ${Config.vendorName}" + customAssets.from(rootDir.resolve("docs/static/icon-512-maskable.png")) + homepageLink = Config.url + } + dokkaPublications.configureEach { + suppressInheritedMembers = false + suppressObviousFunctions = true + } + dokkaSourceSets.configureEach { + reportUndocumented = false + enableJdkDocumentationLink = true + enableAndroidDocumentationLink = true + enableKotlinStdLibDocumentationLink = true + skipEmptyPackages = true + skipDeprecated = true + jdkVersion = Config.javaVersion.majorVersion.toInt() + documentedVisibilities(VisibilityModifier.Public) + } + // remoteUrl = Config.docsUrl +} + +dependencies { + dokkaPlugin(libs.requireLib("dokka-android")) +} diff --git a/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts new file mode 100644 index 0000000..36d0e87 --- /dev/null +++ b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("android") + id("com.android.library") +} + +kotlin { + explicitApi() +} + +android { + configureAndroidLibrary(this) + + kotlinOptions { + jvmTarget = Config.jvmTarget.target + } +} diff --git a/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts b/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts index 9beb915..c38b778 100644 --- a/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts +++ b/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts @@ -1,11 +1,9 @@ import org.gradle.kotlin.dsl.kotlin -import org.gradle.kotlin.dsl.signing import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("multiplatform") id("com.android.library") - signing } kotlin { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 951e098..88d3d93 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("pro.respawn.shared-library") alias(libs.plugins.maven.publish) + dokkaDocumentation // alias(libs.plugins.atomicfu) } @@ -10,5 +11,6 @@ android { dependencies { commonMainApi(libs.kotlin.coroutines.core) + jvmTestImplementation(libs.bundles.unittest) } diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt index 3a466b5..317cdbc 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt @@ -106,6 +106,7 @@ public value class ApiResult private constructor(@PublishedApi internal v else -> "ApiResult.Success: $value" } + @Suppress("UndocumentedPublicClass", "FunctionName") public companion object { /** @@ -132,6 +133,7 @@ public value class ApiResult private constructor(@PublishedApi internal v */ public inline operator fun invoke(value: T): ApiResult = when (value) { is Exception -> Error(e = value) + is Loading -> Loading() else -> Success(value) } @@ -187,7 +189,9 @@ public inline fun T.runResulting(block: T.() -> R): ApiResult = ApiRes public inline fun runResulting(block: () -> T): ApiResult = ApiResult(call = { block() }) /** - * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.value] + * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.value]. + * + * * [Loading] will result in [NotFinishedException] */ @Suppress("UNCHECKED_CAST") @@ -302,32 +306,32 @@ public inline infix fun ApiResult.onLoading(block: () -> Unit): ApiResult return this } -/** - * Makes [this] an [Error] if [predicate] returns false - * @see errorIf - */ -public inline fun ApiResult.errorUnless( - exception: () -> Exception = { ConditionNotSatisfiedException() }, - predicate: (T) -> Boolean, -): ApiResult = errorIf(exception) { !predicate(it) } - /** * Makes [this] an [Error] if [predicate] returns true * @see errorUnless */ public inline fun ApiResult.errorIf( - exception: () -> Exception = { ConditionNotSatisfiedException() }, + exception: (T) -> Exception = { ConditionNotSatisfiedException() }, predicate: (T) -> Boolean, ): ApiResult { contract { callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) callsInPlace(exception, InvocationKind.AT_MOST_ONCE) } - if (!isSuccess) return this - if (!predicate(value as T)) return this - return Error(e = exception()) + val value = orElse { return this } + if (!predicate(value)) return this + return Error(e = exception(value)) } +/** + * Makes [this] an [Error] if [predicate] returns false + * @see errorIf + */ +public inline fun ApiResult.errorUnless( + exception: (T) -> Exception = { ConditionNotSatisfiedException() }, + predicate: (T) -> Boolean, +): ApiResult = errorIf(exception) { !predicate(it) } + /** * Makes this result an [Error] if [this] result is [Loading] */ @@ -347,7 +351,9 @@ public inline fun ApiResult.errorOnLoading( /** * Alias for [errorOnNull] */ -public inline fun ApiResult?.requireNotNull(): ApiResult = errorOnNull() +public inline fun ApiResult?.requireNotNull( + message: () -> String = { "ApiResult value was null" } +): ApiResult = errorOnNull { IllegalArgumentException(message()) } /** * Alias for [orThrow] @@ -365,14 +371,17 @@ public inline infix fun ApiResult.map(block: (T) -> R): ApiResult { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - if (isSuccess) return Success(value = block(value as T)) + onSuccess { return ApiResult(block(it)) } return this as ApiResult } /** * Map the [Success] result using [transform], and if the result is not a success, return [default] */ -public inline fun ApiResult.mapOrDefault(default: (e: Exception) -> R, transform: (T) -> R): R { +public inline fun ApiResult.mapOrDefault( + default: (e: Exception) -> R, + transform: (T) -> R +): R { contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) callsInPlace(default, InvocationKind.AT_MOST_ONCE) @@ -405,7 +414,7 @@ public inline infix fun ApiResult.mapLoading(block: () -> R): ApiR * Change the exception of the [Error] response without affecting loading/success results */ public inline infix fun ApiResult.mapError( - block: (Exception) -> Exception + block: (Exception) -> Exception, ): ApiResult = mapError(block) /** @@ -413,7 +422,9 @@ public inline infix fun ApiResult.mapError( * [Loading] and [Success] are unaffected */ @JvmName("mapErrorTyped") -public inline infix fun ApiResult.mapError(block: (R) -> Exception): ApiResult { +public inline infix fun ApiResult.mapError( + block: (R) -> Exception +): ApiResult { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } @@ -424,12 +435,12 @@ public inline infix fun ApiResult.mapError(block: } /** - * Maps the error of the result, if present, to its cause, or self if cause is not available + * Maps the error of the result, if present, to its `cause`, or self if `cause` is not available */ public inline fun ApiResult.mapErrorToCause(): ApiResult = mapError { it.cause as? Exception ?: it } /** - * Unwrap an ApiResult> to be ApiResult + * Unwrap an `ApiResult>` to become `ApiResult` */ public inline fun ApiResult>.unwrap(): ApiResult = when (value) { is Error, is Loading -> this @@ -500,11 +511,13 @@ public inline infix fun ApiResult.recover( * calls [recover] catching and wrapping any exceptions thrown inside [block]. */ @JvmName("tryRecoverTyped") -public inline infix fun ApiResult.tryRecover(block: (T) -> R): ApiResult = - recover(another = { ApiResult(call = { block(it) }) }) +public inline infix fun ApiResult.tryRecover( + block: (T) -> R +): ApiResult = recover(another = { ApiResult(call = { block(it) }) }) /** * Calls [recover] catching and wrapping any exceptions thrown inside [block]. + * * See also the typed version of this function to recover from a specific exception type */ public inline infix fun ApiResult.tryRecover( @@ -545,7 +558,9 @@ public inline fun ApiResult.recoverIf( * Effectively, requires for another [ApiResult] to succeed before proceeding with this one. * @see [ApiResult.then] */ -public inline infix fun ApiResult.chain(another: (T) -> ApiResult<*>): ApiResult { +public inline infix fun ApiResult.chain( + another: (T) -> ApiResult<*> +): ApiResult { contract { callsInPlace(another, InvocationKind.AT_MOST_ONCE) } @@ -596,10 +611,10 @@ public inline infix fun ApiResult.flatMap(another: (T) -> ApiResult * using specified [message] if the [predicate] returns false. */ public inline fun ApiResult.require( - message: () -> String? = { null }, + message: (T) -> String? = { null }, predicate: (T) -> Boolean ): ApiResult = errorUnless( - exception = { ConditionNotSatisfiedException(message()) }, + exception = { ConditionNotSatisfiedException(message(it)) }, predicate = predicate ) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index d19df1b..a244f94 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -53,17 +53,19 @@ public inline infix fun , R> ApiResult.ifEmpty( /** * Makes [this] an [error] if the collection is empty. */ +@Suppress("ThrowingExceptionsWithoutMessageOrCause") // fp public inline fun > ApiResult.errorIfEmpty( exception: () -> Exception = { ConditionNotSatisfiedException("Collection was empty") }, -): ApiResult = errorIf(exception) { it.none() } +): ApiResult = errorIf({ exception() }) { it.none() } /** * Makes [this] an [error] if the collection is empty. */ @JvmName("sequenceErrorIfEmpty") +@Suppress("ThrowingExceptionsWithoutMessageOrCause") // fp public inline fun > ApiResult.errorIfEmpty( exception: () -> Exception = { ConditionNotSatisfiedException("Sequence was empty") }, -): ApiResult = errorIf(exception) { it.none() } +): ApiResult = errorIf({ exception() }) { it.none() } /** * Executes [ApiResult.map] on each value of the collection @@ -147,7 +149,7 @@ public inline fun Sequence>.filterSuccesses(): Sequence = ma * Filters all null values of results */ public inline fun Iterable>.filterNulls(): List> = - filter { !it.isSuccess || it.value != null }.mapResults { it!! } + asSequence().filterNulls().toList() /** * Filters all null values of results @@ -155,6 +157,8 @@ public inline fun Iterable>.filterNulls(): List Sequence>.filterNulls(): Sequence> = filter { !it.isSuccess || it.value != null }.mapResults { it!! } +// TODO: Not possible to provide `vararg` overloads due to https://youtrack.jetbrains.com/issue/KT-33565 + /** * Merges all results into a single [List], or if any has failed, returns [Error]. */ @@ -171,10 +175,7 @@ public inline fun merge(results: Iterable>): ApiResult> */ public inline fun ApiResult.merge( results: Iterable> -): ApiResult> = sequence { - yield(this@merge) - yieldAll(results) -}.asIterable().merge() +): ApiResult> = sequenceOf(this@merge).plus(results).asIterable().merge() /** * Returns a list of only successful values, discarding any errors diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt index d385ee9..c3e53eb 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt @@ -13,6 +13,6 @@ public class NotFinishedException( /** * Exception representing unsatisfied condition when using [errorIf] */ -public class ConditionNotSatisfiedException( +public open class ConditionNotSatisfiedException( message: String? = "ApiResult condition was not satisfied", ) : IllegalArgumentException(message) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt index 77a08f5..7ad1ed5 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt @@ -11,6 +11,7 @@ package pro.respawn.apiresult import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.catch @@ -46,7 +47,10 @@ public inline fun Flow.catchExceptions( public suspend inline fun SuspendResult( context: CoroutineContext = EmptyCoroutineContext, noinline block: suspend CoroutineScope.() -> T, -): ApiResult = withContext(context) { ApiResult(call = { supervisorScope(block) }) } +): ApiResult { + if (context === EmptyCoroutineContext) return ApiResult(call = { coroutineScope(block) }) + return ApiResult(call = { withContext(context, block) }) +} /** * Emits [ApiResult.Loading], then executes [call] and wraps it. diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt index fa7a931..3bb4e09 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt @@ -52,7 +52,6 @@ class ErrorOperatorTests : FreeSpec({ "given success value" - { val result = ApiResult.Error(e = exception) - "then isSuccess should be false" { result.isSuccess shouldBe false } @@ -212,7 +211,7 @@ class ErrorOperatorTests : FreeSpec({ row(ApiResult.Success(value)), row(ApiResult.Loading()), ) { other -> - "for value $other" - { + "for value $other" { result.flatMap { other } shouldBe result } } @@ -221,7 +220,7 @@ class ErrorOperatorTests : FreeSpec({ "then unit does not do anything" { result.unit() shouldBe result } - "then requireIs returns error" - { + "then requireIs returns error" { result.requireIs() shouldBe result } } diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt index 4a382ad..877a872 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt @@ -126,12 +126,10 @@ class LoadingOperatorTests : FreeSpec({ result.requireNotNull() shouldBe result } "then map returns the same value" { - shouldNotCall { - result.map { - it + 1 - markCalled() - } shouldBe result - } + result.map { + fail("Called map") + it + 1 + } shouldBe result } "then mapOrDefault returns new value" { val default = 0 diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt index b445778..a93c60e 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt @@ -3,7 +3,6 @@ package pro.respawn.apiresult.test import io.kotest.matchers.Matcher import io.kotest.matchers.MatcherResult import io.kotest.matchers.should -import io.kotest.matchers.shouldNot inline fun haveCalled(crossinline block: CallScope.(value: T) -> Unit) = Matcher { val scope = CallScope() @@ -28,8 +27,3 @@ inline infix fun T.shouldCall(crossinline block: CallScope.(value: T) -> Uni this should haveCalled(block) return this } - -inline infix fun T.shouldNotCall(crossinline block: CallScope.(value: T) -> Unit): T { - this shouldNot haveCalled(block) - return this -} diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt index 1410658..51168ad 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt @@ -112,6 +112,15 @@ class SuccessOperatorTests : FreeSpec({ result.errorIf { false }.isError shouldBe false result.errorIf { true }.isError shouldBe true } + "then errorIf provides the success value" { + result.shouldCall { + it.errorIf { + it shouldBe value + markCalled() + false + } + } + } "then errorUnless returns the opposite value" { result.errorUnless { true }.isError shouldBe false result.errorUnless { false }.isError shouldBe true diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt new file mode 100644 index 0000000..f594ece --- /dev/null +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt @@ -0,0 +1,75 @@ +package pro.respawn.apiresult.test + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.core.test.testCoroutineScheduler +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import pro.respawn.apiresult.SuspendResult +import pro.respawn.apiresult.exceptionOrNull +import kotlin.coroutines.EmptyCoroutineContext + +@OptIn(ExperimentalStdlibApi::class) +class SuspendResultTests : FreeSpec({ + coroutineTestScope = true + + val e = IllegalStateException("Failure") + + "Given empty context" - { + val ctx = EmptyCoroutineContext + "And SuspendResult that throws in a coroutine" - { + val result = SuspendResult(ctx) { + launch { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that throws in asyncs" - { + val result = SuspendResult(ctx) { + async { 42 } + async { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that nests coroutines" - { + val result = SuspendResult(ctx) { + launch { launch { throw e } } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that throws in another context" - { + val result = SuspendResult(ctx) { + launch(Dispatchers.Default) { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + } + "Given non-empty context" - { + val ctx = UnconfinedTestDispatcher(testCoroutineScheduler, "Ctx") + "And SuspendResult that throws in a coroutine" - { + val result = SuspendResult(ctx) { + launch { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that nests coroutines" - { + val result = SuspendResult(ctx) { + launch { launch { throw e } } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + } +}) diff --git a/gradle.properties b/gradle.properties index 09a2657..b183749 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,45 @@ -org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2g -kotlin.daemon.jvmargs=-Xmx6g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=2g +# suppress inspection "UnusedProperty" for whole file +org.gradle.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=2g android.useAndroidX=true kotlin.code.style=official org.gradle.caching=true org.gradle.parallel=true -org.gradle.daemon=true android.enableR8.fullMode=true org.gradle.configureondemand=true android.enableJetifier=false kotlin.incremental.usePreciseJavaTracking=true +org.gradle.configuration-cache.problems=warn android.nonTransitiveRClass=true android.experimental.enableSourceSetPathsMap=true android.experimental.cacheCompileLibResources=true kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true -kotlin.mpp.androidGradlePluginCompatibility.nowarn=true org.gradle.unsafe.configuration-cache=true kotlin.mpp.androidSourceSetLayoutVersion=2 android.disableResourceValidation=true +org.gradle.daemon=true android.nonFinalResIds=true kotlinx.atomicfu.enableJvmIrTransformation=true -org.gradle.configuration-cache.problems=warn +android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true +# Do not garbage collect on timeout on native when appExtensions are used and app is in bacground +kotlin.native.binary.appStateTracking=enabled +# Lift main thread suspending function invocation restriction +kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none +# Native incremental compilation +kotlin.incremental.native=true +android.experimental.additionalArtifactsInModel=true +kotlin.apple.xcodeCompatibility.nowarn=true +# Enable new k/n GC +kotlin.native.binary.gc=cms +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +org.gradle.configuration-cache.parallel=true release=false +#kotlin.kmp.isolated-projects.support=enable +kotlin.incremental.wasm=true +#org.gradle.unsafe.isolated-projects=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10180ad..cd8763a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,21 @@ [versions] -compose = "1.7.0-beta07" -compose-activity = "1.9.1" -compose-material3 = "1.3.0-beta05" -composeDetektPlugin = "1.3.0" -core-ktx = "1.13.1" -coroutines = "1.9.0-RC" -dependencyAnalysisPlugin = "1.32.0" -detekt = "1.23.6" -detektFormattingPlugin = "1.23.6" -dokka = "1.9.20" -gradleAndroid = "8.6.0-rc01" +atomicfu = "0.26.0" +compose = "1.7.6" +compose-activity = "1.10.0-rc01" +compose-material3 = "1.3.1" +composeDetektPlugin = "1.4.0" +core-ktx = "1.15.0" +coroutines = "1.10.1" +dependencyAnalysisPlugin = "2.6.1" +detekt = "1.23.7" +detektFormattingPlugin = "1.23.7" +dokka = "2.0.0" +gradleAndroid = "8.7.3" gradleDoctorPlugin = "0.10.0" -kotest = "5.9.1" -# @pin -kotlin = "2.0.10" -atomicfu = "0.25.0" -lifecycle = "2.8.4" -maven-publish-plugin = "0.29.0" +kotest = "6.0.0.M1" +kotlin = "2.1.0" +lifecycle = "2.8.7" +maven-publish-plugin = "0.30.0" turbine = "1.0.0" versionCatalogUpdatePlugin = "0.8.4" @@ -47,6 +46,7 @@ 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" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } [bundles] unittest = [ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c35211..a4b76b9 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 09523c0..cea7a79 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.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..f3b75f3 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum