Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonEncoder and JsonDecoder implmentations to their Bson counterparts #1253

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions bson-kotlinx/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {

implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.0"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")

api(project(path = ":bson", configuration = "default"))
implementation("org.jetbrains.kotlin:kotlin-reflect")
Expand Down
105 changes: 104 additions & 1 deletion bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,28 @@ import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.modules.SerializersModule
import org.bson.AbstractBsonReader
import org.bson.BsonBinarySubType
import org.bson.BsonInvalidOperationException
import org.bson.BsonReader
import org.bson.BsonType
import org.bson.BsonValue
import org.bson.UuidRepresentation
import org.bson.codecs.BsonValueCodec
import org.bson.codecs.DecoderContext
import org.bson.internal.UuidHelper
import org.bson.types.ObjectId
import java.util.Base64

/**
* The BsonDecoder interface
Expand All @@ -58,7 +71,14 @@ internal open class DefaultBsonDecoder(
internal val reader: AbstractBsonReader,
override val serializersModule: SerializersModule,
internal val configuration: BsonConfiguration
) : BsonDecoder, AbstractDecoder() {
) : BsonDecoder, JsonDecoder, AbstractDecoder() {

override val json = Json {
explicitNulls = configuration.explicitNulls
encodeDefaults = configuration.encodeDefaults
classDiscriminator = configuration.classDiscriminator
serializersModule = this@DefaultBsonDecoder.serializersModule
}

private data class ElementMetadata(val name: String, val nullable: Boolean, var processed: Boolean = false)
private var elementsMetadata: Array<ElementMetadata>? = null
Expand Down Expand Up @@ -178,8 +198,91 @@ internal open class DefaultBsonDecoder(

override fun decodeObjectId(): ObjectId = readOrThrow({ reader.readObjectId() }, BsonType.OBJECT_ID)
override fun decodeBsonValue(): BsonValue = bsonValueCodec.decode(reader, DecoderContext.builder().build())

@Suppress("ComplexMethod")
override fun decodeJsonElement(): JsonElement = reader.run {

if (state == AbstractBsonReader.State.INITIAL ||
state == AbstractBsonReader.State.SCOPE_DOCUMENT ||
state == AbstractBsonReader.State.TYPE) {
readBsonType()
}

if (state == AbstractBsonReader.State.NAME) {
// ignore name
skipName()
}

// @formatter:off
return when (currentBsonType) {
BsonType.DOCUMENT -> readJsonObject()
BsonType.ARRAY -> readJsonArray()
BsonType.NULL -> JsonPrimitive(decodeNull())
BsonType.STRING -> JsonPrimitive(decodeString())
BsonType.BOOLEAN -> JsonPrimitive(decodeBoolean())
BsonType.INT32 -> JsonPrimitive(decodeInt())
BsonType.INT64 -> JsonPrimitive(decodeLong())
BsonType.DOUBLE -> JsonPrimitive(decodeDouble())
BsonType.DECIMAL128 -> JsonPrimitive(reader.readDecimal128())
BsonType.OBJECT_ID -> JsonPrimitive(decodeObjectId().toHexString())
BsonType.DATE_TIME -> JsonPrimitive(reader.readDateTime())
BsonType.TIMESTAMP -> JsonPrimitive(reader.readTimestamp().value)
BsonType.BINARY -> {
val subtype = reader.peekBinarySubType()
val data = reader.readBinaryData().data
when (subtype) {
BsonBinarySubType.UUID_LEGACY.value -> JsonPrimitive(
UuidHelper.decodeBinaryToUuid(
data, subtype,
UuidRepresentation.JAVA_LEGACY
).toString()
)
BsonBinarySubType.UUID_STANDARD.value -> JsonPrimitive(
UuidHelper.decodeBinaryToUuid(
data, subtype,
UuidRepresentation.STANDARD
).toString()
)
else -> JsonPrimitive(Base64.getEncoder().encodeToString(data))
}
}
else -> error("unsupported json type: $currentBsonType")
}
// @formatter:on
}

override fun reader(): BsonReader = reader

private fun readJsonObject(): JsonObject {

reader.readStartDocument()
val obj = buildJsonObject {
var type = reader.readBsonType()
while (type != BsonType.END_OF_DOCUMENT) {
put(reader.readName(), decodeJsonElement())
type = reader.readBsonType()
}
}

reader.readEndDocument()
return obj
}

private fun readJsonArray(): JsonArray {

reader.readStartArray()
val array = buildJsonArray {
var type = reader.readBsonType()
while (type != BsonType.END_OF_DOCUMENT) {
add(decodeJsonElement())
type = reader.readBsonType()
}
}

reader.readEndArray()
return array
}

private inline fun <T> readOrThrow(action: () -> T, bsonType: BsonType): T {
return try {
action()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,24 @@ import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.double
import kotlinx.serialization.json.int
import kotlinx.serialization.json.long
import kotlinx.serialization.modules.SerializersModule
import org.bson.BsonValue
import org.bson.BsonWriter
import org.bson.codecs.BsonValueCodec
import org.bson.codecs.EncoderContext
import org.bson.types.Decimal128
import org.bson.types.ObjectId
import java.math.BigDecimal

/**
* The BsonEncoder interface
Expand Down Expand Up @@ -62,17 +74,29 @@ internal class DefaultBsonEncoder(
private val writer: BsonWriter,
override val serializersModule: SerializersModule,
private val configuration: BsonConfiguration
) : BsonEncoder, AbstractEncoder() {
) : BsonEncoder, JsonEncoder, AbstractEncoder() {

companion object {
val validKeyKinds = setOf(PrimitiveKind.STRING, PrimitiveKind.CHAR, SerialKind.ENUM)
val bsonValueCodec = BsonValueCodec()
private val DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE)
private val DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE)
private val INT_MIN_VALUE = BigDecimal.valueOf(Int.MIN_VALUE.toLong())
private val INT_MAX_VALUE = BigDecimal.valueOf(Int.MAX_VALUE.toLong())
private val LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE)
private val LONG_MAX_VALUE = BigDecimal.valueOf(Long.MAX_VALUE)
}

private var isPolymorphic = false
private var state = STATE.VALUE
private var mapState = MapState()
private var deferredElementName: String? = null
override val json = Json {
explicitNulls = configuration.explicitNulls
encodeDefaults = configuration.encodeDefaults
classDiscriminator = configuration.classDiscriminator
serializersModule = this@DefaultBsonEncoder.serializersModule
}

override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean =
configuration.encodeDefaults
Expand Down Expand Up @@ -143,10 +167,10 @@ internal class DefaultBsonEncoder(
deferredElementName?.let {
if (value != null || configuration.explicitNulls) {
encodeName(it)
super.encodeNullableSerializableValue(serializer, value)
super<AbstractEncoder>.encodeNullableSerializableValue(serializer, value)
}
}
?: super.encodeNullableSerializableValue(serializer, value)
?: super<AbstractEncoder>.encodeNullableSerializableValue(serializer, value)
}

override fun encodeByte(value: Byte) = encodeInt(value.toInt())
Expand Down Expand Up @@ -183,8 +207,55 @@ internal class DefaultBsonEncoder(
bsonValueCodec.encode(writer, value, EncoderContext.builder().build())
}

override fun encodeJsonElement(element: JsonElement) = when(element) {
is JsonNull -> encodeNull()
is JsonPrimitive -> encodeJsonPrimitive(element)
is JsonObject -> encodeJsonObject(element)
is JsonArray -> encodeJsonArray(element)
}

override fun writer(): BsonWriter = writer

private fun encodeJsonPrimitive(primitive: JsonPrimitive) {
val content = primitive.content
when {
primitive.isString -> encodeString(content)
content == "true" || content == "false" ->
encodeBoolean(content.toBooleanStrict())
else -> {
val decimal = BigDecimal(content)
when {
decimal.stripTrailingZeros().scale() > 0 ->
if (DOUBLE_MIN_VALUE <= decimal && decimal <= DOUBLE_MAX_VALUE) {
encodeDouble(primitive.double)
} else {
writer.writeDecimal128(Decimal128(decimal))
}
INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE ->
encodeInt(primitive.int)
LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE ->
encodeLong(primitive.long)
else -> writer.writeDecimal128(Decimal128(decimal))
}
}
}
}

private fun encodeJsonObject(obj: JsonObject) {
writer.writeStartDocument()
obj.forEach { k, v ->
writer.writeName(k)
encodeJsonElement(v)
}
writer.writeEndDocument()
}

private fun encodeJsonArray(array: JsonArray) {
writer.writeStartArray()
array.forEach(::encodeJsonElement)
writer.writeEndArray()
}

private fun encodeName(value: Any) {
writer.writeName(value.toString())
deferredElementName = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import kotlin.test.assertEquals
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.MissingFieldException
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
Expand Down Expand Up @@ -71,6 +73,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault
import org.bson.codecs.kotlinx.samples.DataClassWithEnum
import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey
import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit
import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement
import org.bson.codecs.kotlinx.samples.DataClassWithMutableList
import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap
import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet
Expand Down Expand Up @@ -129,6 +132,46 @@ class KotlinSerializerCodecTest {

private val allBsonTypesDocument = BsonDocument.parse(allBsonTypesJson)

@Test
fun testDataClassWithJsonElement() {

/*
* We need to encode all integer values as longs because the JsonElementSerializer
* doesn't actually use our JsonEncoder instead it uses an inferior
* JsonPrimitiveSerializer and ignores ours altogether encoding all integers as longs
*
* On the other hand, BsonDocument decodes everything as integers unless it's explicitly
* set as a long, and therefore we get a type mismatch
*/

val expected = """{"value": {
|"char": "c",
|"byte": {"$numberLong": "0"},
|"short": {"$numberLong": "1"},
|"int": {"$numberLong": "22"},
|"long": {"$numberLong": "42"},
|"float": 4.0,
|"double": 4.2,
|"boolean": true,
|"string": "String"
|}}""".trimMargin()
val dataClass = DataClassWithJsonElement(
buildJsonObject {
put("char", "c")
put("byte", 0)
put("short", 1)
put("int", 22)
put("long", 42L)
put("float", 4.0)
put("double", 4.2)
put("boolean", true)
put("string", "String")
}
)

assertRoundTrips(expected, dataClass)
}

@Test
fun testDataClassWithSimpleValues() {
val expected =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import org.bson.BsonArray
import org.bson.BsonBinary
import org.bson.BsonBoolean
Expand Down Expand Up @@ -50,6 +51,11 @@ import org.bson.codecs.pojo.annotations.BsonProperty
import org.bson.codecs.pojo.annotations.BsonRepresentation
import org.bson.types.ObjectId

@Serializable
data class DataClassWithJsonElement(
val value: JsonElement
)

@Serializable
data class DataClassWithSimpleValues(
val char: Char,
Expand Down