From d0ec280678b8f5aa6d00a1646ef9a0c1a3a19c57 Mon Sep 17 00:00:00 2001 From: Vaibhav <68665948+dead8309@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:56:23 +0530 Subject: [PATCH] feat(frontend): add kotlinx-serialization extensions for HttpFetcher and ApiFetcher - Closes #602 - Add HttpPage in playground for testing Serialization - Sort the ordering of new module in settings.gradle - Allow for serializing body payloads too --- frontend/kobwebx-core-serialization/README.md | 1 + .../build.gradle.kts | 35 ++ .../kobweb/browser/ApiFetcherExtensions.kt | 341 ++++++++++++++++++ .../browser/http/HttpFetcherExtensions.kt | 277 ++++++++++++++ playground/site/build.gradle.kts | 3 + .../jsMain/kotlin/playground/pages/Http.kt | 91 +++++ settings.gradle.kts | 1 + 7 files changed, 749 insertions(+) create mode 100644 frontend/kobwebx-core-serialization/README.md create mode 100644 frontend/kobwebx-core-serialization/build.gradle.kts create mode 100644 frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/ApiFetcherExtensions.kt create mode 100644 frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/http/HttpFetcherExtensions.kt create mode 100644 playground/site/src/jsMain/kotlin/playground/pages/Http.kt diff --git a/frontend/kobwebx-core-serialization/README.md b/frontend/kobwebx-core-serialization/README.md new file mode 100644 index 000000000..42c39cab0 --- /dev/null +++ b/frontend/kobwebx-core-serialization/README.md @@ -0,0 +1 @@ +This module provides a collection of useful extensions for Kotlinx Serialization, specifically designed for browser APIs. These extensions are not applicable for Node APIs. \ No newline at end of file diff --git a/frontend/kobwebx-core-serialization/build.gradle.kts b/frontend/kobwebx-core-serialization/build.gradle.kts new file mode 100644 index 000000000..e3def2ce2 --- /dev/null +++ b/frontend/kobwebx-core-serialization/build.gradle.kts @@ -0,0 +1,35 @@ +import com.varabyte.kobweb.gradle.publish.FILTER_OUT_MULTIPLATFORM_PUBLICATIONS +import com.varabyte.kobweb.gradle.publish.set + +plugins { + alias(libs.plugins.kotlin.multiplatform) + id("com.varabyte.kobweb.internal.publish") +} + +group = "com.varabyte.kobweb" +version = libs.versions.kobweb.libs.get() + +kotlin { + js { + browser() + } + + sourceSets { + jsMain.dependencies { + api(libs.kotlinx.coroutines) + api(projects.frontend.kobwebCore) + implementation(libs.kotlinx.serialization.json) + } + + jsTest.dependencies { + implementation(kotlin("test-js")) + implementation(libs.truthish) + } + } +} + +kobwebPublication { + artifactId.set("kobwebx-core-serialization") + description.set("Generally useful Kotlinx Serialization extensions for various Kobweb APIs.") + filter.set(FILTER_OUT_MULTIPLATFORM_PUBLICATIONS) +} diff --git a/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/ApiFetcherExtensions.kt b/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/ApiFetcherExtensions.kt new file mode 100644 index 000000000..d7d7b697d --- /dev/null +++ b/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/ApiFetcherExtensions.kt @@ -0,0 +1,341 @@ +package com.varabyte.kobweb.browser + +import com.varabyte.kobweb.browser.http.AbortController +import com.varabyte.kobweb.browser.http.delete +import com.varabyte.kobweb.browser.http.get +import com.varabyte.kobweb.browser.http.head +import com.varabyte.kobweb.browser.http.options +import com.varabyte.kobweb.browser.http.patch +import com.varabyte.kobweb.browser.http.post +import com.varabyte.kobweb.browser.http.put +import com.varabyte.kobweb.browser.http.tryDelete +import com.varabyte.kobweb.browser.http.tryGet +import com.varabyte.kobweb.browser.http.tryHead +import com.varabyte.kobweb.browser.http.tryOptions +import com.varabyte.kobweb.browser.http.tryPatch +import com.varabyte.kobweb.browser.http.tryPost +import com.varabyte.kobweb.browser.http.tryPut +import kotlinx.browser.window +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer + + +/** + * Call GET on a target API path with [T] as the expected return type (deserializable by kotlinx-serialization using the given [serializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * See also [tryGet], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.get( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T = Json.decodeFromString(serializer, window.api.get(apiPath, headers, abortController, autoPrefix).decodeToString()) + +/** + * Like [get], but returns null if the request failed for any reason. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryGet( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T? = window.api.tryGet(apiPath, headers, abortController, autoPrefix)?.decodeToString() + ?.let { Json.decodeFromString(serializer, it) } + +/** + * Call POST on a target API path with [R] as the expected return type (deserializable by kotlinx-serialization using the given [responseSerializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPost], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.post( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R = Json.decodeFromString( + responseSerializer, + window.api.post( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix + ).decodeToString() +) + +/** + * Like [post], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryPost( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R? = window.api.tryPost( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix +) + ?.let { Json.decodeFromString(responseSerializer, it.decodeToString()) } + +/** + * Call PUT on a target API path with [R] as the expected return type (deserializable by kotlinx-serialization using the given [responseSerializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPut], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.put( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R = Json.decodeFromString( + responseSerializer, + window.api.put( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix + ).decodeToString() +) + +/** + * Like [put], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryPut( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): T? = window.api.tryPut( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix +) + ?.let { Json.decodeFromString(responseSerializer, it.decodeToString()) } + +/** + * Call PATCH on a target API path with [R] as the expected return type (deserializable by kotlinx-serialization using the given [responseSerializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPatch], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.patch( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R = Json.decodeFromString( + responseSerializer, + window.api.patch( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix + ).decodeToString() +) + +/** + * Like [patch], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryPatch( + apiPath: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R? = window.api.tryPatch( + apiPath, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController, + autoPrefix +) + ?.let { Json.decodeFromString(responseSerializer, it.decodeToString()) } + +/** + * Call DELETE on a target API path with [T] as the expected return type (deserializable by kotlinx-serialization using the given [serializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * See also [tryDelete], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.delete( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T = + Json.decodeFromString(serializer, window.api.delete(apiPath, headers, abortController, autoPrefix).decodeToString()) + +/** + * Like [delete], but returns null if the request failed for any reason. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryDelete( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T? = window.api.tryDelete(apiPath, headers, abortController, autoPrefix) + ?.let { Json.decodeFromString(serializer, it.decodeToString()) } + + +/** + * Call HEAD on a target API path with [T] as the expected return type (deserializable by kotlinx-serialization using the given [serializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * See also [tryHead], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.head( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T = + Json.decodeFromString(serializer, window.api.head(apiPath, headers, abortController, autoPrefix).decodeToString()) + +/** + * Like [head], but returns null if the request failed for any reason. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryHead( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T? = window.api.tryHead(apiPath, headers, abortController, autoPrefix) + ?.let { Json.decodeFromString(serializer, it.decodeToString()) } + +/** + * Call OPTIONS on a target API path with [T] as the expected return type (deserializable by kotlinx-serialization using the given [serializer]). + * + * @param autoPrefix If true AND if a route prefix is configured for this site, auto-affix it to the front. You + * usually want this to be true, unless you are intentionally linking outside this site's root folder while still + * staying in the same domain. + * + * See also [tryOptions], which will return null if the request fails for any reason. + * + * Note: you should NOT prepend your path with "api/", as that will be added automatically. + */ +suspend inline fun ApiFetcher.options( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T = Json.decodeFromString( + serializer, window.api.options(apiPath, headers, abortController, autoPrefix).decodeToString() +) + +/** + * Like [options], but returns null if the request failed for any reason. + * + * Additionally, if [ApiFetcher.logOnError] is set to true, any failure will be logged to the console. By default, this will + * be true for debug builds and false for release builds. + */ +suspend inline fun ApiFetcher.tryOptions( + apiPath: String, + headers: Map? = null, + abortController: AbortController? = null, + autoPrefix: Boolean = true, + serializer: DeserializationStrategy = serializer() +): T? = window.api.tryOptions(apiPath, headers, abortController, autoPrefix) + ?.let { Json.decodeFromString(serializer, it.decodeToString()) } \ No newline at end of file diff --git a/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/http/HttpFetcherExtensions.kt b/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/http/HttpFetcherExtensions.kt new file mode 100644 index 000000000..47b3adb45 --- /dev/null +++ b/frontend/kobwebx-core-serialization/src/jsMain/kotlin/com/varabyte/kobweb/browser/http/HttpFetcherExtensions.kt @@ -0,0 +1,277 @@ +package com.varabyte.kobweb.browser.http + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import org.khronos.webgl.get + +/** + * Call GET on a target resource with [T] as the expected return type. + * + * See also [tryGet], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.get( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T { + val response = get(resource, headers, abortController) + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Like [get], but returns null if the request failed for any reason. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryGet( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T? { + val response = tryGet(resource, headers, abortController) ?: return null + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Call POST on a target resource with [R] as the expected return type. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPost], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.post( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R { + val response = post( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Like [post], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryPost( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R? { + val response = tryPost( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) ?: return null + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Call PUT on a target resource with [R] as the expected return type. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPut], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.put( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R { + val response = put( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Like [put], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryPut( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R? { + val response = tryPut( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) ?: return null + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Call PATCH on a target resource with [R] as the expected return type. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * See also [tryPatch], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.patch( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R { + val response = patch( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Like [patch], but returns null if the request failed for any reason. + * + * @param body The body to send with the request. Make sure your class is marked with @Serializable or provide a custom + * [bodySerializer]. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryPatch( + resource: String, + headers: Map? = null, + body: B? = null, + abortController: AbortController? = null, + bodySerializer: SerializationStrategy = serializer(), + responseSerializer: DeserializationStrategy = serializer() +): R? { + val response = tryPatch( + resource, + headers, + body?.let { Json.encodeToString(bodySerializer, it).encodeToByteArray() }, + abortController + ) ?: return null + return Json.decodeFromString(responseSerializer, response.decodeToString()) +} + +/** + * Call DELETE on a target resource with [T] as the expected return type. + * + * See also [tryDelete], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.delete( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T { + val response = delete(resource, headers, abortController) + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Like [delete], but returns null if the request failed for any reason. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryDelete( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T? { + val response = tryDelete(resource, headers, abortController) ?: return null + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Call HEAD on a target resource with [T] as the expected return type. + * + * See also [tryHead], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.head( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T { + val response = head(resource, headers, abortController) + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Like [head], but returns null if the request failed for any reason. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryHead( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T? { + val response = tryHead(resource, headers, abortController) ?: return null + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Call OPTIONS on a target resource with [T] as the expected return type. + * + * See also [tryOptions], which will return null if the request fails for any reason. + */ +suspend inline fun HttpFetcher.options( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T { + val response = options(resource, headers, abortController) + return Json.decodeFromString(serializer, response.decodeToString()) +} + +/** + * Like [options], but returns null if the request failed for any reason. + * + * Additionally, if [HttpFetcher.logOnError] is set to true, any failure will be logged to the console. + */ +suspend inline fun HttpFetcher.tryOptions( + resource: String, + headers: Map? = null, + abortController: AbortController? = null, + serializer: DeserializationStrategy = serializer() +): T? { + val response = tryOptions(resource, headers, abortController) ?: return null + return Json.decodeFromString(serializer, response.decodeToString()) +} \ No newline at end of file diff --git a/playground/site/build.gradle.kts b/playground/site/build.gradle.kts index 392b9373b..92ad33236 100644 --- a/playground/site/build.gradle.kts +++ b/playground/site/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.compose.compiler) id("com.varabyte.kobweb.application") id("com.varabyte.kobwebx.markdown") + alias(libs.plugins.kotlinx.serialization) } group = "playground" @@ -36,12 +37,14 @@ kotlin { } jsMain.dependencies { implementation(libs.compose.html.core) + implementation(libs.kotlinx.serialization.json) implementation("com.varabyte.kobweb:kobweb-core") implementation("com.varabyte.kobweb:kobweb-silk") implementation("com.varabyte.kobwebx:silk-icons-fa") implementation("com.varabyte.kobwebx:kobwebx-markdown") implementation(project(":sitelib")) implementation(project(":worker")) + implementation("com.varabyte.kobweb:kobwebx-core-serialization") } jvmMain.dependencies { implementation("com.varabyte.kobweb:kobweb-api") diff --git a/playground/site/src/jsMain/kotlin/playground/pages/Http.kt b/playground/site/src/jsMain/kotlin/playground/pages/Http.kt new file mode 100644 index 000000000..02428c006 --- /dev/null +++ b/playground/site/src/jsMain/kotlin/playground/pages/Http.kt @@ -0,0 +1,91 @@ +package playground.pages + +import androidx.compose.runtime.* +import com.varabyte.kobweb.browser.http.get +import com.varabyte.kobweb.browser.http.http +import com.varabyte.kobweb.browser.http.patch +import com.varabyte.kobweb.browser.http.post +import com.varabyte.kobweb.browser.http.put +import com.varabyte.kobweb.core.Page +import kotlinx.browser.window +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.dom.Text +import playground.components.layouts.PageLayout + +@Serializable +data class Post( + @SerialName("id") + val id: Int, + @SerialName("title") + val title: String, + @SerialName("body") + val body: String, + @SerialName("userId") + val userId: Int +) + +@Page +@Composable +fun HttpPage() { + PageLayout("Http Serialization test") { + var post by remember { mutableStateOf(null) } + var posts by remember { mutableStateOf?>(null) } + + LaunchedEffect(Unit) { + val response = window.http.get>("https://jsonplaceholder.typicode.com/posts") + posts = response + } + + LaunchedEffect(Unit) { + val response = window.http.post( + "https://jsonplaceholder.typicode.com/posts", + body = """{"title": "foo", "body": "bar", "userId": 1}""".encodeToByteArray(), + headers = mapOf("Content-type" to "application/json") + ) + post = response + } + + LaunchedEffect(Unit) { + val response = window.http.put( + "https://jsonplaceholder.typicode.com/posts/1", + body = """{"id": 1, "title": "updated foo", "body": "updated bar", "userId": 1}""".encodeToByteArray(), + headers = mapOf("Content-type" to "application/json") + ) + post = response + } + + LaunchedEffect(Unit) { + val response = window.http.patch( + "https://jsonplaceholder.typicode.com/posts/1", + body = """{"title": "foo"}""".encodeToByteArray(), + headers = mapOf("Content-type" to "application/json") + ) + post = response + } + + Text("Post:") + P() + if (post != null) { + Text("Title: ${post!!.title}") + P() + Text("Body: ${post!!.body}") + } else { + Text("Loading...") + } + + Text("Posts:") + P() + if (posts != null) { + posts!!.forEach { p -> + Text("Title: ${p.title}") + P() + Text("Body: ${p.body}") + P() + } + } else { + Text("Loading...") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e31d1f004..d8d763525 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ include(":frontend:kobweb-compose") include(":frontend:kobweb-silk") include(":frontend:kobweb-worker") include(":frontend:kobweb-worker-interface") +include(":frontend:kobwebx-core-serialization") include(":frontend:silk-foundation") include(":frontend:silk-widgets") include(":frontend:silk-widgets-kobweb")