From 171e413c5f9c812271cf2ca802bb0c102fc87803 Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 4 Dec 2025 17:05:07 +0100 Subject: [PATCH] v1.0.5-SNAPSHOT started json and kotlinx serialization support --- docs/json_and_kotlin_serialization.md | 43 +++++++++++++++++++ docs/tutorial.md | 4 +- lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/obj/Obj.kt | 38 +++++++++++++++- .../kotlin/net/sergeych/lyng/obj/ObjBool.kt | 6 +++ .../kotlin/net/sergeych/lyng/obj/ObjInt.kt | 6 +++ .../kotlin/net/sergeych/lyng/obj/ObjList.kt | 6 +++ .../kotlin/net/sergeych/lyng/obj/ObjMap.kt | 8 ++++ .../kotlin/net/sergeych/lyng/obj/ObjReal.kt | 6 +++ .../kotlin/net/sergeych/lyng/obj/ObjString.kt | 6 +++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 14 ++++++ lynglib/src/jvmTest/kotlin/BookTest.kt | 5 +++ 12 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 docs/json_and_kotlin_serialization.md diff --git a/docs/json_and_kotlin_serialization.md b/docs/json_and_kotlin_serialization.md new file mode 100644 index 0000000..9809979 --- /dev/null +++ b/docs/json_and_kotlin_serialization.md @@ -0,0 +1,43 @@ +# Json support + +Since 1.0.5 we start adding JSON support. + +Right now we only support basic types: maps, lists, strings, numbers, booleans. It is not yet capable of serializing classes. This functionality will be added in the 1.0.6 release. + +## Serializae to kotlin string + + // in lyng + assertEquals("{\"a\":1}", {a: 1}.toJsonString()) + void + >>> void + +From the kotln side, you can use `Obj.toJson()` and deserialization helpers: + +```kotlin +/** + * Decodes the current object into a deserialized form using the provided deserialization strategy. + * It is based on [Obj.toJson] and uses existing Kotlin Json serialization, without string representation + * (only `JsonElement` to carry information between Kotlin and Lyng serialization worlds), thus efficient. + * + * @param strategy The deserialization strategy that defines how the object should be decoded. + * @param scope An optional scope used during deserialization to define the context. Defaults to a new instance of Scope. + * @return The deserialized object of type T. + */ +suspend fun Obj.decodeSerializableWith(strategy: DeserializationStrategy, scope: Scope = Scope()): T = + Json.decodeFromJsonElement(strategy,toJson(scope)) + +/** + * Decodes a serializable object of type [T] using the provided decoding scope. The deserialization uses + * [Obj.toJson] and existing Json based serialization ithout using actual string representation, thus + * efficient. + * + * @param T The type of the object to be decoded. Must be a reified type. + * @param scope The scope used during decoding. Defaults to a new instance of [Scope]. + */ +suspend inline fun Obj.decodeSerializable(scope: Scope= Scope()) = + decodeSerializableWith(serializer(), scope) +``` + +Note that lyng-2-kotlin deserialization with `kotlinx.serialization` is working based on JsonElement as information carrier, without formatting and parsing actual Json strings. This is why we use `Json.decodeFromJsonElement` instead of `Json.decodeFromString`. Such approach gives satisfactory performance without writing and supporting custom `kotlinx.serialization` codecs. + + diff --git a/docs/tutorial.md b/docs/tutorial.md index b278c30..12137ee 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -205,7 +205,7 @@ Much like let, but it does not alter returned value: >>> void While it is not altering return value, the source object could be changed: - +also class Point(x,y) val p = Point(1,2).also { it.x++ } assertEquals(p.x, 2) @@ -1533,7 +1533,7 @@ assertEquals(null, (buzz as? Foo)?.runA()) Notes: - Resolution order uses C3 MRO (active): deterministic, monotonic order suitable for diamonds and complex hierarchies. Example: for `class D() : B(), C()` where both `B()` and `C()` derive from `A()`, the C3 order is `D → B → C → A`. The first visible match wins. -- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualification (`this@Type`) or casts do not bypass visibility. +- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility. - Safe‑call `?.` works with `as?` for optional dispatch. To get details on OOP in Lyng, see [OOP notes](oop.md). \ No newline at end of file diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 7531e62..84bdc71 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.0.4-SNAPSHOT" +version = "1.0.5-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 951a620..4ca97eb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -17,8 +17,13 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.serializer import net.sergeych.lyng.* import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -376,6 +381,10 @@ open class Obj { return null } + open suspend fun toJson(scope: Scope = Scope()): JsonElement { + scope.raiseNotImplemented("toJson for ${objClass.className}") + } + companion object { val rootObjectType = ObjClass("Obj").apply { @@ -418,7 +427,9 @@ open class Obj { thisObj.putAt(this, requiredArg(0), newValue) newValue } - + addFn("toJsonString") { + thisObj.toJson(this).toString().toObj() + } } @@ -520,6 +531,10 @@ object ObjNull : Obj() { } } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonNull + } + override val objClass: ObjClass by lazy { object : ObjClass("Null") { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { @@ -572,4 +587,25 @@ data class ObjNamespace(val name: String) : Obj() { } } +/** + * Decodes the current object into a deserialized form using the provided deserialization strategy. + * It is based on [Obj.toJson] and uses existing Kotlin Json serialization, without string representation + * (only `JsonElement` to carry information between Kotlin and Lyng serialization worlds), thus efficient. + * + * @param strategy The deserialization strategy that defines how the object should be decoded. + * @param scope An optional scope used during deserialization to define the context. Defaults to a new instance of Scope. + * @return The deserialized object of type T. + */ +suspend fun Obj.decodeSerializableWith(strategy: DeserializationStrategy, scope: Scope = Scope()): T = + Json.decodeFromJsonElement(strategy,toJson(scope)) +/** + * Decodes a serializable object of type [T] using the provided decoding scope. The deserialization uses + * [Obj.toJson] and existing Json based serialization ithout using actual string representation, thus + * efficient. + * + * @param T The type of the object to be decoded. Must be a reified type. + * @param scope The scope used during decoding. Defaults to a new instance of [Scope]. + */ +suspend inline fun Obj.decodeSerializable(scope: Scope= Scope()) = + decodeSerializableWith(serializer(), scope) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt index 8f76e5c..e99cd2c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.Scope import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -62,6 +64,10 @@ data class ObjBool(val value: Boolean) : Obj() { return value == other.value } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonPrimitive(value) + } + companion object { val type = object : ObjClass("Bool") { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder,lynonType: LynonType?): Obj { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt index 03c79f4..5c395e8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.Scope import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -161,6 +163,10 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu } } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonPrimitive(value) + } + companion object { val Zero = ObjInt(0, true) val One = ObjInt(1, true) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 011d3c1..fba2f07 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import net.sergeych.lyng.Scope import net.sergeych.lyng.Statement import net.sergeych.lyng.statement @@ -188,6 +190,10 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { override suspend fun lynonType(): LynonType = LynonType.List + override suspend fun toJson(scope: Scope): JsonElement { + return JsonArray(list.map { it.toJson(scope) }) + } + companion object { val type = object : ObjClass("List", ObjArray) { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt index 4d560e2..58b65ba 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import net.sergeych.lyng.Scope import net.sergeych.lyng.Statement import net.sergeych.lynon.LynonDecoder @@ -141,6 +143,12 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { encoder.encodeAnyList(scope, values, fixedSize = true) } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonObject( + map.map { it.key.toString(scope).value to it.value.toJson(scope) }.toMap() + ) + } + companion object { suspend fun listToMap(scope: Scope, list: List): MutableMap { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt index ce608f9..2dbc4ec 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng.obj +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.Pos import net.sergeych.lyng.Scope import net.sergeych.lyng.statement @@ -100,6 +102,10 @@ data class ObjReal(val value: Double) : Obj(), Numeric { encoder.encodeReal(value) } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonPrimitive(value) + } + companion object { val type: ObjClass = object : ObjClass("Real") { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index e5227fa..f97a648 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt @@ -19,6 +19,8 @@ package net.sergeych.lyng.obj import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.RegexCache import net.sergeych.lyng.Scope @@ -116,6 +118,10 @@ data class ObjString(val value: String) : Obj() { encoder.encodeBinaryData(value.encodeToByteArray()) } + override suspend fun toJson(scope: Scope): JsonElement { + return JsonPrimitive(value) + } + companion object { val type = object : ObjClass("String") { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index e732bf7..9d68192 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import kotlinx.serialization.Serializable import net.sergeych.lyng.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.InlineSourcesImportProvider @@ -3797,8 +3798,21 @@ class ScriptTest { """ ) + } + @Serializable + data class JSTest1(val foo: String,val one: Int, val ok: Boolean) + @Test + fun testToJson() = runTest { + val x = eval("""{ "foo": "bar", "one": 1, "ok": true }""") + println(x.toJson()) + assertEquals(x.toJson().toString(), """{"foo":"bar","one":1,"ok":true}""") + assertEquals( + (eval("""{ "foo": "bar", "one": 1, "ok": true }.toJsonString()""") as ObjString).value, + """{"foo":"bar","one":1,"ok":true}""") + println(x.decodeSerializable()) + assertEquals(JSTest1("bar", 1, true), x.decodeSerializable()) } } diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index 8e42f62..4e61557 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.kt @@ -352,4 +352,9 @@ class BookTest { fun testRegex() = runBlocking { runDocTests("../docs/Regex.md") } + + @Test + fun testJson() = runBlocking { + runDocTests("../docs/json_and_kotlin_serialization.md") + } } \ No newline at end of file