diff --git a/documentation.md b/documentation.md index 8f3fec1c..607377c4 100644 --- a/documentation.md +++ b/documentation.md @@ -427,8 +427,8 @@ server. You can add it to your server like any other path: val auth = AuthEndpoints( path = path("auth"), - userSerializer = Serialization.module.serializer(), - idSerializer = Serialization.module.serializer(), + userSerializer = Serialization.module.contextualSerializerIfHandled(), + idSerializer = Serialization.module.contextualSerializerIfHandled(), authRequirement = AuthInfo<User>(), jwtSigner = userSigner, email = email, diff --git a/gradle/serverlibs.versions.toml b/gradle/serverlibs.versions.toml index 94a81746..e7e55b45 100644 --- a/gradle/serverlibs.versions.toml +++ b/gradle/serverlibs.versions.toml @@ -3,10 +3,10 @@ agp="8.2.2" androidDesugaring="2.0.4" angusMail="2.0.3" awsVersion="2.25.24" -awssdk="2.17.232" +awssdk="2.25.24" azureFunctions="3.1.0" azureStorage="12.25.3" -bouncyCastle="1.77" +bouncyCastle="1.78.1" bson="5.1.0" coroutines="1.8.1" dokka="1.9.20" @@ -20,10 +20,10 @@ graalVmNative="0.9.24" guava="33.0.0-jre" hierynomusSshj="0.38.0" javaJwt="4.4.0" -kaml="0.58.0" +kaml="0.61.0" kbson="0.5.0" kotlin="2.0.0" -kotlinXSerialization="1.6.3" +kotlinXSerialization="1.7.1" kotlinXDatetime="0.6.0" kotlinHtmlJvm="0.11.0" kotlinerCli="1.0.3" @@ -40,12 +40,13 @@ oneTimePass="2.4.0" orgCrac="0.1.3" postgresql="42.7.3" proguard="7.3.2" +#noinspection GradleDependency,OutdatedLibrary sentry9="1.7.30" sentry="7.9.0" -serializationLibs="1.6.3" +serializationLibs="1.7.1" serializationXmlUtil="0.86.3" -shadow="7.1.0" -xmlUtilJvm="0.86.3" +shadow="8.1.1" +xmlUtilJvm="0.90.1" clamAv="2.1.2" apacheTika="2.9.2" @@ -120,6 +121,7 @@ sentry9Logback={module="io.sentry:sentry-logback", version.ref="sentry9"} sentry={module="io.sentry:sentry", version.ref="sentry"} serialization={module="org.jetbrains.kotlin:kotlin-serialization", version.ref="kotlin"} serializationCbor={module="org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref="serializationLibs"} +serializationProtobuf={module="org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref="serializationLibs"} serializationProperties={module="org.jetbrains.kotlinx:kotlinx-serialization-properties", version.ref="serializationLibs"} xmlUtilJvm={module="io.github.pdvrieze.xmlutil:serialization-jvm", version.ref="xmlUtilJvm"} diff --git a/server-core/build.gradle.kts b/server-core/build.gradle.kts index 81dc787a..675500d6 100644 --- a/server-core/build.gradle.kts +++ b/server-core/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { api(serverlibs.mongoBson) api(serverlibs.kBson) api(serverlibs.kaml) + api(serverlibs.serializationProtobuf) api(serverlibs.kotlinReflect) implementation(serverlibs.bouncyCastleBcprov) implementation(serverlibs.bouncyCastleBcpkix) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningdb/Mask.kt b/server-core/src/main/kotlin/com/lightningkite/lightningdb/Mask.kt index 870e1506..e3af0c6f 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningdb/Mask.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningdb/Mask.kt @@ -1,5 +1,6 @@ package com.lightningkite.lightningdb +import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import org.slf4j.LoggerFactory @@ -83,7 +84,7 @@ data class Mask( } inline fun mask(builder: Mask.Builder.(DataClassPath)->Unit): Mask { - return Mask.Builder(serializerOrContextual()).apply { builder(path()) }.build() + return Mask.Builder(Serialization.module.contextualSerializerIfHandled()).apply { builder(path()) }.build() } operator fun Condition.invoke(map: Partial): Boolean? { diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningdb/UpdateRestrictions.kt b/server-core/src/main/kotlin/com/lightningkite/lightningdb/UpdateRestrictions.kt index cb1700b8..175ec067 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningdb/UpdateRestrictions.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningdb/UpdateRestrictions.kt @@ -1,5 +1,6 @@ package com.lightningkite.lightningdb +import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -72,5 +73,5 @@ data class UpdateRestrictions( * DSL for defining [UpdateRestrictions] */ inline fun updateRestrictions(builder: UpdateRestrictions.Builder.(DataClassPath)->Unit): UpdateRestrictions { - return UpdateRestrictions.Builder(serializerOrContextual()).apply { builder(path()) }.build() + return UpdateRestrictions.Builder(Serialization.module.contextualSerializerIfHandled()).apply { builder(path()) }.build() } diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthCallbackEndpoint.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthCallbackEndpoint.kt index 8ecb4b66..b9a546b6 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthCallbackEndpoint.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthCallbackEndpoint.kt @@ -1,10 +1,10 @@ package com.lightningkite.lightningserver.auth.oauth +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.core.ServerPath import com.lightningkite.lightningserver.http.* import com.lightningkite.lightningserver.serialization.* import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer class OauthCallbackEndpoint( path: ServerPath, @@ -67,7 +67,7 @@ inline fun ServerPath.oauthCallback( }, noinline onAccess: suspend (OauthResponse, STATE) -> HttpResponse ) = OauthCallbackEndpoint( - stateSerializer = Serialization.module.serializer(), + stateSerializer = Serialization.module.contextualSerializerIfHandled(), path = this, oauthProviderInfo = oauthProviderInfo, credentials = credentials, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthClientEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthClientEndpoints.kt index b1505885..744d1879 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthClientEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/oauth/OauthClientEndpoints.kt @@ -4,7 +4,6 @@ import com.lightningkite.lightningdb.* import com.lightningkite.lightningserver.auth.* import com.lightningkite.lightningserver.core.ServerPath import com.lightningkite.lightningserver.core.ServerPathGroup -import com.lightningkite.lightningserver.db.ModelInfoWithDefault import com.lightningkite.lightningserver.db.ModelRestEndpoints import com.lightningkite.lightningserver.db.ModelSerializationInfo import com.lightningkite.lightningserver.db.modelInfoWithDefault @@ -12,7 +11,6 @@ import com.lightningkite.lightningserver.encryption.secureHash import com.lightningkite.lightningserver.exceptions.NotFoundException import com.lightningkite.lightningserver.serialization.Serialization import com.lightningkite.lightningserver.typed.* -import kotlinx.serialization.serializer import java.util.Base64 import kotlin.random.Random @@ -32,8 +30,8 @@ class OauthClientEndpoints( val modelInfo = modelInfoWithDefault( serialization = ModelSerializationInfo( - serializer = Serialization.module.serializer(), - idSerializer = Serialization.module.serializer() + serializer = Serialization.module.contextualSerializerIfHandled(), + idSerializer = Serialization.module.contextualSerializerIfHandled() ), authOptions = @Suppress("UNCHECKED_CAST") (maintainPermissions as AuthOptions>) + noAuth, getBaseCollection = { database().collection() }, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt index b409fb0e..7525df1c 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt @@ -2,6 +2,7 @@ package com.lightningkite.lightningserver.auth +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.encryption.* import com.lightningkite.lightningserver.encryption.SecureHasher.* import com.lightningkite.lightningserver.exceptions.UnauthorizedException @@ -96,7 +97,7 @@ data class JwtSigner( ReplaceWith("token(subject, Duration.ofMillis(expireDuration))", "java.time.Duration") ) inline fun token(subject: T, expireDuration: Long): String = - token(Serialization.module.serializer(), subject, expireDuration.milliseconds) + token(Serialization.module.contextualSerializerIfHandled(), subject, expireDuration.milliseconds) @Deprecated( "Use the version with duration instead", @@ -106,7 +107,7 @@ data class JwtSigner( token(serializer, subject, expireDuration.milliseconds) inline fun token(subject: T, expireDuration: Duration = expiration): String = - token(Serialization.module.serializer(), subject, expireDuration) + token(Serialization.module.contextualSerializerIfHandled(), subject, expireDuration) fun token(serializer: KSerializer, subject: T, expireDuration: Duration = expiration): String { return hasher.signJwt(JwtClaims( @@ -119,7 +120,7 @@ data class JwtSigner( )) } - inline fun verify(token: String): T = verify(Serialization.module.serializer(), token) + inline fun verify(token: String): T = verify(Serialization.module.contextualSerializerIfHandled(), token) fun verify(serializer: KSerializer, token: String): T { return try { hasher.verifyJwt(token, audience)!!.sub!!.let { diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/CacheHandle.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/CacheHandle.kt index 179a4cb0..c6809861 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/CacheHandle.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/cache/CacheHandle.kt @@ -1,8 +1,8 @@ package com.lightningkite.lightningserver.cache +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer import kotlin.time.Duration /** @@ -25,4 +25,4 @@ class CacheHandle(val cache: () -> Cache, val key: String, val serializer: KS } inline operator fun (() -> Cache).get(key: String) = - CacheHandle(this, key, Serialization.module.serializer()) + CacheHandle(this, key, Serialization.module.contextualSerializerIfHandled()) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/core/ContentType.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/core/ContentType.kt index 8756ebe5..9e10f87d 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/core/ContentType.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/core/ContentType.kt @@ -51,6 +51,7 @@ class ContentType(val type: String, val subtype: String, val parameters: Map, reified T : HasId, reified ID : Compara noinline getCollection: () -> FieldCollection, noinline getBaseCollection: () -> FieldCollection = { getCollection() }, noinline forUser: suspend FieldCollection.(principal: USER) -> FieldCollection, - modelName: String = Serialization.module.serializer().descriptor.serialName.substringBefore('<') + modelName: String = Serialization.module.contextualSerializerIfHandled().descriptor.serialName.substringBefore('<') .substringAfterLast('.'), ) = ModelInfo( serialization = ModelSerializationInfo(), diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelInfoWithDefault.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelInfoWithDefault.kt index b325e6d5..42b96cc1 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelInfoWithDefault.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelInfoWithDefault.kt @@ -1,14 +1,10 @@ package com.lightningkite.lightningserver.db -import com.lightningkite.lightningdb.CollectionChanges -import com.lightningkite.lightningdb.FieldCollection -import com.lightningkite.lightningdb.HasId -import com.lightningkite.lightningdb.withChangeListeners +import com.lightningkite.lightningdb.* import com.lightningkite.lightningserver.auth.AuthOptions import com.lightningkite.lightningserver.auth.RequestAuth import com.lightningkite.lightningserver.serialization.Serialization import com.lightningkite.lightningserver.typed.AuthAccessor -import kotlinx.serialization.serializer @Suppress("DEPRECATION") @Deprecated("User newer version with auth accessor instead, as it enables more potential optimizations.") @@ -16,7 +12,7 @@ inline fun ?, reified T : HasId, reified ID : Compara noinline getCollection: () -> FieldCollection, noinline getBaseCollection: () -> FieldCollection = { getCollection() }, noinline forUser: suspend FieldCollection.(principal: USER) -> FieldCollection, - modelName: String = Serialization.module.serializer().descriptor.serialName.substringBefore('<').substringAfterLast('.'), + modelName: String = Serialization.module.contextualSerializerIfHandled().descriptor.serialName.substringBefore('<').substringAfterLast('.'), noinline defaultItem: suspend (auth: USER) -> T, noinline exampleItem: ()->T? = { null }, ): ModelInfoWithDefault = ModelInfoWithDefault( diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelSerializationInfo.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelSerializationInfo.kt index a4dc6824..17d042ef 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelSerializationInfo.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/db/ModelSerializationInfo.kt @@ -1,15 +1,19 @@ package com.lightningkite.lightningserver.db import com.lightningkite.lightningdb.HasId +import com.lightningkite.lightningdb.serializableProperties +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer -inline fun , reified ID : Comparable> ModelSerializationInfo() = - ModelSerializationInfo( - serializer = Serialization.module.serializer(), - idSerializer = Serialization.module.serializer(), +inline fun , reified ID : Comparable> ModelSerializationInfo(): ModelSerializationInfo { + val ser = Serialization.module.contextualSerializerIfHandled() + @Suppress("UNCHECKED_CAST") + return ModelSerializationInfo( + serializer = ser, + idSerializer = (ser.serializableProperties?.find { it.name == "_id" }?.serializer as? KSerializer) ?: Serialization.module.contextualSerializerIfHandled(), ) +} data class ModelSerializationInfo, ID : Comparable>( val serializer: KSerializer, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/exceptions/GroupedDatabaseExceptionReporter.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/exceptions/GroupedDatabaseExceptionReporter.kt index 64a03839..8f282cd9 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/exceptions/GroupedDatabaseExceptionReporter.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/exceptions/GroupedDatabaseExceptionReporter.kt @@ -11,7 +11,6 @@ import com.lightningkite.lightningserver.serialization.Serialization import com.lightningkite.now import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.serializer import java.net.NetworkInterface class GroupedDatabaseExceptionReporter(val packageName: String, val database: Database): ExceptionReporter { @@ -65,8 +64,8 @@ class GroupedDatabaseExceptionReporter(val packageName: String, val database: Da @Suppress("UNCHECKED_CAST") val modelInfo = modelInfoWithDefault( serialization = ModelSerializationInfo( - serializer = Serialization.module.serializer(), - idSerializer = Serialization.module.serializer() + serializer = Serialization.module.contextualSerializerIfHandled(), + idSerializer = Serialization.module.contextualSerializerIfHandled() ), authOptions = Authentication.isDeveloper as AuthOptions>, getBaseCollection = { database.collection() }, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/forms/FormSerialization.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/forms/FormSerialization.kt index f462f73d..397218ca 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/forms/FormSerialization.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/forms/FormSerialization.kt @@ -1,6 +1,7 @@ package com.lightningkite.lightningserver.forms +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.html.FlowContent import kotlinx.serialization.SerializationException @@ -9,12 +10,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.serializer typealias HtmlRenderer = FlowContent.(inputKey: String, value: T) -> Unit class HtmlSerializer(val serializersModule: SerializersModule = EmptySerializersModule(), val module: Module) { - inline fun render(value: T, into: FlowContent) = render(Serialization.module.serializer(), value, into) + inline fun render(value: T, into: FlowContent) = render(Serialization.module.contextualSerializerIfHandled(), value, into) fun render(serializer: SerializationStrategy, value: T, into: FlowContent) { HtmlEncoder(into).encodeSerializableValue(serializer, value) } diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/Serialization.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/Serialization.kt index 222ecfb4..6e39d390 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/Serialization.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/Serialization.kt @@ -30,8 +30,13 @@ import nl.adaptivity.xmlutil.serialization.XML import java.math.BigDecimal import kotlinx.datetime.* import kotlinx.serialization.* +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.protobuf.ProtoBuf import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi import nl.adaptivity.xmlutil.serialization.XmlSerializationPolicy +import org.bson.BsonDocument +import org.bson.BsonDocumentWriter +import org.bson.BsonValue import java.util.* import kotlin.collections.HashMap @@ -54,16 +59,25 @@ abstract class Serialization { val h = v.fileObject.head() when { h == null -> ValidationIssuePart(1, "File does not exist") - h.size > t.maxSize -> ValidationIssuePart(1, "File is too big; max size is ${t.maxSize} bytes but file is ${h.size} bytes") - t.types.none { h.type.matches(ContentType(it)) } -> ValidationIssuePart(1, "File type ${h.type} does not match ${t.types.joinToString("; ")}") + h.size > t.maxSize -> ValidationIssuePart( + 1, + "File is too big; max size is ${t.maxSize} bytes but file is ${h.size} bytes" + ) + + t.types.none { h.type.matches(ContentType(it)) } -> ValidationIssuePart( + 1, + "File type ${h.type} does not match ${t.types.joinToString("; ")}" + ) + else -> null } } } + suspend fun validateOrThrow(serializer: SerializationStrategy, value: T) { val out = ArrayList() module.validate(serializer, value) { out.add(it) } - if(out.isNotEmpty()) { + if (out.isNotEmpty()) { throw BadRequestException( detail = "validation-failed", message = out.joinToString("; ") { "${it.path.joinToString(".")}: ${it.text}" }, @@ -100,7 +114,6 @@ abstract class Serialization { } var xml: XML by SetOnce { XML(module) { - this.xmlDeclMode = XmlDeclMode.Auto this.repairNamespaces = true } } @@ -126,6 +139,9 @@ abstract class Serialization { var formData: FormDataFormat by SetOnce { FormDataFormat(StringDeferringConfig(module, deferredFormat = json)) } + var protobuf: ProtoBuf by SetOnce { + ProtoBuf { this.serializersModule = module } + } interface HttpContentParser { val contentType: ContentType @@ -181,22 +197,67 @@ abstract class Serialization { handler(FormDataHandler { formData }) handler(JsonFormatHandler(json = { json }, jsonWithoutDefaults = { jsonWithoutDefaults })) handler(CsvFormatHandler({ csv })) + handler(StringFormatHandler({ object: StringFormat { + override val serializersModule: SerializersModule + get() = yaml.serializersModule + + override fun decodeFromString(deserializer: DeserializationStrategy, string: String): T = + yaml.decodeFromStringPrimitiveSafe(deserializer, string) + + override fun encodeToString(serializer: SerializationStrategy, value: T): String = + yaml.encodeToStringPrimitiveSafe(serializer, value) + + } }, ContentType.Text.Yaml)) handler(BinaryFormatHandler({ cbor }, ContentType.Application.Cbor)) handler(BinaryFormatHandler({ object : BinaryFormat { override val serializersModule: SerializersModule get() = bson.serializersModule - override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T { - return bson.load(deserializer, bytes) - } + override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T = + bson.decodeFromByteArray(deserializer, bytes) - override fun encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray { - return bson.dump(serializer, value) - } + override fun encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray = + bson.encodeToByteArray(serializer, value) } }, ContentType.Application.Bson)) parser(MultipartJsonHandler { json }) } + + fun enablePublicJavaData() { + handler(BinaryFormatHandler({ javaData }, ContentType.Application.StructuredBytes)) + } + fun enablePublicProtobuf() { + handler(BinaryFormatHandler({ protobuf }, ContentType.Application.ProtoBuf)) + } + fun enablePublicXml() { + handler(StringFormatHandler({ xml }, ContentType.Text.Xml)) + } +} + +@Serializable +private data class PrimitiveHolder(val value: T) + +fun KBson.decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T { + if (deserializer.descriptor.kind != StructureKind.CLASS && deserializer.descriptor.kind != StructureKind.MAP) + return load(PrimitiveHolder.serializer(deserializer as KSerializer), bytes).value + else return load(deserializer, bytes) +} + +fun KBson.encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray { + if (serializer.descriptor.kind != StructureKind.CLASS && serializer.descriptor.kind != StructureKind.MAP) + return dump(PrimitiveHolder.serializer(serializer as KSerializer), PrimitiveHolder(value)) + else return dump(serializer, value) +} + +fun Yaml.decodeFromStringPrimitiveSafe(deserializer: DeserializationStrategy, text: String): T { + if (deserializer.descriptor.kind != StructureKind.CLASS && deserializer.descriptor.kind != StructureKind.MAP) + return decodeFromString(PrimitiveHolder.serializer(deserializer as KSerializer), text).value + else return decodeFromString(deserializer, text) } +fun Yaml.encodeToStringPrimitiveSafe(serializer: SerializationStrategy, value: T): String { + if (serializer.descriptor.kind != StructureKind.CLASS && serializer.descriptor.kind != StructureKind.MAP) + return encodeToString(PrimitiveHolder.serializer(serializer as KSerializer), PrimitiveHolder(value)) + else return encodeToString(serializer, value) +} \ No newline at end of file diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/transformations.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/transformations.kt index e3e9b5c3..ac3b17f9 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/transformations.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/serialization/transformations.kt @@ -1,5 +1,6 @@ package com.lightningkite.lightningserver.serialization +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.core.ContentType import com.lightningkite.lightningserver.exceptions.BadRequestException import com.lightningkite.lightningserver.http.HttpContent @@ -29,7 +30,7 @@ fun HttpRequest.queryParameters(serializer: KSerializer): T { } } -suspend inline fun HttpContent.parse(): T = parse(Serialization.module.serializer()) +suspend inline fun HttpContent.parse(): T = parse(Serialization.module.contextualSerializerIfHandled()) suspend fun HttpContent.parse(serializer: KSerializer): T { try { val parser = Serialization.parsers[this.type] @@ -59,7 +60,7 @@ suspend fun HttpContent.parseWithDefault(serializer: KSerializer, default } suspend inline fun T.toHttpContent(acceptedTypes: List): HttpContent? = - toHttpContent(acceptedTypes, Serialization.module.serializer()) + toHttpContent(acceptedTypes, Serialization.module.contextualSerializerIfHandled()) suspend fun T.toHttpContent(acceptedTypes: List, serializer: KSerializer): HttpContent? { if (this == Unit) return null diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/settings/Settings.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/settings/Settings.kt index 53425e87..1d02c756 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/settings/Settings.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/settings/Settings.kt @@ -1,5 +1,6 @@ package com.lightningkite.lightningserver.settings +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.encryption.secretBasis import com.lightningkite.lightningserver.exceptions.exceptionSettings import com.lightningkite.lightningserver.logger @@ -10,7 +11,6 @@ import com.lightningkite.lightningserver.metrics.metricsSettings import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException -import kotlinx.serialization.serializer import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean @@ -117,7 +117,7 @@ inline fun setting( name = name, default = default, optional = optional, - serializer = Serialization.module.serializer(), + serializer = Serialization.module.contextualSerializerIfHandled(), description = description, getter = { it } ) @@ -132,7 +132,7 @@ inline fun Goal, Goal> setting( name = name, default = default, optional = optional, - serializer = Serialization.module.serializer(), + serializer = Serialization.module.contextualSerializerIfHandled(), description = description, getter = { it() } ) @@ -147,7 +147,7 @@ inline fun Goal, Goal : Metricable> setting( name = name, default = default, optional = optional, - serializer = Serialization.module.serializer(), + serializer = Serialization.module.contextualSerializerIfHandled(), description = description, getter = { it().withMetrics(name) } ) diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/tasks/dsl.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/tasks/dsl.kt index 1c545882..7beee3b4 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/tasks/dsl.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/tasks/dsl.kt @@ -11,7 +11,6 @@ import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseContextualSerialization -import kotlinx.serialization.serializer import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -20,7 +19,7 @@ inline fun task( name: String, noinline implementation: suspend Task.RunningTask.(INPUT) -> Unit, ) = - task(name, Serialization.module.serializer(), implementation) + task(name, Serialization.module.contextualSerializerIfHandled(), implementation) @LightningServerDsl fun task( diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.kt index 90a17f86..7d4b16f1 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.kt @@ -1,12 +1,12 @@ package com.lightningkite.lightningserver.typed import com.lightningkite.lightningdb.HasId +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.LSError import com.lightningkite.lightningserver.auth.* import com.lightningkite.lightningserver.core.LightningServerDsl import com.lightningkite.lightningserver.core.ServerPath import com.lightningkite.lightningserver.exceptions.BadRequestException -import com.lightningkite.lightningserver.exceptions.UnauthorizedException import com.lightningkite.lightningserver.http.* import com.lightningkite.lightningserver.serialization.Serialization import com.lightningkite.lightningserver.serialization.parse @@ -14,7 +14,6 @@ import com.lightningkite.lightningserver.serialization.queryParameters import com.lightningkite.lightningserver.serialization.toHttpContent import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.serializer import java.net.URLDecoder data class ApiEndpoint?, PATH: TypedServerPath, INPUT, OUTPUT>( @@ -118,8 +117,8 @@ inline fun ?, PATH: TypedServerPath, reified INPUT, reified OUTPU val api = ApiEndpoint( route = this, authOptions = authOptions, - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, errorCases = errorCases, @@ -177,8 +176,8 @@ inline fun ?, reified INPUT, reified OUTPUT> HttpEndpoint.api( val api = ApiEndpoint( route = TypedHttpEndpoint(TypedServerPath0(path), method), authOptions = authOptions, - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, errorCases = errorCases, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.old.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.old.kt index 0c7fc1e5..d542d369 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.old.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/ApiEndpoint.old.kt @@ -4,6 +4,7 @@ package com.lightningkite.lightningserver.typed import kotlin.time.Duration import com.lightningkite.lightningdb.HasId +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.LSError import com.lightningkite.lightningserver.auth.* import com.lightningkite.lightningserver.core.LightningServerDsl @@ -11,7 +12,6 @@ import com.lightningkite.lightningserver.core.ServerPath import com.lightningkite.lightningserver.http.* import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer import kotlin.reflect.typeOf /** @@ -43,8 +43,8 @@ inline fun HttpEndpoint.type ) } ), - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, @@ -118,9 +118,9 @@ inline fun Htt ) } ), - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), - pathType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), + pathType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, @@ -194,10 +194,10 @@ inline fun ?, PATH: TypedServerPath, INPUT, OUTPUT>( @@ -85,8 +85,8 @@ inline fun ?, PATH: TypedServerPath, reified INPUT, reifi noinline disconnect: suspend TypedWebSocketSender.() -> Unit, ): ApiWebsocket = apiWebsocket( authOptions = authOptions(), - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, errorCases = errorCases, @@ -137,8 +137,8 @@ inline fun ?, reified INPUT, reified OUTPUT> ServerPath.a noinline disconnect: suspend TypedWebSocketSender.() -> Unit, ): ApiWebsocket = apiWebsocket( authOptions = authOptions(), - inputType = Serialization.module.serializer(), - outputType = Serialization.module.serializer(), + inputType = Serialization.module.contextualSerializerIfHandled(), + outputType = Serialization.module.contextualSerializerIfHandled(), summary = summary, description = description, errorCases = errorCases, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/TypedServerPath.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/TypedServerPath.kt index 8ef4db8d..36f13bb4 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/TypedServerPath.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/TypedServerPath.kt @@ -1,12 +1,12 @@ package com.lightningkite.lightningserver.typed +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.core.LightningServerDsl import com.lightningkite.lightningserver.core.ServerPath import com.lightningkite.lightningserver.http.HttpEndpoint import com.lightningkite.lightningserver.http.HttpMethod import com.lightningkite.lightningserver.serialization.Serialization import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializer sealed interface TypedServerPath { val path: ServerPath @@ -53,12 +53,12 @@ data class TypedHttpEndpoint( @LightningServerDsl inline fun TypedServerPath0.arg(name: String, description: String? = null): TypedServerPath1 = TypedServerPath1( path = path.copy(segments = path.segments + ServerPath.Segment.Wildcard(name)), - a = TypedServerPathParameter(name, description, Serialization.module.serializer()), + a = TypedServerPathParameter(name, description, Serialization.module.contextualSerializerIfHandled()), ) @LightningServerDsl inline fun ServerPath.arg(name: String, description: String? = null): TypedServerPath1 = TypedServerPath1( path = copy(segments = segments + ServerPath.Segment.Wildcard(name)), - a = TypedServerPathParameter(name, description, Serialization.module.serializer()), + a = TypedServerPathParameter(name, description, Serialization.module.contextualSerializerIfHandled()), ) @LightningServerDsl fun ServerPath.arg(name: String, serializer: KSerializer, description: String? = null): TypedServerPath1 = TypedServerPath1( diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autodoc.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autodoc.kt index 0ce199b6..d5085a6b 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autodoc.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autodoc.kt @@ -20,6 +20,7 @@ import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.descriptors.elementNames import kotlinx.serialization.internal.GeneratedSerializer +import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator import kotlinx.serialization.serializer import kotlin.reflect.KType @@ -56,6 +57,17 @@ fun ServerPath.apiDocs(packageName: String = "com.mypackage"): HttpEndpoint { ) ) } + get("sdk.protobuf").handler { + HttpResponse( + HttpContent.Text( + string = ProtoBufSchemaGenerator.generateSchemaText( + Documentable.usedTypes.map { it.descriptor }, + packageName = packageName + ), + type = ContentType.Application.ProtoBufDeclaration + ) + ) + } return this.copy(after = ServerPath.Afterwards.TrailingSlash).get.handler { _ -> HttpResponse(body = HttpContent.html { head { title("${generalSettings().projectName} - Generated Documentation") } diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autoform.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autoform.kt index 62defe8b..42dc90b4 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autoform.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/typed/autoform.kt @@ -1,6 +1,7 @@ package com.lightningkite.lightningserver.typed import com.lightningkite.lightningdb.ServerFile +import com.lightningkite.lightningdb.contextualSerializerIfHandled import com.lightningkite.lightningserver.files.UploadEarlyEndpoint import com.lightningkite.lightningserver.jsonschema.schema import com.lightningkite.lightningserver.routes.fullUrl @@ -12,7 +13,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.getContextualDescriptor import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer fun HEAD.includeFormScript() { @@ -50,7 +50,7 @@ inline fun FORM.insideHtmlForm( collapsed: Boolean = false, uploadEarlyEndpoint: UploadEarlyEndpoint? = UploadEarlyEndpoint.default, ): Unit = - insideHtmlForm(title, jsEditorName, Serialization.module.serializer(), defaultValue, collapsed, uploadEarlyEndpoint) + insideHtmlForm(title, jsEditorName, Serialization.module.contextualSerializerIfHandled(), defaultValue, collapsed, uploadEarlyEndpoint) fun FORM.insideHtmlForm( title: String, @@ -109,7 +109,7 @@ inline fun FlowContent.jsForm( jsEditorName: String, defaultValue: T? = null, collapsed: Boolean = false, -) = jsForm(title, jsEditorName, Serialization.module.serializer(), defaultValue, collapsed) +) = jsForm(title, jsEditorName, Serialization.module.contextualSerializerIfHandled(), defaultValue, collapsed) fun FlowContent.jsForm( title: String, @@ -173,7 +173,7 @@ inline fun FlowContent.display( jsEditorName: String, defaultValue: T? = null, collapsed: Boolean = false, -) = displayUntyped(title, jsEditorName, Serialization.module.serializer(), defaultValue, collapsed) +) = displayUntyped(title, jsEditorName, Serialization.module.contextualSerializerIfHandled(), defaultValue, collapsed) fun FlowContent.displayUntyped( title: String, diff --git a/server-core/src/test/kotlin/com/lightningkite/lightningserver/serialization/SerializationTest.kt b/server-core/src/test/kotlin/com/lightningkite/lightningserver/serialization/SerializationTest.kt index 763a4c9a..f8fee97a 100644 --- a/server-core/src/test/kotlin/com/lightningkite/lightningserver/serialization/SerializationTest.kt +++ b/server-core/src/test/kotlin/com/lightningkite/lightningserver/serialization/SerializationTest.kt @@ -2,19 +2,19 @@ package com.lightningkite.lightningserver.serialization import com.lightningkite.lightningserver.metrics.roundTo import com.lightningkite.uuid -import kotlinx.datetime.Clock import com.lightningkite.now import org.junit.Assert.* import org.junit.Test import kotlinx.datetime.Instant -import java.util.* import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds import com.lightningkite.UUID -import com.lightningkite.uuid +import com.lightningkite.lightningdb.* import kotlinx.serialization.* -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModuleCollector +import kotlin.reflect.KClass +import kotlin.test.assertIs +import kotlin.test.assertIsNot @Serializable data class BsonSerTest( @@ -29,4 +29,46 @@ class SerializationTest { println(Serialization.bson.stringify(BsonSerTest.serializer(), v).toJson()) assertEquals(v, Serialization.bson.load(BsonSerTest.serializer(), Serialization.bson.dump(BsonSerTest.serializer(), v))) } + @Test fun contextual() { + assertIs>(Serialization.module.contextualSerializerIfHandled()) + assertIs>(Serialization.module.contextualSerializerIfHandled().nullElement()) + assertIs>(Serialization.module.contextualSerializerIfHandled>().listElement()) + ClientModule.dumpTo(object: SerializersModuleCollector { + override fun contextual( + kClass: KClass, + provider: (typeArgumentsSerializers: List>) -> KSerializer<*> + ) { + if(kClass.typeParameters.isEmpty()) { + println("${kClass} -> ${provider(listOf())}") + } + } + + override fun polymorphic( + baseClass: KClass, + actualClass: KClass, + actualSerializer: KSerializer + ) { +// println("$baseClass -> $") + } + + override fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { +// println("$baseClass -> $") + } + + override fun polymorphicDefaultSerializer( + baseClass: KClass, + defaultSerializerProvider: (value: Base) -> SerializationStrategy? + ) { +// println("$baseClass -> $") + } + }) + assertIs(ClientModule.getContextual()) + assertIs(ClientModule.serializerPreferContextual()) + assertIsNot>(ClientModule.contextualSerializerIfHandled()) + assertIs>(ClientModule.contextualSerializerIfHandled()) + assertIs>(EmptySerializersModule().contextualSerializerIfHandled()) + } } \ No newline at end of file diff --git a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/MaskTest.kt b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/MaskTest.kt index 29362f6c..741e8dc9 100644 --- a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/MaskTest.kt +++ b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/MaskTest.kt @@ -90,7 +90,7 @@ class MaskTest { val notMatchingSortA = SortPart(path().byte) val notMatchingSortV = SortPart(path().string) - val mask = mask { always(it.int.maskedTo(2)) } + val mask = mask { always(it.int.maskedTo(2)) } assertTrue(mask.permitSort(listOf(matchingSort)) is Condition.Never) assertTrue(mask.permitSort(listOf(notMatchingSortA)) is Condition.Always) @@ -104,7 +104,7 @@ class MaskTest { val notMatchingSortA = condition { it.byte eq 2 } val notMatchingSortV = condition { it.string eq "" } - val mask = mask { always(it.int.maskedTo(2)) } + val mask = mask { always(it.int.maskedTo(2)) } assertTrue(mask(matchingSort) is Condition.Never) assertTrue(mask(notMatchingSortA) is Condition.Always) diff --git a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt index cbe140b8..f381aec4 100644 --- a/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt +++ b/server-testing/src/main/kotlin/com/lightningkite/lightningdb/test/models.kt @@ -150,6 +150,26 @@ data class LargeTestModel( companion object } +@GenerateDataClassPaths +@Serializable +data class SimpleLargeTestModel( + override val _id: UUID = uuid(), + var boolean: Boolean = false, + var byte: Byte = 0, + var short: Short = 0, + @Index var int: Int = 0, + var long: Long = 0, + var float: Float = 0f, + var double: Double = 0.0, + var char: Char = ' ', + var string: String = "", + var uuid: UUID = UUID(0L, 0L), + var instant: Instant = Instant.fromEpochMilliseconds(0L), + var listEmbedded: List = listOf(), +) : HasId { + companion object +} + @GenerateDataClassPaths @Serializable data class GeoTest( diff --git a/server-testing/src/test/kotlin/com/lightningkite/lightningdb/test/ModificationSimplifyTest.kt b/server-testing/src/test/kotlin/com/lightningkite/lightningdb/test/ModificationSimplifyTest.kt index 7bed9e48..d5fd1fee 100644 --- a/server-testing/src/test/kotlin/com/lightningkite/lightningdb/test/ModificationSimplifyTest.kt +++ b/server-testing/src/test/kotlin/com/lightningkite/lightningdb/test/ModificationSimplifyTest.kt @@ -15,12 +15,12 @@ class ModificationSimplifyTest { fun test() { prepareModels() val item = LargeTestModel() - val mods = listOf, Modification>>( - modification { it.int assign 1 } to modification { it.int assign 1 }, - modification { + val mods = listOf( + modification { it.int assign 1 } to modification { it.int assign 1 }, + modification { it assign item it.int assign 1 - } to modification { it assign item.copy(int = 1) }, + } to modification { it assign item.copy(int = 1) }, ) for((pre, post) in mods) { assertEquals(post, pre.simplify().also { println("$pre -> $it") }) diff --git a/server-testing/src/test/kotlin/com/lightningkite/lightningserver/TestSettings.kt b/server-testing/src/test/kotlin/com/lightningkite/lightningserver/TestSettings.kt index c2892007..1c7a68cc 100644 --- a/server-testing/src/test/kotlin/com/lightningkite/lightningserver/TestSettings.kt +++ b/server-testing/src/test/kotlin/com/lightningkite/lightningserver/TestSettings.kt @@ -1,8 +1,11 @@ package com.lightningkite.lightningserver +import com.lightningkite.UUID import com.lightningkite.lightningdb.ServerFile import com.lightningkite.lightningdb.collection import com.lightningkite.lightningdb.insertOne +import com.lightningkite.lightningdb.test.LargeTestModel +import com.lightningkite.lightningdb.test.SimpleLargeTestModel import com.lightningkite.lightningdb.test.User import com.lightningkite.lightningdb.test.ValidatedModel import com.lightningkite.lightningserver.auth.JwtSigner @@ -29,6 +32,7 @@ import com.lightningkite.lightningserver.files.FilesSettings import com.lightningkite.lightningserver.files.UploadEarlyEndpoint import com.lightningkite.lightningserver.files.fileObject import com.lightningkite.lightningserver.http.post +import com.lightningkite.lightningserver.serialization.Serialization import com.lightningkite.lightningserver.settings.Settings import com.lightningkite.lightningserver.settings.setting import com.lightningkite.lightningserver.sms.SMSSettings @@ -54,6 +58,10 @@ object TestSettings: ServerPathGroup(ServerPath.root) { val oauthGithub = setting("oauth_github", null) val oauthMicrosoft = setting("oauth_microsoft", null) + init { + Serialization.enablePublicJavaData() + Serialization.enablePublicProtobuf() + } val info = modelInfo( getBaseCollection = { database().collection() }, @@ -75,6 +83,8 @@ object TestSettings: ServerPathGroup(ServerPath.root) { val sample2 = path("sample2").post.api(summary = "Test2", authOptions = noAuth) { input: Int -> input + 42 } val sample3 = path("sample3").post.api(summary = "Test3", authOptions = authRequired { false }) { input: Int -> input + 42 } val sample4 = path("sample4").post.api(summary = "Test4", authOptions = noAuth) { input: ValidatedModel -> input } + val sample5 = path("sample5").post.api(summary = "Test5", authOptions = noAuth) { input: UUID -> input } + val sample6 = path("sample6").post.api(summary = "Test5", authOptions = noAuth) { input: SimpleLargeTestModel -> input } val bulk = path("bulk").bulkRequestEndpoint() init { diff --git a/server-testing/src/test/kotlin/com/lightningkite/lightningserver/typed/ApiEndpointValidateTest.kt b/server-testing/src/test/kotlin/com/lightningkite/lightningserver/typed/ApiEndpointValidateTest.kt index cd653654..1f17c791 100644 --- a/server-testing/src/test/kotlin/com/lightningkite/lightningserver/typed/ApiEndpointValidateTest.kt +++ b/server-testing/src/test/kotlin/com/lightningkite/lightningserver/typed/ApiEndpointValidateTest.kt @@ -1,12 +1,22 @@ package com.lightningkite.lightningserver.typed +import com.lightningkite.UUID +import com.lightningkite.lightningdb.contextualSerializerIfHandled +import com.lightningkite.lightningdb.test.ClassUsedForEmbedding +import com.lightningkite.lightningdb.test.LargeTestModel +import com.lightningkite.lightningdb.test.SimpleLargeTestModel import com.lightningkite.lightningdb.test.ValidatedModel import com.lightningkite.lightningserver.TestSettings +import com.lightningkite.lightningserver.core.ContentType import com.lightningkite.lightningserver.exceptions.BadRequestException import com.lightningkite.lightningserver.http.HttpContent import com.lightningkite.lightningserver.http.HttpHeader import com.lightningkite.lightningserver.http.HttpHeaders import com.lightningkite.lightningserver.http.HttpRequest +import com.lightningkite.lightningserver.serialization.Serialization +import com.lightningkite.lightningserver.serialization.encodeToByteArray +import com.lightningkite.now +import com.lightningkite.uuid import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertFailsWith @@ -42,4 +52,67 @@ class ApiEndpointValidateTest { } } } + + @Test + fun testPrimitiveFormats() { + runBlocking { + TestSettings + for (formatType in Serialization.parsers.keys.toSet().intersect(Serialization.emitters.keys.toSet())) { + println("Checking $formatType") + val parser = Serialization.parsers[formatType]!! + val emitter = Serialization.emitters[formatType]!! + TestSettings.sample5( + HttpRequest( + endpoint = TestSettings.sample5.route.endpoint, + parts = mapOf(), + headers = HttpHeaders { set(HttpHeader.Accept, formatType.toString()) }, + body = emitter(formatType, Serialization.module.contextualSerializerIfHandled(), uuid()) + ) + ).also { + println(it) + parser(it.body!!, Serialization.module.contextualSerializerIfHandled()) + } + } + } + } + + @Test + fun testFormats() { + runBlocking { + TestSettings + for (formatType in Serialization.parsers.keys.toSet().intersect(Serialization.emitters.keys.toSet())) { + println("Checking $formatType") + val parser = Serialization.parsers[formatType]!! + val emitter = Serialization.emitters[formatType]!! + TestSettings.sample6( + HttpRequest( + endpoint = TestSettings.sample6.route.endpoint, + parts = mapOf(), + headers = HttpHeaders { set(HttpHeader.Accept, formatType.toString()) }, + body = emitter(formatType, Serialization.module.contextualSerializerIfHandled(), SimpleLargeTestModel( + boolean = true, + byte = 100, + short = 1000, + int = 10000, + long = 100000, + float = 0.5f, + double = 0.75, + char = 'A', + string = "Test", + uuid = uuid(), + instant = now(), + listEmbedded = listOf( + ClassUsedForEmbedding("first", 1), + ClassUsedForEmbedding("second", 2), + ), +// map = mapOf("first" to 1, "second" to 2), + )) + ) + ).also { + println(it) + parser(it.body!!, Serialization.module.contextualSerializerIfHandled()) + } + } + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/ConditionBuilder.kt b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/ConditionBuilder.kt index 362e4f1f..d6e9ec0a 100644 --- a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/ConditionBuilder.kt +++ b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/ConditionBuilder.kt @@ -2,12 +2,12 @@ package com.lightningkite.lightningdb import com.lightningkite.Distance import com.lightningkite.GeoCoordinate -import com.lightningkite.lightningdb.SerializableProperty import com.lightningkite.miles +import kotlinx.serialization.serializer import kotlin.js.JsName import kotlin.jvm.JvmName -inline fun path(): DataClassPath = DataClassPathSelf(serializerOrContextual()) +inline fun path(): DataClassPath = DataClassPathSelf(serializer()) inline fun condition(setup: (DataClassPath) -> Condition): Condition = path().let(setup) diff --git a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/Partial.kt b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/Partial.kt index 4b1f5f85..6f4d6749 100644 --- a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/Partial.kt +++ b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/Partial.kt @@ -1,6 +1,7 @@ package com.lightningkite.lightningdb import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer data class Partial(val parts: MutableMap, Any?> = mutableMapOf()) { constructor(item: T, paths: Set>) : this() { @@ -36,7 +37,7 @@ inline fun partialOf(builder: PartialBuilder.(DataClassPathSelf().apply { builder( DataClassPathSelf( - serializerOrContextual() + serializer() ) ) }.parts.let { Partial(it) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/SerializationHelpers.kt b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/SerializationHelpers.kt index 0c41e2ab..fee51dbd 100644 --- a/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/SerializationHelpers.kt +++ b/shared/src/commonMain/kotlin/com/lightningkite/lightningdb/SerializationHelpers.kt @@ -3,6 +3,7 @@ package com.lightningkite.lightningdb import kotlinx.serialization.* +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialKind @@ -15,6 +16,7 @@ import kotlinx.serialization.internal.GeneratedSerializer import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule import kotlin.reflect.KClass +import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -204,7 +206,59 @@ fun KSerializer<*>.tryTypeParameterSerializers2(): Array>? = when fun KSerializer<*>.tryChildSerializers(): Array>? = (this as? GeneratedSerializer<*>)?.childSerializers() @Suppress("UNCHECKED_CAST") -inline fun serializerOrContextual(): KSerializer { - val t = typeOf() - return (serializerOrNull(t) ?: ContextualSerializer(t.classifier as KClass<*>)) as KSerializer -} \ No newline at end of file +inline fun serializerOrContextual(): KSerializer = serializerOrContextual(typeOf()) as KSerializer +fun serializerOrContextual(type: KType): KSerializer<*> { + val args = type.arguments.map { serializerOrContextual(it.type!!) } + val kclass = type.classifier as KClass<*> + return try { + EmptySerializersModule().serializer( + kClass = kclass, + typeArgumentsSerializers = args, + isNullable = type.isMarkedNullable + ) + } catch(e: SerializationException) { + ContextualSerializer(kclass, null, args.toTypedArray()).let { + @Suppress("UNCHECKED_CAST") + if(type.isMarkedNullable) (it as KSerializer).nullable + else it + } + } +} + +@Suppress("UNCHECKED_CAST") +inline fun SerializersModule.contextualSerializerIfHandled(): KSerializer = contextualSerializerIfHandled(typeOf()) as KSerializer +fun SerializersModule.contextualSerializerIfHandled(type: KType): KSerializer<*> { + val args = type.arguments.map { contextualSerializerIfHandled(it.type!!) } + val kclass = type.classifier as KClass<*> + return try { + getContextual(type)?.let { c -> + ContextualSerializer(kclass, null, args.toTypedArray()).let { + @Suppress("UNCHECKED_CAST") + if(type.isMarkedNullable) (it as KSerializer).nullable + else it + } + } ?: EmptySerializersModule().serializer( + kClass = kclass, + typeArgumentsSerializers = args, + isNullable = type.isMarkedNullable + ) + } catch(e: SerializationException) { + ContextualSerializer(kclass, null, args.toTypedArray()).let { + @Suppress("UNCHECKED_CAST") + if(type.isMarkedNullable) (it as KSerializer).nullable + else it + } + } +} + +@Suppress("UNCHECKED_CAST") +inline fun SerializersModule.serializerPreferContextual(): KSerializer = serializerPreferContextual(typeOf()) as KSerializer +fun SerializersModule.serializerPreferContextual(type: KType): KSerializer<*> { + return this.getContextual(type.classifier as KClass<*>, type.arguments.map { serializerPreferContextual(it.type!!) }) ?: serializer(type) +} + +@Suppress("UNCHECKED_CAST") +inline fun SerializersModule.getContextual(): KSerializer? = getContextual(typeOf()) as KSerializer? +fun SerializersModule.getContextual(type: KType): KSerializer<*>? { + return this.getContextual(type.classifier as KClass<*>, type.arguments.map { getContextual(it.type!!) ?: serializer(it.type!!) }) +}