Skip to content

Commit

Permalink
Updated RecordEncoderGenerator to use kclass
Browse files Browse the repository at this point in the history
  • Loading branch information
sksamuel committed Apr 27, 2024
1 parent 4eedab6 commit f76ce8b
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.apache.avro.Schema
/**
* Generates a data class model for a given schema.
*/
class DataClassGenerator {
class DataClassModelBuilder {

fun generate(schema: Schema): DataClass {
require(schema.type == Schema.Type.RECORD) { "Type must be a record in order to generate a data class" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,73 @@
package com.sksamuel.centurion.avro.generation

import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.starProjectedType

/**
* Generates Kotlin code to programatically create an Encoder for a specific type.
*/
class RecordEncoderGenerator {

fun generate(ds: DataClass): String {
fun generate(kclass: KClass<*>): String {
require(kclass.isData) { "Generator supports data classes only: was $kclass" }
return buildString {
appendLine("package ${ds.packageName}")
appendLine("package ${kclass.java.packageName}")
appendLine()
appendLine("import com.sksamuel.centurion.avro.encoders.*")
appendLine("import org.apache.avro.Schema")
appendLine("import org.apache.avro.generic.GenericData")
appendLine("import org.apache.avro.generic.GenericRecord")
appendLine()
appendLine("/**")
appendLine(" * This is a generated [Encoder] that encodes [${ds.className}]s to Avro [GenericRecord]s")
appendLine(" * This is a generated [Encoder] that encodes [${kclass.java.simpleName}]s to Avro [GenericRecord]s")
appendLine(" */")
appendLine("object ${ds.className}Encoder : Encoder<${ds.className}> {")
appendLine(" override fun encode(schema: Schema, value: ${ds.className}): GenericRecord {")
appendLine("object ${kclass.java.simpleName}Encoder : Encoder<${kclass.java.simpleName}> {")
appendLine(" override fun encode(schema: Schema, value: ${kclass.java.simpleName}): GenericRecord {")
appendLine(" val record = GenericData.Record(schema)")
ds.members.forEach { member ->
appendLine(" record.put(\"${member.name}\", ${encoderFor(member)})")
kclass.declaredMemberProperties.forEach { property ->
appendLine(" record.put(\"${property.name}\", ${encoderFor(property)})")
}
appendLine(" return record")
appendLine(" }")
appendLine("}")
}
}

private fun encoderFor(member: Member): String {
val getSchema = "schema.getField(\"${member.name}\").schema()"
return when (member.type) {
Type.BooleanType -> "BooleanEncoder.encode($getSchema, value.${member.name})"
Type.DoubleType -> "DoubleEncoder.encode($getSchema, schema, value.${member.name})"
Type.FloatType -> "FloatEncoder.encode($getSchema, schema, value.${member.name})"
Type.IntType -> "IntEncoder.encode($getSchema, value.${member.name})"
Type.LongType -> "LongEncoder.encode($getSchema, value.${member.name})"
is Type.Nullable -> TODO()
is Type.RecordType -> TODO()
Type.StringType -> "StringEncoder.encode($getSchema, value.${member.name})"
is Type.ArrayType -> "ListEncoder.encode($getSchema, value.${member.name})"
is Type.MapType -> "MapEncoder.encode($getSchema, value.${member.name})"
private fun encoderFor(property: KProperty1<out Any, *>): String {
val getSchema = "schema.getField(\"${property.name}\").schema()"
val getValue = "value.${property.name}"
val encoder = encoderFor(property.returnType)
return "$encoder.encode($getSchema, $getValue)"
}

private fun encoderFor(type: KType): String {
return when (val classifier = type.classifier) {
Boolean::class -> "BooleanEncoder"
Double::class -> "DoubleEncoder"
Float::class -> "FloatEncoder"
Int::class -> "IntEncoder"
Long::class -> "LongEncoder"
String::class -> "StringEncoder"
Set::class -> {
val elementEncoder = encoderFor(type.arguments.first().type!!)
"SetEncoder($elementEncoder)"
}

List::class -> {
val elementEncoder = encoderFor(type.arguments.first().type!!)
"ListEncoder($elementEncoder)"
}

Map::class -> {
val valueEncoder = encoderFor(type.arguments[1].type!!)
"MapEncoder(StringEncoder, $valueEncoder)"
}

is KClass<*> -> if (classifier.java.isEnum) "EnumEncoder()" else error("Unsupported type: $type")
else -> error("Unsupported type: $type")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
package com.sksamuel.centurion.avro.generation


import com.sksamuel.centurion.avro.encoders.Wine
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class RecordEncoderGeneratorTest : FunSpec({

data class MyFoo(
val b: Boolean,
val s: String,
val c: Long,
val sets: Set<String>,
val lists: List<Int>,
val maps: Map<String, Double>,
val wine: Wine,
)

test("simple encoder") {
RecordEncoderGenerator().generate(
DataClass(
"a.b",
"Foo",
listOf(
Member("a", Type.BooleanType),
Member("b", Type.StringType),
)
)
).trim() shouldBe """
package a.b
RecordEncoderGenerator().generate(MyFoo::class).trim() shouldBe """
package com.sksamuel.centurion.avro.generation
import com.sksamuel.centurion.avro.encoders.*
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.apache.avro.generic.GenericRecord
/**
* This is a generated [Encoder] that encodes [Foo]s to Avro [GenericRecord]s
* This is a generated [Encoder] that encodes [MyFoo]s to Avro [GenericRecord]s
*/
object FooEncoder : Encoder<Foo> {
override fun encode(schema: Schema, value: Foo): GenericRecord {
object MyFooEncoder : Encoder<MyFoo> {
override fun encode(schema: Schema, value: MyFoo): GenericRecord {
val record = GenericData.Record(schema)
record.put("a", BooleanEncoder.encode(schema.getField("a").schema(), value.a))
record.put("b", StringEncoder.encode(schema.getField("b").schema(), value.b))
record.put("b", BooleanEncoder.encode(schema.getField("b").schema(), value.b))
record.put("c", LongEncoder.encode(schema.getField("c").schema(), value.c))
record.put("lists", ListEncoder(IntEncoder).encode(schema.getField("lists").schema(), value.lists))
record.put("maps", MapEncoder(StringEncoder, DoubleEncoder).encode(schema.getField("maps").schema(), value.maps))
record.put("s", StringEncoder.encode(schema.getField("s").schema(), value.s))
record.put("sets", SetEncoder(StringEncoder).encode(schema.getField("sets").schema(), value.sets))
record.put("wine", EnumEncoder().encode(schema.getField("wine").schema(), value.wine))
return record
}
}
""".trim()
}

})

0 comments on commit f76ce8b

Please sign in to comment.