fix #75 simple classes JSON serialization with custom formats
This commit is contained in:
parent
0c31ec63ee
commit
84f2f8fac4
@ -1,17 +1,69 @@
|
|||||||
# Json support
|
# Json support
|
||||||
|
|
||||||
Since 1.0.5 we start adding JSON support.
|
Since 1.0.5 we start adding JSON support. Versions 1,0,6* support serialization of the basic types, including lists and maps, and simple classes. Multiple inheritance may produce incorrect results, it is work in progress.
|
||||||
|
|
||||||
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.
|
## Serialization in Lyng
|
||||||
|
|
||||||
## Serializae to kotlin string
|
|
||||||
|
|
||||||
// in lyng
|
// in lyng
|
||||||
assertEquals("{\"a\":1}", {a: 1}.toJsonString())
|
assertEquals("{\"a\":1}", {a: 1}.toJsonString())
|
||||||
void
|
void
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
From the kotln side, you can use `Obj.toJson()` and deserialization helpers:
|
Simple classes serialization is supported:
|
||||||
|
|
||||||
|
import lyng.serialization
|
||||||
|
class Point(foo,bar) {
|
||||||
|
val t = 42
|
||||||
|
}
|
||||||
|
// val is not serialized
|
||||||
|
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Note that mutable members are serialized:
|
||||||
|
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point2(foo,bar) {
|
||||||
|
var reason = 42
|
||||||
|
// but we override json serialization:
|
||||||
|
fun toJsonObject() {
|
||||||
|
{ "custom": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// var is serialized instead
|
||||||
|
assertEquals( "{\"custom\":true}", Point2(1,2).toJsonString() )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Custom serialization of user classes is possible by overriding `toJsonObject` method. It must return an object which is serializable to Json. Most often it is a map, but any object is accepted, that makes it very flexible:
|
||||||
|
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point2(foo,bar) {
|
||||||
|
var reason = 42
|
||||||
|
// but we override json serialization:
|
||||||
|
fun toJsonObject() {
|
||||||
|
{ "custom": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Custom {
|
||||||
|
fun toJsonObject() {
|
||||||
|
"full freedom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// var is serialized instead
|
||||||
|
assertEquals( "\"full freedom\"", Custom().toJsonString() )
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Please note that `toJsonString` should be used to get serialized string representation of the object. Don't call `toJsonObject` directly, it is not intended to be used outside the serialization library.
|
||||||
|
|
||||||
|
|
||||||
|
## Kotlin side interfaces
|
||||||
|
|
||||||
|
The "Batteries included" principle is also applied to serialization.
|
||||||
|
|
||||||
|
- `Obj.toJson()` provides Kotlin `JsonElement`
|
||||||
|
- `Obj.toJsonString()` provides Json string representation
|
||||||
|
- `Obj.decodeSerializableWith()` and `Obj.decodeSerializable()` allows to decode Lyng classes as Kotlin objects using `kotlinx.serialization`:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
/**
|
/**
|
||||||
@ -38,6 +90,6 @@ suspend inline fun <reified T>Obj.decodeSerializable(scope: Scope= Scope()) =
|
|||||||
decodeSerializableWith<T>(serializer<T>(), 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.
|
Note that lyng-2-kotlin deserialization with `kotlinx.serialization` uses JsonElement as information carrier without formatting and parsing actual Json strings. This is why we use `Json.decodeFromJsonElement` instead of `Json.decodeFromString`. Such an approach gives satisfactory performance without writing and supporting custom `kotlinx.serialization` codecs.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -232,8 +232,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
||||||
for (entry in meta.params)
|
for (entry in meta.params)
|
||||||
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
|
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
|
||||||
for (i in serializingVars)
|
for (i in serializingVars) {
|
||||||
result[i.key] = i.value.value.toJson(scope)
|
// remove T:: prefix from the field name for JSON
|
||||||
|
val parts = i.key.split("::")
|
||||||
|
result[if( parts.size == 1 ) parts[0] else parts.last()] = i.value.value.toJson(scope)
|
||||||
|
}
|
||||||
return JsonObject(result)
|
return JsonObject(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -928,10 +928,10 @@ class ScriptTest {
|
|||||||
assertEquals(7, c.eval("x++").toInt())
|
assertEquals(7, c.eval("x++").toInt())
|
||||||
assertEquals(
|
assertEquals(
|
||||||
8, c.eval("x")
|
8, c.eval("x")
|
||||||
.also {
|
.also {
|
||||||
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
|
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
|
||||||
}
|
}
|
||||||
.toInt())
|
.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1878,7 +1878,7 @@ class ScriptTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIntExponentRealForm() = runTest {
|
fun testIntExponentRealForm() = runTest {
|
||||||
when(val x = eval("1e-6").toString()) {
|
when (val x = eval("1e-6").toString()) {
|
||||||
"0.000001", "1E-6", "1e-6" -> {}
|
"0.000001", "1E-6", "1e-6" -> {}
|
||||||
else -> fail("Excepted 1e-6 got $x")
|
else -> fail("Excepted 1e-6 got $x")
|
||||||
}
|
}
|
||||||
@ -2577,14 +2577,16 @@ class ScriptTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetAddRemoveSet() = runTest {
|
fun testSetAddRemoveSet() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
val s1 = Set( 1, 2 3)
|
val s1 = Set( 1, 2 3)
|
||||||
val s2 = Set( 3, 4 5)
|
val s2 = Set( 3, 4 5)
|
||||||
assertEquals( Set(1,2,3,4,5), s1 + s2 )
|
assertEquals( Set(1,2,3,4,5), s1 + s2 )
|
||||||
assertEquals( Set(1,2,3,4,5), s1 + s2.toList() )
|
assertEquals( Set(1,2,3,4,5), s1 + s2.toList() )
|
||||||
assertEquals( Set(1,2), s1 - s2 )
|
assertEquals( Set(1,2), s1 - s2 )
|
||||||
assertEquals( Set(1,2), s1 - s2.toList() )
|
assertEquals( Set(1,2), s1 - s2.toList() )
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -2651,7 +2653,7 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjTestFoo(val value: ObjString): Obj() {
|
class ObjTestFoo(val value: ObjString) : Obj() {
|
||||||
|
|
||||||
override val objClass: ObjClass = klass
|
override val objClass: ObjClass = klass
|
||||||
|
|
||||||
@ -2669,13 +2671,15 @@ class ScriptTest {
|
|||||||
fun TestApplyFromKotlin() = runTest {
|
fun TestApplyFromKotlin() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
scope.addConst("testfoo", ObjTestFoo(ObjString("bar2")))
|
scope.addConst("testfoo", ObjTestFoo(ObjString("bar2")))
|
||||||
scope.eval("""
|
scope.eval(
|
||||||
|
"""
|
||||||
assertEquals(testfoo.test(), "bar2")
|
assertEquals(testfoo.test(), "bar2")
|
||||||
testfoo.apply {
|
testfoo.apply {
|
||||||
println(test())
|
println(test())
|
||||||
assertEquals(test(), "bar2")
|
assertEquals(test(), "bar2")
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -2683,7 +2687,8 @@ class ScriptTest {
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
withTimeout(1.seconds) {
|
withTimeout(1.seconds) {
|
||||||
val s = Script.newScope()
|
val s = Script.newScope()
|
||||||
s.eval("""
|
s.eval(
|
||||||
|
"""
|
||||||
fun dosomething() {
|
fun dosomething() {
|
||||||
var x = 0
|
var x = 0
|
||||||
for( i in 1..100) {
|
for( i in 1..100) {
|
||||||
@ -2692,7 +2697,8 @@ class ScriptTest {
|
|||||||
delay(100)
|
delay(100)
|
||||||
assert(x == 5050)
|
assert(x == 5050)
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
(0..100).map {
|
(0..100).map {
|
||||||
globalDefer {
|
globalDefer {
|
||||||
s.eval("dosomething()")
|
s.eval("dosomething()")
|
||||||
@ -2707,7 +2713,8 @@ class ScriptTest {
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
withTimeout(1.seconds) {
|
withTimeout(1.seconds) {
|
||||||
val s = Script.newScope()
|
val s = Script.newScope()
|
||||||
s.eval("""
|
s.eval(
|
||||||
|
"""
|
||||||
// it is intentionally not optimal to provoke
|
// it is intentionally not optimal to provoke
|
||||||
// RC errors:
|
// RC errors:
|
||||||
class AtomicCounter {
|
class AtomicCounter {
|
||||||
@ -2742,7 +2749,8 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
assertEquals( 100, ac.getCounter() )
|
assertEquals( 100, ac.getCounter() )
|
||||||
|
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3580,7 +3588,7 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
fun testMinimumOptimization() = runTest {
|
fun testMinimumOptimization() = runTest {
|
||||||
for (i in 1..200) {
|
for (i in 1..200) {
|
||||||
bm {
|
bm {
|
||||||
@ -3627,16 +3635,24 @@ class ScriptTest {
|
|||||||
val scope1 = Script.newScope()
|
val scope1 = Script.newScope()
|
||||||
|
|
||||||
// extension foo should be local to scope1
|
// extension foo should be local to scope1
|
||||||
assertEquals("a_foo", scope1.eval("""
|
assertEquals(
|
||||||
|
"a_foo", scope1.eval(
|
||||||
|
"""
|
||||||
fun String.foo() { this + "_foo" }
|
fun String.foo() { this + "_foo" }
|
||||||
"a".foo()
|
"a".foo()
|
||||||
""".trimIndent()).toString())
|
""".trimIndent()
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
|
|
||||||
val scope2 = Script.newScope()
|
val scope2 = Script.newScope()
|
||||||
assertEquals("a_bar", scope2.eval("""
|
assertEquals(
|
||||||
|
"a_bar", scope2.eval(
|
||||||
|
"""
|
||||||
fun String.foo() { this + "_bar" }
|
fun String.foo() { this + "_bar" }
|
||||||
"a".foo()
|
"a".foo()
|
||||||
""".trimIndent()).toString())
|
""".trimIndent()
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -3658,15 +3674,18 @@ class ScriptTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRangeIsIterable() = runTest {
|
fun testRangeIsIterable() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
val r = 1..10
|
val r = 1..10
|
||||||
assert( r is Iterable )
|
assert( r is Iterable )
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCallAndResultOrder() = runTest {
|
fun testCallAndResultOrder() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
import lyng.stdlib
|
import lyng.stdlib
|
||||||
|
|
||||||
fun test(a="a", b="b", c="c") { [a, b, c] }
|
fun test(a="a", b="b", c="c") { [a, b, c] }
|
||||||
@ -3681,13 +3700,15 @@ class ScriptTest {
|
|||||||
// the parentheses here are in fact unnecessary:
|
// the parentheses here are in fact unnecessary:
|
||||||
val ok2 = test { void }.last()
|
val ok2 = test { void }.last()
|
||||||
assert( ok2 is Callable)
|
assert( ok2 is Callable)
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIterableMinMax() = runTest {
|
fun testIterableMinMax() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
import lyng.stdlib
|
import lyng.stdlib
|
||||||
assertEquals( -100, (1..100).toList().minOf { -it } )
|
assertEquals( -100, (1..100).toList().minOf { -it } )
|
||||||
assertEquals( -1, (1..100).toList().maxOf { -it } )
|
assertEquals( -1, (1..100).toList().maxOf { -it } )
|
||||||
@ -3764,29 +3785,34 @@ class ScriptTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInlineArrayLiteral() = runTest {
|
fun testInlineArrayLiteral() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
val res = []
|
val res = []
|
||||||
for( i in [4,3,1] ) {
|
for( i in [4,3,1] ) {
|
||||||
res.add(i)
|
res.add(i)
|
||||||
}
|
}
|
||||||
assertEquals( [4,3,1], res )
|
assertEquals( [4,3,1], res )
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInlineMapLiteral() = runTest {
|
fun testInlineMapLiteral() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
val res = {}
|
val res = {}
|
||||||
for( i in {foo: "bar"} ) {
|
for( i in {foo: "bar"} ) {
|
||||||
res[i.key] = i.value
|
res[i.key] = i.value
|
||||||
}
|
}
|
||||||
assertEquals( {foo: "bar"}, res )
|
assertEquals( {foo: "bar"}, res )
|
||||||
""".trimIndent())
|
""".trimIndent()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCommentsInClassConstructor() = runTest {
|
fun testCommentsInClassConstructor() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
class T(
|
class T(
|
||||||
// comment 1
|
// comment 1
|
||||||
val x: Int,
|
val x: Int,
|
||||||
@ -3801,7 +3827,7 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class JSTest1(val foo: String,val one: Int, val ok: Boolean)
|
data class JSTest1(val foo: String, val one: Int, val ok: Boolean)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testToJson() = runTest {
|
fun testToJson() = runTest {
|
||||||
@ -3810,26 +3836,30 @@ class ScriptTest {
|
|||||||
assertEquals(x.toJson().toString(), """{"foo":"bar","one":1,"ok":true}""")
|
assertEquals(x.toJson().toString(), """{"foo":"bar","one":1,"ok":true}""")
|
||||||
assertEquals(
|
assertEquals(
|
||||||
(eval("""{ "foo": "bar", "one": 1, "ok": true }.toJsonString()""") as ObjString).value,
|
(eval("""{ "foo": "bar", "one": 1, "ok": true }.toJsonString()""") as ObjString).value,
|
||||||
"""{"foo":"bar","one":1,"ok":true}""")
|
"""{"foo":"bar","one":1,"ok":true}"""
|
||||||
|
)
|
||||||
println(x.decodeSerializable<JSTest1>())
|
println(x.decodeSerializable<JSTest1>())
|
||||||
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
|
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInstanceVars() = runTest {
|
fun testInstanceVars() = runTest {
|
||||||
var x = eval("""
|
var x = eval(
|
||||||
|
"""
|
||||||
// in this case, x and y are constructor parameters, not instance variables:
|
// in this case, x and y are constructor parameters, not instance variables:
|
||||||
class T(x,y) {
|
class T(x,y) {
|
||||||
// and z is val and therefore needn't serialization either:
|
// and z is val and therefore needn't serialization either:
|
||||||
val z = x + y
|
val z = x + y
|
||||||
}
|
}
|
||||||
T(1, 2)
|
T(1, 2)
|
||||||
""".trimIndent()) as ObjInstance
|
""".trimIndent()
|
||||||
println(x.serializingVars.map { "${it.key}=${it.value.value}"})
|
) as ObjInstance
|
||||||
|
println(x.serializingVars.map { "${it.key}=${it.value.value}" })
|
||||||
// so serializingVars is empty:
|
// so serializingVars is empty:
|
||||||
assertEquals(emptyMap(), x.serializingVars)
|
assertEquals(emptyMap(), x.serializingVars)
|
||||||
|
|
||||||
x = eval("""
|
x = eval(
|
||||||
|
"""
|
||||||
class T(x,y) {
|
class T(x,y) {
|
||||||
// variable z though should be serialized:
|
// variable z though should be serialized:
|
||||||
var z = x + y
|
var z = x + y
|
||||||
@ -3838,17 +3868,19 @@ class ScriptTest {
|
|||||||
x.z = 100
|
x.z = 100
|
||||||
assertEquals(100, x.z)
|
assertEquals(100, x.z)
|
||||||
x
|
x
|
||||||
""".trimIndent()) as ObjInstance
|
""".trimIndent()
|
||||||
|
) as ObjInstance
|
||||||
// z is instance var, it must present
|
// z is instance var, it must present
|
||||||
val z = x.serializingVars["z"] ?: x.serializingVars["T::z"]
|
val z = x.serializingVars["z"] ?: x.serializingVars["T::z"]
|
||||||
// and be mutable:
|
// and be mutable:
|
||||||
assertTrue( z!!.isMutable )
|
assertTrue(z!!.isMutable)
|
||||||
println(x.serializingVars.map { "${it.key}=${it.value.value}"})
|
println(x.serializingVars.map { "${it.key}=${it.value.value}" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun memberValCantBeAssigned() = runTest {
|
fun memberValCantBeAssigned() = runTest {
|
||||||
eval("""
|
eval(
|
||||||
|
"""
|
||||||
class Point(foo,bar) {
|
class Point(foo,bar) {
|
||||||
val t = 42
|
val t = 42
|
||||||
var r = 142
|
var r = 142
|
||||||
@ -3867,23 +3899,46 @@ class ScriptTest {
|
|||||||
|
|
||||||
// but r should be changed:
|
// but r should be changed:
|
||||||
assertEqual(123, p.r)
|
assertEqual(123, p.r)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
@Test
|
||||||
// fun testClassToJson() = runTest {
|
fun testClassToJson() = runTest {
|
||||||
// val x = eval("""
|
eval(
|
||||||
// import lyng.serialization
|
"""
|
||||||
// class Point(foo,bar) {
|
import lyng.serialization
|
||||||
// val t = 42
|
class Point(foo,bar) {
|
||||||
// }
|
val t = 42
|
||||||
// val p = Point(1,2)
|
}
|
||||||
// p.t = 121
|
// val is not serialized
|
||||||
// println(Point(10,"bar").toJsonString())
|
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
|
||||||
// Lynon.encode(Point(1,2))
|
class Point2(foo,bar) {
|
||||||
// """.trimIndent())
|
var reason = 42
|
||||||
// println((x as ObjBitBuffer).bitArray.asUByteArray().toDump())
|
}
|
||||||
//
|
// var is serialized instead
|
||||||
// }
|
assertEquals( "{\"foo\":1,\"bar\":2,\"reason\":42}", Point2(1,2).toJsonString() )
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCustomClassToJson() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
class Point2(foo,bar) {
|
||||||
|
var reason = 42
|
||||||
|
// but we override json serialization:
|
||||||
|
fun toJsonObject() {
|
||||||
|
{ "custom": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// var is serialized instead
|
||||||
|
assertEquals( "{\"custom\":true}", Point2(1,2).toJsonString() )
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user