diff --git a/library/src/main/kotlin/jp/co/panpanini/EnumGenerator.kt b/library/src/main/kotlin/jp/co/panpanini/EnumGenerator.kt index 21dd67d..58b98e6 100644 --- a/library/src/main/kotlin/jp/co/panpanini/EnumGenerator.kt +++ b/library/src/main/kotlin/jp/co/panpanini/EnumGenerator.kt @@ -34,6 +34,7 @@ class EnumGenerator { typeSpec.addProperty(PropertySpec.builder("value", Int::class).initializer("value").build()) typeSpec.addType(companion.build()) typeSpec.addFunction(createToStringFunction()) + typeSpec.addFunction(createToJsonFunction(type.values)) return typeSpec.build() } @@ -79,4 +80,19 @@ class EnumGenerator { .addCode("return name") .build() } + + private fun createToJsonFunction(values: List): FunSpec { + val whenBlock = CodeBlock.builder() + .beginControlFlow("return when(this)") + values.forEach { + whenBlock.addStatement("%L -> %S", it.kotlinValueName, it.name) + } + whenBlock.addStatement("else -> %S", values.first().name) + whenBlock.endControlFlow() + return FunSpec.builder("toJson") + .addModifiers(KModifier.OVERRIDE) + .returns(String::class) + .addCode(whenBlock.build()) + .build() + } } \ No newline at end of file diff --git a/library/src/main/kotlin/jp/co/panpanini/MessageGenerator.kt b/library/src/main/kotlin/jp/co/panpanini/MessageGenerator.kt index 0a8387b..7e7f18e 100644 --- a/library/src/main/kotlin/jp/co/panpanini/MessageGenerator.kt +++ b/library/src/main/kotlin/jp/co/panpanini/MessageGenerator.kt @@ -57,10 +57,13 @@ class MessageGenerator(private val file: File, private val kotlinTypeMappings: M typeSpec.primaryConstructor(constructor.build()) typeSpec.addFunction(createSecondaryConstructor(type)) + typeSpec.addFunction(createToJsonFunction(type)) + mapEntry?.let { typeSpec.addSuperinterface(mapEntry) } + type.nestedTypes.map { it.toTypeSpec(file, kotlinTypeMappings) }.forEach { @@ -517,6 +520,101 @@ class MessageGenerator(private val file: File, private val kotlinTypeMappings: M return typeSpec.build() } + private fun createToJsonFunction(type: File.Type.Message): FunSpec { + val builder = StringBuilder() + .append("return ") + .append("\"\"\"\n{ ") + .append( + type.fields.joinToString(", ") { field -> + "\n\"${field.name}\"·:·${getJsonValue(field)}" + } + ) + .append("\n}\n\"\"\".trimIndent()") + + return FunSpec.builder("toJson") + .addModifiers(KModifier.OVERRIDE) + .addCode(CodeBlock.of(builder.toString())) + .build() + } + + private fun getJsonValue(field: File.Field, prefix: String = ""): String { + return when (field) { + is File.Field.Standard -> { + when { + field.map -> { + getMapJsonValue(field) + } + field.repeated -> { + getListJsonValue(field) + } + else -> { + "\"\${${getStandardJsonValue(field, "$prefix${field.kotlinFieldName}")}}\"" + + } + } + } + is File.Field.OneOf -> { + val block = CodeBlock.builder() + block.add("\${ ") + block.beginControlFlow("when (${field.kotlinFieldName})") + field.fields.forEach { + val code = CodeBlock.builder() + .beginControlFlow("is ${field.kotlinTypeName}.${it.kotlinFieldName.beginWithUpperCase()} ->") + .addStatement( + getJsonValue(it, "${field.kotlinFieldName}.") + ) + .endControlFlow() + block.add(code.build()) + } + block.beginControlFlow("is ${field.kotlinTypeName}.NotSet ->") + .addStatement("null") + .endControlFlow() + + block.endControlFlow() + block.addStatement("}") + block.build().toString() + } + } + } + + private fun getStandardJsonValue(field: File.Field.Standard, fieldName: String): String { + return when (field.type) { + File.Field.Type.BYTES -> "$fieldName.base64Encode()" + File.Field.Type.ENUM -> "$fieldName.toJson()" + File.Field.Type.MESSAGE -> "$fieldName.toJson()" + File.Field.Type.STRING -> fieldName + // TODO: INT64 -> return String value + else -> "$fieldName.toString()" + } + } + + private fun getMapJsonValue(field: File.Field.Standard): String { + // get the value type so we can call the correct function to get the correct json representation + val value = field.mapEntry()?.fields?.get(1) as? File.Field.Standard + ?: throw IllegalStateException("map value must not be null") + + val builder = StringBuilder() + .append("{·") + .append("\${${field.kotlinFieldName}.entries.joinToString(\",·\")·{·(k,·v)·->\"\$k·:·\${${getStandardJsonValue(value, "v")}}\"·}·}") + .append("·}") + + return builder.toString() + + + } + + private fun getListJsonValue(field: File.Field.Standard): String { + val builder = StringBuilder() + builder.append("[ ") + builder.append( + "\${ ${field.kotlinFieldName}.joinToString(\", \") { ${getStandardJsonValue(field, "it")} }" + ) + builder.append(" }") + + builder.append(" ]") + return builder.toString() + } + private fun File.Field.Standard.sizeExpression(): CodeBlock { val sizer = Sizer::class val codeBlock = CodeBlock.builder() diff --git a/library/src/test/resources/input/mappy.input b/library/src/test/resources/input/mappy.input index 99693d1..21ebd3d 100644 Binary files a/library/src/test/resources/input/mappy.input and b/library/src/test/resources/input/mappy.input differ diff --git a/library/src/test/resources/kotlin/Item.kt b/library/src/test/resources/kotlin/Item.kt index fcde792..f2b9dcc 100644 --- a/library/src/test/resources/kotlin/Item.kt +++ b/library/src/test/resources/kotlin/Item.kt @@ -21,6 +21,11 @@ data class Item(@JvmField val id: String = "", val unknownFields: Map "PROTOBUF" + KOTLIN -> "KOTLIN" + JAVA -> "JAVA" + SWIFT -> "SWIFT" + GO -> "GO" + else -> "PROTOBUF" + } + companion object : Message.Enum.Companion { @JvmStatic override fun fromValue(value: Int): Language = when(value) { diff --git a/library/src/test/resources/kotlin/Mappy.kt b/library/src/test/resources/kotlin/Mappy.kt index cc696d2..2ec7811 100644 --- a/library/src/test/resources/kotlin/Mappy.kt +++ b/library/src/test/resources/kotlin/Mappy.kt @@ -10,6 +10,7 @@ import jp.co.panpanini.Unmarshaller import kotlin.ByteArray import kotlin.Int import kotlin.String +import kotlin.collections.List import kotlin.collections.Map import kotlin.jvm.JvmField import kotlin.jvm.JvmStatic @@ -17,13 +18,29 @@ import kotlin.jvm.JvmStatic data class Mappy( @JvmField val id: String = "", @JvmField val things: Map = emptyMap(), + @JvmField val otherThings: Map = emptyMap(), + @JvmField val thingList: List = emptyList(), val unknownFields: Map = emptyMap() ) : Message, Serializable { override val protoSize: Int = protoSizeImpl() - constructor(id: String, things: Map) : this(id, things, emptyMap()) - + constructor( + id: String, + things: Map, + otherThings: Map, + thingList: List + ) : this(id, things, otherThings, thingList, emptyMap()) + + override fun toJson() = """ + { + "id" : "${id}", + "things" : { ${things.entries.joinToString(", ") { (k, v) ->"$k : ${v.toJson()}" } } }, + "otherThings" : { ${otherThings.entries.joinToString(", ") { (k, v) ->"$k : ${v.toString()}" } } }, + + "thingList" : [ ${ thingList.joinToString(", ") { it.toJson() } } ] + } + """.trimIndent() fun Mappy.protoSizeImpl(): Int { var protoSize = 0 if (id != DEFAULT_ID) { @@ -32,6 +49,13 @@ data class Mappy( if (things.isNotEmpty()) { protoSize += jp.co.panpanini.Sizer.mapSize(2, things, api.Mappy::ThingsEntry) } + if (otherThings.isNotEmpty()) { + protoSize += jp.co.panpanini.Sizer.mapSize(3, otherThings, api.Mappy::OtherThingsEntry) + } + if (thingList.isNotEmpty()) { + protoSize += jp.co.panpanini.Sizer.tagSize(4) * thingList.size + + thingList.sumBy(jp.co.panpanini.Sizer::messageSize) + } protoSize += unknownFields.entries.sumBy { it.value.size() } return protoSize } @@ -44,6 +68,14 @@ data class Mappy( if (things.isNotEmpty()) { protoMarshal.writeMap(18, things, api.Mappy::ThingsEntry) + } + if (otherThings.isNotEmpty()) { + protoMarshal.writeMap(26, otherThings, api.Mappy::OtherThingsEntry) + + } + if (thingList.isNotEmpty()) { + thingList.forEach { protoMarshal.writeTag(34).writeMessage(it) } + } if (unknownFields.isNotEmpty()) { protoMarshal.writeUnknownFields(unknownFields) @@ -52,6 +84,8 @@ data class Mappy( fun Mappy.protoMergeImpl(other: Mappy?): Mappy = other?.copy( things = things + other.things, + otherThings = otherThings + other.otherThings, + thingList = thingList + other.thingList, unknownFields = unknownFields + other.unknownFields ) ?: this @@ -67,6 +101,8 @@ data class Mappy( fun newBuilder(): Builder = Builder() .id(id) .things(things) + .otherThings(otherThings) + .thingList(thingList) .unknownFields(unknownFields) data class ThingsEntry( @@ -79,6 +115,12 @@ data class Mappy( constructor(key: String, value: api.Thing) : this(key, value, emptyMap()) + override fun toJson() = """ + { + "key" : "${key}", + "value" : "${value.toJson()}" + } + """.trimIndent() fun ThingsEntry.protoSizeImpl(): Int { var protoSize = 0 if (key != DEFAULT_KEY) { @@ -146,6 +188,90 @@ data class Mappy( } } + data class OtherThingsEntry( + override val key: String, + override val value: Int, + val unknownFields: Map = emptyMap() + ) : Message, Serializable, Map.Entry { + override val protoSize: Int = protoSizeImpl() + + + constructor(key: String, value: Int) : this(key, value, emptyMap()) + + override fun toJson() = """ + { + "key" : "${key}", + "value" : "${value.toString()}" + } + """.trimIndent() + fun OtherThingsEntry.protoSizeImpl(): Int { + var protoSize = 0 + if (key != DEFAULT_KEY) { + protoSize += jp.co.panpanini.Sizer.tagSize(1) + + jp.co.panpanini.Sizer.stringSize(key) + } + if (value != DEFAULT_VALUE) { + protoSize += jp.co.panpanini.Sizer.tagSize(2) + + jp.co.panpanini.Sizer.int32Size(value) + } + protoSize += unknownFields.entries.sumBy { it.value.size() } + return protoSize + } + + fun OtherThingsEntry.protoMarshalImpl(protoMarshal: Marshaller) { + if (key != DEFAULT_KEY) { + protoMarshal.writeTag(10).writeString(key) + + } + if (value != DEFAULT_VALUE) { + protoMarshal.writeTag(16).writeInt32(value) + + } + if (unknownFields.isNotEmpty()) { + protoMarshal.writeUnknownFields(unknownFields) + } + } + + fun OtherThingsEntry.protoMergeImpl(other: OtherThingsEntry?): OtherThingsEntry = + other?.copy( + unknownFields = unknownFields + other.unknownFields + ) ?: this + + override fun protoMarshal(marshaller: Marshaller) = protoMarshalImpl(marshaller) + + override operator fun plus(other: OtherThingsEntry?): OtherThingsEntry = + protoMergeImpl(other) + + fun encode(): ByteArray = protoMarshal() + + override fun protoUnmarshal(protoUnmarshal: Unmarshaller): OtherThingsEntry = + Companion.protoUnmarshal(protoUnmarshal) + + companion object : Message.Companion { + @JvmField + val DEFAULT_KEY: String = "" + + @JvmField + val DEFAULT_VALUE: Int = 0 + + override fun protoUnmarshal(protoUnmarshal: Unmarshaller): OtherThingsEntry { + var key = "" + var value = 0 + while (true) { + when (protoUnmarshal.readTag()) { + 0 -> return OtherThingsEntry(key, value, protoUnmarshal.unknownFields()) + 10 -> key = protoUnmarshal.readString() + 16 -> value = protoUnmarshal.readInt32() + else -> protoUnmarshal.unknownField() + } + } + } + + @JvmStatic + fun decode(arr: ByteArray): OtherThingsEntry = protoUnmarshal(arr) + } + } + companion object : Message.Companion { @JvmField val DEFAULT_ID: String = "" @@ -153,15 +279,28 @@ data class Mappy( @JvmField val DEFAULT_THINGS: Map = emptyMap() + @JvmField + val DEFAULT_OTHER_THINGS: Map = emptyMap() + + @JvmField + val DEFAULT_THING_LIST: List = emptyList() + override fun protoUnmarshal(protoUnmarshal: Unmarshaller): Mappy { var id = "" var things: Map = emptyMap() + var otherThings: Map = emptyMap() + var thingList: List = emptyList() while (true) { when (protoUnmarshal.readTag()) { - 0 -> return Mappy(id, HashMap(things), protoUnmarshal.unknownFields()) + 0 -> return Mappy(id, HashMap(things), HashMap(otherThings), + thingList, protoUnmarshal.unknownFields()) 10 -> id = protoUnmarshal.readString() 18 -> things = protoUnmarshal.readMap(things, api.Mappy.ThingsEntry.Companion, true) + 26 -> otherThings = protoUnmarshal.readMap(otherThings, + api.Mappy.OtherThingsEntry.Companion, true) + 34 -> thingList = protoUnmarshal.readRepeatedMessage(thingList, + api.Thing.Companion, true) else -> protoUnmarshal.unknownField() } } @@ -176,6 +315,10 @@ data class Mappy( var things: Map = DEFAULT_THINGS + var otherThings: Map = DEFAULT_OTHER_THINGS + + var thingList: List = DEFAULT_THING_LIST + var unknownFields: Map = emptyMap() fun id(id: String?): Builder { @@ -188,11 +331,21 @@ data class Mappy( return this } + fun otherThings(otherThings: Map?): Builder { + this.otherThings = otherThings ?: DEFAULT_OTHER_THINGS + return this + } + + fun thingList(thingList: List?): Builder { + this.thingList = thingList ?: DEFAULT_THING_LIST + return this + } + fun unknownFields(unknownFields: Map): Builder { this.unknownFields = unknownFields return this } - fun build(): Mappy = Mappy(id, things, unknownFields) + fun build(): Mappy = Mappy(id, things, otherThings, thingList, unknownFields) } } diff --git a/library/src/test/resources/kotlin/OneOfTest.kt b/library/src/test/resources/kotlin/OneOfTest.kt index 461ea56..8a92c08 100644 --- a/library/src/test/resources/kotlin/OneOfTest.kt +++ b/library/src/test/resources/kotlin/OneOfTest.kt @@ -26,6 +26,41 @@ data class OneOfTest(@JvmField val oneofField: OneofField = OneofField.NotSet, v constructor(oneofField: OneofField) : this(oneofField, emptyMap()) + override fun toJson() = """ + { + "oneof_field" : ${ when (oneofField) { + is OneofField.OneofUint32 -> { + "${oneofField.oneofUint32.toString()}" + } + is OneofField.OneofString -> { + "${oneofField.oneofString}" + } + is OneofField.OneofBytes -> { + "${oneofField.oneofBytes.base64Encode()}" + } + is OneofField.OneofBool -> { + "${oneofField.oneofBool.toString()}" + } + is OneofField.OneofUint64 -> { + "${oneofField.oneofUint64.toString()}" + } + is OneofField.OneofFloat -> { + "${oneofField.oneofFloat.toString()}" + } + is OneofField.OneofDouble -> { + "${oneofField.oneofDouble.toString()}" + } + is OneofField.OneofItem -> { + "${oneofField.oneofItem.toJson()}" + } + is OneofField.NotSet -> { + null + } + } + } + + } + """.trimIndent() fun OneOfTest.protoSizeImpl(): Int { var protoSize = 0 if (oneofField !is OneofField.NotSet) { diff --git a/library/src/test/resources/kotlin/Thing.kt b/library/src/test/resources/kotlin/Thing.kt index d678a48..714a992 100644 --- a/library/src/test/resources/kotlin/Thing.kt +++ b/library/src/test/resources/kotlin/Thing.kt @@ -21,6 +21,11 @@ data class Thing(@JvmField val id: String = "", val unknownFields: Map things = 2; + map otherThings = 3; + repeated Thing thingList = 4; } message Thing { diff --git a/runtime/src/main/kotlin/ByteArr.kt b/runtime/src/main/kotlin/ByteArr.kt index 5783620..6d15e51 100644 --- a/runtime/src/main/kotlin/ByteArr.kt +++ b/runtime/src/main/kotlin/ByteArr.kt @@ -1,9 +1,19 @@ package jp.co.panpanini import java.io.Serializable +import java.nio.charset.StandardCharsets +import java.util.Base64 class ByteArr(val array: ByteArray = ByteArray(0)) : Serializable { override fun equals(other: Any?) = other is ByteArr && array.contentEquals(other.array) override fun hashCode() = array.contentHashCode() override fun toString() = array.contentToString() + fun base64Encode(): String { + return String(Base64.getUrlEncoder().encode(array), StandardCharsets.UTF_8) + } + companion object { + fun base64Decode(base64String: String): ByteArr { + return ByteArr(Base64.getUrlDecoder().decode(base64String)) + } + } } \ No newline at end of file diff --git a/runtime/src/main/kotlin/Message.kt b/runtime/src/main/kotlin/Message.kt index a30a874..328e9b5 100644 --- a/runtime/src/main/kotlin/Message.kt +++ b/runtime/src/main/kotlin/Message.kt @@ -14,6 +14,8 @@ interface Message> : Serializable { fun protoMarshal(m: Marshaller) fun protoMarshal(): ByteArray = Marshaller.allocate(protoSize).also(::protoMarshal).complete()!! + fun toJson(): String + interface Companion> { fun protoUnmarshal(u: Unmarshaller): T fun protoUnmarshal(arr: ByteArray) = protoUnmarshal(Unmarshaller.fromByteArray(arr)) @@ -21,6 +23,7 @@ interface Message> : Serializable { interface Enum : Serializable { val value: Int + fun toJson(): String interface Companion { fun fromValue(value: Int): T