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

feat: Support kotlin's value classes #183

Merged
merged 8 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = false
max_line_length = 120
max_line_length = 180
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
Expand All @@ -23,7 +23,6 @@ ij_editorconfig_spaces_around_assignment_operators = true

[{*.kt,*.kts}]
ktlint_standard_filename = disabled
max_line_length = 180
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@ class AnnotationExtractor(private val annotations: List<Annotation>) {

fun fixed(): Int? = annotations.filterIsInstance<AvroFixed>().firstOrNull()?.size

fun scalePrecision(): Pair<Int, Int>? = annotations.filterIsInstance<ScalePrecision>().firstOrNull()?.let { it.scale to it.precision }

fun namespace(): String? = annotations.filterIsInstance<AvroNamespace>().firstOrNull()?.value

fun name(): String? = annotations.filterIsInstance<AvroName>().firstOrNull()?.value

fun valueType(): Boolean = annotations.filterIsInstance<AvroInline>().isNotEmpty()

fun doc(): String? = annotations.filterIsInstance<AvroDoc>().firstOrNull()?.value

fun aliases(): List<String> =
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class AvroOutputStreamBuilder<T>(
@OptIn(ExperimentalSerializationApi::class)
class Avro(
override val serializersModule: SerializersModule = defaultModule,
private val configuration: AvroConfiguration = AvroConfiguration(),
internal val configuration: AvroConfiguration = AvroConfiguration(),
) : SerialFormat, BinaryFormat {
constructor(configuration: AvroConfiguration) : this(defaultModule, configuration)

Expand Down
25 changes: 5 additions & 20 deletions src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ annotation class AvroNamespace(val value: String)
annotation class AvroName(val value: String)

@SerialInfo
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Target(AnnotationTarget.PROPERTY)
annotation class ScalePrecision(val scale: Int = 2, val precision: Int = 8)

@SerialInfo
Expand Down Expand Up @@ -89,10 +89,6 @@ enum class LogicalTimeTypeEnum(val logicalTypeName: String, val kind: PrimitiveK
),
}

@SerialInfo
@Target(AnnotationTarget.CLASS)
annotation class AvroInline

@SerialInfo
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class AvroDoc(val value: String)
Expand All @@ -110,26 +106,15 @@ annotation class AvroAlias(vararg val value: String)
annotation class AvroAliases(val value: Array<String>)

/**
* [AvroFixed] overrides the schema type for a field or a value class
* so that the schema is set to org.apache.avro.Schema.Type.FIXED
* rather than whatever the default would be.
*
* This annotation can be used in the following ways:
*
* - On a field, eg data class `Foo(@AvroField(10) val name: String)`
* which results in the field `name` having schema type FIXED with
* a size of 10.
*
* - On a value type, eg `@AvroField(7) data class Foo(val name: String)`
* which results in all usages of this type having schema
* FIXED with a size of 7 rather than the default.
* Indicates that the annotated property should be encoded as an Avro fixed type.
* @param size The number of bytes of the fixed type. Note that smaller values will be padded with 0s during encoding, but not unpadded when decoding.
*/
@SerialInfo
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Target(AnnotationTarget.PROPERTY)
annotation class AvroFixed(val size: Int)

@SerialInfo
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Target(AnnotationTarget.PROPERTY)
annotation class AvroDefault(
@Language("JSON") val value: String,
)
Expand Down

This file was deleted.

63 changes: 51 additions & 12 deletions src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,63 +31,102 @@ class MapDecoder(
private var index = -1

override fun decodeString(): String {
val entry = entries[index / 2]
val value =
when {
index % 2 == 0 -> entry.first
else -> entry.second
}
val value = keyOrValue()
return StringFromAvroValue.fromValue(value)
}

private fun keyOrValue() = if (expectKey()) key() else value()

private fun expectKey() = index % 2 == 0

private fun key(): Any? = entries[index / 2].first

private fun value(): Any? = entries[index / 2].second

override fun decodeNotNullMark(): Boolean {
return keyOrValue() != null
}

override fun decodeFloat(): Float {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Float -> v
is CharSequence -> v.toString().toFloat()
null -> throw SerializationException("Cannot decode <null> as a Float")
else -> throw SerializationException("Unsupported type for Float ${v::class.qualifiedName}")
}
}

override fun decodeInt(): Int {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Int -> v
is CharSequence -> v.toString().toInt()
null -> throw SerializationException("Cannot decode <null> as a Int")
else -> throw SerializationException("Unsupported type for Int ${v::class.qualifiedName}")
}
}

override fun decodeLong(): Long {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Long -> v
is Int -> v.toLong()
is CharSequence -> v.toString().toLong()
null -> throw SerializationException("Cannot decode <null> as a Long")
else -> throw SerializationException("Unsupported type for Long ${v::class.qualifiedName}")
}
}

override fun decodeDouble(): Double {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Double -> v
is Float -> v.toDouble()
is CharSequence -> v.toString().toDouble()
null -> throw SerializationException("Cannot decode <null> as a Double")
else -> throw SerializationException("Unsupported type for Double ${v::class.qualifiedName}")
}
}

override fun decodeByte(): Byte {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Byte -> v
is Int -> v.toByte()
is CharSequence -> v.toString().toByte()
null -> throw SerializationException("Cannot decode <null> as a Byte")
else -> throw SerializationException("Unsupported type for Byte ${v::class.qualifiedName}")
}
}

override fun decodeChar(): Char {
return when (val v = keyOrValue()) {
is Char -> v
is Int -> v.toChar()
is CharSequence -> v.first()
null -> throw SerializationException("Cannot decode <null> as a Char")
else -> throw SerializationException("Unsupported type for Char ${v::class.qualifiedName}")
}
}

override fun decodeShort(): Short {
return when (val v = keyOrValue()) {
is Short -> v
is Int -> v.toShort()
is CharSequence -> v.toString().toShort()
null -> throw SerializationException("Cannot decode <null> as a Byte")
else -> throw SerializationException("Unsupported type for Byte ${v::class.qualifiedName}")
}
}

override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
return when (val v = keyOrValue()) {
is CharSequence -> enumDescriptor.getElementIndex(v.toString())
null -> throw SerializationException("Cannot decode <null> as a $enumDescriptor")
else -> throw SerializationException("Unsupported type for $enumDescriptor: ${v::class.qualifiedName}")
}
}

override fun decodeBoolean(): Boolean {
return when (val v = value()) {
return when (val v = keyOrValue()) {
is Boolean -> v
is CharSequence -> v.toString().toBooleanStrict()
null -> throw SerializationException("Cannot decode <null> as a Boolean")
else -> throw SerializationException("Unsupported type for Boolean. Actual: ${v::class.qualifiedName}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,9 @@ class RecordDecoder(

@Suppress("UNCHECKED_CAST")
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val valueType = AnnotationExtractor(descriptor.annotations).valueType()
val value = fieldValue()
return when (descriptor.kind) {
StructureKind.CLASS ->
if (valueType) {
InlineDecoder(fieldValue(), serializersModule)
} else {
RecordDecoder(descriptor, value as GenericRecord, serializersModule, configuration)
}
StructureKind.CLASS -> RecordDecoder(descriptor, value as GenericRecord, serializersModule, configuration)
StructureKind.MAP ->
MapDecoder(
descriptor,
Expand All @@ -55,6 +49,7 @@ class RecordDecoder(
serializersModule,
configuration
)

StructureKind.LIST -> {
val decoder: CompositeDecoder =
if (descriptor.getElementDescriptor(0).kind == PrimitiveKind.BYTE) {
Expand All @@ -75,13 +70,15 @@ class RecordDecoder(
}
decoder
}

PolymorphicKind.SEALED, PolymorphicKind.OPEN ->
UnionDecoder(
descriptor,
value as GenericRecord,
serializersModule,
configuration
)

else -> throw UnsupportedOperationException("Decoding descriptor of kind ${descriptor.kind} is currently not supported")
}
}
Expand Down
105 changes: 96 additions & 9 deletions src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,91 @@ class MapEncoder(
) : AbstractEncoder(),
CompositeEncoder,
StructureEncoder {
private val map = mutableMapOf<Utf8, Any>()
private var key: Utf8? = null
private val map = mutableMapOf<Utf8, Any?>()
private var key: String? = null
private val valueSchema = schema.valueType

override fun encodeString(value: String) {
val k = key
if (k == null) {
key = Utf8(value)
if (key == null) {
key = value
} else {
finalizeMapEntry(StringToAvroValue.toValue(valueSchema, value))
}
}

override fun encodeBoolean(value: Boolean) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeByte(value: Byte) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeChar(value: Char) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeDouble(value: Double) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeEnum(
enumDescriptor: SerialDescriptor,
index: Int,
) {
val value = enumDescriptor.getElementName(index)
if (key == null) {
key = value
} else {
finalizeMapEntry(value)
}
}

override fun encodeInt(value: Int) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeLong(value: Long) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

override fun encodeFloat(value: Float) {
if (key == null) {
key = value.toString()
} else {
map[k] = StringToAvroValue.toValue(valueSchema, value)
key = null
finalizeMapEntry(value)
}
}

override fun encodeShort(value: Short) {
if (key == null) {
key = value.toString()
} else {
finalizeMapEntry(value)
}
}

Expand All @@ -38,11 +112,24 @@ class MapEncoder(
if (k == null) {
throw SerializationException("Expected key but received value $value")
} else {
map[k] = value
key = null
finalizeMapEntry(value)
}
}

override fun encodeNull() {
val k = key
if (k == null) {
throw SerializationException("Expected key but received <null>")
} else {
finalizeMapEntry(null)
}
}

private fun finalizeMapEntry(value: Any?) {
map[Utf8(key)] = value
key = null
}

override fun endStructure(descriptor: SerialDescriptor) {
callback(map.toMap())
}
Expand Down
Loading
Loading