Skip to content

Conversation

@camka14
Copy link

@camka14 camka14 commented Feb 27, 2025

What does this PR do?

Adds a KMP SDK

Test Plan

Run KMPAndroid14Java17Test
Test OAuth2, cookies, and iOS file uploads using custom project in Android Studio

Related PRs and Issues

Have you read the Contributing Guidelines on issues?

yes

Summary by CodeRabbit

  • New Features

    • Added Kotlin Multiplatform (KMP) SDK generation with full project scaffolding (shared, Android, iOS), sample Android app, CI/publish workflows, and packaging templates.
    • Mobile OAuth2 web-auth flows with cookie handling, platform-specific WebAuth integrations, and utilities for URL parsing.
    • Rich client features: chunked file upload with progress, realtime subscriptions, comprehensive JSON/serialization helpers, and platform IO utilities.
  • Tests

    • Added end-to-end Kotlin tests and PHP test harness for KMP build scenarios.

@abnegate abnegate self-assigned this Mar 19, 2025
The socket was being created unnecessarily whenever a subscription was removed. This change ensures that the socket is only created when there are active channels.
}
}

rootProject.name = "Appwrite_KMP_SDK"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name this sdk-for-kmp

Comment on lines 47 to 61
} else {
engine {
config {
val trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, trustManagerFactory.trustManagers, SecureRandom())
}
sslSocketFactory(
sslContext.socketFactory,
trustManagerFactory.trustManagers[0] as X509TrustManager
)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this else block be skipped? Is it the same as a the default behaviour?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with certificates, but from my understanding this uses a PKIX algorithm instead of Trust All. I'm not sure if that's the correct approach, but I figured if there was an option to not run setSelfSigned then it should use a standard algorithm?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the else block, since the behavior when not setting self-signed will be the same with or without it 👌

$ret = $this->toPascalCase($method['responseModel']);

if ($this->hasGenericType($method['responseModel'], $spec)) {
if ($this->hasGenericType($method['responseModel'], $spec) && $withGeneric) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for skipping the generic?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for situations like this:
serializer = io.appwrite.models.Document.serializer(actualSerializer)

without the skip I would get serializer = io.appwrite.models.Document<T>.serializer(actualSerializer)

I just found how it's been done for the same situation:
{{sdk.namespace | caseDot}}.models.{{ method.responseModel | caseUcfirst }}

I can change it to match the original method and get rid of this change if you like that better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep let's do it so we can handle both generics and non-generics in one shot

private val appVersion by lazy {
try {
val properties = Properties()
properties.load(this.javaClass.getResourceAsStream("/version.properties"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe? Can we guarantee the file will always exist?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in a position to get the JVM stuff working properly right now so I was thinking of removing it entirely. I added it for a quick test that I was doing so I just did a port from the androidMain to jvmMain with chatGPT. I wasn't planning on this PR being a v1.0 or anything so I thought I'd add my JVM stuff to get the ball rolling. Let me know if you think JVM should just be removed in this case. It shouldn't be too difficult to add back since it's so similar to the android side.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, we can remove for now and add other platform support in phases 👌

Comment on lines 28 to 31
"origin" to "appwrite-jvm://app",
"content-type" to "application/json",
"origin" to "{{ spec.title | caseLower }}-jvm://app",
"user-agent" to "JVM/$appVersion, ${System.getProperty("java.version")}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the first origin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's changed in these php tests?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, I think they got caught in me updating the line endings. I'll revert them

'chmod +x tests/sdks/kmp/gradlew',
];
protected string $command =
'docker run --rm --platform linux/amd64 --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kmp alvrme/alpine-android:android-34-jdk17 sh -c "./gradlew :shared:testDebugUnitTest --stacktrace -q && cat shared/result.txt"';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to specify platform here? I think this image is multi-arch right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests for iOS and JVM too. We can also add them in .github/workflows/tests.yml so they run for each PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any luck here?

camka14 and others added 10 commits May 22, 2025 10:20
This commit refactors the OAuth2 authentication flow on iOS to use `ASWebAuthenticationSession` instead of `UIApplication.openURL`. This provides a more secure and user-friendly authentication experience.

Key changes:
- `WebAuthComponent.ios.kt`:
    - Replaced `UIApplication.openURL` with `ASWebAuthenticationSession`.
    - Added `PresentationContextProviderImpl` to provide a presentation anchor for the authentication session.
    - Updated `PendingAuth` to store the `ASWebAuthenticationSession`.
    - Improved error handling and cancellation.
    - Added `setCookieStorage` to allow passing the cookie storage instance.
    - Modified `handleIncomingCookie` to parse cookie details from the callback URL and store them using the provided `IosCookieStorage` and `NSUserDefaults`.
    - Added a `cleanup` method to cancel pending authentication sessions.
- `OAuth2Extensions.kt`:
    - Updated `createOAuth2Session` and `createOAuth2Token` to use the new `WebAuthComponent` and pass the `iosCookieStorage`.
    - Ensured cookies are correctly constructed and stored.
    - Improved error handling for missing authentication cookies.
sourceSets {
commonMain {
dependencies {
implementation("io.github.camka14.appwrite:sdk-for-kmp:0.1.0")
Copy link
Member

@abnegate abnegate Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's template the group name and version here

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Appwrite KMP SDK</string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's template the values here

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AppwriteKMPSDK" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's template

import okio.Path.Companion.toPath

actual class Client constructor(
private val context: Context,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts here? If users don't pass application context specifically this will be a memory leak if the client outlives the context

storage = dataStoreCookieStorage
}
install(WebSockets) {
pingInterval = 30.seconds
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this integrate with our custom ping 🤔

Comment on lines 171 to 176
val properties = mutableMapOf<Any?, Any?>(
NSHTTPCookieName to storedCookie.name,
NSHTTPCookieValue to storedCookie.value,
NSHTTPCookiePath to storedCookie.path,
NSHTTPCookieDomain to storedCookie.domain
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets also handle expires, max-age, samesite, httponly, and secure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth methods should be handled in the service template so they appear along other Account methods

}

result.getOrNull()?.let { callbackUrl ->
runBlocking {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid runBlocking here

private val appVersion by lazy {
try {
val properties = Properties()
properties.load(this.javaClass.getResourceAsStream("/version.properties"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, we can remove for now and add other platform support in phases 👌

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any luck here?

@KrisReal
Copy link

Any news when the sdk will be available?

@camka14
Copy link
Author

camka14 commented Nov 10, 2025

Any news when the sdk will be available?

I missed Jake's previous comments, for some reason I haven't been getting notifications from Github like I used to. I'll implement his suggestions and we'll see if anything else is needed.

- Updated OAuth2Extensions.kt.twig to use CoroutineScope for asynchronous operations during OAuth authentication.
- Removed AllCertsTrustManager.kt.twig, Client.jvm.kt.twig, HttpClient.kt.twig, WebAuthComponent.jvm.kt.twig, and related files as they are no longer needed.
- Cleaned up OAuth2Extensions.kt.twig by removing redundant code and improving error handling.
- Removed InputFile.jvm.kt.twig, JvmParsedUrl.kt.twig, and UrlParser.jvm.kt.twig as part of the cleanup.
- Updated test files for Android and KMP to reflect changes in the SDK structure and ensure compatibility with the latest changes.
@camka14
Copy link
Author

camka14 commented Nov 11, 2025

Any news when the sdk will be available?

In the mean time you can use my sdk here: https://github.com/camka14/sdk-for-kmp

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

Adds Kotlin Multiplatform (KMP) support to the SDK generator by introducing a new language class Appwrite\SDK\Language\KMP and a large set of KMP templates (project scaffolding, Gradle, Android/iOS apps, shared common code, models, services, serializers, utilities, cookie stores, WebAuth components, and tests). Updates Kotlin and Android language helpers to accept an optional withGeneric flag for return-type generation. Adds example.php routing for KMP SDK generation and multiple CI/workflow, resource, and test files for KMP projects.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to focus review on:

  • src/SDK/Language/KMP.php — new language implementation, Twig filters, JSON property assignment, return-type handling, and getFiles template mappings
  • templates/kmp/shared/** — BaseClient, service templates (request building, chunked upload), realtime service, and generated service method complexity
  • WebAuthComponent implementations and OAuth extension templates:
    • templates/kmp/shared/src/androidMain/.../WebAuthComponent.android.kt.twig
    • templates/kmp/shared/src/iosMain/.../WebAuthComponent.ios.kt.twig
    • templates/kmp/shared/src/*/extensions/OAuth2Extensions.kt.twig
  • Serializers and JSON utilities:
    • DynamicLookupSerializer, StringCollectionSerializer, JsonExtensions, and contextual serializer usage
  • Platform cookie/storage implementations and HTTP client config:
    • DataStoreCookieStorage, IosCookieStorage, createHttpClient for Android/iOS
  • Kotlin/Android generator signature changes:
    • src/SDK/Language/Kotlin.php and src/SDK/Language/Android.php (new withGeneric parameter and Twig filter updates)
  • New templates and CI/workflows:
    • templates/kmp/.github/workflows/* and publish/gradle wrapper files
  • Tests and examples:
    • tests/KMPAndroid14Java17Test.php and tests/languages/kmp/Tests.kt

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Feat kmp sdk' is vague and generic, using minimal descriptive information about the changeset without conveying the main purpose or scope of the KMP SDK implementation. Use a more descriptive title such as 'Add Kotlin Multiplatform (KMP) SDK support' or 'Implement KMP SDK generator with Android and iOS support' to clarify the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f11a2b and 132f8c8.

📒 Files selected for processing (1)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 44

♻️ Duplicate comments (1)
templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig (1)

12-45: Do not retain the passed-in Context; promote applicationContext.

We’re still caching whatever Context the caller supplies. If they hand us an Activity (very likely in sample apps), we leak it for the lifetime of the client—exactly the concern from the previous review. Capture context.applicationContext ?: context once and use that everywhere so we only retain the Application.

-actual class Client constructor(
-    private val context: Context,
+actual class Client constructor(
+    context: Context,
     endpoint: String = "{{spec.endpoint}}",
     endpointRealtime: String? = null,
     selfSigned: Boolean = false,
 ) : BaseClient<Client>(endpoint, endpointRealtime) {
+    private val appContext = context.applicationContext ?: context
     actual override val coroutineContext = Job() + Dispatchers.Default
 
     private val dataStoreManager = DataStoreManager(
         PreferenceDataStoreFactory.createWithPath (
-            produceFile = { context.filesDir.resolve("appwriteCookies.preferences_pb").absolutePath.toPath() }
+            produceFile = { appContext.filesDir.resolve("appwriteCookies.preferences_pb").absolutePath.toPath() }
         ))
     val dataStoreCookieStorage = DataStoreCookieStorage(dataStoreManager)
 
     private val appVersion by lazy {
         try {
-            val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
+            val pInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
             return@lazy pInfo.versionName
         } catch (e: PackageManager.NameNotFoundException) {
             e.printStackTrace()
             return@lazy ""
         }
@@
         httpClient = createHttpClient(selfSigned, dataStoreCookieStorage)
         headers = mutableMapOf(
               "content-type" to "application/json",
-              "origin" to "{{ spec.title | caseLower }}-android://${context.packageName}",
-              "user-agent" to "${context.packageName}/${appVersion}, ${System.getProperty("http.agent")}",
+              "origin" to "{{ spec.title | caseLower }}-android://${appContext.packageName}",
+              "user-agent" to "${appContext.packageName}/${appVersion}, ${System.getProperty("http.agent")}",
               "x-sdk-name" to "{{ sdk.name }}",
               "x-sdk-platform" to "{{ sdk.platform }}",
               "x-sdk-language" to "{{ language.name | caseLower }}",
🧹 Nitpick comments (17)
templates/kmp/shared/src/commonMain/kotlin/io/package/coroutines/Callback.kt.twig (2)

8-10: Add KDoc documentation to the public Callback interface.

This is a public API in the SDK and lacks documentation describing the callback contract. Consider adding KDoc to clarify behavior.

/**
 * Callback interface for handling asynchronous operation results.
 *
 * @param T The type of the result value on success.
 */
interface Callback<T> {
    /**
     * Called when the operation completes.
     *
     * Exactly one of [result] or [error] will be non-null:
     * - If successful, [result] contains the value and [error] is null.
     * - If failed, [error] contains the exception and [result] is null.
     *
     * @param result The result value, or null if the operation failed.
     * @param error The exception if the operation failed, or null if successful.
     */
    fun onComplete(result: T?, error: Throwable?)
}

12-19: CoroutineCallback bridge is correctly implemented.

The class properly implements Continuation<T> and correctly unwraps Result<T> using getOrNull() and exceptionOrNull(), ensuring the callback receives exactly one of value or error as non-null. The use of @JvmOverloads and Dispatchers.Default as the default context is idiomatic and appropriate for Kotlin/Java interop.

Consider adding KDoc for consistency and to document the bridge pattern.

/**
 * Bridges Kotlin coroutine [Continuation] to a [Callback]-based API.
 *
 * This adapter allows coroutine-based code to drive callback-based APIs
 * by converting [Result<T>] completions into [Callback] invocations.
 *
 * @param T The type of the result value.
 * @param callback The callback to invoke upon completion.
 * @param context The coroutine context for this continuation. Defaults to [Dispatchers.Default].
 */
class CoroutineCallback<T> @JvmOverloads constructor(
    private val callback: Callback<T>,
    override val context: CoroutineContext = Dispatchers.Default
) : Continuation<T> {
    override fun resumeWith(result: Result<T>) {
        callback.onComplete(result.getOrNull(), result.exceptionOrNull())
    }
}
templates/kmp/androidApp/src/main/res/values/themes.xml.twig (1)

5-11: Consider making the color scheme more generic for SDK scaffolding.

The hardcoded purple and teal color scheme is typical for demo applications but may not be suitable for production SDK scaffolding. SDK consumers typically want to define their own branding colors.

Consider either:

  • Using more neutral/generic colors (e.g., grays)
  • Making the color scheme configurable via template variables
  • Adding comments in the generated code encouraging developers to customize the colors
templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/TypeExtensions.kt.twig (1)

6-9: Replace force-unwrap operator for better error handling.

Line 8 uses the !! operator, which will throw an NPE with no context if classifier is null. While this should not happen for reified types in practice, using a safer alternative provides better error messages during debugging.

Apply this diff to use requireNotNull instead:

 inline fun <reified T : Any> classOf(): KClass<T> {
     @Suppress("UNCHECKED_CAST")
-    return (typeOf<T>().classifier!! as KClass<T>)
+    return requireNotNull(typeOf<T>().classifier as? KClass<T>) {
+        "Unable to obtain KClass for type ${typeOf<T>()}"
+    }
 }
templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/IOExtensions.kt.twig (1)

9-10: Replace force-unwrap for safer file size access.

Line 10 uses the !! operator on size, which will throw an NPE if the size is null (e.g., for certain file types or in error conditions). While Okio typically provides size for regular files, using a safer approach prevents runtime crashes.

Apply this diff:

 fun readFileSize(filePath: String): Long =
-    FileSystem.SYSTEM.metadata(filePath.toPath()).size!!
+    FileSystem.SYSTEM.metadata(filePath.toPath()).size 
+        ?: error("Unable to determine file size for: $filePath")

Or if you expect size to always be present for valid files:

 fun readFileSize(filePath: String): Long =
-    FileSystem.SYSTEM.metadata(filePath.toPath()).size!!
+    requireNotNull(FileSystem.SYSTEM.metadata(filePath.toPath()).size) {
+        "File size is unavailable for: $filePath"
+    }
templates/kmp/shared/src/commonMain/kotlin/io/package/webInterface/ParsedUrl.kt.twig (1)

3-5: Consider removing override keyword for convention.

While technically valid since interfaces implicitly inherit from Any, using the override keyword on toString() in an interface declaration is unconventional in Kotlin. The standard practice is to declare fun toString(): String without the override modifier.

Apply this diff for more idiomatic Kotlin:

 interface ParsedUrl {
-    override fun toString(): String
+    fun toString(): String
 }
templates/kmp/androidApp/build.gradle.kts.twig (1)

31-37: Consider upgrading to Java 11 or 17.

While Java 1.8 is still widely supported, modern Android development typically uses Java 11 or Java 17 for better language features and performance. Since this is a template for new SDK projects, consider defaulting to a more recent Java version.

Apply this diff to upgrade to Java 17:

     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_1_8
-        targetCompatibility = JavaVersion.VERSION_1_8
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
     }
     kotlinOptions {
-        jvmTarget = "1.8"
+        jvmTarget = "17"
     }
templates/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig (2)

27-35: Consider using a coroutine scope instead of runBlocking.

The runBlocking call blocks the Firebase messaging thread, which can impact performance. Consider using GlobalScope.launch or a proper CoroutineScope for better concurrency handling.

Apply this diff:

-        runBlocking {
+        GlobalScope.launch {
             if (targetId == null) {
                 val target = account!!.createPushTarget(ID.unique(), token)
 
                 prefs.edit().putString("targetId", target.id).apply()
             } else {
                 account!!.updatePushTarget(targetId, token)
             }
         }

Additionally, add the import:

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

29-33: Prefer safe call or let over force unwrap (!!).

After the null check at Line 21, account is guaranteed non-null, but the !! operator is considered a code smell in Kotlin.

Apply this diff:

         runBlocking {
+            val acc = account ?: return@runBlocking
             if (targetId == null) {
-                val target = account!!.createPushTarget(ID.unique(), token)
+                val target = acc.createPushTarget(ID.unique(), token)
 
                 prefs.edit().putString("targetId", target.id).apply()
             } else {
-                account!!.updatePushTarget(targetId, token)
+                acc.updatePushTarget(targetId, token)
             }
         }
templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig (1)

35-43: Consider extracting SharedPreferences helper.

The SharedPreferences access pattern is repeated multiple times. For improved maintainability, consider extracting a helper function or property.

Example refactor:

private val examplePrefs
    get() = context?.getSharedPreferences("example", Context.MODE_PRIVATE)

// Then use:
binding.login.setOnClickListener{
    viewModel.onLogin(
        binding.email.text.toString(),
        binding.password.text.toString(),
        examplePrefs?.getString("fcmToken", null) ?: ""
    )
}

Also applies to: 73-81

templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreManager.kt.twig (1)

14-23: Consider adding error handling for DataStore operations.

Both read() and write() operations can fail due to I/O errors, but no exception handling is present. Consider wrapping these operations in try-catch blocks or documenting that callers must handle potential exceptions.

Example with error handling:

 suspend fun read(): String? {
-    val preferences = dataStore.data.first()
-    return preferences[key]
+    return try {
+        val preferences = dataStore.data.first()
+        preferences[key]
+    } catch (e: Exception) {
+        null // or log and rethrow
+    }
 }

 suspend fun write(value: String) {
-    dataStore.edit { preferences ->
-        preferences[key] = value
+    try {
+        dataStore.edit { preferences ->
+            preferences[key] = value
+        }
+    } catch (e: Exception) {
+        // log error
+        throw e
     }
 }
templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/DynamicLookupSerializer.kt.twig (2)

67-82: Recursive deserialization lacks depth protection.

The recursive calls in deserializeJsonElement (lines 79) and the recursive nature of the serializer (line 50) have no depth limit, which could lead to stack overflow with deeply nested structures.

Consider adding a depth counter:

private fun deserializeJsonElement(element: JsonElement, depth: Int = 0): Any? {
    require(depth < 1000) { "Maximum nesting depth exceeded" }
    return when (element) {
        // ... existing cases ...
        is JsonArray -> element.map { deserializeJsonElement(it, depth + 1) }
        // ... etc
    }
}

58-58: Fallback to toString() may lose type information.

For types not explicitly handled, serialization falls back to encoding the toString() representation. This means custom objects will be serialized as strings and cannot be deserialized back to their original type, potentially causing data loss.

Consider either:

  • Documenting this limitation clearly
  • Throwing an exception for unsupported types to fail fast
  • Supporting additional common types (ByteArray, Enum, etc.)
templates/kmp/shared/src/commonMain/kotlin/io/package/models/InputFile.kt.twig (1)

9-15: Lateinit properties risk uninitialized access.

All properties use lateinit, which will throw UninitializedPropertyAccessException if accessed before initialization. Since the constructor is private and only factory methods should be used, consider making these properties non-nullable with proper initialization, or add validation that all required properties are set.

Alternative approach with constructor parameters:

class InputFile private constructor(
    val path: String = "",
    val filename: String,
    val sourceType: String,
    val mimeType: String,
    @Contextual val data: Buffer = Buffer()
)
src/SDK/Language/KMP.php (1)

44-44: Remove unused $spec parameter.

The $spec parameter is not used in the method body and should be removed to clean up the signature.

Apply this diff:

-    protected function getJsonPropertyAssignment(array $property, array $spec): string
+    protected function getJsonPropertyAssignment(array $property): string

Also update the filter registration on line 35:

-        $filters[] = new TwigFilter('propertyAssignmentKmp', function (array $property, array $spec) {
-            return $this->getJsonPropertyAssignment($property, $spec);
+        $filters[] = new TwigFilter('propertyAssignmentKmp', function (array $property) {
+            return $this->getJsonPropertyAssignment($property);

Based on learnings

templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig (1)

380-407: Duplicate empty body check is redundant.

Line 382 checks if the body is empty, but lines 386-393 contain another nested empty body check. The outer check makes the inner one at line 386 unreachable.

Apply this diff to remove the duplicate check:

             else -> {
                 val body = response.bodyAsText()
-                if (body.isEmpty()) {
-                    true as T
-                } else {
-                    when {
-                        body.isEmpty() -> when (responseType) {
-                            String::class -> "" as T
-                            Boolean::class -> true as T
-                            else -> throw {{ spec.title | caseUcfirst }}Exception(
-                                "Empty response body",
-                                response.status.value
-                            )
-                        }
-
-                        responseType == String::class -> body as T
-                        else -> try {
-                            serializer?.let { json.decodeFromString(it, body) }
-                                ?: json.decodeFromString(responseType.serializer(), body)
-                        } catch (e: Exception) {
-                            throw {{ spec.title | caseUcfirst }}Exception(
-                                "Failed to parse JSON response: ${e.message}",
-                                response.status.value,
-                                response = body
-                            )
-                        }
+                when {
+                    body.isEmpty() -> when (responseType) {
+                        String::class -> "" as T
+                        Boolean::class -> true as T
+                        else -> throw {{ spec.title | caseUcfirst }}Exception(
+                            "Empty response body",
+                            response.status.value
+                        )
                     }
+
+                    responseType == String::class -> body as T
+                    else -> try {
+                        serializer?.let { json.decodeFromString(it, body) }
+                            ?: json.decodeFromString(responseType.serializer(), body)
+                    } catch (e: Exception) {
+                        throw {{ spec.title | caseUcfirst }}Exception(
+                            "Failed to parse JSON response: ${e.message}",
+                            response.status.value,
+                            response = body
+                        )
+                    }
                 }
             }
templates/kmp/shared/src/commonMain/kotlin/io/package/services/Service.kt.twig (1)

35-35: Inconsistent newline handling in method descriptions.

Line 35 removes newlines entirely from method descriptions (replace({"\n": "", "\r": ""})), while line 27 replaces them with spaces. This creates inconsistent formatting where method descriptions become hard to read as run-on text.

Apply this diff to use spaces consistently:

-     * {{ method.description | raw | replace({"\n": "", "\r": ""}) }}
+     * {{ method.description | raw | replace({"\n": " ", "\r": " "}) }}

Also apply to line 160 which has the same pattern.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a7a3b8 and 8bfffce.

⛔ Files ignored due to path filters (2)
  • composer.lock is excluded by !**/*.lock
  • templates/kmp/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (84)
  • example.php (2 hunks)
  • src/SDK/Language/Android.php (1 hunks)
  • src/SDK/Language/KMP.php (1 hunks)
  • src/SDK/Language/Kotlin.php (3 hunks)
  • templates/kmp/.github/workflows/autoclose.yml (1 hunks)
  • templates/kmp/.github/workflows/publish.yml (1 hunks)
  • templates/kmp/.gitignore (1 hunks)
  • templates/kmp/CHANGELOG.md.twig (1 hunks)
  • templates/kmp/LICENSE.md.twig (1 hunks)
  • templates/kmp/README.md.twig (1 hunks)
  • templates/kmp/androidApp/build.gradle.kts.twig (1 hunks)
  • templates/kmp/androidApp/src/main/AndroidManifest.xml.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/MainActivity.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/utils/Client.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig (1 hunks)
  • templates/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/layout/activity_main.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/layout/fragment_account.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/values/colors.xml (1 hunks)
  • templates/kmp/androidApp/src/main/res/values/strings.xml.twig (1 hunks)
  • templates/kmp/androidApp/src/main/res/values/themes.xml.twig (1 hunks)
  • templates/kmp/build.gradle.kts.twig (1 hunks)
  • templates/kmp/gradle.properties (1 hunks)
  • templates/kmp/gradle/libs.versions.toml (1 hunks)
  • templates/kmp/gradle/wrapper/gradle-wrapper.properties (1 hunks)
  • templates/kmp/gradlew (1 hunks)
  • templates/kmp/gradlew.bat (1 hunks)
  • templates/kmp/settings.gradle.kts (1 hunks)
  • templates/kmp/shared/build.gradle.kts.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/HttpClientConfig.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/KeepAliveService.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/WebAuthComponent.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/SerializableCookie.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreCookieStorage.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreManager.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/models/InputFile.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/views/CallbackActivity.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/webInterface/AndroidParsedUrl.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/webInterface/UrlParser.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Client.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/ID.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Permission.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Role.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Service.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/WebAuthComponent.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/coroutines/Callback.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/enums/Enum.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/exceptions/Exception.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/CollectionExtensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/IOExtensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/JsonExtensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/TypeExtensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/models/InputFile.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/models/Model.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/models/RealtimeModels.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/models/UploadProgress.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/DynamicLookupSerializer.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSeriailizer.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/services/Realtime.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/services/Service.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/webInterface/ParsedUrl.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/webInterface/UrlParser.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/Client.ios.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/HttpClientConfig.ios.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/cookies/IosCookieStorage.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/models/InputFile.ios.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/webInterface/IosParsedUrl.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/webInterface/UrlParser.ios.kt.twig (1 hunks)
  • tests/Android14Java11Test.php (1 hunks)
  • tests/Android14Java8Test.php (1 hunks)
  • tests/KMPAndroid14Java17Test.php (1 hunks)
  • tests/languages/kmp/Tests.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
example.php (2)
src/SDK/SDK.php (1)
  • SDK (21-941)
src/SDK/Language/KMP.php (1)
  • KMP (7-636)
tests/KMPAndroid14Java17Test.php (1)
tests/Base.php (1)
  • Base (17-326)
src/SDK/Language/Kotlin.php (3)
src/SDK/Language/KMP.php (1)
  • getReturnType (101-107)
src/SDK/Language/Apple.php (1)
  • getReturnType (494-500)
src/SDK/Language/Android.php (1)
  • getReturnType (347-353)
src/SDK/Language/KMP.php (3)
src/SDK/SDK.php (2)
  • SDK (21-941)
  • hasWebAuth (868-877)
src/SDK/Language.php (2)
  • Language (5-279)
  • toPascalCase (115-118)
src/SDK/Language/Kotlin.php (6)
  • Kotlin (8-651)
  • getName (13-16)
  • getFilters (475-500)
  • getPropertyType (536-560)
  • getReturnType (502-526)
  • getFiles (302-473)
src/SDK/Language/Android.php (4)
src/SDK/Language/KMP.php (1)
  • getReturnType (101-107)
src/SDK/Language/Kotlin.php (1)
  • getReturnType (502-526)
src/SDK/Language/Swift.php (1)
  • getReturnType (555-579)
src/SDK/Language/Apple.php (1)
  • getReturnType (494-500)
tests/Android14Java8Test.php (1)
tests/Base.php (1)
  • Base (17-326)
🪛 detekt (1.23.8)
tests/languages/kmp/Tests.kt

[warning] 326-326: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🪛 PHPMD (2.15.0)
src/SDK/Language/KMP.php

44-44: Avoid unused parameters such as '$spec'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (36)
templates/kmp/androidApp/src/main/res/values/strings.xml.twig (1)

1-3: Well-formed Android string resource template.

The template is properly structured, follows Android naming conventions for string resources, and appropriately uses the Twig variable for dynamic app name generation. The identifier app_name and resource format align with Android best practices.

templates/kmp/shared/src/commonMain/kotlin/io/package/coroutines/Callback.kt.twig (1)

1-6: Package and imports look good.

Template variable in package name is appropriate for the code generator context, and all imports are necessary and correctly ordered.

templates/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml (1)

1-170: Launcher background template resource looks good.

The vector drawable is syntactically correct and uses standard Android attributes. The grid-based design is an unconventional but valid template launcher background for the Android app module.

templates/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (1)

1-5: Adaptive launcher icon template is correct.

The resource follows standard Android adaptive icon structure for API 26+ and correctly references the background and foreground drawables.

templates/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (1)

1-5: Standard adaptive launcher icon template.

The structure is correct and follows Android best practices. The identical definition shared with ic_launcher_round.xml is intentional—both can reference the same drawables when the design intent is consistent across icon variants.

templates/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml (1)

1-30: Launcher foreground template resource is properly structured.

The vector drawable correctly uses AAPT for gradient fills and includes valid SVG path data. The design appears to be a stylized mascot suitable for a template launcher icon.

templates/kmp/shared/src/commonMain/kotlin/io/package/enums/Enum.kt.twig (3)

1-4: LGTM! Package and imports are correct.

The package declaration and serialization imports are properly structured for KMP enum generation.


6-7: LGTM! Enum declaration follows Kotlin best practices.

The enum class is properly annotated with @Serializable and uses a value parameter to store the serialized string value, which is the standard pattern for string-based enums.


15-15: LGTM! toString() override is appropriate.

Overriding toString() to return the value property is the standard pattern for string-based enums, ensuring consistent serialization and string representation.

tests/Android14Java11Test.php (1)

1-38: LGTM! Formatting normalization.

The changes are purely whitespace/formatting adjustments with no functional modifications.

templates/kmp/LICENSE.md.twig (1)

1-1: LGTM! Simple and correct license template.

The template appropriately uses the raw filter to output license content without HTML escaping.

templates/kmp/.gitignore (1)

1-10: LGTM! Comprehensive ignore rules for KMP projects.

The .gitignore appropriately covers IDE files, build artifacts, platform-specific files, and Gradle cache directories.

tests/Android14Java8Test.php (1)

1-38: LGTM! Formatting normalization.

The changes are purely whitespace/formatting adjustments with no functional modifications.

templates/kmp/.github/workflows/autoclose.yml (1)

1-11: LGTM! Safe use of pull_request_target.

The workflow appropriately delegates to a reusable workflow from the official Appwrite organization without executing untrusted code from external PRs.

templates/kmp/gradle.properties (1)

1-12: LGTM! Well-configured Gradle properties for KMP.

The configuration appropriately sets memory limits, enables caching for performance, and includes necessary KMP-specific settings for iOS interoperability.

templates/kmp/.github/workflows/publish.yml (1)

11-34: LGTM! Environment setup and build steps are well-structured.

The workflow properly sets up JDK 17, decodes and stores the GPG signing key securely, and executes the build and documentation generation steps.

templates/kmp/shared/src/androidMain/kotlin/io/package/KeepAliveService.kt.twig (1)

1-13: LGTM! Standard bound service implementation.

The KeepAliveService follows the standard Android bound service pattern with a singleton binder, which is appropriate for a keep-alive service that doesn't need client-specific state.

templates/kmp/README.md.twig (1)

55-66: LGTM!

The Maven installation instructions are properly templated and follow standard conventions.

templates/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig (1)

1-27: LGTM!

This is a standard and correct implementation of the Event wrapper pattern for single-use LiveData events. The logic properly prevents re-consumption via getContentIfNotHandled() while still allowing inspection via peekContent().

templates/kmp/gradlew.bat (1)

1-89: LGTM!

This is the standard Gradle wrapper batch script for Windows. The implementation follows Gradle conventions and requires no changes.

templates/kmp/shared/src/iosMain/kotlin/io/package/models/InputFile.ios.kt.twig (1)

16-44: LGTM!

The iOS MIME type detection correctly uses CoreServices UTI APIs with proper CF bridging and memory management via CFBridgingRelease. The fallback to "application/octet-stream" is appropriate for missing extensions or MIME types.

templates/kmp/gradlew (1)

1-185: LGTM!

This is the standard Gradle wrapper shell script for Unix-based systems. The implementation follows Gradle conventions and handles various platform-specific paths and configurations correctly.

templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/IOExtensions.kt.twig (2)

1-1: Verify the intended package name.

The package is declared as {{ sdk.namespace | caseDot }}.fileOperations, but the AI summary indicates these extensions should be in the .extensions package. Please confirm which package is correct and ensure consistency across related files.


17-22: LGTM!

The chunk reading implementation correctly uses Okio's buffered source with skip and read operations. The use block ensures proper resource cleanup.

templates/kmp/shared/src/commonMain/kotlin/io/package/Service.kt.twig (1)

1-8: LGTM!

Clean base class design with immutable client reference and clear documentation.

templates/kmp/build.gradle.kts.twig (1)

1-9: LGTM!

Correct use of the convention plugins pattern with apply(false) to centralize plugin version management while allowing subprojects to selectively apply plugins.

templates/kmp/shared/src/commonMain/kotlin/io/package/models/UploadProgress.kt.twig (1)

3-9: LGTM!

Well-designed immutable data class with appropriate types for upload progress tracking.

templates/kmp/shared/src/androidMain/kotlin/io/package/webInterface/AndroidParsedUrl.kt.twig (1)

5-7: LGTM!

Clean platform-specific implementation with proper encapsulation and straightforward delegation to Android's Uri.

templates/kmp/androidApp/build.gradle.kts.twig (1)

1-47: Configuration looks solid overall.

The Android app module configuration follows modern best practices with Compose support, appropriate SDK versions, and clean dependency management.

templates/kmp/shared/src/androidMain/kotlin/io/package/webInterface/UrlParser.android.kt.twig (1)

5-11: LGTM! Implementation is correct.

The Android UrlParser implementation correctly delegates to android.net.Uri. Each method independently parses the URL string, which is straightforward and correct for the template-generated code.

src/SDK/Language/Android.php (1)

347-353: LGTM! Signature change aligns with parent class.

The addition of the $withGeneric parameter with a default value of true maintains backward compatibility while enabling generic type suppression when needed. The parameter is correctly forwarded to the parent method.

templates/kmp/shared/src/commonMain/kotlin/io/package/WebAuthComponent.kt.twig (1)

3-8: LGTM! Correct KMP expect declaration.

The expect class pattern is correctly used to define a multiplatform interface with platform-specific implementations. The companion object provides static-like functions for callback and cookie handling.

templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSeriailizer.kt.twig (1)

12-25: LGTM! Serializer implementation is correct.

The custom serializer properly handles Collection<String> by encoding/decoding as JsonArray with string primitives. The descriptor delegation to ListSerializer is appropriate.

templates/kmp/shared/src/iosMain/kotlin/io/package/HttpClientConfig.ios.kt.twig (1)

36-45: LGTM! Self-signed certificate handling is correctly implemented.

The conditional self-signed certificate acceptance is appropriate for development and testing. The implementation correctly uses NSURLSessionAuthChallengeUseCredential and creates credentials from the server trust.

Note: Ensure users understand that enabling selfSigned mode bypasses certificate validation and should only be used in development environments.

templates/kmp/shared/src/iosMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1)

48-49: Fix malformed callback scheme string literal.

The nested client.config["project"] breaks Kotlin string interpolation and the generated code will not compile. Please build the string without embedding double quotes inside the template expression.

-    val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.config["project"]}"
+    val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-" + (client.config["project"]?.toString() ?: "")

Likely an incorrect or invalid review comment.

src/SDK/Language/KMP.php (1)

183-192: Address commented-out documentation generation code.

The commented-out code for Kotlin and Java example documentation has been disabled. Per the past review discussion, this should either be uncommented and tested to ensure docs generate correctly, or removed entirely if not needed.

Please clarify whether documentation generation is:

  1. Temporarily disabled and should be re-enabled after testing, or
  2. Not needed for KMP and should be removed

Based on learnings

Comment on lines +36 to +44
- name: Publish to Maven Central
run: |
if ${{ contains(github.event.release.tag_name, '-rc') }}; then
echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository
else
echo "Publishing Release Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the conditional syntax for tag-based publishing.

The shell conditional syntax is incorrect. The expression if ${{ contains(...) }} will evaluate to literal strings if true or if false in bash, which won't work as intended.

Apply this diff to fix the conditional:

-      - name: Publish to Maven Central
-        run: |
-          if ${{ contains(github.event.release.tag_name, '-rc') }}; then
-            echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
-            ./gradlew publishAllPublicationsToSonatypeRepository
-          else
-            echo "Publishing Release Version ${{ github.event.release.tag_name}}"
-            ./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
-          fi
+      - name: Publish Snapshot to Maven Central
+        if: contains(github.event.release.tag_name, '-rc')
+        run: |
+          echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
+          ./gradlew publishAllPublicationsToSonatypeRepository
+        env:
+          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
+          SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+          SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
+          SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
+          SDK_VERSION: ${{ github.event.release.tag_name }}
+
+      - name: Publish Release to Maven Central
+        if: "!contains(github.event.release.tag_name, '-rc')"
+        run: |
+          echo "Publishing Release Version ${{ github.event.release.tag_name}}"
+          ./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
+        env:
+          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
+          SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+          SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
+          SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
+          SDK_VERSION: ${{ github.event.release.tag_name }}

Alternatively, if you prefer to keep a single step with bash conditionals:

       - name: Publish to Maven Central
         run: |
-          if ${{ contains(github.event.release.tag_name, '-rc') }}; then
+          if [[ "${{ contains(github.event.release.tag_name, '-rc') }}" == "true" ]]; then
             echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
             ./gradlew publishAllPublicationsToSonatypeRepository
           else
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Publish to Maven Central
run: |
if ${{ contains(github.event.release.tag_name, '-rc') }}; then
echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository
else
echo "Publishing Release Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
fi
- name: Publish Snapshot to Maven Central
if: contains(github.event.release.tag_name, '-rc')
run: |
echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
SDK_VERSION: ${{ github.event.release.tag_name }}
- name: Publish Release to Maven Central
if: "!contains(github.event.release.tag_name, '-rc')"
run: |
echo "Publishing Release Version ${{ github.event.release.tag_name}}"
./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
SDK_VERSION: ${{ github.event.release.tag_name }}
🤖 Prompt for AI Agents
templates/kmp/.github/workflows/publish.yml lines 36-44: the step uses GitHub
expressions inside a bash if incorrectly (producing literal "if true"/"if
false"); fix by either moving the contains(...) expression to the step-level if:
attribute so the step runs only for RC vs release, or keep a single step but
change the bash conditional to compare the expression string to "true" (e.g. use
a test like [ "<expression>" = "true" ]), then run the appropriate gradle
commands in the branches.

android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.{{ spec.title | caseUcfirst }}KMPSDK"
android:usesCleartextTraffic="true">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical security risk: cleartext traffic enabled.

Setting android:usesCleartextTraffic="true" allows unencrypted HTTP connections, exposing user data to interception and man-in-the-middle attacks. This should not be enabled in production builds.

If cleartext traffic is required for development/testing only, consider using build variants:

-        android:usesCleartextTraffic="true">
+        android:usesCleartextTraffic="false"
+        android:networkSecurityConfig="@xml/network_security_config">

Then create res/xml/network_security_config.xml with debug-only cleartext domains if needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="false"
android:networkSecurityConfig="@xml/network_security_config">
🤖 Prompt for AI Agents
In templates/kmp/androidApp/src/main/AndroidManifest.xml.twig around line 12,
android:usesCleartextTraffic="true" enables unencrypted HTTP and must be removed
or set to "false" for production; change the manifest to not enable cleartext
globally, and implement a debug-only configuration instead by adding a network
security config XML (res/xml/network_security_config.xml) that whitelists any
required development domains and reference it only in the debug build (use build
variants/manifest placeholders or a debug AndroidManifest merge) so production
builds never allow cleartext traffic.

<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="appwrite-callback-6070749e6acd4" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Hardcoded callback scheme should be parameterized.

The callback scheme appwrite-callback-6070749e6acd4 appears to be hardcoded. For a reusable SDK template, this should be configurable via a template variable to allow different projects to use unique callback schemes.

Apply this change to parameterize the scheme:

-                <data android:scheme="appwrite-callback-6070749e6acd4"  />
+                <data android:scheme="{{ callback_scheme | default('appwrite-callback-' ~ sdk.namespace) }}"  />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<data android:scheme="appwrite-callback-6070749e6acd4" />
<data android:scheme="{{ callback_scheme | default('appwrite-callback-' ~ sdk.namespace) }}" />
🤖 Prompt for AI Agents
In templates/kmp/androidApp/src/main/AndroidManifest.xml.twig around line 26,
the android:scheme value is hardcoded as "appwrite-callback-6070749e6acd4";
replace it with a template variable (e.g. {{ callback_scheme }} or
project-specific name) so the scheme is configurable when the template is
rendered. Ensure the variable is documented or has a sensible default in the
template context, and update any template usage or README to pass the chosen
callback_scheme when generating the project.

Comment on lines +54 to +56
binding.oAuth.setOnClickListener{
viewModel.oAuthLogin(activity as ComponentActivity)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use safe cast for activity.

The unsafe cast activity as ComponentActivity could throw ClassCastException if the activity is not a ComponentActivity. Use requireActivity() with a safe cast instead.

Apply this diff:

         binding.oAuth.setOnClickListener{
-            viewModel.oAuthLogin(activity as ComponentActivity)
+            (requireActivity() as? ComponentActivity)?.let { activity ->
+                viewModel.oAuthLogin(activity)
+            }
         }
🤖 Prompt for AI Agents
In
templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig
around lines 54 to 56, the code unsafely casts activity to ComponentActivity
which can throw ClassCastException; change to use requireActivity() with a safe
cast and early return or error handling if the cast fails (e.g., val
componentActivity = requireActivity() as? ComponentActivity ?: return) and then
call viewModel.oAuthLogin(componentActivity) so the fragment won’t crash when
the host activity isn’t a ComponentActivity.

Comment on lines +84 to +86
"appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/success",
"appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/failure"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parameterize the OAuth callback URIs.

Both callback URIs embed the sample project ID (6070749e6acd4), so generated apps that use any other Appwrite project cannot complete OAuth. Template these values from the configured project or delegate to the client’s WebAuthComponent helpers instead of hard-coding the sample IDs.

🤖 Prompt for AI Agents
In
templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig
around lines 84 to 86, the OAuth callback URIs currently embed the hard-coded
sample project ID (6070749e6acd4); update the template to build these URIs from
the configured Appwrite project ID (or call the client’s WebAuthComponent
helper) instead of hard-coding the ID so generated apps use the correct project;
ensure both the success and failure URIs are produced from the same templated
value/helper and that any necessary import or parameter is added to the
template.

Comment on lines +255 to +309
writeToFile(Query.equal("released", listOf(true)))
writeToFile(Query.equal("title", listOf("Spiderman", "Dr. Strange")))
writeToFile(Query.notEqual("title", "Spiderman"))
writeToFile(Query.lessThan("releasedYear", 1990))
writeToFile(Query.greaterThan("releasedYear", 1990))
writeToFile(Query.search("name", "john"))
writeToFile(Query.isNull("name"))
writeToFile(Query.isNotNull("name"))
writeToFile(Query.between("age", 50, 100))
writeToFile(Query.between("age", 50.5, 100.5))
writeToFile(Query.between("name", "Anna", "Brad"))
writeToFile(Query.startsWith("name", "Ann"))
writeToFile(Query.endsWith("name", "nne"))
writeToFile(Query.select(listOf("name", "age")))
writeToFile(Query.orderAsc("title"))
writeToFile(Query.orderDesc("title"))
writeToFile(Query.cursorAfter("my_movie_id"))
writeToFile(Query.cursorBefore("my_movie_id"))
writeToFile(Query.limit(50))
writeToFile(Query.offset(20))
writeToFile(Query.contains("title", listOf("Spider")))
writeToFile(Query.contains("labels", listOf("first")))
writeToFile(
Query.or(
listOf(
Query.equal("released", listOf(true)),
Query.lessThan("releasedYear", 1990)
)
)
)
writeToFile(
Query.and(
listOf(
Query.equal("released", listOf(false)),
Query.greaterThan("releasedYear", 2015)
)
)
)

// Permission & Roles helper tests
writeToFile(Permission.read(Role.any()))
writeToFile(Permission.write(Role.user(ID.custom("userid"))))
writeToFile(Permission.create(Role.users()))
writeToFile(Permission.update(Role.guests()))
writeToFile(Permission.delete(Role.team("teamId", "owner")))
writeToFile(Permission.delete(Role.team("teamId")))
writeToFile(Permission.create(Role.member("memberId")))
writeToFile(Permission.update(Role.users("verified")))
writeToFile(Permission.update(Role.user(ID.custom("userid"), "unverified")))
writeToFile(Permission.create(Role.label("admin")))

// ID helper tests
writeToFile(ID.unique())
writeToFile(ID.custom("custom_id"))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore full query helper coverage to match Base assertions.

expectedOutput includes Base::QUERY_HELPER_RESPONSES, but this test only writes the first handful of helper strings. Everything from orderRandom() through the geospatial helpers is never appended to result.txt, so the comparison against Base::QUERY_HELPER_RESPONSES will fail every run. Please emit the remaining helper outputs (or drop them from expectedOutput), otherwise the Android KMP test can’t pass.

                 writeToFile(Query.orderAsc("title"))
                 writeToFile(Query.orderDesc("title"))
+                writeToFile(Query.orderRandom())
                 writeToFile(Query.cursorAfter("my_movie_id"))
                 writeToFile(Query.cursorBefore("my_movie_id"))
                 writeToFile(Query.limit(50))
                 writeToFile(Query.offset(20))
                 writeToFile(Query.contains("title", listOf("Spider")))
                 writeToFile(Query.contains("labels", listOf("first")))
+                writeToFile(Query.notContains("title", listOf("Spider")))
+                writeToFile(Query.notSearch("name", "john"))
+                writeToFile(Query.notBetween("age", 50, 100))
+                writeToFile(Query.notStartsWith("name", "Ann"))
+                writeToFile(Query.notEndsWith("name", "nne"))
+                writeToFile(Query.lessThan("\$createdAt", "2023-01-01"))
+                writeToFile(Query.greaterThan("\$createdAt", "2023-01-01"))
+                writeToFile(Query.between("\$createdAt", "2023-01-01", "2023-12-31"))
+                writeToFile(Query.lessThan("\$updatedAt", "2023-01-01"))
+                writeToFile(Query.greaterThan("\$updatedAt", "2023-01-01"))
+                writeToFile(Query.between("\$updatedAt", "2023-01-01", "2023-12-31"))
+                writeToFile(Query.distanceEqual("location", listOf(listOf(listOf(40.7128, -74.0), listOf(40.7128, -74.0)), 1000, true)))
+                writeToFile(Query.distanceEqual("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceNotEqual("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceNotEqual("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceGreaterThan("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceGreaterThan("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceLessThan("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.distanceLessThan("location", listOf(listOf(40.7128, -74.0), 1000, true)))
+                writeToFile(Query.intersects("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.notIntersects("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.crosses("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.notCrosses("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.overlaps("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.notOverlaps("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.touches("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.notTouches("location", listOf(40.7128, -74.0)))
+                writeToFile(Query.contains("location", listOf(listOf(40.7128, -74.0), listOf(40.7128, -74.0))))
+                writeToFile(Query.notContains("location", listOf(listOf(40.7128, -74.0), listOf(40.7128, -74.0))))
+                writeToFile(Query.equal("location", listOf(listOf(40.7128, -74.0), listOf(40.7128, -74.0))))
+                writeToFile(Query.notEqual("location", listOf(listOf(40.7128, -74.0), listOf(40.7128, -74.0))))
                 writeToFile(
                     Query.or(
                         listOf(
                             Query.equal("released", listOf(true)),
🤖 Prompt for AI Agents
In tests/languages/kmp/Tests.kt around lines 255 to 309, the test writes only a
subset of query/permission/ID helper outputs but expectedOutput includes
Base::QUERY_HELPER_RESPONSES (which contains many more entries from
orderRandom() through geospatial helpers); restore full coverage by adding
writeToFile calls for the remaining helper helpers in the same pattern
(orderRandom(), orderNatural(), the rest of ordering, cursor/limit/offset
variants if any, contains/containsAny/containsAll variants, and all geospatial
helpers like near/within/geoBoundingBox, plus any missing permission/role
helpers) so the file emits every string included in Base::QUERY_HELPER_RESPONSES
(or alternatively remove those extra entries from expectedOutput if
intentionally reducing coverage).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +163 to +172
if (method.uppercase() == "GET") {
filteredParams.forEach { (key, value) ->
when (value) {
is List<*> -> value.forEach { item ->
parameter("${key}[]", item.toString())
}

else -> parameter(key, value.toString())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify array parameter serialization and object conversion.

Line 167 serializes list parameters as "key[]" query parameters, and line 170 converts all other values to strings using toString(). Both approaches should be verified:

  1. Confirm that the API expects array parameters in the "key[]" format.
  2. For complex objects (non-primitives), toString() may produce invalid or unexpected query parameter values.

Run the following script to check how array parameters are used in method definitions:


🏁 Script executed:

#!/bin/bash
# Search for methods with array parameters to understand expected format
rg -nP --type-add 'spec:*.{json,yaml,yml}' --type=spec -C3 'type.*array|array.*type' 

Length of output: 5402


I need to gather more information about how the API parameters are actually serialized. Let me verify the implementation details and test expectations.

Let me search the codebase for test files and actual usage patterns to better understand the expected parameter format.





Let me run a proper search on the repository:


🏁 Script executed:

#!/bin/bash
# Search for tests related to parameters or BaseClient
find . -type f -name "*.kt" -o -name "*.kts" | xargs grep -l "parameter\|BaseClient\|z\[\]" 2>/dev/null | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for test files that validate query parameter serialization
fd -e ".kt" "test" --type f | xargs grep -l "parameter.*\[\]" 2>/dev/null

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for parameter usage and array handling in tests
rg -n "parameter.*\[\]|collectionFormat" --type=kt --type=json

Length of output: 26







---

🏁 Script executed:

```shell
#!/bin/bash
# Search for the template file and any related test files
rg -n "key\[\]|collectionFormat|multi" --type=json --type=yaml -C2

Length of output: 2397


🏁 Script executed:

#!/bin/bash
# Find and examine the template file mentioned in the review
find . -name "BaseClient.kt.twig" -o -name "*.twig" | head -20

Length of output: 1283


🏁 Script executed:

#!/bin/bash
# Search for test expectations around array parameters
rg -n "z\[\]" --type-add 'template:*.twig' --type=template -C3

Length of output: 0







---

🏁 Script executed:

```shell
#!/bin/bash
# Search specifically for the kmp BaseClient template
find . -path "*kmp*" -name "BaseClient.kt.twig"

Length of output: 74


🏁 Script executed:

#!/bin/bash
# List all files in kmp templates directory
find . -path "*kmp*" -type f | grep -E "(BaseClient|Client)" | head -20

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Search for the kmp template directory structure
find . -type d -name "kmp" 

Length of output: 37







---

🏁 Script executed:

```shell
#!/bin/bash
# View the relevant section of the template file
sed -n '160,175p' ./templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig

Length of output: 632


🏁 Script executed:

#!/bin/bash
# Search for test files that validate parameter serialization
find ./tests/languages/kmp -type f | head -30

Length of output: 30


🏁 Script executed:

#!/bin/bash
# Search for tests that check array parameters or query params
rg -n "z\[\]|parameter|array" ./tests/languages/kmp -C3

Length of output: 7652







---

🏁 Script executed:

```shell
#!/bin/bash
# Search for mock server or request validation code
find ./mock-server -type f -name "*.php" | head -20

Length of output: 208


🏁 Script executed:

#!/bin/bash
# Search for how array parameters are validated in mock server
rg -n "collectionFormat|multi|\[\]" ./mock-server -C2

Length of output: 5634


🏁 Script executed:

#!/bin/bash
# Look for expected query parameter format in the codebase
rg -n "z=|z\[\]" ./tests -C3

Length of output: 0


[/web_search]


🌐 Web query:

Ktor HTTP client parameter function multiple values array query string

💡 Result:

Short answer: call parameter/append repeatedly or use a Parameters/ParametersBuilder that accepts multiple values.

Examples:

  • Repeated parameter() calls (builds ?tag=a&tag=b):
    client.get("...") {
    url {
    parameter("tag", "a")
    parameter("tag", "b")
    }
    }
    (parameter() appends a single query parameter). [1][2]

  • Use URLBuilder.parameters (ParametersBuilder) directly:
    client.get("...") {
    url {
    parameters.append("tag", "a")
    parameters.append("tag", "b")
    }
    }
    Or build once from a list:
    url { parameters += Parameters.build { appendAll("tag", listOf("a","b")) } }
    Or construct Parameters from pairs / list: Parameters.build { append("a","b1"); append("a","b2") } or parametersOf("a", listOf("b1","b2")).formUrlEncode() to see encoding. [1][3]

Notes:

  • Ktor encodes parameters by default; repeated keys produce multiple key=value entries in the query string (server-side can read them as a list). [1][3]

Sources:

  1. Ktor — Making requests / query parameters. [1]
  2. Ktor API — HttpRequestBuilder.parameter(). [2]
  3. Ktor — Parameters / ParametersBuilder / formUrlEncode examples. [3]

Fix array parameter serialization format to match API specification.

The template uses parameter("${key}[]", item.toString()) which generates query parameters like ?z[]=val1&z[]=val2. However, the API specification defines "collectionFormat": "multi" for array parameters, which expects ?z=val1&z=val2 format.

The fix is to remove the [] suffix from the parameter name:

is List<*> -> value.forEach { item ->
    parameter(key, item.toString())
}

Calling parameter() multiple times with the same key automatically creates the repeated query parameter format that Ktor sends as ?key=val1&key=val2, matching the collectionFormat: "multi" specification.

The toString() conversion for other values is acceptable since the spec only defines string types within arrays.

🤖 Prompt for AI Agents
In templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig
around lines 163 to 172, array query parameters are serialized as key[] (e.g.,
?z[]=a&z[]=b) which doesn't match the API spec's collectionFormat: "multi";
update the List handling to call parameter with the plain key for each item
(i.e., remove the "[]" suffix) so repeated parameter(key, item.toString()) calls
produce ?key=val1&key=val2 as required.

Comment on lines +290 to +347
var result: Map<*, *> = mapOf<String, Any>()
while (offset < size) {
val bytesToRead = if (size - offset < CHUNK_SIZE) {
(size - offset).toInt()
} else {
CHUNK_SIZE.toInt()
}
val chunk = data.readByteArray(bytesToRead)

httpClient.submitFormWithBinaryData(
url = endpoint + path,
formData = formData {
filteredParams.forEach { (key, value) ->
when (value) {
is List<*> -> value.forEach { item ->
append("${key}[]", item.toString())
}

else -> append(key, value.toString())
}
}
append(
paramName,
chunk,
Headers.build {
append(HttpHeaders.ContentType, input.mimeType)
append(
HttpHeaders.ContentDisposition,
"filename=${input.filename.quote()}"
)
}
)
}
) {
header(
HttpHeaders.ContentRange,
"bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size - 1)}/$size"
)
combinedHeaders.forEach { (key, value) ->
header(key, value)
}
if (chunksUploaded != 0L) {
header("x-{{ spec.title | caseLower }}-id", result["\$id"].toString().removeSurrounding("\""))
}
}.let { response ->
result = handleResponse(response, Map::class, getSerializer(Map::class))
offset += bytesToRead
chunksUploaded++
onProgress?.invoke(
UploadProgress(
id = result["\$id"].toString(),
progress = offset.coerceAtMost(size).toDouble() / size * 100,
sizeUploaded = offset.coerceAtMost(size),
chunksTotal = result["chunksTotal"].toString().toInt(),
chunksUploaded = result["chunksUploaded"].toString().toInt(),
)
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple unsafe map accesses without null checks.

Lines 332, 340, and 343-344 access map fields without null checks:

  • Line 332: result["\$id"]
  • Line 340: result["\$id"]
  • Lines 343-344: result["chunksTotal"], result["chunksUploaded"]

These could throw exceptions if the response structure is unexpected.

Apply this diff to add proper null-safety:

                 if (chunksUploaded != 0L) {
-                    header("x-{{ spec.title | caseLower }}-id", result["\$id"].toString().removeSurrounding("\""))
+                    result["\$id"]?.toString()?.removeSurrounding("\"")?.let { id ->
+                        header("x-{{ spec.title | caseLower }}-id", id)
+                    }
                 }
             }.let { response ->
                 result = handleResponse(response, Map::class, getSerializer(Map::class))
                 offset += bytesToRead
                 chunksUploaded++
                 onProgress?.invoke(
                     UploadProgress(
-                        id = result["\$id"].toString(),
+                        id = result["\$id"]?.toString() ?: "",
                         progress = offset.coerceAtMost(size).toDouble() / size * 100,
                         sizeUploaded = offset.coerceAtMost(size),
-                        chunksTotal = result["chunksTotal"].toString().toInt(),
-                        chunksUploaded = result["chunksUploaded"].toString().toInt(),
+                        chunksTotal = result["chunksTotal"]?.toString()?.toIntOrNull() ?: 0,
+                        chunksUploaded = result["chunksUploaded"]?.toString()?.toIntOrNull() ?: 0,
                     )
                 )
             }

Comment on lines +60 to +88
fun JsonObjectBuilder.addToJsonObject(key: String, value: Any?) {
when (value) {
is String -> put(key, value)
is Number -> put(key, value)
is Boolean -> put(key, value)
is List<*> -> {
val jsonArray = getJsonArray(value)
put(key, jsonArray)
}

is Enum<*> -> put(key, JsonPrimitive(value.name))
null -> put(key, JsonNull)
else -> put(key, JsonPrimitive(value.toString()))
}
}

fun getJsonArray(items: List<Any?>): JsonArray {
return JsonArray(items.map { item ->
when (item) {
is String -> JsonPrimitive(item)
is Number -> JsonPrimitive(item)
is Boolean -> JsonPrimitive(item)
is Enum<*> -> JsonPrimitive(item.name)
null -> JsonNull
is List<*> -> getJsonArray(item)
else -> JsonPrimitive(item.toString())
}
})
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback to toString() for complex objects risks data corruption.

The else branches at lines 72 and 85 use toString() as a fallback for unknown types. For complex objects without a meaningful toString() implementation, this produces non-JSON strings (e.g., "SomeClass@1a2b3c4d") that cannot be deserialized and may corrupt data.

Consider one of these approaches:

  1. Throw an exception for unsupported types to fail fast:
-        else -> put(key, JsonPrimitive(value.toString()))
+        else -> throw IllegalArgumentException("Unsupported type for JSON serialization: ${value::class}")
  1. Serialize complex objects properly using the Json instance:
-        else -> put(key, JsonPrimitive(value.toString()))
+        else -> put(key, json.encodeToJsonElement(DynamicLookupSerializer, value))

Apply the same fix to line 85 in getJsonArray.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun JsonObjectBuilder.addToJsonObject(key: String, value: Any?) {
when (value) {
is String -> put(key, value)
is Number -> put(key, value)
is Boolean -> put(key, value)
is List<*> -> {
val jsonArray = getJsonArray(value)
put(key, jsonArray)
}
is Enum<*> -> put(key, JsonPrimitive(value.name))
null -> put(key, JsonNull)
else -> put(key, JsonPrimitive(value.toString()))
}
}
fun getJsonArray(items: List<Any?>): JsonArray {
return JsonArray(items.map { item ->
when (item) {
is String -> JsonPrimitive(item)
is Number -> JsonPrimitive(item)
is Boolean -> JsonPrimitive(item)
is Enum<*> -> JsonPrimitive(item.name)
null -> JsonNull
is List<*> -> getJsonArray(item)
else -> JsonPrimitive(item.toString())
}
})
}
fun JsonObjectBuilder.addToJsonObject(key: String, value: Any?) {
when (value) {
is String -> put(key, value)
is Number -> put(key, value)
is Boolean -> put(key, value)
is List<*> -> {
val jsonArray = getJsonArray(value)
put(key, jsonArray)
}
is Enum<*> -> put(key, JsonPrimitive(value.name))
null -> put(key, JsonNull)
else -> throw IllegalArgumentException("Unsupported type for JSON serialization: ${value::class}")
}
}
fun getJsonArray(items: List<Any?>): JsonArray {
return JsonArray(items.map { item ->
when (item) {
is String -> JsonPrimitive(item)
is Number -> JsonPrimitive(item)
is Boolean -> JsonPrimitive(item)
is Enum<*> -> JsonPrimitive(item.name)
null -> JsonNull
is List<*> -> getJsonArray(item)
else -> throw IllegalArgumentException("Unsupported type for JSON serialization: ${item::class}")
}
})
}
🤖 Prompt for AI Agents
In
templates/kmp/shared/src/commonMain/kotlin/io/package/extensions/JsonExtensions.kt.twig
around lines 60 to 88, the fallback branches use value.toString() /
item.toString(), which can produce non-JSON or non-deserializable
representations for complex objects; replace these fallbacks by either
(preferably) serializing unknown objects properly with the kotlinx.serialization
Json instance (e.g., call Json.encodeToJsonElement(serializerOrDefault, value)
or Json.encodeToJsonElement(value) for polymorphic/reflective serialization) so
complex types become proper JsonElements, or if serialization cannot be
guaranteed, throw an IllegalArgumentException indicating the unsupported type to
fail fast; apply the same change to both the addToJsonObject else branch and the
getJsonArray else branch so neither uses toString() as the fallback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig (1)

12-19: Address the outstanding context caching concern.

While using .applicationContext (line 19) prevents the typical memory leak scenario mentioned in the previous review, the reviewer specifically asked whether we can avoid caching the context altogether. Consider extracting the needed values during initialization:

  • packageName (used in lines 29, 41)
  • appVersion (used in line 42)
  • DataStore file path (used in line 23)

This refactor would address the reviewer's concern:

 actual class Client constructor(
     context: Context,
     endpoint: String = "{{spec.endpoint}}",
     endpointRealtime: String? = null,
     selfSigned: Boolean = false,
 ) : BaseClient<Client>(endpoint, endpointRealtime) {
     actual override val coroutineContext = Job() + Dispatchers.Default
-    private val appContext = context.applicationContext
+    private val packageName: String
+    private val appVersion: String
+    private val cookieStoragePath: okio.Path

     private val dataStoreManager = DataStoreManager(
         PreferenceDataStoreFactory.createWithPath (
-            produceFile = { appContext.filesDir.resolve("appwriteCookies.preferences_pb").absolutePath.toPath() }
+            produceFile = { cookieStoragePath }
         ))
     val dataStoreCookieStorage = DataStoreCookieStorage(dataStoreManager)

-    private val appVersion by lazy {
+    init {
+        val appContext = context.applicationContext
+        packageName = appContext.packageName
+        cookieStoragePath = appContext.filesDir.resolve("appwriteCookies.preferences_pb").absolutePath.toPath()
+        
         try {
             val pInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
-            return@lazy pInfo.versionName
+            appVersion = pInfo.versionName
         } catch (e: PackageManager.NameNotFoundException) {
-            e.printStackTrace()
-            return@lazy ""
+            appVersion = ""
+            // Consider proper logging instead of printStackTrace
         }
-    }

-    init {
         httpClient = createHttpClient(selfSigned, dataStoreCookieStorage)
         headers = mutableMapOf(
               "content-type" to "application/json",
-              "origin" to "{{ spec.title | caseLower }}-android://${appContext.packageName}",
+              "origin" to "{{ spec.title | caseLower }}-android://${packageName}",
               "user-agent" to "${packageName}/${appVersion}, ${System.getProperty("http.agent")}",
🧹 Nitpick comments (1)
templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig (1)

27-35: Improve error handling.

Using printStackTrace() on line 32 is not ideal for production code. Consider using a proper logging framework to ensure errors are captured and monitored appropriately.

         } catch (e: PackageManager.NameNotFoundException) {
-            e.printStackTrace()
+            // Log the error using your logging framework
+            // e.g., Log.w("Client", "Failed to get package info", e)
             return@lazy ""
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bfffce and 327cb68.

📒 Files selected for processing (2)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig (1 hunks)
🔇 Additional comments (2)
templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig (2)

36-38: Previous issue resolved.

The guard that prevented the first assignment has been removed, and cookieStorage is now assigned unconditionally. This fixes the critical bug flagged in the previous review.


198-208: Ensure valid presentation anchor fallback.

If no key window is found, returning an empty UIWindow() (line 207) may cause the authentication session to fail. Verify this scenario is handled or throw an exception to surface the issue early.

Test this edge case or add defensive error handling:

override fun presentationAnchorForWebAuthenticationSession(session: ASWebAuthenticationSession): ASPresentationAnchor {
    val windows = UIApplication.sharedApplication.windows
    val count = windows.size
    for (i in 0 until count) {
        val window = windows[i] as? UIWindow
        if (window?.isKeyWindow() == true) {
            return window
        }
    }
    // Consider: throw IllegalStateException("No key window available for authentication")
    return UIWindow()
}

endpointRealtime: String? = null,
selfSigned: Boolean = false,
) : BaseClient<Client>(endpoint, endpointRealtime) {
actual override val coroutineContext = Job() + Dispatchers.Default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential resource leak: Job not cancelled.

The Job() created here should be cancelled when the Client is no longer needed to prevent resource leaks. Consider adding a cleanup method (e.g., close() or dispose()) that cancels the job.

fun close() {
    coroutineContext.cancel()
    httpClient.close()
}
🤖 Prompt for AI Agents
In templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig
around line 18, the coroutineContext is created with a bare Job() which is never
cancelled; change to store the Job in a field (e.g., val job = Job()) and
compose coroutineContext = job + Dispatchers.Default, then add a public cleanup
method (e.g., fun close() or override fun close() if implementing Closeable)
that calls job.cancel() and also httpClient.close() to release resources and
avoid leaks.

Comment on lines +55 to +58
actual fun setSelfSigned(value: Boolean): Client {
httpClient = createHttpClient(value, dataStoreCookieStorage)
return this
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Close the old httpClient before replacing it.

When setSelfSigned recreates the httpClient, the previous instance is replaced without being closed. This can leak connections and other resources managed by Ktor's HttpClient.

 actual fun setSelfSigned(value: Boolean): Client {
+    httpClient.close()
     httpClient = createHttpClient(value, dataStoreCookieStorage)
     return this
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
actual fun setSelfSigned(value: Boolean): Client {
httpClient = createHttpClient(value, dataStoreCookieStorage)
return this
}
actual fun setSelfSigned(value: Boolean): Client {
httpClient.close()
httpClient = createHttpClient(value, dataStoreCookieStorage)
return this
}
🤖 Prompt for AI Agents
In templates/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig
around lines 55–58, the method recreates httpClient without closing the previous
instance; before assigning the new client call close() on the existing
httpClient (safely handling nullability and catching any exceptions—for example
wrap in runCatching or try/catch) to release resources, then assign the new
client and return this.

Comment on lines +67 to +74
callbackURL != null -> {
val callbackUrlString = callbackURL.absoluteString ?: ""
handleIncomingCookie(callbackUrlString)
pending?.onComplete?.invoke(Result.success(callbackUrlString))
if (pending?.continuation?.isActive == true) {
pending.continuation.resume(callbackUrlString)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix double-resume race condition.

The continuation is resumed twice when a callback URL is received:

  1. handleIncomingCookie (line 69) → onCallback (line 177) → continuation.resume (line 112)
  2. Then the completion handler resumes again at line 72

This will crash with IllegalStateException: Already resumed. Either remove the resume at lines 71-72 or refactor handleIncomingCookie to not call onCallback in this flow.

Apply this diff to prevent double-resume:

                     callbackURL != null -> {
                         val callbackUrlString = callbackURL.absoluteString ?: ""
                         handleIncomingCookie(callbackUrlString)
-                        pending?.onComplete?.invoke(Result.success(callbackUrlString))
-                        if (pending?.continuation?.isActive == true) {
-                            pending.continuation.resume(callbackUrlString)
-                        }
+                        // Note: handleIncomingCookie calls onCallback which resumes the continuation
                     }
🤖 Prompt for AI Agents
In
templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig
around lines 67-74, the continuation is being resumed twice when a callback URL
arrives (once via handleIncomingCookie -> onCallback path and again here),
causing IllegalStateException: Already resumed; remove the explicit
continuation.resume call in this block (the check and resume on lines 71-72) and
keep invoking pending?.onComplete?.invoke(Result.success(callbackUrlString)) so
the resume happens only via the established onCallback flow.

Comment on lines +130 to +134
val rawDomain = cookieParts["domain"] ?: return
val key = cookieParts["key"] ?: return
val secret = cookieParts["secret"] ?: return

val domain = if (rawDomain.isNotEmpty()) rawDomain.substring(1) else rawDomain
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify domain parsing logic.

The code unconditionally strips the first character from rawDomain (line 134), assuming it's a leading dot. If the domain doesn't start with a dot (e.g., from malformed query parameters), this produces an incorrect domain.

Consider:

-            val domain = if (rawDomain.isNotEmpty()) rawDomain.substring(1) else rawDomain
+            val domain = rawDomain.removePrefix(".")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val rawDomain = cookieParts["domain"] ?: return
val key = cookieParts["key"] ?: return
val secret = cookieParts["secret"] ?: return
val domain = if (rawDomain.isNotEmpty()) rawDomain.substring(1) else rawDomain
val rawDomain = cookieParts["domain"] ?: return
val key = cookieParts["key"] ?: return
val secret = cookieParts["secret"] ?: return
val domain = rawDomain.removePrefix(".")
🤖 Prompt for AI Agents
In
templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig
around lines 130 to 134, the domain parsing unconditionally strips the first
character from rawDomain which corrupts domains that do not begin with a dot;
update the logic to only remove a leading dot (e.g., check
rawDomain.startsWith(".") or use removePrefix(".")) and also trim whitespace and
guard against rawDomain being just "." so you preserve the original rawDomain
when no leading dot is present.

Comment on lines +143 to +145
expires = cookieParts["expires"]?.let {
null // You may need to parse the date properly
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Implement cookie expiration parsing.

Cookie expires is always null (line 144), so cookies never expire via the expires attribute. This may cause stale authentication state or security issues if long-lived cookies aren't cleaned up.

Parse the ISO-8601 or HTTP-date format from cookieParts["expires"]:

expires = cookieParts["expires"]?.let {
    // Parse using NSDateFormatter or kotlinx-datetime
    parseHttpDate(it)
}
🤖 Prompt for AI Agents
In
templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig
around lines 143-145, the expires field is always set to null; replace the
placeholder with real parsing of cookieParts["expires"] by attempting to parse
both HTTP-date and ISO-8601 formats (e.g., using
NSDateFormatter/ISO8601DateFormatter on iOS or kotlinx-datetime on common code),
convert the parsed date to the expected expires type, and return null only if
parsing fails; ensure parsing is tolerant of common HTTP-date variants and time
zones and does not throw on invalid input.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (5)
templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSerializer.kt.twig (1)

1-26: Filename typo flagged in previous review.

templates/kmp/shared/src/iosMain/kotlin/io/package/cookies/IosCookieStorage.kt.twig (1)

185-185: HttpOnly flag not preserved during cookie restoration.

Line 185 uses the raw string "HttpOnly" which NSHTTPCookie ignores, causing restored cookies to lose their HttpOnly protection. This is a security regression.

templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig (2)

163-172: Array parameter serialization format mismatch with API specification.

Lines 167, 241, and 305 serialize array parameters using "${key}[]" format (e.g., ?z[]=val1&z[]=val2), but the API specification defines "collectionFormat": "multi" which expects ?z=val1&z=val2 format without the [] suffix.


332-344: Unsafe map accesses without null-safety checks.

Lines 332, 340, 343, and 344 access map fields without null checks, which could throw exceptions if the response structure is unexpected:

  • Line 332: result["\$id"]
  • Line 340: result["\$id"]
  • Lines 343-344: result["chunksTotal"] and result["chunksUploaded"]
templates/kmp/shared/build.gradle.kts.twig (1)

16-16: Guard against missing SDK_VERSION env var.

System.getenv("SDK_VERSION") returns null in local builds, causing project.ext["PUBLISH_VERSION"].toString() on line 29 to fail with an NPE. Provide a fallback to the template version.

Apply this diff:

-    set("PUBLISH_VERSION", System.getenv("SDK_VERSION"))
+    set("PUBLISH_VERSION", System.getenv("SDK_VERSION") ?: "{{ sdk.version }}")
🧹 Nitpick comments (3)
templates/kmp/shared/src/androidMain/kotlin/io/package/models/InputFile.android.kt.twig (1)

4-4: Remove unused import.

The RequiresApi annotation is no longer used after removing the annotation from the function. Consider removing this import for cleaner code.

-import androidx.annotation.RequiresApi
templates/kmp/shared/build.gradle.kts.twig (2)

8-8: Consider moving the AtomicFU version to the version catalog.

The AtomicFU plugin version is hardcoded while other plugins use the version catalog. For consistency and easier maintenance, move this version to templates/kmp/gradle/libs.versions.toml.

Apply this diff to use the version catalog:

-    id("org.jetbrains.kotlinx.atomicfu") version "0.29.0"
+    alias(libs.plugins.kotlinx.atomicfu)

Then add the plugin reference to the version catalog file.


99-99: Move Firebase BOM version to the version catalog.

The Firebase BOM version is hardcoded in the build script. For consistency with other dependencies and easier maintenance, move this version to templates/kmp/gradle/libs.versions.toml.

Apply this diff:

-            implementation(project.dependencies.platform("com.google.firebase:firebase-bom:33.6.0"))
+            implementation(project.dependencies.platform(libs.firebase.bom))

Then add the BOM reference to the version catalog file with version 33.6.0.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 327cb68 and 2aa84da.

📒 Files selected for processing (12)
  • src/SDK/Language/KMP.php (1 hunks)
  • templates/kmp/gradle/wrapper/gradle-wrapper.properties (1 hunks)
  • templates/kmp/shared/build.gradle.kts.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/SerializableCookie.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/androidMain/kotlin/io/package/models/InputFile.android.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig (1 hunks)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSerializer.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/cookies/IosCookieStorage.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1 hunks)
  • templates/kmp/shared/src/iosMain/kotlin/io/package/webInterface/UrlParser.ios.kt.twig (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • templates/kmp/gradle/wrapper/gradle-wrapper.properties
  • templates/kmp/shared/src/iosMain/kotlin/io/package/webInterface/UrlParser.ios.kt.twig
  • templates/kmp/shared/src/androidMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig
  • templates/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig
  • templates/kmp/shared/src/androidMain/kotlin/io/package/cookies/SerializableCookie.kt.twig
🧰 Additional context used
🧬 Code graph analysis (1)
src/SDK/Language/KMP.php (2)
src/SDK/Language.php (2)
  • Language (5-279)
  • toPascalCase (115-118)
src/SDK/Language/Kotlin.php (6)
  • Kotlin (8-651)
  • getName (13-16)
  • getFilters (475-500)
  • getPropertyType (536-560)
  • getReturnType (502-526)
  • getFiles (302-473)
🪛 PHPMD (2.15.0)
src/SDK/Language/KMP.php

44-44: Avoid unused parameters such as '$spec'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (4)
templates/kmp/shared/src/androidMain/kotlin/io/package/models/InputFile.android.kt.twig (1)

9-17: Android API compatibility issue resolved.

The previous concern about the @RequiresApi annotation has been addressed. The implementation now uses a runtime API level check, allowing the function to work on all Android API levels while still leveraging Files.probeContentType() when available (API 26+). The fallback to URLConnection.guessContentTypeFromName() ensures compatibility with older devices.

templates/kmp/shared/src/iosMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig (1)

62-72: LGTM - Validation logic correctly fixed.

The null-check validation now correctly uses || to ensure both key and secret are non-null before constructing the Cookie. This prevents NPEs and aligns with the intended authentication flow.

templates/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig (1)

283-284: LGTM - Safe cast implemented correctly.

The unsafe cast has been properly fixed with safe nullable casting and a fallback to 0L. This prevents ClassCastException when the response structure is unexpected.

templates/kmp/shared/build.gradle.kts.twig (1)

182-199: LGTM! Android configuration is correct.

The compileSdk has been properly set to 35 (the latest public SDK), and the rest of the Android configuration (minSdk, Java 17 compatibility, test options) looks good.

Comment on lines +60 to +78
// Arrays of primitives
if (($property['type'] ?? '') === 'array') {
$itemType = $property['array']['type'] ?? 'string';
$mapper = match ($itemType) {
'integer' => 'it.jsonPrimitive.long',
'number' => 'it.jsonPrimitive.double',
'boolean' => 'it.jsonPrimitive.boolean',
default => 'it.jsonPrimitive.content',
};
if ($property['required']) {
return "$key!!.jsonArray.map { $mapper }";
}
return "$key?.jsonArray?.map { $mapper }";
}

// Objects (kept as JsonElement/Any via @Contextual)
if (($property['type'] ?? '') === 'object') {
return $key;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Reintroduce nested-model deserialization logic.

Properties that declare a sub_schema (e.g., arrays of nested models such as Collection.rules) now hit the “arrays of primitives” branch, so the generated KMP code ends up mapping each element to JsonPrimitive.content. That produces a List<String>/JsonElement where the templates expect List<YourSubModel>, causing type mismatches or crashes at runtime. Please mirror the Kotlin generator’s handling for sub-schema objects before the primitive branches so we instantiate the proper model types.

One way to address this is to add a dedicated sub-schema branch ahead of the primitive array block:

@@
-        // Arrays of primitives
-        if (($property['type'] ?? '') === 'array') {
+        // Nested models (objects or arrays of objects)
+        if (isset($property['sub_schema']) && !empty($property['sub_schema'])) {
+            $nestedClass = $this->toPascalCase($property['sub_schema']);
+            $genericSuffix = $this->hasGenericType($property['sub_schema'], $spec) ? '<T>' : '';
+
+            if (($property['type'] ?? '') === self::TYPE_ARRAY) {
+                $mapper = $property['required']
+                    ? "$key!!.jsonArray.map { $nestedClass$genericSuffix.fromJsonObject(it.jsonObject) }"
+                    : "$key?.jsonArray?.map { $nestedClass$genericSuffix.fromJsonObject(it.jsonObject) }";
+                return $mapper;
+            }
+
+            return $property['required']
+                ? "$nestedClass$genericSuffix.fromJsonObject($key!!.jsonObject)"
+                : "$key?.jsonObject?.let { $nestedClass$genericSuffix.fromJsonObject(it) }";
+        }
+
+        // Arrays of primitives
+        if (($property['type'] ?? '') === 'array') {
             $itemType = $property['array']['type'] ?? 'string';

(Adjust the factory method naming to whatever the generated models expose—fromJsonObject, from, etc.)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +101 to +107
protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string
{
if ($method['type'] === 'webAuth') {
return 'Bool';
}
return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use the Kotlin Boolean type for webAuth methods.

Bool isn’t a valid Kotlin type (that’s Swift); this will break compilation for every generated webAuth method. The KMP code should return Boolean, just like the base Kotlin generator.

         if ($method['type'] === 'webAuth') {
-            return 'Bool';
+            return 'Boolean';
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string
{
if ($method['type'] === 'webAuth') {
return 'Bool';
}
return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric);
}
protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string
{
if ($method['type'] === 'webAuth') {
return 'Boolean';
}
return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric);
}
🤖 Prompt for AI Agents
In src/SDK/Language/KMP.php around lines 101 to 107, the getReturnType override
returns the Swift type "Bool" for webAuth methods which is invalid in Kotlin;
change the return value to the Kotlin type "Boolean" so webAuth methods match
the base Kotlin generator's return type and compile correctly.

alias(libs.plugins.kotlinx.serialization)
id("org.jetbrains.kotlinx.atomicfu") version "0.29.0"
id("maven-publish")
id("signing")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify signing configuration for Maven Central publishing.

The signing plugin is declared but no signing { } configuration block is present. Maven Central requires signed artifacts. Confirm that signing is configured elsewhere (e.g., in a parent script or convention plugin) or add the signing configuration here.

Run this script to search for signing configuration:


🏁 Script executed:

#!/bin/bash
# Search for signing configuration blocks in the repository
ast-grep --pattern $'signing {
  $$$
}'

# Also check for signing-related setup in other gradle files
rg -n 'sign\(' -g '*.kts' -g '*.gradle'

Length of output: 0


I need to broaden the search with different patterns and approaches, since the initial search produced no results. Let me investigate further:

I need to conduct a broader search to understand the signing configuration context better, since the initial patterns returned no results:


🏁 Script executed:

#!/bin/bash
# Broader search for signing-related configuration
# Check for signing with various patterns and whitespace variations
rg -n -i 'signing\s*\{' -g '*.kts' -g '*.gradle' -g '*.properties' -g '*.twig'

# Check for signing plugin configuration in DSL
rg -n 'id.*signing' -g '*.kts' -g '*.gradle' -g '*.twig'

# Look for parent build scripts and settings
echo "=== Checking settings.gradle(.kts) ==="
find . -name 'settings.gradle*' -type f | head -5

# Check for convention plugins or build configuration
echo "=== Checking for convention plugins ==="
find . -path '*/convention*' -o -path '*/buildSrc*' | head -10

# Search for Maven Central or publishing related config
rg -n 'MavenCentral\|publishToMavenCentral\|signing' -g '*.kts' -g '*.gradle' | head -20

Length of output: 369


🏁 Script executed:

#!/bin/bash
# Broader search for signing-related configuration across all relevant files
echo "=== Searching for signing patterns (relaxed) ==="
rg -n -i 'signing' -g '*.kts' -g '*.gradle' -g '*.twig' -C 2

echo "=== Checking gradle.properties files ==="
find . -name 'gradle.properties' -type f -exec grep -l 'sign' {} \;

echo "=== Looking for build configuration structure ==="
find . -type d \( -name 'buildSrc' -o -name 'build-logic' -o -name 'convention' \) | head -5

echo "=== Checking settings file ==="
find . -maxdepth 2 -name 'settings.gradle*' -type f

echo "=== Search in all gradle/twig files for publish or maven config ==="
rg -n 'publish|maven|sonatype' -i -g '*.kts' -g '*.gradle' -g '*.twig' | head -15

Length of output: 6483


I need to see the complete KMP file to determine if signing configuration exists:


🏁 Script executed:

#!/bin/bash
# View the complete KMP build.gradle.kts.twig file
echo "=== Full KMP build.gradle.kts.twig file ==="
cat templates/kmp/shared/build.gradle.kts.twig

# Also check if KMP applies external scripts like the other templates
echo -e "\n=== Check for script imports in KMP ==="
rg -n 'apply|include|script' templates/kmp/shared/build.gradle.kts.twig

Length of output: 7406


Let me check for any root-level or parent build scripts that might handle signing configuration globally:


🏁 Script executed:

#!/bin/bash
# Check for root build.gradle files and any parent configuration
echo "=== Root-level gradle files ==="
find . -maxdepth 1 -name 'build.gradle*' -type f

echo "=== Root gradle.properties ==="
find . -maxdepth 1 -name 'gradle.properties' -type f

echo "=== Check if KMP shares parent build config ==="
find . -path '*/kmp/*' -name 'build.gradle*' -o -path '*/kmp/*' -name 'gradle.properties'

echo "=== Search for any gradle plugins applied at root or shared level ==="
rg -n 'signing\|id.*signing' --type gradle --type properties

Length of output: 381


Let me check the parent KMP build file and gradle properties:


🏁 Script executed:

#!/bin/bash
# Check parent KMP build file
echo "=== Parent KMP build.gradle.kts.twig ==="
cat templates/kmp/build.gradle.kts.twig

echo -e "\n=== KMP gradle.properties ==="
cat templates/kmp/gradle.properties

echo -e "\n=== Search for signing in KMP directory ==="
rg -n 'signing' templates/kmp/ -g '*.gradle*' -g '*.properties'

Length of output: 982


Add the signing configuration block to complete Maven Central publishing setup.

The signing plugin is declared on line 10 but no signing { } configuration block exists in this file or parent/sibling build scripts. Unlike the Kotlin and Android templates (which include signing configuration in their publish scripts), the KMP template is missing this critical component. Maven Central requires signed artifacts.

Add the signing configuration block with:

signing {
    sign publishing.publications
}

Ensure signing credentials are provided via environment variables or properties matching the Kotlin/Android template pattern (e.g., SIGNING_KEY_ID, SIGNING_PASSWORD, SIGNING_SECRET_KEY_RING_FILE).

🤖 Prompt for AI Agents
In templates/kmp/shared/build.gradle.kts.twig around line 10, the signing plugin
is applied but there is no signing { } configuration block; add a signing
configuration that signs the publishing.publications (i.e., configure signing to
sign publishing.publications) and ensure the signing credentials are read from
environment variables or Gradle properties following the Kotlin/Android template
pattern (e.g., SIGNING_KEY_ID, SIGNING_PASSWORD, SIGNING_SECRET_KEY_RING_FILE)
so artifacts can be accepted by Maven Central.

Comment on lines +175 to +176
username = System.getenv("OSSRH_USERNAME")
password = System.getenv("OSSRH_PASSWORD")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify OSSRH credentials handling in local builds.

The credentials pull from environment variables without fallback. Confirm that local builds (where OSSRH_USERNAME and OSSRH_PASSWORD are unset) either skip publishing or gracefully handle missing credentials.

Run this script to check if there are any guards or conditional publishing logic:


🏁 Script executed:

#!/bin/bash
# Check for credential handling patterns in related files
rg -n -C3 'OSSRH_USERNAME|OSSRH_PASSWORD|credentials' -g '*.yml' -g '*.yaml' -g '*.kts'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# First, verify the file exists and show its content around lines 175-176
find . -name "build.gradle.kts.twig" -type f | head -5

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Search for OSSRH patterns without file type restrictions
rg -n 'OSSRH_USERNAME|OSSRH_PASSWORD' --type-list | head -20
rg -n 'OSSRH_USERNAME|OSSRH_PASSWORD'

Length of output: 1103


🏁 Script executed:

#!/bin/bash
# Look for publishing or credentials configuration more broadly
rg -n 'credentials|publishing' -A2 -B2

Length of output: 3535


🏁 Script executed:

#!/bin/bash
# Check for gradle.properties or environment variable handling
find . -name "gradle.properties" -o -name "*.gradle" -o -name "*.gradle.kts" | head -20

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Search for task configurations that might condition publishing
rg -n 'task|publishing\s*{|repositories\s*{' -g '*.kts' -g '*.twig' -C1

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Show full context of credentials block and surrounding code in KMP template
cat -n templates/kmp/shared/build.gradle.kts.twig | sed -n '160,180p'

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Check for ext property definitions or gradle.properties in KMP
cat templates/kmp/gradle.properties

Length of output: 399


🏁 Script executed:

#!/bin/bash
# Show full credentials and repositories blocks in KMP
cat -n templates/kmp/shared/build.gradle.kts.twig | sed -n '128,180p'

Length of output: 2272


🏁 Script executed:

#!/bin/bash
# Compare the three credential patterns side by side
echo "=== KMP Template (lines 175-176) ===" && \
rg -n -A1 'username = System.getenv' templates/kmp/shared/build.gradle.kts.twig && \
echo -e "\n=== Kotlin Template (lines 18-19) ===" && \
rg -n -A1 'ossrhUsername' templates/kotlin/scripts/setup.gradle && \
echo -e "\n=== Android Template (lines 18-19) ===" && \
rg -n -A1 'ossrhUsername' templates/android/scripts/publish-config.gradle

Length of output: 855


🏁 Script executed:

#!/bin/bash
# Search for signing configuration in KMP template
rg -n 'signing\s*{' templates/kmp/shared/build.gradle.kts.twig -A10

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Check if there are any task conditionals or onlyIf statements in KMP
rg -n 'onlyIf|enabled|gradle.taskGraph' templates/kmp/shared/build.gradle.kts.twig -C2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the beginning of the KMP build file for imports and plugins
head -50 templates/kmp/shared/build.gradle.kts.twig

Length of output: 1935


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about credential handling
rg -n 'SNAPSHOT|publish|release|local' templates/kmp/shared/build.gradle.kts.twig -B1 -A1

Length of output: 845


Add fallback to OSSRH credentials to prevent null values in local builds.

The KMP template directly assigns System.getenv() results to credentials without fallback, unlike the Kotlin and Android templates which use the Elvis operator (?:) with initialized defaults. When OSSRH_USERNAME and OSSRH_PASSWORD environment variables are unset (common in local dev builds), credentials will be null and publishing will fail silently or with unclear errors.

Fix: Add fallback values matching the pattern in templates/kotlin/scripts/setup.gradle and templates/android/scripts/publish-config.gradle:

  • Initialize ext["ossrhUsername"] and ext["ossrhPassword"] to empty strings
  • Use System.getenv("OSSRH_USERNAME") ?: ext["ossrhUsername"] instead of direct System.getenv() call

Lines 175–176 in templates/kmp/shared/build.gradle.kts.twig

🤖 Prompt for AI Agents
In templates/kmp/shared/build.gradle.kts.twig around lines 175-176, the OSSRH
credentials are set directly from System.getenv(...) which can be null in local
builds; initialize ext["ossrhUsername"] and ext["ossrhPassword"] to empty
strings (matching the other templates) and replace the direct System.getenv
calls with fallbacks using the Elvis operator, e.g.
System.getenv("OSSRH_USERNAME") ?: ext["ossrhUsername"] and
System.getenv("OSSRH_PASSWORD") ?: ext["ossrhPassword"], so credentials are
never null during local builds.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig (1)

81-81: Minor formatting inconsistency.

This method is formatted on a single line while others use multi-line formatting. Consider matching the style used in surrounding methods for consistency.

</review_comment_end -->

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa84da and 9f11a2b.

📒 Files selected for processing (1)
  • templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig (1 hunks)
🔇 Additional comments (4)
templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig (4)

9-14: LGTM!

The class structure is well-designed with appropriate use of @Contextual for handling polymorphic value types in kotlinx.serialization, and nullable types for optional parameters.


15-20: LGTM!

The toString override correctly uses kotlinx.serialization to produce a JSON representation.


260-302: LGTM!

The date helper methods provide a clean API by delegating to the base comparison operators with the special $createdAt and $updatedAt attributes. The dollar signs are correctly escaped.


329-362: Nested list structure is correct and intentional.

The test cases confirm the implementation matches the expected API format. The methods correctly wrap the parameters in listOf(listOf(values, distance, meters)) to create the proper structure for the Query API, supporting both simple coordinates like listOf(40.7128, -74) and complex coordinate lists.

Comment on lines +310 to +318
fun or(queries: List<String>) = Query("or", null, queries.map { it.fromJson<Query>() }).toString()

/**
* Combine multiple queries using logical AND operator.
*
* @param queries The list of query strings to combine.
* @returns The query string.
*/
fun and(queries: List<String>) = Query("and", null, queries.map { it.fromJson<Query>() }).toString()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the necessity of deserializing query strings.

Both or() and and() methods deserialize query strings back to Query objects (it.fromJson<Query>()) before re-serializing. This round-trip could be inefficient and may fail if the input strings aren't valid JSON Query objects.

Please verify:

  1. Is this round-trip necessary for API compatibility, or can query strings be passed directly in the values list?
  2. Should there be error handling for invalid query strings?

🏁 Script executed:

#!/bin/bash
# Search for tests or documentation that show how or/and methods are used
rg -n -C3 '\bor\s*\(|\band\s*\(' --type=kotlin -g '!**/build/**'

Length of output: 2811


I can see the tests are passing serialized Query objects (strings) to or() and and(). Let me check the Query data class definition to understand if the round-trip is necessary.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find Query data class definition to check constructor parameter types
rg -n -A10 'data class Query' --type=kotlin -g '!**/build/**' | head -50

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Look for Query class in generated files and check constructor
fd -e 'Query.kt' -g '!**/build/**' | head -20

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Read the Query.kt.twig template file to understand constructor and class structure
cat 'templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig' | head -150

Length of output: 5474



---

🏁 Script executed:

```shell
#!/bin/bash
# Read lines around 310-318 to see or/and method implementations
sed -n '300,330p' 'templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig'

Length of output: 1339


Add error handling for invalid query strings in or() and and() methods.

The deserialization round-trip is intentional and necessary—it ensures nested queries serialize as Query objects within the JSON structure. However, the current implementation lacks error handling: if a query string isn't valid JSON, fromJson<Query>() will throw an uncaught exception.

Add try-catch with a descriptive error message or validate inputs before deserialization.

🤖 Prompt for AI Agents
In templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig around
lines 310 to 318, the or() and and() helpers call queries.map {
it.fromJson<Query>() } with no error handling so an invalid JSON string will
throw uncaught exceptions; wrap the deserialization in safe handling (e.g., use
runCatching / try-catch inside the map), and on failure throw a clear
IllegalArgumentException (or similar) that includes which method (or/and), the
index or value of the offending query string and a short reason, or
alternatively validate the string before attempting fromJson and aggregate
failures into the thrown error so callers get a descriptive message rather than
a raw parse exception.

Comment on lines +442 to +447
private fun parseValue(value: Any): List<Any> {
return when (value) {
is List<*> -> value as List<Any>
else -> listOf(value)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix unchecked cast that may cause runtime exceptions.

Line 444 performs an unchecked cast value as List<Any> which can fail at runtime. The Kotlin compiler typically warns about this because it cannot verify type safety at compile time.

Apply this diff to use a safe cast or suppress the warning if the cast is guaranteed safe:

 private fun parseValue(value: Any): List<Any> {
     return when (value) {
-        is List<*> -> value as List<Any>
+        is List<*> -> @Suppress("UNCHECKED_CAST") (value as List<Any>)
         else -> listOf(value)
     }
 }

Alternatively, if you want to be more defensive:

 private fun parseValue(value: Any): List<Any> {
     return when (value) {
-        is List<*> -> value as List<Any>
+        is List<*> -> value.filterNotNull()
         else -> listOf(value)
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun parseValue(value: Any): List<Any> {
return when (value) {
is List<*> -> value as List<Any>
else -> listOf(value)
}
}
private fun parseValue(value: Any): List<Any> {
return when (value) {
is List<*> -> @Suppress("UNCHECKED_CAST") (value as List<Any>)
else -> listOf(value)
}
}
🤖 Prompt for AI Agents
In templates/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig around
lines 442 to 447, the unchecked cast `value as List<Any>` can throw at runtime;
replace it with a safe cast and defensive conversion such as using `value as?
List<*>` and then mapping/typing its elements to List<Any> (e.g. filter/map to
Any or throw a clear exception) or return an empty list if the cast fails; do
not use an unchecked `as` cast or rely on suppression unless you can absolutely
guarantee the input type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants