commonMain.com.adamratzman.spotify.models.serialization.SerializationUtils.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spotify-api-kotlin-jvm Show documentation
Show all versions of spotify-api-kotlin-jvm Show documentation
A Kotlin wrapper for the Spotify Web API.
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
package com.adamratzman.spotify.models.serialization
import com.adamratzman.spotify.GenericSpotifyApi
import com.adamratzman.spotify.SpotifyException
import com.adamratzman.spotify.models.CursorBasedPagingObject
import com.adamratzman.spotify.models.NeedsApi
import com.adamratzman.spotify.models.NullablePagingObject
import com.adamratzman.spotify.models.PagingObject
import com.adamratzman.spotify.models.PagingObjectBase
import com.adamratzman.spotify.models.instantiateLateinitsIfPagingObjects
import kotlin.reflect.KClass
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
internal val nonstrictJson =
Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
}
// Parse function that catches on parse exception
internal fun String.parseJson(producer: String.() -> T): T =
try {
producer(this)
} catch (e: Exception) {
throw SpotifyException.ParseException(
"Unable to parse $this (${e.message})",
e
)
}
// Generic object deserialization
internal fun String.toObject(serializer: KSerializer, api: GenericSpotifyApi?, json: Json): T {
return parseJson {
val obj = json.decodeFromString(serializer, this)
obj.instantiateLateinitsIfSpotifyObject(api)
obj
}
}
internal fun String.toList(serializer: KSerializer>, api: GenericSpotifyApi?, json: Json): List {
return parseJson {
json.decodeFromString(serializer, this).onEach { obj -> obj?.instantiateLateinitsIfSpotifyObject(api) }
}
}
internal fun Any.instantiateLateinitsIfSpotifyObject(api: GenericSpotifyApi?) {
if (api == null) return
if (this is NeedsApi) listOf(this).instantiateAllNeedsApiObjects(api)
this.instantiateLateinitsIfPagingObjects(api)
}
// Inner object deserialization
internal inline fun String.toInnerObject(serializer: KSerializer, innerName: String, json: Json): T {
val map = this.parseJson {
val t = (String.serializer() to serializer)
json.decodeFromString(MapSerializer(t.first, t.second), this)
}
return (map[innerName] ?: error("Inner object with name $innerName doesn't exist in $map"))
}
internal inline fun String.toInnerArray(
serializer: KSerializer>,
innerName: String,
json: Json
): List {
val map = this.parseJson {
val t = (String.serializer() to serializer)
json.decodeFromString(MapSerializer(t.first, t.second), this)
}
return (map[innerName] ?: error("Inner object with name $innerName doesn't exist in $map")).toList()
}
// Paging Object deserialization
internal fun String.toCursorBasedPagingObject(
tClazz: KClass,
tSerializer: KSerializer,
innerObjectName: String? = null,
api: GenericSpotifyApi,
json: Json,
arbitraryInnerNameAllowed: Boolean = false,
skipInnerNameFirstIfPossible: Boolean = true
): CursorBasedPagingObject {
if (innerObjectName != null || (arbitraryInnerNameAllowed && !skipInnerNameFirstIfPossible)) {
val jsonObjectRoot = (json.parseToJsonElement(this) as JsonObject)
val jsonElement =
innerObjectName?.let { jsonObjectRoot[it] } ?: jsonObjectRoot.keys.firstOrNull()?.let { jsonObjectRoot[it] }
?: throw SpotifyException.ParseException("Json element was null for class $tClazz (json $this)")
val objectString = jsonElement.toString()
val pagingObject = objectString.parseJson {
json.decodeFromString(CursorBasedPagingObject.serializer(tSerializer), this)
}
pagingObject.instantiateLateinitsForPagingObject(tClazz, api)
return pagingObject
}
try {
val pagingObject = parseJson { json.decodeFromString(CursorBasedPagingObject.serializer(tSerializer), this) }
pagingObject.instantiateLateinitsForPagingObject(tClazz, api)
return pagingObject
} catch (jde: SpotifyException.ParseException) {
if (!arbitraryInnerNameAllowed && jde.message?.contains("unable to parse", true) == true) {
return toCursorBasedPagingObject(
tClazz,
tSerializer,
innerObjectName,
api,
json,
arbitraryInnerNameAllowed = true,
skipInnerNameFirstIfPossible = false
)
} else throw jde
}
}
internal fun String.toNonNullablePagingObject(
tClazz: KClass,
tSerializer: KSerializer,
innerObjectName: String? = null,
api: GenericSpotifyApi,
json: Json,
arbitraryInnerNameAllowed: Boolean = false,
skipInnerNameFirstIfPossible: Boolean = true
): NullablePagingObject {
if (innerObjectName != null || (arbitraryInnerNameAllowed && !skipInnerNameFirstIfPossible)) {
val jsonObjectRoot = (json.parseToJsonElement(this) as JsonObject)
val jsonElement =
innerObjectName?.let { jsonObjectRoot[it] } ?: jsonObjectRoot.keys.firstOrNull()?.let { jsonObjectRoot[it] }
?: throw SpotifyException.ParseException("Json element was null for class $tClazz (json $this)")
val objectString = jsonElement.toString()
val pagingObject = objectString.parseJson {
json.decodeFromString(NullablePagingObject.serializer(tSerializer), this)
}
pagingObject.instantiateLateinitsForPagingObject(tClazz, api)
return pagingObject
}
try {
val pagingObject = this.parseJson { json.decodeFromString(NullablePagingObject.serializer(tSerializer), this) }
pagingObject.instantiateLateinitsForPagingObject(tClazz, api)
return pagingObject
} catch (jde: SpotifyException.ParseException) {
if (arbitraryInnerNameAllowed && jde.message?.contains("unable to parse", true) == true) {
return toNonNullablePagingObject(
tClazz,
tSerializer,
innerObjectName,
api,
json,
arbitraryInnerNameAllowed = true,
skipInnerNameFirstIfPossible = false
)
} else throw jde
}
}
internal inline fun String.toNonNullablePagingObject(
tSerializer: KSerializer,
innerObjectName: String? = null,
api: GenericSpotifyApi,
json: Json,
arbitraryInnerNameAllowed: Boolean = false,
skipInnerNameFirstIfPossible: Boolean = true
): PagingObject = toNullablePagingObject(
tSerializer,
innerObjectName,
api,
json,
arbitraryInnerNameAllowed,
skipInnerNameFirstIfPossible
).toPagingObject()
internal inline fun String.toNullablePagingObject(
tSerializer: KSerializer,
innerObjectName: String? = null,
api: GenericSpotifyApi,
json: Json,
arbitraryInnerNameAllowed: Boolean = false,
skipInnerNameFirstIfPossible: Boolean = true
): NullablePagingObject = toNonNullablePagingObject(
T::class,
tSerializer,
innerObjectName,
api,
json,
arbitraryInnerNameAllowed,
skipInnerNameFirstIfPossible
)
// Kotlin object -> JSON string transformations
internal fun Map.mapToJsonString() = JsonObject(this).toString()
internal fun List.instantiateAllNeedsApiObjects(api: GenericSpotifyApi) {
this.instantiateLateinitsIfPagingObjects(api)
asSequence().filterNotNull().map { member -> member.getMembersThatNeedApiInstantiation() }.flatten()
.distinct()
.filterNotNull().toList()
.forEach { member ->
member.api = api
member.instantiateLateinitsIfPagingObjects(api)
}
}
internal fun > PagingObjectBase.instantiateLateinitsForPagingObject(
tClazz: KClass?,
api: GenericSpotifyApi
) {
getMembersThatNeedApiInstantiation().instantiateAllNeedsApiObjects(api)
this.api = api
this.itemClazz = tClazz
}