Index: build.gradle.kts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/build.gradle.kts b/build.gradle.kts --- a/build.gradle.kts (revision b67d8a7f3abfed8322c1c8a1e8024135685f2789) +++ b/build.gradle.kts (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) @@ -1,6 +1,6 @@ import java.util.Date -val LIBRARY_VERSION_NAME = "0.4.4" +val LIBRARY_VERSION_NAME = "0.4.5" val GROUP_ID = "com.github.jershell" val ARTIFACT_ID = rootProject.name val BINTRAY_REPOSITORY = "generic" Index: src/main/kotlin/com/github/jershell/kbson/BsonEncoder.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/main/kotlin/com/github/jershell/kbson/BsonEncoder.kt b/src/main/kotlin/com/github/jershell/kbson/BsonEncoder.kt --- a/src/main/kotlin/com/github/jershell/kbson/BsonEncoder.kt (revision b67d8a7f3abfed8322c1c8a1e8024135685f2789) +++ b/src/main/kotlin/com/github/jershell/kbson/BsonEncoder.kt (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) @@ -8,9 +8,8 @@ import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.modules.SerializersModule -import org.bson.BsonBinary -import org.bson.BsonWriter -import org.bson.UuidRepresentation +import org.bson.* +import org.bson.json.JsonReader import org.bson.types.Decimal128 import org.bson.types.ObjectId import java.util.UUID @@ -226,8 +225,22 @@ } } + fun encodeJson(value: String) { + when (state) { + STATE.VALUE -> writer.pipe(JsonReader(value)) + STATE.NAME -> throw SerializationException("Json is not supported as a key of map") + } + } + + fun encodeDocument(value: BsonDocument) { + when (state) { + STATE.VALUE -> writer.pipe(BsonDocumentReader(value)) + STATE.NAME -> throw SerializationException("Document is not supported as a key of map") + } + } + private fun encodeStructName(value: Any) { writer.writeName(value.toString()) state = STATE.VALUE } -} \ No newline at end of file +} Index: src/test/kotlin/com/github/jershell/kbson/KBsonMapAnyTest.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/test/kotlin/com/github/jershell/kbson/KBsonMapAnyTest.kt b/src/test/kotlin/com/github/jershell/kbson/KBsonMapAnyTest.kt new file mode 100644 --- /dev/null (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) +++ b/src/test/kotlin/com/github/jershell/kbson/KBsonMapAnyTest.kt (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) @@ -0,0 +1,86 @@ +package com.github.jershell.kbson + +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.bson.* +import org.bson.types.Decimal128 +import java.math.BigDecimal +import java.time.LocalDateTime +import java.time.ZoneOffset +import kotlin.test.Test + +/** + * + * + * @author ageorgousakis + * @since 1.0 + */ +class KBsonMapAnyTest { + + @kotlinx.serialization.Serializable + data class ComplexData( + val title: String, + @Serializable(with = MapAnySerializer::class) + val data: Map, + ) + + private val kBson = KBson() + + val sampleDateTime = LocalDateTime.of(2019, 5, 1, 13, 45, 10,0).toInstant(ZoneOffset.UTC) + + val complexDataModel = ComplexData( + title = "test", + data = mapOf( + "foo" to "bar", + "one" to 1, + "twoDotZero" to 2.0, + "boolean" to true, + "none" to null, + "object" to mapOf( + "number" to 5, + "text" to "Cinque", + "dateTime" to sampleDateTime, + "bigDecimal" to BigDecimal.valueOf(15000.34), + ), + "list" to listOf(5, 6, 7), + ) + ) + + val complexDataDoc = BsonDocument().apply { +// put("createAt", BsonDateTime(ts)) + put("title", BsonString("test")) + put("data", BsonDocument().apply { + put("foo", BsonString("bar")) + put("one", BsonInt32(1)) + put("twoDotZero", BsonDouble(2.0)) + put("boolean", BsonBoolean(true)) + put("none", BsonNull()) + put("object", BsonDocument().apply { + put("number", BsonInt32(5)) + put("text", BsonString("Cinque")) + put("dateTime", BsonDateTime(sampleDateTime.toEpochMilli())) + put("bigDecimal", BsonDecimal128(Decimal128(BigDecimal.valueOf(15000.34)))) + }) + put("list", BsonArray( + listOf( + BsonInt32(5), + BsonInt32(6), + BsonInt32(7) + )) + ) + }) + } + + @Test + fun complexDataStringify() { + val actual = kBson.stringify(ComplexData.serializer(), complexDataModel) + println(actual) + } + + @Test + fun complexDataParse() { + val actual = kBson.parse(ComplexData.serializer(), complexDataDoc) + println(actual) + } + +} Index: src/test/kotlin/com/github/jershell/kbson/MapAnySerializer.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/test/kotlin/com/github/jershell/kbson/MapAnySerializer.kt b/src/test/kotlin/com/github/jershell/kbson/MapAnySerializer.kt new file mode 100644 --- /dev/null (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) +++ b/src/test/kotlin/com/github/jershell/kbson/MapAnySerializer.kt (revision 9ce1a7dec2099022388d6f7cb0c5878838b4ad66) @@ -0,0 +1,196 @@ +package com.github.jershell.kbson + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.json.* +import org.bson.BsonType +import java.time.Instant +import java.time.temporal.TemporalAccessor + +/** + * This handles serialization and deserialization of arbitrary JSON trees represented as + * `Map` + * + * Using `@Serializable` objects in the tree respects their declared serializers. + * + */ +object MapAnySerializer : KSerializer> { + @Serializable + private abstract class MapAnyMap : Map + + private fun parseBsonDocument(decoder: BsonFlexibleDecoder, map: MutableMap) { +// decoder.reader.readStartDocument() + var type = decoder.reader.readBsonType() + while (type != BsonType.END_OF_DOCUMENT) { + val key = decoder.reader.readName() + when (type) { + BsonType.NULL -> map[key] = decoder.decodeNull() + BsonType.STRING -> map[key] = decoder.decodeString() + BsonType.BOOLEAN -> map[key] = decoder.decodeBoolean() + BsonType.INT32 -> map[key] = decoder.decodeInt() + BsonType.INT64 -> map[key] = decoder.decodeLong() + BsonType.DOUBLE -> map[key] = decoder.decodeDouble() + BsonType.DECIMAL128 -> map[key] = decoder.reader.readDecimal128().toDouble() + BsonType.DATE_TIME -> map[key] = Instant.ofEpochMilli(decoder.reader.readDateTime()) + BsonType.TIMESTAMP -> map[key] = Instant.ofEpochMilli(decoder.reader.readTimestamp().value) + BsonType.BINARY -> map[key] = decoder.reader.readBinaryData() + BsonType.DOCUMENT -> { + val innerMap = mutableMapOf() + decoder.reader.readStartDocument() + parseBsonDocument(decoder, innerMap) + decoder.reader.readEndDocument() + map[key] = innerMap.toMap() + } + BsonType.ARRAY -> { + val innerList = mutableListOf() + decoder.reader.readStartArray() + var arrayType = decoder.reader.readBsonType() + while (arrayType != BsonType.END_OF_DOCUMENT) { + when (arrayType) { + BsonType.NULL -> innerList.add(decoder.decodeNull()) + BsonType.STRING -> innerList.add(decoder.decodeString()) + BsonType.BOOLEAN -> innerList.add(decoder.decodeBoolean()) + BsonType.INT32 -> innerList.add(decoder.decodeInt()) + BsonType.INT64 -> innerList.add(decoder.decodeLong()) + BsonType.DOUBLE -> innerList.add(decoder.decodeDouble()) + BsonType.DECIMAL128 -> innerList.add(decoder.reader.readDecimal128().toDouble()) + BsonType.DATE_TIME -> innerList.add(Instant.ofEpochMilli(decoder.reader.readDateTime())) + BsonType.TIMESTAMP -> innerList.add(Instant.ofEpochMilli(decoder.reader.readTimestamp().value)) + BsonType.DOCUMENT -> { + val innerMap = mutableMapOf() + decoder.reader.readStartDocument() + parseBsonDocument(decoder, innerMap) + decoder.reader.readEndDocument() + innerList.add(innerMap.toMap()) + } + else -> throw IllegalStateException("Array BsonType '$arrayType' not supported!") + } + arrayType = decoder.reader.readBsonType() + } + decoder.reader.readEndArray() + map[key] = innerList.toList() + } + else -> throw IllegalStateException("BsonType '$type' not supported!") + } + type = decoder.reader.readBsonType() + } +// decoder.reader.readEndDocument() + } + + + override val descriptor: SerialDescriptor = MapAnyMap.serializer().descriptor +// val descriptor: SerialDescriptor = Map.serializer().descriptor + + override fun deserialize(decoder: Decoder): Map { + if (decoder is JsonDecoder) { + val jsonObject = decoder.decodeJsonElement() as JsonObject + return jsonObject.toPrimitiveMap() + } else if (decoder is BsonFlexibleDecoder) { +// Json.decodeFromJsonElement() + return decoder.decodeStructure(descriptor) { + val map = mutableMapOf() + parseBsonDocument(decoder, map) + decoder.reader.readEndDocument() + map.toMap() + } + } else { + throw NotImplementedError("Decoder $decoder is not supported!") + } + } + + + override fun serialize(encoder: Encoder, value: Map) { + if (encoder is JsonEncoder) { + encoder.encodeJsonElement(value.toJsonElement()) + } else if (encoder is BsonEncoder) { + encoder.encodeJson(Json.encodeToString(this, value)) + } else { + throw NotImplementedError("Encoder $encoder is not supported!") + } + } + +} + +fun JsonObject.toPrimitiveMap(): Map = + this.map { + it.key to it.value.toPrimitive() + }.toMap() + +fun JsonElement.toPrimitive(): Any? = when (this) { + is JsonNull -> null + is JsonObject -> this.toPrimitiveMap() + is JsonArray -> this.map { it.toPrimitive() } + is JsonPrimitive -> { + if (isString) { + contentOrNull + } else { + booleanOrNull ?: longOrNull ?: doubleOrNull + } + } + else -> null +} + +fun Any?.toJsonElement(): JsonElement = when (this) { + null -> JsonNull + is JsonElement -> this + is Number -> JsonPrimitive(this) + is String -> JsonPrimitive(this) + is Boolean -> JsonPrimitive(this) + is TemporalAccessor -> JsonPrimitive(this.toString()) + is Enum<*> -> JsonPrimitive(this.toString()) + is Array<*> -> this.toJsonElement() + is Map<*, *> -> this.toJsonElement() + is Iterable<*> -> this.toJsonElement() + else -> throw IllegalStateException("Can't serialize unknown type: $this") +} + + +fun Iterable<*>.toJsonElement(): JsonElement { + val list = mutableListOf() + this.forEach { value -> + when (value) { + null -> list.add(JsonNull) + is JsonElement -> list.add(value) + is Map<*, *> -> list.add(value.toJsonElement()) + is List<*> -> list.add(value.toJsonElement()) + is Array<*> -> list.add(value.toJsonElement()) + is Boolean -> list.add(JsonPrimitive(value)) + is Number -> list.add(JsonPrimitive(value)) + is TemporalAccessor -> list.add(JsonPrimitive(value.toString())) + is String -> list.add(JsonPrimitive(value)) + is Enum<*> -> list.add(JsonPrimitive(value.toString())) + else -> throw IllegalStateException("Can't serialize unknown collection type: $value") + } + } + return JsonArray(list) +} + +fun Array<*>.toJsonElement(): JsonArray { + val list = mutableListOf() + this.forEach { list.add(it.toJsonElement()) } + return JsonArray(list) +} + +fun Map<*, *>.toJsonElement(): JsonObject { + val map = mutableMapOf() + this.forEach { (key, value) -> + key as String + when (value) { + null -> map[key] = JsonNull + is JsonElement -> map[key] = value + is Map<*, *> -> map[key] = value.toJsonElement() + is Iterable<*> -> map[key] = value.toJsonElement() + is Boolean -> map[key] = JsonPrimitive(value) + is Number -> map[key] = JsonPrimitive(value) + is TemporalAccessor -> map[key] = JsonPrimitive(value.toString()) + is String -> map[key] = JsonPrimitive(value) + is Enum<*> -> map[key] = JsonPrimitive(value.toString()) + else -> throw IllegalStateException("Can't serialize unknown type: $value") + } + } + return JsonObject(map) +}