All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jsMain.kotlinx.serialization.json.internal.DynamicEncoders.kt Maven / Gradle / Ivy

There is a newer version: 1.7.3
Show newest version
/*
 * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */
@file:OptIn(ExperimentalSerializationApi::class)

package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
import kotlin.math.*

/**
 * Converts Kotlin data structures to plain Javascript objects
 *
 *
 * Limitations:
 * * Map keys must be of primitive or enum type
 * * Enums are serialized as the value of `@SerialName` if present or their name, in that order.
 * * Currently does not support polymorphism
 *
 * Example of usage:
 * ```
 *  @Serializable
 *  open class DataWrapper(open val s: String, val d: String?)
 *
 *  val wrapper = DataWrapper("foo", "bar")
 *  val plainJS: dynamic = DynamicObjectSerializer().serialize(DataWrapper.serializer(), wrapper)
 * ```
 */
@JsName("encodeToDynamic")
@OptIn(ExperimentalSerializationApi::class)
internal fun  Json.encodeDynamic(serializer: SerializationStrategy, value: T): dynamic {
    if (serializer.descriptor.kind is PrimitiveKind || serializer.descriptor.kind is SerialKind.ENUM) {
        val encoder = DynamicPrimitiveEncoder(this)
        encoder.encodeSerializableValue(serializer, value)
        return encoder.result
    }
    val encoder = DynamicObjectEncoder(this, false)
    encoder.encodeSerializableValue(serializer, value)
    return encoder.result
}

@OptIn(ExperimentalSerializationApi::class)
private class DynamicObjectEncoder(
    override val json: Json,
    private val encodeNullAsUndefined: Boolean
) : AbstractEncoder(), JsonEncoder {

    override val serializersModule: SerializersModule
        get() = json.serializersModule

    var result: dynamic = NoOutputMark
    private lateinit var current: Node
    private var currentName: String? = null
    private lateinit var currentDescriptor: SerialDescriptor
    private var currentElementIsMapKey = false

    /**
     * Flag of usage polymorphism with discriminator attribute
     */
    private var polymorphicDiscriminator: String? = null

    private object NoOutputMark

    class Node(val writeMode: WriteMode, val jsObject: dynamic) {
        var index: Int = 0
        lateinit var parent: Node
    }

    enum class WriteMode {
        OBJ, MAP, LIST
    }

    override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
        current.index = index
        currentDescriptor = descriptor

        when {
            current.writeMode == WriteMode.MAP -> currentElementIsMapKey = current.index % 2 == 0
            current.writeMode == WriteMode.LIST && descriptor.kind is PolymorphicKind -> currentName = index.toString()
            else -> currentName = descriptor.getElementName(index)
        }

        return true
    }

    override fun encodeValue(value: Any) {
        if (currentElementIsMapKey) {
            currentName = value.toString()
        } else if (isNotStructured()) {
            result = value
        } else {
            current.jsObject[currentName] = value
        }
    }

    override fun encodeChar(value: Char) {
        encodeValue(value.toString())
    }

    override fun encodeNull() {
        if (currentElementIsMapKey) {
            currentName = null
        } else {
            if (encodeNullAsUndefined) return // omit element

            current.jsObject[currentName] = null
        }
    }

    override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
        encodeValue(enumDescriptor.getElementName(index))
    }

    override fun encodeLong(value: Long) {
        val asDouble = value.toDouble()
        val conversionHasLossOfPrecision = abs(asDouble) > MAX_SAFE_INTEGER
        // todo: shall it be driven by isLenient or another configuration key?
        if (!json.configuration.isLenient && conversionHasLossOfPrecision) {
            throw IllegalArgumentException(
                "$value can't be serialized to number due to a potential precision loss. " +
                        "Use the JsonConfiguration option isLenient to serialize anyway"
            )
        }

        if (currentElementIsMapKey && conversionHasLossOfPrecision) {
            throw IllegalArgumentException(
                "Long with value $value can't be used in json as map key, because its value is larger than Number.MAX_SAFE_INTEGER"
            )
        }

        encodeValue(asDouble)
    }

    override fun encodeFloat(value: Float) {
        encodeDouble(value.toDouble())
    }

    override fun encodeDouble(value: Double) {
        if (currentElementIsMapKey) {
            val hasNonZeroFractionalPart = floor(value) != value
            if (!value.isFinite() || hasNonZeroFractionalPart) {
                throw IllegalArgumentException(
                    "Double with value $value can't be used in json as map key, because its value is not an integer."
                )
            }
        }
        encodeValue(value)
    }

    override fun  encodeNullableSerializableElement(
        descriptor: SerialDescriptor,
        index: Int,
        serializer: SerializationStrategy,
        value: T?
    ) {
        if (value != null || json.configuration.explicitNulls) {
            super.encodeNullableSerializableElement(descriptor, index, serializer, value)
        }
    }

    override fun encodeJsonElement(element: JsonElement) {
        encodeSerializableValue(JsonElementSerializer, element)
    }

    override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int) =
        json.configuration.encodeDefaults

    private fun enterNode(jsObject: dynamic, writeMode: WriteMode) {
        val child = Node(writeMode, jsObject)
        child.parent = current
        current = child
    }

    private fun exitNode() {
        current = current.parent
        currentElementIsMapKey = false
    }

    private fun isNotStructured() = result === NoOutputMark

    override fun  encodeSerializableValue(serializer: SerializationStrategy, value: T) {
        encodePolymorphically(serializer, value) {
            polymorphicDiscriminator = it
        }
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
        // we currently do not structures as map key
        if (currentElementIsMapKey) {
            throw IllegalArgumentException(
                "Value of type ${descriptor.serialName} can't be used in json as map key. " +
                        "It should have either primitive or enum kind, but its kind is ${descriptor.kind}."
            )
        }

        val newMode = selectMode(descriptor)
        if (result === NoOutputMark) {
            result = newChild(newMode)
            current = Node(newMode, result)
            current.parent = current
        } else {
            val child = newChild(newMode)
            current.jsObject[currentName] = child
            enterNode(child, newMode)
        }

        if (polymorphicDiscriminator != null) {
            current.jsObject[polymorphicDiscriminator!!] = descriptor.serialName
            polymorphicDiscriminator = null
        }

        current.index = 0
        return this
    }

    private fun newChild(writeMode: WriteMode) = when (writeMode) {
        WriteMode.OBJ, WriteMode.MAP -> js("{}")
        WriteMode.LIST -> js("[]")
    }

    override fun endStructure(descriptor: SerialDescriptor) {
        exitNode()
    }

    @OptIn(ExperimentalSerializationApi::class)
    fun selectMode(desc: SerialDescriptor) = when (desc.kind) {
        StructureKind.CLASS, StructureKind.OBJECT, SerialKind.CONTEXTUAL -> WriteMode.OBJ
        StructureKind.LIST, is PolymorphicKind -> WriteMode.LIST
        StructureKind.MAP -> WriteMode.MAP
        is PrimitiveKind, SerialKind.ENUM -> {
            // the two cases are handled in DynamicObjectSerializer. But compiler does not know
            error("DynamicObjectSerializer does not support serialization of singular primitive values or enum types.")
        }
    }
}

private class DynamicPrimitiveEncoder(
    override val json: Json,
) : AbstractEncoder(), JsonEncoder {

    override val serializersModule: SerializersModule
        get() = json.serializersModule

    var result: dynamic = null

    override fun encodeNull() {
        result = null
    }

    override fun encodeLong(value: Long) {
        val asDouble = value.toDouble()
        // todo: shall it be driven by isLenient or another configuration key?
        if (!json.configuration.isLenient && abs(value) > MAX_SAFE_INTEGER) {
            throw IllegalArgumentException(
                "$value can't be deserialized to number due to a potential precision loss. " +
                        "Use the JsonConfiguration option isLenient to serialise anyway"
            )
        }
        encodeValue(asDouble)
    }

    override fun encodeChar(value: Char) {
        encodeValue(value.toString())
    }

    override fun encodeValue(value: Any) {
        result = value
    }

    @OptIn(ExperimentalSerializationApi::class)
    override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
        encodeValue(enumDescriptor.getElementName(index))
    }

    override fun endStructure(descriptor: SerialDescriptor) {
    }

    override fun encodeJsonElement(element: JsonElement) {
        encodeSerializableValue(JsonElementSerializer, element)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy