v1.0.5-SNAPSHOT started json and kotlinx serialization support

This commit is contained in:
Sergey Chernov 2025-12-04 17:05:07 +01:00
parent 5cfc15cf17
commit 171e413c5f
12 changed files with 140 additions and 4 deletions

View File

@ -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 <T>Obj.decodeSerializableWith(strategy: DeserializationStrategy<T>, 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 <reified T>Obj.decodeSerializable(scope: Scope= Scope()) =
decodeSerializableWith<T>(serializer<T>(), 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.

View File

@ -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).

View File

@ -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

View File

@ -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<Obj>(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 <T>Obj.decodeSerializableWith(strategy: DeserializationStrategy<T>, 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 <reified T>Obj.decodeSerializable(scope: Scope= Scope()) =
decodeSerializableWith<T>(serializer<T>(), scope)

View File

@ -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 {

View File

@ -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)

View File

@ -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<Obj> = 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 {

View File

@ -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<Obj, Obj> = 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<Obj>): MutableMap<Obj, Obj> {

View File

@ -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 =

View File

@ -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 =

View File

@ -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<JSTest1>())
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
}
}

View File

@ -352,4 +352,9 @@ class BookTest {
fun testRegex() = runBlocking {
runDocTests("../docs/Regex.md")
}
@Test
fun testJson() = runBlocking {
runDocTests("../docs/json_and_kotlin_serialization.md")
}
}