diff --git a/settings.gradle.kts b/settings.gradle.kts index dbefda03..ccedc97f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,24 +10,24 @@ rootProject.name = "avro4k-core" dependencyResolutionManagement { versionCatalogs { create("libs") { - version("kotlin", "1.8.20") + version("kotlin", "1.9.22") version("jvm", "18") library("xerial-snappy", "org.xerial.snappy", "snappy-java").version("1.1.10.1") library("apache-avro", "org.apache.avro", "avro").version("1.11.3") - val kotlinxSerialization = "1.5.0" + val kotlinxSerialization = "1.6.2" library("kotlinx-serialization-core", "org.jetbrains.kotlinx", "kotlinx-serialization-core").version(kotlinxSerialization) library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version(kotlinxSerialization) - val kotestVersion = "5.6.1" + val kotestVersion = "5.8.0" library("kotest-core", "io.kotest", "kotest-assertions-core").version(kotestVersion) library("kotest-json", "io.kotest", "kotest-assertions-json").version(kotestVersion) library("kotest-junit5", "io.kotest", "kotest-runner-junit5").version(kotestVersion) library("kotest-property", "io.kotest", "kotest-property").version(kotestVersion) - plugin("dokka", "org.jetbrains.dokka").version("1.8.10") - plugin("kotest", "io.kotest").version("0.4.10") + plugin("dokka", "org.jetbrains.dokka").version("1.9.10") + plugin("kotest", "io.kotest").version("0.4.11") plugin("github-versions", "com.github.ben-manes.versions").version("0.46.0") plugin("nexus-publish", "io.github.gradle-nexus.publish-plugin").version("1.3.0") plugin("spotless", "com.diffplug.spotless").version("6.25.0") diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt index 1178ec98..5524bc4c 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt @@ -19,12 +19,6 @@ class AnnotationExtractor(private val annotations: List) { fun fixed(): Int? = annotations.filterIsInstance().firstOrNull()?.size - fun scalePrecision(): Pair? = annotations.filterIsInstance().firstOrNull()?.let { it.scale to it.precision } - - fun namespace(): String? = annotations.filterIsInstance().firstOrNull()?.value - - fun name(): String? = annotations.filterIsInstance().firstOrNull()?.value - fun valueType(): Boolean = annotations.filterIsInstance().isNotEmpty() fun doc(): String? = annotations.filterIsInstance().firstOrNull()?.value @@ -34,7 +28,7 @@ class AnnotationExtractor(private val annotations: List) { annotations.firstNotNullOfOrNull { it as? AvroAlias }?.value ?: emptyArray() - ).asList() + (annotations.firstNotNullOfOrNull { it as? AvroAliases }?.value ?: emptyArray()) + ).asList() fun props(): List> = annotations.filterIsInstance().map { it.key to it.value } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt index 6fea4fd5..917bdd6d 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt @@ -280,7 +280,7 @@ class Avro( obj: T, ): GenericRecord { var record: Record? = null - val encoder = RootRecordEncoder(schema, serializersModule) { record = it } + val encoder = RootRecordEncoder(schema, serializersModule, configuration) { record = it } encoder.encodeSerializableValue(serializer, obj) return record!! } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt index e159739f..18cd7c85 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt @@ -1,10 +1,11 @@ package com.github.avrokotlin.avro4k -import com.github.avrokotlin.avro4k.schema.DefaultNamingStrategy -import com.github.avrokotlin.avro4k.schema.NamingStrategy +import com.github.avrokotlin.avro4k.schema.FieldNamingStrategy +import com.github.avrokotlin.avro4k.schema.RecordNamingStrategy data class AvroConfiguration( - val namingStrategy: NamingStrategy = DefaultNamingStrategy, + val recordNamingStrategy: RecordNamingStrategy = RecordNamingStrategy.Default, + val fieldNamingStrategy: FieldNamingStrategy = FieldNamingStrategy.Default, /** * By default, during decoding, any missing value for a nullable field without default [null] value (e.g. `val field: Type?` without `= null`) is failing. * When set to [true], the nullable fields that haven't any default value are set as null if the value is missing. It also adds `"default": null` to those fields when generating schema using avro4k. diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/FieldNaming.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/FieldNaming.kt deleted file mode 100644 index 7d9331d4..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/FieldNaming.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor - -@ExperimentalSerializationApi -class FieldNaming(private val name: String, annotations: List) { - private val extractor = AnnotationExtractor(annotations) - - companion object { - operator fun invoke( - desc: SerialDescriptor, - index: Int, - ): FieldNaming = - FieldNaming( - desc.getElementName(index), - desc.getElementAnnotations(index) - ) - } - - /** - * Returns the avro name for the current element. - * Takes into account @AvroName. - */ - fun name(): String = extractor.name() ?: name - - /** - * Returns the avro aliases for the current element. - * Takes into account @AvroAlias. - */ - fun aliases(): List = extractor.aliases() -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/RecordNaming.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/RecordNaming.kt deleted file mode 100644 index d3e4b2c7..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/RecordNaming.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.avrokotlin.avro4k - -import com.github.avrokotlin.avro4k.schema.DefaultNamingStrategy -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor - -@ExperimentalSerializationApi -data class RecordNaming internal constructor( - /** - * The record name for this type to be used when creating - * an avro record. This method takes into account type parameters and - * annotations. - * - * The general format for a record name is `resolved-name__typea_typeb_typec`. - * That is a double underscore delimits the resolved name from the start of the - * type parameters and then each type parameter is delimited by a single underscore. - * - * The resolved name is the class name with any annotations applied, such - * as @AvroName or @AvroNamespace, or @AvroErasedName, which, if present, - * means the type parameters will not be included in the final name. - */ - val name: String, - /** - * The namespace for this type to be used when creating - * an avro record. This method takes into account @AvroNamespace. - */ - val namespace: String, -) { - companion object { - operator fun invoke( - name: String, - annotations: List, - namingStrategy: NamingStrategy, - ): RecordNaming { - val className = - name - .replace(".", "") - .replace(".", "") - val annotationExtractor = AnnotationExtractor(annotations) - val namespace = annotationExtractor.namespace() ?: className.split('.').dropLast(1).joinToString(".") - val avroName = annotationExtractor.name() ?: className.split('.').last() - return RecordNaming( - name = namingStrategy.to(avroName), - namespace = namespace - ) - } - - operator fun invoke( - descriptor: SerialDescriptor, - namingStrategy: NamingStrategy, - ): RecordNaming = - RecordNaming( - if (descriptor.isNullable) descriptor.serialName.removeSuffix("?") else descriptor.serialName, - descriptor.annotations, - namingStrategy - ) - - operator fun invoke( - descriptor: SerialDescriptor, - index: Int, - namingStrategy: NamingStrategy, - ): RecordNaming = - RecordNaming( - descriptor.getElementName(index), - descriptor.getElementAnnotations(index), - namingStrategy - ) - - operator fun invoke( - name: String, - annotations: List, - ): RecordNaming = invoke(name, annotations, DefaultNamingStrategy) - - operator fun invoke(descriptor: SerialDescriptor): RecordNaming = invoke(descriptor, DefaultNamingStrategy) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt index 18b195b3..d0513187 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt @@ -10,6 +10,14 @@ import org.apache.avro.Schema import org.apache.avro.SchemaBuilder import org.intellij.lang.annotations.Language + +/** + * When annotated on a property, overrides the namespace for the nested record. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +annotation class AvroNamespaceOverride(val value: String) + @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class AvroProp(val key: String, val value: String) @@ -21,14 +29,6 @@ annotation class AvroJsonProp( @Language("JSON") val jsonValue: String, ) -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroNamespace(val value: String) - -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroName(val value: String) - @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class ScalePrecision(val scale: Int = 2, val precision: Int = 8) @@ -55,35 +55,29 @@ annotation class AvroUuidLogicalType @Target(AnnotationTarget.PROPERTY) annotation class AvroTimeLogicalType(val type: LogicalTimeTypeEnum) -enum class LogicalTimeTypeEnum(val logicalTypeName: String, val kind: PrimitiveKind, val schemaFor: () -> Schema) { - DATE("date", PrimitiveKind.INT, { LogicalTypes.date().addToSchema(SchemaBuilder.builder().intType()) }), +enum class LogicalTimeTypeEnum(val kind: PrimitiveKind, val schemaFor: () -> Schema) { + DATE(PrimitiveKind.INT, { LogicalTypes.date().addToSchema(SchemaBuilder.builder().intType()) }), TIME_MILLIS( - "time-millis", PrimitiveKind.INT, { LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType()) } ), TIME_MICROS( - "time-micros", PrimitiveKind.LONG, { LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType()) } ), TIMESTAMP_MILLIS( - "timestamp-millis", PrimitiveKind.LONG, { LogicalTypes.timestampMillis().addToSchema(SchemaBuilder.builder().longType()) } ), TIMESTAMP_MICROS( - "timestamp-micros", PrimitiveKind.LONG, { LogicalTypes.timestampMicros().addToSchema(SchemaBuilder.builder().longType()) } ), LOCAL_TIMESTAMP_MILLIS( - "local-timestamp-millis", PrimitiveKind.LONG, { LogicalTypes.localTimestampMillis().addToSchema(SchemaBuilder.builder().longType()) } ), LOCAL_TIMESTAMP_MICROS( - "local-timestamp-micros", PrimitiveKind.LONG, { LogicalTypes.localTimestampMicros().addToSchema(SchemaBuilder.builder().longType()) } ), @@ -97,18 +91,14 @@ annotation class AvroInline @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class AvroDoc(val value: String) +/** + * Adds aliases to a field of a record. It helps to allow having different names for the same field for better compatibility when changing a schema. + * @param value The aliases for the annotated property. Note that the given aliases won't be changed by the configured [AvroConfiguration.fieldNamingStrategy]. + */ @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class AvroAlias(vararg val value: String) -@SerialInfo -@Deprecated( - message = "Will be removed in the next major release", - replaceWith = ReplaceWith("@AvroAlias(alias1, alias2)") -) -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroAliases(val value: Array) - /** * [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 diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt index f41a7518..76396bb1 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt @@ -2,7 +2,6 @@ package com.github.avrokotlin.avro4k.decoder import com.github.avrokotlin.avro4k.AnnotationExtractor import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.FieldNaming import com.github.avrokotlin.avro4k.schema.extractNonNull import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -91,7 +90,7 @@ class RecordDecoder( return record.get(resolvedFieldName()) } - FieldNaming(desc, currentIndex).aliases().forEach { + AnnotationExtractor(desc.getElementAnnotations(currentIndex)).aliases().forEach { if (record.hasField(it)) { return record.get(it) } @@ -100,7 +99,7 @@ class RecordDecoder( return null } - private fun resolvedFieldName(): String = configuration.namingStrategy.to(FieldNaming(desc, currentIndex).name()) + private fun resolvedFieldName(): String = configuration.fieldNamingStrategy.resolve(desc, currentIndex, desc.getElementName(currentIndex)) private fun field(): Schema.Field = record.schema.getField(resolvedFieldName()) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt index a58a817b..a8bdcb7d 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt @@ -1,8 +1,8 @@ package com.github.avrokotlin.avro4k.decoder import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.RecordNaming import com.github.avrokotlin.avro4k.possibleSerializationSubclasses +import com.github.avrokotlin.avro4k.schema.RecordName import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -33,8 +33,8 @@ class UnionDecoder( private var leafDescriptor: SerialDescriptor = descriptor.possibleSerializationSubclasses(serializersModule).firstOrNull { - val schemaName = RecordNaming(value.schema.fullName, emptyList()) - val serialName = RecordNaming(it) + val schemaName = RecordName(name = value.schema.name, namespace = value.schema.namespace) + val serialName = configuration.recordNamingStrategy.resolve(it, it.serialName) serialName == schemaName } ?: throw SerializationException("Cannot find a subtype of ${descriptor.serialName} that can be used to deserialize a record of schema ${value.schema}.") diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt index 7228d3a4..3cac4e51 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt @@ -1,5 +1,6 @@ package com.github.avrokotlin.avro4k.encoder +import com.github.avrokotlin.avro4k.AvroConfiguration import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.AbstractEncoder @@ -14,6 +15,7 @@ import java.nio.ByteBuffer class ListEncoder( private val schema: Schema, override val serializersModule: SerializersModule, + override val configuration: AvroConfiguration, private val callback: (GenericData.Array) -> Unit, ) : AbstractEncoder(), StructureEncoder { private val list = mutableListOf() diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt index 57d2229d..5e54b3dd 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt @@ -1,5 +1,6 @@ package com.github.avrokotlin.avro4k.encoder +import com.github.avrokotlin.avro4k.AvroConfiguration import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor @@ -15,6 +16,7 @@ import java.nio.ByteBuffer class MapEncoder( schema: Schema, override val serializersModule: SerializersModule, + override val configuration: AvroConfiguration, private val callback: (Map) -> Unit, ) : AbstractEncoder(), CompositeEncoder, diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt index 9ba188cd..6ef9886c 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt @@ -1,6 +1,7 @@ package com.github.avrokotlin.avro4k.encoder import com.github.avrokotlin.avro4k.AnnotationExtractor +import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.ListRecord import com.github.avrokotlin.avro4k.Record import com.github.avrokotlin.avro4k.schema.extractNonNull @@ -19,17 +20,19 @@ import java.nio.ByteBuffer @ExperimentalSerializationApi interface StructureEncoder : FieldEncoder { + val configuration: AvroConfiguration + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { return when (descriptor.kind) { StructureKind.LIST -> { when (descriptor.getElementDescriptor(0).kind) { PrimitiveKind.BYTE -> ByteArrayEncoder(fieldSchema(), serializersModule) { addValue(it) } - else -> ListEncoder(fieldSchema(), serializersModule) { addValue(it) } + else -> ListEncoder(fieldSchema(), serializersModule, configuration) { addValue(it) } } } - StructureKind.CLASS -> RecordEncoder(fieldSchema(), serializersModule) { addValue(it) } - StructureKind.MAP -> MapEncoder(fieldSchema(), serializersModule) { addValue(it) } - is PolymorphicKind -> UnionEncoder(fieldSchema(), serializersModule) { addValue(it) } + StructureKind.CLASS -> RecordEncoder(fieldSchema(), serializersModule, configuration) { addValue(it) } + StructureKind.MAP -> MapEncoder(fieldSchema(), serializersModule, configuration) { addValue(it) } + is PolymorphicKind -> UnionEncoder(fieldSchema(), serializersModule, configuration) { addValue(it) } else -> throw SerializationException(".beginStructure was called on a non-structure type [$descriptor]") } } @@ -39,6 +42,7 @@ interface StructureEncoder : FieldEncoder { class RecordEncoder( private val schema: Schema, override val serializersModule: SerializersModule, + override val configuration: AvroConfiguration, val callback: (Record) -> Unit, ) : AbstractEncoder(), StructureEncoder { private val builder = RecordBuilder(schema) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt index 8dded399..05600355 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt @@ -1,5 +1,6 @@ package com.github.avrokotlin.avro4k.encoder +import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.Record import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -15,12 +16,13 @@ import org.apache.avro.Schema class RootRecordEncoder( private val schema: Schema, override val serializersModule: SerializersModule, + private val configuration: AvroConfiguration, private val callback: (Record) -> Unit, ) : AbstractEncoder() { override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { return when (descriptor.kind) { - is StructureKind.CLASS -> RecordEncoder(schema, serializersModule, callback) - is PolymorphicKind -> UnionEncoder(schema, serializersModule, callback) + is StructureKind.CLASS -> RecordEncoder(schema, serializersModule, configuration, callback) + is PolymorphicKind -> UnionEncoder(schema, serializersModule, configuration, callback) else -> throw SerializationException("Unsupported root element passed to root record encoder") } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt index 3681d9b5..4397451e 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt @@ -1,8 +1,8 @@ package com.github.avrokotlin.avro4k.encoder +import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.Record -import com.github.avrokotlin.avro4k.RecordNaming -import com.github.avrokotlin.avro4k.schema.DefaultNamingStrategy +import com.github.avrokotlin.avro4k.schema.RecordName import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor @@ -16,6 +16,7 @@ import org.apache.avro.Schema class UnionEncoder( private val unionSchema: Schema, override val serializersModule: SerializersModule, + private val configuration: AvroConfiguration, private val callback: (Record) -> Unit, ) : AbstractEncoder() { override fun encodeString(value: String) { @@ -28,11 +29,11 @@ class UnionEncoder( // Hand in the concrete schema for the specified SerialDescriptor so that fields can be correctly decoded. val leafSchema = unionSchema.types.first { - val schemaName = RecordNaming(it.fullName, emptyList(), DefaultNamingStrategy) - val serialName = RecordNaming(descriptor, DefaultNamingStrategy) + val schemaName = RecordName(name = it.name, namespace = it.namespace) + val serialName = configuration.recordNamingStrategy.resolve(descriptor, descriptor.serialName) serialName == schemaName } - RecordEncoder(leafSchema, serializersModule, callback) + RecordEncoder(leafSchema, serializersModule, configuration, callback) } else -> throw SerializationException("Unsupported root element passed to root record encoder") } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt index b5a05b84..561d7a78 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt @@ -2,10 +2,11 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AnnotationExtractor import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAlias import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.AvroJsonProp +import com.github.avrokotlin.avro4k.AvroNamespaceOverride import com.github.avrokotlin.avro4k.AvroProp -import com.github.avrokotlin.avro4k.RecordNaming import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.json.Json @@ -26,10 +27,10 @@ class ClassSchemaFor( private val descriptor: SerialDescriptor, private val configuration: AvroConfiguration, private val serializersModule: SerializersModule, - private val resolvedSchemas: MutableMap, + private val resolvedSchemas: MutableMap, ) : SchemaFor { private val entityAnnotations = AnnotationExtractor(descriptor.annotations) - private val naming = RecordNaming(descriptor, DefaultNamingStrategy) + private val naming = configuration.recordNamingStrategy.resolve(descriptor, descriptor.serialName) private val json by lazy { Json { serializersModule = this@ClassSchemaFor.serializersModule @@ -73,91 +74,63 @@ class ClassSchemaFor( } private fun buildField(index: Int): Schema.Field { - val fieldDescriptor = descriptor.getElementDescriptor(index) - val annos = - AnnotationExtractor( - descriptor.getElementAnnotations( - index - ) - ) - val fieldNaming = RecordNaming(descriptor, index, configuration.namingStrategy) - val schema = - schemaFor( - serializersModule, - fieldDescriptor, - descriptor.getElementAnnotations(index), - configuration, - resolvedSchemas - ).schema() - - // if we have annotated the field @AvroFixed then we override the type and change it to a Fixed schema - // if someone puts @AvroFixed on a complex type, it makes no sense, but that's their cross to bear - // in addition, someone could annotate the target type, so we need to check into that too - val (size, name) = - when (val a = annos.fixed()) { - null -> { - val fieldAnnos = AnnotationExtractor(fieldDescriptor.annotations) - val n = RecordNaming(fieldDescriptor, configuration.namingStrategy) - when (val b = fieldAnnos.fixed()) { - null -> 0 to n.name - else -> b to n.name - } - } - else -> a to fieldNaming.name - } - - val schemaOrFixed = - when (size) { - 0 -> schema - else -> - SchemaBuilder.fixed(name) - .doc(annos.doc()) - .namespace(annos.namespace() ?: naming.namespace) - .size(size) - } - - // the field can override the containingNamespace if the Namespace annotation is present on the field - // we may have annotated our field with @AvroNamespace so this containingNamespace should be applied - // to any schemas we have generated for this field - val schemaWithResolvedNamespace = - when (val ns = annos.namespace()) { - null -> schemaOrFixed - else -> schemaOrFixed.overrideNamespace(ns) - } - - val default: Any? = getDefaultValue(annos, schemaWithResolvedNamespace, fieldDescriptor) - - val field = Schema.Field(fieldNaming.name, schemaWithResolvedNamespace, annos.doc(), default) - this.descriptor.getElementAnnotations(index) - .filterIsInstance() - .forEach { field.addProp(it.key, it.value) } - this.descriptor.getElementAnnotations(index) - .filterIsInstance() - .forEach { field.addProp(it.key, json.parseToJsonElement(it.jsonValue).convertToAvroDefault()) } - annos.aliases().forEach { field.addAlias(it) } - + val fieldTypeDescriptor = descriptor.getElementDescriptor(index) + val annos = AnnotationExtractor(descriptor.getElementAnnotations(index)) + val fieldSpecificNamespace: String? = descriptor.getElementAnnotations(index).filterIsInstance().firstOrNull()?.value + val fieldName = configuration.fieldNamingStrategy.resolve(descriptor, index, descriptor.getElementName(index)) + val schema = getFixedSchema(fieldName, annos) ?: schemaFor( + serializersModule, + fieldTypeDescriptor, + descriptor.getElementAnnotations(index), + configuration, + resolvedSchemas + ).schema() + + // If the field is annotated with a specific namespace, then we need to override the namespace of the field's schema + val schemaWithResolvedNamespace = fieldSpecificNamespace?.let { schema.overrideNamespace(it) } ?: schema + + val default: Any? = getDefaultValue(annos, schemaWithResolvedNamespace, fieldTypeDescriptor) + + val field = Schema.Field(fieldName, schemaWithResolvedNamespace, annos.doc(), default) + field.mutateFieldFromAnnotations(this.descriptor.getElementAnnotations(index)) return field } + private fun getFixedSchema(fieldName: String, annos: AnnotationExtractor): Schema? { + val size = annos.fixed() ?: return null + return SchemaBuilder.fixed(fieldName) + .doc(annos.doc()) + .namespace(naming.namespace) + .size(size) + } + + private fun Schema.Field.mutateFieldFromAnnotations(annotations: List) = annotations.forEach { + when (it) { + is AvroProp -> this.addProp(it.key, it.value) + is AvroJsonProp -> this.addProp(it.key, json.parseToJsonElement(it.jsonValue).convertToAvroDefault()) + is AvroAlias -> it.value.forEach { this.addAlias(it) } + } + } + private fun getDefaultValue( annos: AnnotationExtractor, schemaWithResolvedNamespace: Schema, - fieldDescriptor: SerialDescriptor, + fieldTypeDescriptor: SerialDescriptor, ) = annos.default()?.let { annotationDefaultValue -> when { annotationDefaultValue == Avro.NULL -> Schema.Field.NULL_DEFAULT_VALUE schemaWithResolvedNamespace.extractNonNull().type in - listOf( - Schema.Type.FIXED, - Schema.Type.BYTES, - Schema.Type.STRING, - Schema.Type.ENUM - ) + listOf( + Schema.Type.FIXED, + Schema.Type.BYTES, + Schema.Type.STRING, + Schema.Type.ENUM + ) -> annotationDefaultValue else -> json.parseToJsonElement(annotationDefaultValue).convertToAvroDefault() } - } ?: if (configuration.implicitNulls && fieldDescriptor.isNullable) { + } ?: if (configuration.implicitNulls && fieldTypeDescriptor.isNullable) { Schema.Field.NULL_DEFAULT_VALUE } else { null @@ -183,4 +156,4 @@ class ClassSchemaFor( } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt index 7f62a39d..58a8870c 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt @@ -4,10 +4,10 @@ import com.github.avrokotlin.avro4k.AnnotationExtractor import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.AvroDecimalLogicalType +import com.github.avrokotlin.avro4k.AvroFixed import com.github.avrokotlin.avro4k.AvroTimeLogicalType import com.github.avrokotlin.avro4k.AvroUuidLogicalType import com.github.avrokotlin.avro4k.LogicalDecimalTypeEnum -import com.github.avrokotlin.avro4k.RecordNaming import com.github.avrokotlin.avro4k.ScalePrecision import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi @@ -52,9 +52,10 @@ interface SchemaFor { @ExperimentalSerializationApi class EnumSchemaFor( private val descriptor: SerialDescriptor, + private val configuration: AvroConfiguration, ) : SchemaFor { override fun schema(): Schema { - val naming = RecordNaming(descriptor, DefaultNamingStrategy) + val naming = configuration.recordNamingStrategy.resolve(descriptor, descriptor.serialName) val entityAnnotations = AnnotationExtractor(descriptor.annotations) val symbols = (0 until descriptor.elementsCount).map { descriptor.getElementName(it) } @@ -82,7 +83,7 @@ class ListSchemaFor( private val descriptor: SerialDescriptor, private val serializersModule: SerializersModule, private val configuration: AvroConfiguration, - private val resolvedSchemas: MutableMap, + private val resolvedSchemas: MutableMap, ) : SchemaFor { override fun schema(): Schema { val elementType = descriptor.getElementDescriptor(0) // don't use unwrapValueClass to prevent losing serial annotations @@ -108,7 +109,7 @@ class MapSchemaFor( private val descriptor: SerialDescriptor, private val serializersModule: SerializersModule, private val configuration: AvroConfiguration, - private val resolvedSchemas: MutableMap, + private val resolvedSchemas: MutableMap, ) : SchemaFor { override fun schema(): Schema { val keyType = descriptor.getElementDescriptor(0).unwrapValueClass @@ -158,10 +159,10 @@ fun schemaFor( descriptor: SerialDescriptor, annos: List, configuration: AvroConfiguration, - resolvedSchemas: MutableMap, + resolvedSchemas: MutableMap, ): SchemaFor { - val schemaFor: SchemaFor = - schemaForLogicalTypes(descriptor, annos) ?: when (descriptor.unwrapValueClass.kind) { + val schemaFor: SchemaFor = schemaForLogicalTypes(descriptor, annos, configuration)?.let(SchemaFor::const) + ?: when (descriptor.unwrapValueClass.kind) { PrimitiveKind.STRING -> SchemaFor.StringSchemaFor PrimitiveKind.LONG -> SchemaFor.LongSchemaFor PrimitiveKind.INT -> SchemaFor.IntSchemaFor @@ -170,7 +171,7 @@ fun schemaFor( PrimitiveKind.DOUBLE -> SchemaFor.DoubleSchemaFor PrimitiveKind.FLOAT -> SchemaFor.FloatSchemaFor PrimitiveKind.BOOLEAN -> SchemaFor.BooleanSchemaFor - SerialKind.ENUM -> EnumSchemaFor(descriptor) + SerialKind.ENUM -> EnumSchemaFor(descriptor, configuration) SerialKind.CONTEXTUAL -> schemaFor( serializersModule, @@ -199,7 +200,8 @@ fun schemaFor( private fun schemaForLogicalTypes( descriptor: SerialDescriptor, annos: List, -): SchemaFor? { + configuration: AvroConfiguration, +): Schema? { val annotations = annos + descriptor.annotations + (if (descriptor.isInline) descriptor.unwrapValueClass.annotations else emptyList()) @@ -210,26 +212,35 @@ private fun schemaForLogicalTypes( when (decimalLogicalType.schema) { LogicalDecimalTypeEnum.BYTES -> SchemaBuilder.builder().bytesType() LogicalDecimalTypeEnum.STRING -> SchemaBuilder.builder().stringType() - LogicalDecimalTypeEnum.FIXED -> TODO() + LogicalDecimalTypeEnum.FIXED -> { + val fixedSize = annotations.filterIsInstance().firstOrNull()?.size + ?: throw UnsupportedOperationException("Fixed size must be specified for FIXED decimal type with @AvroFixed annotation") + createFixedSchema(descriptor, fixedSize, configuration) + } } - return object : SchemaFor { - override fun schema() = LogicalTypes.decimal(scaleAndPrecision.precision, scaleAndPrecision.scale).addToSchema(schema) - } + return LogicalTypes.decimal(scaleAndPrecision.precision, scaleAndPrecision.scale).addToSchema(schema) } if (annotations.any { it is AvroUuidLogicalType }) { - return object : SchemaFor { - override fun schema() = LogicalTypes.uuid().addToSchema(SchemaBuilder.builder().stringType()) - } + return LogicalTypes.uuid().addToSchema(SchemaBuilder.builder().stringType()) } if (annotations.any { it is AvroTimeLogicalType }) { val timeLogicalType = annotations.filterIsInstance().first() - return object : SchemaFor { - override fun schema() = timeLogicalType.type.schemaFor() - } + return timeLogicalType.type.schemaFor() + } + if (annotations.any { it is AvroFixed }) { + val fixedSize = annotations.filterIsInstance().first().size + return createFixedSchema(descriptor, fixedSize, configuration) } return null } +@OptIn(ExperimentalSerializationApi::class) +private fun createFixedSchema(descriptor: SerialDescriptor, fixedSize: Int, configuration: AvroConfiguration): Schema { + return configuration.recordNamingStrategy.resolve(descriptor, descriptor.serialName).let { + SchemaBuilder.fixed(it.name).namespace(it.namespace).size(fixedSize) + } +} + // copy-paste from kotlinx serialization because it internal @ExperimentalSerializationApi internal val SerialDescriptor.unwrapValueClass: SerialDescriptor diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt index 8131469f..832e723c 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt @@ -1,7 +1,6 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.RecordNaming import com.github.avrokotlin.avro4k.possibleSerializationSubclasses import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor @@ -13,7 +12,7 @@ class UnionSchemaFor( private val descriptor: SerialDescriptor, private val configuration: AvroConfiguration, private val serializersModule: SerializersModule, - private val resolvedSchemas: MutableMap, + private val resolvedSchemas: MutableMap, ) : SchemaFor { override fun schema(): Schema { val leafSerialDescriptors = diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt index fa851eb6..19ca8f72 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt @@ -1,24 +1,70 @@ package com.github.avrokotlin.avro4k.schema -interface NamingStrategy { - fun to(name: String): String = name -} +import kotlinx.serialization.descriptors.SerialDescriptor -object DefaultNamingStrategy : NamingStrategy { - override fun to(name: String): String = name -} +interface RecordNamingStrategy { + fun resolve(descriptor: SerialDescriptor, serialName: String): RecordName -object PascalCaseNamingStrategy : NamingStrategy { - override fun to(name: String): String = name.take(1).uppercase() + name.drop(1) + companion object Builtins { + val Default = object : RecordNamingStrategy { + override fun resolve(descriptor: SerialDescriptor, serialName: String): RecordName { + val lastDot = serialName.lastIndexOf('.').takeIf { it >= 0 && it + 1 < serialName.length } + val lastIndex = if (serialName.endsWith('?')) serialName.length - 1 else serialName.length + return RecordName( + name = lastDot?.let { serialName.substring(lastDot + 1, lastIndex) } ?: serialName, + namespace = lastDot?.let { serialName.substring(0, lastDot) }?.takeIf { it.isNotEmpty() }, + ) + } + } + } } -object SnakeCaseNamingStrategy : NamingStrategy { - override fun to(name: String): String = - name.fold(StringBuilder()) { sb, c -> - if (c.isUpperCase()) { - sb.append('_').append(c.lowercase()) - } else { - sb.append(c.lowercase()) +data class RecordName(val name: String, val namespace: String?) + +interface FieldNamingStrategy { + fun resolve(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String + + companion object Builtins { + val Default: FieldNamingStrategy = object : FieldNamingStrategy { + override fun resolve(descriptor: SerialDescriptor, elementIndex: Int, serialName: String) = serialName + } + val SnakeCase: FieldNamingStrategy = object : FieldNamingStrategy { + override fun resolve(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String = buildString(serialName.length * 2) { + var bufferedChar: Char? = null + var previousUpperCharsCount = 0 + + serialName.forEach { c -> + if (c.isUpperCase()) { + if (previousUpperCharsCount == 0 && isNotEmpty() && last() != '_') + append('_') + + bufferedChar?.let(::append) + + previousUpperCharsCount++ + bufferedChar = c.lowercaseChar() + } else { + if (bufferedChar != null) { + if (previousUpperCharsCount > 1 && c.isLetter()) { + append('_') + } + append(bufferedChar) + previousUpperCharsCount = 0 + bufferedChar = null + } + append(c) + } + } + + if (bufferedChar != null) { + append(bufferedChar) + } } - }.toString() -} \ No newline at end of file + } + val PascalCase: FieldNamingStrategy = object : FieldNamingStrategy { + override fun resolve(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String = serialName.replaceFirstChar { it.uppercaseChar() } + } + val CamelCase: FieldNamingStrategy = object : FieldNamingStrategy { + override fun resolve(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String = serialName.replaceFirstChar { it.lowercaseChar() } + } + } +} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt index 8175dfe4..5949a351 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt @@ -3,7 +3,6 @@ package com.github.avrokotlin.avro4k.decoder import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroDefault import com.github.avrokotlin.avro4k.AvroEnumDefault -import com.github.avrokotlin.avro4k.AvroName import com.github.avrokotlin.avro4k.ScalePrecision import com.github.avrokotlin.avro4k.io.AvroDecodeFormat import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer @@ -12,6 +11,7 @@ import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToByteArray import org.apache.avro.generic.GenericData @@ -23,13 +23,13 @@ data class FooElement( ) @Serializable -@AvroName("container") +@SerialName("container") data class ContainerWithoutDefaultFields( val name: String, ) @Serializable -@AvroName("container") +@SerialName("container") data class ContainerWithDefaultFields( val name: String, @AvroDefault("hello") diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt index 31054557..40e7e1a8 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt @@ -1,10 +1,10 @@ package com.github.avrokotlin.avro4k.endecode -import com.github.avrokotlin.avro4k.AvroName import com.github.avrokotlin.avro4k.record import io.kotest.core.factory.TestFactory import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.stringSpec +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable class AvroNameEncoderTest : FunSpec({ @@ -16,7 +16,7 @@ fun avroNameEncodingTests(endecoder: EnDecoder): TestFactory { "take into account @AvroName on fields" { @Serializable data class Foo( - @AvroName("bar") val foo: String, + @SerialName("bar") val foo: String, ) endecoder.testEncodeDecode(Foo("hello"), record("hello")) } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt index 81003838..04539d93 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt @@ -3,8 +3,7 @@ package com.github.avrokotlin.avro4k.endecode import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.schema.PascalCaseNamingStrategy -import com.github.avrokotlin.avro4k.schema.SnakeCaseNamingStrategy +import com.github.avrokotlin.avro4k.schema.FieldNamingStrategy import io.kotest.core.factory.TestFactory import io.kotest.core.spec.style.WordSpec import io.kotest.core.spec.style.stringSpec @@ -21,14 +20,14 @@ fun namingStrategyEncoderTests(enDecoder: EnDecoder): TestFactory { data class Foo(val fooBar: String) "encode/decode fields with snake_casing" { - enDecoder.avro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) + enDecoder.avro = Avro(AvroConfiguration(fieldNamingStrategy = FieldNamingStrategy.SnakeCase)) val schema = enDecoder.avro.schema(Foo.serializer()) schema.getField("foo_bar") shouldNotBe null enDecoder.testEncodeDecode(Foo("hello"), record("hello")) } "encode/decode fields with PascalCasing" { - enDecoder.avro = Avro(AvroConfiguration(PascalCaseNamingStrategy)) + enDecoder.avro = Avro(AvroConfiguration(fieldNamingStrategy = FieldNamingStrategy.PascalCase)) val schema = enDecoder.avro.schema(Foo.serializer()) schema.getField("FooBar") shouldNotBe null enDecoder.testEncodeDecode(Foo("hello"), record("hello")) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt index fbcee5b7..eafece76 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt @@ -1,9 +1,9 @@ package com.github.avrokotlin.avro4k.io import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.apache.avro.Schema import org.apache.avro.SchemaBuilder @@ -67,7 +67,7 @@ class AvroNameIoTest : StringSpec({ }) { @Serializable data class Composer( - @AvroName("fullname") val name: String, + @SerialName("fullname") val name: String, val status: String, ) } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt index 7eea5b62..3dc48fca 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt @@ -2,7 +2,7 @@ package com.github.avrokotlin.avro4k.io import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.schema.SnakeCaseNamingStrategy +import com.github.avrokotlin.avro4k.schema.FieldNamingStrategy import io.kotest.core.spec.style.StringSpec import io.kotest.engine.spec.tempfile import io.kotest.matchers.shouldBe @@ -16,7 +16,7 @@ import java.nio.file.Files class NamingStrategyIoTest : StringSpec({ - val snakeCaseAvro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) + val snakeCaseAvro = Avro(AvroConfiguration(fieldNamingStrategy = FieldNamingStrategy.SnakeCase)) "using snake_case namingStrategy to write out a record" { val ennio = Composer("Ennio Morricone", "Maestro") diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt index dd12ca7a..399b4bb8 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt @@ -1,9 +1,9 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable class AvroNameSchemaTest : FunSpec({ @@ -24,11 +24,11 @@ class AvroNameSchemaTest : FunSpec({ }) { @Serializable data class FieldNamesFoo( - @AvroName("foo") val wibble: String, + @SerialName("foo") val wibble: String, val wobble: String, ) - @AvroName("wibble") + @SerialName("com.github.avrokotlin.avro4k.schema.AvroNameSchemaTest.wibble") @Serializable data class ClassNameFoo(val a: String, val b: String) } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceTest.kt index 5ce20389..a568cfdf 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceTest.kt @@ -1,10 +1,10 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName -import com.github.avrokotlin.avro4k.AvroNamespace +import com.github.avrokotlin.avro4k.AvroNamespaceOverride import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable class AvroNamespaceSchemaTest : FunSpec({ @@ -43,45 +43,41 @@ class AvroNamespaceSchemaTest : FunSpec({ schema.toString(true) shouldBe expected.toString(true) } }) { - @AvroNamespace("com.yuval") + @SerialName("com.yuval.AnnotatedNamespace") @Serializable data class AnnotatedNamespace(val s: String) - @AvroNamespace("com.yuval.internal") + @SerialName("com.yuval.internal.InternalAnnotated") @Serializable data class InternalAnnotated(val i: Int) - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") + @SerialName("com.yuval.AnnotatedNamespace") @Serializable data class NestedAnnotatedNamespace(val s: String, val internal: InternalAnnotated) @Serializable - @AvroName("InternalAnnotated") + @SerialName("InternalAnnotated") data class Internal(val i: Int) @Serializable - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") + @SerialName("com.yuval.AnnotatedNamespace") data class InternalAnnotatedNamespace( val s: String, - @AvroNamespace("com.yuval.internal") val internal: Internal, + @AvroNamespaceOverride("com.yuval.internal") val internal: Internal, ) @Serializable - @AvroName("InternalAnnotated") - @AvroNamespace("ignore") + @SerialName("shouldbeignored.InternalAnnotated") data class InternalIgnoreAnnotated(val i: Int) @Serializable - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") + @SerialName("com.yuval.AnnotatedNamespace") data class FieldAnnotatedNamespace( val s: String, - @AvroNamespace("com.yuval.internal") val internal: InternalIgnoreAnnotated, + @AvroNamespaceOverride("com.yuval.internal") val internal: InternalIgnoreAnnotated, ) - @AvroNamespace("") + @SerialName("Foo") @Serializable data class Foo(val s: String) } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ByteArraySchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ByteArraySchemaTest.kt index ffa6a1fc..ac04fae1 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ByteArraySchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ByteArraySchemaTest.kt @@ -1,9 +1,9 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable class ByteArraySchemaTest : FunSpec({ @@ -45,9 +45,10 @@ class ByteArraySchemaTest : FunSpec({ // } }) { @Serializable + @SerialName("ByteArrayTest") data class ByteArrayTest(val z: ByteArray) @Serializable - @AvroName("ByteArrayTest") + @SerialName("ByteArrayTest") data class ByteListTest(val z: List) } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt index c8a7a7bd..2306b8d2 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable class NamingStrategySchemaTest : WordSpec({ "NamingStrategy" should { "convert schema with snake_case to camelCase" { - val snakeCaseAvro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) + val snakeCaseAvro = Avro(AvroConfiguration(fieldNamingStrategy = FieldNamingStrategy.SnakeCase)) val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/snake_case_schema.json")) @@ -19,7 +19,7 @@ class NamingStrategySchemaTest : WordSpec({ } "convert schema with PascalCase to camelCase" { - val pascalCaseAvro = Avro(AvroConfiguration(PascalCaseNamingStrategy)) + val pascalCaseAvro = Avro(AvroConfiguration(fieldNamingStrategy = FieldNamingStrategy.PascalCase)) val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/pascal_case_schema.json")) diff --git a/src/test/resources/byte_array.json b/src/test/resources/byte_array.json index 15e80c9d..cc70a892 100644 --- a/src/test/resources/byte_array.json +++ b/src/test/resources/byte_array.json @@ -1,7 +1,6 @@ { "type": "record", "name": "ByteArrayTest", - "namespace": "com.github.avrokotlin.avro4k.schema.ByteArraySchemaTest", "fields": [ { "name": "z",