diff --git a/.editorconfig b/.editorconfig index 5ad239a3..d24e3874 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,147 @@ -root = true - [*] -indent_style = space -indent_size = 3 +charset = utf-8 end_of_line = lf +indent_size = 4 +indent_style = space insert_final_newline = false -max_line_length = 120 \ No newline at end of file +max_line_length = 180 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_wrap_on_typing = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.kt,*.kts}] +ktlint_standard_filename = disabled +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[*.json] +indent_size = 2 + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3e021202 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 880a87bd..22fa2034 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,47 +1,47 @@ name: build-main on: - push: - branches: - - main - - release/* - paths-ignore: - - 'doc/**' - - '*.md' + push: + branches: + - main + - release/* + paths-ignore: + - 'doc/**' + - '*.md' jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v3 - - - name: Run tests - run: ./gradlew check - - - name: Bundle the build report - if: failure() - run: find . -type d -name 'reports' | zip -@ -r build-reports.zip - - - name: Upload the build report - if: failure() - uses: actions/upload-artifact@v3 - with: - name: error-report - path: build-reports.zip - - deploy: - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v3 - - - name: deploy to sonatype snapshots - run: ./gradlew publish - env: - ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} - ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} - -env: - GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Run tests + run: ./gradlew check + + - name: Bundle the build report + if: failure() + run: find . -type d -name 'reports' | zip -@ -r build-reports.zip + - name: Upload the build report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: error-report + path: build-reports.zip + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: deploy to sonatype snapshots + run: ./gradlew publish + env: + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index edf75f75..aa4c11f2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,31 +1,30 @@ name: build-pr on: - pull_request: - paths-ignore: - - 'doc/**' - - '*.md' + pull_request: + paths-ignore: + - 'doc/**' + - '*.md' jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v3 + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - - name: Run tests - run: ./gradlew check + - name: Run tests + run: ./gradlew check - - name: Bundle the build report - if: failure() - run: find . -type d -name 'reports' | zip -@ -r build-reports.zip + - name: Bundle the build report + if: failure() + run: find . -type d -name 'reports' | zip -@ -r build-reports.zip - - name: Upload the build report - if: failure() - uses: actions/upload-artifact@v3 - with: - name: error-report - path: build-reports.zip - -env: - GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" + - name: Upload the build report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: error-report + path: build-reports.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c3eaf96..0162b2c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,32 +1,36 @@ name: release on: - workflow_dispatch: - inputs: - version: - description: "The release version (e.g., '1.9.0')." - required: true + workflow_dispatch: + inputs: + version: + description: "The release version (e.g., '1.9.0')." + required: true jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v3 + publish: + runs-on: ubuntu-latest + steps: + - name: Fail if branch is not main + if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/main-v2' + run: | + echo "This workflow should only be triggered on main and main-v2 branch. + exit 1 + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - - name: publish release - run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository - env: - RELEASE_VERSION: ${{ github.event.inputs.version }} - ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} - ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} - - name: create release - run: gh release create "v${RELEASE_VERSION}" --generate-notes - env: - RELEASE_VERSION: ${{ github.event.inputs.version }} - GH_TOKEN: ${{ github.token }} - -env: - GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" + - name: publish release + run: ./gradlew publishToSonatype closeAndReleaseStagingRepositories + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} + - name: create release + run: gh release create "v${RELEASE_VERSION}" --generate-notes --target "${GITHUB_REF}" + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + GH_TOKEN: ${{ github.token }} diff --git a/Migrating-from-v1.md b/Migrating-from-v1.md new file mode 100644 index 00000000..88b4fd34 --- /dev/null +++ b/Migrating-from-v1.md @@ -0,0 +1,246 @@ +Here is the guide of how to migrate from Avro4k v1 to v2 using examples. + +> [!INFO] +> If you are missing a migration need, please [file an issue](https://github.com/avro-kotlin/avro4k/issues/new/choose) or [make a PR](https://github.com/avro-kotlin/avro4k/compare). + +## Pure avro serialization + +```kotlin +// Previously +val bytes = Avro.default.encodeToByteArray(TheDataClass.serializer(), TheDataClass(...)) +Avro.default.decodeFromByteArray(TheDataClass.serializer(), bytes) + +// Now +val bytes = Avro.encodeToByteArray(TheDataClass(...)) +Avro.decodeFromByteArray(bytes) +``` + +## Set a field default value to null + +```kotlin +// Previously +data class TheDataClass( + @AvroDefault(Avro.NULL) + val field: String? +) + +// Now +data class TheDataClass( + // ... Nothing, as it is the default behavior! + val field: String? +) + +// Or +val avro = Avro { implicitNulls = false } +data class TheDataClass( + @AvroDefault("null") + val field: String? +) +``` + +## Set a field default value to empty array + +```kotlin +// Previously +data class TheDataClass( + @AvroDefault("[]") + val field: List +) + +// Now +data class TheDataClass( + // ... Nothing, as it is the default behavior! + val field: List +) + +// Or +val avro = Avro { implicitEmptyCollections = false } +data class TheDataClass( + @AvroDefault("[]") + val field: List +) +``` + +## Set a field default value to empty map + +```kotlin +// Previously +data class TheDataClass( + @AvroDefault("{}") + val field: Map +) + +// Now +data class TheDataClass( + // ... Nothing, as it is the default behavior! + val field: Map +) + +// Or +val avro = Avro { implicitEmptyCollections = false } +data class TheDataClass( + @AvroDefault("{}") + val field: Map +) +``` + +## generic data serialization +Convert a kotlin data class to a `GenericRecord` to then be handled by a `GenericDatumWriter` in avro. + +```kotlin +// Previously +val genericRecord: GenericRecord = Avro.default.toRecord(TheDataClass.serializer(), TheDataClass(...)) +Avro.default.fromRecord(TheDataClass.serializer(), genericRecord) + +// Now +val genericData: Any? = Avro.encodeToGenericData(TheDataClass(...)) +Avro.decodeFromGenericData(genericData) +``` + +## Configure the `Avro` instance + +```kotlin +// Previously +val avro = Avro( + AvroConfiguration( + namingStrategy = FieldNamingStrategy.SnackCase, + implicitNulls = true, + ), + SerializersModule { + contextual(CustomSerializer()) + } +) + +// Now +val avro = Avro { + namingStrategy = FieldNamingStrategy.SnackCase + implicitNulls = true + serializersModule = SerializersModule { + contextual(CustomSerializer()) + } +} +``` + +## Changing the name of a record + +```kotlin +// Previously +@AvroName("TheName") +@AvroNamespace("a.custom.namespace") +data class TheDataClass(...) + +// Now +@SerialName("a.custom.namespace.TheName") +data class TheDataClass(...) +``` + +## Writing an avro object container file with a custom field naming strategy + +```kotlin +// Previously +Files.newOutputStream(Path("/your/file.avro")).use { outputStream -> + Avro(AvroConfiguration(namingStrategy = SnakeCaseNamingStrategy)) + .openOutputStream(TheDataClass.serializer()) { encodeFormat = AvroEncodeFormat.Data(CodecFactory.snappyCodec()) } + .to(outputStream) + .write(TheDataClass(...)) + .write(TheDataClass(...)) + .write(TheDataClass(...)) + .close() +} + + +// Now +val dataSequence = sequenceOf( + TheDataClass(...), + TheDataClass(...), + TheDataClass(...), +) +Files.newOutputStream(Path("/your/file.avro")).use { outputStream -> + AvroObjectContainer { fieldNamingStrategy = FieldNamingStrategy.SnakeCase } + .encodeToStream(dataSequence, outputStream) { + codec(CodecFactory.snappyCodec()) + // you can also add your metadata ! + metadata("myProp", 1234L) + metadata("a string metadata", "hello") + } +} +``` + +## Writing a collection of Byte as BYTES or FIXED + +If you really want to encode a `BYTES` type, just use the `ByteArray` type or write your own `AvroSerializer` to control the schema and its serialization. + +For encoding to `FIXED`, then just use the `ByteArray` type with the `AvroFixed` annotation (or still write your own serializer). + +```kotlin +// Previously +@Serializable +data class TheDataClass( + val collectionOfBytes: List, + val listOfBytes: List, + val setOfBytes: List, +) + +// Now +@Serializable +data class TheDataClass( + val collectionOfBytes: ByteArray, + val listOfBytes: ByteArray, + val setOfBytes: ByteArray, +) +``` + +## Serialize a `BigDecimal` as a string + +> [!INFO] +> Note that you can replace `@Serializable(with = BigDecimalAsStringSerializer::class)` with `@Contextual` to use the default global `BigDecimalSerializer` already registered, +> which is already compatible with the `@AvroStringable` feature. + +```kotlin +// Previously +@Serializable +data class MyData( + @Serializable(with = BigDecimalAsStringSerializer::class) + val bigDecimalAsString: BigDecimal, +) + +// Now +@Serializable +data class MyData( + @Contextual + @AvroStringable + val bigDecimalAsString: BigDecimal, +) +``` + +## Serialize a `BigDecimal` as a BYTES or FIXED + +Previously, a BigDecimal was serialized as bytes with 2 as scale and 8 as precision. Now you have to explicitly declare the needed scale and precision using `@AvroDecimal`, +or use `@AvroStringable` to serialize it as a string which doesn't need scale nor precision. + +> [!INFO] +> Note that you can replace `@Serializable(with = BigDecimalSerializer::class)` with `@Contextual` to use the default global `BigDecimalSerializer` already registered. + +```kotlin +// Previously +@Serializable +data class MyData( + @Serializable(with = BigDecimalSerializer::class) + val bigDecimalAsBytes: BigDecimal, + @AvroFixed(10) + @Serializable(with = BigDecimalSerializer::class) + val bigDecimalAsFixed: BigDecimal, +) + +// Now +@Serializable +data class MyData( + @Contextual + @AvroDecimal(scale = 8, precision = 2) + val bigDecimalAsBytes: BigDecimal, + @Contextual + @AvroFixed(10) + @AvroDecimal(scale = 8, precision = 2) + val bigDecimalAsFixed: BigDecimal, +) +``` diff --git a/README.md b/README.md index 85fc67b6..2528b9f0 100644 --- a/README.md +++ b/README.md @@ -1,627 +1,879 @@ -# -[Avro](https://avro.apache.org/) format for [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization). This library is a port of [sksamuel's](https://github.com/sksamuel) Scala Avro generator [avro4s](https://github.com/sksamuel/avro4s). - ![build-main](https://github.com/avro-kotlin/avro4k/workflows/build-main/badge.svg) -[](http://search.maven.org/#search%7Cga%7C1%7Cavro4k) +[![Download](https://img.shields.io/maven-central/v/com.github.avro-kotlin.avro4k/avro4k-core)](https://search.maven.org/artifact/com.github.avro-kotlin.avro4k/avro4k-core) +[![Kotlin](https://img.shields.io/badge/kotlin-2.0.0-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlinx serialization](https://img.shields.io/badge/kotlinx--serialization-1.7.0-blue?logo=kotlin)](https://github.com/Kotlin/kotlinx.serialization) +[![Avro spec](https://img.shields.io/badge/avro%20spec-1.11.3-blue.svg?logo=apache)](https://avro.apache.org/docs/1.11.3/specification/) + +# Introduction +**Avro4k** (or Avro for Kotlin) is a library that brings [Avro](https://avro.apache.org/) serialization format in kotlin, based on the **reflection-less** kotlin library +called [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization). -## Introduction +Here are the main features: -Avro4k is a Kotlin library that brings support for Avro to the Kotlin Serialization framework. This library supports reading and writing to/from binary and json streams as well as supporting Avro schema generation. +- **Full avro support**, including logical types, unions, recursive types, and schema evolution :white_check_mark: +- **Encode and decode** anything to and from binary format, and also in generic data :toolbox: +- **Generate schemas** based on your values and data classes :pencil: +- **Customize** the generated schemas and encoded data with annotations :construction_worker: +- **Fast** as it is reflection-less :rocket: (check the benchmarks [here](benchmark/README.md#results)) +- **Simple API** to get started quickly, also with native support of java standard classes like `UUID`, `BigDecimal`, `BigInteger` and `java.time` module :1st_place_medal: +- **Relaxed matching** for easy schema evolution as it natively [adapts compatible types](#types-matrix) :cyclone: -- [Generate schemas](https://github.com/avro-kotlin/avro4k#schemas) from Kotlin data classes. This allows you to use data classes as the canonical source for the schemas and generate Avro schemas from _them_, rather than define schemas externally and then generate (or manually write) data classes to match. -- Marshall data classes to / from instances of Avro Generic Records. The basic _structure type_ in Avro is the _IndexedRecord_ or it's more common subclass, the _GenericRecord_. This library will marshall data to and from Avro records. This can be useful for interop with frameworks like Kafka which provide serializers which work at the record level. -- [Read / Write](https://github.com/avro-kotlin/avro4k#input--output) data classes to input or output streams. Avro records can be serialized as binary (with or without embedded schema) or json, and this library provides _AvroInputStream_ and _AvroOutputStream_ classes to support data classes directly. -- [Support logical types](https://github.com/avro-kotlin/avro4k#types). This library provides support for the Avro [logical types](https://avro.apache.org/docs/1.11.0/spec.html#Logical+Types) out of the box, in addition to the _standard_ types supported by the Kotlin serialization framework. -- Add custom serializer for other types. With Avro4k you can easily add your own _AvroSerializer_ instances that provides schemas and serialization for types not supported by default. +> [!WARNING] +> **Important**: As of today, avro4k is **only available for JVM platform**, and theoretically for android platform (as apache avro library is already **android-ready**).
If +> you would like to have js/wasm/native compatible platforms, please put a :thumbsup: on [this issue](https://github.com/avro-kotlin/avro4k/issues/207) -## Schemas +# Quick start -Writing schemas manually through the Java based `SchemaBuilder` classes can be tedious for complex domain models. -Avro4k allows us to generate schemas directly from data classes at compile time using the Kotlin Serialization library. -This gives you both the convenience of generated code, without the annoyance of having to run a code generation step, as well as avoiding the performance penalty of runtime reflection based code. +## Basic encoding -Let's define some classes. +
+Example: ```kotlin -@Serializable -data class Ingredient(val name: String, val sugar: Double, val fat: Double) +package myapp + +import com.github.avrokotlin.avro4k.* +import kotlinx.serialization.* @Serializable -data class Pizza(val name: String, val ingredients: List, val vegetarian: Boolean, val kcals: Int) +data class Project(val name: String, val language: String) + +fun main() { + // Generating schemas + val schema = Avro.schema() + println(schema.toString()) // {"type":"record","name":"Project","namespace":"myapp","fields":[{"name":"name","type":"string"},{"name":"language","type":"string"}]} + + // Serializing objects + val data = Project("kotlinx.serialization", "Kotlin") + val bytes = Avro.encodeToByteArray(data) + + // Deserializing objects + val obj = Avro.decodeFromByteArray(bytes) + println(obj) // Project(name=kotlinx.serialization, language=Kotlin) +} ``` -To generate an Avro Schema, we need to use the `Avro` object, invoking `schema` and passing in the serializer generated by the Kotlin Serialization compiler plugin for your target class. This will return an `org.apache.avro.Schema` instance. +
-In other words: +## Single object + +Avro4k provides a way to encode and decode single objects with `AvroSingleObject` class. This encoding will prefix the binary data with the schema fingerprint to +allow knowing the writer schema when reading the data. The downside is that you need to provide a schema registry to get the schema from the fingerprint. +This format is perfect for payloads sent through message brokers like kafka or rabbitmq as it is the most compact schema-aware format. + +
+Example: ```kotlin -val schema = Avro.default.schema(Pizza.serializer()) -println(schema.toString(true)) -``` +package myapp -Where the generated schema is as follows: +import com.github.avrokotlin.avro4k.* +import kotlinx.serialization.* +import org.apache.avro.SchemaNormalization -```json -{ - "type":"record", - "name":"Pizza", - "namespace":"com.github.avrokotlin.avro4k.example", - "fields":[ - { - "name":"name", - "type":"string" - }, - { - "name":"ingredients", - "type":{ - "type":"array", - "items":{ - "type":"record", - "name":"Ingredient", - "fields":[ - { - "name":"name", - "type":"string" - }, - { - "name":"sugar", - "type":"double" - }, - { - "name":"fat", - "type":"double" - } - ] - } - } - }, - { - "name":"vegetarian", - "type":"boolean" - }, - { - "name":"kcals", - "type":"int" - } - ] +@Serializable +data class Project(val name: String, val language: String) + +fun main() { + val schema = Avro.schema() + val schemasByFingerprint = mapOf(SchemaNormalization.parsingFingerprint64(schema), schema) + val singleObjectInstance = AvroSingleObject { schemasByFingerprint[it] } + + // Serializing objects + val data = Project("kotlinx.serialization", "Kotlin") + val bytes = singleObjectInstance.encodeToByteArray(data) + + // Deserializing objects + val obj = singleObjectInstance.decodeFromByteArray(bytes) + println(obj) // Project(name=kotlinx.serialization, language=Kotlin) } ``` -You can see that the schema generator handles nested data classes, lists, primitives, etc. For a full list of supported object types, see the table later. +
+> For more details, check in the avro spec the [single object encoding](https://avro.apache.org/docs/1.11.3/specification/#single-object-encoding). -### Overriding class name and namespace +## Object container -Avro schemas for complex types (RECORDs) contain a name and a namespace. -By default, these are the name of the class and the enclosing package name, but it is possible to customize these using the annotations `@AvroName` and `@AvroNamespace`. +Avro4k provides a way to encode and decode object container — also known as data file — with `AvroObjectContainer` class. This encoding will prefix the binary data with the +full schema to +allow knowing the writer schema when reading the data. This format is perfect for storing many long-term objects in a single file. -For example, the following class: +The main difference with the `AvroObjectContainer` is that you will encode and decode `Sequence` of objects instead of single objects. + +Be aware that consuming the decoded `Sequence` needs to be done **before** closing the stream, or you will get an exception as a sequence is a "hot" source, +which means that if there is millions of objects in the file, all the objects are extracted one-by-one when requested. If you take only the first 10 objects and close the stream, +the remaining objects won't be extracted. Use carefully `sequence.toList()` as it could lead to OutOfMemoryError as extracting millions of objects may not fit in memory. + +
+Example: ```kotlin -package com.github.avrokotlin.avro4k.example +package myapp -data class Foo(a: String) -``` +import com.github.avrokotlin.avro4k.* +import kotlinx.serialization.* -Would normally have a schema like this: +@Serializable +data class Project(val name: String, val language: String) + +fun main() { + // Serializing objects + val valuesToEncode = sequenceOf( + Project("kotlinx.serialization", "Kotlin"), + Project("java.lang", "Java"), + Project("avro4k", "Kotlin"), + ) + Files.newOutputStream(Path("your-file.bin")).use { fileStream -> + AvroObjectContainer.encodeToStream(valuesToEncode, fileStream, builder) + } -```json -{ - "type":"record", - "name":"Foo", - "namespace":"com.github.avrokotlin.avro4k.example", - "fields":[ - { - "name":"a", - "type":"string" + // Deserializing objects + Files.newInputStream(Path("your-file.bin")).use { fileStream -> + AvroObjectContainer.decodeFromStream(valuesToEncode, fileStream, builder).forEach { + println(it) // Project(name=kotlinx.serialization, language=Kotlin) ... + } } - ] } ``` -However we can override the name and/or the namespace like this: +
+ +> For more details, check in the avro spec the [single object encoding](https://avro.apache.org/docs/1.11.3/specification/#single-object-encoding). + +# Important notes + +- **Avro4k** is highly based on apache avro library, that implies all the schema validation is done by it +- All members annotated with `@ExperimentalSerializationApi` are **subject to changes** in future releases without any notice as they are experimental, so please + check the release notes to check the needed migration. At least, given a version `A.B.C`, only the minor `B` number will be incremented, not the major `A`. +- **Avro4k** also supports encoding and decoding generic data, mainly because of confluent schema registry compatibility as their serializers only handle generic data. When avro4k + will support their schema registry, the generic encoding will be removed to keep this library as simple as possible. + +# Setup + +
+ Gradle Kotlin DSL ```kotlin -package com.github.avrokotlin.avro4k.example +plugins { + kotlin("jvm") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion +} -@AvroName("Wibble") -@AvroNamespace("com.other") -data class Foo(val a: String) +dependencies { + implementation("com.github.avro-kotlin.avro4k:avro4k-core:$avro4kVersion") +} ``` -And then the generated schema looks like this: +
-```json -{ - "type":"record", - "name":"Wibble", - "namespace":"com.other", - "fields":[ - { - "name":"a", - "type":"string" - } - ] +
+ +
+ Gradle Groovy DSL + +```groovy +plugins { + id 'org.jetbrains.kotlin.multiplatform' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.serialization' version kotlinVersion +} + +dependencies { + implementation "com.github.avro-kotlin.avro4k:avro4k-core:$avro4kVersion" } ``` -Note: It is possible, but not necessary, to use both AvroName and AvroNamespace. You can just use either of them if you wish. +
+ +
+ +
+ Maven + +Add serialization plugin to Kotlin compiler plugin: + +```xml + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + +``` + +Add the avro4k dependency: + +```xml + + + com.github.avro-kotlin.avro4k + avro4k-core + ${avro4k.version} + +``` +
+## Versions matrix +| Avro4k | Kotlin | Kotlin API/language | Kotlin serialization | +|------------|----------|---------------------|----------------------| +| `>= 2.0.0` | `>= 2.0` | `>= 1.9` | `>= 1.7` | +| `< 2.0.0` | `>= 1.6` | `>= 1.6` | `>= 1.3` | +# How to generate schemas -### Overriding a field name +Writing schemas manually or using the Java based `SchemaBuilder` can be tedious. +`kotlinx-serialization` simplifies this generating for us the corresponding descriptors to allow generating avro schemas easily, without any reflection. +Also, it provides native compatibility with data classes (including open and sealed classes), inline classes, any collection, array, enums, and primitive values. -The `@AvroName` annotation can also be used to override field names. -This is useful when the record instances you are generating or reading need to have field names different from the Kotlin data classes. -For example if you are reading data generated by another system or another language. +> [!NOTE] +> For more information about the avro schema, please refer to the [avro specification](https://avro.apache.org/docs/1.11.3/specification/) -Given the following class. +To allow generating a schema for a specific class, you need to annotate it with `@Serializable`: ```kotlin -package com.github.avrokotlin.avro4k.example +@Serializable +data class Ingredient(val name: String, val sugar: Double) -data class Foo(val a: String, @AvroName("z") val b : String) +@Serializable +data class Pizza(val name: String, val ingredients: List, val topping: Ingredient?, val vegetarian: Boolean) ``` -Then the generated schema would look like this: +Then you can generate the schema using the `Avro.schema` function: + +```kotlin +val schema = Avro.schema() +println(schema.toString(true)) +``` + +The generated schema will look as follows: ```json { - "type":"record", - "name":"Foo", - "namespace":"com.github.avrokotlin.avro4k.example", - "fields":[ - { - "name":"a", - "type":"string" - }, - { - "name":"z", - "type":"string" - } - ] + "type": "record", + "name": "Pizza", + "namespace": "com.github.avrokotlin.avro4k.example", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "ingredients", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "Ingredient", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "sugar", + "type": "double" + } + ] + } + } + }, + { + "name": "topping", + "type": [ + "null", + { + "type": "record", + "name": "Ingredient" + } + ], + "default": null + }, + { + "name": "vegetarian", + "type": "boolean" + } + ] } ``` -Notice that the second field is z and not b. +If you need to configure your `Avro` instance, you need to create your own instance of `Avro` with the wanted configuration, and then use it to generate the schema: -Note: `@AvroName` does not add an alternative name for the field, but an override. -If you wish to have alternatives then you should use `@AvroAlias`. +```kotlin +val yourAvroInstance = Avro { + // your configuration +} +yourAvroInstance.schema() +``` +# Usage +## Customizing the configuration +By default, `Avro` is configured with the following behavior: +- `implicitNulls`: The nullable fields are considered null when decoding if the writer record's schema does not contain this field. +- `implicitEmptyCollections`: The non-nullable map and collection fields are considered empty when decoding if the writer record's schema does not contain this field. + - If `implicitNulls` is true, it takes precedence so the empty collections are set as null if the value is missing instead of an empty collection. +- `validateSerialization`: There is no validation of the schema when encoding or decoding data, which means that serializing using a custom serializer could lead to unexpected behavior. Be careful with your custom serializers. More details [in this section](#set-a-custom-schema). +- `fieldNamingStrategy`: The record's field naming strategy is using the original kotlin field name. To change it, [check this section](#changing-records-field-name). +So each time you call a method on the `Avro` object implicitely invoke the default configuration. Example: -### Adding properties and docs to a Schema +```kotlin +Avro.encodeToByteArray(MyData("value")) +Avro.decodeFromByteArray(bytes) +Avro.schema() +``` -Avro allows a doc field, and arbitrary key/values to be added to generated schemas. -Avro4k supports this through the use of `@AvroDoc` and `@AvroProp` annotations. +If you need to change the default behavior, you need to create your own instance of `Avro` with the wanted configuration: -These properties works on either complex or simple types - in other words, on both fields and classes. +```kotlin +val yourAvroInstance = Avro { + fieldNamingStrategy = FieldNamingStrategy.Builtins.SnakeCase + implicitNulls = false + implicitEmptyCollections = false + validateSerialization = true +} +yourAvroInstance.encodeToByteArray(MyData("value")) +yourAvroInstance.decodeFromByteArray(bytes) +yourAvroInstance.schema() +``` -For example, the following code: +## Types matrix + +| Kotlin type | Generated schema type | Other compatible writer types | Compatible logical type | Note / Serializer class | +|------------------------------|-----------------------|--------------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `Boolean` | `boolean` | `string` | | | +| `Byte`, `Short`, `Int` | `int` | `long`, `float`, `double`, `string` | | | +| `Long` | `long` | `int`, `float`, `double`, `string` | | | +| `Float` | `float` | `double`, `string` | | | +| `Double` | `double` | `float`, `string` | | | +| `Char` | `int` | `string` (exactly 1 char required) | `char` | The value serialized is the char code. When reading from a `string`, requires exactly 1 char | +| `String` | `string` | `bytes` (UTF8), `fixed` (UTF8) | | | +| `ByteArray` | `bytes` | `string` (UTF8), `fixed` (UTF8) | | | +| `Map<*, *>` | `map` | | | The map key must be string-able. Mainly everything is string-able except null and composite types (collection, data classes) | +| `Collection<*>` | `array` | | | | +| `data class` | `record` | | | | +| `enum class` | `enum` | `string` | | | +| `@AvroFixed`-compatible | `fixed` | `bytes`, `string` | | Throws an error at runtime if the writer type is not present in the column "other compatible writer types" | +| `@AvroStringable`-compatible | `string` | `int`, `long`, `float`, `double`, `string`, `fixed`, `bytes` | | Ignored when the writer type is not present in the column "other compatible writer types" | +| `java.math.BigDecimal` | `bytes` | `int`, `long`, `float`, `double`, `string`, `fixed`, `bytes` | `decimal` | To use it, annotate the field with `@AvroDecimal` to give the `scale` and the `precision` | +| `java.math.BigDecimal` | `string` | `int`, `long`, `float`, `double`, `fixed`, `bytes` | | To use it, annotate the field with `@AvroStringable`. `@AvroDecimal` is ignored in that case | +| `java.util.UUID` | `string` | | `uuid` | To use it, just annotate the field with `@Contextual` | +| `java.net.URL` | `string` | | | To use it, just annotate the field with `@Contextual` | +| `java.math.BigInteger` | `string` | `int`, `long`, `float`, `double` | | To use it, just annotate the field with `@Contextual` | +| `java.time.LocalDate` | `int` | `long`, `string` (ISO8601) | `date` | To use it, just annotate the field with `@Contextual` | +| `java.time.Instant` | `long` | `string` (ISO8601) | `timestamp-millis` | To use it, just annotate the field with `@Contextual` | +| `java.time.Instant` | `long` | `string` (ISO8601) | `timestamp-micros` | To use it, [register the serializer](#support-additional-non-serializable-types) `com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer` | +| `java.time.LocalDateTime` | `long` | `string` (ISO8601) | `timestamp-millis` | To use it, just annotate the field with `@Contextual` | +| `java.time.LocalTime` | `int` | `long`, `string` (ISO8601) | `time-millis` | To use it, just annotate the field with `@Contextual` | +| `java.time.Duration` | `fixed` of 12 | `string` (ISO8601) | `duration` | To use it, just annotate the field with `@Contextual` | +| `java.time.Period` | `fixed` of 12 | `string` (ISO8601) | `duration` | To use it, just annotate the field with `@Contextual` | +| `kotlin.time.Duration` | `fixed` of 12 | `string` (ISO8601) | `duration` | | + +> [!NOTE] +> For more details, check the [built-in classes in kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/builtin-classes.md) + +## Add documentation to a schema + +You may want to add documentation to a schema to provide more information about a field or a named type (only RECORD and ENUM for the moment). + +> [!WARNING] +> Do not use `@org.apache.avro.reflect.AvroDoc` as this annotation is not visible by Avro4k. ```kotlin -package com.github.avrokotlin.avro4k.example +import com.github.avrokotlin.avro4k.AvroDoc @Serializable -@AvroDoc("hello, is it me you're looking for?") -data class Foo(@AvroDoc("I am a string") val str: String, - @AvroDoc("I am a long") val long: Long, - val int: Int) -``` - -Would result in the following schema: +@AvroDoc("This is a record documentation") +data class MyData( + @AvroDoc("This is a field documentation") + val myField: String +) -```json -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.example", - "doc":"hello, is it me you're looking for?", - "fields": [ - { - "name": "str", - "type": "string", - "doc" : "I am a string" - }, - { - "name": "long", - "type": "long", - "doc" : "I am a long" - }, - { - "name": "int", - "type": "int" - } - ] +@Serializable +@AvroDoc("This is an enum documentation") +enum class MyEnum { + A, + B } ``` -Similarly, for properties: +> [!NOTE] +> This impacts only the schema generation. -```kotlin -package com.github.avrokotlin.avro4k.example +## Support additional non-serializable types -@Serializable -@AvroProp("jack", "bruce") -data class Annotated(@AvroProp("richard", "ashcroft") val str: String, - @AvroProp("kate", "bush") val long: Long, - val int: Int) -``` +When looking at the [types matrix](#types-matrix), you can see some of them natively supported by Avro4k, but some others are not. +Also, your own types may not be serializable. -Would generate this schema: +To fix it, you need to create a custom **serializer** that will handle the serialization and deserialization of the value, and provide a descriptor. -```json -{ - "type": "record", - "name": "Annotated", - "namespace": "com.github.avrokotlin.avro4k.example", - "fields": [ - { - "name": "str", - "type": "string", - "richard": "ashcroft" - }, - { - "name": "long", - "type": "long", - "kate": "bush" - }, - { - "name": "int", - "type": "int" - } - ], - "jack": "bruce" -} -``` +> [!NOTE] +> This impacts the serialization and the deserialization. It can also impact the schema generation if the serializer is providing a custom logical type or a custom schema through +> the descriptor. -### Decimal scale and precision +### Write your own serializer -In order to customize the scale and precision used by BigDecimal schema generators, -you can add the `@ScalePrecision` annotation to instances of BigDecimal. +To create a custom serializer, you need to implement the `AvroSerializer` abstract class and override the `serializeAvro` and `deserializeAvro` methods. +You also need to override `getSchema` to provide the schema of your custom type as a custom serializer means non-standard encoding and decoding. -For example, this code: +
+Create a serializer that needs Avro features like getting the schema or encoding bytes and fixed types ```kotlin -@Serializable -data class Test(@ScalePrecision(1, 4) val decimal: BigDecimal) +object YourTypeSerializer : AvroSerializer(YourType::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + // you can access the data class element, inlined elements from value classes, and their annotations + // you can also access the avro configuration in the context + return ... /* create the corresponding schema using SchemaBuilder or Schema.create */ + } -val schema = Avro.default.schema(Test.serializer()) -``` + override fun serializeAvro(encoder: AvroEncoder, value: YourType) { + encoder.currentWriterSchema // you can access the current writer schema + encoder.encodeString(value.toString()) + } -Would generate the following schema: + override fun deserializeAvro(decoder: AvroDecoder): YourType { + decoder.currentWriterSchema // you can access the current writer schema + return YourType.fromString(decoder.decodeString()) + } -```json -{ - "type":"record", - "name":"Test", - "namespace":"com.foo", - "fields":[{ - "name":"decimal", - "type":{ - "type":"bytes", - "logicalType":"decimal", - "scale":"1", - "precision":"4" + override fun serializeGeneric(encoder: Encoder, value: YourType) { + // you may want to implement this function if you also want to use the serializer outside of Avro4k + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): YourType { + // you may want to implement this function if you also want to use the serializer outside of Avro4k + return YourType.fromString(decoder.decodeString()) } - }] } ``` -### Avro Fixed +
-Avro supports the idea of fixed length byte arrays. -To use these we can either override the schema generated for a type to return Schema.Type.Fixed. -This will work for types like String or UUID. -You can also annotate a field with `@AvroFixed(size)`. +You may want to just implement a `KSerializer` if you don't need specific Avro features, but you won't be able to associate a custom schema to it: -For example: +
+Create a generic serializer that doesn't need specific Avro features ```kotlin -package com.github.avrokotlin.avro4k.example +object YourTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("YourType", PrimitiveKind.STRING) -data class Foo(@AvroFixed(7) val mystring: String) + override fun serialize(encoder: Encoder, value: YourType) { + encoder.encodeString(value.toString()) + } -val schema = Avro.default.schema(Foo.serializer()) + override fun deserialize(decoder: Decoder): YourType { + return YourType.fromString(decoder.decodeString()) + } +} ``` -Will generate the following schema: +
-```json -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.example", - "fields": [ - { - "name": "mystring", - "type": { - "type": "fixed", - "name": "mystring", - "size": 7 - } +### Register the serializer globally (not compile time) + +You first need to configure your `Avro` instance with the wanted serializer instance: + +```kotlin +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual + +val myCustomizedAvroInstance = Avro { + serializersModule = SerializersModule { + // give the object serializer instance + contextual(YourTypeSerializerObject) + // or instanciate it if it's a class and not an object + contextual(YourTypeSerializerClass()) } - ] } ``` -If you have a type that you always want to be represented as fixed, then rather than annotate every single location - it is used, you can annotate the type itself. +Then just annotated the field with `@Contextual`: ```kotlin -package com.github.avrokotlin.avro4k.example - -@AvroFixed(4) @Serializable -data class FixedA(val bytes: ByteArray) +data class MyData( + @Contextual val myField: YourType +) +``` -@Serializable -data class Foo(val a: FixedA) +### Register the serializer just for a field at compile time -val schema = Avro.default.schema(Foo.serializer()) +```kotlin +@Serializable +data class MyData( + @Serializable(with = YourTypeSerializer::class) val myField: YourType +) ``` -And this would generate: +## Changing record's field name -```json -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.example", - "fields": [ - { - "name": "a", - "type": { - "type": "fixed", - "name": "FixedA", - "size": 4 - } - } - ] -} +By default, field names are the original name of the kotlin fields in the data classes. + +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization of the field. + +### Individual field name change + +To change a field name, annotate it with `@SerialName`: + +```kotlin +@Serializable +data class MyData( + @SerialName("custom_field_name") val myField: String +) ``` -### Transient Fields +> [!NOTE] +> `@SerialName` will still be handled by the naming strategy + +### Field naming strategy (overall change) + +To apply a naming strategy to all fields, you need to set the `fieldNamingStrategy` in the `Avro` configuration. + +> [!NOTE] +> This is only applicable for RECORD fields, and not for ENUM symbols. -The kotlinx.serialization framework does not support the standard @transient anotation to mark a field as ignored, but instead supports its own `@kotlinx.serialization.Transient` annotation to do the same job. - Any field marked with this will be excluded from the generated schema. +There is 3 built-ins strategies: -For example, the following code: +- `NoOp` (default): keeps the original kotlin field name +- `SnakeCase`: converts the original kotlin field name to snake_case with underscores before each uppercase letter +- `PascalCase`: upper-case the first letter of the original kotlin field name +- If you need more, please [file an issue](https://github.com/avro-kotlin/avro4k/issues/new/choose) + +First, create your own instance of `Avro` with the wanted naming strategy: ```kotlin -package com.github.avrokotlin.avro4k.example -data class Foo(val a: String, @AvroTransient val b: String) +val myCustomizedAvroInstance = Avro { + fieldNamingStrategy = FieldNamingStrategy.Builtins.SnakeCase +} + ``` -Would result in the following schema: +Then, use this instance to generate the schema or encode/decode data: -```json -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.example", - "fields": [ - { - "name": "a", - "type": "string" - } - ] -} +```kotlin +package my.package + +@Serializable +data class MyData(val myField: String) + +val schema = myCustomizedAvroInstance.schema() // {...,"fields":[{"name":"my_field",...}]} ``` -### Nullable fields, optional fields and compatibility +## Set a default field value + +While reading avro binary data, you can miss a field (a kotlin field is present but not in the avro binary data), so Avro4k fails as it is not capable of constructing the kotlin +type without the missing field value. + +By default: +- nullable fields are optional and `default: null` is automatically added to the field ([check this section](#disable-implicit-default-null-for-nullable-fields) to opt out from this default behavior). +- nullable fields are optional and `default: null` is automatically added to the field ([check this section](#disable-implicit-default-null-for-nullable-fields) to opt out from this default behavior). + +### @AvroDefault + +To avoid this error, you can set a default value for a field by annotating it with `@AvroDefault`: -#### TL;DR; -To make your nullable fields optional (put `default: null` on all nullable fields if no other explicit default provided) and be able to remove nullable fields regarding compatibility checks, -you can set in the configuration the `defaultNullForNullableFields` to `true`. Example: ```kotlin -Avro(AvroConfiguration(defaultNullForNullableFields = true)) +import com.github.avrokotlin.avro4k.AvroDefault + +@Serializable +data class MyData( + @AvroDefault("default value") val stringField: String, + @AvroDefault("42") val intField: Int?, + @AvroDefault("""{"stringField":"custom value"}""") val nestedType: MyData? = null +) ``` -#### Longer story +> [!NOTE] +> This impacts only the schema generation and the deserialization of the field, and not the serialization. -With avro, you can have nullable fields and optional fields, that are taken into account for compatibility checking when using the schema registry. +> [!WARNING] +> Do not use `@org.apache.avro.reflect.AvroDefault` as this annotation is not visible by Avro4k. -But if you want to remove a nullable field that is not optional, depending on the compatibility mode, it may not be compatible because of the missing default value. +### kotlin default value -- What is an optional field ? -> An optional field is a field that have a *default* value, like an int with a default as `-1`. - -- What is a nullable field ? -> A nullable field is a field that contains a `null` type in its type union, but **it's not an optional field if you don't put `default` value to `null`**. +You can also set a kotlin default value, but this default won't be present into the generated schema as Avro4k is not able to retrieve it: -So to mark a field as optional and facilitate avro contract evolution regarding compatibility checks, then set `default` to `null`. +```kotlin +@Serializable +data class MyData( + val stringField: String = "default value", + val intField: Int? = 42, +) +``` + +> This impacts only the deserialization of the field, and not the serialization or the schema generation. + +## Add aliases + +To be able of reading from different written schemas, or able of writing to different schemas, you can add aliases to a named type (record, enum) field by annotating it +with `@AvroAlias`. The given aliases may contain the full name of the alias type or only the name. +> [Avro spec link](https://avro.apache.org/docs/1.11.3/specification/#aliases) -## Types - -Avro4k supports the Avro logical types out of the box as well as other common JDK types. - -Avro has no understanding of Kotlin types, or anything outside of it's built in set of supported types, so all values must be converted to something that is compatible with Avro. - -For example a `java.sql.Timestamp` is usually encoded as a `Long`, and a `java.util.UUID` is encoded as a `String`. - -Some values can be mapped in multiple ways depending on how the schema was generated. For example a `String`, which is usually encoded as an `org.apache.avro.util.Utf8` could also be encoded as an array of bytes if the generated schema for that field was `Schema.Type.BYTES`. -Therefore some serializers will take into account the schema passed to them when choosing the avro compatible type. - -The following table shows how types used in your code will be mapped / encoded in the generated Avro schemas and files. -If a type can be mapped in multiple ways, it is listed more than once. - -| JVM Type | Schema Type | Logical Type | Encoded Type | -|--------------------------------|------------------|--------------------|--------------------------| -| String | STRING | | Utf8 | -| String | FIXED | | GenericFixed | -| String | BYTES | | ByteBuffer | -| Boolean | BOOLEAN | | java.lang.Boolean | -| Long | LONG | | java.lang.Long | -| Int | INT | | java.lang.Integer | -| Short | INT | | java.lang.Integer | -| Byte | INT | | java.lang.Integer | -| Double | DOUBLE | | java.lang.Double | -| Float | FLOAT | | java.lang.Float | -| UUID | STRING | UUID | Utf8 | -| LocalDate | INT | Date | java.lang.Int | -| LocalTime | INT | Time-Millis | java.lang.Int | -| LocalDateTime | LONG | Timestamp-Millis | java.lang.Long | -| Instant | LONG | Timestamp-Millis | java.lang.Long | -| Timestamp | LONG | Timestamp-Millis | java.lang.Long | -| BigDecimal | BYTES | Decimal<8,2> | ByteBuffer | -| BigDecimal | FIXED | Decimal<8,2> | GenericFixed | -| BigDecimal | STRING | Decimal<8,2> | String | -| T? (nullable type) | UNION | | null, T | -| ByteArray | BYTES | | ByteBuffer | -| ByteAray | FIXED | | GenericFixed | -| ByteBuffer | BYTES | | ByteBuffer | -| List[Byte] | BYTES | | ByteBuffer | -| Array | ARRAY | | Array[T] | -| List | ARRAY | | Array[T] | -| Set | ARRAY | | Array[T] | -| Map[String, V] | MAP | | java.util.Map[String, V] | -| data class T | RECORD | | GenericRecord | -| enum class | ENUM | | GenericEnumSymbol | - -In order to use logical types, annotate the value with an appropriate Serializer: +> [!NOTE] +> Aliases are not impacted by [naming strategy](#field-naming-strategy-overall-change), so you need to provide aliases directly applying the corresponding naming strategy if you +> need to respect it. ```kotlin -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.InstantSerializer -import kotlinx.serialization.Serializable -import java.time.Instant +import com.github.avrokotlin.avro4k.AvroAlias @Serializable -data class WithInstant( - @Serializable(with=InstantSerializer::class) val inst: Instant +@AvroAlias("full.name.RecordName", "JustOtherRecordName") +data class MyData( + @AvroAlias("anotherFieldName", "old_field_name") val myField: String ) ``` -All the logical type serializers are available in the package `com.github.avrokotlin.avro4k.serializer`. You can find additional serializers for [arrow](https://arrow-kt.io/) types in the -[avro4k-arrow](https://github.com/avro-kotlin/avro4k-arrow) project. +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. -## Input / Output +> [!WARNING] +> Do not use `@org.apache.avro.reflect.AvroAlias` as this annotation is not visible by Avro4k. -### Formats +## Add metadata to a schema (custom properties) -Avro supports four different encoding types [serializing records](https://avro.apache.org/docs/current/spec.html#Data+Serialization+and+Deserialization). +You can add custom properties to a schema to have additional metadata on a type. +To do so, you can annotate the data class or field with `@AvroProp`. The value can be a regular string or any json content: -These are binary with schema, binary without schema, json and single object encoding. +```kotlin +@Serializable +@AvroProp("custom_string_property", "The default non-json value") +@AvroProp("custom_int_property", "42") +@AvroProp("custom_json_property", """{"key":"value"}""") +data class MyData( + @AvroProp("custom_field_property", "Also working on fields") + val myField: String +) +``` + +To add metadata to a type not owned by you, you can use a value class. Here an example with a `BigQuery` type that needs the property `sqlType = JSON` on `string` type: +```kotlin +@Serializable +value class BigQueryJson(@AvroProp("sqlType", "JSON") val value: String) -In avro4k these are represented by an `AvroFormat` enum with three values - `AvroFormat.Binary` (binary no schema), `AvroFormat.Data` (binary with schema), and `AvroFormat.Json`. -The single object encoding is currently not supported. +println(Avro.schema().toString(true)) // {"type":"string","sqlType":"JSON"} +``` -Binary encoding without the schema does not include field names, self-contained information about the types of individual bytes, nor field or record separators. -Therefore readers are wholly reliant on the schema used when the data was encoded but the format is by far the most compact. Binary encodings are [fast](https://www.slideshare.net/oom65/orc-files?next_slideshow=1). +> [!NOTE] +> This impacts only the schema generation. For more details, check the [avro specification](https://avro.apache.org/docs/1.11.3/specification/#schema_props). -Binary encoding with the schema is still quick to deserialize, but is obviously less compact. However, as the schema is included readers do not need to have access to the original schema. +> [!WARNING] +> Do not use `@org.apache.avro.reflect.AvroMeta` as this annotation is not visible by Avro4k. -Json encoding is the largest and slowest, but the easist to work with outside of Avro, and of course is easy to view on the wire (if that is a concern). +## Change scale and precision for `decimal` logical type +By default, the scale is `2` and the precision `8`. To change it, annotate the field with `@AvroDecimal`: -### Serializing +```kotlin +@Serializable +data class MyData( + @AvroDecimal(scale = 4, precision = 10) val myField: BigDecimal +) +``` -Avro4k allows us to easily serialize data classes using an instance of `AvroOutputStream` which we write to, and close, just like you would any regular output stream. It is created by calling `openOutputStream` on an `Avro` instance. +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. -When creating an output stream we specify the target, such as a File, Path, or another output stream. -If nothing more is specified, the default encode format `AvroEncodeFormat.Data` is used and the schema will be deducted -from the passed `SerializationStrategy`. +## Change enum values' name -For example, to serialize instances of the `Pizza` class: +By default, enum symbols are exactly the name of the enum values in the enum classes. To change this default, you need to annotate enum values with `@SerialName`. ```kotlin @Serializable -data class Ingredient(val name: String, val sugar: Double, val fat: Double) +enum class MyEnum { + @SerialName("CUSTOM_NAME") + A, + B, + C +} +``` + +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. + +## Set enum default + +When reading with a schema but was written with a different schema, sometimes the reader can miss the enum symbol that triggers an error. +To avoid this error, you can set a default symbol for an enum by annotating the expected fallback with `@AvroEnumDefault`. +```kotlin @Serializable -data class Pizza(val name: String, val ingredients: List, val vegetarian: Boolean, val kcals: Int) +enum class MyEnum { + A, -val veg = Pizza("veg", listOf(Ingredient("peppers", 0.1, 0.3), Ingredient("onion", 1.0, 0.4)), true, 265) -val hawaiian = Pizza("hawaiian", listOf(Ingredient("ham", 1.5, 5.6), Ingredient("pineapple", 5.2, 0.2)), false, 391) + @AvroEnumDefault + B, -val pizzaSchema = Avro.default.schema(Pizza.serializer()) -val os = Avro.default.openOutputStream(Pizza.serializer()) { - encodeFormat = AvroEncodeFormat.Binary - schema = pizzaSchema -}.to(File("pizzas.avro")) -os.write(listOf(veg, hawaiian)) -os.flush() + C +} ``` -### Deserializing +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. + +## Change type name (RECORD and ENUM) -We can easily deserialize a file back into data classes using instances of `AvroInputStream` which work in a similar way to the output stream version. -Given the `pizzas.avro` file we generated in the previous section on serialization, we will read the records back in as instances of the `Pizza` class. -First create an instance of the input stream by calling `openInputStream` on an `Avro` object specifying the types we will read back. -Optionally, we can also specify a decoding format. -Depending on the chosen format, the source and the writer schema also needs to be configured. +RECORD and ENUM types in Avro have a name and a namespace (composing a full-name like `namespace.name`). By default, the name is the name of the class/enum and the namespace is the +package name. +To change this default, you need to annotate data classes and enums with `@SerialName`. -`AvroInputStream` has functions to get the next single value, or to return all values as an iterator. +> [!WARNING] +> `@SerialName` is redefining the full-name of the annotated class or enum, so you **must** repeat the name or the namespace if you only need to change the namespace or the name +> respectively. -For example, the following code: +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. + +### Changing the name while keeping the namespace ```kotlin -@Serializable -data class Ingredient(val name: String, val sugar: Double, val fat: Double) +package my.package @Serializable -data class Pizza(val name: String, val ingredients: List, val vegetarian: Boolean, val kcals: Int) +@SerialName("my.package.MyRecord") +data class MyData(val myField: String) +``` + +### Changing the namespace while keeping the name -val veg = Pizza("veg", listOf(Ingredient("peppers", 0.1, 0.3), Ingredient("onion", 1.0, 0.4)), true, 265) -val hawaiian = Pizza("hawaiian", listOf(Ingredient("ham", 1.5, 5.6), Ingredient("pineapple", 5.2, 0.2)), false, 391) +```kotlin +package my.package + +@Serializable +@SerialName("custom.namespace.MyData") +data class MyData(val myField: String) +``` -val pizzaSchema = Avro.default.schema(Pizza.serializer()) +### Changing the name and the namespace -val output = Avro.default.openOutputStream(Pizza.serializer()) { - encodeFormat = AvroEncodeFormat.Binary - schema = pizzaSchema -}.to(File("pizzas.avro")) -output.write(listOf(veg, hawaiian)) -output.close() +```kotlin +package my.package -val input = Avro.default.openInputStream(Pizza.serializer()) { - decodeFormat = AvroDecodeFormat.Binary(/* writerSchema */ pizzaSchema, /* readerSchema */pizzaSchema) -}.from(File("pizzas.avro")) -input.iterator().forEach { println(it) } -input.close() +@Serializable +@SerialName("custom.namespace.MyRecord") +data class MyData(val myField: String) ``` -Will print out the following: +## Change type name (FIXED only) + +> [!WARNING] +> For the moment, it is not possible to manually change the namespace or the name of a FIXED type as the type name is coming from the field name and the namespace from the +> enclosing data class package. + +## Set a custom schema + +To associate a type or a field to a custom schema, you need to create a serializer that will handle the serialization and deserialization of the value, and provide the expected schema. + +See [support additional non-serializable types](#support-additional-non-serializable-types) section to get detailed explanation about writing a serializer and +registering it. -```text -Pizza(name=veg, ingredients=[Ingredient(name=peppers, sugar=0.1, fat=0.3), Ingredient(name=onion, sugar=1.0, fat=0.4)], vegetarian=true, kcals=265) -Pizza(name=hawaiian, ingredients=[Ingredient(name=ham, sugar=1.5, fat=5.6), Ingredient(name=pineapple, sugar=5.2, fat=0.2)], vegetarian=false, kcals=391) +## Skip a kotlin field + +To skip a field during encoding, you can annotate it with `@kotlinx.serialization.Transient`. +Note that you need to provide a default value for the field as the field will be totally discarded also during encoding (IntelliJ should trigger a warn). + +```kotlin +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +data class Foo(val a: String, @Transient val b: String = "default value") ``` -### Using avro4k in your project +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. -Gradle -```compile 'com.github.avro-kotlin.avro4k:avro4k-core:xxx'``` +## Force a field to be a `string` type -Maven -```xml - - com.github.avro-kotlin.avro4k - avro4k-core - xxx - +You can force a field (or the value class' property) to have its inferred schema as a `string` type by annotating it with `@AvroString`. + +Compatible types visible in the [types matrix](#types-matrix), indicated by the "Other compatible writer types" column. The **writer schema compatibility is still respected**, so if the field has been written as an int, a stringified int will be deserialized as an int without the need of parsing it. It is the same for the rerverse: If an int has been written as a string, it will be deserialized as an int by parsing the string content. + +> [!INFO] +> Note that the type must be compatible with the `string` type, otherwise it will be ignored. +> Your custom serializer generated schema must handle this annotation, or it will be ignored. + +**Examples:** +```kotlin +@Serializable +data class MyData( + @AvroString val anInt: Int, + @AvroString val rawString: ByteArray, + @AvroString @Contextual val bigDecimal: BigDecimal, +) +@JvmInline +@Serializable +value class StringifiedPrice( + @AvroString val amount: Double, +) ``` -Check the latest released version on Maven Central +> [!NOTE] +> This impacts the schema generation, the serialization and the deserialization. + +# Nullable fields, optional fields and compatibility + +With avro, you can have nullable fields and optional fields, that are taken into account for compatibility checking when using the schema registry. + +But if you want to remove a nullable field that is not optional, depending on the compatibility mode, it may not be compatible because of the missing default value. + +- What is an optional field ? + +> An optional field is a field that have a *default* value, like an int with a default as `-1`. + +- What is a nullable field ? + +> A nullable field is a field that contains a `null` type in its type union, but **it's not an optional field if you don't put `default` value to `null`**. + +So to mark a field as optional and facilitate avro contract evolution regarding compatibility checks, then set `default` to `null`. + +# Known problems -### Known problems +- Kotlin 1.7.20 up to 1.8.10 cannot properly compile @SerialInfo-Annotations on enums (see https://github.com/Kotlin/kotlinx.serialization/issues/2121). + This is fixed with kotlin 1.8.20. So if you are planning to use any of avro4k's annotations on enum types, please make sure that you are using kotlin >= 1.8.20. -Kotlin 1.7.20 up to 1.8.10 cannot properly compile @SerialInfo-Annotations on enums (see https://github.com/Kotlin/kotlinx.serialization/issues/2121). -This is fixed with kotlin 1.8.20. So if you are planning to use any of avro4k's annotations on enum types, please make sure that you are using kotlin >= 1.8.20. +# Migrating from v1 to v2 +Heads up to the [migration guide](Migrating-from-v1.md) to update your code from avro4k v1 to v2. -### Contributions +# Contributions Contributions to avro4k are always welcome. Good ways to contribute include: - Raising bugs and feature requests -- Fixing bugs and enhancing the DSL +- Fixing bugs and enhancing the API - Improving the performance of avro4k -= Adding to the documentation +- Adding documentation diff --git a/api/avro4k-core.api b/api/avro4k-core.api new file mode 100644 index 00000000..9d6b11d1 --- /dev/null +++ b/api/avro4k-core.api @@ -0,0 +1,551 @@ +public abstract interface class com/github/avrokotlin/avro4k/AnyValueDecoder { + public abstract fun decodeAny (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; +} + +public abstract class com/github/avrokotlin/avro4k/Avro : kotlinx/serialization/BinaryFormat { + public static final field Default Lcom/github/avrokotlin/avro4k/Avro$Default; + public synthetic fun (Lcom/github/avrokotlin/avro4k/AvroConfiguration;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun decodeFromByteArray (Lkotlinx/serialization/DeserializationStrategy;[B)Ljava/lang/Object; + public final fun decodeFromByteArray (Lorg/apache/avro/Schema;Lkotlinx/serialization/DeserializationStrategy;[B)Ljava/lang/Object; + public fun encodeToByteArray (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B + public final fun encodeToByteArray (Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B + public final fun getConfiguration ()Lcom/github/avrokotlin/avro4k/AvroConfiguration; + public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; + public final fun schema (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lorg/apache/avro/Schema; +} + +public final class com/github/avrokotlin/avro4k/Avro$Default : com/github/avrokotlin/avro4k/Avro { +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroAlias : java/lang/annotation/Annotation { + public abstract fun value ()[Ljava/lang/String; +} + +public synthetic class com/github/avrokotlin/avro4k/AvroAlias$Impl : com/github/avrokotlin/avro4k/AvroAlias { + public fun ([Ljava/lang/String;)V + public final synthetic fun value ()[Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/AvroBuilder { + public final fun getFieldNamingStrategy ()Lcom/github/avrokotlin/avro4k/FieldNamingStrategy; + public final fun getImplicitEmptyCollections ()Z + public final fun getImplicitNulls ()Z + public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; + public final fun getValidateSerialization ()Z + public final fun setFieldNamingStrategy (Lcom/github/avrokotlin/avro4k/FieldNamingStrategy;)V + public final fun setImplicitEmptyCollections (Z)V + public final fun setImplicitNulls (Z)V + public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V + public final fun setValidateSerialization (Z)V +} + +public final class com/github/avrokotlin/avro4k/AvroConfiguration { + public fun ()V + public fun (Lcom/github/avrokotlin/avro4k/FieldNamingStrategy;ZZZ)V + public synthetic fun (Lcom/github/avrokotlin/avro4k/FieldNamingStrategy;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/github/avrokotlin/avro4k/FieldNamingStrategy; + public final fun component2 ()Z + public final fun component3 ()Z + public final fun component4 ()Z + public final fun copy (Lcom/github/avrokotlin/avro4k/FieldNamingStrategy;ZZZ)Lcom/github/avrokotlin/avro4k/AvroConfiguration; + public static synthetic fun copy$default (Lcom/github/avrokotlin/avro4k/AvroConfiguration;Lcom/github/avrokotlin/avro4k/FieldNamingStrategy;ZZZILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/AvroConfiguration; + public fun equals (Ljava/lang/Object;)Z + public final fun getFieldNamingStrategy ()Lcom/github/avrokotlin/avro4k/FieldNamingStrategy; + public final fun getImplicitEmptyCollections ()Z + public final fun getImplicitNulls ()Z + public final fun getValidateSerialization ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroDecimal : java/lang/annotation/Annotation { + public abstract fun precision ()I + public abstract fun scale ()I +} + +public synthetic class com/github/avrokotlin/avro4k/AvroDecimal$Impl : com/github/avrokotlin/avro4k/AvroDecimal { + public fun (II)V + public final synthetic fun precision ()I + public final synthetic fun scale ()I +} + +public abstract interface class com/github/avrokotlin/avro4k/AvroDecoder : kotlinx/serialization/encoding/Decoder { + public abstract fun decodeBytes ()[B + public abstract fun decodeFixed ()Lorg/apache/avro/generic/GenericFixed; + public abstract fun decodeValue ()Ljava/lang/Object; + public abstract fun getCurrentWriterSchema ()Lorg/apache/avro/Schema; +} + +public final class com/github/avrokotlin/avro4k/AvroDecoder$DefaultImpls { + public static fun decodeNullableSerializableValue (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; + public static fun decodeSerializableValue (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; +} + +public final class com/github/avrokotlin/avro4k/AvroDecoderKt { + public static final fun decodeResolvingAny (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun decodeResolvingBoolean (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Z + public static final fun decodeResolvingByte (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)B + public static final fun decodeResolvingChar (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)C + public static final fun decodeResolvingDouble (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)D + public static final fun decodeResolvingFloat (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)F + public static final fun decodeResolvingInt (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)I + public static final fun decodeResolvingLong (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)J + public static final fun decodeResolvingShort (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)S + public static final fun findValueDecoder (Lcom/github/avrokotlin/avro4k/AvroDecoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroDefault : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + +public synthetic class com/github/avrokotlin/avro4k/AvroDefault$Impl : com/github/avrokotlin/avro4k/AvroDefault { + public fun (Ljava/lang/String;)V + public final synthetic fun value ()Ljava/lang/String; +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroDoc : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + +public synthetic class com/github/avrokotlin/avro4k/AvroDoc$Impl : com/github/avrokotlin/avro4k/AvroDoc { + public fun (Ljava/lang/String;)V + public final synthetic fun value ()Ljava/lang/String; +} + +public abstract interface class com/github/avrokotlin/avro4k/AvroEncoder : kotlinx/serialization/encoding/Encoder { + public abstract fun encodeBytes (Ljava/nio/ByteBuffer;)V + public abstract fun encodeBytes ([B)V + public abstract fun encodeFixed (Lorg/apache/avro/generic/GenericFixed;)V + public abstract fun encodeFixed ([B)V + public abstract fun getCurrentWriterSchema ()Lorg/apache/avro/Schema; +} + +public final class com/github/avrokotlin/avro4k/AvroEncoder$DefaultImpls { + public static fun beginCollection (Lcom/github/avrokotlin/avro4k/AvroEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/CompositeEncoder; + public static fun encodeNotNullMark (Lcom/github/avrokotlin/avro4k/AvroEncoder;)V + public static fun encodeNullableSerializableValue (Lcom/github/avrokotlin/avro4k/AvroEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V + public static fun encodeSerializableValue (Lcom/github/avrokotlin/avro4k/AvroEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V +} + +public final class com/github/avrokotlin/avro4k/AvroEncoderKt { + public static final fun encodeResolving (Lcom/github/avrokotlin/avro4k/AvroEncoder;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun resolveUnion (Lcom/github/avrokotlin/avro4k/AvroEncoder;Lorg/apache/avro/Schema;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroEnumDefault : java/lang/annotation/Annotation { +} + +public synthetic class com/github/avrokotlin/avro4k/AvroEnumDefault$Impl : com/github/avrokotlin/avro4k/AvroEnumDefault { + public fun ()V +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroFixed : java/lang/annotation/Annotation { + public abstract fun size ()I +} + +public synthetic class com/github/avrokotlin/avro4k/AvroFixed$Impl : com/github/avrokotlin/avro4k/AvroFixed { + public fun (I)V + public final synthetic fun size ()I +} + +public final class com/github/avrokotlin/avro4k/AvroGenericDataExtensionsKt { + public static final fun decodeFromGenericData (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object; + public static final fun encodeToGenericData (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class com/github/avrokotlin/avro4k/AvroJVMExtensionsKt { + public static final fun decodeFromStream (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/DeserializationStrategy;Ljava/io/InputStream;)Ljava/lang/Object; + public static final fun encodeToStream (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Ljava/io/OutputStream;)V +} + +public final class com/github/avrokotlin/avro4k/AvroKt { + public static final fun Avro (Lcom/github/avrokotlin/avro4k/Avro;Lkotlin/jvm/functions/Function1;)Lcom/github/avrokotlin/avro4k/Avro; + public static synthetic fun Avro$default (Lcom/github/avrokotlin/avro4k/Avro;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/Avro; + public static final fun schema (Lcom/github/avrokotlin/avro4k/Avro;Lkotlinx/serialization/KSerializer;)Lorg/apache/avro/Schema; +} + +public abstract class com/github/avrokotlin/avro4k/AvroObjectContainer { + public static final field Default Lcom/github/avrokotlin/avro4k/AvroObjectContainer$Default; + public synthetic fun (Lcom/github/avrokotlin/avro4k/Avro;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun decodeFromStream (Lkotlinx/serialization/DeserializationStrategy;Ljava/io/InputStream;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; + public static synthetic fun decodeFromStream$default (Lcom/github/avrokotlin/avro4k/AvroObjectContainer;Lkotlinx/serialization/DeserializationStrategy;Ljava/io/InputStream;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/sequences/Sequence; + public final fun encodeToStream (Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Lkotlin/sequences/Sequence;Ljava/io/OutputStream;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun encodeToStream$default (Lcom/github/avrokotlin/avro4k/AvroObjectContainer;Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Lkotlin/sequences/Sequence;Ljava/io/OutputStream;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun getAvro ()Lcom/github/avrokotlin/avro4k/Avro; +} + +public final class com/github/avrokotlin/avro4k/AvroObjectContainer$Default : com/github/avrokotlin/avro4k/AvroObjectContainer { +} + +public final class com/github/avrokotlin/avro4k/AvroObjectContainerBuilder { + public fun (Lorg/apache/avro/file/DataFileWriter;)V + public final fun codec (Lorg/apache/avro/file/CodecFactory;)V + public final fun metadata (Ljava/lang/String;J)V + public final fun metadata (Ljava/lang/String;Ljava/lang/String;)V + public final fun metadata (Ljava/lang/String;[B)V +} + +public final class com/github/avrokotlin/avro4k/AvroObjectContainerKt { + public static final fun AvroObjectContainer (Lcom/github/avrokotlin/avro4k/Avro;Lkotlin/jvm/functions/Function1;)Lcom/github/avrokotlin/avro4k/AvroObjectContainer; + public static synthetic fun AvroObjectContainer$default (Lcom/github/avrokotlin/avro4k/Avro;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/AvroObjectContainer; +} + +public final class com/github/avrokotlin/avro4k/AvroObjectContainerMetadataDumper { + public fun (Lorg/apache/avro/file/DataFileStream;)V + public final fun metadata (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/AvroObjectContainerMetadataDumper$MetadataAccessor; +} + +public final class com/github/avrokotlin/avro4k/AvroObjectContainerMetadataDumper$MetadataAccessor { + public fun (Lcom/github/avrokotlin/avro4k/AvroObjectContainerMetadataDumper;[B)V + public final fun asBytes ()[B + public final fun asLong ()J + public final fun asString ()Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/AvroOkioExtensionsKt { + public static final fun decodeFromSource (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/DeserializationStrategy;Lokio/BufferedSource;)Ljava/lang/Object; + public static final fun encodeToSink (Lcom/github/avrokotlin/avro4k/Avro;Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Lokio/BufferedSink;)V +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroProp : java/lang/annotation/Annotation { + public abstract fun key ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroProp$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Lcom/github/avrokotlin/avro4k/AvroProp; +} + +public synthetic class com/github/avrokotlin/avro4k/AvroProp$Impl : com/github/avrokotlin/avro4k/AvroProp { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final synthetic fun key ()Ljava/lang/String; + public final synthetic fun value ()Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/AvroSingleObject : kotlinx/serialization/BinaryFormat { + public fun (Lkotlin/jvm/functions/Function1;Lcom/github/avrokotlin/avro4k/Avro;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Lcom/github/avrokotlin/avro4k/Avro;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun decodeFromByteArray (Lkotlinx/serialization/DeserializationStrategy;[B)Ljava/lang/Object; + public final fun decodeFromStream (Lkotlinx/serialization/DeserializationStrategy;Ljava/io/InputStream;)Ljava/lang/Object; + public fun encodeToByteArray (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B + public final fun encodeToStream (Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Ljava/io/OutputStream;)V + public final fun getAvro ()Lcom/github/avrokotlin/avro4k/Avro; + public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; +} + +public final class com/github/avrokotlin/avro4k/AvroSingleObjectKt { + public static final fun encodeToByteArray (Lcom/github/avrokotlin/avro4k/AvroSingleObject;Lorg/apache/avro/Schema;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B +} + +public abstract interface annotation class com/github/avrokotlin/avro4k/AvroStringable : java/lang/annotation/Annotation { +} + +public synthetic class com/github/avrokotlin/avro4k/AvroStringable$Impl : com/github/avrokotlin/avro4k/AvroStringable { + public fun ()V +} + +public abstract interface class com/github/avrokotlin/avro4k/BooleanValueDecoder { + public abstract fun decodeBoolean (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Z +} + +public abstract interface class com/github/avrokotlin/avro4k/ByteValueDecoder { + public abstract fun decodeByte (Lcom/github/avrokotlin/avro4k/AvroDecoder;)B +} + +public abstract interface class com/github/avrokotlin/avro4k/CharValueDecoder { + public abstract fun decodeChar (Lcom/github/avrokotlin/avro4k/AvroDecoder;)C +} + +public abstract interface class com/github/avrokotlin/avro4k/DoubleValueDecoder { + public abstract fun decodeDouble (Lcom/github/avrokotlin/avro4k/AvroDecoder;)D +} + +public abstract interface class com/github/avrokotlin/avro4k/FieldNamingStrategy { + public static final field Builtins Lcom/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins; + public abstract fun resolve (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins { +} + +public final class com/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$OriginalElementName : com/github/avrokotlin/avro4k/FieldNamingStrategy { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$OriginalElementName; + public fun resolve (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$PascalCase : com/github/avrokotlin/avro4k/FieldNamingStrategy { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$PascalCase; + public fun resolve (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$SnakeCase : com/github/avrokotlin/avro4k/FieldNamingStrategy { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/FieldNamingStrategy$Builtins$SnakeCase; + public fun resolve (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String; +} + +public abstract interface class com/github/avrokotlin/avro4k/FloatValueDecoder { + public abstract fun decodeFloat (Lcom/github/avrokotlin/avro4k/AvroDecoder;)F +} + +public abstract interface class com/github/avrokotlin/avro4k/IntValueDecoder { + public abstract fun decodeInt (Lcom/github/avrokotlin/avro4k/AvroDecoder;)I +} + +public final class com/github/avrokotlin/avro4k/ListRecord : com/github/avrokotlin/avro4k/Record { + public fun (Lorg/apache/avro/Schema;Ljava/util/List;)V + public fun (Lorg/apache/avro/Schema;[Ljava/lang/Object;)V + public final fun copy (Lorg/apache/avro/Schema;Ljava/util/List;)Lcom/github/avrokotlin/avro4k/ListRecord; + public static synthetic fun copy$default (Lcom/github/avrokotlin/avro4k/ListRecord;Lorg/apache/avro/Schema;Ljava/util/List;ILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/ListRecord; + public fun equals (Ljava/lang/Object;)Z + public fun get (I)Ljava/lang/Object; + public fun get (Ljava/lang/String;)Ljava/lang/Object; + public fun getSchema ()Lorg/apache/avro/Schema; + public fun hashCode ()I + public fun put (ILjava/lang/Object;)V + public fun put (Ljava/lang/String;Ljava/lang/Object;)V + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/github/avrokotlin/avro4k/LongValueDecoder { + public abstract fun decodeLong (Lcom/github/avrokotlin/avro4k/AvroDecoder;)J +} + +public abstract interface class com/github/avrokotlin/avro4k/Record : org/apache/avro/generic/GenericRecord, org/apache/avro/specific/SpecificRecord { +} + +public abstract interface class com/github/avrokotlin/avro4k/ShortValueDecoder { + public abstract fun decodeShort (Lcom/github/avrokotlin/avro4k/AvroDecoder;)S +} + +public abstract interface class com/github/avrokotlin/avro4k/UnionDecoder : com/github/avrokotlin/avro4k/AvroDecoder { + public abstract fun decodeAndResolveUnion ()V +} + +public final class com/github/avrokotlin/avro4k/UnionDecoder$DefaultImpls { + public static fun decodeNullableSerializableValue (Lcom/github/avrokotlin/avro4k/UnionDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; + public static fun decodeSerializableValue (Lcom/github/avrokotlin/avro4k/UnionDecoder;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; +} + +public abstract interface class com/github/avrokotlin/avro4k/UnionEncoder : com/github/avrokotlin/avro4k/AvroEncoder { + public abstract fun encodeUnionIndex (I)V +} + +public final class com/github/avrokotlin/avro4k/UnionEncoder$DefaultImpls { + public static fun beginCollection (Lcom/github/avrokotlin/avro4k/UnionEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/CompositeEncoder; + public static fun encodeNotNullMark (Lcom/github/avrokotlin/avro4k/UnionEncoder;)V + public static fun encodeNullableSerializableValue (Lcom/github/avrokotlin/avro4k/UnionEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V + public static fun encodeSerializableValue (Lcom/github/avrokotlin/avro4k/UnionEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroDuration { + public static final field Companion Lcom/github/avrokotlin/avro4k/serializer/AvroDuration$Companion; + public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-pVg5ArA ()I + public final fun component2-pVg5ArA ()I + public final fun component3-pVg5ArA ()I + public final fun copy-zly0blg (III)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public static synthetic fun copy-zly0blg$default (Lcom/github/avrokotlin/avro4k/serializer/AvroDuration;IIIILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public fun equals (Ljava/lang/Object;)Z + public final fun getDays-pVg5ArA ()I + public final fun getMillis-pVg5ArA ()I + public final fun getMonths-pVg5ArA ()I + public fun hashCode ()I + public static final fun parse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public fun toString ()Ljava/lang/String; + public static final fun tryParse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroDuration$Companion { + public final fun parse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public final fun tryParse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroDurationParseException : kotlinx/serialization/SerializationException { + public fun (Ljava/lang/String;)V +} + +public abstract class com/github/avrokotlin/avro4k/serializer/AvroSerializer : com/github/avrokotlin/avro4k/serializer/AvroSchemaSupplier, kotlinx/serialization/KSerializer { + public fun (Ljava/lang/String;)V + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public abstract fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public abstract fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroSerializerKt { + public static final fun createSchema (Lcom/github/avrokotlin/avro4k/AvroFixed;Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;)Lorg/apache/avro/Schema; + public static final fun createSchema (Lcom/github/avrokotlin/avro4k/AvroStringable;)Lorg/apache/avro/Schema; + public static final fun getDecimal (Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;)Lcom/github/avrokotlin/avro4k/AvroDecimal; + public static final fun getFixed (Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;)Lcom/github/avrokotlin/avro4k/AvroFixed; + public static final fun getStringable (Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;)Lcom/github/avrokotlin/avro4k/AvroStringable; +} + +public final class com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/BigDecimalSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/math/BigDecimal; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/math/BigDecimal;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigDecimal;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/BigIntegerSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/math/BigInteger; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigInteger; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/math/BigInteger;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigInteger;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/ElementLocation { + public fun (Lkotlinx/serialization/descriptors/SerialDescriptor;I)V + public final fun component1 ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun component2 ()I + public final fun copy (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lcom/github/avrokotlin/avro4k/serializer/ElementLocation; + public static synthetic fun copy$default (Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;Lkotlinx/serialization/descriptors/SerialDescriptor;IILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/serializer/ElementLocation; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun getElementIndex ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/serializer/InstantSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/InstantSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Instant; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Instant;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Instant;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/InstantToMicroSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/InstantToMicroSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Instant; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Instant;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Instant;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaDurationSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/JavaDurationSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Duration; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Duration; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Duration;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaPeriodSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/JavaPeriodSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Period; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Period; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Period;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Period;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializersKt { + public static final fun getJavaStdLibSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaTimeSerializersKt { + public static final fun getJavaTimeSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; +} + +public final class com/github/avrokotlin/avro4k/serializer/LocalDateSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/LocalDateSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalDate; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDate; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalDate;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalDate;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/LocalDateTimeSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/LocalDateTimeSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalDateTime; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDateTime; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalDateTime;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalDateTime;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/LocalTimeSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/LocalTimeSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalTime; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalTime; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalTime;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalTime;)V +} + +public abstract interface class com/github/avrokotlin/avro4k/serializer/SchemaSupplierContext { + public abstract fun getConfiguration ()Lcom/github/avrokotlin/avro4k/AvroConfiguration; + public abstract fun getInlinedElements ()Ljava/util/List; +} + +public final class com/github/avrokotlin/avro4k/serializer/URLSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/URLSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/net/URL; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/net/URL;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/UUIDSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/UUIDSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/util/UUID; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/UUID; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/util/UUID;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/util/UUID;)V +} + diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..a5bc6556 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,69 @@ +# Kotlin Avro Benchmark + +This project contains a benchmark that compares the serialization / deserialization performance of the following avro libraries: + +- [Avro4k](https://github.com/avro-kotlin/avro4k/) +- [Jackson Avro](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/avro) +- [Avro (using ReflectData)](https://avro.apache.org/) + +Each benchmark is executed with the following configuration: +- Reading from a prepared byte array +- Writing to a null output stream +- All with the exact same schema generated by avro4k +- Generating a maximum of use cases: + - nullable fields + - unions + - arrays + - records + - enums + - primitives (string, int, float, double, boolean) + - logical types (date, timestamp-millis, char, uuid) +- not benchmarking uuid as jackson uses a different representation (fixed) than avro4k and apache avro (string) + +## Results + +Computer: Macbook air M2 + +``` +Benchmark Mode Cnt Score Error Units Relative Difference (%) +c.g.a.b.complex.Avro4kBenchmark.read thrpt 5 23897.142 ± 565.722 ops/s 0.00% +c.g.a.b.complex.ApacheAvroReflectBenchmark.read thrpt 5 21609.748 ± 194.480 ops/s -9.57% +c.g.a.b.complex.Avro4kGenericWithApacheAvroBenchmark.read thrpt 5 14003.544 ± 41.383 ops/s -41.42% + +c.g.a.b.complex.Avro4kBenchmark.write thrpt 5 54174.691 ± 79.533 ops/s 0.00% +c.g.a.b.complex.ApacheAvroReflectBenchmark.write thrpt 5 48289.132 ± 1718.797 ops/s -10.85% +c.g.a.b.complex.JacksonAvroBenchmark.write thrpt 5 36604.599 ± 242.069 ops/s -32.43% +c.g.a.b.complex.Avro4kGenericWithApacheAvroBenchmark.write thrpt 5 28092.785 ± 1267.990 ops/s -48.15% + + +c.g.a.b.simple.Avro4kSimpleBenchmark.read thrpt 5 174063.241 ± 7852.646 ops/s 0.00% +c.g.a.b.simple.ApacheAvroReflectSimpleBenchmark.read thrpt 5 159204.806 ± 413.516 ops/s -8.54% +c.g.a.b.simple.Avro4kGenericWithApacheAvroSimpleBenchmark.read thrpt 5 114511.133 ± 227.407 ops/s -34.23% +c.g.a.b.simple.JacksonAvroSimpleBenchmark.read thrpt 5 67811.540 ± 212.367 ops/s -61.05% + +c.g.a.b.simple.Avro4kSimpleBenchmark.write thrpt 5 459751.939 ± 46513.718 ops/s 0.00% +c.g.a.b.simple.ApacheAvroReflectSimpleBenchmark.write thrpt 5 355544.645 ± 14835.972 ops/s -22.65% +c.g.a.b.simple.Avro4kGenericWithApacheAvroSimpleBenchmark.write thrpt 5 190365.959 ± 1014.944 ops/s -58.60% +c.g.a.b.simple.JacksonAvroSimpleBenchmark.write thrpt 5 131492.564 ± 10888.843 ops/s -71.40% +``` + +> [!WARNING] +> JacksonAvroBenchmark.read is failing because of a bug in the library when combining kotlin and avro format. + +> [!NOTE] +> To add the relative difference, just ask to chatgpt "can you add another column in this benchmark that indicates the relative difference in percent regarding +> Avro4kDirectBenchmark:" + +## Run the benchmark locally + +Just execute the benchmark: + +```shell +../gradlew benchmark +``` + +You can get the results in the `build/reports/benchmarks/main` directory. + +## Other information + +Thanks for [@twinprime](https://github.com/twinprime) for this initiative. diff --git a/benchmark/api/benchmark.api b/benchmark/api/benchmark.api new file mode 100644 index 00000000..0aa10e44 --- /dev/null +++ b/benchmark/api/benchmark.api @@ -0,0 +1,95 @@ +public final class com/github/avrokotlin/benchmark/internal/CharJacksonDeserializer : com/fasterxml/jackson/databind/deser/std/StdDeserializer { + public fun ()V + public fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Character; + public synthetic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object; +} + +public final class com/github/avrokotlin/benchmark/internal/CharJacksonSerializer : com/fasterxml/jackson/databind/ser/std/StdSerializer { + public fun ()V + public fun acceptJsonFormatVisitor (Lcom/fasterxml/jackson/databind/jsonFormatVisitors/JsonFormatVisitorWrapper;Lcom/fasterxml/jackson/databind/JavaType;)V + public fun serialize (Ljava/lang/Character;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V + public synthetic fun serialize (Ljava/lang/Object;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V +} + +public abstract interface class com/github/avrokotlin/benchmark/internal/Partner { + public static final field Companion Lcom/github/avrokotlin/benchmark/internal/Partner$Companion; +} + +public final class com/github/avrokotlin/benchmark/internal/Partner$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/github/avrokotlin/benchmark/internal/SimpleDataClass { + public static final field Companion Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass$Companion; + public fun ()V + public fun (ZBSIJFDLjava/lang/String;[B)V + public final fun component1 ()Z + public final fun component2 ()B + public final fun component3 ()S + public final fun component4 ()I + public final fun component5 ()J + public final fun component6 ()F + public final fun component7 ()D + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()[B + public final fun copy (ZBSIJFDLjava/lang/String;[B)Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass; + public static synthetic fun copy$default (Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass;ZBSIJFDLjava/lang/String;[BILjava/lang/Object;)Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass; + public fun equals (Ljava/lang/Object;)Z + public final fun getBool ()Z + public final fun getByte ()B + public final fun getBytes ()[B + public final fun getDouble ()D + public final fun getFloat ()F + public final fun getInt ()I + public final fun getLong ()J + public final fun getShort ()S + public final fun getString ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public synthetic class com/github/avrokotlin/benchmark/internal/SimpleDataClass$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/github/avrokotlin/benchmark/internal/SimpleDataClass$Companion { + public final fun create ()Lcom/github/avrokotlin/benchmark/internal/SimpleDataClass; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/github/avrokotlin/benchmark/internal/SimpleDatasClass { + public static final field Companion Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass$Companion; + public fun ()V + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass; + public static synthetic fun copy$default (Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass;Ljava/util/List;ILjava/lang/Object;)Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public synthetic class com/github/avrokotlin/benchmark/internal/SimpleDatasClass$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/github/avrokotlin/benchmark/internal/SimpleDatasClass$Companion { + public final fun create (I)Lcom/github/avrokotlin/benchmark/internal/SimpleDatasClass; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts new file mode 100644 index 00000000..5a4e71fb --- /dev/null +++ b/benchmark/build.gradle.kts @@ -0,0 +1,44 @@ +import kotlinx.benchmark.gradle.JvmBenchmarkTarget + +plugins { + java + kotlin("jvm") version libs.versions.kotlin + id("org.jetbrains.kotlinx.benchmark") version "0.4.11" + kotlin("plugin.allopen") version libs.versions.kotlin + kotlin("plugin.serialization") version libs.versions.kotlin + kotlin("plugin.noarg") version libs.versions.kotlin +} + +allOpen { + annotation("org.openjdk.jmh.annotations.State") +} + +noArg { + annotation("kotlinx.serialization.Serializable") +} + +benchmark { + configurations { + named("main") { + reportFormat = "text" + } + } + targets { + register("main") { + this as JvmBenchmarkTarget + jmhVersion = "1.37" + } + } +} + +dependencies { + implementation("org.apache.commons:commons-lang3:3.14.0") + implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.11") + + val jacksonVersion = "2.17.1" + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-avro:$jacksonVersion") + + implementation(project(":")) +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt new file mode 100644 index 00000000..8b155976 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt @@ -0,0 +1,30 @@ +package com.github.avrokotlin.benchmark + +import com.github.avrokotlin.benchmark.complex.Avro4kBenchmark +import com.github.avrokotlin.benchmark.simple.JacksonAvroSimpleBenchmark + +internal object ManualProfilingWrite { + @JvmStatic + fun main(vararg args: String) { + JacksonAvroSimpleBenchmark().apply { + initTestData() + for (i in 0 until 1_000_000) { + if (i % 1_000 == 0) println("Iteration $i") + write() + read() + } + } + } +} +internal object ManualProfilingRead { + @JvmStatic + fun main(vararg args: String) { + Avro4kBenchmark().apply { + initTestData() + for (i in 0 until 1_000_000) { + if (i % 1_000 == 0) println("Iteration $i") + read() + } + } + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/ApacheAvroReflectBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/ApacheAvroReflectBenchmark.kt new file mode 100644 index 00000000..a0100bf9 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/ApacheAvroReflectBenchmark.kt @@ -0,0 +1,54 @@ +package com.github.avrokotlin.benchmark.complex + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import org.apache.avro.Conversions +import org.apache.avro.data.TimeConversions +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import org.apache.avro.reflect.ReflectData +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class ApacheAvroReflectBenchmark : SerializationBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + ReflectData.get().addLogicalTypeConversion(Conversions.UUIDConversion()) + ReflectData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = ReflectData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = ReflectData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + reader.read(null, decoder) + } + + @Benchmark + fun write() { + if (!writeMode) writeMode = true + writer.write(clients, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kBenchmark.kt new file mode 100644 index 00000000..b0ddb95f --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kBenchmark.kt @@ -0,0 +1,35 @@ +package com.github.avrokotlin.benchmark.complex + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromByteArray +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToStream +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import java.io.OutputStream + +internal class Avro4kBenchmark : SerializationBenchmark() { + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + Avro.decodeFromByteArray(schema, data) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + Avro.encodeToStream(schema, clients, OutputStream.nullOutputStream()) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kGenericWithApacheAvroBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kGenericWithApacheAvroBenchmark.kt new file mode 100644 index 00000000..edd9ac7e --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/Avro4kGenericWithApacheAvroBenchmark.kt @@ -0,0 +1,59 @@ +package com.github.avrokotlin.benchmark.complex + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromGenericData +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToGenericData +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import org.apache.avro.Conversions +import org.apache.avro.generic.GenericData +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class Avro4kGenericWithApacheAvroBenchmark : SerializationBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + GenericData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = GenericData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = GenericData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + val genericData = reader.read(null, decoder) + Avro.decodeFromGenericData(schema, genericData) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + val genericData = Avro.encodeToGenericData(schema, clients) + writer.write(genericData, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/JacksonAvroBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/JacksonAvroBenchmark.kt new file mode 100644 index 00000000..a8cfcfd2 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/JacksonAvroBenchmark.kt @@ -0,0 +1,62 @@ +package com.github.avrokotlin.benchmark.complex + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.dataformat.avro.AvroMapper +import com.fasterxml.jackson.dataformat.avro.AvroSchema +import com.fasterxml.jackson.dataformat.avro.jsr310.AvroJavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import java.io.OutputStream + + +internal class JacksonAvroBenchmark : SerializationBenchmark() { + lateinit var writer: ObjectWriter + lateinit var reader: ObjectReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + writer = Clients::class.java.createWriter() + reader = Clients::class.java.createReader() + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + reader.readValue(data) + } + + @Benchmark + fun write() { + if (!writeMode) writeMode = true + writer.writeValue(OutputStream.nullOutputStream(), clients) + } + + private fun Class.createWriter(): ObjectWriter { + val mapper = avroMapper() + + return mapper.writer(AvroSchema(schema)).forType(this) + } + + private fun Class.createReader(): ObjectReader { + val mapper = avroMapper() + + return mapper.reader(AvroSchema(schema)).forType(this) + } + + private fun avroMapper(): ObjectMapper = AvroMapper() + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .registerKotlinModule() + .registerModule(AvroJavaTimeModule()) +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/SerializationBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/SerializationBenchmark.kt new file mode 100644 index 00000000..a236a477 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/complex/SerializationBenchmark.kt @@ -0,0 +1,29 @@ +package com.github.avrokotlin.benchmark.complex + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.schema +import com.github.avrokotlin.benchmark.internal.Clients +import com.github.avrokotlin.benchmark.internal.ClientsGenerator +import kotlinx.benchmark.* +import java.util.concurrent.TimeUnit + + +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@BenchmarkMode(Mode.Throughput) +@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +internal abstract class SerializationBenchmark { + lateinit var clients: Clients + val schema = Avro.schema() + + @Setup + fun initTestData() { + setup() + clients = ClientsGenerator.generate(15) + prepareBinaryData() + } + + abstract fun setup() + + abstract fun prepareBinaryData() +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt new file mode 100644 index 00000000..ac63ea02 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt @@ -0,0 +1,103 @@ +package com.github.avrokotlin.benchmark.internal + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.github.avrokotlin.avro4k.AvroStringable +import com.github.avrokotlin.avro4k.serializer.InstantSerializer +import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDate + +@Serializable +internal data class Clients( + val clients: List +) + +class CharJacksonSerializer : StdSerializer(Char::class.java) { + override fun serialize(value: Char?, gen: JsonGenerator, provider: SerializerProvider) { + value?.code?.let { gen.writeNumber(it) } ?: gen.writeNull() + } + + override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType) { + visitor.expectIntegerFormat(typeHint).numberType(JsonParser.NumberType.INT) + } +} + +class CharJacksonDeserializer : StdDeserializer(Char::class.java) { + override fun deserialize(p0: JsonParser, p1: com.fasterxml.jackson.databind.DeserializationContext): Char { + return p0.intValue.toChar() + } +} + +@Serializable +internal data class Client( + val id: Long, + val index: Int, + val isActive: Boolean, + @Contextual + @AvroStringable + @JsonFormat(shape = JsonFormat.Shape.STRING) + val balance: BigDecimal?, + val picture: ByteArray?, + val age: Int, + val eyeColor: EyeColor?, + val name: String?, + @JsonSerialize(using = CharJacksonSerializer::class) + @JsonDeserialize(using = CharJacksonDeserializer::class) + val gender: Char?, + val company: String?, + val emails: Array, + val phones: LongArray, + val address: String?, + val about: String?, + @Serializable(with = LocalDateSerializer::class) + val registered: LocalDate?, + val latitude: Double, + val longitude: Float, + val tags: List, + val partner: Partner?, + val map: Map, +) + +@Serializable +internal enum class EyeColor { + BROWN, + BLUE, + GREEN; +} + +@Serializable +sealed interface Partner + +@Serializable +internal class GoodPartner( + val id: Long, + val name: String, + @Serializable(with = InstantSerializer::class) + val since: Instant +) : Partner + +@Serializable +internal class BadPartner( + val id: Long, + val name: String, + @Serializable(with = InstantSerializer::class) + val since: Instant +) : Partner + +@Serializable +internal enum class Stranger : Partner { + KNOWN_STRANGER, + UNKNOWN_STRANGER +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt new file mode 100644 index 00000000..6cb25ba3 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt @@ -0,0 +1,55 @@ +package com.github.avrokotlin.benchmark.internal + +import java.time.Instant +import java.time.LocalDate +import kotlin.math.absoluteValue + +internal object ClientsGenerator { + fun generate(size: Int) = Clients( + buildList { + repeat(size) { index -> + add(Client( + partner = when (RandomUtils.nextInt(4)) { + 0 -> GoodPartner(RandomUtils.nextLong(), RandomUtils.randomAlphabetic(30), Instant.ofEpochMilli(RandomUtils.nextLong())) + 1 -> BadPartner(RandomUtils.nextLong(), RandomUtils.randomAlphabetic(30), Instant.ofEpochMilli(RandomUtils.nextLong().absoluteValue)) + 2 -> if (RandomUtils.nextBoolean()) Stranger.KNOWN_STRANGER else Stranger.UNKNOWN_STRANGER + 3 -> null + else -> throw IllegalStateException("Unexpected value") + }, + id = RandomUtils.nextLong().absoluteValue, + index = RandomUtils.nextInt(0, Int.MAX_VALUE), + isActive = RandomUtils.nextBoolean(), + balance = if (index % 2 == 0) null else RandomUtils.randomBigDecimal(), + picture = if (index % 3 == 0) null else RandomUtils.randomBytes(4048), + age = RandomUtils.nextInt(0, 100), + eyeColor = if (index % 2 == 0) null else EyeColor.entries[RandomUtils.nextInt(3)], + name = if (index % 2 == 0) null else RandomUtils.randomAlphanumeric(20), + gender = if (index % 2 == 0) null else if (RandomUtils.nextBoolean()) 'M' else 'F', + company = if (index % 2 == 0) null else RandomUtils.randomAlphanumeric(20), + emails = RandomUtils.stringArray(RandomUtils.nextInt(5, 10), 10), + phones = RandomUtils.longArray(RandomUtils.nextInt(5, 10)), + address = if (index % 2 == 0) null else RandomUtils.randomAlphanumeric(20), + about = RandomUtils.randomAlphanumeric(20), + registered = if (index % 3 == 0) null else + LocalDate.of( + 1900 + RandomUtils.nextInt(110), + 1 + RandomUtils.nextInt(12), + 1 + RandomUtils.nextInt(28) + ), + latitude = RandomUtils.nextDouble(0.0, 90.0), + longitude = RandomUtils.nextFloat(0.0f, 180.0f), + tags = buildList { + repeat(RandomUtils.nextInt(5, 25)) { + add(if (it % 2 == 0) null else RandomUtils.randomAlphanumeric(10)) + } + }, + map = buildMap { + repeat(10) { + put(RandomUtils.randomAlphanumeric(10), RandomUtils.randomAlphanumeric(10)) + } + } + )) + } + } + ) +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt new file mode 100644 index 00000000..ffc1dbda --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt @@ -0,0 +1,111 @@ +package com.github.avrokotlin.benchmark.internal + +import org.apache.commons.lang3.RandomStringUtils +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlin.math.abs +import kotlin.random.Random +import kotlin.random.asJavaRandom + +internal object RandomUtils { + private val RANDOM: Random = Random(139793881379292435L) + + fun randomAlphabetic(count: Int): String { + return random(count, true, false) + } + + fun randomAlphanumeric(count: Int): String { + return random(count, true, true) + } + + fun randomNumeric(count: Int): String { + return random(count, false, true) + } + + fun random( + count: Int, + letters: Boolean, + numbers: Boolean, + start: Int = 0, + end: Int = 0, + chars: CharArray? = null + ): String { + return RandomStringUtils.random(count, start, end, letters, numbers, chars, RANDOM.asJavaRandom()) + } + + fun randomBytes(count: Int) : ByteArray = RANDOM.nextBytes(count) + + fun randomBigDecimal(): BigDecimal { + return BigDecimal.valueOf(RANDOM.nextDouble()).multiply(BigDecimal(1000)).setScale(4, RoundingMode.HALF_UP) + } + + fun nextUUID(): UUID { + return UUID(RANDOM.nextLong(), RANDOM.nextLong()) + } + + fun nextLong(): Long { + return RANDOM.nextLong() + } + + fun nextBoolean(): Boolean { + return RANDOM.nextBoolean() + } + + fun longArray(size: Int): LongArray { + val arr = LongArray(size) + for (i in 0.. { + val arr = mutableListOf() + for (i in 0..= startInclusive) + assert(startInclusive >= 0) + return if (startInclusive == endExclusive) { + startInclusive + } else startInclusive + RANDOM.nextInt( + endExclusive - startInclusive + ) + } + + fun nextDouble(startInclusive: Double, endInclusive: Double): Double { + assert(endInclusive >= startInclusive) + assert(startInclusive >= 0) + return if (startInclusive == endInclusive) { + startInclusive + } else startInclusive + (endInclusive - startInclusive) * RANDOM.nextDouble() + } + + fun nextDouble(): Double { + return RANDOM.nextDouble() + } + fun nextFloat(): Float { + return RANDOM.nextFloat() + } + fun nextFloat(startInclusive: Float, endInclusive: Float): Float { + assert(endInclusive >= startInclusive) + assert(startInclusive >= 0) + return if (startInclusive == endInclusive) { + startInclusive + } else startInclusive + (endInclusive - startInclusive) * RANDOM.nextFloat() + } +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/SimpleDataClass.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/SimpleDataClass.kt new file mode 100644 index 00000000..83847a0b --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/SimpleDataClass.kt @@ -0,0 +1,41 @@ +package com.github.avrokotlin.benchmark.internal + +import kotlinx.serialization.Serializable + +@Serializable +data class SimpleDataClass( + val bool: Boolean, + val byte: Byte, + val short: Short, + val int: Int, + val long: Long, + val float: Float, + val double: Double, + val string: String, + val bytes: ByteArray, +) { + companion object { + fun create() = SimpleDataClass( + bool = RandomUtils.nextBoolean(), + byte = RandomUtils.nextInt(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte(), + short = RandomUtils.nextInt(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort(), + int = RandomUtils.nextInt(), + long = RandomUtils.nextLong(), + float = RandomUtils.nextFloat(), + double = RandomUtils.nextDouble(), + string = RandomUtils.randomAlphanumeric(25), + bytes = RandomUtils.randomBytes(50), + ) + } +} + +@Serializable +data class SimpleDatasClass( + val data: List +) { + companion object { + fun create(size: Int) = SimpleDatasClass( + data = List(size) { SimpleDataClass.create() } + ) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json new file mode 100644 index 00000000..188e8495 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json @@ -0,0 +1,245 @@ +{ + "type": "record", + "name": "Clients", + "namespace": "com.github.avrokotlin.benchmark.internal", + "fields": [ + { + "name": "clients", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "Client", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "index", + "type": "int" + }, + { + "name": "guid", + "type": [ + "null", + { + "type": "string", + "logicalType": "uuid" + } + ], + "default": null + }, + { + "name": "isActive", + "type": "boolean" + }, + { + "name": "balance", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "picture", + "type": [ + "null", + "bytes" + ], + "default": null + }, + { + "name": "age", + "type": "int" + }, + { + "name": "eyeColor", + "type": [ + "null", + { + "type": "enum", + "name": "EyeColor", + "symbols": [ + "BROWN", + "BLUE", + "GREEN" + ] + } + ], + "default": null + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "gender", + "type": [ + "null", + { + "type": "int", + "logicalType": "char" + } + ], + "default": null + }, + { + "name": "company", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "emails", + "type": { + "type": "array", + "items": "string" + } + }, + { + "name": "phones", + "type": { + "type": "array", + "items": "long" + } + }, + { + "name": "address", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "about", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "registered", + "type": [ + "null", + { + "type": "int", + "logicalType": "date" + } + ], + "default": null + }, + { + "name": "latitude", + "type": "double" + }, + { + "name": "longitude", + "type": "float" + }, + { + "name": "tags", + "type": { + "type": "array", + "items": [ + "null", + "string" + ] + } + }, + { + "name": "partners", + "type": { + "type": "array", + "items": [ + { + "type": "record", + "name": "BadPartner", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + }, + { + "type": "record", + "name": "GoodPartner", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + }, + { + "type": "enum", + "name": "Stranger", + "symbols": [ + "KNOWN_STRANGER", + "UNKNOWN_STRANGER" + ] + } + ] + } + }, + { + "name": "map", + "type": { + "type": "map", + "values": "string" + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json new file mode 100644 index 00000000..eac886bb --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json @@ -0,0 +1,260 @@ +{ + "type": "record", + "name": "Clients", + "namespace": "com.github.avrokotlin.benchmark.internal", + "fields": [ + { + "name": "clients", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "Client", + "fields": [ + { + "name": "about", + "type": [ + "null", + "string" + ] + }, + { + "name": "address", + "type": [ + "null", + "string" + ] + }, + { + "name": "age", + "type": { + "type": "int", + "java-class": "java.lang.Integer" + } + }, + { + "name": "balance", + "type": [ + "null", + { + "type": "string", + "java-class": "java.math.BigDecimal" + } + ] + }, + { + "name": "company", + "type": [ + "null", + "string" + ] + }, + { + "name": "emails", + "type": { + "type": "array", + "items": "string", + "java-class": "[Ljava.lang.String;" + } + }, + { + "name": "eyeColor", + "type": [ + "null", + { + "type": "enum", + "name": "EyeColor", + "symbols": [ + "BROWN", + "BLUE", + "GREEN" + ] + } + ] + }, + { + "name": "gender", + "type": [ + "null", + { + "type": "int", + "java-class": "java.lang.Character" + } + ] + }, + { + "name": "guid", + "type": [ + "null", + { + "type": "fixed", + "name": "UUID", + "namespace": "java.util", + "doc": "", + "size": 16 + } + ] + }, + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "index", + "type": { + "type": "int", + "java-class": "java.lang.Integer" + } + }, + { + "name": "isActive", + "type": "boolean" + }, + { + "name": "latitude", + "type": { + "type": "double", + "java-class": "java.lang.Double" + } + }, + { + "name": "longitude", + "type": { + "type": "float", + "java-class": "java.lang.Float" + } + }, + { + "name": "map", + "type": { + "type": "map", + "values": "string" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "partners", + "type": { + "type": "array", + "items": [ + { + "type": "record", + "name": "BadPartner", + "fields": [ + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "java-class": "java.time.Instant" + } + ] + } + ] + }, + { + "type": "record", + "name": "GoodPartner", + "fields": [ + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "java-class": "java.time.Instant" + } + ] + } + ] + }, + { + "type": "enum", + "name": "Stranger", + "symbols": [ + "KNOWN_STRANGER", + "UNKNOWN_STRANGER" + ] + } + ] + } + }, + { + "name": "phones", + "type": { + "type": "array", + "items": "long", + "java-class": "[J" + } + }, + { + "name": "picture", + "type": [ + "null", + { + "type": "bytes", + "java-class": "[B" + } + ] + }, + { + "name": "registered", + "type": [ + "null", + { + "type": "int", + "java-class": "java.time.LocalDate" + } + ] + }, + { + "name": "tags", + "type": { + "type": "array", + "items": "string" + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/ApacheAvroReflectSimpleBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/ApacheAvroReflectSimpleBenchmark.kt new file mode 100644 index 00000000..c314983d --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/ApacheAvroReflectSimpleBenchmark.kt @@ -0,0 +1,54 @@ +package com.github.avrokotlin.benchmark.simple + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.SimpleDatasClass +import kotlinx.benchmark.Benchmark +import org.apache.avro.Conversions +import org.apache.avro.data.TimeConversions +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import org.apache.avro.reflect.ReflectData +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class ApacheAvroReflectSimpleBenchmark : SerializationSimpleBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + ReflectData.get().addLogicalTypeConversion(Conversions.UUIDConversion()) + ReflectData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = ReflectData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = ReflectData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + reader.read(null, decoder) + } + + @Benchmark + fun write() { + if (!writeMode) writeMode = true + writer.write(clients, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kGenericWithApacheAvroSimpleBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kGenericWithApacheAvroSimpleBenchmark.kt new file mode 100644 index 00000000..87b733c1 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kGenericWithApacheAvroSimpleBenchmark.kt @@ -0,0 +1,59 @@ +package com.github.avrokotlin.benchmark.simple + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromGenericData +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToGenericData +import com.github.avrokotlin.benchmark.internal.SimpleDatasClass +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import org.apache.avro.Conversions +import org.apache.avro.generic.GenericData +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class Avro4kGenericWithApacheAvroSimpleBenchmark : SerializationSimpleBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + GenericData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = GenericData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = GenericData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + val genericData = reader.read(null, decoder) + Avro.decodeFromGenericData(schema, genericData) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + val genericData = Avro.encodeToGenericData(schema, clients) + writer.write(genericData, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kSimpleBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kSimpleBenchmark.kt new file mode 100644 index 00000000..9c4a0fab --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/Avro4kSimpleBenchmark.kt @@ -0,0 +1,35 @@ +package com.github.avrokotlin.benchmark.simple + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromByteArray +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToStream +import com.github.avrokotlin.benchmark.internal.SimpleDatasClass +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import java.io.OutputStream + +internal class Avro4kSimpleBenchmark : SerializationSimpleBenchmark() { + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + Avro.decodeFromByteArray(schema, data) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + Avro.encodeToStream(schema, clients, OutputStream.nullOutputStream()) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/JacksonAvroSimpleBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/JacksonAvroSimpleBenchmark.kt new file mode 100644 index 00000000..dc7599ed --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/JacksonAvroSimpleBenchmark.kt @@ -0,0 +1,59 @@ +package com.github.avrokotlin.benchmark.simple + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.dataformat.avro.AvroMapper +import com.fasterxml.jackson.dataformat.avro.AvroSchema +import com.fasterxml.jackson.dataformat.avro.jsr310.AvroJavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.SimpleDatasClass +import kotlinx.benchmark.Benchmark +import java.io.OutputStream + + +internal class JacksonAvroSimpleBenchmark : SerializationSimpleBenchmark() { + lateinit var writer: ObjectWriter + lateinit var reader: ObjectReader + + var data: ByteArray? = null + + override fun setup() { + writer = SimpleDatasClass::class.java.createWriter() + reader = SimpleDatasClass::class.java.createReader() + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + reader.readValue(data) + } + + @Benchmark + fun write() { + writer.writeValue(OutputStream.nullOutputStream(), clients) + } + + private fun Class.createWriter(): ObjectWriter { + val mapper = avroMapper() + + return mapper.writer(AvroSchema(schema)).forType(this) + } + + private fun Class.createReader(): ObjectReader { + val mapper = avroMapper() + + return mapper.reader(AvroSchema(schema)).forType(this) + } + + private fun avroMapper(): ObjectMapper = AvroMapper() + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .registerKotlinModule() + .registerModule(AvroJavaTimeModule()) +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/SerializationSimpleBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/SerializationSimpleBenchmark.kt new file mode 100644 index 00000000..d317b1e2 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/simple/SerializationSimpleBenchmark.kt @@ -0,0 +1,28 @@ +package com.github.avrokotlin.benchmark.simple + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.schema +import com.github.avrokotlin.benchmark.internal.SimpleDatasClass +import kotlinx.benchmark.* +import java.util.concurrent.TimeUnit + + +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@BenchmarkMode(Mode.Throughput) +@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +internal abstract class SerializationSimpleBenchmark { + lateinit var clients: SimpleDatasClass + val schema = Avro.schema() + + @Setup + fun initTestData() { + setup() + clients = SimpleDatasClass.create(25) + prepareBinaryData() + } + + abstract fun setup() + + abstract fun prepareBinaryData() +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cf2affa8..acd44abe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,164 +1,162 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - repositories { - mavenCentral() - mavenLocal() - google() - gradlePluginPortal() - } -} - plugins { - java - id("java-library") - kotlin("jvm") version libs.versions.kotlin - kotlin("plugin.serialization") version libs.versions.kotlin - id("maven-publish") - signing - alias(libs.plugins.dokka) - alias(libs.plugins.kotest) - alias(libs.plugins.github.versions) - alias(libs.plugins.nexus.publish) -} - -repositories { - mavenCentral() - google() + java + id("java-library") + kotlin("jvm") version libs.versions.kotlin + kotlin("plugin.serialization") version libs.versions.kotlin + id("maven-publish") + signing + alias(libs.plugins.dokka) + alias(libs.plugins.kover) + alias(libs.plugins.kotest) + alias(libs.plugins.github.versions) + alias(libs.plugins.nexus.publish) + alias(libs.plugins.spotless) + alias(libs.plugins.binary.compatibility.validator) } tasks { - javadoc { - } + javadoc } group = "com.github.avro-kotlin.avro4k" version = Ci.publishVersion dependencies { - api(libs.apache.avro) - api(libs.kotlinx.serialization.core) - implementation(libs.kotlinx.serialization.json) - implementation(kotlin("reflect")) - implementation(libs.xerial.snappy) - testImplementation(libs.kotest.junit5) - testImplementation(libs.kotest.core) - testImplementation(libs.kotest.json) - testImplementation(libs.kotest.property) + api(libs.apache.avro) + api(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.okio) + testImplementation(libs.kotest.junit5) + testImplementation(libs.kotest.core) + testImplementation(libs.kotest.json) + testImplementation(libs.kotest.property) } -tasks.withType().configureEach { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.5" - kotlinOptions.languageVersion = "1.5" - kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" +kotlin { + explicitApi() + + compilerOptions { + optIn = listOf("kotlin.RequiresOptIn", "kotlinx.serialization.ExperimentalSerializationApi") + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9) + freeCompilerArgs = listOf("-Xcontext-receivers") + } } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - withJavadocJar() - withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withJavadocJar() + withSourcesJar() } tasks.named("test") { - useJUnitPlatform() - filter { - isFailOnNoMatchingTests = false - } - testLogging { - showExceptions = true - showStandardStreams = true - events = setOf( - org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, - org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED - ) - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - } + useJUnitPlatform() + filter { + isFailOnNoMatchingTests = false + } + testLogging { + showExceptions = true + showStandardStreams = true + events = + setOf( + org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, + org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED + ) + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } } tasks.named("javadocJar") { - from(tasks.named("dokkaJavadoc")) + from(tasks.named("dokkaJavadoc")) } -//Configuration for publication on maven central -val signingKey: String? by project -val signingPassword: String? by project - -val publications: PublicationContainer = (extensions.getByName("publishing") as PublishingExtension).publications - -signing { - useGpgCmd() - if (signingKey != null && signingPassword != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - if (Ci.isRelease) { - sign(publications) - } +publishing { + publications { + create("mavenJava") { + from(components["java"]) + pom { + val projectUrl = "https://github.com/avro-kotlin/avro4k" + name.set("avro4k-core") + description.set("Avro binary format support for kotlin, built on top of kotlinx-serialization") + url.set(projectUrl) + + scm { + connection.set("scm:git:$projectUrl") + developerConnection.set("scm:git:$projectUrl") + url.set(projectUrl) + } + + licenses { + license { + name.set("Apache-2.0") + url.set("https://opensource.org/licenses/Apache-2.0") + } + } + + developers { + developer { + id.set("thake") + name.set("Thorsten Hake") + email.set("mail@thorsten-hake.com") + } + developer { + id.set("chuckame") + name.set("Antoine Michaud") + email.set("contact@antoine-michaud.fr") + } + } + } + } + } } nexusPublishing { - this.repositories { - sonatype() - } + repositories { + sonatype() + } } -publishing { - publications { - register("mavenJava", MavenPublication::class) { - from(components["java"]) - pom { - val projectUrl = "https://github.com/avro-kotlin/avro4k" - name.set("avro4k-core") - description.set("Avro format support for kotlinx.serialization") - url.set(projectUrl) - - scm { - connection.set("scm:git:$projectUrl") - developerConnection.set("scm:git:$projectUrl") - url.set(projectUrl) - } - licenses { - license { - name.set("Apache-2.0") - url.set("https://opensource.org/licenses/Apache-2.0") - } - } - - developers { - developer { - id.set("thake") - name.set("Thorsten Hake") - email.set("mail@thorsten-hake.com") - } - developer { - id.set("chuckame") - name.set("Antoine Michaud") - email.set("contact@antoine-michaud.fr") - } - } - } - } - } +signing { + if (Ci.isRelease) { + val signingKey: String? by project + val signingPassword: String? by project + + if (signingKey != null && signingPassword != null) { + useInMemoryPgpKeys(signingKey, signingPassword) + } else { + throw IllegalStateException("No signing key or password found") + } + sign(publishing.publications) + } } -fun Project.publishing(action: PublishingExtension.() -> Unit) = - configure(action) - -fun Project.signing(configure: SigningExtension.() -> Unit): Unit = - configure(configure) - object Ci { - // this is the version used for building snapshots - // .buildnumber-snapshot will be appended - private const val snapshotBase = "1.9.0" + // this is the version used for building snapshots + // .buildnumber-snapshot will be appended + private const val SNAPSHOT_BASE = "1.9.0" - private val githubBuildNumber = System.getenv("GITHUB_RUN_NUMBER") + private val githubBuildNumber = System.getenv("GITHUB_RUN_NUMBER") - private val snapshotVersion = when (githubBuildNumber) { - null -> "$snapshotBase-SNAPSHOT" - else -> "$snapshotBase.${githubBuildNumber}-SNAPSHOT" - } + private val snapshotVersion = + when (githubBuildNumber) { + null -> "$SNAPSHOT_BASE-SNAPSHOT" + else -> "$SNAPSHOT_BASE.$githubBuildNumber-SNAPSHOT" + } - private val releaseVersion = System.getenv("RELEASE_VERSION") + private val releaseVersion = System.getenv("RELEASE_VERSION") - val isRelease = releaseVersion != null - val publishVersion = releaseVersion ?: snapshotVersion + val isRelease = releaseVersion != null + val publishVersion = releaseVersion ?: snapshotVersion } + +spotless { + kotlin { + ktlint() + } + json { + target("src/test/resources/**.json") + prettier() + } + kotlinGradle { + ktlint() + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..b82aa23a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c94df8f..b73d99e2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,38 +1,58 @@ pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("com.gradle.enterprise") version ("3.16.2") } rootProject.name = "avro4k-core" -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - version("kotlin", "1.8.20") - 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" - 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" - 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("github-versions", "com.github.ben-manes.versions").version("0.46.0") - plugin("nexus-publish", "io.github.gradle-nexus.publish-plugin").version("1.3.0") - } - } - repositories { - mavenCentral() - } +include("benchmark") + +gradleEnterprise { + if (System.getenv("CI") != null) { + buildScan { + publishAlways() + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + } + } } + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + version("kotlin", "2.0.0") + version("jvm", "21") + + library("apache-avro", "org.apache.avro", "avro").version("1.11.3") + library("okio", "com.squareup.okio", "okio").version("3.9.0") + + val kotlinxSerialization = "1.7.0" + 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.9.1" + 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.9.20") + plugin("kotest", "io.kotest").version("0.4.11") + plugin("github-versions", "com.github.ben-manes.versions").version("0.51.0") + plugin("nexus-publish", "io.github.gradle-nexus.publish-plugin").version("2.0.0") + plugin("spotless", "com.diffplug.spotless").version("6.25.0") + plugin("kover", "org.jetbrains.kotlinx.kover").version("0.8.1") + plugin("binary-compatibility-validator", "org.jetbrains.kotlinx.binary-compatibility-validator").version("0.14.0") + } + } + @Suppress("UnstableApiUsage") + repositories { + mavenCentral() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt deleted file mode 100644 index 4cb5d5de..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AnnotationExtractor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.avrokotlin.avro4k - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor - -@ExperimentalSerializationApi -class AnnotationExtractor(private val annotations: List) { - - companion object { - fun entity(descriptor: SerialDescriptor) = AnnotationExtractor( - descriptor.annotations) - - operator fun invoke(descriptor: SerialDescriptor, index: Int): AnnotationExtractor = - AnnotationExtractor(descriptor.getElementAnnotations(index)) - } - - 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 - fun aliases(): List = (annotations.firstNotNullOfOrNull { it as? AvroAlias }?.value ?: emptyArray()).asList() + (annotations.firstNotNullOfOrNull {it as? AvroAliases}?.value ?: emptyArray()) - fun props(): List> = annotations.filterIsInstance().map { it.key to it.value } - fun jsonProps(): List> = annotations.filterIsInstance().map { it.key to it.jsonValue } - fun default(): String? = annotations.filterIsInstance().firstOrNull()?.value - fun enumDefault(): String? = annotations.filterIsInstance().firstOrNull()?.value -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt new file mode 100644 index 00000000..02f28a16 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt @@ -0,0 +1,121 @@ +package com.github.avrokotlin.avro4k + +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialInfo +import org.intellij.lang.annotations.Language + +/** + * Adds a property to the Avro schema or field. Its value could be any valid JSON or just a string. + * + * When annotated on a value class or its underlying field, the props are applied to the underlying type. + * + * Only works with classes (data, enum & object types) and class properties (not enum values). + * Fails at runtime when used in value classes wrapping a named schema (fixed, enum or record). + */ +@SerialInfo +@Repeatable +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) +public annotation class AvroProp( + val key: String, + @Language("JSON") val value: String, +) + +/** + * Adds documentation to: + * - a record's field when annotated on a data class property + * - a record when annotated on a data class or object + * - an enum type when annotated on an enum class + * + * Only works with classes (data, enum & object types) and class properties (not enum values). Ignored in value classes. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) +public 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. + * + * Only works with classes (data, enum & object types) and class properties (not enum values). Ignored in value classes. + * + * @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) +public annotation class AvroAlias(vararg val value: String) + +/** + * Sets the annotated property as a `string` type when inferring the class' schema. Takes precedence over any other schema modifier built-in annotation, except + * the fields referring to a custom serializer implementing [AvroSerializer] where you'll need to handle the annotation. + * + * If the given property is not string-able, the schema will be generated as a string, but it will fail at runtime during serialization or deserialization. + * You may need to provide a custom serializer for the given property to handle the string specifications. + * + * Only works with class properties for the following inferred schemas: + * - `string` + * - `boolean` + * - `int` + * - `long` + * - `float` + * - `double` + * - `bytes` & `fixed` (will take the fixed bytes as UTF-8 string, or custom toString/parse for logical types) + * - works also for all the nullable types of the above + * The rest will fail, except if your custom serializers handle the string type. + */ +@SerialInfo +@ExperimentalSerializationApi +@Target(AnnotationTarget.PROPERTY) +public annotation class AvroStringable + +/** + * To be used with [BigDecimalSerializer] to specify the scale, precision, type and rounding mode of the decimal value. + * + * Can be used with [AvroFixed] to serialize value as a fixed type. + * + * Only works with [java.math.BigDecimal] property type. + */ +@SerialInfo +@ExperimentalSerializationApi +@Target(AnnotationTarget.PROPERTY) +public annotation class AvroDecimal( + val scale: Int, + val precision: Int, +) + +/** + * Indicates that the annotated property should be encoded as an Avro fixed type. + * + * Only works with [ByteArray], [String] and [java.math.BigDecimal] property types. + * + * @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) +public annotation class AvroFixed(val size: Int) + +/** + * Sets the default avro value for a record's field. + * + * - Records and maps have to be represented as a json object + * - Arrays have to be represented as a json array + * - Nulls have to be represented as a json `null`. To set the string `"null"`, don't forget to quote the string, example: `""""null""""` or `"\"null\""`. + * - Any non json content will be treated as a string + * + * Only works with data class properties (not enum values). Ignored in value classes. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +public annotation class AvroDefault( + @Language("JSON") val value: String, +) + +/** + * Sets the enum default value when decoded an unknown enum value. + * + * Only works with enum classes. + */ +@SerialInfo +@ExperimentalSerializationApi +@Target(AnnotationTarget.PROPERTY) +public annotation class AvroEnumDefault \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt index aad93b3b..0112f21d 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt @@ -1,280 +1,152 @@ -@file:Suppress("DEPRECATION") - package com.github.avrokotlin.avro4k -import com.github.avrokotlin.avro4k.decoder.RootRecordDecoder -import com.github.avrokotlin.avro4k.encoder.RootRecordEncoder -import com.github.avrokotlin.avro4k.io.AvroDecodeFormat -import com.github.avrokotlin.avro4k.io.AvroEncodeFormat -import com.github.avrokotlin.avro4k.io.AvroFormat -import com.github.avrokotlin.avro4k.io.AvroInputStream -import com.github.avrokotlin.avro4k.io.AvroOutputStream -import com.github.avrokotlin.avro4k.schema.schemaFor -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer +import com.github.avrokotlin.avro4k.internal.EnumResolver +import com.github.avrokotlin.avro4k.internal.PolymorphicResolver +import com.github.avrokotlin.avro4k.internal.RecordResolver +import com.github.avrokotlin.avro4k.internal.schema.ValueVisitor +import com.github.avrokotlin.avro4k.serializer.JavaStdLibSerializersModule +import com.github.avrokotlin.avro4k.serializer.JavaTimeSerializersModule import kotlinx.serialization.BinaryFormat import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.contextual +import kotlinx.serialization.modules.overwriteWith +import kotlinx.serialization.modules.plus +import kotlinx.serialization.serializer +import okio.Buffer import org.apache.avro.Schema -import org.apache.avro.file.CodecFactory -import org.apache.avro.generic.GenericRecord -import java.io.* -import java.nio.ByteBuffer -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths - -open class AvroInputStreamBuilder( - private val converter: (Any) -> T - ) { - /** - * The format that should be used to decode the bytes from the input stream. - */ - var decodeFormat : AvroDecodeFormat = defaultDecodeFormat - @Deprecated("please use decodeFormat to specify the format") - var format: AvroFormat = AvroFormat.DataFormat - @Deprecated("please use decodeFormat to specify the format") - var writerSchema: Schema? = null - @Deprecated("please use decodeFormat to specify the format") - var readerSchema: Schema? = null - - companion object { - val defaultDecodeFormat = AvroDecodeFormat.Data(null, null) - } - - private val derivedDecodeFormat : AvroDecodeFormat - get() { - return when(format){ - is AvroFormat.JsonFormat -> { - val wschema = writerSchema ?: error("Writer schema needs to be supplied for Json format") - AvroDecodeFormat.Json(wschema, readerSchema?:wschema) - } - is AvroFormat.BinaryFormat -> { - val wschema = writerSchema ?: error("Writer schema needs to be supplied for Binary format") - AvroDecodeFormat.Binary(wschema, readerSchema?:wschema) - } - is AvroFormat.DataFormat -> AvroDecodeFormat.Data(writerSchema, readerSchema) - } - } - - fun from(path: Path): AvroInputStream = from(Files.newInputStream(path)) - fun from(path: String): AvroInputStream = from(Paths.get(path)) - fun from(file: File): AvroInputStream = from(file.toPath()) - fun from(bytes: ByteArray): AvroInputStream = from(ByteArrayInputStream(bytes)) - fun from(buffer: ByteBuffer): AvroInputStream = from(ByteArrayInputStream(buffer.array())) - - fun from(source: InputStream): AvroInputStream { - val currentDerivedFormat = derivedDecodeFormat - val decodeFormatToUse = if(defaultDecodeFormat != currentDerivedFormat){ - currentDerivedFormat - }else { - decodeFormat - } - return decodeFormatToUse.createInputStream(source, converter) - } -} -class AvroDeserializerInputStreamBuilder( - private val deserializer: DeserializationStrategy, - private val avro : Avro, - converter: (Any) -> T -) : AvroInputStreamBuilder(converter) { - val defaultReadSchema : Schema by lazy { avro.schema(deserializer.descriptor) } +import org.apache.avro.util.WeakIdentityHashMap +import java.io.ByteArrayInputStream + +/** + * The goal of this class is to serialize and deserialize in avro binary format, not in GenericRecords. + */ +public sealed class Avro( + public val configuration: AvroConfiguration, + public override val serializersModule: SerializersModule, +) : BinaryFormat { + // We use the identity hash map because we could have multiple descriptors with the same name, especially + // when having 2 different version of the schema for the same name. kotlinx-serialization is instantiating the descriptors + // only once, so we are safe in the main use cases. Combined with weak references to avoid memory leaks. + private val schemaCache: MutableMap = WeakIdentityHashMap() + + internal val recordResolver = RecordResolver(this) + internal val polymorphicResolver = PolymorphicResolver(serializersModule) + internal val enumResolver = EnumResolver() + + public companion object Default : Avro( + AvroConfiguration(), + JavaStdLibSerializersModule + + JavaTimeSerializersModule + ) + + public fun schema(descriptor: SerialDescriptor): Schema { + return schemaCache.getOrPut(descriptor) { + lateinit var output: Schema + ValueVisitor(this) { output = it }.visitValue(descriptor) + output + } + } + + public fun encodeToByteArray( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, + ): ByteArray { + val buffer = Buffer() + encodeToSink(writerSchema, serializer, value, buffer) + return buffer.readByteArray() + } + + public fun decodeFromByteArray( + writerSchema: Schema, + deserializer: DeserializationStrategy, + bytes: ByteArray, + ): T { + val inputStream = ByteArrayInputStream(bytes) + val result = decodeFromStream(writerSchema, deserializer, inputStream) + if (inputStream.available() > 0) { + throw SerializationException("Not all bytes were consumed during deserialization") + } + return result + } + + override fun decodeFromByteArray( + deserializer: DeserializationStrategy, + bytes: ByteArray, + ): T { + return decodeFromByteArray(schema(deserializer.descriptor), deserializer, bytes) + } + + override fun encodeToByteArray( + serializer: SerializationStrategy, + value: T, + ): ByteArray { + return encodeToByteArray(schema(serializer.descriptor), serializer, value) + } } -class AvroOutputStreamBuilder( - private val serializer: SerializationStrategy, - private val avro: Avro, - private val converterFn: (Schema) -> (T) -> GenericRecord -){ - var encodeFormat : AvroEncodeFormat = defaultEncodeFormat - @Deprecated("please use AvroEncodeFormat to specify the format") - var format: AvroFormat = AvroFormat.DataFormat - var schema: Schema? = null - @Deprecated("please use AvroEncodeFormat to specify the format") - var codec: CodecFactory = CodecFactory.nullCodec() - - companion object { - val defaultEncodeFormat = AvroEncodeFormat.Data() - } - private val derivedEncodeFormat : AvroEncodeFormat - get() = when(format){ - AvroFormat.JsonFormat -> AvroEncodeFormat.Json - AvroFormat.BinaryFormat -> AvroEncodeFormat.Binary - AvroFormat.DataFormat -> AvroEncodeFormat.Data(codec) - } - fun to(path: Path): AvroOutputStream = to(Files.newOutputStream(path)) - fun to(path: String): AvroOutputStream = to(Paths.get(path)) - fun to(file: File): AvroOutputStream = to(file.toPath()) - - fun to(output: OutputStream): AvroOutputStream { - val schema = schema ?: avro.schema(serializer) - val converter = converterFn(schema) - return createOutputStream(output, schema, converter) - } - private fun createOutputStream(output: OutputStream, schema : Schema, converter: (T) -> GenericRecord): AvroOutputStream { - val currentDirectEncodeFormat = derivedEncodeFormat - val encodeFormatToUse = if(currentDirectEncodeFormat != defaultEncodeFormat) { - currentDirectEncodeFormat - } else{ - encodeFormat - } - return encodeFormatToUse.createOutputStream(output, schema, converter) - } +public fun Avro( + from: Avro = Avro, + builderAction: AvroBuilder.() -> Unit, +): Avro { + val builder = AvroBuilder(from) + builder.builderAction() + return AvroImpl(builder.build(), from.serializersModule.overwriteWith(builder.serializersModule)) } -@OptIn(ExperimentalSerializationApi::class) -class Avro( - override val serializersModule: SerializersModule = defaultModule, - private val configuration: AvroConfiguration = AvroConfiguration() -) : SerialFormat, BinaryFormat { - constructor(configuration: AvroConfiguration) : this(defaultModule, configuration) - companion object { - val defaultModule = SerializersModule { - contextual(UUIDSerializer()) - } - val default = Avro(defaultModule) - /** - * Use this constant if you want to explicitly set a default value of a field to avro null - */ - const val NULL = "com.github.avrokotlin.avro4k.Avro.AVRO_NULL_DEFAULT" - } +public class AvroBuilder internal constructor(avro: Avro) { + @ExperimentalSerializationApi + public var fieldNamingStrategy: FieldNamingStrategy = avro.configuration.fieldNamingStrategy - /** - * Loads an instance of from the given ByteArray, with the assumption that the record was stored - * using [AvroEncodeFormat.Data]. The schema used will be the embedded schema. - */ - override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T = - openInputStream(deserializer) { - decodeFormat = AvroDecodeFormat.Data(null, null) - }.from(bytes).nextOrThrow() + @ExperimentalSerializationApi + public var implicitNulls: Boolean = avro.configuration.implicitNulls - /** - * Creates an [AvroInputStreamBuilder] that will read avro values such as GenericRecord. - * Supply a function to this method to configure the builder, eg - * - *
-    * val input = openInputStream(serializer) {
-    *    decodeFormat = AvroDecodeFormat.Data(writerSchema = null, readerSchema = mySchema)
-    * }
-    * 
- */ - fun openInputStream(f: AvroInputStreamBuilder.() -> Unit = {}): AvroInputStreamBuilder { - val builder = AvroInputStreamBuilder { it } - builder.f() - return builder - } + @ExperimentalSerializationApi + public var implicitEmptyCollections: Boolean = avro.configuration.implicitEmptyCollections + @ExperimentalSerializationApi + public var validateSerialization: Boolean = avro.configuration.validateSerialization + public var serializersModule: SerializersModule = EmptySerializersModule() - /** - * Creates an [AvroInputStreamBuilder] that will read instances of . - * Supply a function to this method to configure the builder, eg - * - *
-    * val input = openInputStream(serializer) {
-    *    decodeFormat = AvroDecodeFormat.Data(writerSchema = null, readerSchema = mySchema)
-    * }
-    * 
- */ - fun openInputStream( - deserializer: DeserializationStrategy, - f: AvroDeserializerInputStreamBuilder.() -> Unit = {} - ): AvroDeserializerInputStreamBuilder { - val builder = AvroDeserializerInputStreamBuilder(deserializer, this) { - fromRecord(deserializer, it as GenericRecord) - } - builder.f() - return builder - } - - - /** - * Writes an instance of using a [Schema] derived from the type. - * This method will use the [AvroEncodeFormat.Data] format without a codec. - * The written object will be returned as a [ByteArray]. - */ - override fun encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray { - val baos = ByteArrayOutputStream() - openOutputStream(serializer) { - encodeFormat = AvroEncodeFormat.Data() - }.to(baos).write(value).close() - return baos.toByteArray() - } - - /** - * Creates an [AvroOutputStreamBuilder] that will write instances of . - * Supply a function to this method to configure the builder, eg - * - *
-    * val output = openOutputStream(serializer) {
-    *    encodeFormat = AvroEncodeFormat.Data()
-    *    schema = mySchema
-    * }
-    * 
- * - * If the schema is not supplied in the configuration function then it will - * be derived from the type using [Avro.schema]. - */ - fun openOutputStream( - serializer: SerializationStrategy, - f: AvroOutputStreamBuilder.() -> Unit = {} - ): AvroOutputStreamBuilder { - val builder = AvroOutputStreamBuilder(serializer, this) { schema -> { toRecord(serializer, schema, it) } } - builder.f() - return builder - } - - /** - * Converts an instance of to an Avro [Record] using a [Schema] derived from the type. - */ - fun toRecord( - serializer: SerializationStrategy, - obj: T, - ): GenericRecord { - return toRecord(serializer, schema(serializer), obj) - } - - /** - * Converts an instance of to an Avro [Record] using the given [Schema]. - */ - fun toRecord(serializer: SerializationStrategy, - schema: Schema, - obj: T): GenericRecord { - var record: Record? = null - val encoder = RootRecordEncoder(schema, serializersModule) { record = it } - encoder.encodeSerializableValue(serializer, obj) - return record!! - } + internal fun build(): AvroConfiguration = + AvroConfiguration( + fieldNamingStrategy = fieldNamingStrategy, + implicitNulls = implicitNulls, + implicitEmptyCollections = implicitEmptyCollections, + validateSerialization = validateSerialization + ) +} - /** - * Converts an Avro [GenericRecord] to an instance of using the schema - * present in the record. - */ - fun fromRecord( - deserializer: DeserializationStrategy, - record: GenericRecord, - ): T { - return RootRecordDecoder(record, serializersModule, configuration).decodeSerializableValue( - deserializer - ) - } +private class AvroImpl(configuration: AvroConfiguration, serializersModule: SerializersModule) : + Avro(configuration, serializersModule) - fun schema(descriptor: SerialDescriptor): Schema = - schemaFor( - serializersModule, - descriptor, - descriptor.annotations, - configuration, - mutableMapOf() - ).schema() +public inline fun Avro.schema(): Schema { + val serializer = serializersModule.serializer() + return schema(serializer.descriptor) +} - fun schema( - serializer: SerializationStrategy, - ): Schema { - return schema(serializer.descriptor) - } +public fun Avro.schema(serializer: KSerializer): Schema { + return schema(serializer.descriptor) +} +public inline fun Avro.encodeToByteArray( + writerSchema: Schema, + value: T, +): ByteArray { + val serializer = serializersModule.serializer() + return encodeToByteArray(writerSchema, serializer, value) } + +public inline fun Avro.decodeFromByteArray( + writerSchema: Schema, + bytes: ByteArray, +): T { + val serializer = serializersModule.serializer() + return decodeFromByteArray(writerSchema, serializer, bytes) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt index 3ac834db..a6029798 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroConfiguration.kt @@ -1,13 +1,119 @@ package com.github.avrokotlin.avro4k -import com.github.avrokotlin.avro4k.schema.DefaultNamingStrategy -import com.github.avrokotlin.avro4k.schema.NamingStrategy - -data class AvroConfiguration( - val namingStrategy: NamingStrategy = DefaultNamingStrategy, - /** - * 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. - */ - val implicitNulls: Boolean = false, +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor + +public data class AvroConfiguration( + /** + * The naming strategy to use for records' fields name. + * + * Default: [FieldNamingStrategy.Builtins.OriginalElementName] + */ + @ExperimentalSerializationApi + val fieldNamingStrategy: FieldNamingStrategy = FieldNamingStrategy.Builtins.OriginalElementName, + /** + * By default, 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. + * + * When set to `false`, during decoding, any missing value for a nullable field without default `null` value (e.g. `val field: Type?` without `= null`) is failing. + */ + @ExperimentalSerializationApi + val implicitNulls: Boolean = true, + /** + * By default, set to `true`, the array & map fields that haven't any default value are set as an empty array or map if the value is missing. It also adds `"default": []` for arrays or `"default": {}` for maps to those fields when generating schema using avro4k. + * + * If `implicitNulls` is true, the empty collections are set as null if the value is missing. + * + * When set to `false`, during decoding, any missing content for an array or a map field without its empty default value is failing. + */ + @ExperimentalSerializationApi + val implicitEmptyCollections: Boolean = true, + /** + * **To be removed when binary support is stable.** + * + * Set it to `true` to enable validation in case of failure, mainly for debug purpose. + * + * By default, to `false`. + * + * @see [org.apache.avro.io.ValidatingEncoder] + * @see [org.apache.avro.io.ValidatingDecoder] + */ + @ExperimentalSerializationApi + val validateSerialization: Boolean = false, ) + +/** + * @see AvroConfiguration.fieldNamingStrategy + */ +public interface FieldNamingStrategy { + public fun resolve( + descriptor: SerialDescriptor, + elementIndex: Int, + ): String + + public companion object Builtins { + /** + * Simply returns the element name. + */ + @ExperimentalSerializationApi + public object OriginalElementName : FieldNamingStrategy { + override fun resolve( + descriptor: SerialDescriptor, + elementIndex: Int, + ): String = descriptor.getElementName(elementIndex) + } + + /** + * Convert the field name to snake_case by adding an underscore before each capital letter, and lowercase those capital letters. + */ + public object SnakeCase : FieldNamingStrategy { + override fun resolve( + descriptor: SerialDescriptor, + elementIndex: Int, + ): String = + descriptor.getElementName(elementIndex).let { serialName -> + 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) + } + } + } + } + + /** + * Enforce camelCase naming strategy by upper-casing the first field name letter. + */ + @ExperimentalSerializationApi + public object PascalCase : FieldNamingStrategy { + override fun resolve( + descriptor: SerialDescriptor, + elementIndex: Int, + ): String = descriptor.getElementName(elementIndex).replaceFirstChar { it.uppercaseChar() } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt new file mode 100644 index 00000000..569bf6a4 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt @@ -0,0 +1,320 @@ +package com.github.avrokotlin.avro4k + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encoding.Decoder +import org.apache.avro.Schema +import org.apache.avro.generic.GenericFixed + +/** + * Interface to decode Avro values. + * Here are the main methods to decode values. Each decode method is adapting the raw type to the wanted type, that means unions are resolved if needed, and also all primitives are converted automatically (a wanted `long` could be decoded from a `int`). + * + * Primitives: + * - [decodeNull] + * - [decodeBoolean] + * - [decodeByte] + * - [decodeShort] + * - [decodeInt] + * - [decodeLong] + * - [decodeFloat] + * - [decodeDouble] + * - [decodeString] + * - [decodeChar] + * - [decodeEnum] + * + * Avro specific: + * - [decodeBytes] + * - [decodeFixed] + * + * Use the following methods to allow complex decoding using raw values, mainly for logical types: + * - [decodeResolvingAny] + * - [decodeResolvingBoolean] + * - [decodeResolvingByte] + * - [decodeResolvingShort] + * - [decodeResolvingInt] + * - [decodeResolvingLong] + * - [decodeResolvingFloat] + * - [decodeResolvingDouble] + * - [decodeResolvingChar] + */ +public interface AvroDecoder : Decoder { + /** + * Provides the schema used to encode the current value. + * It won't return a union as the schema correspond to the actual value. + */ + @ExperimentalSerializationApi + public val currentWriterSchema: Schema + + /** + * Decode a [Schema.Type.BYTES] value. + * + * A bytes value is a sequence of bytes prefixed with an int corresponding to its length. + */ + @ExperimentalSerializationApi + public fun decodeBytes(): ByteArray + + /** + * Decode a [Schema.Type.FIXED] value. + * + * A fixed value is a fixed-size sequence of bytes, where the length is not materialized in the binary output as it is known by the [currentWriterSchema]. + */ + @ExperimentalSerializationApi + public fun decodeFixed(): GenericFixed + + /** + * Decode a value that corresponds to the [currentWriterSchema]. + * + * You should prefer using directly [currentWriterSchema] to get the schema and then decode the value using the appropriate **decode*** method. + * + * Will be removed in the future as direct decoding isn't capable of it. + */ + @Deprecated("Use currentWriterSchema to get the schema and then decode the value using the appropriate decode* method, or use decodeResolving* for more complex use cases.") + @ExperimentalSerializationApi + public fun decodeValue(): Any +} + +/** + * This interface is used internally to decode and resolve union types. + */ +@PublishedApi +internal interface UnionDecoder : AvroDecoder { + /** + * Decode the union schema and set the resolved type in [currentWriterSchema]. + */ + fun decodeAndResolveUnion() +} + +/** + * Allows you to decode a value differently depending on the schema (generally its name, type, logicalType), even if it is a union. + * + * This reduces the need to manually resolve the type in a union **and** not in a union. + * + * For examples, see the [com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer] as it resolves a lot of types and also logical types. + * + * **Important note:** Use the specific methods for primitives to avoid auto-boxing and improve performances. + * + * @param resolver A lambda that returns a [AnyValueDecoder] that contains the logic to decode the value only when the schema matches. The decoding **MUST** be done in the [AnyValueDecoder] to avoid decoding the value if it is not the right schema. Return null when it is not matching the expected schema. + * @param error A lambda that throws an exception if the decoder cannot be resolved. + * + * @see decodeResolvingBoolean + * @see decodeResolvingByte + * @see decodeResolvingShort + * @see decodeResolvingInt + * @see decodeResolvingLong + * @see decodeResolvingFloat + * @see decodeResolvingDouble + * @see decodeResolvingChar + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingAny( + error: () -> Throwable, + resolver: (Schema) -> AnyValueDecoder?, +): T { + return findValueDecoder(error, resolver).decodeAny() +} + +/** + * An [Byte] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingByte( + error: () -> Throwable, + resolver: (Schema) -> ByteValueDecoder?, +): Byte { + return findValueDecoder(error, resolver).decodeByte() +} + +/** + * An [Short] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingShort( + error: () -> Throwable, + resolver: (Schema) -> ShortValueDecoder?, +): Short { + return findValueDecoder(error, resolver).decodeShort() +} + +/** + * An [Int] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingInt( + error: () -> Throwable, + resolver: (Schema) -> IntValueDecoder?, +): Int { + return findValueDecoder(error, resolver).decodeInt() +} + +/** + * A [Long] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingLong( + error: () -> Throwable, + resolver: (Schema) -> LongValueDecoder?, +): Long { + return findValueDecoder(error, resolver).decodeLong() +} + +/** + * A [Boolean] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingBoolean( + error: () -> Throwable, + resolver: (Schema) -> BooleanValueDecoder?, +): Boolean { + return findValueDecoder(error, resolver).decodeBoolean() +} + +/** + * A [Float] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingFloat( + error: () -> Throwable, + resolver: (Schema) -> FloatValueDecoder?, +): Float { + return findValueDecoder(error, resolver).decodeFloat() +} + +/** + * A [Double] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingDouble( + error: () -> Throwable, + resolver: (Schema) -> DoubleValueDecoder?, +): Double { + return findValueDecoder(error, resolver).decodeDouble() +} + +/** + * A [Char] specific [decodeResolvingAny] to prevent auto-boxing (improving performances avoiding primitive<->object conversions). + * + * @see decodeResolvingAny + */ +@ExperimentalSerializationApi +public inline fun AvroDecoder.decodeResolvingChar( + error: () -> Throwable, + resolver: (Schema) -> CharValueDecoder?, +): Char { + return findValueDecoder(error, resolver).decodeChar() +} + +/** + * @see AvroDecoder.decodeResolvingAny + */ +@ExperimentalSerializationApi +public fun interface AnyValueDecoder { + context(AvroDecoder) + public fun decodeAny(): T +} + +/** + * @see AvroDecoder.decodeResolvingBoolean + */ +@ExperimentalSerializationApi +public fun interface BooleanValueDecoder { + context(AvroDecoder) + public fun decodeBoolean(): Boolean +} + +/** + * @see AvroDecoder.decodeResolvingByte + */ +@ExperimentalSerializationApi +public fun interface ByteValueDecoder { + context(AvroDecoder) + public fun decodeByte(): Byte +} + +/** + * @see AvroDecoder.decodeResolvingShort + */ +@ExperimentalSerializationApi +public fun interface ShortValueDecoder { + context(AvroDecoder) + public fun decodeShort(): Short +} + +/** + * @see AvroDecoder.decodeResolvingInt + */ +@ExperimentalSerializationApi +public fun interface IntValueDecoder { + context(AvroDecoder) + public fun decodeInt(): Int +} + +/** + * @see AvroDecoder.decodeResolvingLong + */ +@ExperimentalSerializationApi +public fun interface LongValueDecoder { + context(AvroDecoder) + public fun decodeLong(): Long +} + +/** + * @see AvroDecoder.decodeResolvingFloat + */ +@ExperimentalSerializationApi +public fun interface FloatValueDecoder { + context(AvroDecoder) + public fun decodeFloat(): Float +} + +/** + * @see AvroDecoder.decodeResolvingDouble + */ +@ExperimentalSerializationApi +public fun interface DoubleValueDecoder { + context(AvroDecoder) + public fun decodeDouble(): Double +} + +/** + * @see AvroDecoder.decodeResolvingChar + */ +@ExperimentalSerializationApi +public fun interface CharValueDecoder { + context(AvroDecoder) + public fun decodeChar(): Char +} + +@PublishedApi +internal inline fun AvroDecoder.findValueDecoder( + error: () -> Throwable, + resolver: (Schema) -> T?, +): T { + val schema = currentWriterSchema + + val foundResolver = + if (schema.isUnion) { + if (this is UnionDecoder) { + decodeAndResolveUnion() + resolver(currentWriterSchema) + } else { + currentWriterSchema.types.firstNotNullOfOrNull(resolver) + } + } else { + resolver(schema) + } + return foundResolver ?: throw error() +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt new file mode 100644 index 00000000..1e791ec2 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt @@ -0,0 +1,107 @@ +package com.github.avrokotlin.avro4k + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encoding.Encoder +import org.apache.avro.Schema +import org.apache.avro.generic.GenericFixed +import java.nio.ByteBuffer + +/** + * Interface to encode Avro values. + * Here are the main methods to encode values. Each encode method is adapting the type to the raw type, that means unions are resolved if needed, and also all primitives are converted automatically (a wanted `int` could be encoded to a `long`). + * - [encodeNull] + * - [encodeBoolean] + * - [encodeByte] + * - [encodeShort] + * - [encodeInt] + * - [encodeLong] + * - [encodeFloat] + * - [encodeDouble] + * - [encodeString] + * - [encodeChar] + * - [encodeEnum] + * - [encodeBytes] + * - [encodeFixed] + * + * Use the following methods to allow complex encoding using raw values, mainly for logical types: + * - [encodeResolving] + */ +public interface AvroEncoder : Encoder { + /** + * Provides the schema used to encode the current value. + */ + @ExperimentalSerializationApi + public val currentWriterSchema: Schema + + /** + * Encodes a [Schema.Type.BYTES] value from a [ByteBuffer]. + */ + @ExperimentalSerializationApi + public fun encodeBytes(value: ByteBuffer) + + /** + * Encodes a [Schema.Type.BYTES] value from a [ByteArray]. + */ + @ExperimentalSerializationApi + public fun encodeBytes(value: ByteArray) + + /** + * Encodes a [Schema.Type.FIXED] value from a [ByteArray]. Its size must match the size of the fixed schema in [currentWriterSchema]. + */ + @ExperimentalSerializationApi + public fun encodeFixed(value: ByteArray) + + /** + * Encodes a [Schema.Type.FIXED] value from a [GenericFixed]. Its size must match the size of the fixed schema in [currentWriterSchema]. + */ + @ExperimentalSerializationApi + public fun encodeFixed(value: GenericFixed) +} + +@PublishedApi +internal interface UnionEncoder : AvroEncoder { + /** + * Encode the selected union schema and set the selected type in [currentWriterSchema]. + */ + fun encodeUnionIndex(index: Int) +} + +/** + * Allows you to encode a value differently depending on the schema (generally its name, type, logicalType). + * If the [AvroEncoder.currentWriterSchema] is a union, it takes **the first matching encoder** as the final encoder. + * + * This reduces the need to manually resolve the type in a union **and** not in a union. + * + * For examples, see the [com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer] as it resolves a lot of types and also logical types. + * + * @param resolver A lambda that returns a lambda (the encoding lambda) that contains the logic to encode the value only when the schema matches. The encoding **MUST** be done in the encoder lambda to avoid encoding the value if it is not the right schema. Return null when it is not matching the expected schema. + * @param error A lambda that throws an exception if the encoder cannot be resolved. + */ +@ExperimentalSerializationApi +public inline fun AvroEncoder.encodeResolving( + error: () -> Throwable, + resolver: (Schema) -> (() -> T)?, +): T { + val schema = currentWriterSchema + return if (schema.isUnion) { + resolveUnion(schema, error, resolver) + } else { + resolver(schema)?.invoke() ?: throw error() + } +} + +@PublishedApi +internal inline fun AvroEncoder.resolveUnion( + schema: Schema, + error: () -> Throwable, + resolver: (Schema) -> (() -> T)?, +): T { + for (index in schema.types.indices) { + val subSchema = schema.types[index] + resolver(subSchema)?.let { + (this as UnionEncoder).encodeUnionIndex(index) + return it.invoke() + } + } + throw error() +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroGenericDataExtensions.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroGenericDataExtensions.kt new file mode 100644 index 00000000..47e8bc14 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroGenericDataExtensions.kt @@ -0,0 +1,64 @@ +package com.github.avrokotlin.avro4k + +import com.github.avrokotlin.avro4k.internal.decoder.generic.AvroValueGenericDecoder +import com.github.avrokotlin.avro4k.internal.encoder.generic.AvroValueGenericEncoder +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.generic.GenericContainer + +@ExperimentalSerializationApi +public fun Avro.encodeToGenericData( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, +): Any? { + var result: Any? = null + AvroValueGenericEncoder(this, writerSchema) { + result = it + }.encodeSerializableValue(serializer, value) + return result +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToGenericData(value: T): Any? { + val serializer = serializersModule.serializer() + return encodeToGenericData(schema(serializer), serializer, value) +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToGenericData( + writerSchema: Schema, + value: T, +): Any? { + val serializer = serializersModule.serializer() + return encodeToGenericData(writerSchema, serializer, value) +} + +@ExperimentalSerializationApi +public fun Avro.decodeFromGenericData( + writerSchema: Schema, + deserializer: DeserializationStrategy, + value: Any?, +): T { + return AvroValueGenericDecoder(this, value, writerSchema) + .decodeSerializableValue(deserializer) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromGenericData( + writerSchema: Schema, + value: Any?, +): T { + val deserializer = serializersModule.serializer() + return decodeFromGenericData(writerSchema, deserializer, value) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromGenericData(value: GenericContainer?): T? { + if (value == null) return null + val deserializer = serializersModule.serializer() + return decodeFromGenericData(value.schema, deserializer, value) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroJVMExtensions.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroJVMExtensions.kt new file mode 100644 index 00000000..b50a5b10 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroJVMExtensions.kt @@ -0,0 +1,86 @@ +package com.github.avrokotlin.avro4k + +import com.github.avrokotlin.avro4k.internal.decodeWithBinaryDecoder +import com.github.avrokotlin.avro4k.internal.encodeWithBinaryEncoder +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.EncoderFactory +import java.io.InputStream +import java.io.OutputStream + +@ExperimentalSerializationApi +public fun Avro.encodeToStream( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, + outputStream: OutputStream, +) { + val avroEncoder = + EncoderFactory.get().directBinaryEncoder(outputStream, null).let { + if (configuration.validateSerialization) { + EncoderFactory.get().validatingEncoder(writerSchema, it) + } else { + it + } + } + + encodeWithBinaryEncoder(writerSchema, serializer, value, avroEncoder) + + avroEncoder.flush() +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToStream( + value: T, + outputStream: OutputStream, +) { + val serializer = serializersModule.serializer() + encodeToStream(schema(serializer), serializer, value, outputStream) +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToStream( + writerSchema: Schema, + value: T, + outputStream: OutputStream, +) { + val serializer = serializersModule.serializer() + encodeToStream(writerSchema, serializer, value, outputStream) +} + +@ExperimentalSerializationApi +public fun Avro.decodeFromStream( + writerSchema: Schema, + deserializer: DeserializationStrategy, + inputStream: InputStream, +): T { + val avroDecoder = + DecoderFactory.get().directBinaryDecoder(inputStream, null).let { + if (configuration.validateSerialization) { + DecoderFactory.get().validatingDecoder(writerSchema, it) + } else { + it + } + } + + return decodeWithBinaryDecoder(writerSchema, deserializer, avroDecoder) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromStream(inputStream: InputStream): T { + val serializer = serializersModule.serializer() + return decodeFromStream(schema(serializer.descriptor), serializer, inputStream) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromStream( + writerSchema: Schema, + inputStream: InputStream, +): T { + val serializer = serializersModule.serializer() + return decodeFromStream(writerSchema, serializer, inputStream) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainer.kt new file mode 100644 index 00000000..9956f3a5 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainer.kt @@ -0,0 +1,172 @@ +package com.github.avrokotlin.avro4k + +import com.github.avrokotlin.avro4k.internal.decodeWithBinaryDecoder +import com.github.avrokotlin.avro4k.internal.encodeWithBinaryEncoder +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.file.CodecFactory +import org.apache.avro.file.DataFileStream +import org.apache.avro.file.DataFileWriter +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import java.io.InputStream +import java.io.OutputStream + +/** + * Encode and decode values in object container files, also known as avro data file format. + * + * [spec](https://avro.apache.org/docs/1.11.1/specification/#object-container-files) + */ +@ExperimentalSerializationApi +public sealed class AvroObjectContainer( + @PublishedApi + internal val avro: Avro, +) { + public companion object Default : AvroObjectContainer(Avro) + + /** + * Encodes the given sequence to the given output stream. + * + * Note that the output stream is not closed after the operation, which means you need to handle it to avoid resource leaks. + */ + public fun encodeToStream( + schema: Schema, + serializer: SerializationStrategy, + values: Sequence, + outputStream: OutputStream, + builder: AvroObjectContainerBuilder.() -> Unit = {}, + ) { + val datumWriter: DatumWriter = KotlinxSerializationDatumWriter(serializer, avro) + val dataFileWriter = DataFileWriter(datumWriter) + try { + builder(AvroObjectContainerBuilder(dataFileWriter)) + dataFileWriter.create(schema, outputStream) + values.forEach { + dataFileWriter.append(it) + } + } finally { + dataFileWriter.flush() + } + } + + public fun decodeFromStream( + deserializer: DeserializationStrategy, + inputStream: InputStream, + metadataDumper: AvroObjectContainerMetadataDumper.() -> Unit = {}, + ): Sequence { + return sequence { + val datumReader: DatumReader = KotlinxSerializationDatumReader(deserializer, avro) + val dataFileStream = DataFileStream(inputStream, datumReader) + metadataDumper(AvroObjectContainerMetadataDumper(dataFileStream)) + yieldAll(dataFileStream.iterator()) + }.constrainOnce() + } +} + +private class AvroObjectContainerImpl(avro: Avro) : AvroObjectContainer(avro) + +public fun AvroObjectContainer( + from: Avro = Avro, + builderAction: AvroBuilder.() -> Unit, +): AvroObjectContainer { + return AvroObjectContainerImpl(Avro(from, builderAction)) +} + +@ExperimentalSerializationApi +public inline fun AvroObjectContainer.encodeToStream( + values: Sequence, + outputStream: OutputStream, + noinline builder: AvroObjectContainerBuilder.() -> Unit = {}, +) { + val serializer = avro.serializersModule.serializer() + encodeToStream(avro.schema(serializer), serializer, values, outputStream, builder) +} + +@ExperimentalSerializationApi +public inline fun AvroObjectContainer.decodeFromStream( + inputStream: InputStream, + noinline metadataDumper: AvroObjectContainerMetadataDumper.() -> Unit = {}, +): Sequence { + val serializer = avro.serializersModule.serializer() + return decodeFromStream(serializer, inputStream, metadataDumper) +} + +public class AvroObjectContainerBuilder(private val fileWriter: DataFileWriter<*>) { + public fun metadata( + key: String, + value: ByteArray, + ) { + fileWriter.setMeta(key, value) + } + + public fun metadata( + key: String, + value: String, + ) { + fileWriter.setMeta(key, value) + } + + public fun metadata( + key: String, + value: Long, + ) { + fileWriter.setMeta(key, value) + } + + public fun codec(codec: CodecFactory) { + fileWriter.setCodec(codec) + } +} + +public class AvroObjectContainerMetadataDumper(private val fileStream: DataFileStream<*>) { + public fun metadata(key: String): MetadataAccessor? { + return fileStream.getMeta(key)?.let { MetadataAccessor(it) } + } + + public inner class MetadataAccessor(private val value: ByteArray) { + public fun asBytes(): ByteArray = value + + public fun asString(): String = value.decodeToString() + + public fun asLong(): Long = asString().toLong() + } +} + +private class KotlinxSerializationDatumWriter( + private val serializer: SerializationStrategy, + private val avro: Avro, +) : DatumWriter { + private lateinit var writerSchema: Schema + + override fun setSchema(schema: Schema) { + writerSchema = schema + } + + override fun write( + datum: T, + encoder: org.apache.avro.io.Encoder, + ) { + avro.encodeWithBinaryEncoder(writerSchema, serializer, datum, encoder) + } +} + +private class KotlinxSerializationDatumReader( + private val deserializer: DeserializationStrategy, + private val avro: Avro, +) : DatumReader { + private lateinit var writerSchema: Schema + + override fun setSchema(schema: Schema) { + writerSchema = schema + } + + override fun read( + reuse: T?, + decoder: org.apache.avro.io.Decoder, + ): T { + return avro.decodeWithBinaryDecoder(writerSchema, deserializer, decoder) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroOkioExtensions.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroOkioExtensions.kt new file mode 100644 index 00000000..d4e4d329 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroOkioExtensions.kt @@ -0,0 +1,86 @@ +package com.github.avrokotlin.avro4k + +import com.github.avrokotlin.avro4k.internal.decodeWithBinaryDecoder +import com.github.avrokotlin.avro4k.internal.encodeWithBinaryEncoder +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import okio.BufferedSink +import okio.BufferedSource +import org.apache.avro.Schema +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.EncoderFactory + +@ExperimentalSerializationApi +public fun Avro.encodeToSink( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, + sink: BufferedSink, +) { + val avroEncoder = + EncoderFactory.get().directBinaryEncoder(sink.outputStream(), null).let { + if (configuration.validateSerialization) { + EncoderFactory.get().validatingEncoder(writerSchema, it) + } else { + it + } + } + + encodeWithBinaryEncoder(writerSchema, serializer, value, avroEncoder) + + avroEncoder.flush() +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToSink( + value: T, + sink: BufferedSink, +) { + val serializer = serializersModule.serializer() + encodeToSink(schema(serializer), serializer, value, sink) +} + +@ExperimentalSerializationApi +public inline fun Avro.encodeToSink( + writerSchema: Schema, + value: T, + sink: BufferedSink, +) { + val serializer = serializersModule.serializer() + encodeToSink(writerSchema, serializer, value, sink) +} + +@ExperimentalSerializationApi +public fun Avro.decodeFromSource( + writerSchema: Schema, + deserializer: DeserializationStrategy, + source: BufferedSource, +): T { + val avroDecoder = + DecoderFactory.get().directBinaryDecoder(source.inputStream(), null).let { + if (configuration.validateSerialization) { + DecoderFactory.get().validatingDecoder(writerSchema, it) + } else { + it + } + } + + return decodeWithBinaryDecoder(writerSchema, deserializer, avroDecoder) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromSource(source: BufferedSource): T { + val serializer = serializersModule.serializer() + return decodeFromSource(schema(serializer.descriptor), serializer, source) +} + +@ExperimentalSerializationApi +public inline fun Avro.decodeFromSource( + writerSchema: Schema, + source: BufferedSource, +): T { + val serializer = serializersModule.serializer() + return decodeFromSource(writerSchema, serializer, source) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroSingleObject.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroSingleObject.kt new file mode 100644 index 00000000..3ca113fe --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroSingleObject.kt @@ -0,0 +1,103 @@ +package com.github.avrokotlin.avro4k + +import kotlinx.serialization.BinaryFormat +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.SchemaNormalization +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Single Avro objects are encoded as follows: + * - A two-byte marker, C3 01, to show that the message is Avro and uses this single-record format (version 1). + * - The 8-byte little-endian CRC-64-AVRO fingerprint of the object’s schema. + * - The Avro object encoded using Avro’s binary encoding. + * + * [spec](https://avro.apache.org/docs/1.11.1/specification/#single-object-encoding) + * + * @param schemaRegistry a function to find a schema by its fingerprint, and returns null when not found. You should use [SchemaNormalization.parsingFingerprint64] to generate the fingerprint. + */ +@ExperimentalSerializationApi +public class AvroSingleObject( + private val schemaRegistry: (fingerprint: Long) -> Schema?, + @PublishedApi + internal val avro: Avro = Avro, +) : BinaryFormat { + override val serializersModule: SerializersModule + get() = avro.serializersModule + + private fun Schema.crc64avro(): ByteArray = SchemaNormalization.parsingFingerprint("CRC-64-AVRO", this) + + public fun encodeToStream( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, + outputStream: OutputStream, + ) { + outputStream.write(MAGIC_BYTE) + outputStream.write(FORMAT_VERSION) + outputStream.write(writerSchema.crc64avro()) + avro.encodeToStream(writerSchema, serializer, value, outputStream) + } + + public fun decodeFromStream( + deserializer: DeserializationStrategy, + inputStream: InputStream, + ): T { + check(inputStream.read() == MAGIC_BYTE) { "Not a valid single-object avro format, bad magic byte" } + check(inputStream.read() == FORMAT_VERSION) { "Not a valid single-object avro format, bad version byte" } + val fingerprint = ByteBuffer.wrap(ByteArray(8).apply { inputStream.read(this) }).order(ByteOrder.LITTLE_ENDIAN).getLong() + val writerSchema = + schemaRegistry(fingerprint) ?: throw SerializationException("schema not found for the given object's schema fingerprint 0x${fingerprint.toString(16)}") + + return avro.decodeFromStream(writerSchema, deserializer, inputStream) + } + + public override fun decodeFromByteArray( + deserializer: DeserializationStrategy, + bytes: ByteArray, + ): T { + return bytes.inputStream().use { + decodeFromStream(deserializer, it) + } + } + + override fun encodeToByteArray( + serializer: SerializationStrategy, + value: T, + ): ByteArray { + return encodeToByteArray(avro.schema(serializer.descriptor), serializer, value) + } +} + +private const val MAGIC_BYTE: Int = 0xC3 +private const val FORMAT_VERSION: Int = 1 + +public fun AvroSingleObject.encodeToByteArray( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, +): ByteArray = + ByteArrayOutputStream().apply { + encodeToStream(writerSchema, serializer, value, this) + }.toByteArray() + +public inline fun AvroSingleObject.encodeToByteArray( + writerSchema: Schema, + value: T, +): ByteArray = encodeToByteArray(writerSchema, avro.serializersModule.serializer(), value) + +public inline fun AvroSingleObject.encodeToByteArray(value: T): ByteArray { + val serializer = avro.serializersModule.serializer() + return encodeToByteArray(avro.schema(serializer), serializer, value) +} + +public inline fun AvroSingleObject.decodeFromByteArray(bytes: ByteArray): T = decodeFromByteArray(avro.serializersModule.serializer(), bytes) \ No newline at end of file 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 b24a6482..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/FieldNaming.kt +++ /dev/null @@ -1,31 +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/ListRecord.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/ListRecord.kt new file mode 100644 index 00000000..fc6f82f9 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/ListRecord.kt @@ -0,0 +1,47 @@ +package com.github.avrokotlin.avro4k + +import kotlinx.serialization.ExperimentalSerializationApi +import org.apache.avro.Schema +import org.apache.avro.generic.GenericRecord +import org.apache.avro.specific.SpecificRecord + +/** + * An implementation of [org.apache.avro.generic.GenericContainer] that implements + * both [GenericRecord] and [SpecificRecord]. + */ +@ExperimentalSerializationApi +public interface Record : GenericRecord, SpecificRecord + +@ExperimentalSerializationApi +public data class ListRecord( + private val s: Schema, + private val values: List, +) : Record { + public constructor(s: Schema, vararg values: Any?) : this(s, values.asList()) + + init { + require(schema.type == Schema.Type.RECORD) { "Cannot create a Record with a schema that is not of type Schema.Type.RECORD [was $s]" } + } + + override fun getSchema(): Schema = s + + override fun put( + key: String, + v: Any, + ): Unit = throw UnsupportedOperationException("This implementation of Record is immutable") + + override fun put( + i: Int, + v: Any, + ): Unit = throw UnsupportedOperationException("This implementation of Record is immutable") + + override fun get(key: String): Any? { + val index = schema.fields.indexOfFirst { it.name() == key } + if (index == -1) { + throw RuntimeException("Field $key does not exist in this record (schema=$schema, values=$values)") + } + return get(index) + } + + override fun get(i: Int): Any? = values[i] +} \ 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 06c6dc4f..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/RecordNaming.kt +++ /dev/null @@ -1,64 +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) - - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/SerialDescriptor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/SerialDescriptor.kt deleted file mode 100644 index 52236ce0..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/SerialDescriptor.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.avrokotlin.avro4k - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.modules.SerializersModule - -@ExperimentalSerializationApi -fun SerialDescriptor.possibleSerializationSubclasses(serializersModule: SerializersModule): List { - return when (this.kind) { - StructureKind.CLASS, StructureKind.OBJECT -> listOf(this) - PolymorphicKind.SEALED -> elementDescriptors.filter { it.kind == SerialKind.CONTEXTUAL } - .flatMap { it.elementDescriptors } - .flatMap { it.possibleSerializationSubclasses(serializersModule) } - PolymorphicKind.OPEN -> - serializersModule.getPolymorphicDescriptors(this) - .flatMap { it.possibleSerializationSubclasses(serializersModule) } - else -> throw UnsupportedOperationException("Can't get possible serialization subclasses for the SerialDescriptor of kind ${this.kind}.") - } -} - diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt deleted file mode 100644 index c1b1334f..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt +++ /dev/null @@ -1,70 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) -package com.github.avrokotlin.avro4k - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialInfo -import org.intellij.lang.annotations.Language - -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroProp(val key: String, val value: String) - -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroJsonProp(val key: String, @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, val precision: Int) - -@SerialInfo -@Target(AnnotationTarget.CLASS) -annotation class AvroInline - -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroDoc(val value: String) - -@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 - * 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. - */ -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroFixed(val size: Int) - -@SerialInfo -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class AvroDefault(@Language("JSON") val value: String) - -@SerialInfo -@Target(AnnotationTarget.CLASS) -annotation class AvroEnumDefault(val value: String) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoder.kt deleted file mode 100644 index 89d24acc..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoder.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE -import kotlinx.serialization.modules.SerializersModule - -@ExperimentalSerializationApi -class ByteArrayDecoder(val data: ByteArray, override val serializersModule: SerializersModule) : AbstractDecoder() { - - private var index = -1 - - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = data.size - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - index++ - return if(index < data.size) index else DECODE_DONE - } - - override fun decodeByte(): Byte { - return data[index] - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt deleted file mode 100644 index c6813dee..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import kotlinx.serialization.SerializationException -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericEnumSymbol -import org.apache.avro.util.Utf8 -import java.nio.ByteBuffer - -interface FromAvroValue { - fun fromValue(value: T): R -} - -object StringFromAvroValue : FromAvroValue { - override fun fromValue(value: Any?): String { - return when (value) { - is String -> value - is Utf8 -> value.toString() - is GenericData.Fixed -> String(value.bytes()) - is ByteArray -> String(value) - is CharSequence -> value.toString() - is ByteBuffer -> String(value.array()) - null -> throw SerializationException("Cannot decode as a string") - else -> throw SerializationException("Unsupported type for String [is ${value.javaClass}]") - } - } -} - -object EnumFromAvroValue : FromAvroValue { - override fun fromValue(value: Any): String { - return when (value) { - is GenericEnumSymbol<*> -> value.toString() - is String -> value - else -> value.toString() - } - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/InlineDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/InlineDecoder.kt deleted file mode 100644 index 7184bfe9..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/InlineDecoder.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE -import kotlinx.serialization.modules.SerializersModule - -@ExperimentalSerializationApi -class InlineDecoder(private val value: Any?, override val serializersModule: SerializersModule) : AbstractDecoder() { - private var index = -1 - override fun decodeElementIndex(descriptor: SerialDescriptor): Int = if(++index < 1) index else DECODE_DONE - - override fun decodeString(): String { - return StringFromAvroValue.fromValue(value) - } - - override fun decodeValue(): Any { - return value!! - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ListDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ListDecoder.kt deleted file mode 100644 index e529b20e..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/ListDecoder.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - - -import com.github.avrokotlin.avro4k.AvroConfiguration -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericArray -import org.apache.avro.generic.GenericRecord - -@ExperimentalSerializationApi -class ListDecoder( - private val schema: Schema, - private val array: List, - override val serializersModule: SerializersModule, - private val configuration: AvroConfiguration, -) : AbstractDecoder(), FieldDecoder { - - init { - require(schema.type == Schema.Type.ARRAY) - } - - private var index = -1 - - override fun decodeBoolean(): Boolean { - return array[index] as Boolean - } - - override fun decodeLong(): Long { - return array[index] as Long - } - - override fun decodeString(): String { - val raw = array[index] - return StringFromAvroValue.fromValue(raw) - } - - override fun decodeDouble(): Double { - return array[index] as Double - } - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - index++ - return if(index < array.size) index else DECODE_DONE - } - - override fun decodeFloat(): Float { - return array[index] as Float - } - - override fun decodeByte(): Byte { - return array[index] as Byte - } - - override fun decodeInt(): Int { - return array[index] as Int - } - - override fun decodeChar(): Char { - return array[index] as Char - } - - override fun decodeAny(): Any? { - return array[index] - } - - override fun decodeNotNullMark() = array[index] != null - override fun fieldSchema(): Schema = schema.elementType - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - val symbol = EnumFromAvroValue.fromValue(array[index]!!) - return (0 until enumDescriptor.elementsCount).find { enumDescriptor.getElementName(it) == symbol } ?: -1 - } - - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - return deserializer.deserialize(this) - } - - @Suppress("UNCHECKED_CAST") - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - return when (descriptor.kind) { - StructureKind.CLASS -> RecordDecoder(descriptor, array[index] as GenericRecord, serializersModule, configuration) - StructureKind.LIST -> ListDecoder(schema.elementType, array[index] as GenericArray<*>, serializersModule, configuration) - StructureKind.MAP -> MapDecoder(descriptor, schema.elementType, array[index] as Map, serializersModule, configuration) - PolymorphicKind.SEALED, PolymorphicKind.OPEN -> UnionDecoder(descriptor,array[index] as GenericRecord, serializersModule, configuration) - else -> throw UnsupportedOperationException("Kind ${descriptor.kind} is currently not supported.") - } - } - - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = array.size -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt deleted file mode 100644 index 14f4d3a1..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.AvroConfiguration -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericArray -import org.apache.avro.generic.GenericRecord -import java.nio.ByteBuffer - -@ExperimentalSerializationApi -class MapDecoder( - private val desc: SerialDescriptor, - private val schema: Schema, - map: Map<*, *>, - override val serializersModule: SerializersModule, - private val configuration: AvroConfiguration -) : AbstractDecoder(), CompositeDecoder { - - init { - require(schema.type == Schema.Type.MAP) - } - - private val entries = map.toList() - private var index = -1 - - override fun decodeString(): String { - val entry = entries[index / 2] - val value = when { - index % 2 == 0 -> entry.first - else -> entry.second - } - return StringFromAvroValue.fromValue(value) - } - - private fun value(): Any? = entries[index / 2].second - - override fun decodeNotNullMark() : Boolean { - val entry = entries[index / 2] - return when { - index % 2 == 0 -> entry.first - else -> entry.second - } != null - } - - override fun decodeFloat(): Float { - return when (val v = value()) { - is Float -> v - null -> throw SerializationException("Cannot decode as a Float") - else -> throw SerializationException("Unsupported type for Float ${v.javaClass}") - } - } - - override fun decodeInt(): Int { - return when (val v = value()) { - is Int -> v - null -> throw SerializationException("Cannot decode as a Int") - else -> throw SerializationException("Unsupported type for Int ${v.javaClass}") - } - } - - override fun decodeLong(): Long { - return when (val v = value()) { - is Long -> v - is Int -> v.toLong() - null -> throw SerializationException("Cannot decode as a Long") - else -> throw SerializationException("Unsupported type for Long ${v.javaClass}") - } - } - - override fun decodeDouble(): Double { - return when (val v = value()) { - is Double -> v - is Float -> v.toDouble() - null -> throw SerializationException("Cannot decode as a Double") - else -> throw SerializationException("Unsupported type for Double ${v.javaClass}") - } - } - - override fun decodeByte(): Byte { - return when (val v = value()) { - is Byte -> v - is Int -> v.toByte() - null -> throw SerializationException("Cannot decode as a Byte") - else -> throw SerializationException("Unsupported type for Byte ${v.javaClass}") - } - } - - override fun decodeBoolean(): Boolean { - return when (val v = value()) { - is Boolean -> v - null -> throw SerializationException("Cannot decode as a Boolean") - else -> throw SerializationException("Unsupported type for Boolean ${v.javaClass}") - } - } - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - index++ - return if (index == entries.size * 2) CompositeDecoder.DECODE_DONE else index - } - - @Suppress("UNCHECKED_CAST") - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - return when (descriptor.kind) { - StructureKind.CLASS -> RecordDecoder(descriptor, value() as GenericRecord, serializersModule, configuration) - StructureKind.LIST -> when(descriptor.getElementDescriptor(0).kind) { - PrimitiveKind.BYTE -> ByteArrayDecoder((value() as ByteBuffer).array(), serializersModule) - else -> ListDecoder(schema.valueType, value() as GenericArray<*>, serializersModule, configuration) - } - StructureKind.MAP -> MapDecoder(descriptor, schema.valueType, value() as Map, serializersModule, configuration) - PolymorphicKind.SEALED, PolymorphicKind.OPEN -> UnionDecoder(descriptor, value() as GenericRecord, serializersModule, configuration) - else -> throw UnsupportedOperationException("Kind ${descriptor.kind} is currently not supported.") - } - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt deleted file mode 100644 index 46b9eab1..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt +++ /dev/null @@ -1,197 +0,0 @@ -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 -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericFixed -import org.apache.avro.generic.GenericRecord -import java.nio.ByteBuffer - -interface ExtendedDecoder : Decoder { - fun decodeAny(): Any? -} - -interface FieldDecoder : ExtendedDecoder { - fun fieldSchema(): Schema -} - -@ExperimentalSerializationApi -class RecordDecoder( - private val desc: SerialDescriptor, - private val record: GenericRecord, - override val serializersModule: SerializersModule, - private val configuration: AvroConfiguration, -) : AbstractDecoder(), FieldDecoder { - - private var currentIndex = -1 - - @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.MAP -> MapDecoder( - descriptor, - fieldSchema(), - value as Map, - serializersModule, - configuration - ) - StructureKind.LIST -> { - val decoder: CompositeDecoder = if (descriptor.getElementDescriptor(0).kind == PrimitiveKind.BYTE) { - when (value) { - is List<*> -> ByteArrayDecoder((value as List).toByteArray(), serializersModule) - is Array<*> -> ByteArrayDecoder((value as Array).toByteArray(), serializersModule) - is ByteArray -> ByteArrayDecoder(value, serializersModule) - is ByteBuffer -> ByteArrayDecoder(value.array(), serializersModule) - is GenericFixed -> ByteArrayDecoder(value.bytes(), serializersModule) - else -> this - } - } else { - when (value) { - is List<*> -> ListDecoder(fieldSchema(), value, serializersModule, configuration) - is Array<*> -> ListDecoder(fieldSchema(), value.asList(), serializersModule, configuration) - else -> this - } - } - 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") - } - } - - private fun fieldValue(): Any? { - if (record.hasField(resolvedFieldName())) { - return record.get(resolvedFieldName()) - } - - FieldNaming(desc, currentIndex).aliases().forEach { - if (record.hasField(it)) { - return record.get(it) - } - } - - return null - } - - private fun resolvedFieldName(): String = configuration.namingStrategy.to(FieldNaming(desc, currentIndex).name()) - - private fun field(): Schema.Field = record.schema.getField(resolvedFieldName()) - - override fun fieldSchema(): Schema { - // if the element is nullable, then we should have a union schema which we can extract the non-null schema from - val schema = field().schema() - return if (schema.isNullable) { - schema.extractNonNull() - } else { - schema - } - } - - override fun decodeString(): String = StringFromAvroValue.fromValue(fieldValue()) - - override fun decodeBoolean(): Boolean { - return when (val v = fieldValue()) { - is Boolean -> v - null -> throw SerializationException("Cannot decode as a Boolean") - else -> throw SerializationException("Unsupported type for Boolean ${v.javaClass}") - } - } - - override fun decodeAny(): Any? = fieldValue() - - override fun decodeByte(): Byte { - return when (val v = fieldValue()) { - is Byte -> v - is Int -> if (v < 255) v.toByte() else throw SerializationException("Out of bound integer cannot be converted to byte [$v]") - null -> throw SerializationException("Cannot decode as a Byte") - else -> throw SerializationException("Unsupported type for Byte ${v.javaClass}") - } - } - - override fun decodeNotNullMark(): Boolean { - return fieldValue() != null - } - - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - val symbol = EnumFromAvroValue.fromValue(fieldValue()!!) - val enumValueByEnumName = - (0 until enumDescriptor.elementsCount).associateBy { enumDescriptor.getElementName(it) } - - return enumValueByEnumName[symbol] ?: AnnotationExtractor(enumDescriptor.annotations).enumDefault()?.let { - enumValueByEnumName[it] - } ?: -1 - } - - override fun decodeFloat(): Float { - return when (val v = fieldValue()) { - is Float -> v - null -> throw SerializationException("Cannot decode as a Float") - else -> throw SerializationException("Unsupported type for Float ${v.javaClass}") - } - } - - override fun decodeInt(): Int { - return when (val v = fieldValue()) { - is Int -> v - null -> throw SerializationException("Cannot decode as a Int") - else -> throw SerializationException("Unsupported type for Int ${v.javaClass}") - } - } - - override fun decodeShort(): Short { - return when (val v = fieldValue()) { - is Short -> v - is Int -> v.toShort() - null -> throw SerializationException("Cannot decode as a Short") - else -> throw SerializationException("Unsupported type for Short ${v.javaClass}") - } - } - - override fun decodeLong(): Long { - return when (val v = fieldValue()) { - is Long -> v - is Int -> v.toLong() - null -> throw SerializationException("Cannot decode as a Long") - else -> throw SerializationException("Unsupported type for Long [is ${v.javaClass}]") - } - } - - override fun decodeDouble(): Double { - return when (val v = fieldValue()) { - is Double -> v - is Float -> v.toDouble() - null -> throw SerializationException("Cannot decode as a Double") - else -> throw SerializationException("Unsupported type for Double ${v.javaClass}") - } - } - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - currentIndex++ - return if (currentIndex < descriptor.elementsCount) currentIndex else CompositeDecoder.DECODE_DONE - } -} - diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RootRecordDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RootRecordDecoder.kt deleted file mode 100644 index 7bcf4d66..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RootRecordDecoder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.AvroConfiguration -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.generic.GenericRecord - -@ExperimentalSerializationApi -class RootRecordDecoder( - private val record: GenericRecord, - override val serializersModule: SerializersModule, - private val configuration: AvroConfiguration, -) : AbstractDecoder() { - var decoded = false - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - return when (descriptor.kind) { - StructureKind.CLASS, StructureKind.OBJECT -> RecordDecoder( - descriptor, - record, - serializersModule, - configuration - ) - PolymorphicKind.SEALED -> UnionDecoder(descriptor, record, serializersModule, configuration) - else -> throw SerializationException("Non-class structure passed to root record decoder") - } - } - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - val index = if(decoded) DECODE_DONE else 0 - decoded = true - return index - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt deleted file mode 100644 index e5c3d890..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/UnionDecoder.kt +++ /dev/null @@ -1,58 +0,0 @@ -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 kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericRecord - -@ExperimentalSerializationApi -class UnionDecoder (descriptor: SerialDescriptor, - private val value: GenericRecord, - override val serializersModule: SerializersModule, - private val configuration: AvroConfiguration -) : AbstractDecoder(), FieldDecoder -{ - private enum class DecoderState(val index : Int){ - BEFORE(0), - READ_CLASS_NAME(1), - READ_DONE(CompositeDecoder.DECODE_DONE); - fun next() = values().firstOrNull{ it.ordinal > this.ordinal }?:READ_DONE - } - private var currentState = DecoderState.BEFORE - - private var leafDescriptor : SerialDescriptor = descriptor.possibleSerializationSubclasses(serializersModule).firstOrNull { - val schemaName = RecordNaming(value.schema.fullName, emptyList()) - val serialName = RecordNaming(it) - serialName == schemaName - }?:throw SerializationException("Cannot find a subtype of ${descriptor.serialName} that can be used to deserialize a record of schema ${value.schema}.") - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - val currentIndex = currentState.index - currentState = currentState.next() - return currentIndex - } - - override fun fieldSchema(): Schema = value.schema - - /** - * Decode string needs to return the class name of the actual decoded class. - */ - override fun decodeString(): String { - return leafDescriptor.serialName - } - - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - val recordDecoder = RootRecordDecoder(value, serializersModule, configuration) - return recordDecoder.decodeSerializableValue(deserializer) - } - - override fun decodeAny(): Any = UnsupportedOperationException() -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ByteArrayEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ByteArrayEncoder.kt deleted file mode 100644 index 0f02ddf3..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ByteArrayEncoder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import java.nio.ByteBuffer - -@ExperimentalSerializationApi -class ByteArrayEncoder(private val schema: Schema, - override val serializersModule: SerializersModule, - private val callback: (Any) -> Unit) : AbstractEncoder() { - - private val bytes = mutableListOf() - - override fun encodeByte(value: Byte) { - bytes.add(value) - } - - override fun endStructure(descriptor: SerialDescriptor) { - when (schema.type) { - Schema.Type.FIXED -> { - // the array passed in must be padded to size - val padding = schema.fixedSize - bytes.size - val padded = ByteBuffer.allocate(schema.fixedSize) - .put(ByteArray(padding) { 0 }) - .put(bytes.toByteArray()) - .array() - callback(GenericData.get().createFixed(null, padded, schema)) - } - //Wrapping the resulting byte array directly as this does not duplicate the byte array - Schema.Type.BYTES -> callback(ByteBuffer.wrap(bytes.toByteArray())) - else -> throw SerializationException("Cannot encode byte array when schema is ${schema.type}") - } - - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/FieldEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/FieldEncoder.kt deleted file mode 100644 index af1dcc5e..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/FieldEncoder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import kotlinx.serialization.encoding.Encoder -import org.apache.avro.Schema -import org.apache.avro.generic.GenericFixed -import java.nio.ByteBuffer - -interface ExtendedEncoder : Encoder { - fun encodeByteArray(buffer: ByteBuffer) - fun encodeFixed(fixed: GenericFixed) -} - -interface FieldEncoder : ExtendedEncoder { - fun addValue(value: Any) - fun fieldSchema(): Schema -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt deleted file mode 100644 index 34211c37..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ListEncoder.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericFixed -import java.nio.ByteBuffer - -@ExperimentalSerializationApi -class ListEncoder(private val schema: Schema, - override val serializersModule: SerializersModule, - private val callback: (GenericData.Array) -> Unit) : AbstractEncoder(), StructureEncoder { - - private val list = mutableListOf() - - override fun endStructure(descriptor: SerialDescriptor) { - val generic = GenericData.Array(schema, list.toList()) - callback(generic) - } - - override fun fieldSchema(): Schema = schema.elementType - - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - return super.beginStructure(descriptor) - } - - override fun addValue(value: Any) { - list.add(value) - } - - override fun encodeNull() { - list.add(null) - } - - override fun encodeString(value: String) { - list.add(StringToAvroValue.toValue(schema, value)) - } - - override fun encodeLong(value: Long) { - list.add(value) - } - - override fun encodeDouble(value: Double) { - list.add(value) - } - - override fun encodeBoolean(value: Boolean) { - list.add(value) - } - - override fun encodeShort(value: Short) { - list.add(value) - } - - override fun encodeByteArray(buffer: ByteBuffer) { - list.add(buffer) - } - - override fun encodeFixed(fixed: GenericFixed) { - list.add(fixed) - } - - override fun encodeByte(value: Byte) { - list.add(value) - } - - override fun encodeFloat(value: Float) { - list.add(value) - } - - override fun encodeInt(value: Int) { - list.add(value) - } - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - list.add(ValueToEnum.toValue(fieldSchema(), enumDescriptor, index)) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt deleted file mode 100644 index 11776dfb..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/MapEncoder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericFixed -import org.apache.avro.util.Utf8 -import java.nio.ByteBuffer - -@ExperimentalSerializationApi -class MapEncoder(schema: Schema, - override val serializersModule: SerializersModule, - private val callback: (Map) -> Unit) : AbstractEncoder(), - CompositeEncoder, - StructureEncoder { - - private val map = mutableMapOf() - private var key: Utf8? = null - private val valueSchema = schema.valueType - - override fun encodeString(value: String) { - val k = key - if (k == null) key = Utf8(value) else { - map[k] = StringToAvroValue.toValue(valueSchema, value) - key = null - } - } - - override fun encodeValue(value: Any) { - val k = key - if (k == null) throw SerializationException("Expected key but received value $value") else { - map[k] = value - key = null - } - } - - override fun encodeNull() { - val k = key - if (k == null) throw SerializationException("Expected key but received null value") else { - map[k] = null - key = null - } - } - - override fun endStructure(descriptor: SerialDescriptor) { - callback(map.toMap()) - } - - override fun encodeByteArray(buffer: ByteBuffer) { - encodeValue(buffer) - } - - override fun encodeFixed(fixed: GenericFixed) { - encodeValue(fixed) - } - - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - return super.beginStructure(descriptor) - } - - override fun addValue(value: Any) { - encodeValue(value) - } - - override fun fieldSchema(): Schema = valueSchema -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt deleted file mode 100644 index bf9f2f1b..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RecordEncoder.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import com.github.avrokotlin.avro4k.AnnotationExtractor -import com.github.avrokotlin.avro4k.ListRecord -import com.github.avrokotlin.avro4k.Record -import com.github.avrokotlin.avro4k.schema.extractNonNull -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.generic.GenericFixed -import java.nio.ByteBuffer - -@ExperimentalSerializationApi -interface StructureEncoder : FieldEncoder { - - 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) } - } - } - StructureKind.CLASS -> RecordEncoder(fieldSchema(), serializersModule) { addValue(it) } - StructureKind.MAP -> MapEncoder(fieldSchema(), serializersModule) { addValue(it) } - is PolymorphicKind -> UnionEncoder(fieldSchema(), serializersModule) { addValue(it) } - else -> throw SerializationException(".beginStructure was called on a non-structure type [$descriptor]") - } - } -} - -@ExperimentalSerializationApi -class RecordEncoder(private val schema: Schema, - override val serializersModule: SerializersModule, - val callback: (Record) -> Unit) : AbstractEncoder(), StructureEncoder { - - private val builder = RecordBuilder(schema) - private var currentIndex = -1 - - override fun fieldSchema(): Schema { - // if the element is nullable, then we should have a union schema which we can extract the non-null schema from - val currentFieldSchema = schema.fields[currentIndex].schema() - return if (currentFieldSchema.isNullable) { - currentFieldSchema.extractNonNull() - } else { - currentFieldSchema - } - } - - override fun addValue(value: Any) { - builder.add(value) - } - - override fun encodeString(value: String) { - builder.add(StringToAvroValue.toValue(fieldSchema(), value)) - } - - override fun encodeValue(value: Any) { - builder.add(value) - } - - override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { - currentIndex = index - return true - } - - override fun encodeByteArray(buffer: ByteBuffer) { - builder.add(buffer) - } - - override fun encodeFixed(fixed: GenericFixed) { - builder.add(fixed) - } - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - builder.add(ValueToEnum.toValue(fieldSchema(), enumDescriptor, index)) - } - - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - // if we have a value type, then we don't want to begin a new structure - return if (AnnotationExtractor(descriptor.annotations).valueType()) - this - else - super.beginStructure(descriptor) - } - - override fun endStructure(descriptor: SerialDescriptor) { - callback(builder.record()) - } - - override fun encodeNull() { - builder.add(null) - } -} - -class RecordBuilder(private val schema: Schema) { - - private val values = ArrayList(schema.fields.size) - - fun add(value: Any?) = values.add(value) - - fun record(): Record = ListRecord(schema, values) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt deleted file mode 100644 index 41b7aee2..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/RootRecordEncoder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import com.github.avrokotlin.avro4k.Record -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema - -@ExperimentalSerializationApi -class RootRecordEncoder(private val schema: Schema, - override val serializersModule: SerializersModule, - 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) - else -> throw SerializationException("Unsupported root element passed to root record encoder") - } - } - - override fun endStructure(descriptor: SerialDescriptor) { - - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ToAvroValue.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ToAvroValue.kt deleted file mode 100644 index 6878a6d5..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/ToAvroValue.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import com.github.avrokotlin.avro4k.schema.extractNonNull -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import org.apache.avro.AvroRuntimeException -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 -import java.nio.ByteBuffer - -object StringToAvroValue { - fun toValue(schema: Schema, t: String): Any { - return when (schema.type) { - Schema.Type.FIXED -> { - val size = t.toByteArray().size - if (size > schema.fixedSize) - throw AvroRuntimeException("Cannot write string with $size bytes to fixed type of size ${schema.fixedSize}") - // the array passed in must be padded to size - val bytes = ByteBuffer.allocate(schema.fixedSize).put(t.toByteArray()).array() - GenericData.get().createFixed(null, bytes, schema) - } - Schema.Type.BYTES -> ByteBuffer.wrap(t.toByteArray()) - else -> Utf8(t) - } - } -} -@ExperimentalSerializationApi -object ValueToEnum { - fun toValue(schema: Schema, enumDescription: SerialDescriptor, ordinal: Int): GenericData.EnumSymbol { - // the schema provided will be a union, so we should extract the correct schema - val symbol = enumDescription.getElementName(ordinal) - val nonNullSchema = schema.extractNonNull() - return GenericData.get().createEnum(symbol, nonNullSchema) as GenericData.EnumSymbol - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt deleted file mode 100644 index 6c955be2..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/encoder/UnionEncoder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.avrokotlin.avro4k.encoder - -import com.github.avrokotlin.avro4k.Record -import com.github.avrokotlin.avro4k.RecordNaming -import com.github.avrokotlin.avro4k.schema.DefaultNamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractEncoder -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema - -@ExperimentalSerializationApi -class UnionEncoder(private val unionSchema : Schema, - override val serializersModule: SerializersModule, - private val callback: (Record) -> Unit) : AbstractEncoder() { - override fun encodeString(value: String){ - //No need to encode the name of the concrete type. The name will never be encoded in the avro schema. - } - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - return when (descriptor.kind) { - is StructureKind.CLASS, is StructureKind.OBJECT -> { - //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) - serialName == schemaName - } - RecordEncoder(leafSchema, serializersModule, callback) - } - else -> throw SerializationException("Unsupported root element passed to root record encoder") - } - } - - override fun endStructure(descriptor: SerialDescriptor) { - - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/AvroInternalExtensions.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/AvroInternalExtensions.kt new file mode 100644 index 00000000..e71fd901 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/AvroInternalExtensions.kt @@ -0,0 +1,27 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.decoder.direct.AvroValueDirectDecoder +import com.github.avrokotlin.avro4k.internal.encoder.direct.AvroValueDirectEncoder +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import org.apache.avro.Schema + +internal fun Avro.encodeWithBinaryEncoder( + writerSchema: Schema, + serializer: SerializationStrategy, + value: T, + binaryEncoder: org.apache.avro.io.Encoder, +) { + AvroValueDirectEncoder(writerSchema, this, binaryEncoder) + .encodeSerializableValue(serializer, value) +} + +internal fun Avro.decodeWithBinaryDecoder( + writerSchema: Schema, + deserializer: DeserializationStrategy, + binaryDecoder: org.apache.avro.io.Decoder, +): T { + return AvroValueDirectDecoder(writerSchema, this, binaryDecoder) + .decodeSerializableValue(deserializer) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt new file mode 100644 index 00000000..4f21a9a3 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt @@ -0,0 +1,30 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroEnumDefault +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.util.WeakIdentityHashMap + +internal class EnumResolver { + private val defaultIndexCache: MutableMap = WeakIdentityHashMap() + + private data class EnumDefault(val index: Int?) + + fun getDefaultValueIndex(enumDescriptor: SerialDescriptor): Int? { + return defaultIndexCache.getOrPut(enumDescriptor) { + loadCache(enumDescriptor) + }.index + } + + private fun loadCache(enumDescriptor: SerialDescriptor): EnumDefault { + var foundIndex: Int? = null + for (i in 0 until enumDescriptor.elementsCount) { + if (enumDescriptor.getElementAnnotations(i).any { it is AvroEnumDefault }) { + if (foundIndex != null) { + throw UnsupportedOperationException("Multiple default values found in enum $enumDescriptor") + } + foundIndex = i + } + } + return EnumDefault(foundIndex) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/NumberUtils.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/NumberUtils.kt new file mode 100644 index 00000000..480385eb --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/NumberUtils.kt @@ -0,0 +1,88 @@ +package com.github.avrokotlin.avro4k.internal + +import kotlinx.serialization.SerializationException +import java.math.BigDecimal + +internal fun BigDecimal.toLongExact(): Long { + if (this.toLong().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Long") + } + return this.toLong() +} + +internal fun Int.toByteExact(): Byte { + if (this.toByte().toInt() != this) { + throw SerializationException("Value $this is not a valid Byte") + } + return this.toByte() +} + +internal fun Long.toByteExact(): Byte { + if (this.toByte().toLong() != this) { + throw SerializationException("Value $this is not a valid Byte") + } + return this.toByte() +} + +internal fun BigDecimal.toByteExact(): Byte { + if (this.toInt().toByte().toInt().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Byte") + } + return this.toInt().toByte() +} + +internal fun Int.toShortExact(): Short { + if (this.toShort().toInt() != this) { + throw SerializationException("Value $this is not a valid Short") + } + return this.toShort() +} + +internal fun Long.toShortExact(): Short { + if (this.toShort().toLong() != this) { + throw SerializationException("Value $this is not a valid Short") + } + return this.toShort() +} + +internal fun BigDecimal.toShortExact(): Short { + if (this.toInt().toShort().toInt().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Short") + } + return this.toInt().toShort() +} + +internal fun Long.toIntExact(): Int { + if (this.toInt().toLong() != this) { + throw SerializationException("Value $this is not a valid Int") + } + return this.toInt() +} + +internal fun BigDecimal.toIntExact(): Int { + if (this.toInt().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Int") + } + return this.toInt() +} + +internal fun BigDecimal.toFloatExact(): Float { + if (this.toFloat().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Float") + } + return this.toFloat() +} + +internal fun Double.toFloatExact(): Float { + if (this.toFloat().toDouble() != this) { + throw SerializationException("Value $this is not a valid Float") + } + return this.toFloat() +} + +internal fun BigDecimal.toDoubleExact(): Double { + if (this.toDouble().toBigDecimal() != this) { + throw SerializationException("Value $this is not a valid Double") + } + return this.toDouble() +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/PolymorphicResolver.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/PolymorphicResolver.kt new file mode 100644 index 00000000..20b1b661 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/PolymorphicResolver.kt @@ -0,0 +1,24 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroAlias +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.util.WeakIdentityHashMap + +internal class PolymorphicResolver(private val serializersModule: SerializersModule) { + private val cache = WeakIdentityHashMap>() + + fun getFullNamesAndAliasesToSerialName(descriptor: SerialDescriptor): Map { + return cache.getOrPut(descriptor) { + descriptor.possibleSerializationSubclasses(serializersModule) + .flatMap { + sequence { + yield(it.nonNullSerialName to it.nonNullSerialName) + it.findAnnotation()?.value?.forEach { alias -> + yield(alias to it.nonNullSerialName) + } + } + }.toMap() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt new file mode 100644 index 00000000..1614fb47 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt @@ -0,0 +1,419 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.internal.schema.CHAR_LOGICAL_TYPE_NAME +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.double +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.long +import org.apache.avro.Schema +import org.apache.avro.generic.GenericData +import org.apache.avro.util.WeakIdentityHashMap +import java.util.WeakHashMap + +internal class RecordResolver( + private val avro: Avro, +) { + /** + * For a class descriptor + writerSchema, it returns a map of the field index to the schema field. + * + * Note: We use the descriptor in the key as we could have multiple descriptors for the same record schema, and multiple record schemas for the same descriptor. + */ + private val fieldCache: MutableMap> = WeakIdentityHashMap() + + /** + * Maps the class fields to the schema fields. + * For encoding: + * - prepares the fields to be encoded in the order they are in the writer schema + * - + * + * For decoding: + * - prepares the fields to be decoded in the order they are in the writer schema + * - prepares the default values for the fields that are not in the writer schema + * - prepares the fields to be skipped as they are in the writer schema but not in the class descriptor + * - fails if a field is not optional, without a default value and not in the writer schema + */ + fun resolveFields( + writerSchema: Schema, + classDescriptor: SerialDescriptor, + ): ClassDescriptorForWriterSchema { + if (classDescriptor.elementsCount == 0) { + return ClassDescriptorForWriterSchema.EMPTY + } + return fieldCache.getOrPut(classDescriptor) { WeakHashMap() }.getOrPut(writerSchema) { + loadCache(classDescriptor, writerSchema) + } + } + + /** + * Here the different steps to get the schema field corresponding to the serial descriptor element: + * - class field name -> schema field name + * - class field name -> schema field aliases + * - class field aliases -> schema field name + * - class field aliases -> schema field aliases + * - if field is optional, returns null + * - if still not found, [SerializationException] thrown + */ + private fun loadCache( + classDescriptor: SerialDescriptor, + writerSchema: Schema, + ): ClassDescriptorForWriterSchema { + val readerSchema = avro.schema(classDescriptor) + + val encodingSteps = computeEncodingSteps(classDescriptor, writerSchema) + return ClassDescriptorForWriterSchema( + sequentialEncoding = encodingSteps.areWriterFieldsSequentiallyOrdered(), + computeDecodingSteps(classDescriptor, writerSchema, readerSchema), + encodingSteps + ) + } + + private fun Array.areWriterFieldsSequentiallyOrdered(): Boolean { + var lastWriterFieldIndex = -1 + forEach { step -> + when (step) { + is EncodingStep.SerializeWriterField -> { + if (step.writerFieldIndex > lastWriterFieldIndex) { + lastWriterFieldIndex = step.writerFieldIndex + } else { + return false + } + } + + is EncodingStep.MissingWriterFieldFailure -> { + if (step.writerFieldIndex > lastWriterFieldIndex) { + lastWriterFieldIndex = step.writerFieldIndex + } else { + return false + } + } + + is EncodingStep.IgnoreElement -> { + // nothing to check + } + } + } + return true + } + + private fun computeDecodingSteps( + classDescriptor: SerialDescriptor, + writerSchema: Schema, + readerSchema: Schema, + ): Array { + val decodingSteps = mutableListOf() + val elementIndexByWriterFieldIndex = + classDescriptor.elementNames + .mapIndexedNotNull { elementIndex, _ -> + writerSchema.tryGetField(avro.configuration.fieldNamingStrategy.resolve(classDescriptor, elementIndex), classDescriptor, elementIndex) + ?.let { it.pos() to elementIndex } + }.toMap().toMutableMap() + val visitedElements = BooleanArray(classDescriptor.elementsCount) { false } + + writerSchema.fields.forEachIndexed { writerFieldIndex, field -> + decodingSteps += elementIndexByWriterFieldIndex.remove(writerFieldIndex) + ?.let { elementIndex -> + visitedElements[elementIndex] = true + DecodingStep.DeserializeWriterField( + elementIndex = elementIndex, + writerFieldIndex = writerFieldIndex, + schema = field.schema() + ) + } ?: DecodingStep.SkipWriterField(writerFieldIndex, field.schema()) + } + + // iterate over remaining elements in the class descriptor that are not in the writer schema + visitedElements.forEachIndexed { elementIndex, visited -> + if (visited) return@forEachIndexed + + val readerDefaultAnnotation = classDescriptor.findElementAnnotation(elementIndex) + val readerField = readerSchema.fields[elementIndex] + + decodingSteps += + if (readerDefaultAnnotation != null) { + DecodingStep.GetDefaultValue( + elementIndex = elementIndex, + schema = readerField.schema(), + defaultValue = readerDefaultAnnotation.parseValueToGenericData(readerField.schema()) + ) + } else if (classDescriptor.isElementOptional(elementIndex)) { + DecodingStep.IgnoreOptionalElement(elementIndex) + } else if (avro.configuration.implicitNulls && readerField.schema().isNullable) { + DecodingStep.GetDefaultValue( + elementIndex = elementIndex, + schema = readerField.schema().asSchemaList().first { it.type === Schema.Type.NULL }, + defaultValue = null + ) + } else if (avro.configuration.implicitEmptyCollections && readerField.schema().isTypeOf(Schema.Type.ARRAY)) { + DecodingStep.GetDefaultValue( + elementIndex = elementIndex, + schema = readerField.schema().asSchemaList().first { it.type === Schema.Type.ARRAY }, + defaultValue = emptyList() + ) + } else if (avro.configuration.implicitEmptyCollections && readerField.schema().isTypeOf(Schema.Type.MAP)) { + DecodingStep.GetDefaultValue( + elementIndex = elementIndex, + schema = readerField.schema().asSchemaList().first { it.type === Schema.Type.MAP }, + defaultValue = emptyMap() + ) + } else { + DecodingStep.MissingElementValueFailure(elementIndex) + } + } + return decodingSteps.toTypedArray() + } + + private fun Schema.isTypeOf(expectedType: Schema.Type): Boolean { + return asSchemaList().any { it.type === expectedType } + } + + private fun computeEncodingSteps( + classDescriptor: SerialDescriptor, + writerSchema: Schema, + ): Array { + // Encoding steps are ordered regarding the class descriptor and not the writer schema. + // Because kotlinx-serialization doesn't provide a way to encode non-sequentially elements. + val encodingSteps = mutableListOf() + val visitedWriterFields = BooleanArray(writerSchema.fields.size) { false } + + classDescriptor.elementNames.forEachIndexed { elementIndex, _ -> + val avroFieldName = avro.configuration.fieldNamingStrategy.resolve(classDescriptor, elementIndex) + val writerField = writerSchema.tryGetField(avroFieldName, classDescriptor, elementIndex) + + if (writerField != null) { + visitedWriterFields[writerField.pos()] = true + encodingSteps += + EncodingStep.SerializeWriterField( + elementIndex = elementIndex, + writerFieldIndex = writerField.pos(), + schema = writerField.schema() + ) + } else { + encodingSteps += EncodingStep.IgnoreElement(elementIndex) + } + } + + visitedWriterFields.forEachIndexed { writerFieldIndex, visited -> + if (!visited) { + encodingSteps += EncodingStep.MissingWriterFieldFailure(writerFieldIndex) + } + } + + return encodingSteps.toTypedArray() + } + + private fun Schema.tryGetField( + avroFieldName: String, + classDescriptor: SerialDescriptor, + elementIndex: Int, + ): Schema.Field? = + getField(avroFieldName) + ?: fields.firstOrNull { avroFieldName in it.aliases() } + ?: classDescriptor.findElementAnnotation(elementIndex)?.value?.let { aliases -> + fields.firstOrNull { schemaField -> + schemaField.name() in aliases || schemaField.aliases().any { it in aliases } + } + } +} + +internal class ClassDescriptorForWriterSchema( + /** + * If true, indicates that the encoding steps are ordered the same as the writer schema fields. + * If false, indicates that the encoding steps are **NOT** ordered the same as the writer schema fields. + */ + val sequentialEncoding: Boolean, + /** + * Decoding steps are ordered regarding the writer schema and not the class descriptor. + */ + val decodingSteps: Array, + /** + * Encoding steps are ordered regarding the class descriptor and not the writer schema. + */ + val encodingSteps: Array, +) { + val hasMissingWriterField by lazy { encodingSteps.any { it is EncodingStep.MissingWriterFieldFailure } } + + companion object { + val EMPTY = + ClassDescriptorForWriterSchema( + sequentialEncoding = true, + decodingSteps = emptyArray(), + encodingSteps = emptyArray() + ) + } +} + +internal sealed interface DecodingStep { + /** + * This is a flag indicating that the element is deserializable. + */ + sealed interface ValidatedDecodingStep : DecodingStep { + val elementIndex: Int + val schema: Schema + } + + /** + * The element is present in the writer schema and the class descriptor. + */ + data class DeserializeWriterField( + override val elementIndex: Int, + val writerFieldIndex: Int, + override val schema: Schema, + ) : DecodingStep, ValidatedDecodingStep + + /** + * The element is present in the class descriptor but not in the writer schema, so the default value is used. + * Also: + * - if the [com.github.avrokotlin.avro4k.AvroConfiguration.implicitNulls] is enabled, the default value is `null`. + * - if the [com.github.avrokotlin.avro4k.AvroConfiguration.implicitEmptyCollections] is enabled, the default value is an empty array or map. + */ + data class GetDefaultValue( + override val elementIndex: Int, + override val schema: Schema, + val defaultValue: Any?, + ) : DecodingStep, ValidatedDecodingStep + + /** + * The element is present in the writer schema but not in the class descriptor, so it is skipped. + */ + data class SkipWriterField( + val writerFieldIndex: Int, + val schema: Schema, + ) : DecodingStep + + /** + * The element is present in the class descriptor but not in the writer schema. + * Also, the element don't have a default value but is optional, so we can skip the element. + */ + data class IgnoreOptionalElement( + val elementIndex: Int, + ) : DecodingStep + + /** + * The element is present in the class descriptor but not in the writer schema. + * It does not have any default value and is not optional, so it fails as it cannot get any value for it. + */ + data class MissingElementValueFailure( + val elementIndex: Int, + ) : DecodingStep +} + +internal sealed interface EncodingStep { + /** + * The element is present in the writer schema and the class descriptor. + */ + data class SerializeWriterField( + val elementIndex: Int, + val writerFieldIndex: Int, + val schema: Schema, + ) : EncodingStep + + /** + * The element is present in the class descriptor but not in the writer schema, so the element is ignored as nothing has to be serialized. + */ + data class IgnoreElement( + val elementIndex: Int, + ) : EncodingStep + + /** + * The writer field doesn't have a corresponding element in the class descriptor, so we aren't able to serialize a value. + */ + data class MissingWriterFieldFailure( + val writerFieldIndex: Int, + ) : EncodingStep +} + +private fun AvroDefault.parseValueToGenericData(schema: Schema): Any? { + if (value.isStartingAsJson()) { + return Json.parseToJsonElement(value).convertDefaultToObject(schema) + } + return JsonPrimitive(value).convertDefaultToObject(schema) +} + +private fun JsonElement.convertDefaultToObject(schema: Schema): Any? { + return when (this) { + is JsonArray -> + when (schema.type) { + Schema.Type.ARRAY -> this.map { it.convertDefaultToObject(schema.elementType) } + Schema.Type.UNION -> this.convertDefaultToObject(schema.resolveUnion(this, Schema.Type.ARRAY)) + else -> throw SerializationException("Not a valid array value for schema $schema: $this") + } + + is JsonNull -> null + is JsonObject -> + when (schema.type) { + Schema.Type.RECORD -> { + GenericData.Record(schema).apply { + entries.forEach { (fieldName, value) -> + val schemaField = schema.getField(fieldName) + put(schemaField.pos(), value.convertDefaultToObject(schemaField.schema())) + } + } + } + + Schema.Type.MAP -> entries.associate { (key, value) -> key to value.convertDefaultToObject(schema.valueType) } + Schema.Type.UNION -> this.convertDefaultToObject(schema.resolveUnion(this, Schema.Type.RECORD, Schema.Type.MAP)) + else -> throw SerializationException("Not a valid record value for schema $schema: $this") + } + + is JsonPrimitive -> + when (schema.type) { + Schema.Type.BYTES -> this.content.toByteArray() + Schema.Type.FIXED -> GenericData.Fixed(schema, this.content.toByteArray()) + Schema.Type.STRING -> this.content + Schema.Type.ENUM -> this.content + Schema.Type.BOOLEAN -> this.boolean + + Schema.Type.INT -> + when (schema.logicalType?.name) { + CHAR_LOGICAL_TYPE_NAME -> this.content.single().code + else -> this.int + } + + Schema.Type.LONG -> this.long + Schema.Type.FLOAT -> this.float + Schema.Type.DOUBLE -> this.double + + Schema.Type.UNION -> + this.convertDefaultToObject( + schema.resolveUnion( + this, + Schema.Type.BYTES, + Schema.Type.FIXED, + Schema.Type.STRING, + Schema.Type.ENUM, + Schema.Type.BOOLEAN, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + ) + + else -> throw SerializationException("Not a valid primitive value for schema $schema: $this") + } + } +} + +private fun Schema.resolveUnion( + value: JsonElement?, + vararg expectedTypes: Schema.Type, +): Schema { + val index = types.indexOfFirst { it.type in expectedTypes } + if (index < 0) { + throw SerializationException("Union type does not contain one of ${expectedTypes.asList()}, unable to convert default value '$value' for schema $this") + } + return types[index] +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt new file mode 100644 index 00000000..36486eea --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt @@ -0,0 +1,190 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.internal.decoder.direct.AbstractAvroDirectDecoder +import com.github.avrokotlin.avro4k.serializer.AvroDuration +import com.github.avrokotlin.avro4k.serializer.AvroDurationSerializer +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext +import com.github.avrokotlin.avro4k.serializer.SerialDescriptorWithAvroSchemaDelegate +import com.github.avrokotlin.avro4k.serializer.createSchema +import com.github.avrokotlin.avro4k.serializer.fixed +import com.github.avrokotlin.avro4k.serializer.stringable +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.internal.AbstractCollectionSerializer +import org.apache.avro.Schema +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds + +/** + * This middleware is here to intercept some native types like kotlin Duration or ByteArray as we want to apply some + * specific rules on them for generating custom schemas or having specific serialization strategies. + */ +@Suppress("UNCHECKED_CAST") +internal object SerializerLocatorMiddleware { + fun apply(serializer: SerializationStrategy): SerializationStrategy { + return when { + serializer === ByteArraySerializer() -> AvroByteArraySerializer + serializer === Duration.serializer() -> KotlinDurationSerializer + else -> serializer + } as SerializationStrategy + } + + @OptIn(InternalSerializationApi::class) + fun apply(deserializer: DeserializationStrategy): DeserializationStrategy { + return when { + deserializer === ByteArraySerializer() -> AvroByteArraySerializer + deserializer === Duration.serializer() -> KotlinDurationSerializer + deserializer is AbstractCollectionSerializer<*, T, *> -> AvroCollectionSerializer(deserializer) + else -> deserializer + } as DeserializationStrategy + } + + fun apply(descriptor: SerialDescriptor): SerialDescriptor { + return when { + descriptor === ByteArraySerializer().descriptor -> AvroByteArraySerializer.descriptor + descriptor === String.serializer().descriptor -> AvroStringSerialDescriptor + descriptor === Duration.serializer().descriptor -> KotlinDurationSerializer.descriptor + else -> descriptor + } + } +} + +private val AvroStringSerialDescriptor: SerialDescriptor = + SerialDescriptorWithAvroSchemaDelegate(String.serializer().descriptor) { context -> + context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() ?: it.fixed?.createSchema(it) + } ?: Schema.create(Schema.Type.STRING) + } + +private object KotlinDurationSerializer : AvroSerializer(Duration::class.qualifiedName!!) { + private const val MILLIS_PER_DAY = 1000 * 60 * 60 * 24 + + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { it.stringable?.createSchema() } + ?: AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Duration, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Duration { + return AvroDurationSerializer.deserializeAvro(decoder).toKotlinDuration() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Duration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Duration { + return Duration.parse(decoder.decodeString()) + } + + private fun AvroDuration.toKotlinDuration(): Duration { + if (months == UInt.MAX_VALUE && days == UInt.MAX_VALUE && millis == UInt.MAX_VALUE) { + return Duration.INFINITE + } + if (months != 0u) { + throw SerializationException("java.time.Duration cannot contains months") + } + return days.toLong().days + millis.toLong().milliseconds + } + + private fun Duration.toAvroDuration(): AvroDuration { + if (isNegative()) { + throw SerializationException("${Duration::class.qualifiedName} cannot be converted to ${AvroDuration::class.qualifiedName} as it cannot be negative") + } + if (isInfinite()) { + return AvroDuration( + months = UInt.MAX_VALUE, + days = UInt.MAX_VALUE, + millis = UInt.MAX_VALUE + ) + } + val millis = inWholeMilliseconds + return AvroDuration( + months = 0u, + days = (millis / MILLIS_PER_DAY).toUInt(), + millis = (millis % MILLIS_PER_DAY).toUInt() + ) + } +} + +private object AvroByteArraySerializer : AvroSerializer(ByteArray::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() ?: it.fixed?.createSchema(it) + } ?: Schema.create(Schema.Type.BYTES) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: ByteArray, + ) { + // encoding related to the type (fixed or bytes) is handled in AvroEncoder + encoder.encodeBytes(value) + } + + override fun deserializeAvro(decoder: AvroDecoder): ByteArray { + // decoding related to the type (fixed or bytes) is handled in AvroDecoder + return decoder.decodeBytes() + } + + @OptIn(ExperimentalEncodingApi::class) + override fun serializeGeneric( + encoder: Encoder, + value: ByteArray, + ) { + encoder.encodeString(Base64.Mime.encode(value)) + } + + @OptIn(ExperimentalEncodingApi::class) + override fun deserializeGeneric(decoder: Decoder): ByteArray { + return Base64.Mime.decode(decoder.decodeString()) + } +} + +@OptIn(InternalSerializationApi::class) +internal class AvroCollectionSerializer(private val original: AbstractCollectionSerializer<*, T, *>) : KSerializer { + override val descriptor: SerialDescriptor + get() = original.descriptor + + override fun deserialize(decoder: Decoder): T { + if (decoder is AbstractAvroDirectDecoder) { + var result: T? = null + decoder.decodedCollectionSize = -1 + do { + result = original.merge(decoder, result) + } while (decoder.decodedCollectionSize > 0) + return result!! + } + return original.deserialize(decoder) + } + + override fun serialize( + encoder: Encoder, + value: T, + ) { + original.serialize(encoder, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt new file mode 100644 index 00000000..431615e5 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt @@ -0,0 +1,65 @@ +package com.github.avrokotlin.avro4k.internal.decoder + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import com.github.avrokotlin.avro4k.internal.isNamedSchema +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema + +internal abstract class AbstractPolymorphicDecoder( + protected val avro: Avro, + private val descriptor: SerialDescriptor, + private val schema: Schema, +) : AbstractDecoder() { + final override val serializersModule: SerializersModule + get() = avro.serializersModule + + private lateinit var chosenSchema: Schema + + final override fun decodeString(): String { + return tryFindSerialName()?.also { chosenSchema = it.second }?.first + ?: throw SerializationException("Unknown schema name '${schema.fullName}' for polymorphic type ${descriptor.serialName}. Full schema: $schema") + } + + private fun tryFindSerialName(): Pair? { + val namesAndAliasesToSerialName: Map = avro.polymorphicResolver.getFullNamesAndAliasesToSerialName(descriptor) + return tryFindSerialName(namesAndAliasesToSerialName, schema) + } + + protected abstract fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? + + protected fun tryFindSerialName( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + if (schema.isUnion) { + return tryFindSerialNameForUnion(namesAndAliasesToSerialName, schema) + } + return ( + namesAndAliasesToSerialName[schema.fullName] + ?: schema.takeIf { it.isNamedSchema() }?.aliases?.firstNotNullOfOrNull { namesAndAliasesToSerialName[it] } + ) + ?.let { it to schema } + } + + final override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return newDecoder(chosenSchema) + .decodeSerializableValue(deserializer) + } + + abstract fun newDecoder(chosenSchema: Schema): Decoder + + final override fun decodeSequentially() = true + + final override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt new file mode 100644 index 00000000..c8fdde60 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt @@ -0,0 +1,459 @@ +package com.github.avrokotlin.avro4k.internal.decoder.direct + +import com.github.avrokotlin.avro4k.AnyValueDecoder +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.BooleanValueDecoder +import com.github.avrokotlin.avro4k.CharValueDecoder +import com.github.avrokotlin.avro4k.DoubleValueDecoder +import com.github.avrokotlin.avro4k.FloatValueDecoder +import com.github.avrokotlin.avro4k.IntValueDecoder +import com.github.avrokotlin.avro4k.LongValueDecoder +import com.github.avrokotlin.avro4k.UnionDecoder +import com.github.avrokotlin.avro4k.decodeResolvingAny +import com.github.avrokotlin.avro4k.decodeResolvingBoolean +import com.github.avrokotlin.avro4k.decodeResolvingChar +import com.github.avrokotlin.avro4k.decodeResolvingDouble +import com.github.avrokotlin.avro4k.decodeResolvingFloat +import com.github.avrokotlin.avro4k.decodeResolvingInt +import com.github.avrokotlin.avro4k.decodeResolvingLong +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware +import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import com.github.avrokotlin.avro4k.internal.decoder.AbstractPolymorphicDecoder +import com.github.avrokotlin.avro4k.internal.getElementIndexNullable +import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch +import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import com.github.avrokotlin.avro4k.internal.toByteExact +import com.github.avrokotlin.avro4k.internal.toFloatExact +import com.github.avrokotlin.avro4k.internal.toIntExact +import com.github.avrokotlin.avro4k.internal.toShortExact +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericFixed + +internal abstract class AbstractAvroDirectDecoder( + protected val avro: Avro, + protected val binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractInterceptingDecoder(), UnionDecoder { + abstract override var currentWriterSchema: Schema + internal var decodedCollectionSize = -1 + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + @Deprecated("Do not use it for direct encoding") + final override fun decodeValue(): Any { + throw UnsupportedOperationException("Direct decoding doesn't support decoding generic values") + } + + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return SerializerLocatorMiddleware.apply(deserializer) + .deserialize(this) + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + return when (descriptor.kind) { + StructureKind.LIST -> + decodeResolvingAny({ UnexpectedDecodeSchemaError(descriptor.nonNullSerialName, Schema.Type.ARRAY) }) { + when (it.type) { + Schema.Type.ARRAY -> { + AnyValueDecoder { ArrayBlockDirectDecoder(it, decodeFirstBlock = decodedCollectionSize == -1, { decodedCollectionSize = it }, avro, binaryDecoder) } + } + + else -> null + } + } + + StructureKind.MAP -> + decodeResolvingAny({ UnexpectedDecodeSchemaError(descriptor.nonNullSerialName, Schema.Type.MAP) }) { + when (it.type) { + Schema.Type.MAP -> { + AnyValueDecoder { MapBlockDirectDecoder(it, decodeFirstBlock = decodedCollectionSize == -1, { decodedCollectionSize = it }, avro, binaryDecoder) } + } + + else -> null + } + } + + StructureKind.CLASS, StructureKind.OBJECT -> + decodeResolvingAny({ UnexpectedDecodeSchemaError(descriptor.nonNullSerialName, Schema.Type.RECORD) }) { + when (it.type) { + Schema.Type.RECORD -> { + AnyValueDecoder { RecordDirectDecoder(it, descriptor, avro, binaryDecoder) } + } + + else -> null + } + } + + is PolymorphicKind -> PolymorphicDecoder(avro, descriptor, currentWriterSchema, binaryDecoder) + else -> throw SerializationException("Unsupported descriptor for structure decoding: $descriptor") + } + } + + override fun decodeAndResolveUnion() { + if (currentWriterSchema.isUnion) { + currentWriterSchema = currentWriterSchema.types[binaryDecoder.readIndex()] + } + } + + override fun decodeNotNullMark(): Boolean { + decodeAndResolveUnion() + return currentWriterSchema.type != Schema.Type.NULL + } + + override fun decodeNull(): Nothing? { + decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "null", + Schema.Type.NULL + ) + }) { + when (it.type) { + Schema.Type.NULL -> { + AnyValueDecoder { binaryDecoder.readNull() } + } + + else -> null + } + } + return null + } + + override fun decodeBoolean(): Boolean { + return decodeResolvingBoolean({ + UnexpectedDecodeSchemaError( + "boolean", + Schema.Type.BOOLEAN, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.BOOLEAN -> { + BooleanValueDecoder { binaryDecoder.readBoolean() } + } + + Schema.Type.STRING -> { + BooleanValueDecoder { binaryDecoder.readString().toBooleanStrict() } + } + + else -> null + } + } + } + + override fun decodeByte(): Byte { + return decodeInt().toByteExact() + } + + override fun decodeShort(): Short { + return decodeInt().toShortExact() + } + + override fun decodeInt(): Int { + return decodeResolvingInt({ + UnexpectedDecodeSchemaError( + "int", + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + IntValueDecoder { binaryDecoder.readInt() } + } + + Schema.Type.LONG -> { + IntValueDecoder { binaryDecoder.readLong().toIntExact() } + } + + Schema.Type.FLOAT -> { + IntValueDecoder { binaryDecoder.readDouble().toInt() } + } + + Schema.Type.DOUBLE -> { + IntValueDecoder { binaryDecoder.readDouble().toInt() } + } + + Schema.Type.STRING -> { + IntValueDecoder { binaryDecoder.readString().toInt() } + } + + else -> null + } + } + } + + override fun decodeLong(): Long { + return decodeResolvingLong({ + UnexpectedDecodeSchemaError( + "long", + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + LongValueDecoder { binaryDecoder.readInt().toLong() } + } + + Schema.Type.LONG -> { + LongValueDecoder { binaryDecoder.readLong() } + } + + Schema.Type.FLOAT -> { + LongValueDecoder { binaryDecoder.readFloat().toLong() } + } + + Schema.Type.DOUBLE -> { + LongValueDecoder { binaryDecoder.readDouble().toLong() } + } + + Schema.Type.STRING -> { + LongValueDecoder { binaryDecoder.readString().toLong() } + } + + else -> null + } + } + } + + override fun decodeFloat(): Float { + return decodeResolvingFloat({ + UnexpectedDecodeSchemaError( + "float", + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + FloatValueDecoder { binaryDecoder.readInt().toFloat() } + } + + Schema.Type.LONG -> { + FloatValueDecoder { binaryDecoder.readLong().toFloat() } + } + + Schema.Type.FLOAT -> { + FloatValueDecoder { binaryDecoder.readFloat() } + } + + Schema.Type.DOUBLE -> { + FloatValueDecoder { binaryDecoder.readDouble().toFloatExact() } + } + + Schema.Type.STRING -> { + FloatValueDecoder { binaryDecoder.readString().toFloat() } + } + + else -> null + } + } + } + + override fun decodeDouble(): Double { + return decodeResolvingDouble({ + UnexpectedDecodeSchemaError( + "double", + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + DoubleValueDecoder { binaryDecoder.readInt().toDouble() } + } + + Schema.Type.LONG -> { + DoubleValueDecoder { binaryDecoder.readLong().toDouble() } + } + + Schema.Type.FLOAT -> { + DoubleValueDecoder { binaryDecoder.readFloat().toDouble() } + } + + Schema.Type.DOUBLE -> { + DoubleValueDecoder { binaryDecoder.readDouble() } + } + + Schema.Type.STRING -> { + DoubleValueDecoder { binaryDecoder.readString().toDouble() } + } + + else -> null + } + } + } + + override fun decodeChar(): Char { + return decodeResolvingChar({ + UnexpectedDecodeSchemaError( + "char", + Schema.Type.INT, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + CharValueDecoder { binaryDecoder.readInt().toChar() } + } + + Schema.Type.STRING -> { + CharValueDecoder { binaryDecoder.readString(null).single() } + } + + else -> null + } + } + } + + override fun decodeString(): String { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "string", + Schema.Type.STRING, + Schema.Type.BYTES, + Schema.Type.FIXED + ) + }) { + when (it.type) { + Schema.Type.STRING, + Schema.Type.BYTES, + -> { + AnyValueDecoder { binaryDecoder.readString() } + } + + Schema.Type.FIXED -> { + AnyValueDecoder { ByteArray(it.fixedSize).also { buf -> binaryDecoder.readFixed(buf) }.decodeToString() } + } + + else -> null + } + } + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + return decodeResolvingInt({ + UnexpectedDecodeSchemaError( + enumDescriptor.nonNullSerialName, + Schema.Type.ENUM, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.ENUM -> + if (it.isFullNameOrAliasMatch(enumDescriptor)) { + IntValueDecoder { + val enumName = it.enumSymbols[binaryDecoder.readEnum()] + enumDescriptor.getElementIndexNullable(enumName) + ?: avro.enumResolver.getDefaultValueIndex(enumDescriptor) + ?: throw SerializationException( + "Unknown enum symbol name '$enumName' for Enum '${enumDescriptor.serialName}' for writer schema $currentWriterSchema" + ) + } + } else { + null + } + + Schema.Type.STRING -> { + IntValueDecoder { + val enumSymbol = binaryDecoder.readString() + enumDescriptor.getElementIndex(enumSymbol) + .takeIf { index -> index >= 0 } + ?: avro.enumResolver.getDefaultValueIndex(enumDescriptor) + ?: throw SerializationException("Unknown enum symbol '$enumSymbol' for Enum '${enumDescriptor.serialName}'") + } + } + + else -> null + } + } + } + + override fun decodeBytes(): ByteArray { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "ByteArray", + Schema.Type.BYTES, + Schema.Type.FIXED, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.BYTES -> { + AnyValueDecoder { binaryDecoder.readBytes(null).array() } + } + + Schema.Type.FIXED -> { + AnyValueDecoder { ByteArray(it.fixedSize).also { buf -> binaryDecoder.readFixed(buf) } } + } + + Schema.Type.STRING -> { + AnyValueDecoder { binaryDecoder.readString(null).bytes } + } + + else -> null + } + } + } + + override fun decodeFixed(): GenericFixed { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "GenericFixed", + Schema.Type.BYTES, + Schema.Type.FIXED + ) + }) { + when (it.type) { + Schema.Type.BYTES -> { + AnyValueDecoder { GenericData.Fixed(it, binaryDecoder.readBytes(null).array()) } + } + + Schema.Type.FIXED -> { + AnyValueDecoder { GenericData.Fixed(it, ByteArray(it.fixedSize).also { buf -> binaryDecoder.readFixed(buf) }) } + } + + else -> null + } + } + } +} + +private class PolymorphicDecoder( + avro: Avro, + descriptor: SerialDescriptor, + schema: Schema, + private val binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractPolymorphicDecoder(avro, descriptor, schema) { + override fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + return tryFindSerialName(namesAndAliasesToSerialName, schema.types[binaryDecoder.readIndex()]) + } + + override fun newDecoder(chosenSchema: Schema): Decoder { + return AvroValueDirectDecoder(chosenSchema, avro, binaryDecoder) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractInterceptingDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractInterceptingDecoder.kt new file mode 100644 index 00000000..510c0adf --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractInterceptingDecoder.kt @@ -0,0 +1,131 @@ +package com.github.avrokotlin.avro4k.internal.decoder.direct + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder + +internal abstract class AbstractInterceptingDecoder : Decoder, CompositeDecoder { + protected open fun beginElement( + descriptor: SerialDescriptor, + index: Int, + ) { + } + + override fun decodeInline(descriptor: SerialDescriptor): Decoder = this + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = this + + override fun endStructure(descriptor: SerialDescriptor) { + } + + override fun decodeBooleanElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + beginElement(descriptor, index) + return decodeBoolean() + } + + override fun decodeByteElement( + descriptor: SerialDescriptor, + index: Int, + ): Byte { + beginElement(descriptor, index) + return decodeByte() + } + + override fun decodeShortElement( + descriptor: SerialDescriptor, + index: Int, + ): Short { + beginElement(descriptor, index) + return decodeShort() + } + + override fun decodeIntElement( + descriptor: SerialDescriptor, + index: Int, + ): Int { + beginElement(descriptor, index) + return decodeInt() + } + + override fun decodeLongElement( + descriptor: SerialDescriptor, + index: Int, + ): Long { + beginElement(descriptor, index) + return decodeLong() + } + + override fun decodeFloatElement( + descriptor: SerialDescriptor, + index: Int, + ): Float { + beginElement(descriptor, index) + return decodeFloat() + } + + override fun decodeDoubleElement( + descriptor: SerialDescriptor, + index: Int, + ): Double { + beginElement(descriptor, index) + return decodeDouble() + } + + override fun decodeCharElement( + descriptor: SerialDescriptor, + index: Int, + ): Char { + beginElement(descriptor, index) + return decodeChar() + } + + override fun decodeStringElement( + descriptor: SerialDescriptor, + index: Int, + ): String { + beginElement(descriptor, index) + return decodeString() + } + + override fun decodeInlineElement( + descriptor: SerialDescriptor, + index: Int, + ): Decoder { + beginElement(descriptor, index) + return decodeInline(descriptor.getElementDescriptor(index)) + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T?, + ): T { + beginElement(descriptor, index) + return decodeSerializableValue(deserializer) + } + + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T?, + ): T? { + beginElement(descriptor, index) + return decodeIfNullable(deserializer) { + decodeSerializableValue(deserializer) + } + } +} + +private inline fun Decoder.decodeIfNullable( + deserializer: DeserializationStrategy, + block: () -> T?, +): T? { + val isNullabilitySupported = deserializer.descriptor.isNullable + return if (isNullabilitySupported || decodeNotNullMark()) block() else decodeNull() +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AvroValueDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AvroValueDirectDecoder.kt new file mode 100644 index 00000000..9ec930e5 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AvroValueDirectDecoder.kt @@ -0,0 +1,20 @@ +package com.github.avrokotlin.avro4k.internal.decoder.direct + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class AvroValueDirectDecoder( + writerSchema: Schema, + avro: Avro, + binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractAvroDirectDecoder(avro, binaryDecoder) { + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } + + @ExperimentalSerializationApi + override var currentWriterSchema: Schema = writerSchema +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt new file mode 100644 index 00000000..6d75bbd7 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt @@ -0,0 +1,74 @@ +package com.github.avrokotlin.avro4k.internal.decoder.direct + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class ArrayBlockDirectDecoder( + private val arraySchema: Schema, + private val decodeFirstBlock: Boolean, + private val onCollectionSizeDecoded: (Int) -> Unit, + avro: Avro, + binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractAvroDirectDecoder(avro, binaryDecoder) { + override lateinit var currentWriterSchema: Schema + + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { + return if (decodeFirstBlock) { + binaryDecoder.readArrayStart().toInt() + } else { + binaryDecoder.arrayNext().toInt() + }.also { onCollectionSizeDecoded(it) } + } + + override fun beginElement( + descriptor: SerialDescriptor, + index: Int, + ) { + // reset the current writer schema in of the element is a union (as a union is resolved) + currentWriterSchema = arraySchema.elementType + } + + override fun decodeSequentially() = true + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } +} + +internal class MapBlockDirectDecoder( + private val mapSchema: Schema, + private val decodeFirstBlock: Boolean, + private val onCollectionSizeDecoded: (Int) -> Unit, + avro: Avro, + binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractAvroDirectDecoder(avro, binaryDecoder) { + override lateinit var currentWriterSchema: Schema + + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { + return if (decodeFirstBlock) { + binaryDecoder.readMapStart().toInt() + } else { + binaryDecoder.mapNext().toInt() + }.also { onCollectionSizeDecoded(it) } + } + + override fun beginElement( + descriptor: SerialDescriptor, + index: Int, + ) { + // reset the current writer schema in of the element is a union (as a union is resolved) + currentWriterSchema = if (index % 2 == 0) KEY_SCHEMA else mapSchema.valueType + } + + override fun decodeSequentially() = true + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } + + companion object { + private val KEY_SCHEMA = Schema.create(Schema.Type.STRING) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt new file mode 100644 index 00000000..b5bd2cfe --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt @@ -0,0 +1,219 @@ +package com.github.avrokotlin.avro4k.internal.decoder.direct + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.DecodingStep +import com.github.avrokotlin.avro4k.internal.decoder.generic.AvroValueGenericDecoder +import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import org.apache.avro.Schema +import org.apache.avro.generic.GenericFixed +import org.apache.avro.io.Decoder + +internal class RecordDirectDecoder( + private val writerRecordSchema: Schema, + descriptor: SerialDescriptor, + avro: Avro, + binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractAvroDirectDecoder(avro, binaryDecoder) { + // from descriptor element index to schema field. The missing fields are at the end to decode the default values + private val classDescriptor = avro.recordResolver.resolveFields(writerRecordSchema, descriptor) + private lateinit var currentDecodingStep: DecodingStep.ValidatedDecodingStep + private var nextDecodingStepIndex = 0 + + override lateinit var currentWriterSchema: Schema + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + var field: DecodingStep + while (true) { + if (nextDecodingStepIndex == classDescriptor.decodingSteps.size) { + return CompositeDecoder.DECODE_DONE + } + field = classDescriptor.decodingSteps[nextDecodingStepIndex++] + when (field) { + is DecodingStep.IgnoreOptionalElement -> { + // loop again to ignore the optional element + } + + is DecodingStep.SkipWriterField -> binaryDecoder.skip(field.schema) + is DecodingStep.MissingElementValueFailure -> { + throw SerializationException( + "Reader field '${descriptor.nonNullSerialName}.${descriptor.getElementName( + field.elementIndex + )}' has no corresponding field in writer schema $writerRecordSchema" + ) + } + + is DecodingStep.DeserializeWriterField -> { + currentDecodingStep = field + currentWriterSchema = field.schema + return field.elementIndex + } + + is DecodingStep.GetDefaultValue -> { + currentDecodingStep = field + currentWriterSchema = field.schema + return field.elementIndex + } + } + } + } + + private fun decodeDefault( + element: DecodingStep.GetDefaultValue, + deserializer: DeserializationStrategy, + ): T { + return AvroValueGenericDecoder(avro, element.defaultValue, currentWriterSchema) + .decodeSerializableValue(deserializer) + } + + override fun decodeNotNullMark(): Boolean { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeNotNullMark() + is DecodingStep.GetDefaultValue -> element.defaultValue != null + } + } + + override fun decodeNull(): Nothing? { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeNull() + is DecodingStep.GetDefaultValue -> { + if (element.defaultValue != null) { + // Should not occur as decodeNotNullMark() should be called first + throw SerializationException("Trying to decode a null value for a missing field while the default value is not null") + } + null + } + } + } + + override fun decodeFixed(): GenericFixed { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeFixed() + is DecodingStep.GetDefaultValue -> element.defaultValue as GenericFixed + } + } + + override fun decodeInt(): Int { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeInt() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Int.serializer()) + } + } + + override fun decodeLong(): Long { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeLong() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Long.serializer()) + } + } + + override fun decodeBoolean(): Boolean { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeBoolean() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Boolean.serializer()) + } + } + + override fun decodeChar(): Char { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeChar() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Char.serializer()) + } + } + + override fun decodeString(): String { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeString() + is DecodingStep.GetDefaultValue -> decodeDefault(element, String.serializer()) + } + } + + override fun decodeDouble(): Double { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeDouble() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Double.serializer()) + } + } + + override fun decodeFloat(): Float { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeFloat() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Float.serializer()) + } + } + + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeSerializableValue(deserializer) + is DecodingStep.GetDefaultValue -> decodeDefault(element, deserializer) + } + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeEnum(enumDescriptor) + is DecodingStep.GetDefaultValue -> decodeDefault(element, Int.serializer()) + } + } + + override fun decodeBytes(): ByteArray { + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeBytes() + is DecodingStep.GetDefaultValue -> decodeDefault(element, ByteArraySerializer()) + } + } +} + +private fun Decoder.skip(s: Schema) { + val schema = + if (s.isUnion) { + s.types[readIndex()] + } else { + s + } + when (schema.type) { + Schema.Type.BOOLEAN -> readBoolean() + Schema.Type.INT -> readInt() + Schema.Type.LONG -> readLong() + Schema.Type.FLOAT -> readFloat() + Schema.Type.DOUBLE -> readDouble() + Schema.Type.STRING -> skipString() + Schema.Type.BYTES -> skipBytes() + Schema.Type.FIXED -> skipFixed(schema.fixedSize) + Schema.Type.ENUM -> readEnum() + Schema.Type.ARRAY -> { + var arrayBlockItems: Long = skipArray() + while (arrayBlockItems > 0L) { + for (i in 0L until arrayBlockItems) { + skip(schema.elementType) + } + arrayBlockItems = skipArray() + } + } + + Schema.Type.MAP -> { + var mapBlockItems: Long = skipMap() + while (mapBlockItems > 0L) { + for (i in 0L until mapBlockItems) { + skipString() + skip(schema.elementType) + } + mapBlockItems = skipMap() + } + } + + Schema.Type.NULL -> readNull() + Schema.Type.RECORD -> { + schema.fields.forEach { + skip(it.schema()) + } + } + + else -> throw SerializationException("Unsupported schema type for $schema") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt new file mode 100644 index 00000000..c9f91a41 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt @@ -0,0 +1,212 @@ +package com.github.avrokotlin.avro4k.internal.decoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.internal.BadDecodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware +import com.github.avrokotlin.avro4k.internal.toByteExact +import com.github.avrokotlin.avro4k.internal.toDoubleExact +import com.github.avrokotlin.avro4k.internal.toFloatExact +import com.github.avrokotlin.avro4k.internal.toIntExact +import com.github.avrokotlin.avro4k.internal.toLongExact +import com.github.avrokotlin.avro4k.internal.toShortExact +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.generic.GenericArray +import org.apache.avro.generic.GenericContainer +import org.apache.avro.generic.GenericEnumSymbol +import org.apache.avro.generic.GenericFixed +import org.apache.avro.generic.IndexedRecord +import java.math.BigDecimal +import java.nio.ByteBuffer + +internal abstract class AbstractAvroGenericDecoder : AbstractDecoder(), AvroDecoder { + internal abstract val avro: Avro + + abstract override fun decodeNotNullMark(): Boolean + + abstract override fun decodeValue(): Any + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return SerializerLocatorMiddleware.apply(deserializer) + .deserialize(this) + } + + @Suppress("UNCHECKED_CAST") + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + return when (descriptor.kind) { + StructureKind.LIST -> + when (val value = decodeValue()) { + is GenericArray<*> -> + ArrayGenericDecoder( + collection = value, + writerSchema = value.schema, + avro = avro + ) + + is Collection<*> -> + ArrayGenericDecoder( + collection = value, + writerSchema = currentWriterSchema, + avro = avro + ) + + else -> throw BadDecodedValueError(value, StructureKind.LIST, GenericArray::class, Collection::class, ByteBuffer::class) + } + + StructureKind.MAP -> + when (val value = decodeValue()) { + is Map<*, *> -> + MapGenericDecoder( + value as Map, + currentWriterSchema, + avro + ) + + else -> throw BadDecodedValueError(value, StructureKind.MAP, Map::class) + } + + StructureKind.CLASS, StructureKind.OBJECT -> + when (val value = decodeValue()) { + is IndexedRecord -> RecordGenericDecoder(value, descriptor, avro) + else -> throw BadDecodedValueError(value, descriptor.kind, IndexedRecord::class) + } + + is PolymorphicKind -> + when (val value = decodeValue()) { + is GenericContainer -> PolymorphicGenericDecoder(avro, descriptor, value.schema, value) + else -> PolymorphicGenericDecoder(avro, descriptor, currentWriterSchema, value) + } + + else -> throw SerializationException("Unsupported descriptor for structure decoding: $descriptor") + } + } + + override fun decodeBoolean(): Boolean { + return when (val value = decodeValue()) { + is Boolean -> value + 1 -> true + 0 -> false + is CharSequence -> value.toString().toBoolean() + else -> throw BadDecodedValueError(value, PrimitiveKind.BOOLEAN, Boolean::class, Int::class, CharSequence::class) + } + } + + override fun decodeByte(): Byte { + return when (val value = decodeValue()) { + is Int -> value.toByteExact() + is Long -> value.toByteExact() + is BigDecimal -> value.toByteExact() + is CharSequence -> value.toString().toByte() + else -> throw BadDecodedValueError(value, PrimitiveKind.BYTE, Int::class, Long::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeShort(): Short { + return when (val value = decodeValue()) { + is Int -> value.toShortExact() + is Long -> value.toShortExact() + is BigDecimal -> value.toShortExact() + is CharSequence -> value.toString().toShort() + else -> throw BadDecodedValueError(value, PrimitiveKind.SHORT, Int::class, Long::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeInt(): Int { + return when (val value = decodeValue()) { + is Int -> value + is Long -> value.toIntExact() + is BigDecimal -> value.toIntExact() + is CharSequence -> value.toString().toInt() + else -> throw BadDecodedValueError(value, PrimitiveKind.INT, Int::class, Long::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeLong(): Long { + return when (val value = decodeValue()) { + is Long -> value + is Int -> value.toLong() + is BigDecimal -> value.toLongExact() + is CharSequence -> value.toString().toLong() + else -> throw BadDecodedValueError(value, PrimitiveKind.LONG, Int::class, Long::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeFloat(): Float { + return when (val value = decodeValue()) { + is Float -> value + is Double -> value.toFloatExact() + is BigDecimal -> value.toFloatExact() + is CharSequence -> value.toString().toFloat() + else -> throw BadDecodedValueError(value, PrimitiveKind.FLOAT, Float::class, Double::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeDouble(): Double { + return when (val value = decodeValue()) { + is Double -> value + is Float -> value.toDouble() + is BigDecimal -> value.toDoubleExact() + is CharSequence -> value.toString().toDouble() + else -> throw BadDecodedValueError(value, PrimitiveKind.DOUBLE, Float::class, Double::class, BigDecimal::class, CharSequence::class) + } + } + + override fun decodeChar(): Char { + val value = decodeValue() + return when { + value is Int -> value.toChar() + value is CharSequence && value.length == 1 -> value[0] + else -> throw BadDecodedValueError(value, PrimitiveKind.CHAR, Int::class, CharSequence::class) + } + } + + override fun decodeString(): String { + return when (val value = decodeValue()) { + is CharSequence -> value.toString() + is ByteArray -> value.decodeToString() + is GenericFixed -> value.bytes().decodeToString() + else -> throw BadDecodedValueError(value, PrimitiveKind.STRING, CharSequence::class, ByteArray::class, GenericFixed::class) + } + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + return when (val value = decodeValue()) { + is GenericEnumSymbol<*>, is CharSequence -> { + enumDescriptor.getElementIndex(value.toString()).takeIf { it >= 0 } + ?: avro.enumResolver.getDefaultValueIndex(enumDescriptor) + ?: throw SerializationException("Unknown enum symbol '$value' for Enum '${enumDescriptor.serialName}'") + } + + else -> throw BadDecodedValueError(value, SerialKind.ENUM, GenericEnumSymbol::class, CharSequence::class) + } + } + + override fun decodeBytes(): ByteArray { + return when (val value = decodeValue()) { + is ByteArray -> value + is ByteBuffer -> value.array() + is GenericFixed -> value.bytes() + is CharSequence -> value.toString().toByteArray() + else -> throw BadDecodedValueError(value, ByteArray::class, GenericFixed::class, CharSequence::class) + } + } + + override fun decodeFixed(): GenericFixed { + return when (val value = decodeValue()) { + is GenericFixed -> value + else -> throw BadDecodedValueError(value, GenericFixed::class) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AvroValueGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AvroValueGenericDecoder.kt new file mode 100644 index 00000000..432ce8b8 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AvroValueGenericDecoder.kt @@ -0,0 +1,21 @@ +package com.github.avrokotlin.avro4k.internal.decoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.DecodedNullError +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class AvroValueGenericDecoder( + override val avro: Avro, + private val value: Any?, + override val currentWriterSchema: Schema, +) : AbstractAvroGenericDecoder() { + override fun decodeNotNullMark() = value != null + + override fun decodeValue() = value ?: throw DecodedNullError() + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/CollectionGenericDecoders.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/CollectionGenericDecoders.kt new file mode 100644 index 00000000..4a485c8e --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/CollectionGenericDecoders.kt @@ -0,0 +1,91 @@ +package com.github.avrokotlin.avro4k.internal.decoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.DecodedNullError +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class MapGenericDecoder( + private val map: Map, + private val writerSchema: Schema, + override val avro: Avro, +) : AbstractAvroGenericDecoder() { + private val iterator = map.asSequence().flatMap { sequenceOf(true to it.key, true to it.value) }.iterator() + private lateinit var currentData: Pair + private var decodedNotNullMark = false + + override val currentWriterSchema: Schema + get() = + if (currentData.first) { + STRING_SCHEMA + } else { + writerSchema.valueType + } + + override fun decodeNotNullMark(): Boolean { + decodedNotNullMark = true + currentData = iterator.next() + return currentData.second != null + } + + override fun decodeValue(): Any { + if (!decodedNotNullMark) { + currentData = iterator.next() + } else { + decodedNotNullMark = false + } + return currentData.second ?: throw DecodedNullError() + } + + override fun decodeNull(): Nothing? { + decodedNotNullMark = false + return null + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } + + override fun decodeCollectionSize(descriptor: SerialDescriptor) = map.size + + override fun decodeSequentially() = true + + companion object { + private val STRING_SCHEMA = Schema.create(Schema.Type.STRING) + } +} + +internal class ArrayGenericDecoder( + private val collection: Collection, + private val writerSchema: Schema, + override val avro: Avro, +) : AbstractAvroGenericDecoder() { + private val iterator = collection.iterator() + + private var currentItem: Any? = null + private var decodedNullMark = false + + override val currentWriterSchema: Schema + get() = writerSchema.elementType + + override fun decodeNotNullMark(): Boolean { + decodedNullMark = true + currentItem = iterator.next() + return currentItem != null + } + + override fun decodeValue(): Any { + val value = if (decodedNullMark) currentItem else iterator.next() + decodedNullMark = false + return value ?: throw DecodedNullError() + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } + + override fun decodeCollectionSize(descriptor: SerialDescriptor) = collection.size + + override fun decodeSequentially() = true +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt new file mode 100644 index 00000000..213854ff --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt @@ -0,0 +1,25 @@ +package com.github.avrokotlin.avro4k.internal.decoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.decoder.AbstractPolymorphicDecoder +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import org.apache.avro.Schema + +internal class PolymorphicGenericDecoder( + avro: Avro, + descriptor: SerialDescriptor, + schema: Schema, + private val value: Any?, +) : AbstractPolymorphicDecoder(avro, descriptor, schema) { + override fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + return schema.types.firstNotNullOfOrNull { tryFindSerialName(namesAndAliasesToSerialName, it) } + } + + override fun newDecoder(chosenSchema: Schema): Decoder { + return AvroValueGenericDecoder(avro, value, chosenSchema) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/RecordGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/RecordGenericDecoder.kt new file mode 100644 index 00000000..11d77406 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/RecordGenericDecoder.kt @@ -0,0 +1,48 @@ +package com.github.avrokotlin.avro4k.internal.decoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.DecodedNullError +import com.github.avrokotlin.avro4k.internal.DecodingStep +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import org.apache.avro.Schema +import org.apache.avro.generic.IndexedRecord + +internal class RecordGenericDecoder( + private val record: IndexedRecord, + private val descriptor: SerialDescriptor, + override val avro: Avro, +) : AbstractAvroGenericDecoder() { + // from descriptor element index to schema field + private val classDescriptor = avro.recordResolver.resolveFields(record.schema, descriptor) + private lateinit var currentElement: DecodingStep.ValidatedDecodingStep + private var nextDecodingStep = 0 + + override val currentWriterSchema: Schema + get() = currentElement.schema + + override fun decodeNotNullMark() = decodeNullableValue() != null + + override fun decodeValue(): Any { + return decodeNullableValue() ?: throw DecodedNullError(descriptor, currentElement.elementIndex) + } + + private fun decodeNullableValue(): Any? { + return when (val element = currentElement) { + is DecodingStep.DeserializeWriterField -> record.get(element.writerFieldIndex) + is DecodingStep.GetDefaultValue -> element.defaultValue + } + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + var field: DecodingStep + do { + if (nextDecodingStep == classDescriptor.decodingSteps.size) { + return CompositeDecoder.DECODE_DONE + } + field = classDescriptor.decodingSteps[nextDecodingStep++] + } while (field !is DecodingStep.ValidatedDecodingStep) + currentElement = field + return field.elementIndex + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt new file mode 100644 index 00000000..87d36beb --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt @@ -0,0 +1,460 @@ +package com.github.avrokotlin.avro4k.internal.encoder.direct + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.UnionEncoder +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware +import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema +import org.apache.avro.generic.GenericFixed +import org.apache.avro.util.Utf8 +import java.nio.ByteBuffer + +internal class AvroValueDirectEncoder( + override var currentWriterSchema: Schema, + avro: Avro, + binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractAvroDirectEncoder(avro, binaryEncoder) + +internal sealed class AbstractAvroDirectEncoder( + protected val avro: Avro, + protected val binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractEncoder(), AvroEncoder, UnionEncoder { + private var selectedUnionIndex: Int = -1 + + abstract override var currentWriterSchema: Schema + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun encodeSerializableValue( + serializer: SerializationStrategy, + value: T, + ) { + SerializerLocatorMiddleware.apply(serializer) + .serialize(this, value) + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return when (descriptor.kind) { + StructureKind.CLASS, + StructureKind.OBJECT, + -> + encodeResolving( + { BadEncodedValueError(null, currentWriterSchema, Schema.Type.RECORD) } + ) { schema -> + if (schema.type == Schema.Type.RECORD && schema.isFullNameOrAliasMatch(descriptor)) { + { + val elementDescriptors = avro.recordResolver.resolveFields(schema, descriptor) + RecordDirectEncoder(elementDescriptors, schema, avro, binaryEncoder) + } + } else { + null + } + } + + is PolymorphicKind -> PolymorphicDirectEncoder(avro, currentWriterSchema, binaryEncoder) + else -> throw SerializationException("Unsupported structure kind: $descriptor") + } + } + + override fun beginCollection( + descriptor: SerialDescriptor, + collectionSize: Int, + ): CompositeEncoder { + return when (descriptor.kind) { + StructureKind.LIST -> + encodeResolving({ BadEncodedValueError(emptyList(), currentWriterSchema, Schema.Type.ARRAY) }) { schema -> + when (schema.type) { + Schema.Type.ARRAY -> { + { ArrayDirectEncoder(schema, collectionSize, avro, binaryEncoder) } + } + + else -> null + } + } + + StructureKind.MAP -> + encodeResolving({ BadEncodedValueError(emptyMap(), currentWriterSchema, Schema.Type.MAP) }) { schema -> + when (schema.type) { + Schema.Type.MAP -> { + { MapDirectEncoder(schema, collectionSize, avro, binaryEncoder) } + } + + else -> null + } + } + + else -> throw SerializationException("Unsupported collection kind: $descriptor") + } + } + + override fun encodeUnionIndex(index: Int) { + if (selectedUnionIndex > -1) { + throw SerializationException("Already selected union index: $selectedUnionIndex, got $index, for selected schema $currentWriterSchema") + } + if (currentWriterSchema.isUnion) { + binaryEncoder.writeIndex(index) + selectedUnionIndex = index + currentWriterSchema = currentWriterSchema.types[index] + } else { + throw SerializationException("Cannot select union index for non-union schema: $currentWriterSchema") + } + } + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + selectedUnionIndex = -1 + return true + } + + override fun encodeNull() { + encodeResolving( + { BadEncodedValueError(null, currentWriterSchema, Schema.Type.NULL) } + ) { + when (it.type) { + Schema.Type.NULL -> { + { binaryEncoder.writeNull() } + } + + else -> null + } + } + } + + override fun encodeBytes(value: ByteBuffer) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.BYTES, Schema.Type.STRING, Schema.Type.FIXED) } + ) { + when (it.type) { + Schema.Type.BYTES -> { + { binaryEncoder.writeBytes(value) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(Utf8(value.array())) } + } + + Schema.Type.FIXED -> { + if (value.remaining() == it.fixedSize) { + { binaryEncoder.writeFixed(value.array()) } + } else { + null + } + } + + else -> null + } + } + } + + override fun encodeBytes(value: ByteArray) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.BYTES, Schema.Type.STRING, Schema.Type.FIXED) } + ) { + when (it.type) { + Schema.Type.BYTES -> { + { binaryEncoder.writeBytes(value) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(Utf8(value)) } + } + + Schema.Type.FIXED -> { + if (value.size == it.fixedSize) { + { binaryEncoder.writeFixed(value) } + } else { + null + } + } + + else -> null + } + } + } + + override fun encodeFixed(value: GenericFixed) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.FIXED, Schema.Type.STRING, Schema.Type.BYTES) } + ) { + when (it.type) { + Schema.Type.FIXED -> { + if (it.fullName == value.schema.fullName && it.fixedSize == value.bytes().size) { + { binaryEncoder.writeFixed(value.bytes()) } + } else { + null + } + } + + Schema.Type.BYTES -> { + { binaryEncoder.writeBytes(value.bytes()) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(Utf8(value.bytes())) } + } + + else -> null + } + } + } + + override fun encodeFixed(value: ByteArray) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.FIXED, Schema.Type.STRING, Schema.Type.BYTES) } + ) { + when (it.type) { + Schema.Type.FIXED -> + if (it.fixedSize == value.size) { + { binaryEncoder.writeFixed(value) } + } else { + null + } + + Schema.Type.BYTES -> { + { binaryEncoder.writeBytes(value) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(Utf8(value)) } + } + + else -> null + } + } + } + + override fun encodeEnum( + enumDescriptor: SerialDescriptor, + index: Int, + ) { + val enumName = enumDescriptor.getElementName(index) + encodeResolving( + { BadEncodedValueError(index, currentWriterSchema, Schema.Type.ENUM, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.ENUM -> + if (it.isFullNameOrAliasMatch(enumDescriptor)) { + { binaryEncoder.writeEnum(it.getEnumOrdinal(enumName)) } + } else { + null + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(enumName) } + } + + else -> null + } + } + } + + override fun encodeBoolean(value: Boolean) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.BOOLEAN, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.BOOLEAN -> { + { binaryEncoder.writeBoolean(value) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeByte(value: Byte) { + encodeInt(value.toInt()) + } + + override fun encodeShort(value: Short) { + encodeInt(value.toInt()) + } + + override fun encodeInt(value: Int) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.INT, Schema.Type.LONG, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.INT -> { + { binaryEncoder.writeInt(value) } + } + + Schema.Type.LONG -> { + { binaryEncoder.writeLong(value.toLong()) } + } + + Schema.Type.FLOAT -> { + { binaryEncoder.writeFloat(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { binaryEncoder.writeDouble(value.toDouble()) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeLong(value: Long) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.LONG, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.LONG -> { + { binaryEncoder.writeLong(value) } + } + + Schema.Type.FLOAT -> { + { binaryEncoder.writeFloat(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { binaryEncoder.writeDouble(value.toDouble()) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeFloat(value: Float) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.FLOAT -> { + { binaryEncoder.writeFloat(value) } + } + + Schema.Type.DOUBLE -> { + { binaryEncoder.writeDouble(value.toDouble()) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeDouble(value: Double) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.DOUBLE -> { + { binaryEncoder.writeDouble(value) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeChar(value: Char) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.INT, Schema.Type.STRING) } + ) { + when (it.type) { + Schema.Type.INT -> { + { binaryEncoder.writeInt(value.code) } + } + + Schema.Type.STRING -> { + { binaryEncoder.writeString(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeString(value: String) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED, Schema.Type.ENUM) } + ) { + when (it.type) { + Schema.Type.STRING -> { + { binaryEncoder.writeString(value) } + } + + Schema.Type.BYTES -> { + { binaryEncoder.writeBytes(value.encodeToByteArray()) } + } + + Schema.Type.FIXED -> { + if (value.length == it.fixedSize) { + { binaryEncoder.writeFixed(value.encodeToByteArray()) } + } else { + null + } + } + + Schema.Type.ENUM -> { + { binaryEncoder.writeEnum(it.getEnumOrdinal(value)) } + } + + else -> null + } + } + } +} + +internal class PolymorphicDirectEncoder( + private val avro: Avro, + private val schema: Schema, + private val binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractEncoder() { + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + // index 0 is the type discriminator, index 1 is the value itself + // we don't need the type discriminator here + return index == 1 + } + + override fun encodeSerializableValue( + serializer: SerializationStrategy, + value: T, + ) { + AvroValueDirectEncoder(schema, avro, binaryEncoder) + .encodeSerializableValue(serializer, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/CollectionsEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/CollectionsEncoder.kt new file mode 100644 index 00000000..bf3addf7 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/CollectionsEncoder.kt @@ -0,0 +1,69 @@ +package com.github.avrokotlin.avro4k.internal.encoder.direct + +import com.github.avrokotlin.avro4k.Avro +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class MapDirectEncoder(private val schema: Schema, mapSize: Int, avro: Avro, binaryEncoder: org.apache.avro.io.Encoder) : + AbstractAvroDirectEncoder(avro, binaryEncoder) { + private var isKey: Boolean = true + + init { + binaryEncoder.writeMapStart() + binaryEncoder.setItemCount(mapSize.toLong()) + } + + override fun endStructure(descriptor: SerialDescriptor) { + binaryEncoder.writeMapEnd() + } + + override lateinit var currentWriterSchema: Schema + + companion object { + private val STRING_SCHEMA = Schema.create(Schema.Type.STRING) + } + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + isKey = index % 2 == 0 + currentWriterSchema = + if (isKey) { + binaryEncoder.startItem() + STRING_SCHEMA + } else { + schema.valueType + } + return true + } +} + +internal class ArrayDirectEncoder( + private val arraySchema: Schema, + arraySize: Int, + avro: Avro, + binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractAvroDirectEncoder(avro, binaryEncoder) { + init { + binaryEncoder.writeArrayStart() + binaryEncoder.setItemCount(arraySize.toLong()) + } + + override lateinit var currentWriterSchema: Schema + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + binaryEncoder.startItem() + currentWriterSchema = arraySchema.elementType + return true + } + + override fun endStructure(descriptor: SerialDescriptor) { + binaryEncoder.writeArrayEnd() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt new file mode 100644 index 00000000..5c0e1fc8 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt @@ -0,0 +1,210 @@ +package com.github.avrokotlin.avro4k.internal.encoder.direct + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.UnionEncoder +import com.github.avrokotlin.avro4k.internal.ClassDescriptorForWriterSchema +import com.github.avrokotlin.avro4k.internal.EncodingStep +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema +import org.apache.avro.generic.GenericFixed +import java.nio.ByteBuffer + +@Suppress("FunctionName") +internal fun RecordDirectEncoder( + classDescriptor: ClassDescriptorForWriterSchema, + schema: Schema, + avro: Avro, + binaryEncoder: org.apache.avro.io.Encoder, +): CompositeEncoder { + return if (classDescriptor.sequentialEncoding) { + RecordSequentialDirectEncoder(classDescriptor, schema, avro, binaryEncoder) + } else { + RecordBadOrderDirectEncoder(classDescriptor, schema, avro, binaryEncoder) + } +} + +/** + * Consider that the descriptor elements are in the same order as the schema fields, and all the fields are represented by an element. + */ +private class RecordSequentialDirectEncoder( + private val classDescriptor: ClassDescriptorForWriterSchema, + private val schema: Schema, + avro: Avro, + binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractAvroDirectEncoder(avro, binaryEncoder) { + override lateinit var currentWriterSchema: Schema + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + // index == elementIndex == writerFieldIndex, so the written field is already in the good order + return when (val step = classDescriptor.encodingSteps[index]) { + is EncodingStep.SerializeWriterField -> { + currentWriterSchema = schema.fields[step.writerFieldIndex].schema() + true + } + + is EncodingStep.IgnoreElement -> { + false + } + + is EncodingStep.MissingWriterFieldFailure -> { + throw SerializationException("No serializable element found for writer field ${step.writerFieldIndex} in schema $schema") + } + } + } + + override fun endStructure(descriptor: SerialDescriptor) { + if (classDescriptor.hasMissingWriterField) { + throw SerializationException("The descriptor is not writing all the expected fields of writer schema. Schema: $schema, descriptor: $descriptor") + } + } +} + +/** + * This handles the case where the descriptor elements are not in the same order as the schema fields. + * + * First we buffer all the element encodings to the corresponding field indexes, then we encode them for real in the correct order using [RecordSequentialDirectEncoder]. + * + * Not implementing [UnionEncoder] as all the encoding is delegated to the [RecordSequentialDirectEncoder] which already handles union encoding. + */ +private class RecordBadOrderDirectEncoder( + private val classDescriptor: ClassDescriptorForWriterSchema, + private val schema: Schema, + private val avro: Avro, + private val binaryEncoder: org.apache.avro.io.Encoder, +) : AbstractEncoder(), AvroEncoder { + // Each time we encode a field, if the next expected schema field index is not the good one, it is buffered until it's the time to encode it + private var bufferedFields = Array(schema.fields.size) { null } + private lateinit var encodingStepToBuffer: EncodingStep.SerializeWriterField + + data class BufferedField( + val step: EncodingStep.SerializeWriterField, + val encoder: AvroEncoder.() -> Unit, + ) + + override val currentWriterSchema: Schema + get() = encodingStepToBuffer.schema + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + return when (val step = classDescriptor.encodingSteps[index]) { + is EncodingStep.SerializeWriterField -> { + encodingStepToBuffer = step + true + } + + is EncodingStep.IgnoreElement -> { + false + } + + is EncodingStep.MissingWriterFieldFailure -> { + throw SerializationException("No serializable element found for writer field ${step.writerFieldIndex} in schema $schema") + } + } + } + + private inline fun bufferEncoding(crossinline encoder: AvroEncoder.() -> Unit) { + bufferedFields[encodingStepToBuffer.writerFieldIndex] = BufferedField(encodingStepToBuffer) { encoder() } + } + + override fun endStructure(descriptor: SerialDescriptor) { + encodeBufferedFields(descriptor) + } + + private fun encodeBufferedFields(descriptor: SerialDescriptor) { + val recordEncoder = RecordSequentialDirectEncoder(classDescriptor, schema, avro, binaryEncoder) + bufferedFields.forEach { fieldToEncode -> + if (fieldToEncode == null) { + throw SerializationException("The writer field is missing in the buffered fields, it hasn't been encoded yet") + } + // To simulate the behavior of regular element encoding + // We don't use the return of encodeElement because we know it's always true + recordEncoder.encodeElement(descriptor, fieldToEncode.step.elementIndex) + fieldToEncode.encoder(recordEncoder) + } + } + + override fun encodeSerializableValue( + serializer: SerializationStrategy, + value: T, + ) { + bufferEncoding { encodeSerializableValue(serializer, value) } + } + + override fun encodeNull() { + bufferEncoding { encodeNull() } + } + + override fun encodeBytes(value: ByteArray) { + bufferEncoding { encodeBytes(value) } + } + + override fun encodeBytes(value: ByteBuffer) { + bufferEncoding { encodeBytes(value) } + } + + override fun encodeFixed(value: GenericFixed) { + bufferEncoding { encodeFixed(value) } + } + + override fun encodeFixed(value: ByteArray) { + bufferEncoding { encodeFixed(value) } + } + + override fun encodeBoolean(value: Boolean) { + bufferEncoding { encodeBoolean(value) } + } + + override fun encodeByte(value: Byte) { + bufferEncoding { encodeByte(value) } + } + + override fun encodeShort(value: Short) { + bufferEncoding { encodeShort(value) } + } + + override fun encodeInt(value: Int) { + bufferEncoding { encodeInt(value) } + } + + override fun encodeLong(value: Long) { + bufferEncoding { encodeLong(value) } + } + + override fun encodeFloat(value: Float) { + bufferEncoding { encodeFloat(value) } + } + + override fun encodeDouble(value: Double) { + bufferEncoding { encodeDouble(value) } + } + + override fun encodeChar(value: Char) { + bufferEncoding { encodeChar(value) } + } + + override fun encodeString(value: String) { + bufferEncoding { encodeString(value) } + } + + override fun encodeEnum( + enumDescriptor: SerialDescriptor, + index: Int, + ) { + bufferEncoding { encodeEnum(enumDescriptor, index) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt new file mode 100644 index 00000000..a064891b --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt @@ -0,0 +1,440 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.UnionEncoder +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware +import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch +import com.github.avrokotlin.avro4k.internal.toIntExact +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericFixed +import java.nio.ByteBuffer + +internal abstract class AbstractAvroGenericEncoder : AbstractEncoder(), AvroEncoder, UnionEncoder { + abstract val avro: Avro + + abstract override var currentWriterSchema: Schema + + abstract override fun encodeValue(value: Any) + + abstract override fun encodeNull() + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + selectedUnionIndex = -1 + return true + } + + private var selectedUnionIndex: Int = -1 + + override fun encodeUnionIndex(index: Int) { + if (selectedUnionIndex > -1) { + throw SerializationException("Already selected union index: $selectedUnionIndex, got $index, for selected schema $currentWriterSchema") + } + if (currentWriterSchema.isUnion) { + selectedUnionIndex = index + currentWriterSchema = currentWriterSchema.types[index] + } else { + throw SerializationException("Cannot select union index for non-union schema: $currentWriterSchema") + } + } + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun encodeSerializableValue( + serializer: SerializationStrategy, + value: T, + ) { + SerializerLocatorMiddleware.apply(serializer) + .serialize(this, value) + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return when (descriptor.kind) { + StructureKind.CLASS, + StructureKind.OBJECT, + -> + encodeResolving( + { BadEncodedValueError(null, currentWriterSchema, Schema.Type.RECORD) } + ) { schema -> + if (schema.type == Schema.Type.RECORD && schema.isFullNameOrAliasMatch(descriptor)) { + { RecordGenericEncoder(avro, descriptor, schema) { encodeValue(it) } } + } else { + null + } + } + + is PolymorphicKind -> + PolymorphicEncoder(avro, currentWriterSchema) { + encodeValue(it) + } + + else -> throw SerializationException("Unsupported structure kind: $descriptor") + } + } + + override fun beginCollection( + descriptor: SerialDescriptor, + collectionSize: Int, + ): CompositeEncoder { + return when (descriptor.kind) { + StructureKind.LIST -> + encodeResolving( + { BadEncodedValueError(emptyList(), currentWriterSchema, Schema.Type.ARRAY, Schema.Type.BYTES, Schema.Type.FIXED) } + ) { schema -> + when (schema.type) { + Schema.Type.ARRAY -> { + { ArrayGenericEncoder(avro, collectionSize, schema) { encodeValue(it) } } + } + + Schema.Type.BYTES -> { + { BytesGenericEncoder(avro, collectionSize) { encodeValue(it) } } + } + + Schema.Type.FIXED -> { + { FixedGenericEncoder(avro, collectionSize, schema) { encodeValue(it) } } + } + + else -> null + } + } + + StructureKind.MAP -> + encodeResolving( + { BadEncodedValueError(emptyMap(), currentWriterSchema, Schema.Type.MAP) } + ) { schema -> + when (schema.type) { + Schema.Type.MAP -> { + { MapGenericEncoder(avro, collectionSize, schema) { encodeValue(it) } } + } + + else -> null + } + } + + else -> throw SerializationException("Unsupported collection kind: $descriptor") + } + } + + override fun encodeBytes(value: ByteBuffer) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED) } + ) { schema -> + when (schema.type) { + Schema.Type.BYTES -> { + { encodeValue(value) } + } + + Schema.Type.FIXED -> { + if (value.remaining() == schema.fixedSize) { + { encodeValue(value.array()) } + } else { + null + } + } + + Schema.Type.STRING -> { + { encodeValue(value.array().decodeToString()) } + } + + else -> null + } + } + } + + override fun encodeBytes(value: ByteArray) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED) } + ) { schema -> + when (schema.type) { + Schema.Type.BYTES -> { + { encodeValue(ByteBuffer.wrap(value)) } + } + + Schema.Type.FIXED -> { + if (value.size == schema.fixedSize) { + { encodeValue(value) } + } else { + null + } + } + + Schema.Type.STRING -> { + { encodeValue(value.decodeToString()) } + } + + else -> null + } + } + } + + override fun encodeFixed(value: GenericFixed) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED) } + ) { schema -> + when (schema.type) { + Schema.Type.FIXED -> + if (schema.fullName == value.schema.fullName && schema.fixedSize == value.bytes().size) { + { encodeValue(value) } + } else { + null + } + + Schema.Type.BYTES -> { + { encodeValue(ByteBuffer.wrap(value.bytes())) } + } + + Schema.Type.STRING -> { + { encodeValue(value.bytes().decodeToString()) } + } + + else -> null + } + } + } + + override fun encodeFixed(value: ByteArray) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED) } + ) { schema -> + when (schema.type) { + Schema.Type.FIXED -> { + if (value.size == schema.fixedSize) { + { encodeValue(value) } + } else { + null + } + } + + Schema.Type.BYTES -> { + { encodeValue(ByteBuffer.wrap(value)) } + } + + Schema.Type.STRING -> { + { encodeValue(value.decodeToString()) } + } + + else -> null + } + } + } + + override fun encodeBoolean(value: Boolean) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.BOOLEAN, Schema.Type.STRING) } + ) { schema -> + when (schema.type) { + Schema.Type.BOOLEAN -> { + { encodeValue(value) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeByte(value: Byte) { + encodeInt(value.toInt()) + } + + override fun encodeShort(value: Short) { + encodeInt(value.toInt()) + } + + override fun encodeInt(value: Int) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.LONG, Schema.Type.INT, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { schema -> + when (schema.type) { + Schema.Type.INT -> { + { encodeValue(value) } + } + + Schema.Type.LONG -> { + { encodeValue(value.toLong()) } + } + + Schema.Type.FLOAT -> { + { encodeValue(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { encodeValue(value.toDouble()) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeLong(value: Long) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.LONG, Schema.Type.INT, Schema.Type.FLOAT, Schema.Type.DOUBLE, Schema.Type.STRING) } + ) { schema -> + when (schema.type) { + Schema.Type.LONG -> { + { encodeValue(value) } + } + + Schema.Type.INT -> { + { encodeValue(value.toIntExact()) } + } + + Schema.Type.FLOAT -> { + { encodeValue(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { encodeValue(value.toDouble()) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeFloat(value: Float) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.DOUBLE, Schema.Type.FLOAT) } + ) { schema -> + when (schema.type) { + Schema.Type.FLOAT -> { + { encodeValue(value) } + } + + Schema.Type.DOUBLE -> { + { encodeValue(value.toDouble()) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeDouble(value: Double) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.DOUBLE) } + ) { schema -> + when (schema.type) { + Schema.Type.DOUBLE -> { + { encodeValue(value) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeChar(value: Char) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.INT, Schema.Type.STRING) } + ) { schema -> + when (schema.type) { + Schema.Type.INT -> { + { encodeValue(value.code) } + } + + Schema.Type.STRING -> { + { encodeValue(value.toString()) } + } + + else -> null + } + } + } + + override fun encodeString(value: String) { + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.BYTES, Schema.Type.FIXED, Schema.Type.ENUM) } + ) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + { encodeValue(value) } + } + + Schema.Type.BYTES -> { + { encodeValue(value.encodeToByteArray()) } + } + + Schema.Type.FIXED -> { + if (value.length == schema.fixedSize) { + { encodeValue(value.encodeToByteArray()) } + } else { + null + } + } + + Schema.Type.ENUM -> { + { encodeValue(GenericData.EnumSymbol(schema, value)) } + } + + else -> null + } + } + } + + override fun encodeEnum( + enumDescriptor: SerialDescriptor, + index: Int, + ) { + /* + We allow enums as ENUM (must match the descriptor's full name), STRING or UNION. + For UNION, we look for an enum with the descriptor's full name, otherwise a string. + */ + val value = enumDescriptor.getElementName(index) + + encodeResolving( + { BadEncodedValueError(value, currentWriterSchema, Schema.Type.STRING, Schema.Type.ENUM) } + ) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + { encodeValue(value) } + } + + Schema.Type.ENUM -> { + if (schema.isFullNameOrAliasMatch(enumDescriptor)) { + { encodeValue(GenericData.EnumSymbol(schema, value)) } + } else { + null + } + } + + else -> null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/ArrayGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/ArrayGenericEncoder.kt new file mode 100644 index 00000000..925d252b --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/ArrayGenericEncoder.kt @@ -0,0 +1,52 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema +import org.apache.avro.generic.GenericArray +import org.apache.avro.generic.GenericData + +internal class ArrayGenericEncoder( + override val avro: Avro, + arraySize: Int, + private val schema: Schema, + private val onEncoded: (GenericArray<*>) -> Unit, +) : AbstractAvroGenericEncoder() { + private val values: Array = Array(arraySize) { null } + private var index = 0 + + override lateinit var currentWriterSchema: Schema + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + currentWriterSchema = schema.elementType + return true + } + + override fun endStructure(descriptor: SerialDescriptor) { + onEncoded(GenericData.Array(schema, values.asList())) + } + + override fun encodeValue(value: Any) { + values[index++] = value + } + + override fun encodeNull() { + encodeResolving( + { BadEncodedValueError(null, currentWriterSchema, Schema.Type.NULL) } + ) { + when (it.type) { + Schema.Type.NULL -> { + { values[index++] = null } + } + + else -> null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AvroValueGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AvroValueGenericEncoder.kt new file mode 100644 index 00000000..eea26709 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AvroValueGenericEncoder.kt @@ -0,0 +1,29 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import org.apache.avro.Schema + +internal class AvroValueGenericEncoder( + override val avro: Avro, + override var currentWriterSchema: Schema, + private val onEncoded: (Any?) -> Unit, +) : AbstractAvroGenericEncoder() { + override fun encodeValue(value: Any) { + onEncoded(value) + } + + override fun encodeNull() { + encodeResolving( + { BadEncodedValueError(null, currentWriterSchema, Schema.Type.NULL) } + ) { + when (it.type) { + Schema.Type.NULL -> { + { onEncoded(null) } + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/BytesGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/BytesGenericEncoder.kt new file mode 100644 index 00000000..8a0f9161 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/BytesGenericEncoder.kt @@ -0,0 +1,26 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.modules.SerializersModule +import java.nio.ByteBuffer + +internal class BytesGenericEncoder( + private val avro: Avro, + arraySize: Int, + private val onEncoded: (ByteBuffer) -> Unit, +) : AbstractEncoder() { + private val output: ByteBuffer = ByteBuffer.allocate(arraySize) + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun endStructure(descriptor: SerialDescriptor) { + onEncoded(output.rewind()) + } + + override fun encodeByte(value: Byte) { + output.put(value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/FixedGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/FixedGenericEncoder.kt new file mode 100644 index 00000000..e6f02b1c --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/FixedGenericEncoder.kt @@ -0,0 +1,37 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericFixed + +internal class FixedGenericEncoder( + private val avro: Avro, + arraySize: Int, + private val schema: Schema, + private val onEncoded: (GenericFixed) -> Unit, +) : AbstractEncoder() { + private val buffer = ByteArray(schema.fixedSize) + private var pos = 0 + + init { + if (arraySize != schema.fixedSize) { + throw SerializationException("Actual collection size $arraySize is greater than schema fixed size $schema") + } + } + + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun endStructure(descriptor: SerialDescriptor) { + onEncoded(GenericData.Fixed(schema, buffer)) + } + + override fun encodeByte(value: Byte) { + buffer[pos++] = value + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/MapGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/MapGenericEncoder.kt new file mode 100644 index 00000000..c5f98f46 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/MapGenericEncoder.kt @@ -0,0 +1,53 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +private val STRING_SCHEMA = Schema.create(Schema.Type.STRING) + +internal class MapGenericEncoder( + override val avro: Avro, + mapSize: Int, + private val schema: Schema, + private val onEncoded: (Map) -> Unit, +) : AbstractAvroGenericEncoder() { + private val entries: MutableList> = ArrayList(mapSize) + private var currentKey: String? = null + + override lateinit var currentWriterSchema: Schema + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + currentWriterSchema = + if (index % 2 == 0) { + currentKey = null + STRING_SCHEMA + } else { + schema.valueType + } + return true + } + + override fun endStructure(descriptor: SerialDescriptor) { + onEncoded(entries.associate { it.first to it.second }) + } + + override fun encodeValue(value: Any) { + val key = currentKey + if (key == null) { + currentKey = value.toString() + } else { + entries.add(key to value) + } + } + + override fun encodeNull() { + val key = currentKey ?: throw SerializationException("Map key cannot be null") + entries.add(key to null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/PolymorphicEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/PolymorphicEncoder.kt new file mode 100644 index 00000000..e37f31bf --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/PolymorphicEncoder.kt @@ -0,0 +1,36 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema + +internal class PolymorphicEncoder( + private val avro: Avro, + private val schema: Schema, + private val onEncoded: (Any) -> Unit, +) : AbstractEncoder() { + override val serializersModule: SerializersModule + get() = avro.serializersModule + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + // index 0 is the type discriminator, index 1 is the value itself + // we don't need the type discriminator here + return index == 1 + } + + override fun encodeSerializableValue( + serializer: SerializationStrategy, + value: T, + ) { + // Here we don't need to resolve the union, as it is already resolved inside AvroTaggedEncoder.beginStructure + AvroValueGenericEncoder(avro, schema) { + onEncoded(it ?: throw UnsupportedOperationException("Polymorphic types cannot encode null values")) + }.encodeSerializableValue(serializer, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/RecordGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/RecordGenericEncoder.kt new file mode 100644 index 00000000..1ae05fa7 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/RecordGenericEncoder.kt @@ -0,0 +1,58 @@ +package com.github.avrokotlin.avro4k.internal.encoder.generic + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.ListRecord +import com.github.avrokotlin.avro4k.internal.EncodingStep +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema +import org.apache.avro.generic.GenericRecord + +internal class RecordGenericEncoder( + override val avro: Avro, + descriptor: SerialDescriptor, + private val schema: Schema, + private val onEncoded: (GenericRecord) -> Unit, +) : AbstractAvroGenericEncoder() { + private val fieldValues: Array = Array(schema.fields.size) { null } + + private val classDescriptor = avro.recordResolver.resolveFields(schema, descriptor) + private lateinit var currentField: Schema.Field + + override lateinit var currentWriterSchema: Schema + + override fun encodeElement( + descriptor: SerialDescriptor, + index: Int, + ): Boolean { + super.encodeElement(descriptor, index) + return when (val step = classDescriptor.encodingSteps[index]) { + is EncodingStep.SerializeWriterField -> { + val field = schema.fields[step.writerFieldIndex] + currentField = field + currentWriterSchema = field.schema() + true + } + + is EncodingStep.IgnoreElement -> { + false + } + + is EncodingStep.MissingWriterFieldFailure -> { + throw SerializationException("No serializable element found for writer field ${step.writerFieldIndex} in schema $schema") + } + } + } + + override fun endStructure(descriptor: SerialDescriptor) { + onEncoded(ListRecord(schema, fieldValues.asList())) + } + + override fun encodeValue(value: Any) { + fieldValues[currentField.pos()] = value + } + + override fun encodeNull() { + fieldValues[currentField.pos()] = null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/exceptions.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/exceptions.kt new file mode 100644 index 00000000..1455fd3d --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/exceptions.kt @@ -0,0 +1,93 @@ +@file:Suppress("FunctionName") + +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroDecoder +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.apache.avro.Schema +import kotlin.reflect.KClass + +internal class AvroSchemaGenerationException(message: String) : SerializationException(message) + +context(Decoder) +internal fun DecodedNullError() = SerializationException("Unexpected null value, Decoder.decodeTaggedNotNullMark should be called first") + +context(Decoder) +internal fun DecodedNullError( + descriptor: SerialDescriptor, + elementIndex: Int, +) = SerializationException( + "Unexpected null value for field '${descriptor.getElementName(elementIndex)}' for type '${descriptor.serialName}', Decoder.decodeTaggedNotNullMark should be called first" +) + +internal fun Decoder.IllegalIndexedAccessError() = UnsupportedOperationException("${this::class.qualifiedName} does not support indexed access") + +context(Decoder) +internal inline fun BadDecodedValueError( + value: Any?, + firstExpectedType: KClass<*>, + vararg expectedTypes: KClass<*>, +): SerializationException { + val allExpectedTypes = listOf(firstExpectedType) + expectedTypes + return if (value == null) { + SerializationException( + "Decoded null value for ${ExpectedType::class.qualifiedName} kind, expected one of [${allExpectedTypes.joinToString { it.qualifiedName!! }}]" + ) + } else { + SerializationException( + "Decoded value '$value' of type ${value::class.qualifiedName} for " + + "${ExpectedType::class.qualifiedName} kind, expected one of [${allExpectedTypes.joinToString { it.qualifiedName!! }}]" + ) + } +} + +context(Decoder) +internal fun BadDecodedValueError( + value: Any?, + expectedKind: SerialKind, + firstExpectedType: KClass<*>, + vararg expectedTypes: KClass<*>, +): SerializationException { + val allExpectedTypes = listOf(firstExpectedType) + expectedTypes + return if (value == null) { + SerializationException( + "Decoded null value for $expectedKind kind, expected one of [${allExpectedTypes.joinToString { it.qualifiedName!! }}]" + ) + } else { + SerializationException( + "Decoded value '$value' of type ${value::class.qualifiedName} for $expectedKind kind, expected one of [${allExpectedTypes.joinToString { it.qualifiedName!! }}]" + ) + } +} + +internal fun AvroDecoder.UnexpectedDecodeSchemaError( + actualType: String, + firstExpectedType: Schema.Type, + vararg expectedTypes: Schema.Type, +): SerializationException { + val allExpectedTypes = listOf(firstExpectedType) + expectedTypes + return SerializationException( + "For $actualType, expected type one of $allExpectedTypes, but had writer schema $currentWriterSchema" + ) +} + +context(Encoder) +internal fun BadEncodedValueError( + value: Any?, + writerSchema: Schema, + firstExpectedType: Schema.Type, + vararg expectedTypes: Schema.Type, +): SerializationException { + val allExpectedTypes = listOf(firstExpectedType) + expectedTypes + return if (value == null) { + SerializationException("Encoded null value, expected one of $allExpectedTypes, actual writer schema $writerSchema") + } else { + SerializationException( + "Encoded value '$value' of type ${value::class.qualifiedName}, expected one of $allExpectedTypes, actual writer schema $writerSchema" + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt new file mode 100644 index 00000000..3ecfdd3b --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt @@ -0,0 +1,183 @@ +package com.github.avrokotlin.avro4k.internal + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.TextNode +import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroProp +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.capturedKClass +import kotlinx.serialization.descriptors.elementDescriptors +import kotlinx.serialization.descriptors.getContextualDescriptor +import kotlinx.serialization.descriptors.getPolymorphicDescriptors +import kotlinx.serialization.descriptors.nonNullOriginal +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.serializerOrNull +import org.apache.avro.LogicalType +import org.apache.avro.Schema + +internal inline fun SerialDescriptor.findAnnotation() = annotations.firstNotNullOfOrNull { it as? T } + +internal inline fun SerialDescriptor.findAnnotations() = annotations.filterIsInstance() + +@PublishedApi +internal inline fun SerialDescriptor.findElementAnnotation(elementIndex: Int): T? = getElementAnnotations(elementIndex).firstNotNullOfOrNull { it as? T } + +internal inline fun SerialDescriptor.findElementAnnotations(elementIndex: Int) = getElementAnnotations(elementIndex).filterIsInstance() + +internal val SerialDescriptor.nonNullSerialName: String get() = nonNullOriginal.serialName +internal val SerialDescriptor.namespace: String? get() = serialName.substringBeforeLast('.', "").takeIf { it.isNotEmpty() } + +internal val Schema.nullable: Schema + get() { + if (isNullable) return this + return if (isUnion) { + Schema.createUnion(listOf(Schema.create(Schema.Type.NULL)) + this.types) + } else { + Schema.createUnion(Schema.create(Schema.Type.NULL), this) + } + } + +internal fun Schema.asSchemaList(): List { + if (!isUnion) return listOf(this) + return types +} + +internal fun Schema.isNamedSchema(): Boolean { + return this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED +} + +internal fun Schema.isFullNameOrAliasMatch(descriptor: SerialDescriptor): Boolean { + return isFullNameMatch(descriptor.nonNullSerialName) || descriptor.aliases.any { isFullNameMatch(it) } +} + +internal fun Schema.isFullNameMatch(fullNameToMatch: String): Boolean { + return fullName == fullNameToMatch || + (type == Schema.Type.RECORD || type == Schema.Type.ENUM || type == Schema.Type.FIXED) && + aliases.any { it == fullNameToMatch } +} + +internal val SerialDescriptor.aliases: Set + get() = + findAnnotation()?.value?.toSet() ?: emptySet() + +private val SCHEMA_PLACEHOLDER = Schema.create(Schema.Type.NULL) + +internal fun Schema.copy( + name: String = this.name, + doc: String? = this.doc, + namespace: String? = if (this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED) this.namespace else null, + aliases: Set = if (this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED) this.aliases else emptySet(), + isError: Boolean = if (this.type == Schema.Type.RECORD) this.isError else false, + types: List = if (this.isUnion) this.types.toList() else emptyList(), + enumSymbols: List = if (this.type == Schema.Type.ENUM) this.enumSymbols.toList() else emptyList(), + fields: List? = if (this.type == Schema.Type.RECORD && this.hasFields()) this.fields.map { it.copy() } else null, + enumDefault: String? = if (this.type == Schema.Type.ENUM) this.enumDefault else null, + fixedSize: Int = if (this.type == Schema.Type.FIXED) this.fixedSize else -1, + valueType: Schema = if (this.type == Schema.Type.MAP) this.valueType else SCHEMA_PLACEHOLDER, + elementType: Schema = if (this.type == Schema.Type.ARRAY) this.elementType else SCHEMA_PLACEHOLDER, + objectProps: Map = this.objectProps, + additionalProps: Map = emptyMap(), + logicalType: LogicalType? = this.logicalType, +): Schema { + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") + return when (type) { + Schema.Type.RECORD -> if (hasFields()) Schema.createRecord(name, doc, namespace, isError, fields) else Schema.createRecord(name, doc, namespace, isError) + Schema.Type.ENUM -> Schema.createEnum(name, doc, namespace, enumSymbols, enumDefault) + Schema.Type.FIXED -> Schema.createFixed(name, doc, namespace, fixedSize) + + Schema.Type.UNION -> Schema.createUnion(types) + Schema.Type.MAP -> Schema.createMap(valueType) + Schema.Type.ARRAY -> Schema.createArray(elementType) + Schema.Type.BYTES, + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.BOOLEAN, + Schema.Type.NULL, + -> Schema.create(type) + } + .also { newSchema -> + objectProps.forEach { (key, value) -> newSchema.addProp(key, value) } + additionalProps.forEach { (key, value) -> newSchema.addProp(key, value) } + logicalType?.addToSchema(newSchema) + aliases.forEach { newSchema.addAlias(it) } + } +} + +internal fun Schema.Field.copy( + name: String = this.name(), + schema: Schema = this.schema(), + doc: String? = this.doc(), + defaultVal: Any? = this.defaultVal(), + order: Schema.Field.Order = this.order(), + aliases: Set = this.aliases(), + objectProps: Map = this.objectProps, + additionalProps: Map = emptyMap(), +): Schema.Field { + return Schema.Field(name, schema, doc, defaultVal, order) + .also { newSchema -> + objectProps.forEach { (key, value) -> newSchema.addProp(key, value) } + additionalProps.forEach { (key, value) -> newSchema.addProp(key, value) } + aliases.forEach { newSchema.addAlias(it) } + } +} + +@ExperimentalSerializationApi +internal fun SerialDescriptor.possibleSerializationSubclasses(serializersModule: SerializersModule): Sequence { + return when (this.kind) { + PolymorphicKind.SEALED -> + elementDescriptors.asSequence() + .filter { it.kind == SerialKind.CONTEXTUAL } + .flatMap { it.elementDescriptors } + .flatMap { it.possibleSerializationSubclasses(serializersModule) } + + PolymorphicKind.OPEN -> + serializersModule.getPolymorphicDescriptors(this@possibleSerializationSubclasses).asSequence() + .flatMap { it.possibleSerializationSubclasses(serializersModule) } + + SerialKind.CONTEXTUAL -> sequenceOf(getNonNullContextualDescriptor(serializersModule)) + + else -> sequenceOf(this) + } +} + +@OptIn(InternalSerializationApi::class) +internal fun SerialDescriptor.getNonNullContextualDescriptor(serializersModule: SerializersModule) = + requireNotNull(serializersModule.getContextualDescriptor(this) ?: this.capturedKClass?.serializerOrNull()?.descriptor) { + "No descriptor found in serialization context for $this" + } + +/** + * Returns true if the given content is starting with `"`, {`, `[`, a digit or equals to `null`. + * It doesn't check if the content is valid json. + * It skips the whitespaces at the beginning of the content. + */ +internal fun String.isStartingAsJson(): Boolean { + val i = this.indexOfFirst { !it.isWhitespace() } + if (i == -1) { + return false + } + val c = this[i] + return c == '{' || c == '"' || c == '[' || c.isDigit() || this == "null" || this == "true" || this == "false" +} + +private val objectMapper by lazy { ObjectMapper() } + +internal val AvroProp.jsonNode: JsonNode + get() { + if (value.isStartingAsJson()) { + return objectMapper.readTree(value) + } + return TextNode.valueOf(value) + } + +internal fun SerialDescriptor.getElementIndexNullable(name: String): Int? { + return getElementIndex(name).takeIf { it >= 0 } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt new file mode 100644 index 00000000..db09f93c --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt @@ -0,0 +1,211 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.internal.asSchemaList +import com.github.avrokotlin.avro4k.internal.isStartingAsJson +import com.github.avrokotlin.avro4k.internal.jsonNode +import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import com.github.avrokotlin.avro4k.serializer.ElementLocation +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.booleanOrNull +import org.apache.avro.JsonProperties +import org.apache.avro.Schema + +internal class ClassVisitor( + descriptor: SerialDescriptor, + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorClassVisitor { + private val fields = mutableListOf() + private val schemaAlreadyResolved: Boolean + private val schema: Schema + + init { + var schemaAlreadyResolved = true + schema = + context.resolvedSchemas.getOrPut(descriptor.nonNullSerialName) { + schemaAlreadyResolved = false + + val annotations = TypeAnnotations(descriptor) + val schema = + Schema.createRecord( + // name = + descriptor.nonNullSerialName, + // doc = + annotations.doc?.value, + // namespace = + null, + // isError = + false + ) + annotations.aliases?.value?.forEach { schema.addAlias(it) } + annotations.props.forEach { schema.addProp(it.key, it.jsonNode) } + schema + } + this.schemaAlreadyResolved = schemaAlreadyResolved + } + + override fun visitClassElement( + descriptor: SerialDescriptor, + elementIndex: Int, + ): SerialDescriptorValueVisitor? { + if (schemaAlreadyResolved) { + return null + } + return ValueVisitor(context.copy(inlinedElements = listOf(ElementLocation(descriptor, elementIndex)))) { fieldSchema -> + fields.add( + createField( + context.avro.configuration.fieldNamingStrategy.resolve(descriptor, elementIndex), + FieldAnnotations(descriptor, elementIndex), + fieldSchema + ) + ) + } + } + + override fun endClassVisit(descriptor: SerialDescriptor) { + if (!schemaAlreadyResolved) { + schema.fields = fields + } + onSchemaBuilt(schema) + } + + /** + * Create a field with the given annotations. + * Here are managed the generic field level annotations: + * - namespaceOverride + * - default (also sort unions according to the default value) + * - aliases + * - doc + * - props & json props + */ + private fun createField( + fieldName: String, + annotations: FieldAnnotations, + elementSchema: Schema, + ): Schema.Field { + val (finalSchema, fieldDefault) = getDefaultAndReorderUnionIfNeeded(annotations, elementSchema) + + val field = + Schema.Field( + fieldName, + finalSchema, + annotations.doc?.value, + fieldDefault + ) + annotations.aliases?.value?.forEach { field.addAlias(it) } + annotations.props.forEach { field.addProp(it.key, it.jsonNode) } + return field + } + + private fun getDefaultAndReorderUnionIfNeeded( + annotations: FieldAnnotations, + elementSchema: Schema, + ): Pair { + val defaultValue = annotations.default?.toAvroObject() + if (defaultValue == null) { + if (context.configuration.implicitNulls && elementSchema.isNullable) { + return elementSchema.moveToHeadOfUnion { it.type == Schema.Type.NULL } to JsonProperties.NULL_VALUE + } else if (context.configuration.implicitEmptyCollections) { + elementSchema.asSchemaList().forEachIndexed { index, schema -> + if (schema.type == Schema.Type.ARRAY) { + return elementSchema.moveToHeadOfUnion(index) to emptyList() + } + if (schema.type == Schema.Type.MAP) { + return elementSchema.moveToHeadOfUnion(index) to emptyMap() + } + } + } + } else if (defaultValue === JsonProperties.NULL_VALUE) { + // If the user sets "null" but the field is not nullable, maybe the user wanted to set the "null" string default + val finalSchema = elementSchema.moveToHeadOfUnion { it.type == Schema.Type.NULL } + val adaptedDefault = if (!elementSchema.isNullable) "null" else defaultValue + return finalSchema to adaptedDefault + } else if (elementSchema.asSchemaList().any { it.logicalType?.name == CHAR_LOGICAL_TYPE_NAME }) { + // requires a string default value with exactly 1 character, and map the character to the char code as it is an int + if (defaultValue is String && defaultValue.length == 1) { + return elementSchema.moveToHeadOfUnion { it.logicalType?.name == CHAR_LOGICAL_TYPE_NAME } to defaultValue.single().code + } else { + throw SerializationException("Default value for Char must be a single character string. Invalid value of type ${defaultValue::class.qualifiedName}: $defaultValue") + } + } else if (elementSchema.isNullable) { + // default is not null, so let's just put the null schema at the end of the union which should cover the main use cases + return elementSchema.moveToTailOfUnion { it.type === Schema.Type.NULL } to defaultValue + } + return elementSchema to defaultValue + } +} + +private fun AvroDefault.toAvroObject(): Any { + if (value.isStartingAsJson()) { + return Json.parseToJsonElement(value).toAvroObject() + } + return value +} + +private fun JsonElement.toAvroObject(): Any = + when (this) { + is JsonNull -> JsonProperties.NULL_VALUE + is JsonObject -> this.entries.associate { it.key to it.value.toAvroObject() } + is JsonArray -> this.map { it.toAvroObject() } + is JsonPrimitive -> + when { + this.isString -> this.content + this.booleanOrNull != null -> this.boolean + else -> { + this.content.toBigDecimal().stripTrailingZeros().let { + if (it.scale() <= 0) { + it.toBigInteger() + } else { + it + } + } + } + } + } + +private fun Schema.moveToHeadOfUnion(predicate: (Schema) -> Boolean): Schema { + if (!isUnion) { + return this + } + types.indexOfFirst(predicate).let { index -> + if (index == -1) { + return this + } + return moveToHeadOfUnion(index) + } +} + +private fun Schema.moveToHeadOfUnion(typeIndex: Int): Schema { + if (!isUnion || typeIndex >= types.size) { + return this + } + return Schema.createUnion(types.toMutableList().apply { add(0, removeAt(typeIndex)) }) +} + +private fun Schema.moveToTailOfUnion(predicate: (Schema) -> Boolean): Schema { + if (!isUnion) { + return this + } + types.indexOfFirst(predicate).let { index -> + if (index == -1) { + return this + } + return moveToTailOfUnion(index) + } +} + +private fun Schema.moveToTailOfUnion(typeIndex: Int): Schema { + if (!isUnion || typeIndex >= types.size) { + return this + } + return Schema.createUnion(types.toMutableList().apply { add(removeAt(typeIndex)) }) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt new file mode 100644 index 00000000..d3bac92d --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt @@ -0,0 +1,42 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.internal.copy +import com.github.avrokotlin.avro4k.internal.isNamedSchema +import com.github.avrokotlin.avro4k.internal.jsonNode +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.ElementLocation +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class InlineClassVisitor( + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorInlineClassVisitor { + override fun visitInlineClassElement( + inlineClassDescriptor: SerialDescriptor, + inlineElementIndex: Int, + ): SerialDescriptorValueVisitor { + val inlinedElements = context.inlinedElements + ElementLocation(inlineClassDescriptor, inlineElementIndex) + return ValueVisitor(context.copy(inlinedElements = inlinedElements)) { generatedSchema -> + val annotations = InlineClassFieldAnnotations(inlineClassDescriptor) + + val props = annotations.props.toList() + val schema = + if (props.isNotEmpty()) { + if (generatedSchema.isNamedSchema()) { + throw SerializationException( + "The value class property '${inlineClassDescriptor.serialName}.${inlineClassDescriptor.getElementName(0)}' has " + + "forbidden additional properties $props for the named schema ${generatedSchema.fullName}. " + + "Please create your own serializer extending ${AvroSerializer::class.qualifiedName} to add properties to a named schema." + ) + } + generatedSchema.copy(additionalProps = props.associate { it.key to it.jsonNode }) + } else { + generatedSchema + } + + onSchemaBuilt(schema) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ListVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ListVisitor.kt new file mode 100644 index 00000000..2493e79c --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ListVisitor.kt @@ -0,0 +1,24 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class ListVisitor( + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorListVisitor { + private lateinit var itemSchema: Schema + + override fun visitListItem( + listDescriptor: SerialDescriptor, + itemElementIndex: Int, + ): SerialDescriptorValueVisitor { + return ValueVisitor(context) { + itemSchema = it + } + } + + override fun endListVisit(descriptor: SerialDescriptor) { + onSchemaBuilt(Schema.createArray(itemSchema)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt new file mode 100644 index 00000000..2a6681f4 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt @@ -0,0 +1,66 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class MapVisitor( + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorMapVisitor { + private lateinit var valueSchema: Schema + + override fun visitMapKey( + mapDescriptor: SerialDescriptor, + keyElementIndex: Int, + ) = ValueVisitor(context) { + // In avro, the map key must be a string. + // Here we just delegate the schema building to the value visitor + // and then check if the output schema is about a type that we can + // stringify (e.g. when .toString() makes sense). + // Here we are just checking if the schema is string-compatible. We don't need to + // store the schema as it is always a string. + if (it.isNullable()) { + throw AvroSchemaGenerationException("Map key cannot be nullable. Actual generated map key schema: $it") + } + if (!it.isStringable()) { + throw AvroSchemaGenerationException("Map key must be string-able (boolean, number, enum, or string). Actual generated map key schema: $it") + } + } + + override fun visitMapValue( + mapDescriptor: SerialDescriptor, + valueElementIndex: Int, + ) = ValueVisitor(context) { + valueSchema = it + } + + override fun endMapVisit(descriptor: SerialDescriptor) { + onSchemaBuilt(Schema.createMap(valueSchema)) + } +} + +private fun Schema.isStringable(): Boolean = + when (type) { + Schema.Type.BOOLEAN, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE, + Schema.Type.STRING, + Schema.Type.ENUM, + -> true + + Schema.Type.NULL, + // bytes could be stringified, but it's not a good idea as it can produce unreadable strings. + Schema.Type.BYTES, + // same, just bytes. Btw, if the user wants to stringify it, he can use @Contextual or custom @Serializable serializer. + Schema.Type.FIXED, + Schema.Type.ARRAY, + Schema.Type.MAP, + Schema.Type.RECORD, + null, + -> false + + Schema.Type.UNION -> types.all { it.isStringable() } + } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/PolymorphicVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/PolymorphicVisitor.kt new file mode 100644 index 00000000..adcf20b9 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/PolymorphicVisitor.kt @@ -0,0 +1,30 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import kotlinx.serialization.descriptors.SerialDescriptor +import org.apache.avro.Schema + +internal class PolymorphicVisitor( + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorPolymorphicVisitor { + private val possibleSchemas = mutableListOf() + + override fun visitPolymorphicFoundDescriptor(descriptor: SerialDescriptor): SerialDescriptorValueVisitor { + return ValueVisitor(context) { + possibleSchemas += it + } + } + + override fun endPolymorphicVisit(descriptor: SerialDescriptor) { + if (possibleSchemas.isEmpty()) { + throw AvroSchemaGenerationException("Polymorphic descriptor '$descriptor' must have at least one possible schema") + } + if (possibleSchemas.size == 1) { + // flatten the useless union schema + onSchemaBuilt(possibleSchemas.first()) + } else { + onSchemaBuilt(Schema.createUnion(possibleSchemas)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/SerialDescriptorVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/SerialDescriptorVisitor.kt new file mode 100644 index 00000000..b879d542 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/SerialDescriptorVisitor.kt @@ -0,0 +1,169 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.internal.getNonNullContextualDescriptor +import com.github.avrokotlin.avro4k.internal.possibleSerializationSubclasses +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.modules.SerializersModule + +internal interface SerialDescriptorValueVisitor { + val serializersModule: SerializersModule + + /** + * Called when the [descriptor]'s kind is a [PrimitiveKind]. + */ + fun visitPrimitive( + descriptor: SerialDescriptor, + kind: PrimitiveKind, + ) + + /** + * Called when the [descriptor]'s kind is an [SerialKind.ENUM]. + */ + fun visitEnum(descriptor: SerialDescriptor) + + /** + * Called when the [descriptor]'s kind is an [StructureKind.OBJECT]. + */ + fun visitObject(descriptor: SerialDescriptor) + + /** + * Called when the [descriptor]'s kind is a [PolymorphicKind]. + * @return null if we don't want to visit the polymorphic type + */ + fun visitPolymorphic( + descriptor: SerialDescriptor, + kind: PolymorphicKind, + ): SerialDescriptorPolymorphicVisitor? + + /** + * Called when the [descriptor]'s kind is a [StructureKind.CLASS]. + * Note that when the [descriptor] is an inline class, [visitInlineClass] is called instead. + * @return null if we don't want to visit the class + */ + fun visitClass(descriptor: SerialDescriptor): SerialDescriptorClassVisitor? + + /** + * Called when the [descriptor]'s kind is a [StructureKind.LIST]. + * @return null if we don't want to visit the list + */ + fun visitList(descriptor: SerialDescriptor): SerialDescriptorListVisitor? + + /** + * Called when the [descriptor]'s kind is a [StructureKind.MAP]. + * @return null if we don't want to visit the map + */ + fun visitMap(descriptor: SerialDescriptor): SerialDescriptorMapVisitor? + + /** + * Called when the [descriptor] is about a value class (e.g. its kind is a [StructureKind.CLASS] and [SerialDescriptor.isInline] is true). + * @return null if we don't want to visit the inline class + */ + fun visitInlineClass(descriptor: SerialDescriptor): SerialDescriptorInlineClassVisitor? + + fun visitValue(descriptor: SerialDescriptor) { + if (descriptor.isInline) { + visitInlineClass(descriptor)?.apply { + visitInlineClassElement(descriptor, 0)?.visitValue(descriptor.getElementDescriptor(0)) + } + } else { + when (descriptor.kind) { + is PrimitiveKind -> visitPrimitive(descriptor, descriptor.kind as PrimitiveKind) + SerialKind.ENUM -> visitEnum(descriptor) + SerialKind.CONTEXTUAL -> visitValue(descriptor.getNonNullContextualDescriptor(serializersModule)) + StructureKind.CLASS -> + visitClass(descriptor)?.apply { + for (elementIndex in (0 until descriptor.elementsCount)) { + visitClassElement(descriptor, elementIndex)?.visitValue(descriptor.getElementDescriptor(elementIndex)) + } + }?.endClassVisit(descriptor) + + StructureKind.LIST -> + visitList(descriptor)?.apply { + visitListItem(descriptor, 0)?.visitValue(descriptor.getElementDescriptor(0)) + }?.endListVisit(descriptor) + + StructureKind.MAP -> + visitMap(descriptor)?.apply { + visitMapKey(descriptor, 0)?.visitValue(descriptor.getElementDescriptor(0)) + visitMapValue(descriptor, 1)?.visitValue(descriptor.getElementDescriptor(1)) + }?.endMapVisit(descriptor) + + is PolymorphicKind -> + visitPolymorphic(descriptor, descriptor.kind as PolymorphicKind)?.apply { + descriptor.possibleSerializationSubclasses(serializersModule).sortedBy { it.serialName }.forEach { implementationDescriptor -> + visitPolymorphicFoundDescriptor(implementationDescriptor)?.visitValue(implementationDescriptor) + } + }?.endPolymorphicVisit(descriptor) + + StructureKind.OBJECT -> visitObject(descriptor) + } + } + } +} + +internal interface SerialDescriptorMapVisitor { + /** + * @return null if we don't want to visit the map key + */ + fun visitMapKey( + mapDescriptor: SerialDescriptor, + keyElementIndex: Int, + ): SerialDescriptorValueVisitor? + + /** + * @return null if we don't want to visit the map value + */ + fun visitMapValue( + mapDescriptor: SerialDescriptor, + valueElementIndex: Int, + ): SerialDescriptorValueVisitor? + + fun endMapVisit(descriptor: SerialDescriptor) +} + +internal interface SerialDescriptorListVisitor { + /** + * @return null if we don't want to visit the list item + */ + fun visitListItem( + listDescriptor: SerialDescriptor, + itemElementIndex: Int, + ): SerialDescriptorValueVisitor? + + fun endListVisit(descriptor: SerialDescriptor) +} + +internal interface SerialDescriptorPolymorphicVisitor { + /** + * @return null if we don't want to visit the found polymorphic descriptor + */ + fun visitPolymorphicFoundDescriptor(descriptor: SerialDescriptor): SerialDescriptorValueVisitor? + + fun endPolymorphicVisit(descriptor: SerialDescriptor) +} + +internal interface SerialDescriptorClassVisitor { + /** + * @return null if we don't want to visit the class element + */ + fun visitClassElement( + descriptor: SerialDescriptor, + elementIndex: Int, + ): SerialDescriptorValueVisitor? + + fun endClassVisit(descriptor: SerialDescriptor) +} + +internal interface SerialDescriptorInlineClassVisitor { + /** + * @return null if we don't want to visit the inline class element + */ + fun visitInlineClassElement( + inlineClassDescriptor: SerialDescriptor, + inlineElementIndex: Int, + ): SerialDescriptorValueVisitor? +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt new file mode 100644 index 00000000..e2e138bd --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt @@ -0,0 +1,124 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware +import com.github.avrokotlin.avro4k.internal.jsonNode +import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.serializer.AvroSchemaSupplier +import com.github.avrokotlin.avro4k.serializer.stringable +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nonNullOriginal +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.LogicalType +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder + +internal class ValueVisitor internal constructor( + private val context: VisitorContext, + private val onSchemaBuilt: (Schema) -> Unit, +) : SerialDescriptorValueVisitor { + private var isNullable: Boolean = false + + override val serializersModule: SerializersModule + get() = context.avro.serializersModule + + constructor(avro: Avro, onSchemaBuilt: (Schema) -> Unit) : this( + VisitorContext( + avro, + mutableMapOf() + ), + onSchemaBuilt = onSchemaBuilt + ) + + override fun visitPrimitive( + descriptor: SerialDescriptor, + kind: PrimitiveKind, + ) = setSchema(kind.toSchema()) + + override fun visitEnum(descriptor: SerialDescriptor) { + val annotations = TypeAnnotations(descriptor) + + val schema = + SchemaBuilder.enumeration(descriptor.nonNullSerialName) + .doc(annotations.doc?.value) + .defaultSymbol(context.avro.enumResolver.getDefaultValueIndex(descriptor)?.let { descriptor.getElementName(it) }) + .symbols(*descriptor.elementNamesArray) + + annotations.aliases?.value?.forEach { schema.addAlias(it) } + annotations.props.forEach { schema.addProp(it.key, it.jsonNode) } + + setSchema(schema) + } + + private val SerialDescriptor.elementNamesArray: Array + get() = Array(elementsCount) { getElementName(it) } + + override fun visitObject(descriptor: SerialDescriptor) { + // we consider objects as records without fields. + visitClass(descriptor).endClassVisit(descriptor) + } + + override fun visitClass(descriptor: SerialDescriptor) = ClassVisitor(descriptor, context.copy(inlinedElements = emptyList())) { setSchema(it) } + + override fun visitPolymorphic( + descriptor: SerialDescriptor, + kind: PolymorphicKind, + ) = PolymorphicVisitor(context) { setSchema(it) } + + override fun visitList(descriptor: SerialDescriptor) = ListVisitor(context.copy(inlinedElements = emptyList())) { setSchema(it) } + + override fun visitMap(descriptor: SerialDescriptor) = MapVisitor(context.copy(inlinedElements = emptyList())) { setSchema(it) } + + override fun visitInlineClass(descriptor: SerialDescriptor) = InlineClassVisitor(context) { setSchema(it) } + + private fun setSchema(schema: Schema) { + if (isNullable && !schema.isNullable) { + onSchemaBuilt(schema.nullable) + } else { + onSchemaBuilt(schema) + } + } + + override fun visitValue(descriptor: SerialDescriptor) { + val finalDescriptor = SerializerLocatorMiddleware.apply(unwrapNullable(descriptor)) + + if (finalDescriptor is AvroSchemaSupplier) { + setSchema(finalDescriptor.getSchema(context)) + return + } + + if (context.inlinedElements.any { it.stringable != null }) { + setSchema(Schema.create(Schema.Type.STRING)) + return + } + + super.visitValue(finalDescriptor) + } + + private fun unwrapNullable(descriptor: SerialDescriptor): SerialDescriptor { + if (descriptor.isNullable) { + isNullable = true + return descriptor.nonNullOriginal + } + return descriptor + } +} + +internal const val CHAR_LOGICAL_TYPE_NAME = "char" +private val CHAR_LOGICAL_TYPE = LogicalType(CHAR_LOGICAL_TYPE_NAME) + +private fun PrimitiveKind.toSchema(): Schema = + when (this) { + PrimitiveKind.BOOLEAN -> Schema.create(Schema.Type.BOOLEAN) + PrimitiveKind.CHAR -> Schema.create(Schema.Type.INT).also { CHAR_LOGICAL_TYPE.addToSchema(it) } + PrimitiveKind.BYTE -> Schema.create(Schema.Type.INT) + PrimitiveKind.SHORT -> Schema.create(Schema.Type.INT) + PrimitiveKind.INT -> Schema.create(Schema.Type.INT) + PrimitiveKind.LONG -> Schema.create(Schema.Type.LONG) + PrimitiveKind.FLOAT -> Schema.create(Schema.Type.FLOAT) + PrimitiveKind.DOUBLE -> Schema.create(Schema.Type.DOUBLE) + PrimitiveKind.STRING -> Schema.create(Schema.Type.STRING) + } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt new file mode 100644 index 00000000..065f88dc --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt @@ -0,0 +1,85 @@ +package com.github.avrokotlin.avro4k.internal.schema + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroConfiguration +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.AvroDoc +import com.github.avrokotlin.avro4k.AvroProp +import com.github.avrokotlin.avro4k.internal.findAnnotation +import com.github.avrokotlin.avro4k.internal.findAnnotations +import com.github.avrokotlin.avro4k.internal.findElementAnnotation +import com.github.avrokotlin.avro4k.internal.findElementAnnotations +import com.github.avrokotlin.avro4k.serializer.ElementLocation +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import org.apache.avro.Schema + +internal data class VisitorContext( + val avro: Avro, + val resolvedSchemas: MutableMap, + override val inlinedElements: List = emptyList(), +) : SchemaSupplierContext { + override val configuration: AvroConfiguration + get() = avro.configuration +} + +/** + * Contains all the annotations for a field of a class (kind == CLASS && isInline == true). + */ +internal data class InlineClassFieldAnnotations( + val props: Sequence, +) { + constructor(inlineClassDescriptor: SerialDescriptor) : this( + sequence { + yieldAll(inlineClassDescriptor.findAnnotations()) + yieldAll(inlineClassDescriptor.findElementAnnotations(0)) + } + ) { + require(inlineClassDescriptor.isInline) { + "${InlineClassFieldAnnotations::class.qualifiedName} is only for inline classes, but trying to use it with non-inline class descriptor $inlineClassDescriptor" + } + } +} + +/** + * Contains all the annotations for a field of a class (kind == CLASS && isInline == false). + */ +internal data class FieldAnnotations( + val props: Sequence, + val aliases: AvroAlias?, + val doc: AvroDoc?, + val default: AvroDefault?, +) { + constructor(descriptor: SerialDescriptor, elementIndex: Int) : this( + descriptor.findElementAnnotations(elementIndex).asSequence(), + descriptor.findElementAnnotation(elementIndex), + descriptor.findElementAnnotation(elementIndex), + descriptor.findElementAnnotation(elementIndex) + ) { + require(descriptor.kind == StructureKind.CLASS) { + "${FieldAnnotations::class.qualifiedName} is only for classes, but trying at element index $elementIndex with non class descriptor $descriptor" + } + } +} + +/** + * Contains all the annotations for a class, object or enum (kind == CLASS || kind == OBJECT || kind == ENUM). + */ +internal data class TypeAnnotations( + val props: Sequence, + val aliases: AvroAlias?, + val doc: AvroDoc?, +) { + constructor(descriptor: SerialDescriptor) : this( + descriptor.findAnnotations().asSequence(), + descriptor.findAnnotation(), + descriptor.findAnnotation() + ) { + require(descriptor.kind == StructureKind.CLASS || descriptor.kind == StructureKind.OBJECT || descriptor.kind == SerialKind.ENUM) { + "TypeAnnotations are only for classes, objects and enums. Actual: $descriptor" + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataInputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataInputStream.kt deleted file mode 100644 index d8b6b173..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataInputStream.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import org.apache.avro.Schema -import org.apache.avro.file.DataFileStream -import org.apache.avro.generic.GenericData -import java.io.InputStream - -@Suppress("UNCHECKED_CAST") -class AvroDataInputStream(private val source: InputStream, - private val converter: (Any) -> T, - writerSchema: Schema?, - readerSchema: Schema?) : AvroInputStream { - - // if no reader or writer schema is specified, then we create a reader that uses what's present in the files - private val datumReader = when { - writerSchema == null && readerSchema == null -> GenericData.get().createDatumReader(null) - readerSchema == null -> GenericData.get().createDatumReader(writerSchema) - writerSchema == null -> GenericData.get().createDatumReader(readerSchema) - else -> GenericData.get().createDatumReader(writerSchema, readerSchema) - } - - private val dataFileReader = DataFileStream(source, datumReader) - - override fun next(): T? { - return if (dataFileReader.hasNext()) { - val obj = dataFileReader.next(null) - converter(obj) - } else null - } - - override fun close(): Unit = source.close() -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStream.kt deleted file mode 100644 index 39ae333e..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStream.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import org.apache.avro.Schema -import org.apache.avro.file.CodecFactory -import org.apache.avro.file.DataFileWriter -import org.apache.avro.generic.GenericDatumWriter -import org.apache.avro.generic.GenericRecord -import java.io.OutputStream - -/** - * An [AvroOutputStream] that writes the schema along with the messages. - * - * This is usually the format required when writing multiple messages to a single file. - * - * Some frameworks, such as a Kafka, store the Schema separately to messages, in which - * case the [AvroBinaryInputStream], which does not include the schema, might be more appropriate. - * - * @param output the underlying stream that data will be written to. - * @param converter used to convert the input type into a [GenericRecord] - * @param schema the schema that will be used to encode the data, sometimes called the writer schema - * @param codec compression codec - */ -class AvroDataOutputStream(private val output: OutputStream, - private val converter: (T) -> GenericRecord, - private val schema: Schema, - private val codec: CodecFactory) : AvroOutputStream { - - private val datumWriter = GenericDatumWriter(schema) - - private val writer = DataFileWriter(datumWriter).apply { - setCodec(codec) - create(schema, output) - } - - override fun close() { - flush() - writer.close() - } - - override fun flush(): Unit = writer.flush() - override fun fSync(): Unit = writer.fSync() - - override fun write(t: T): AvroOutputStream { - writer.append(converter(t)) - return this - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDecodeFormat.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDecodeFormat.kt deleted file mode 100644 index 40e84928..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroDecodeFormat.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import org.apache.avro.Schema -import java.io.InputStream - -/** - * Formats that can be used to decode the contents of an [InputStream]. - */ -sealed class AvroDecodeFormat { - abstract fun createInputStream(source : InputStream, converter: (Any) -> T) : AvroInputStream - /** - * Decoding a binary format with a header that contains the full schema, this is the format usually used for Avro files. - * - * See https://avro.apache.org/docs/current/spec.html#Data+Serialization+and+Deserialization - */ - data class Data(val writerSchema : Schema?, val readerSchema : Schema?) : AvroDecodeFormat() { - constructor(readWriteSchema : Schema?) : this(readWriteSchema, readWriteSchema) - override fun createInputStream(source : InputStream, converter: (Any) -> T) = when{ - writerSchema != null && readerSchema != null -> - AvroDataInputStream(source, converter, writerSchema, readerSchema) - writerSchema != null -> - AvroDataInputStream(source, converter, writerSchema, null) - readerSchema != null -> - AvroDataInputStream(source, converter, null, readerSchema) - else -> - AvroDataInputStream(source, converter, null, null) - } - } - /** - * Decodes the binary format without the header, the most compact format. - * - * See https://avro.apache.org/docs/current/spec.html#binary_encoding - */ - data class Binary(val writerSchema: Schema, val readerSchema: Schema) : AvroDecodeFormat() { - constructor(readWriteSchema : Schema) : this(readWriteSchema, readWriteSchema) - override fun createInputStream(source : InputStream, converter: (Any) -> T) = AvroBinaryInputStream(source, converter, writerSchema, readerSchema) - } - /** - * Decodes avro records that have been encoded in JSON. The most verbose format, but easy for a human to read. - * - * The avro json format does not include the schema. Thus the write and read schema is needed for decoding. - * - * See https://avro.apache.org/docs/current/spec.html#json_encoding - */ - data class Json(val writerSchema: Schema, val readerSchema: Schema) : AvroDecodeFormat() { - constructor(readWriteSchema : Schema) : this(readWriteSchema, readWriteSchema) - override fun createInputStream(source : InputStream, converter: (Any) -> T) = AvroJsonInputStream(source, converter, writerSchema, readerSchema) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroEncodeFormat.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroEncodeFormat.kt deleted file mode 100644 index 3534087d..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroEncodeFormat.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import org.apache.avro.Schema -import org.apache.avro.file.CodecFactory -import org.apache.avro.generic.GenericRecord -import java.io.OutputStream - -/** - * Formats that can be used to encode a record to an [OutputStream]. - */ -sealed class AvroEncodeFormat { - abstract fun createOutputStream(output : OutputStream, schema : Schema, converter : (T) -> GenericRecord) : AvroOutputStream - /** - * Encodes a record in a binary format with a header that contains the full schema, this is the format usually used when writing Avro files. - * - * See https://avro.apache.org/docs/current/spec.html#Data+Serialization+and+Deserialization - */ - data class Data(private val codecFactory: CodecFactory = CodecFactory.nullCodec()) : AvroEncodeFormat() { - override fun createOutputStream( - output: OutputStream, - schema: Schema, - converter: (T) -> GenericRecord - ): AvroOutputStream { - return AvroDataOutputStream(output, converter, schema, codecFactory) - } - } - /** - * Encodes the record in a binary format without schema information, the most compact format. - * - * See https://avro.apache.org/docs/current/spec.html#binary_encoding - */ - object Binary : AvroEncodeFormat() { - override fun createOutputStream( - output: OutputStream, - schema: Schema, - converter: (T) -> GenericRecord - ) = AvroBinaryOutputStream(output, converter, schema) - - } - /** - * Encodes the avro records as JSON text. The most verbose format, but easy for a human to read. - * - * This format does not include the schema. - * - * See https://avro.apache.org/docs/current/spec.html#json_encoding - */ - object Json : AvroEncodeFormat() { - override fun createOutputStream( - output: OutputStream, - schema: Schema, - converter: (T) -> GenericRecord - ) = AvroJsonOutputStream(output, converter, schema) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroFormat.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroFormat.kt deleted file mode 100644 index d88c3500..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroFormat.kt +++ /dev/null @@ -1,33 +0,0 @@ -@file:Suppress("DEPRECATION") - -package com.github.avrokotlin.avro4k.io - -@Deprecated("use AvroEncodeFormat and AvroDecodeFormat") -sealed class AvroFormat { - - /** - * Binary format without the header, the most compact format. - * - * See https://avro.apache.org/docs/current/spec.html#binary_encoding - */ - @Deprecated("use AvroEncodeFormat and AvroDecodeFormat") - object BinaryFormat : AvroFormat() - - /** - * Text format encoded as JSON. The most verbose format, but easy for a human to read. - * - * This format does not include the schema. Thus the write and read schema is needed for decoding. - * - * See https://avro.apache.org/docs/current/spec.html#json_encoding - */ - @Deprecated("use AvroEncodeFormat and AvroDecodeFormat") - object JsonFormat : AvroFormat() - - /** - * Binary format with a header that contains the full schema, this is the format usually used when writing Avro files. - * - * See https://avro.apache.org/docs/current/spec.html#Data+Serialization+and+Deserialization - */ - @Deprecated("use AvroEncodeFormat and AvroDecodeFormat") - object DataFormat : AvroFormat() -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroInputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroInputStream.kt deleted file mode 100644 index ba6f22a9..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroInputStream.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import kotlinx.serialization.SerializationException - -interface AvroInputStream : AutoCloseable { - - /** - * Returns a [Sequence] for the values of T in the stream. - * This function should not be invoked if using [next]. - */ - fun iterator(): Iterator = iterator { - var next = next() - while (next != null) { - yield(next) - next = next() - } - } - - /** - * Returns the next value of T in the stream. - * This function should not be invoked if using [iterator]. - */ - fun next(): T? - - fun nextOrThrow(): T = next() ?: throw SerializationException("No next entity found") -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroOutputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroOutputStream.kt deleted file mode 100644 index 372b03e4..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/AvroOutputStream.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -/** - * An [AvroOutputStream] will write instances of T to an underlying - * representation. - * - * There are three implementations of this stream - * - a Data stream, - * - a Binary stream - * - a Json stream - * - * See the methods on the companion object to create instances of each - * of these types of stream. - */ -interface AvroOutputStream : AutoCloseable { - - fun flush() - fun fSync() - fun write(t: T): AvroOutputStream - - fun write(ts: List): AvroOutputStream { - ts.forEach { write(it) } - return this - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/DefaultAvroOutputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/DefaultAvroOutputStream.kt deleted file mode 100644 index c468f01f..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/DefaultAvroOutputStream.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationStrategy -import org.apache.avro.Schema -import org.apache.avro.generic.GenericDatumWriter -import org.apache.avro.generic.GenericRecord -import org.apache.avro.io.BinaryEncoder -import org.apache.avro.io.Encoder -import org.apache.avro.io.EncoderFactory -import org.apache.avro.io.JsonEncoder -import java.io.OutputStream - -abstract class DefaultAvroOutputStream(private val output: OutputStream, - private val converter: (T) -> GenericRecord, - schema: Schema) : AvroOutputStream { - - private val datumWriter = GenericDatumWriter(schema) - - abstract val encoder: Encoder - - override fun close() { - flush() - output.close() - } - - override fun flush(): Unit = encoder.flush() - override fun fSync() {} - - override fun write(t: T): AvroOutputStream { - datumWriter.write(converter(t), encoder) - return this - } -} - -@OptIn(ExperimentalSerializationApi::class) -class AvroBinaryOutputStream(output: OutputStream, - converter: (T) -> GenericRecord, - schema: Schema) : DefaultAvroOutputStream(output, converter, schema) { - constructor(output: OutputStream, - serializer: SerializationStrategy, - schema: Schema, - avro: Avro = Avro.default) : this(output, { avro.toRecord(serializer, it) }, schema) - - override val encoder: BinaryEncoder = EncoderFactory.get().binaryEncoder(output, null) -} - -@OptIn(ExperimentalSerializationApi::class) -class AvroJsonOutputStream(output: OutputStream, - converter: (T) -> GenericRecord, - schema: Schema) : DefaultAvroOutputStream(output, converter, schema) { - constructor(output: OutputStream, - serializer: SerializationStrategy, - schema: Schema, - avro: Avro = Avro.default) : this(output, { avro.toRecord(serializer, it) }, schema) - - override val encoder: JsonEncoder = EncoderFactory.get().jsonEncoder(schema, output, true) -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/io/SchemalessAvroInputStream.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/io/SchemalessAvroInputStream.kt deleted file mode 100644 index 575f1c1c..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/io/SchemalessAvroInputStream.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import org.apache.avro.Schema -import org.apache.avro.generic.GenericDatumReader -import org.apache.avro.generic.GenericRecord -import org.apache.avro.io.BinaryDecoder -import org.apache.avro.io.Decoder -import org.apache.avro.io.DecoderFactory -import org.apache.avro.io.JsonDecoder -import java.io.EOFException -import java.io.InputStream - -/** - * Abstract implementation of schema-less formats. - * When using these formats, at least the writer schema must be supplied, as it cannot - * be loaded from the input source. - */ -abstract class SchemalessAvroInputStream(private val input: InputStream, - private val converter: (Any) -> T, - writerSchema: Schema, - readerSchema: Schema) : AvroInputStream { - - private val datumReader = GenericDatumReader(writerSchema, readerSchema) - - abstract val decoder: Decoder - - override fun close(): Unit = input.close() - - override fun next(): T? { - val record = try { - datumReader.read(null, decoder) - } catch (e: EOFException) { - null - } - return when (record) { - null -> null - else -> converter(record) - } - } -} - -class AvroJsonInputStream(input: InputStream, - converter: (Any) -> T, - writerSchema: Schema, - readerSchema: Schema) : - SchemalessAvroInputStream(input, converter, writerSchema, readerSchema) { - override val decoder: JsonDecoder = DecoderFactory.get().jsonDecoder(readerSchema, input) -} - - -/** - * An implementation of [AvroInputStream] that reads values of type T - * written as binary data. - * See https://avro.apache.org/docs/current/spec.html#binary_encoding - */ -class AvroBinaryInputStream(input: InputStream, - converter: (Any) -> T, - writerSchema: Schema, - readerSchema: Schema) : - SchemalessAvroInputStream(input, converter, writerSchema, readerSchema) { - override val decoder: BinaryDecoder = DecoderFactory.get().binaryDecoder(input, null) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/records.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/records.kt deleted file mode 100644 index bc692c76..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/records.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.avrokotlin.avro4k - -import org.apache.avro.Schema -import org.apache.avro.generic.GenericRecord -import org.apache.avro.specific.SpecificRecord - -/** - * An implementation of [org.apache.avro.generic.GenericContainer] that implements - * both [GenericRecord] and [SpecificRecord]. - */ -interface Record : GenericRecord, SpecificRecord - -data class ListRecord(private val s: Schema, - private val values: List) : Record { - - constructor(s: Schema, vararg values: Any?) : this(s, values.toList()) - - init { - require(schema.type == Schema.Type.RECORD) { "Cannot create a Record with a schema that is not of type Schema.Type.RECORD [was $s]" } - } - - override fun getSchema(): Schema = s - - override fun put(key: String, v: Any): Unit = - throw UnsupportedOperationException("This implementation of Record is immutable") - - override fun put(i: Int, v: Any): Unit = - throw UnsupportedOperationException("This implementation of Record is immutable") - - override fun get(key: String): Any? { - val index = schema.fields.indexOfFirst { it.name() == key } - if (index == -1) - throw RuntimeException("Field $key does not exist in this record (schema=$schema, values=$values)") - return get(index) - } - - override fun get(i: Int): Any? = values[i] -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt deleted file mode 100644 index d45115c3..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName - -@OptIn(ExperimentalSerializationApi::class) -abstract class AvroDescriptor(override val serialName: String, - override val kind: SerialKind -) : SerialDescriptor { - - constructor(type: KClass<*>, kind: SerialKind) : this(type.jvmName, kind) - - abstract fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema - - override val elementsCount: Int - get() = 0 - - private fun failNoChildDescriptors() : Nothing = throw SerializationException("AvroDescriptor has no child elements") - override fun isElementOptional(index: Int): Boolean = false - override fun getElementDescriptor(index: Int): SerialDescriptor = failNoChildDescriptors() - override fun getElementAnnotations(index: Int): List = emptyList() - override fun getElementIndex(name: String): Int = -1 - override fun getElementName(index: Int): String = failNoChildDescriptors() -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt deleted file mode 100644 index 8dfb3da3..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ClassSchemaFor.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.AnnotationExtractor -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.AvroJsonProp -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 -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.boolean -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.JsonProperties -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder - -@ExperimentalSerializationApi -class ClassSchemaFor( - private val descriptor: SerialDescriptor, - private val configuration: AvroConfiguration, - private val serializersModule: SerializersModule, - private val resolvedSchemas: MutableMap -) : SchemaFor { - - private val entityAnnotations = AnnotationExtractor(descriptor.annotations) - private val naming = RecordNaming(descriptor, DefaultNamingStrategy) - private val json by lazy { - Json{ - serializersModule = this@ClassSchemaFor.serializersModule - } - } - - override fun schema(): Schema { - // if the class is annotated with @AvroInline then we need to encode the single field - // of that class directly. - return when (entityAnnotations.valueType()) { - true -> valueTypeSchema() - false -> dataClassSchema() - } - } - - private fun valueTypeSchema(): Schema { - require(descriptor.elementsCount == 1) { "A value type must only have a single field" } - return buildField(0).schema() - } - - private fun dataClassSchema(): Schema { - // return schema if already resolved - recursive circuit breaker - resolvedSchemas[naming]?.let { return it } - - // create new schema without fields - val record = Schema.createRecord(naming.name, entityAnnotations.doc(), naming.namespace, false) - - // add schema without fields right now, so that fields could recursively use it - resolvedSchemas[naming] = record - - val fields = (0 until descriptor.elementsCount) - .map { index -> buildField(index) } - - record.fields = fields - entityAnnotations.aliases().forEach { record.addAlias(it) } - entityAnnotations.props().forEach { (k, v) -> record.addProp(k, v) } - entityAnnotations.jsonProps().forEach { (k, v) -> record.addProp(k, json.parseToJsonElement(v).convertToAvroDefault()) } - - return record - } - - 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) } - - return field - } - - private fun getDefaultValue( - annos: AnnotationExtractor, - schemaWithResolvedNamespace: Schema, - fieldDescriptor: 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 - ) -> annotationDefaultValue - - else -> json.parseToJsonElement(annotationDefaultValue).convertToAvroDefault() - } - } ?: if (configuration.implicitNulls && fieldDescriptor.isNullable) { - Schema.Field.NULL_DEFAULT_VALUE - } else null - - private fun JsonElement.convertToAvroDefault() : Any{ - return when(this){ - is JsonNull -> JsonProperties.NULL_VALUE - is JsonObject -> this.map { Pair(it.key,it.value.convertToAvroDefault()) }.toMap() - is JsonArray -> this.map { it.convertToAvroDefault() }.toList() - is JsonPrimitive -> when { - this.isString -> this.content - this.booleanOrNull != null -> this.boolean - else -> { - val number = this.content.toBigDecimal() - if(number.scale() <= 0){ - number.toBigInteger() - }else{ - number - } - } - } - } - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt deleted file mode 100644 index bd8916f6..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt +++ /dev/null @@ -1,227 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.AnnotationExtractor -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroConfiguration -import com.github.avrokotlin.avro4k.RecordNaming -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.serializerOrNull -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder - -interface SchemaFor { - - fun schema(): Schema - - companion object { - - /** - * Creates a [SchemaFor] that always returns the given constant schema. - */ - fun const(schema: Schema) = object : SchemaFor { - override fun schema() = schema - } - - val StringSchemaFor: SchemaFor = const(SchemaBuilder.builder().stringType()) - val LongSchemaFor: SchemaFor = const(SchemaBuilder.builder().longType()) - val IntSchemaFor: SchemaFor = const(SchemaBuilder.builder().intType()) - val ShortSchemaFor: SchemaFor = const(SchemaBuilder.builder().intType()) - val ByteSchemaFor: SchemaFor = const(SchemaBuilder.builder().intType()) - val DoubleSchemaFor: SchemaFor = const(SchemaBuilder.builder().doubleType()) - val FloatSchemaFor: SchemaFor = const(SchemaBuilder.builder().floatType()) - val BooleanSchemaFor: SchemaFor = const(SchemaBuilder.builder().booleanType()) - } -} - -@ExperimentalSerializationApi -class EnumSchemaFor( - private val descriptor: SerialDescriptor -) : SchemaFor { - override fun schema(): Schema { - val naming = RecordNaming(descriptor, DefaultNamingStrategy) - val entityAnnotations = AnnotationExtractor(descriptor.annotations) - val symbols = (0 until descriptor.elementsCount).map { descriptor.getElementName(it) } - - val defaultSymbol = entityAnnotations.enumDefault()?.let { enumDefault -> - descriptor.elementNames.firstOrNull { it == enumDefault } ?: error( - "Could not use: $enumDefault to resolve the enum class ${descriptor.serialName}" - ) - } - - val enumSchema = SchemaBuilder.enumeration(naming.name).doc(entityAnnotations.doc()) - .namespace(naming.namespace) - .defaultSymbol(defaultSymbol) - .symbols(*symbols.toTypedArray()) - - entityAnnotations.aliases().forEach { enumSchema.addAlias(it) } - - return enumSchema - } -} - -@ExperimentalSerializationApi -class PairSchemaFor(private val descriptor: SerialDescriptor, - private val configuration: AvroConfiguration, - private val serializersModule: SerializersModule, - private val resolvedSchemas: MutableMap -) : SchemaFor { - - override fun schema(): Schema { - val a = schemaFor( - serializersModule, - descriptor.getElementDescriptor(0), - descriptor.getElementAnnotations(0), - configuration, - resolvedSchemas - ) - val b = schemaFor( - serializersModule, - descriptor.getElementDescriptor(1), - descriptor.getElementAnnotations(1), - configuration, - resolvedSchemas - ) - return SchemaBuilder.unionOf() - .type(a.schema()) - .and() - .type(b.schema()) - .endUnion() - } -} - -@ExperimentalSerializationApi -class ListSchemaFor(private val descriptor: SerialDescriptor, - private val serializersModule: SerializersModule, - private val configuration: AvroConfiguration, - private val resolvedSchemas: MutableMap -) : SchemaFor { - - override fun schema(): Schema { - - val elementType = descriptor.getElementDescriptor(0) // don't use unwrapValueClass to prevent losing serial annotations - return when (descriptor.unwrapValueClass.getElementDescriptor(0).kind) { - PrimitiveKind.BYTE -> SchemaBuilder.builder().bytesType() - else -> { - val elementSchema = schemaFor(serializersModule, - elementType, - descriptor.getElementAnnotations(0), - configuration, - resolvedSchemas - ).schema() - return Schema.createArray(elementSchema) - } - } - } -} - -@ExperimentalSerializationApi -class MapSchemaFor(private val descriptor: SerialDescriptor, - private val serializersModule: SerializersModule, - private val configuration: AvroConfiguration, - private val resolvedSchemas: MutableMap -) : SchemaFor { - - override fun schema(): Schema { - val keyType = descriptor.getElementDescriptor(0).unwrapValueClass - when (keyType.kind) { - is PrimitiveKind.STRING -> { - val valueType = descriptor.getElementDescriptor(1) - val valueSchema = schemaFor( - serializersModule, - valueType, - descriptor.getElementAnnotations(1), - configuration, - resolvedSchemas - ).schema() - return Schema.createMap(valueSchema) - } - - else -> throw RuntimeException("Avro only supports STRING as the key type in a MAP") - } - } -} - -@ExperimentalSerializationApi -class NullableSchemaFor( - private val schemaFor: SchemaFor, - private val annotations: List, -) : SchemaFor { - - private val nullFirst by lazy { - //The default value can only be of the first type in the union definition. - //Therefore we have to check the default value in order to decide the order of types within the union. - //If no default is set, or if the default value is of type "null", nulls will be first. - val default = AnnotationExtractor(annotations).default() - default == null || default == Avro.NULL - } - - override fun schema(): Schema { - val elementSchema = schemaFor.schema() - val nullSchema = SchemaBuilder.builder().nullType() - return createSafeUnion(nullFirst, elementSchema, nullSchema) - } -} - -@OptIn(InternalSerializationApi::class) -@ExperimentalSerializationApi -fun schemaFor(serializersModule: SerializersModule, - descriptor: SerialDescriptor, - annos: List, - configuration: AvroConfiguration, - resolvedSchemas: MutableMap -): SchemaFor { - - val underlying = if (descriptor.javaClass.simpleName == "SerialDescriptorForNullable") { - val field = descriptor.javaClass.getDeclaredField("original") - field.isAccessible = true - field.get(descriptor) as SerialDescriptor - } else descriptor - - val schemaFor: SchemaFor = when (underlying) { - is AvroDescriptor -> SchemaFor.const(underlying.schema(annos, serializersModule, configuration.namingStrategy)) - else -> when (descriptor.unwrapValueClass.kind) { - PrimitiveKind.STRING -> SchemaFor.StringSchemaFor - PrimitiveKind.LONG -> SchemaFor.LongSchemaFor - PrimitiveKind.INT -> SchemaFor.IntSchemaFor - PrimitiveKind.SHORT -> SchemaFor.ShortSchemaFor - PrimitiveKind.BYTE -> SchemaFor.ByteSchemaFor - PrimitiveKind.DOUBLE -> SchemaFor.DoubleSchemaFor - PrimitiveKind.FLOAT -> SchemaFor.FloatSchemaFor - PrimitiveKind.BOOLEAN -> SchemaFor.BooleanSchemaFor - SerialKind.ENUM -> EnumSchemaFor(descriptor) - SerialKind.CONTEXTUAL -> schemaFor( - serializersModule, - requireNotNull( - serializersModule.getContextualDescriptor(descriptor.unwrapValueClass) - ?: descriptor.capturedKClass?.serializerOrNull()?.descriptor - ) { - "Contextual or default serializer not found for $descriptor " - }, - annos, - configuration, - resolvedSchemas - ) - - StructureKind.CLASS, StructureKind.OBJECT -> when (descriptor.serialName) { - "kotlin.Pair" -> PairSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - else -> ClassSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - } - - StructureKind.LIST -> ListSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) - StructureKind.MAP -> MapSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) - is PolymorphicKind -> UnionSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - else -> throw SerializationException("Unsupported type ${descriptor.serialName} of ${descriptor.kind}") - } - } - - return if (descriptor.isNullable) NullableSchemaFor(schemaFor, annos) else schemaFor -} - -// copy-paste from kotlinx serialization because it internal -@ExperimentalSerializationApi -internal val SerialDescriptor.unwrapValueClass: SerialDescriptor - get() = if (isInline) getElementDescriptor(0) else this \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt deleted file mode 100644 index ae51eb36..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaFor.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema - -@ExperimentalSerializationApi -class UnionSchemaFor( - private val descriptor: SerialDescriptor, - private val configuration: AvroConfiguration, - private val serializersModule: SerializersModule, - private val resolvedSchemas: MutableMap -) : SchemaFor { - override fun schema(): Schema { - val leafSerialDescriptors = - descriptor.possibleSerializationSubclasses(serializersModule).sortedBy { it.serialName } - return Schema.createUnion( - leafSerialDescriptors.map { - ClassSchemaFor(it, configuration, serializersModule, resolvedSchemas).schema() - } - ) - } -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt deleted file mode 100644 index fc6dc39c..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/namingStrategy.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -interface NamingStrategy { - fun to(name: String): String = name -} - -object DefaultNamingStrategy : NamingStrategy { - override fun to(name: String): String = name -} - -object PascalCaseNamingStrategy : NamingStrategy { - override fun to(name: String): String = name.take(1).uppercase() + name.drop(1) -} - -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()) - }.toString() -} diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/schemas.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/schemas.kt deleted file mode 100644 index 4239291a..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/schemas.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import org.apache.avro.Schema - -// creates a union schema type, with nested unions extracted, and duplicate nulls stripped -// union schemas can't contain other union schemas as a direct -// child, so whenever we create a union, we need to check if our -// children are unions and flatten -fun createSafeUnion(nullFirst : Boolean,vararg schemas: Schema): Schema { - val flattened = schemas.flatMap { schema -> runCatching { schema.types }.getOrElse { listOf(schema) } } - val (nulls, rest) = flattened.partition { it.type == Schema.Type.NULL } - return Schema.createUnion(if(nullFirst) nulls + rest else rest + nulls) -} - -fun Schema.extractNonNull(): Schema = when (this.type) { - Schema.Type.UNION -> this.types.filter { it.type != Schema.Type.NULL }.let { if(it.size > 1) Schema.createUnion(it) else it[0] } - else -> this -} - -/** - * Takes an Avro schema, and overrides the namespace of that schema with the given namespace. - */ -/** - * Overrides the namespace of a [Schema] with the given namespace. - */ -fun Schema.overrideNamespace(namespace: String): Schema { - return when (type) { - Schema.Type.RECORD -> { - val fields = fields.map { field -> - Schema.Field( - field.name(), - field.schema().overrideNamespace(namespace), - field.doc(), - field.defaultVal(), - field.order() - ) - } - val copy = Schema.createRecord(name, doc, namespace, isError, fields) - aliases.forEach { copy.addAlias(it) } - this.objectProps.forEach { copy.addProp(it.key, it.value) } - copy - } - Schema.Type.UNION -> Schema.createUnion(types.map { it.overrideNamespace(namespace) }) - Schema.Type.ENUM -> Schema.createEnum(name, doc, namespace, enumSymbols, enumDefault) - Schema.Type.FIXED -> Schema.createFixed(name, doc, namespace, fixedSize) - Schema.Type.MAP -> Schema.createMap(valueType.overrideNamespace(namespace)) - Schema.Type.ARRAY -> Schema.createArray(elementType.overrideNamespace(namespace)) - else -> this - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt new file mode 100644 index 00000000..21c8e433 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt @@ -0,0 +1,203 @@ +package com.github.avrokotlin.avro4k.serializer + +import com.github.avrokotlin.avro4k.AnyValueDecoder +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.decodeResolvingAny +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.apache.avro.LogicalType +import org.apache.avro.Schema +import org.intellij.lang.annotations.Language +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Represents a duration in months, days and milliseconds. + * + * This is the exact representation of the Avro `duration` logical type. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +@Serializable(with = AvroDurationSerializer::class) +@ExperimentalSerializationApi +public data class AvroDuration( + val months: UInt, + val days: UInt, + val millis: UInt, +) { + override fun toString(): String { + if (months == 0u && days == 0u && millis == 0u) { + return "PT0S" + } + return buildString { + append("P") + if (months != 0u) { + append("${months}M") + } + if (days != 0u) { + append("${days}D") + } + if (millis != 0u) { + append("T") + append(millis / 1000u) + val millisPart = millis % 1000u + if (millisPart != 0u) { + append('.') + append(millisPart) + } + append("S") + } + } + } + + public companion object { + @JvmStatic + @Language("RegExp") + private fun part( + name: Char, + @Language("RegExp") digitsRegex: String = "", + ): String { + val digitsPart = if (digitsRegex.isNotEmpty()) "(?:[.,]($digitsRegex))?" else "" + return "(?:\\+?([0-9]+)$digitsPart$name)?" + } + + @JvmStatic + private val PATTERN: Regex = + buildString { + append("P") + append(part('Y')) + append(part('M')) + append(part('W')) + append(part('D')) + append("(?:T") + append(part('H')) + append(part('M')) + append(part('S', digitsRegex = "[0-9]{0,3}")) + append(")?") + }.toRegex(RegexOption.IGNORE_CASE) + + @JvmStatic + @Throws(AvroDurationParseException::class) + public fun tryParse(value: String): AvroDuration? { + val match = PATTERN.matchEntire(value) ?: return null + val (years, months, weeks, days, hours, minutes, seconds, millis) = match.destructured + return AvroDuration( + months = years * 12u + months.toUIntOrZero(), + days = weeks * 7u + days.toUIntOrZero(), + millis = hours * 60u * 60u * 1000u + minutes * 60u * 1000u + seconds * 1000u + millis.toUIntOrZero() + ) + } + + private operator fun String.times(other: UInt): UInt { + return toUIntOrNull()?.times(other) ?: 0u + } + + private fun String.toUIntOrZero(): UInt { + return toUIntOrNull() ?: 0u + } + + @JvmStatic + public fun parse(value: String): AvroDuration { + return tryParse(value) ?: throw AvroDurationParseException(value) + } + } +} + +@ExperimentalSerializationApi +public class AvroDurationParseException(value: String) : SerializationException("Unable to parse duration: $value") + +internal object AvroDurationSerializer : AvroSerializer(AvroDuration::class.qualifiedName!!) { + private const val LOGICAL_TYPE_NAME = "duration" + private const val DURATION_BYTES = 12 + internal val DURATION_SCHEMA = + Schema.createFixed("time.Duration", "A 12-byte byte array encoding a duration in months, days and milliseconds.", null, DURATION_BYTES).also { + LogicalType(LOGICAL_TYPE_NAME).addToSchema(it) + } + + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: AvroDuration, + ) { + with(encoder) { + encodeResolving({ BadEncodedValueError(value, currentWriterSchema, Schema.Type.FIXED, Schema.Type.STRING) }) { + when (it.type) { + Schema.Type.FIXED -> + if (it.logicalType?.name == LOGICAL_TYPE_NAME && it.fixedSize == DURATION_BYTES) { + { encodeFixed(encodeDuration(value)) } + } else { + null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + } + + override fun deserializeAvro(decoder: AvroDecoder): AvroDuration { + return with(decoder) { + decodeResolvingAny({ UnexpectedDecodeSchemaError(AvroDuration::class.qualifiedName!!, Schema.Type.FIXED, Schema.Type.STRING) }) { + when (it.type) { + Schema.Type.FIXED -> { + if (it.logicalType?.name == LOGICAL_TYPE_NAME && it.fixedSize == DURATION_BYTES) { + AnyValueDecoder { decodeDuration(decodeFixed().bytes()) } + } else { + null + } + } + + Schema.Type.STRING -> { + AnyValueDecoder { AvroDuration.parse(decodeString()) } + } + + else -> throw SerializationException("Expected duration fixed or string type") + } + } + } + } + + private fun encodeDuration(value: AvroDuration): ByteArray { + val buffer = ByteBuffer.allocate(DURATION_BYTES).order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(value.months.toInt()) + buffer.putInt(value.days.toInt()) + buffer.putInt(value.millis.toInt()) + return buffer.array() + } + + private fun decodeDuration(bytes: ByteArray): AvroDuration { + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + return AvroDuration( + months = buffer.getInt().toUInt(), + days = buffer.getInt().toUInt(), + millis = buffer.getInt().toUInt() + ) + } + + override fun serializeGeneric( + encoder: Encoder, + value: AvroDuration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): AvroDuration { + return AvroDuration.parse(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt index 454385bc..ad60eb5e 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt @@ -1,39 +1,168 @@ package com.github.avrokotlin.avro4k.serializer -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.decoder.FieldDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.encoder.FieldEncoder -import com.github.avrokotlin.avro4k.schema.extractNonNull +import com.github.avrokotlin.avro4k.AvroConfiguration +import com.github.avrokotlin.avro4k.AvroDecimal +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.AvroStringable +import com.github.avrokotlin.avro4k.internal.findElementAnnotation +import com.github.avrokotlin.avro4k.internal.namespace +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import org.apache.avro.Schema -abstract class AvroSerializer : KSerializer { - - final override fun serialize(encoder: Encoder, value: T) { - val schema = (encoder as FieldEncoder).fieldSchema() - // we may be encoding a nullable schema - val subschema = when (schema.type) { - Schema.Type.UNION -> schema.extractNonNull() - else -> schema - } - encodeAvroValue(subschema, encoder, value) - } - - final override fun deserialize(decoder: Decoder): T { - val schema = (decoder as FieldDecoder).fieldSchema() -// // we may be coming from a nullable schema aka a union -// val subschema = when (schema.type) { -// Schema.Type.UNION -> schema.extractNonNull() -// else -> schema -// } - return decodeAvroValue(schema, decoder) - } - - abstract fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: T) - - abstract fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): T +/** + * Base class for custom Avro serializers. It also provides a way to define custom Avro schema. + * + * Use it at your own risk, as it's directly bypassing the internal checks, so you can have runtime errors. + * + * Don't forget to implement [serializeGeneric] and [deserializeGeneric] if you want to use the serializer outside the Avro serialization, like with json format. + */ +public abstract class AvroSerializer( + descriptorName: String, +) : KSerializer, AvroSchemaSupplier { + @Suppress("LeakingThis") + @OptIn(InternalSerializationApi::class) + final override val descriptor: SerialDescriptor = + SerialDescriptorWithAvroSchemaDelegate(buildSerialDescriptor(descriptorName, SerialKind.CONTEXTUAL), this) + + final override fun serialize( + encoder: Encoder, + value: T, + ) { + if (encoder is AvroEncoder) { + serializeAvro(encoder, value) + return + } + serializeGeneric(encoder, value) + } + + final override fun deserialize(decoder: Decoder): T { + if (decoder is AvroDecoder) { + return deserializeAvro(decoder) + } + return deserializeGeneric(decoder) + } + + /** + * This method is called when the serializer is used outside Avro serialization. + * By default, it throws an exception. + * + * Implement it to provide a generic serialization logic with the standard [Encoder]. + */ + public open fun serializeGeneric( + encoder: Encoder, + value: T, + ) { + throw UnsupportedOperationException("The serializer ${this::class.qualifiedName} is not usable outside of Avro serialization.") + } + + /** + * Serialize the value using an Avro encoder. It is highly recommended to use `encoder.encodeResolving` methods. See [AvroEncoder] for more details. + */ + public abstract fun serializeAvro( + encoder: AvroEncoder, + value: T, + ) + + /** + * This method is called when the serializer is used outside Avro serialization. + * By default, it throws an exception. + * + * Implement it to provide a generic deserialization logic with the standard [Decoder]. + */ + public open fun deserializeGeneric(decoder: Decoder): T { + throw UnsupportedOperationException("The serializer ${this::class.qualifiedName} is not usable outside of Avro serialization.") + } + + /** + * Deserialize the value from an Avro decoder. It is highly recommended to use `decoder.decodeResolvingXx` methods. See [AvroDecoder] for more details. + */ + public abstract fun deserializeAvro(decoder: AvroDecoder): T +} + +@ExperimentalSerializationApi +public interface SchemaSupplierContext { + public val configuration: AvroConfiguration + + /** + * Corresponds to the elements-tree, always starting from the data class property. + * + * The first element is the data class property, and the next elements are the inlined elements when the property type is a value class. + */ + public val inlinedElements: List +} + +/** + * Search for the first annotation of type [T] in the given [ElementLocation]. + */ +@ExperimentalSerializationApi +public inline fun ElementLocation.findAnnotation(): T? { + return descriptor.findElementAnnotation(elementIndex) +} + +/** + * Shorthand for [findAnnotation] with [AvroDecimal] as it is a built-in annotation. + */ +@ExperimentalSerializationApi +public val ElementLocation.decimal: AvroDecimal? + get() = findAnnotation() + +/** + * Shorthand for [findAnnotation] with [AvroStringable] as it is a built-in annotation. + */ +@ExperimentalSerializationApi +public val ElementLocation.stringable: AvroStringable? + get() = findAnnotation() + +/** + * Creates a string schema from the [AvroStringable] annotation. + */ +@ExperimentalSerializationApi +public fun AvroStringable.createSchema(): Schema = Schema.create(Schema.Type.STRING) + +/** + * Shorthand for [findAnnotation] with [AvroFixed] as it is a built-in annotation. + */ +@ExperimentalSerializationApi +public val ElementLocation.fixed: AvroFixed? + get() = findAnnotation() + +/** + * Creates a fixed schema from the [AvroFixed] annotation. + */ +@ExperimentalSerializationApi +public fun AvroFixed.createSchema(elementLocation: ElementLocation): Schema = + Schema.createFixed(elementLocation.descriptor.getElementName(elementLocation.elementIndex), null, elementLocation.descriptor.namespace, size) + +@ExperimentalSerializationApi +public data class ElementLocation + @PublishedApi + internal constructor( + val descriptor: SerialDescriptor, + val elementIndex: Int, + ) + +internal fun interface AvroSchemaSupplier { + fun getSchema(context: SchemaSupplierContext): Schema } +internal class SerialDescriptorWithAvroSchemaDelegate( + private val descriptor: SerialDescriptor, + private val schemaSupplier: AvroSchemaSupplier, +) : SerialDescriptor by descriptor, AvroSchemaSupplier { + override fun getSchema(context: SchemaSupplierContext): Schema { + return schemaSupplier.getSchema(context) + } + + override fun toString(): String { + return "${descriptor.serialName}()" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt deleted file mode 100644 index 40f21ca8..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.AnnotationExtractor -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Conversions -import org.apache.avro.LogicalTypes -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericFixed -import org.apache.avro.util.Utf8 -import java.math.BigDecimal -import java.math.RoundingMode -import java.nio.ByteBuffer -import kotlin.reflect.jvm.jvmName - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = BigDecimal::class) -class BigDecimalSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(BigDecimal::class.jvmName, PrimitiveKind.BYTE) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().bytesType() - val (scale, precision) = AnnotationExtractor(annos).scalePrecision() ?: (2 to 8) - return LogicalTypes.decimal(precision, scale).addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: BigDecimal) { - - // we support encoding big decimals in three ways - fixed, bytes or as a String, depending on the schema passed in - // the scale and precision should come from the schema and the rounding mode from the implicit - - val converter = Conversions.DecimalConversion() - val rm = RoundingMode.UNNECESSARY - - return when (schema.type) { - Schema.Type.STRING -> encoder.encodeString(obj.toString()) - Schema.Type.BYTES -> { - when (val logical = schema.logicalType) { - is LogicalTypes.Decimal -> encoder.encodeByteArray(converter.toBytes(obj.setScale(logical.scale, rm), - schema, - logical)) - else -> throw SerializationException("Cannot encode BigDecimal to FIXED for logical type $logical") - } - } - Schema.Type.FIXED -> { - when (val logical = schema.logicalType) { - is LogicalTypes.Decimal -> encoder.encodeFixed(converter.toFixed(obj.setScale(logical.scale, rm), - schema, - logical)) - else -> throw SerializationException("Cannot encode BigDecimal to FIXED for logical type $logical") - } - - } - else -> throw SerializationException("Cannot encode BigDecimal as ${schema.type}") - } - } - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): BigDecimal { - - fun logical() = when (val l = schema.logicalType) { - is LogicalTypes.Decimal -> l - else -> throw SerializationException("Cannot decode to BigDecimal when field schema [$schema] does not define Decimal logical type [$l]") - } - - return when (val v = decoder.decodeAny()) { - is Utf8 -> BigDecimal(decoder.decodeString()) - is ByteArray -> Conversions.DecimalConversion().fromBytes(ByteBuffer.wrap(v), schema, logical()) - is ByteBuffer -> Conversions.DecimalConversion().fromBytes(v, schema, logical()) - is GenericFixed -> Conversions.DecimalConversion().fromFixed(v, schema, logical()) - else -> throw SerializationException("Unsupported BigDecimal type [$v]") - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt deleted file mode 100644 index 19db3f1b..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import java.math.BigInteger - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = BigInteger::class) -class BigIntegerSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(BigInteger::class, PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema = Schema.create(Schema.Type.STRING) - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: BigInteger) = - encoder.encodeString(obj.toString()) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): BigInteger { - return BigInteger(decoder.decodeString()) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt new file mode 100644 index 00000000..5eea0d1d --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt @@ -0,0 +1,318 @@ +package com.github.avrokotlin.avro4k.serializer + +import com.github.avrokotlin.avro4k.AnyValueDecoder +import com.github.avrokotlin.avro4k.AvroDecimal +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.decodeResolvingAny +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import com.github.avrokotlin.avro4k.internal.copy +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import org.apache.avro.Conversions +import org.apache.avro.LogicalType +import org.apache.avro.LogicalTypes +import org.apache.avro.Schema +import java.math.BigDecimal +import java.math.BigInteger +import java.net.URL +import java.nio.ByteBuffer +import java.util.UUID + +public val JavaStdLibSerializersModule: SerializersModule = + SerializersModule { + contextual(URLSerializer) + contextual(UUIDSerializer) + contextual(BigIntegerSerializer) + contextual(BigDecimalSerializer) + } + +public object URLSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(URL::class.qualifiedName!!, PrimitiveKind.STRING) + + override fun serialize( + encoder: Encoder, + value: URL, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString()) +} + +/** + * Serializes an [UUID] as a string logical type of `uuid`. + * + * Note: it does not check if the schema logical type name is `uuid` as it does not make any conversion. + */ +public object UUIDSerializer : AvroSerializer(UUID::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.STRING).copy(logicalType = LogicalType("uuid")) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: UUID, + ) { + serializeGeneric(encoder, value) + } + + override fun deserializeAvro(decoder: AvroDecoder): UUID { + return deserializeGeneric(decoder) + } + + override fun serializeGeneric( + encoder: Encoder, + value: UUID, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } +} + +public object BigIntegerSerializer : AvroSerializer(BigInteger::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.STRING) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: BigInteger, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError( + value, + encoder.currentWriterSchema, + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + } + }) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + Schema.Type.INT -> { + { encoder.encodeInt(value.intValueExact()) } + } + + Schema.Type.LONG -> { + { encoder.encodeLong(value.longValueExact()) } + } + + Schema.Type.FLOAT -> { + { encoder.encodeFloat(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { encoder.encodeDouble(value.toDouble()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: BigInteger, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): BigInteger { + with(decoder) { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "BigInteger", + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + }) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + AnyValueDecoder { decoder.decodeString().toBigInteger() } + } + + Schema.Type.INT -> { + AnyValueDecoder { decoder.decodeInt().toBigInteger() } + } + + Schema.Type.LONG -> { + AnyValueDecoder { decoder.decodeLong().toBigInteger() } + } + + Schema.Type.FLOAT -> { + AnyValueDecoder { decoder.decodeFloat().toBigDecimal().toBigIntegerExact() } + } + + Schema.Type.DOUBLE -> { + AnyValueDecoder { decoder.decodeDouble().toBigDecimal().toBigIntegerExact() } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): BigInteger { + return decoder.decodeString().toBigInteger() + } +} + +public object BigDecimalSerializer : AvroSerializer(BigDecimal::class.qualifiedName!!) { + private val converter = Conversions.DecimalConversion() + + override fun getSchema(context: SchemaSupplierContext): Schema { + val logicalType = context.inlinedElements.firstNotNullOfOrNull { it.decimal }?.logicalType + + fun nonNullLogicalType(): LogicalTypes.Decimal { + if (logicalType == null) { + throw AvroSchemaGenerationException("BigDecimal requires @${AvroDecimal::class.qualifiedName} to works with 'fixed' or 'bytes' schema types.") + } + return logicalType + } + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() ?: it.fixed?.createSchema(it)?.copy(logicalType = nonNullLogicalType()) + } ?: Schema.create(Schema.Type.BYTES).copy(logicalType = nonNullLogicalType()) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: BigDecimal, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError( + value, + encoder.currentWriterSchema, + Schema.Type.BYTES, + Schema.Type.FIXED, + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + } + }) { schema -> + when (schema.type) { + Schema.Type.BYTES -> + when (schema.logicalType) { + is LogicalTypes.Decimal -> { + { encoder.encodeBytes(converter.toBytes(value, schema, schema.logicalType)) } + } + + else -> null + } + + Schema.Type.FIXED -> + when (schema.logicalType) { + is LogicalTypes.Decimal -> { + { encoder.encodeFixed(converter.toFixed(value, schema, schema.logicalType)) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + Schema.Type.INT -> { + { encoder.encodeInt(value.intValueExact()) } + } + + Schema.Type.LONG -> { + { encoder.encodeLong(value.longValueExact()) } + } + + Schema.Type.FLOAT -> { + { encoder.encodeFloat(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { encoder.encodeDouble(value.toDouble()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: BigDecimal, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): BigDecimal { + with(decoder) { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "BigDecimal", + Schema.Type.STRING, + Schema.Type.BYTES, + Schema.Type.FIXED + ) + }) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + AnyValueDecoder { decoder.decodeString().toBigDecimal() } + } + + Schema.Type.BYTES -> + when (schema.logicalType) { + is LogicalTypes.Decimal -> { + AnyValueDecoder { converter.fromBytes(ByteBuffer.wrap(decoder.decodeBytes()), schema, schema.logicalType) } + } + + else -> null + } + + Schema.Type.FIXED -> + when (schema.logicalType) { + is LogicalTypes.Decimal -> { + AnyValueDecoder { converter.fromFixed(decoder.decodeFixed(), schema, schema.logicalType) } + } + + else -> null + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): BigDecimal { + return decoder.decodeString().toBigDecimal() + } + + private val AvroDecimal.logicalType: LogicalTypes.Decimal + get() { + return LogicalTypes.decimal(precision, scale) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt new file mode 100644 index 00000000..9d7d711e --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt @@ -0,0 +1,595 @@ +package com.github.avrokotlin.avro4k.serializer + +import com.github.avrokotlin.avro4k.AnyValueDecoder +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.decodeResolvingAny +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import com.github.avrokotlin.avro4k.internal.copy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import org.apache.avro.LogicalType +import org.apache.avro.Schema +import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.Period +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit + +public val JavaTimeSerializersModule: SerializersModule = + SerializersModule { + contextual(LocalDateSerializer) + contextual(LocalTimeSerializer) + contextual(LocalDateTimeSerializer) + contextual(InstantSerializer) + contextual(JavaDurationSerializer) + contextual(JavaPeriodSerializer) + } + +private const val LOGICAL_TYPE_NAME_DATE = "date" +private const val LOGICAL_TYPE_NAME_TIME_MILLIS = "time-millis" +private const val LOGICAL_TYPE_NAME_TIME_MICROS = "time-micros" +private const val LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS = "timestamp-millis" +private const val LOGICAL_TYPE_NAME_TIMESTAMP_MICROS = "timestamp-micros" + +public object LocalDateSerializer : AvroSerializer(LocalDate::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: Schema.create(Schema.Type.INT).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_DATE)) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: LocalDate, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError(value, encoder.currentWriterSchema, Schema.Type.INT, Schema.Type.LONG) + } + }) { schema -> + when (schema.type) { + Schema.Type.INT -> + when (schema.logicalType?.name) { + LOGICAL_TYPE_NAME_DATE, null -> { + { encoder.encodeInt(value.toEpochDay().toInt()) } + } + + else -> null + } + + Schema.Type.LONG -> + when (schema.logicalType) { + // Date is not compatible with LONG, so we require a null logical type to encode the timestamp + null -> { + { encoder.encodeLong(value.toEpochDay()) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: LocalDate, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): LocalDate { + with(decoder) { + return decoder.decodeResolvingAny({ + UnexpectedDecodeSchemaError("LocalDate", Schema.Type.INT, Schema.Type.LONG) + }) { + when (it.type) { + Schema.Type.INT -> { + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_DATE, null -> { + AnyValueDecoder { LocalDate.ofEpochDay(decoder.decodeInt().toLong()) } + } + + else -> null + } + } + + Schema.Type.LONG -> { + when (it.logicalType?.name) { + null -> { + AnyValueDecoder { LocalDate.ofEpochDay(decoder.decodeLong()) } + } + + else -> null + } + } + + Schema.Type.STRING -> { + AnyValueDecoder { LocalDate.parse(decoder.decodeString()) } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): LocalDate { + return LocalDate.parse(decoder.decodeString()) + } +} + +private const val NANOS_PER_MILLISECOND = 1_000_000L +private const val NANOS_PER_MICROSECOND = 1_000L + +public object LocalTimeSerializer : AvroSerializer(LocalTime::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: Schema.create(Schema.Type.INT).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIME_MILLIS)) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: LocalTime, + ) { + with(encoder) { + encodeResolving({ + BadEncodedValueError(value, encoder.currentWriterSchema, Schema.Type.INT, Schema.Type.LONG, Schema.Type.STRING) + }) { schema -> + when (schema.type) { + Schema.Type.INT -> + when (schema.logicalType?.name) { + LOGICAL_TYPE_NAME_TIME_MILLIS, null -> { + { encoder.encodeInt(value.toMillisOfDay()) } + } + + else -> null + } + + Schema.Type.LONG -> + when (schema.logicalType?.name) { + // TimeMillis is not compatible with LONG, so we require a null logical type to encode the timestamp + null -> { + { encoder.encodeLong(value.toMillisOfDay().toLong()) } + } + + LOGICAL_TYPE_NAME_TIME_MICROS -> { + { encoder.encodeLong(value.toMicroOfDay()) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: LocalTime, + ) { + encoder.encodeString(value.toString()) + } + + private fun LocalTime.toMillisOfDay() = (toNanoOfDay() / NANOS_PER_MILLISECOND).toInt() + + private fun LocalTime.toMicroOfDay() = toNanoOfDay() / NANOS_PER_MICROSECOND + + override fun deserializeAvro(decoder: AvroDecoder): LocalTime { + with(decoder) { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "LocalTime", + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.STRING + ) + }) { + when (it.type) { + Schema.Type.INT -> { + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIME_MILLIS, null -> { + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeInt() * NANOS_PER_MILLISECOND) } + } + + else -> null + } + } + + Schema.Type.LONG -> { + when (it.logicalType?.name) { + null -> { + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * NANOS_PER_MILLISECOND) } + } + + LOGICAL_TYPE_NAME_TIME_MICROS -> { + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * NANOS_PER_MICROSECOND) } + } + + else -> null + } + } + + Schema.Type.STRING -> { + AnyValueDecoder { LocalTime.parse(decoder.decodeString()) } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): LocalTime { + return LocalTime.parse(decoder.decodeString()) + } +} + +public object LocalDateTimeSerializer : AvroSerializer(LocalDateTime::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS)) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: LocalDateTime, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError(value, encoder.currentWriterSchema, Schema.Type.LONG, Schema.Type.STRING) + } + }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { + { encoder.encodeLong(value.toInstant(ZoneOffset.UTC).toEpochMilli()) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { + { encoder.encodeLong(value.toInstant(ZoneOffset.UTC).toEpochMicros()) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: LocalDateTime, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): LocalDateTime { + return with(decoder) { + decodeResolvingAny({ UnexpectedDecodeSchemaError("Instant", Schema.Type.LONG) }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { + AnyValueDecoder { LocalDateTime.ofInstant(Instant.ofEpochMilli(decoder.decodeLong()), ZoneOffset.UTC) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { + AnyValueDecoder { LocalDateTime.ofInstant(Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS), ZoneOffset.UTC) } + } + + else -> null + } + + Schema.Type.STRING -> { + AnyValueDecoder { LocalDateTime.parse(decoder.decodeString()) } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): LocalDateTime { + return LocalDateTime.parse(decoder.decodeString()) + } +} + +public object InstantSerializer : AvroSerializer(Instant::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS)) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Instant, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError(value, encoder.currentWriterSchema, Schema.Type.LONG, Schema.Type.STRING) + } + }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { + { encoder.encodeLong(value.toEpochMilli()) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { + { encoder.encodeLong(value.toEpochMicros()) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: Instant, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Instant = + with(decoder) { + decodeResolvingAny({ UnexpectedDecodeSchemaError("Instant", Schema.Type.LONG) }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { + AnyValueDecoder { Instant.ofEpochMilli(decoder.decodeLong()) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { + AnyValueDecoder { Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS) } + } + + else -> null + } + + Schema.Type.STRING -> { + AnyValueDecoder { Instant.parse(decoder.decodeString()) } + } + + else -> null + } + } + } + + override fun deserializeGeneric(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +} + +public object InstantToMicroSerializer : AvroSerializer(Instant::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MICROS)) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Instant, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError(value, encoder.currentWriterSchema, Schema.Type.LONG, Schema.Type.STRING) + } + }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS, null -> { + { encoder.encodeLong(value.toEpochMicros()) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS -> { + { encoder.encodeLong(value.toEpochMilli()) } + } + + else -> null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: Instant, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Instant { + with(decoder) { + return decodeResolvingAny({ UnexpectedDecodeSchemaError("Instant", Schema.Type.LONG, Schema.Type.STRING) }) { + when (it.type) { + Schema.Type.LONG -> + when (it.logicalType?.name) { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS, null -> { + AnyValueDecoder { Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS) } + } + + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS -> { + AnyValueDecoder { Instant.ofEpochMilli(decoder.decodeLong()) } + } + + else -> null + } + + Schema.Type.STRING -> { + AnyValueDecoder { Instant.parse(decoder.decodeString()) } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +} + +private fun Instant.toEpochMicros() = ChronoUnit.MICROS.between(Instant.EPOCH, this) + +/** + * Serializes an [Duration] as a fixed logical type of `duration`. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +public object JavaDurationSerializer : AvroSerializer(Duration::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Duration, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Duration { + return AvroDurationSerializer.deserializeAvro(decoder).toJavaDuration() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Duration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Duration { + return Duration.parse(decoder.decodeString()) + } + + private fun AvroDuration.toJavaDuration(): Duration { + if (months != 0u) { + throw SerializationException("java.time.Duration cannot contains months") + } + return Duration.ofMillis(days.toLong() * MILLIS_PER_DAY + millis.toLong()) + } + + private fun Duration.toAvroDuration(): AvroDuration { + if (isNegative) { + throw SerializationException("${Duration::class.qualifiedName} cannot be converted to ${AvroDuration::class.qualifiedName} as it cannot be negative") + } + val millis = this.toMillis() + return AvroDuration( + months = 0u, + days = (millis / MILLIS_PER_DAY).toUInt(), + millis = (millis % MILLIS_PER_DAY).toUInt() + ) + } +} + +/** + * Serializes an [Period] as a fixed logical type of `duration`. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +public object JavaPeriodSerializer : AvroSerializer(Period::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.inlinedElements.firstNotNullOfOrNull { + it.stringable?.createSchema() + } ?: AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Period, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Period { + return AvroDurationSerializer.deserializeAvro(decoder).toJavaPeriod() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Period, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Period { + return Period.parse(decoder.decodeString()) + } + + private fun AvroDuration.toJavaPeriod(): Period { + val years = (months / 12u).toInt() + val months = (months % 12u).toInt() + val days = days.toInt() + (millis.toLong() / MILLIS_PER_DAY).toInt() + // Ignore the remaining millis as Period does not support less than a day + + return Period.of(years, months, days).also { + if (it.isNegative) { + throw SerializationException("java.time.Period overflow from $this") + } + } + } + + private fun Period.toAvroDuration(): AvroDuration { + return AvroDuration( + months = (years * DAYS_PER_YEAR + months).toUInt(), + days = days.toUInt(), + millis = 0u + ) + } +} + +private const val MILLIS_PER_DAY = 1000 * 60 * 60 * 24 +private const val DAYS_PER_YEAR = 12 \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt deleted file mode 100644 index 21d77a25..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.util.Utf8 -import java.net.URL -import kotlin.reflect.jvm.jvmName - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = URL::class) -class URLSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(URL::class.jvmName, PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema = SchemaBuilder.builder().stringType() - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: URL) { - encoder.encodeString(obj.toString()) - } - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): URL { - return when (val v = decoder.decodeAny()) { - is Utf8 -> URL(v.toString()) - is String -> URL(v) - else -> throw SerializationException("Unsupported URL type [$v : ${v?.javaClass?.name}]") - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt deleted file mode 100644 index bf9856b7..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.LogicalTypes -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import java.util.* - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = UUID::class) -class UUIDSerializer : AvroSerializer() { - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: UUID) = encoder.encodeString(obj.toString()) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): UUID = - UUID.fromString(decoder.decodeString()) - - override val descriptor: SerialDescriptor = object : AvroDescriptor("uuid", PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().stringType() - return LogicalTypes.uuid().addToSchema(schema) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt deleted file mode 100644 index 0447df3a..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt +++ /dev/null @@ -1,127 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.LogicalTypes -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import java.sql.Timestamp -import java.time.* -import java.time.temporal.ChronoUnit -import kotlin.reflect.jvm.jvmName - -@Serializer(forClass = LocalDate::class) -class LocalDateSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(LocalDate::class.jvmName, PrimitiveKind.INT) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().intType() - return LogicalTypes.date().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalDate) = - encoder.encodeInt(obj.toEpochDay().toInt()) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): LocalDate = LocalDate.ofEpochDay(decoder.decodeLong()) -} - -@Serializer(forClass = LocalTime::class) -class LocalTimeSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(LocalTime::class.jvmName, PrimitiveKind.INT) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().intType() - return LogicalTypes.timeMillis().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalTime) = - encoder.encodeInt(obj.toSecondOfDay() * 1000 + obj.nano / 1000) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): LocalTime { - // avro stores times as either millis since midnight or micros since midnight - return when (schema.logicalType) { - is LogicalTypes.TimeMicros -> LocalTime.ofNanoOfDay(decoder.decodeInt() * 1000L) - is LogicalTypes.TimeMillis -> LocalTime.ofNanoOfDay(decoder.decodeInt() * 1000000L) - else -> throw SerializationException("Unsupported logical type for LocalTime [${schema.logicalType}]") - } - } -} -@Serializer(forClass = LocalDateTime::class) -class LocalDateTimeSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = - object : AvroDescriptor(LocalDateTime::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalDateTime) = - InstantSerializer().encodeAvroValue(schema, encoder, obj.toInstant(ZoneOffset.UTC)) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): LocalDateTime = - LocalDateTime.ofInstant(Instant.ofEpochMilli(decoder.decodeLong()), ZoneOffset.UTC) -} -@Serializer(forClass = Timestamp::class) -class TimestampSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(Timestamp::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Timestamp) = - InstantSerializer().encodeAvroValue(schema, encoder, obj.toInstant()) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): Timestamp = Timestamp(decoder.decodeLong()) -} -@Serializer(forClass = Instant::class) -class InstantSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(Instant::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Instant) = - encoder.encodeLong(obj.toEpochMilli()) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): Instant = Instant.ofEpochMilli(decoder.decodeLong()) -} - -@Serializer(forClass = Instant::class) -class InstantToMicroSerializer : AvroSerializer() { - - override val descriptor: SerialDescriptor = object : AvroDescriptor(Instant::class.jvmName, PrimitiveKind.LONG) { - override fun schema( - annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy - ): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMicros().addToSchema(schema) - } - } - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Instant) = - encoder.encodeLong(ChronoUnit.MICROS.between(Instant.EPOCH, obj)) - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): Instant = - Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt new file mode 100644 index 00000000..15f47e7e --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt @@ -0,0 +1,205 @@ +package com.github.avrokotlin.avro4k + +import io.kotest.assertions.Actual +import io.kotest.assertions.Expected +import io.kotest.assertions.failure +import io.kotest.assertions.print.Printed +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer +import org.apache.avro.Conversions +import org.apache.avro.Schema +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericDatumReader +import org.apache.avro.generic.GenericDatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.EncoderFactory +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.nio.file.Path + +internal class AvroEncodingAssertions( + private val valueToEncode: T, + private val serializer: KSerializer, +) { + private var avro: Avro = + Avro { + validateSerialization = true + } + + fun withConfig(builder: AvroBuilder.() -> Unit): AvroEncodingAssertions { + this.avro = Avro(from = avro, builderAction = builder) + return this + } + + fun generatesSchema(expectedSchema: Schema): AvroEncodingAssertions { + avro.schema(serializer).toString(true) shouldBe expectedSchema.toString(true) + return this + } + + fun generatesSchema( + expectedSchemaResourcePath: Path, + schemaTransformer: (Schema) -> Schema = { it }, + ): AvroEncodingAssertions { + generatesSchema(Schema.Parser().parse(javaClass.getResourceAsStream(expectedSchemaResourcePath.toString())).let(schemaTransformer)) + return this + } + + fun isEncodedAs( + expectedEncodedGenericValue: Any?, + expectedDecodedValue: T = valueToEncode, + writerSchema: Schema = avro.schema(serializer), + decodedComparator: (actual: T, expected: T) -> Unit = { a, b -> a shouldBe b }, + ): AvroEncodingAssertions { + val actualEncodedBytes = avro4kEncode(valueToEncode, writerSchema) + val apacheEncodedBytes = avroApacheEncode(expectedEncodedGenericValue, writerSchema) + withClue("Encoded bytes are not the same as apache avro library.") { + if (!actualEncodedBytes.contentEquals(apacheEncodedBytes)) { + val expectedAvroJson = bytesToAvroJson(apacheEncodedBytes, writerSchema) + val actualAvroJson = bytesToAvroJson(actualEncodedBytes, writerSchema) + throw failure(Expected(Printed(expectedAvroJson)), Actual(Printed(actualAvroJson))) + } + } + + val actualGenericData = normalizeGenericData(avro4kGenericEncode(valueToEncode, writerSchema)) + val normalizeGenericData = normalizeGenericData(expectedEncodedGenericValue) + withClue("Encoded generic data is not the same as the expected one.") { + actualGenericData shouldBe normalizeGenericData + } + + val decodedValue = avro4kDecode(apacheEncodedBytes, writerSchema, serializer) + withClue("Decoded value is not the same as the expected one.") { + decodedComparator(decodedValue, expectedDecodedValue) + } + return this + } + + inline fun isDecodedAs( + expected: R, + serializer: KSerializer = avro.serializersModule.serializer(), + writerSchema: Schema = avro.schema(this.serializer), + ) { + val encodedBytes = avro4kEncode(valueToEncode, writerSchema) + + val decodedValue = avro4kDecode(encodedBytes, writerSchema, serializer) + withClue("Decoded value is not the same as the expected one.") { + decodedValue shouldBe expected + } + } + + private fun avro4kEncode( + value: T, + schema: Schema, + ): ByteArray { + return avro.encodeToByteArray(schema, serializer, value) + } + + private fun avro4kGenericEncode( + value: T, + schema: Schema, + ): Any? { + return avro.encodeToGenericData(schema, serializer, value) + } + + private fun avro4kDecode( + bytes: ByteArray, + writerSchema: Schema, + serializer: KSerializer, + ): R { + return avro.decodeFromByteArray(writerSchema, serializer, bytes) + } + + private fun avroApacheEncode( + value: Any?, + writerSchema: Schema, + ): ByteArray { + val writer = + GenericDatumWriter( + writerSchema, + GenericData().apply { + addLogicalTypeConversion(Conversions.UUIDConversion()) + addLogicalTypeConversion(Conversions.DecimalConversion()) + } + ) + val byteArrayOutputStream = ByteArrayOutputStream() + val encoder = EncoderFactory.get().binaryEncoder(byteArrayOutputStream, null) + writer.write(convertToAvroGenericValue(value, writerSchema), encoder) + encoder.flush() + return byteArrayOutputStream.toByteArray() + } + + private fun bytesToAvroJson( + bytes: ByteArray, + schema: Schema, + ): String { + return avroApacheEncodeJson(avroApacheDecode(bytes, schema), schema) + } + + private fun avroApacheDecode( + bytes: ByteArray, + schema: Schema, + ): Any? { + val reader = GenericDatumReader(schema) + val decoder = DecoderFactory.get().binaryDecoder(ByteArrayInputStream(bytes), null) + return reader.read(null, decoder) + } + + private fun avroApacheEncodeJson( + value: Any?, + schema: Schema, + ): String { + val writer = GenericDatumWriter(schema) + val byteArrayOutputStream = ByteArrayOutputStream() + val encoder = EncoderFactory.get().jsonEncoder(schema, byteArrayOutputStream, true) + writer.write(convertToAvroGenericValue(value, schema), encoder) + encoder.flush() + return byteArrayOutputStream.toByteArray().decodeToString() + } +} + +internal open class AvroSchemaAssertions( + private val serializer: KSerializer, + private var avro: Avro = Avro {}, +) { + private lateinit var generatedSchema: Schema + + fun withConfig(builder: AvroBuilder.() -> Unit): AvroSchemaAssertions { + this.avro = Avro(builderAction = builder) + return this + } + + fun generatesSchema(expectedSchema: Schema) { + avro.schema(serializer).toString(true) shouldBe expectedSchema.toString(true) + generatedSchema = expectedSchema + } + + fun generatesSchema( + expectedSchemaResourcePath: Path, + schemaTransformer: (Schema) -> Schema = { it }, + ) { + generatesSchema(Schema.Parser().parse(javaClass.getResourceAsStream(expectedSchemaResourcePath.toString())).let(schemaTransformer)) + } +} + +internal object AvroAssertions { + inline fun assertThat(): AvroSchemaAssertions { + return AvroSchemaAssertions(Avro.serializersModule.serializer()) + } + + fun assertThat(serializer: KSerializer): AvroSchemaAssertions { + return AvroSchemaAssertions(serializer) + } + + inline fun assertThat(value: T): AvroEncodingAssertions { + return AvroEncodingAssertions(value, Avro.serializersModule.serializer()) + } + + @Suppress("UNCHECKED_CAST") + inline fun assertThat( + value: T, + serializer: KSerializer, + ): AvroEncodingAssertions { + return AvroEncodingAssertions(value, serializer as KSerializer) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerTest.kt new file mode 100644 index 00000000..cc1b1bea --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerTest.kt @@ -0,0 +1,170 @@ +package com.github.avrokotlin.avro4k + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.apache.avro.file.DataFileStream +import org.apache.avro.file.DataFileWriter +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericDatumReader +import org.apache.avro.generic.GenericDatumWriter +import org.apache.avro.generic.GenericRecord +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.util.UUID + +internal class AvroObjectContainerTest : StringSpec({ + val firstProfile = + UserProfile( + id = UserId(UUID.randomUUID()), + name = "John Doe", + age = 30, + gender = GenderEnum.Male, + address = null + ) + val firstProfileGenericData = + record( + firstProfile.id.value.toString(), + "John Doe", + 30, + GenericData.EnumSymbol(Avro.schema(), "Male"), + null + ) + val secondProfile = + UserProfile( + id = UserId(UUID.randomUUID()), + name = "Jane Doe", + age = 25, + gender = GenderEnum.Female, + address = Address(city = "New York", country = "USA") + ) + val secondProfileGenericData = + record( + secondProfile.id.value.toString(), + "Jane Doe", + 25, + GenericData.EnumSymbol(Avro.schema(), "Female"), + record( + "New York", + "USA" + ) + ) + + "support writing avro object container file with metadata" { + // write with avro4k + val bytes = + ByteArrayOutputStream().use { + AvroObjectContainer.encodeToStream(sequenceOf(firstProfile, secondProfile), it) { + metadata("meta-string", "awesome string") + metadata("meta-long", 42) + metadata("bytes", byteArrayOf(1, 3, 2, 42)) + } + it.toByteArray() + } + // read with apache avro lib + val dataFile = DataFileStream(bytes.inputStream(), GenericDatumReader(Avro.schema())) + dataFile.getMetaString("meta-string") shouldBe "awesome string" + dataFile.getMetaLong("meta-long") shouldBe 42 + dataFile.getMeta("bytes") shouldBe byteArrayOf(1, 3, 2, 42) + normalizeGenericData(dataFile.next()) shouldBe firstProfileGenericData + normalizeGenericData(dataFile.next()) shouldBe secondProfileGenericData + dataFile.hasNext() shouldBe false + } + "support reading avro object container file with metadata" { + // write with apache avro lib + val bytes = + ByteArrayOutputStream().use { + val dataFileWriter = DataFileWriter(GenericDatumWriter()) + dataFileWriter.setMeta("meta-string", "awesome string") + dataFileWriter.setMeta("meta-long", 42) + dataFileWriter.setMeta("bytes", byteArrayOf(1, 3, 2, 42)) + dataFileWriter.create(Avro.schema(), it) + dataFileWriter.append(firstProfileGenericData.createRecord(Avro.schema())) + dataFileWriter.append(secondProfileGenericData.createRecord(Avro.schema())) + dataFileWriter.close() + it.toByteArray() + } + // read with avro4k + val profiles = + bytes.inputStream().use { + AvroObjectContainer.decodeFromStream(it) { + metadata("meta-string")?.asString() shouldBe "awesome string" + metadata("meta-long")?.asLong() shouldBe 42 + metadata("bytes")?.asBytes() shouldBe byteArrayOf(1, 3, 2, 42) + }.toList() + } + profiles.size shouldBe 2 + profiles[0] shouldBe firstProfile + profiles[1] shouldBe secondProfile + } + "encoding error is not closing the stream" { + class SimpleOutputStream : OutputStream() { + var closed = false + + override fun write(b: Int) { + throw UnsupportedOperationException() + } + + override fun close() { + closed = true + } + } + + val os = SimpleOutputStream() + shouldThrow { + AvroObjectContainer.encodeToStream(sequence {}, os) + } + os.closed shouldBe false + } + "decoding error is not closing the stream" { + class SimpleInputStream : InputStream() { + var closed = false + + override fun read(): Int { + throw UnsupportedOperationException() + } + + override fun close() { + closed = true + } + } + + val input = SimpleInputStream() + shouldThrow { + AvroObjectContainer.decodeFromStream(input).toList() + } + input.closed shouldBe false + } +}) { + @Serializable + private data class UserProfile( + val id: UserId, + val name: String, + val age: Int, + val gender: GenderEnum, + val address: Address?, + ) + + @Serializable + private enum class GenderEnum { + @AvroEnumDefault + Unknown, + Female, + Male, + } + + @Serializable + private data class Address( + val city: String, + val country: String, + ) + + @JvmInline + @Serializable + private value class UserId( + @Contextual val value: UUID, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroSingleObjectTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroSingleObjectTest.kt new file mode 100644 index 00000000..2fdd1c16 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroSingleObjectTest.kt @@ -0,0 +1,55 @@ +package com.github.avrokotlin.avro4k + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToByteArray +import org.apache.avro.SchemaNormalization +import java.time.Instant + +internal class AvroSingleObjectTest : StringSpec({ + val orderEvent = + OrderEvent( + OrderId("123"), + Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MILLIS), + 42.0 + ) + val schema = Avro.schema(OrderEvent.serializer()) + val schemas = mapOf(SchemaNormalization.parsingFingerprint64(schema) to schema) + val avroSingleObject = AvroSingleObject(schemas::get) + + "support writing avro single object" { + // write with avro4k + val bytes = avroSingleObject.encodeToByteArray(orderEvent) + + // check + bytes[0] shouldBe 0xC3.toByte() + bytes[1] shouldBe 1 + bytes.sliceArray(2..9) shouldBe SchemaNormalization.parsingFingerprint("CRC-64-AVRO", schema) + bytes.sliceArray(10 until bytes.size) shouldBe Avro.encodeToByteArray(orderEvent) + } + + "support reading avro single object" { + // write with apache avro lib + val bytes = + byteArrayOf(0xC3.toByte(), 1) + + SchemaNormalization.parsingFingerprint("CRC-64-AVRO", schema) + + Avro.encodeToByteArray(orderEvent) + + val decoded = avroSingleObject.decodeFromByteArray(bytes) + + decoded shouldBe orderEvent + } +}) { + @Serializable + private data class OrderEvent( + val orderId: OrderId, + @Contextual val date: Instant, + val amount: Double, + ) + + @Serializable + @JvmInline + private value class OrderId(val value: String) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/DefaultAvroTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/DefaultAvroTest.kt deleted file mode 100644 index 14b47a4b..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/DefaultAvroTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.avrokotlin.avro4k - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable -import java.util.UUID - -class DefaultAvroTest : FunSpec({ - - test("encoding UUID") { - val uuid = UUID.randomUUID() - val record = Avro.default.toRecord(Foo.serializer(), Foo(uuid)) - record.get("a").toString() shouldBe uuid.toString() - } -}) { - @Serializable - data class Foo(@Contextual val a: UUID) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt index ed0e3541..f4f7243e 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt @@ -2,43 +2,98 @@ package com.github.avrokotlin.avro4k import org.apache.avro.Schema import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericEnumSymbol +import org.apache.avro.generic.GenericFixed import org.apache.avro.generic.GenericRecord +import org.apache.avro.generic.IndexedRecord +import java.nio.ByteBuffer -data class RecordBuilderForTest( - val fields: List +internal data class RecordBuilderForTest( + val fields: List, + val explicitSchema: Schema? = null, ) { fun createRecord(schema: Schema): GenericRecord { - val record = GenericData.Record(schema) + val record = GenericData.Record(explicitSchema ?: schema) fields.forEachIndexed { index, value -> - val fieldSchema = schema.fields[index].schema() - record.put(index, convertValue(value, fieldSchema)) + val fieldSchema = record.schema.fields[index].schema() + record.put(index, convertToAvroGenericValue(value, fieldSchema)) } return record } +} - private fun convertValue( - value: Any?, - schema: Schema - ): Any? { - return when (value) { - is RecordBuilderForTest -> value.createRecord(schema) - is Map<*, *> -> createMap(schema, value) - is List<*> -> createList(schema, value) - else -> value - } +internal fun convertToAvroGenericValue( + value: Any?, + schema: Schema, +): Any? { + val nonNullSchema = schema.nonNull + return when (value) { + is RecordBuilderForTest -> value.createRecord(nonNullSchema) + is Map<*, *> -> createMap(nonNullSchema, value) + is Collection<*> -> createList(nonNullSchema, value) + is ByteArray -> if (nonNullSchema.type == Schema.Type.FIXED) GenericData.Fixed(nonNullSchema, value) else ByteBuffer.wrap(value) + is String -> if (nonNullSchema.type == Schema.Type.ENUM) GenericData.get().createEnum(value, nonNullSchema) else value + else -> value } +} - fun createList(schema: Schema, value: List<*>): List<*> { - val valueSchema = schema.elementType - return value.map { convertValue(it, valueSchema) } - } +internal fun normalizeGenericData(value: Any?): Any? { + return when (value) { + is IndexedRecord -> + RecordBuilderForTest( + value.schema.fields.map { field -> + normalizeGenericData(value[field.pos()]) + } + ) - fun createMap(schema: Schema, value: Map): Map { - val valueSchema = schema.valueType - return value.mapValues { convertValue(it.value, valueSchema) } + is Map<*, *> -> value.entries.associate { it.key.toString() to normalizeGenericData(it.value) } + is Collection<*> -> value.map { normalizeGenericData(it) } + is ByteArray -> value.toList() + is ByteBuffer -> value.array().toList() + is GenericFixed -> value.bytes().toList() + is CharSequence -> value.toString() + + is RecordBuilderForTest -> RecordBuilderForTest(value.fields.map { normalizeGenericData(it) }) + is Byte -> value.toInt() + is Short -> value.toInt() + is GenericEnumSymbol<*>, + is Boolean, is Char, is Number, null, + -> value + + else -> TODO("Not implemented for ${value.javaClass}") } } -fun record(vararg fields: Any?): RecordBuilderForTest { +private fun createList( + schema: Schema, + value: Collection<*>, +): List<*> { + val valueSchema = schema.elementType + return value.map { convertToAvroGenericValue(it, valueSchema) } +} + +private fun createMap( + schema: Schema, + value: Map, +): Map { + val valueSchema = schema.valueType + return value.mapValues { convertToAvroGenericValue(it.value, valueSchema) } +} + +internal fun record(vararg fields: Any?): RecordBuilderForTest { return RecordBuilderForTest(listOf(*fields)) -} \ No newline at end of file +} + +internal fun recordWithSchema( + schema: Schema, + vararg fields: Any?, +): RecordBuilderForTest { + return RecordBuilderForTest(listOf(*fields), schema) +} + +private val Schema.nonNull: Schema + get() = + when { + isUnion && isNullable -> this.types.filter { it.type != Schema.Type.NULL }.let { if (it.size > 1) Schema.createUnion(it) else it[0] } + else -> this + } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/RecordEncoderTest.kt deleted file mode 100644 index da945bd2..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordEncoderTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.avrokotlin.avro4k - -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class RecordEncoderTest : FunSpec({ - test("encoding basic data class") { - val input = Foo( - "string value", - 2.2, - true, - S(setOf(1, 2, 3)), - ValueClass(ByteArray(3) { it.toByte() }) // 0,1,2 array - ) - val record = Avro.default.toRecord( - Foo.serializer(), input - ) - val output = Avro.default.fromRecord(Foo.serializer(), record) - output shouldBe input - } -}) { - @Serializable - data class Foo(val a: String, val b: Double, val c: Boolean, val s: S, val vc: ValueClass) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Foo) return false - - if (a != other.a) return false - if (b != other.b) return false - if (c != other.c) return false - if (s != other.s) return false - if (!vc.value.contentEquals(other.vc.value)) return false - - return true - } - - override fun hashCode(): Int { - var result = a.hashCode() - result = 31 * result + b.hashCode() - result = 31 * result + c.hashCode() - result = 31 * result + s.hashCode() - result = 31 * result + vc.value.contentHashCode() - return result - } - } - - @Serializable - data class S(val t: Set) - - @JvmInline - @Serializable - value class ValueClass(val value: ByteArray) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/dataClassesForTests.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/dataClassesForTests.kt new file mode 100644 index 00000000..552a3110 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/dataClassesForTests.kt @@ -0,0 +1,55 @@ +package com.github.avrokotlin.avro4k + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal enum class SomeEnum { + A, + B, + C, +} + +@Serializable +@SerialName("RecordWithGenericField") +internal data class RecordWithGenericField(val value: T) + +@Serializable +@JvmInline +internal value class WrappedBoolean(val value: Boolean) + +@Serializable +@JvmInline +internal value class WrappedByte(val value: Byte) + +@Serializable +@JvmInline +internal value class WrappedChar(val value: Char) + +@Serializable +@JvmInline +internal value class WrappedShort(val value: Short) + +@Serializable +@JvmInline +internal value class WrappedInt(val value: Int) + +@Serializable +@JvmInline +internal value class WrappedLong(val value: Long) + +@Serializable +@JvmInline +internal value class WrappedFloat(val value: Float) + +@Serializable +@JvmInline +internal value class WrappedDouble(val value: Double) + +@Serializable +@JvmInline +internal value class WrappedString(val value: String) + +@Serializable +@JvmInline +internal value class WrappedEnum(val value: SomeEnum) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ArrayDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ArrayDecoderTest.kt deleted file mode 100644 index 81216e72..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ArrayDecoderTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.WordSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData - -@Serializable -data class TestArrayBooleans(val booleans: Array) - -@Serializable -data class TestListDoubles(val doubles: List) - -@Serializable -data class TestSetString(val strings: Set) - -@Serializable -data class TestArrayRecords(val records: Array) - -@Serializable -data class TestListRecords(val records: List) - -@Serializable -data class TestSetRecords(val records: Set) - -@Serializable -data class Record(val str: String, val double: Double) - -class ArrayDecoderTest : WordSpec({ - - "Decoder" should { - listOf( - "array" to arrayOf(true, false, true), - "list" to listOf(true, false, true), - "GenericData.Array" to GenericData.Array( - Schema.createArray(Schema.create(Schema.Type.BOOLEAN)), listOf(true, false, true) - ) - ).forEach { - "support ${it.first} for an Array of booleans" { - val schema = Avro.default.schema(TestArrayBooleans.serializer()) - val record = GenericData.Record(schema) - record.put("booleans", it.second) - Avro.default.fromRecord(TestArrayBooleans.serializer(), record).booleans.toList() shouldBe listOf( - true, false, true - ) - } - } - listOf( - "array" to arrayOf(12.54, 23.5, 9123.2314), - "list" to listOf(12.54, 23.5, 9123.2314), - "GenericData.Array" to GenericData.Array( - Schema.createArray(Schema.create(Schema.Type.DOUBLE)), listOf(12.54, 23.5, 9123.2314) - ) - ).forEach { - "support ${it.first} for a List of doubles" { - val schema = Avro.default.schema(TestListDoubles.serializer()) - val record = GenericData.Record(schema) - record.put("doubles", it.second) - Avro.default.fromRecord(TestListDoubles.serializer(), record) shouldBe TestListDoubles( - listOf( - 12.54, 23.5, 9123.2314 - ) - ) - } - } - val recordSchema = Avro.default.schema(Record.serializer()) - val records = listOf(GenericData.Record(recordSchema).apply { - put("str", "qwe") - put("double", 123.4) - }, GenericData.Record(recordSchema).apply { - put("str", "wer") - put("double", 8234.324) - }) - listOf( - "array" to records.toTypedArray(), - "list" to records, - "GenericData.Array" to GenericData.Array( - Schema.createArray(recordSchema), records - ) - ).forEach { - "support ${it.first} for a List of records" { - val containerSchema = Avro.default.schema(TestListRecords.serializer()) - val container = GenericData.Record(containerSchema) - container.put("records", it.second) - - Avro.default.fromRecord( - TestListRecords.serializer(), container - ) shouldBe TestListRecords(listOf(Record("qwe", 123.4), Record("wer", 8234.324))) - } - "support ${it.first} for a Set of records" { - val containerSchema = Avro.default.schema(TestSetRecords.serializer()) - val container = GenericData.Record(containerSchema) - container.put("records", it.second) - - Avro.default.fromRecord( - TestSetRecords.serializer(), container - ) shouldBe TestSetRecords(setOf(Record("qwe", 123.4), Record("wer", 8234.324))) - } - } - - listOf( - "array" to arrayOf("Qwe", "324", "q"), - "list" to listOf("Qwe", "324", "q"), - "GenericData.Array" to GenericData.Array( - Schema.createArray(Schema.create(Schema.Type.STRING)), listOf("Qwe", "324", "q") - ) - ).forEach { - "support ${it.first} for a Set of strings" { - val schema = Avro.default.schema(TestSetString.serializer()) - val record = GenericData.Record(schema) - record.put("strings", it.second) - Avro.default.fromRecord(TestSetString.serializer(), record) shouldBe TestSetString(setOf("Qwe", "324", "q")) - } - } - } - -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroAliasTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroAliasTest.kt deleted file mode 100644 index d57c3817..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroAliasTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroAlias -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericData - -@Serializable -data class OldFooString(val s: String) -@Serializable -data class FooStringWithAlias(@AvroAlias("s") val str: String) - -class AvroAliasTest : FunSpec({ - - test("decode with alias") { - val schema = Avro.default.schema(OldFooString.serializer()) - val record = GenericData.Record(schema) - record.put("s", "hello") - Avro.default.fromRecord(FooStringWithAlias.serializer(), record) shouldBe FooStringWithAlias("hello") - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt deleted file mode 100644 index 73213729..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.* -import com.github.avrokotlin.avro4k.io.AvroDecodeFormat -import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer -import io.kotest.core.spec.style.FunSpec -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.Serializable -import kotlinx.serialization.encodeToByteArray -import org.apache.avro.generic.GenericData -import java.math.BigDecimal - -@Serializable -data class FooElement( - val content : String -) - -@Serializable -@AvroName("container") -data class ContainerWithoutDefaultFields( - val name : String -) -@Serializable -@AvroName("container") -data class ContainerWithDefaultFields( - val name : String, - @AvroDefault("hello") - val strDefault : String, - @AvroDefault("1") - val intDefault : Int, - @AvroDefault("true") - val booleanDefault : Boolean, - @AvroDefault("1.23") - val doubleDefault : Double, - @AvroDefault(Avro.NULL) - val nullableStr : String?, - @AvroDefault("""{"content":"foo"}""") - val foo : FooElement, - @AvroDefault("[]") - val emptyFooList : List, - @AvroDefault("""[{"content":"bar"}]""") - val filledFooList : List, - @AvroDefault("\u0000") - @ScalePrecision(0,10) - @Serializable(BigDecimalSerializer::class) - val bigDecimal : BigDecimal -) -@Serializable -@AvroEnumDefault("UNKNOWN") -enum class EnumWithDefault { - UNKNOWN, A -} - -@Serializable -@AvroEnumDefault("UNKNOWN") -enum class FutureEnumWithDefault { - UNKNOWN, A, C -} - -@Serializable -data class Wrap(val value: EnumWithDefault) - -@Serializable -data class FutureWrap(val value: FutureEnumWithDefault) - -class AvroDefaultValuesDecoderTest : FunSpec({ - test("test default values correctly decoded") { - val name = "abc" - val writerSchema = Avro.default.schema(ContainerWithoutDefaultFields.serializer()) - val record = GenericData.Record(writerSchema) - record.put("name", name) - - val byteArray = Avro.default.encodeToByteArray(ContainerWithoutDefaultFields("abc")) - val deserialized = Avro.default.openInputStream(ContainerWithDefaultFields.serializer()){ - decodeFormat = AvroDecodeFormat.Data(writerSchema, defaultReadSchema) - }.from(byteArray).nextOrThrow() - deserialized.name.shouldBe("abc") - deserialized.strDefault.shouldBe("hello") - deserialized.intDefault.shouldBe(1) - deserialized.booleanDefault.shouldBe(true) - deserialized.doubleDefault.shouldBe(1.23) - deserialized.nullableStr.shouldBeNull() - deserialized.foo.content.shouldBe("foo") - deserialized.emptyFooList.shouldBeEmpty() - deserialized.filledFooList.shouldContainExactly(FooElement("bar")) - deserialized.bigDecimal.shouldBe(BigDecimal.ZERO) - - } - test("Decoding enum with an unknown or future value uses default value") { - val encoded = Avro.default.encodeToByteArray( - FutureWrap.serializer(), - FutureWrap(FutureEnumWithDefault.C) - ) - val decoded = Avro.default.decodeFromByteArray(Wrap.serializer(), encoded) - - decoded shouldBe Wrap(EnumWithDefault.UNKNOWN) - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoderTest.kt deleted file mode 100644 index b0070987..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/ByteArrayDecoderTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.Schema -import org.apache.avro.generic.GenericData -import java.nio.ByteBuffer - -class ByteArrayDecoderTest : StringSpec({ - val byteArray = byteArrayOf(1, 4, 9) - listOf( - "ByteBuffer" to ByteBuffer.wrap(byteArray), - "ByteArray" to byteArray, - "Array" to arrayOf(1, 4, 9), - "GenericData.Array" to GenericData.Array( - Schema.createArray(Schema.create(Schema.Type.BYTES)), - byteArray.toList() - ) - ).forEach { - "decode ${it.first} to ByteArray" { - val schema = Avro.default.schema(ByteArrayTest.serializer()) - val record = GenericData.Record(schema) - record.put("z", it.second) - Avro.default.fromRecord(ByteArrayTest.serializer(), record).z shouldBe byteArrayOf(1, 4, 9) - } - "decode ${it.first} to List" { - val schema = Avro.default.schema(ListByteTest.serializer()) - val record = GenericData.Record(schema) - record.put("z", it.second) - Avro.default.fromRecord(ListByteTest.serializer(), record).z shouldBe listOf(1, 4, 9) - } - "decode ${it.first} to Array" { - val schema = Avro.default.schema(ArrayByteTest.serializer()) - val record = GenericData.Record(schema) - record.put("z", it.second) - Avro.default.fromRecord(ArrayByteTest.serializer(), record).z shouldBe arrayOf(1, 4, 9) - } - } -}) { - - @Serializable - data class ByteArrayTest(val z: ByteArray) - - @Serializable - data class ArrayByteTest(val z: Array) - - @Serializable - data class ListByteTest(val z: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodeUtf8.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodeUtf8.kt deleted file mode 100644 index f8ad79a4..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodeUtf8.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 - -class DecodeUtf8 : StringSpec({ - "decode utf8" { - @Serializable - data class Foo(val a: String) - - val schema = Avro.default.schema(Foo.serializer()) - - val record = GenericData.Record(schema) - record.put("a", Utf8("utf8-string")) - Avro.default.fromRecord(Foo.serializer(), record) shouldBe Foo("utf8-string") - } -}) { -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodingDefaultValuesTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodingDefaultValuesTest.kt deleted file mode 100644 index b2c906c1..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/DecodingDefaultValuesTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -@file:UseSerializers( - LocalDateSerializer::class -) -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.time.LocalDate - -class DecodingDefaultValuesTest : FunSpec({ - test("data class decoder should override defaults with avro data") { - val foo = Foo("a", "b", LocalDate.of(2019, 1, 2), LocalDate.of(2018, 4, 5)) - val bytes = Avro.default.encodeToByteArray(Foo.serializer(), foo) - Avro.default.decodeFromByteArray(Foo.serializer(), bytes) shouldBe foo - } -}) - -@Serializable -data class Foo( - val a: String, - val b: String = "hello", - val c: LocalDate = LocalDate.of(1979, 9, 10), - val d: LocalDate -) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/NestedClassDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/NestedClassDecoderTest.kt deleted file mode 100644 index f812c800..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/NestedClassDecoderTest.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroDefault -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.WordSpec -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 - -// data class OptionCounty(county: Option[County]) - -@Serializable -data class County(val name: String, val towns: List, val ceremonial: Boolean, val lat: Double, val long: Double) - -@Serializable -data class Town(val name: String, val population: Int) - -@Serializable -data class Birthplace(val person: String, val town: Town) - -@Serializable -data class PersonV1(val name: String, val hasChickenPoxVaccine: Boolean) - -@Serializable -data class PersonV2(val name: String, - val hasChickenPoxVaccine: Boolean, - @AvroDefault(Avro.NULL) - val hasCovidVaccine: Boolean? = null) - -class NestedClassDecoderTest : WordSpec({ - - "Decoder" should { - - "decode nested class" { - - val townSchema = Avro.default.schema(Town.serializer()) - val birthplaceSchema = Avro.default.schema(Birthplace.serializer()) - - val hardwick = GenericData.Record(townSchema) - hardwick.put("name", Utf8("Hardwick")) - hardwick.put("population", 123) - - val birthplace = GenericData.Record(birthplaceSchema) - birthplace.put("person", Utf8("Sammy Sam")) - birthplace.put("town", hardwick) - - Avro.default.fromRecord(Birthplace.serializer(), birthplace) shouldBe - Birthplace(person = "Sammy Sam", town = Town(name = "Hardwick", population = 123)) - } - - "decode nested list of classes" { - - val countySchema = Avro.default.schema(County.serializer()) - val townSchema = Avro.default.schema(Town.serializer()) - - val hardwick = GenericData.Record(townSchema) - hardwick.put("name", "Hardwick") - hardwick.put("population", 123) - - val weedon = GenericData.Record(townSchema) - weedon.put("name", "Weedon") - weedon.put("population", 225) - - val bucks = GenericData.Record(countySchema) - bucks.put("name", "Bucks") - bucks.put("towns", listOf(hardwick, weedon)) - bucks.put("ceremonial", true) - bucks.put("lat", 12.34) - bucks.put("long", 0.123) - - Avro.default.fromRecord(County.serializer(), bucks) shouldBe - County( - name = "Bucks", - towns = listOf(Town(name = "Hardwick", population = 123), Town(name = "Weedon", population = 225)), - ceremonial = true, - lat = 12.34, - long = 0.123 - ) - } - - "decode nested class from previous schema (new schema is back compat)" { - - val personV1Schema = Avro.default.schema(PersonV1.serializer()) - - val personV1 = GenericData.Record(personV1Schema) - personV1.put("name", Utf8("Ryan")) - personV1.put("hasChickenPoxVaccine", true) - - Avro.default.fromRecord(PersonV2.serializer(), personV1) shouldBe - PersonV2(name="Ryan", hasChickenPoxVaccine = true, hasCovidVaccine = null) - } - -// "decode optional structs" { -// val countySchema = AvroSchema[County] -// val townSchema = AvroSchema[Town] -// val optionCountySchema = AvroSchema[OptionCounty] -// -// val obj = OptionCounty(Some(County("Bucks", Seq(Town("Hardwick", 123), Town("Weedon", 225)), true, 12.34, 0.123))) -// -// val hardwick = new GenericData.Record(townSchema) -// hardwick.put("name", "Hardwick") -// hardwick.put("population", 123) -// -// val weedon = new GenericData.Record(townSchema) -// weedon.put("name", "Weedon") -// weedon.put("population", 225) -// -// val bucks = new GenericData.Record(countySchema) -// bucks.put("name", "Bucks") -// bucks.put("towns", List(hardwick, weedon).asJava) -// bucks.put("ceremonial", true) -// bucks.put("lat", 12.34) -// bucks.put("long", 0.123) -// -// val record = new GenericData.Record(optionCountySchema) -// record.put("county", bucks) -// -// Decoder[OptionCounty].decode(record, optionCountySchema, DefaultFieldMapper) shouldBe obj -// -// val emptyRecord = new GenericData.Record(optionCountySchema) -// emptyRecord.put("county", null) -// -// Decoder[OptionCounty].decode(emptyRecord, optionCountySchema, DefaultFieldMapper) shouldBe OptionCounty(None) -// } - } - -}) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/URLDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/URLDecoderTest.kt deleted file mode 100644 index beae1450..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/URLDecoderTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:UseSerializers(URLSerializer::class) - -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.URLSerializer -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 -import java.net.URL - -class URLDecoderTest : FunSpec({ - - test("decode UT8 to URL") { - - val schema = Avro.default.schema(TestUrl.serializer()) - - val record = GenericData.Record(schema) - record.put("b", Utf8("http://www.sksamuel.com")) - Avro.default.fromRecord(TestUrl.serializer(), record) shouldBe TestUrl(URL("http://www.sksamuel.com")) - } -}) { - - @Serializable - data class TestUrl(val b: URL) - - @Serializable - data class TestUrlList(val urls: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/UUIDDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/UUIDDecoderTest.kt deleted file mode 100644 index 11fa6bff..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/UUIDDecoderTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.avrokotlin.avro4k.decoder - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 -import java.util.* - -class UUIDDecoderTest : StringSpec({ - - "decode UUIDs encoded as Utf8" { - @Serializable - data class UUIDTest(val uuid: UUID) - - val uuid = UUID.randomUUID() - val schema = Avro.default.schema(UUIDTest.serializer()) - - val record = GenericData.Record(schema) - record.put("uuid", Utf8(uuid.toString())) - - Avro.default.fromRecord(UUIDTest.serializer(), record) shouldBe UUIDTest(uuid) - } -}) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/ArrayEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/ArrayEncodingTest.kt new file mode 100644 index 00000000..9a54145e --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/ArrayEncodingTest.kt @@ -0,0 +1,103 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable + +internal class ArrayEncodingTest : StringSpec({ + "support array of booleans" { + @Serializable + data class TestArrayBooleans(val booleans: List) + + AvroAssertions.assertThat(TestArrayBooleans(listOf(true, false, true))) + .isEncodedAs(record(listOf(true, false, true))) + } + "support array of nullable booleans" { + @Serializable + data class TestArrayBooleans(val booleans: List) + + AvroAssertions.assertThat(TestArrayBooleans(listOf(true, null, false))) + .isEncodedAs(record(listOf(true, null, false))) + } + "support List of doubles" { + AvroAssertions.assertThat(TestListDoubles(listOf(12.54, 23.5, 9123.2314))) + .isEncodedAs(record(listOf(12.54, 23.5, 9123.2314))) + } + "support List of records" { + @Serializable + data class TestListRecords(val records: List) + + AvroAssertions.assertThat( + TestListRecords( + listOf( + Record("qwe", 123.4), + Record("wer", 8234.324) + ) + ) + ).isEncodedAs( + record( + listOf( + record("qwe", 123.4), + record("wer", 8234.324) + ) + ) + ) + } + "support List of nullable records" { + @Serializable + data class TestListNullableRecords(val records: List) + + AvroAssertions.assertThat( + TestListNullableRecords( + listOf( + Record("qwe", 123.4), + null, + Record("wer", 8234.324) + ) + ) + ).isEncodedAs( + record( + listOf( + record("qwe", 123.4), + null, + record("wer", 8234.324) + ) + ) + ) + } + "support Set of records" { + AvroAssertions.assertThat( + TestSetRecords( + setOf( + Record("qwe", 123.4), + Record("wer", 8234.324) + ) + ) + ).isEncodedAs( + record( + listOf( + record("qwe", 123.4), + record("wer", 8234.324) + ) + ) + ) + } + + "support Set of strings" { + AvroAssertions.assertThat(TestSetString(setOf("Qwe", "324", "q"))) + .isEncodedAs(record(listOf("Qwe", "324", "q"))) + } +}) { + @Serializable + private data class TestListDoubles(val doubles: List) + + @Serializable + private data class TestSetString(val strings: Set) + + @Serializable + private data class TestSetRecords(val records: Set) + + @Serializable + private data class Record(val str: String, val double: Double) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroAliasEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroAliasEncodingTest.kt new file mode 100644 index 00000000..dad51225 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroAliasEncodingTest.kt @@ -0,0 +1,103 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.SomeEnum +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.recordWithSchema +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder +import org.apache.avro.generic.GenericData + +internal class AvroAliasEncodingTest : StringSpec({ + "support alias on field" { + AvroAssertions.assertThat(EncodedField("hello")) + .isEncodedAs(record("hello")) + .isDecodedAs(DecodedFieldWithAlias(3, "hello")) + } + + "support alias on record" { + AvroAssertions.assertThat(EncodedRecord("hello")) + .isEncodedAs(record("hello")) + .isDecodedAs(DecodedRecordWithAlias("hello")) + } + + "support alias on record inside an union" { + val writerSchema = + Schema.createUnion( + SchemaBuilder.enumeration("OtherEnum").symbols("OTHER"), + SchemaBuilder.record("UnknownRecord").aliases("RecordA") + .fields().name("field").type().stringType().noDefault() + .endRecord() + ) + AvroAssertions.assertThat(EncodedRecord("hello")) + .isEncodedAs(recordWithSchema(writerSchema.types[1], "hello"), writerSchema = writerSchema) + .isDecodedAs(DecodedRecordWithAlias("hello")) + } + + "support alias on enum" { + val writerSchema = + SchemaBuilder.record("EnumWrapperRecord").fields() + .name("value") + .type( + SchemaBuilder.enumeration("UnknownEnum").aliases("com.github.avrokotlin.avro4k.SomeEnum").symbols("A", "B", "C") + ) + .noDefault() + .endRecord() + AvroAssertions.assertThat(EnumWrapperRecord(SomeEnum.A)) + .isEncodedAs(record(GenericData.EnumSymbol(writerSchema.fields[0].schema(), "A")), writerSchema = writerSchema) + } + + "support alias on enum inside an union" { + val writerSchema = + SchemaBuilder.record("EnumWrapperRecord").fields() + .name("value") + .type( + Schema.createUnion( + SchemaBuilder.enumeration("OtherEnum").symbols("OTHER"), + SchemaBuilder.record("UnknownRecord").aliases("RecordA") + .fields().name("field").type().stringType().noDefault() + .endRecord(), + SchemaBuilder.enumeration("UnknownEnum").aliases("com.github.avrokotlin.avro4k.SomeEnum").symbols("A", "B", "C") + ) + ) + .noDefault() + .endRecord() + AvroAssertions.assertThat(EnumWrapperRecord(SomeEnum.A)) + .isEncodedAs(record(GenericData.EnumSymbol(writerSchema.fields[0].schema().types[2], "A")), writerSchema = writerSchema) + } +}) { + @Serializable + @SerialName("Record") + private data class EncodedField( + val s: String, + ) + + @Serializable + @SerialName("Record") + private data class DecodedFieldWithAlias( + val newField: Int = 3, + @AvroAlias("s") val str: String, + ) + + @Serializable + @SerialName("RecordA") + private data class EncodedRecord( + val field: String, + ) + + @Serializable + @AvroAlias("RecordA") + private data class DecodedRecordWithAlias( + val field: String, + ) + + @Serializable + @SerialName("EnumWrapperRecord") + private data class EnumWrapperRecord( + val value: SomeEnum, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroDefaultEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroDefaultEncodingTest.kt new file mode 100644 index 00000000..3502b33f --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroDefaultEncodingTest.kt @@ -0,0 +1,124 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecimal +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.SomeEnum +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +internal class AvroDefaultEncodingTest : StringSpec({ + "test default values correctly decoded" { + AvroAssertions.assertThat(ContainerWithoutDefaultFields("abc")) + .isEncodedAs(record("abc")) + .isDecodedAs( + ContainerWithDefaultFields( + "abc", + "hello", + "hello", + null, + SomeEnum.B, + SomeEnum.B, + null, + 1, + 1, + true, + 'a', + 'a', + null, + 1.23, + FooElement("foo"), + emptyList(), + listOf(FooElement("bar")), + BigDecimal.ZERO, + BigDecimal.ZERO, + null, + BigDecimal.ZERO, + BigDecimal.ZERO, + null + ) + ) + } +}) { + @Serializable + private data class FooElement( + val content: String, + ) + + @Serializable + @SerialName("container") + private data class ContainerWithoutDefaultFields( + val name: String, + ) + + @Serializable + @SerialName("container") + private data class ContainerWithDefaultFields( + val name: String, + @AvroDefault("hello") + val strDefault: String, + @AvroDefault("hello") + val strDefaultNullable: String?, + @AvroDefault("null") + val strDefaultNullableNull: String?, + @AvroDefault("B") + val enumDefault: SomeEnum, + @AvroDefault("B") + val enumDefaultNullable: SomeEnum?, + @AvroDefault("null") + val enumDefaultNullableNull: SomeEnum?, + @AvroDefault("1") + val intDefault: Int, + @AvroDefault("1") + val shouldBe1AndNot42: Int = 42, + @AvroDefault("true") + val booleanDefault: Boolean, + @AvroDefault("a") + val charDefault: Char, + @AvroDefault("a") + val charDefaultNullable: Char?, + @AvroDefault("null") + val charDefaultNullableNull: Char?, + @AvroDefault("1.23") + val doubleDefault: Double, + @AvroDefault("""{"content":"foo"}""") + val foo: FooElement, + @AvroDefault("[]") + val emptyFooList: List, + @AvroDefault("""[{"content":"bar"}]""") + val filledFooList: List, + @Contextual + @AvroDecimal(scale = 0, precision = 8) + @AvroDefault("\u0000") + val bigDecimal: BigDecimal, + @Contextual + @AvroDecimal(scale = 0, precision = 8) + @AvroDefault("\u0000") + val bigDecimalNullable: BigDecimal?, + @Contextual + @AvroDecimal(scale = 2, precision = 8) + @AvroDefault("null") + val bigDecimalNullableNull: BigDecimal?, + @Contextual + @AvroFixed(size = 16) + @AvroDecimal(scale = 0, precision = 8) + @AvroDefault("\u0000") + val bigDecimalFixed: BigDecimal, + @Contextual + @AvroFixed(size = 16) + @AvroDecimal(scale = 0, precision = 8) + @AvroDefault("\u0000") + val bigDecimalFixedNullable: BigDecimal?, + @Contextual + @AvroFixed(size = 16) + @AvroDecimal(scale = 2, precision = 8) + @AvroDefault("null") + val bigDecimalFixedNullableNull: BigDecimal?, + val kotlinDefault: Int = 42, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt new file mode 100644 index 00000000..30b05a65 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt @@ -0,0 +1,158 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToByteArray +import org.apache.avro.generic.GenericData +import org.junit.jupiter.api.assertThrows +import kotlin.io.path.Path + +internal class AvroFixedEncodingTest : StringSpec({ + "support fixed on data class fields" { + AvroAssertions.assertThat() + .generatesSchema(Path("/fixed_string.json")) + + val schema = Avro.schema().fields[0].schema() + AvroAssertions.assertThat(FixedStringField("1234567")) + .isEncodedAs(record(GenericData.Fixed(schema, "1234567".toByteArray()))) + } + + "support fixed on string value classes" { + AvroAssertions.assertThat() + .generatesSchema(Path("/fixed_string.json")) + + val schema = Avro.schema().fields[0].schema() + AvroAssertions.assertThat(FixedNestedStringField(FixedStringValueClass("1234567"))) + .isEncodedAs(record(GenericData.Fixed(schema, "1234567".toByteArray()))) + + AvroAssertions.assertThat(FixedStringValueClass("1234567")) + .isEncodedAs(GenericData.Fixed(Avro.schema(), "1234567".toByteArray())) + } + + "support @AvroFixed on ByteArray" { + AvroAssertions.assertThat(FixedByteArrayField("1234567".toByteArray())) + .generatesSchema(Path("/fixed_string.json")) + .isEncodedAs(record(GenericData.Fixed(Avro.schema().fields[0].schema(), "1234567".toByteArray()))) + } + + "top-est @AvroFixed annotation takes precedence over nested @AvroFixed annotations" { + AvroAssertions.assertThat() + .generatesSchema(Path("/fixed_string_5.json")) + + val schema = Avro.schema().fields[0].schema() + AvroAssertions.assertThat(FieldPriorToValueClass(FixedStringValueClass("12345"))) + .isEncodedAs(record(GenericData.Fixed(schema, "12345".toByteArray()))) + + // Not 5 chars fixed + shouldThrow { + Avro.schema() + Avro.encodeToByteArray(FieldPriorToValueClass(FixedStringValueClass("1234567"))) + } + } + + "Should fail when the fixed size is not respected" { + assertThrows { + Avro.encodeToByteArray(ByteArrayFixedTest(byteArrayOf(1, 4, 9))) + } + + @Serializable + @SerialName("Foo") + data class StringFoo( + @AvroFixed(7) val a: String?, + ) + + assertThrows { + Avro.encodeToByteArray(StringFoo("hello")) + } + } + + "encode/decode ByteArray as FIXED" { + AvroAssertions.assertThat(ByteArrayFixedTest(byteArrayOf(0, 0, 0, 0, 0, 1, 4, 9))) + .isEncodedAs(record(byteArrayOf(0, 0, 0, 0, 0, 1, 4, 9))) + } + + "encode/decode strings as GenericFixed when schema is Type.FIXED" { + @Serializable + @SerialName("Foo") + data class StringFoo( + @AvroFixed(5) val a: String?, + ) + + AvroAssertions.assertThat(StringFoo("hello")) + .isEncodedAs(record(byteArrayOf(104, 101, 108, 108, 111))) + } + +// "Handle FIXED in unions with the good and bad fullNames and aliases" { +// fail("TODO") +// } +}) { + @Serializable + @SerialName("Fixed") + private data class FixedStringField( + @AvroFixed(7) val mystring: String, + ) + + @Serializable + @SerialName("Fixed") + private data class FixedByteArrayField( + @AvroFixed(7) val mystring: ByteArray, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FixedByteArrayField + + return mystring.contentEquals(other.mystring) + } + + override fun hashCode(): Int { + return mystring.contentHashCode() + } + } + + @Serializable + @SerialName("Fixed") + private data class FixedNestedStringField( + val mystring: FixedStringValueClass, + ) + + @Serializable + @SerialName("Fixed") + private data class FieldPriorToValueClass( + @AvroFixed(5) val mystring: FixedStringValueClass, + ) + + @JvmInline + @Serializable + @SerialName("FixedString") + private value class FixedStringValueClass( + @AvroFixed(7) val mystring: String, + ) + + @Serializable + private data class ByteArrayFixedTest( + @AvroFixed(8) val z: ByteArray, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayFixedTest + + return z.contentEquals(other.z) + } + + override fun hashCode(): Int { + return z.contentHashCode() + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroStringableEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroStringableEncodingTest.kt new file mode 100644 index 00000000..9b965bdc --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroStringableEncodingTest.kt @@ -0,0 +1,180 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.AvroStringable +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder +import java.math.BigDecimal +import kotlin.time.Duration.Companion.days + +internal class AvroStringableEncodingTest : StringSpec({ + listOf( + true to "true", + 1.toByte() to "1", + 2.toShort() to "2", + 3 to "3", + 4L to "4", + 5.0f to "5.0", + 6.0 to "6.0", + '7' to "7", + "1234567" to "1234567", + 1.5.days to "P1DT43200S", + BigDecimal("1234567890.1234567890") to "1234567890.1234567890", + java.time.Duration.parse("PT36H") to "P1DT43200S", + java.time.Period.parse("P3Y4D") to "P36M4D", + java.time.LocalTime.parse("12:34:56") to "12:34:56", + java.time.LocalDate.parse("2021-01-01") to "2021-01-01", + java.time.LocalDateTime.parse("2021-01-01T12:34:56") to "2021-01-01T12:34:56", + java.time.Instant.parse("2020-01-01T12:34:56Z") to "2020-01-01T12:34:56Z" + ).forEach { (value, stringifiedValue) -> + "${value::class.qualifiedName}: data class property" { + AvroAssertions.assertThat(StringifiedDataClass(value), StringifiedDataClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), false))) + .generatesSchema( + SchemaBuilder.record(StringifiedDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING)).noDefault() + .endRecord() + ) + .isEncodedAs(record(stringifiedValue)) + } + "${value::class.qualifiedName}: nullable data class property" { + AvroAssertions.assertThat(StringifiedDataClass(value), StringifiedDataClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), true))) + .generatesSchema( + SchemaBuilder.record(StringifiedDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .endRecord() + ) + .isEncodedAs(record(stringifiedValue)) + AvroAssertions.assertThat(StringifiedDataClass(null), StringifiedDataClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), true))) + .generatesSchema( + SchemaBuilder.record(StringifiedDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .endRecord() + ) + .isEncodedAs(record(null)) + } + "${value::class.qualifiedName}: value class" { + AvroAssertions.assertThat(StringifiedValueClass(value), StringifiedValueClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), false))) + .generatesSchema(Schema.create(Schema.Type.STRING)) + .isEncodedAs(stringifiedValue, decodedComparator = { a, b -> a.value shouldBe b.value }) + } + "${value::class.qualifiedName}: nullable value class" { + AvroAssertions.assertThat(StringifiedValueClass(value), StringifiedValueClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), true))) + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + .isEncodedAs(stringifiedValue, decodedComparator = { a, b -> a.value shouldBe b.value }) + AvroAssertions.assertThat(StringifiedValueClass(null), StringifiedValueClass.serializer(Avro.serializersModule.serializer(value::class, emptyList(), true))) + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + .isEncodedAs(null) + } + } + + "ByteArray: data class property" { + AvroAssertions.assertThat(StringifiedByteArrayDataClass("hello".toByteArray())) + .generatesSchema( + SchemaBuilder.record(StringifiedByteArrayDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING)).noDefault() + .endRecord() + ) + .isEncodedAs(record("hello")) + } + "ByteArray: nullable data class property" { + AvroAssertions.assertThat(StringifiedNullableByteArrayDataClass("hello".toByteArray())) + .generatesSchema( + SchemaBuilder.record(StringifiedNullableByteArrayDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .endRecord() + ) + .isEncodedAs(record("hello")) + AvroAssertions.assertThat(StringifiedNullableByteArrayDataClass(null)) + .generatesSchema( + SchemaBuilder.record(StringifiedNullableByteArrayDataClass::class.qualifiedName).fields() + .name("value").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .endRecord() + ) + .isEncodedAs(record(null)) + } + "ByteArray: value class" { + AvroAssertions.assertThat(StringifiedValueClass("hello".toByteArray()), StringifiedValueClass.serializer(ByteArraySerializer())) + .generatesSchema(Schema.create(Schema.Type.STRING)) + .isEncodedAs("hello", decodedComparator = { a, b -> a.value shouldBe b.value }) + } + "ByteArray: nullable value class" { + AvroAssertions.assertThat(StringifiedValueClass("hello".toByteArray()), StringifiedValueClass.serializer(ByteArraySerializer().nullable)) + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + .isEncodedAs("hello", decodedComparator = { a, b -> a.value shouldBe b.value }) + AvroAssertions.assertThat(StringifiedValueClass(null), StringifiedValueClass.serializer(ByteArraySerializer().nullable)) + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + .isEncodedAs(null) + } +}) { + @Serializable + private data class StringifiedByteArrayDataClass( + @AvroFixed(10) // ignored as @AvroStringable takes precedence + @AvroStringable + val value: ByteArray, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StringifiedByteArrayDataClass + + return value.contentEquals(other.value) + } + + override fun hashCode(): Int { + return value.contentHashCode() + } + } + + @Serializable + private data class StringifiedNullableByteArrayDataClass( + @AvroFixed(10) // ignored as @AvroStringable takes precedence + @AvroStringable + val value: ByteArray?, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StringifiedNullableByteArrayDataClass + + if (value != null) { + if (other.value == null) return false + if (!value.contentEquals(other.value)) return false + } else if (other.value != null) { + return false + } + + return true + } + + override fun hashCode(): Int { + return value?.contentHashCode() ?: 0 + } + } + + @Serializable + private data class StringifiedDataClass( + @AvroFixed(10) // ignored as @AvroStringable takes precedence + @AvroStringable + val value: T, + ) + + @JvmInline + @Serializable + private value class StringifiedValueClass( + @AvroFixed(10) // ignored as @AvroStringable takes precedence + @AvroStringable + val value: T, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt new file mode 100644 index 00000000..8b0d8c40 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt @@ -0,0 +1,105 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable +import org.apache.avro.Schema + +internal class BytesEncodingTest : StringSpec({ + "encode/decode nullable ByteArray to BYTES" { + AvroAssertions.assertThat(NullableByteArrayTest(byteArrayOf(1, 4, 9))) + .isEncodedAs(record(byteArrayOf(1, 4, 9))) + AvroAssertions.assertThat(NullableByteArrayTest(null)) + .isEncodedAs(record(null)) + + AvroAssertions.assertThat(byteArrayOf(1, 4, 9)) + .generatesSchema(Schema.create(Schema.Type.BYTES).nullable) + .isEncodedAs(byteArrayOf(1, 4, 9)) + AvroAssertions.assertThat(null) + .generatesSchema(Schema.create(Schema.Type.BYTES).nullable) + .isEncodedAs(null) + } + + "encode/decode ByteArray to BYTES" { + AvroAssertions.assertThat(ByteArrayTest(byteArrayOf(1, 4, 9))) + .isEncodedAs(record(byteArrayOf(1, 4, 9))) + + AvroAssertions.assertThat() + .generatesSchema(Schema.create(Schema.Type.BYTES)) + AvroAssertions.assertThat(byteArrayOf(1, 4, 9)) + .isEncodedAs(byteArrayOf(1, 4, 9)) + } + + "encode/decode List to ARRAY[INT]" { + AvroAssertions.assertThat(ListByteTest(listOf(1, 4, 9))) + .isEncodedAs(record(listOf(1, 4, 9))) + + AvroAssertions.assertThat>() + .generatesSchema(Schema.createArray(Schema.create(Schema.Type.INT))) + AvroAssertions.assertThat(listOf(1, 4, 9)) + .isEncodedAs(listOf(1, 4, 9)) + } + + "encode/decode Array to ARRAY[INT]" { + AvroAssertions.assertThat(ArrayByteTest(arrayOf(1, 4, 9))) + .isEncodedAs(record(listOf(1, 4, 9))) + + AvroAssertions.assertThat>() + .generatesSchema(Schema.createArray(Schema.create(Schema.Type.INT))) + AvroAssertions.assertThat(arrayOf(1, 4, 9)) + .isEncodedAs(listOf(1, 4, 9)) + } +}) { + @Serializable + private data class ByteArrayTest(val z: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayTest + + return z.contentEquals(other.z) + } + + override fun hashCode(): Int { + return z.contentHashCode() + } + } + + @Serializable + private data class NullableByteArrayTest(val z: ByteArray?) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NullableByteArrayTest + + return z.contentEquals(other.z) + } + + override fun hashCode(): Int { + return z.contentHashCode() + } + } + + @Serializable + private data class ArrayByteTest(val z: Array) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ArrayByteTest + + return z.contentEquals(other.z) + } + + override fun hashCode(): Int { + return z.contentHashCode() + } + } + + @Serializable + private data class ListByteTest(val z: List) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt new file mode 100644 index 00000000..a42ad64f --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt @@ -0,0 +1,119 @@ +@file:UseSerializers(UUIDSerializer::class) + +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroEnumDefault +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.schema +import com.github.avrokotlin.avro4k.serializer.UUIDSerializer +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import org.apache.avro.generic.GenericData + +internal class EnumEncodingTest : StringSpec({ + + "read / write enums" { + AvroAssertions.assertThat(EnumTest(Cream.Bruce, BBM.Moore)) + .isEncodedAs(record(GenericData.EnumSymbol(Avro.schema(), "Bruce"), GenericData.EnumSymbol(Avro.schema(), "Moore"))) + + AvroAssertions.assertThat(Cream.Bruce) + .isEncodedAs(GenericData.EnumSymbol(Avro.schema(), "Bruce")) + AvroAssertions.assertThat(CreamValueClass(Cream.Bruce)) + .isEncodedAs(GenericData.EnumSymbol(Avro.schema(), "Bruce")) + } + + "read / write list of enums" { + AvroAssertions.assertThat(EnumListTest(listOf(Cream.Bruce, Cream.Clapton))) + .isEncodedAs(record(listOf(GenericData.EnumSymbol(Avro.schema(), "Bruce"), GenericData.EnumSymbol(Avro.schema(), "Clapton")))) + + AvroAssertions.assertThat(listOf(Cream.Bruce, Cream.Clapton)) + .isEncodedAs(listOf(GenericData.EnumSymbol(Avro.schema(), "Bruce"), GenericData.EnumSymbol(Avro.schema(), "Clapton"))) + AvroAssertions.assertThat(listOf(CreamValueClass(Cream.Bruce), CreamValueClass(Cream.Clapton))) + .isEncodedAs(listOf(GenericData.EnumSymbol(Avro.schema(), "Bruce"), GenericData.EnumSymbol(Avro.schema(), "Clapton"))) + } + + "read / write nullable enums" { + AvroAssertions.assertThat(NullableEnumTest(null)) + .isEncodedAs(record(null)) + AvroAssertions.assertThat(NullableEnumTest(Cream.Bruce)) + .isEncodedAs(record(GenericData.EnumSymbol(Avro.schema(), "Bruce"))) + + AvroAssertions.assertThat(Cream.Bruce) + .isEncodedAs(GenericData.EnumSymbol(Avro.schema(), "Bruce")) + AvroAssertions.assertThat(null) + .isEncodedAs(null) + + AvroAssertions.assertThat(CreamValueClass(Cream.Bruce)) + .isEncodedAs(GenericData.EnumSymbol(Avro.schema(), "Bruce")) + AvroAssertions.assertThat(null) + .isEncodedAs(null) + } + + "Decoding enum with an unknown uses @AvroEnumDefault value" { + AvroAssertions.assertThat(EnumV2WrapperRecord(EnumV2.B)) + .isEncodedAs(record(GenericData.EnumSymbol(Avro.schema(), "B"))) + .isDecodedAs(EnumV1WrapperRecord(EnumV1.UNKNOWN)) + + AvroAssertions.assertThat(EnumV2.B) + .isEncodedAs(GenericData.EnumSymbol(Avro.schema(), "B")) + .isDecodedAs(EnumV1.UNKNOWN) + } +}) { + @Serializable + @SerialName("EnumWrapper") + private data class EnumV1WrapperRecord( + val value: EnumV1, + ) + + @Serializable + @SerialName("EnumWrapper") + private data class EnumV2WrapperRecord( + val value: EnumV2, + ) + + @Serializable + @SerialName("Enum") + private enum class EnumV1 { + @AvroEnumDefault + UNKNOWN, + A, + } + + @Serializable + @SerialName("Enum") + private enum class EnumV2 { + @AvroEnumDefault + UNKNOWN, + A, + B, + } + + @Serializable + private data class EnumTest(val a: Cream, val b: BBM) + + @JvmInline + @Serializable + private value class CreamValueClass(val a: Cream) + + @Serializable + private data class EnumListTest(val a: List) + + @Serializable + private data class NullableEnumTest(val a: Cream?) + + private enum class Cream { + Bruce, + Baker, + Clapton, + } + + private enum class BBM { + Bruce, + Baker, + Moore, + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt new file mode 100644 index 00000000..1e60f0d8 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt @@ -0,0 +1,210 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecimal +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.AvroStringable +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.schema +import com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.apache.avro.Conversions +import org.apache.avro.SchemaBuilder +import java.math.BigDecimal +import java.math.BigInteger +import java.net.URL +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.util.UUID +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +internal class LogicalTypesEncodingTest : StringSpec({ + "support logical types at root level" { + val schema = Avro.schema().fields[0].schema() + AvroAssertions.assertThat(BigDecimal("123.45")) + .isEncodedAs( + Conversions.DecimalConversion().toBytes( + BigDecimal("123.45"), + null, + org.apache.avro.LogicalTypes.decimal(8, 2) + ), + writerSchema = schema + ) + } + + "support non-nullable logical types" { + println(Avro.schema()) + AvroAssertions.assertThat( + LogicalTypes( + BigDecimal("123.45"), + BigDecimal("123.45"), + BigDecimal("123.45"), + LocalDate.ofEpochDay(18262), + LocalTime.ofSecondOfDay(45296), + Instant.ofEpochSecond(1577889296), + Instant.ofEpochSecond(1577889296, 424000), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), + URL("http://example.com"), + BigInteger("1234567890"), + LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC), + 36.hours + 24456.seconds, + java.time.Period.of(12, 3, 4), + (36.hours + 24456.seconds).toJavaDuration() + ) + ) + .isEncodedAs( + record( + Conversions.DecimalConversion().toBytes( + BigDecimal("123.45"), + null, + org.apache.avro.LogicalTypes.decimal(8, 2) + ), + Conversions.DecimalConversion().toFixed( + BigDecimal("123.45"), + SchemaBuilder.fixed("decimalFixed").size(42), + org.apache.avro.LogicalTypes.decimal(8, 2) + ), + "123.45", + 18262, + 45296000, + 1577889296000, + 1577889296000424, + "123e4567-e89b-12d3-a456-426614174000", + "http://example.com", + "1234567890", + 1577889296424, + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4), + byteArrayOf(-109, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0), + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4) + ) + ) + } + + "support nullable logical types" { + AvroAssertions.assertThat( + NullableLogicalTypes( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ) + ) + .isEncodedAs( + record( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ) + ) + AvroAssertions.assertThat( + NullableLogicalTypes( + BigDecimal("123.45"), + BigDecimal("123.45"), + BigDecimal("123.45"), + LocalDate.ofEpochDay(18262), + LocalTime.ofSecondOfDay(45296), + Instant.ofEpochSecond(1577889296), + Instant.ofEpochSecond(1577889296, 424000), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), + URL("http://example.com"), + BigInteger("1234567890"), + LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC), + 36.hours + 24456.seconds, + java.time.Period.of(12, 3, 4), + (36.hours + 24456.seconds).toJavaDuration() + ) + ) + .isEncodedAs( + record( + Conversions.DecimalConversion().toBytes( + BigDecimal("123.45"), + null, + org.apache.avro.LogicalTypes.decimal(8, 2) + ), + Conversions.DecimalConversion().toFixed( + BigDecimal("123.45"), + SchemaBuilder.fixed("com.github.avrokotlin.avro4k.encoding.LogicalTypesEncodingTest.decimalFixedNullable").size(42), + org.apache.avro.LogicalTypes.decimal(8, 2) + ), + "123.45", + 18262, + 45296000, + 1577889296000, + 1577889296000424, + "123e4567-e89b-12d3-a456-426614174000", + "http://example.com", + "1234567890", + 1577889296424, + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4), + byteArrayOf(-109, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0), + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4) + ) + ) + } +}) { + @Serializable + @SerialName("LogicalTypes") + private data class LogicalTypes( + @Contextual @AvroDecimal(scale = 2, precision = 8) val decimalBytes: BigDecimal, + @Contextual @AvroDecimal(scale = 2, precision = 8) @AvroFixed(42) val decimalFixed: BigDecimal, + @Contextual @AvroStringable val decimalString: BigDecimal, + @Contextual val date: LocalDate, + @Contextual val time: LocalTime, + @Contextual val instant: Instant, + @Serializable(InstantToMicroSerializer::class) val instantMicros: Instant, + @Contextual val uuid: UUID, + @Contextual val url: URL, + @Contextual val bigInteger: BigInteger, + @Contextual val dateTime: LocalDateTime, + val kotlinDuration: kotlin.time.Duration, + @Contextual val period: java.time.Period, + @Contextual val javaDuration: java.time.Duration, + ) + + @Serializable + private data class NullableLogicalTypes( + @Contextual @AvroDecimal(scale = 2, precision = 8) val decimalBytesNullable: BigDecimal?, + @Contextual @AvroDecimal(scale = 2, precision = 8) @AvroFixed(42) val decimalFixedNullable: BigDecimal?, + @Contextual @AvroStringable val decimalStringNullable: BigDecimal?, + @Contextual val dateNullable: LocalDate?, + @Contextual val timeNullable: LocalTime?, + @Contextual val instantNullable: Instant?, + @Serializable(InstantToMicroSerializer::class) val instantMicrosNullable: Instant?, + @Contextual val uuidNullable: UUID?, + @Contextual val urlNullable: URL?, + @Contextual val bigIntegerNullable: BigInteger?, + @Contextual val dateTimeNullable: LocalDateTime?, + val kotlinDuration: kotlin.time.Duration?, + @Contextual val period: java.time.Period?, + @Contextual val javaDuration: java.time.Duration?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/MapEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/MapEncodingTest.kt new file mode 100644 index 00000000..8d0de9af --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/MapEncodingTest.kt @@ -0,0 +1,293 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.SomeEnum +import com.github.avrokotlin.avro4k.WrappedBoolean +import com.github.avrokotlin.avro4k.WrappedByte +import com.github.avrokotlin.avro4k.WrappedChar +import com.github.avrokotlin.avro4k.WrappedDouble +import com.github.avrokotlin.avro4k.WrappedEnum +import com.github.avrokotlin.avro4k.WrappedFloat +import com.github.avrokotlin.avro4k.WrappedInt +import com.github.avrokotlin.avro4k.WrappedLong +import com.github.avrokotlin.avro4k.WrappedShort +import com.github.avrokotlin.avro4k.WrappedString +import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.Contextual +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.serializersModuleOf +import kotlinx.serialization.serializer +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder +import kotlin.io.path.Path + +@OptIn(InternalSerializationApi::class) +internal class MapEncodingTest : FunSpec({ + test("should support map in the middle of other fields") { + @Serializable + data class MyRecord(val n: String, val map: Map, val z: String) + + AvroAssertions.assertThat(MyRecord("a", mapOf("a" to 12, "z" to 42), "end")) + .isEncodedAs( + record( + "a", + mapOf("a" to 12, "z" to 42), + "end" + ) + ) + } + test("generate map type for a Map of ints") { + val map = mapOf("a" to 1, "b" to 20, "c" to 5) + AvroAssertions.assertThat(StringIntTest(map)) + .isEncodedAs(record(map)) + AvroAssertions.assertThat() + .generatesSchema(Path("/map_int.json")) + } + + test("generate map type for a Map of records") { + AvroAssertions.assertThat() + .generatesSchema(Path("/map_record.json")) + AvroAssertions.assertThat(StringNestedTest(mapOf("a" to Nested("goo")))) + .isEncodedAs(record(mapOf("a" to record("goo")))) + } + + test("generate map type for map of nullable booleans") { + val map = mapOf("a" to null, "b" to true, "c" to false) + AvroAssertions.assertThat() + .generatesSchema(Path("/map_boolean_null.json")) + AvroAssertions.assertThat(StringBooleanTest(map)) + .isEncodedAs(record(map)) + } + + test("support maps of sets of records") { + AvroAssertions.assertThat() + .generatesSchema(Path("/map_set_nested.json")) + AvroAssertions.assertThat(StringSetNestedTest(mapOf("a" to setOf(Nested("goo"))))) + .isEncodedAs( + record(mapOf("a" to listOf(record("goo")))) + ) + } + + test("support array of maps") { + AvroAssertions.assertThat() + .generatesSchema(Path("/array_of_maps.json")) + AvroAssertions.assertThat(ArrayTest(arrayOf(mapOf("a" to "b")))) + .isEncodedAs(record(listOf(mapOf("a" to "b")))) + } + + test("support array of maps where the key is a value class") { + AvroAssertions.assertThat() + .generatesSchema(Path("/array_of_maps.json")) + AvroAssertions.assertThat(WrappedStringArrayTest(arrayOf(mapOf(WrappedString("a") to "b")))) + .isEncodedAs(record(listOf(mapOf("a" to "b")))) + } + + test("support lists of maps") { + AvroAssertions.assertThat() + .generatesSchema(Path("/list_of_maps.json")) + AvroAssertions.assertThat(ListTest(listOf(mapOf("a" to "b")))) + .isEncodedAs(record(listOf(mapOf("a" to "b")))) + } + + test("support sets of maps") { + AvroAssertions.assertThat() + .generatesSchema(Path("/set_of_maps.json")) + AvroAssertions.assertThat(SetTest(setOf(mapOf("a" to "b")))) + .isEncodedAs(record(listOf(mapOf("a" to "b")))) + } + + test("support data class of list of data class with maps") { + AvroAssertions.assertThat() + .generatesSchema(Path("/class_of_list_of_maps.json")) + AvroAssertions.assertThat(List2Test(listOf(mapOf("a" to "b")))) + .isEncodedAs( + record(listOf(mapOf("a" to "b"))) + ) + } + + test("support maps with contextual keys") { + AvroAssertions.assertThat(ContextualKeyTests(mapOf(NonSerializableKey("a") to 12))) + .withConfig { + serializersModule = serializersModuleOf(NonSerializableKeyKSerializer) + } + .generatesSchema( + SchemaBuilder.record("ContextualKeyTests").fields() + .name("map").type(Schema.createMap(Schema.create(Schema.Type.INT))).withDefault(mapOf()) + .endRecord() + ) + .isEncodedAs(record(mapOf("a" to 12))) + } + + listOf( + true, + 1.toByte(), + 1.toShort(), + 1, + 1.toLong(), + 1.toFloat(), + 1.toDouble(), + 1.toString(), + 'a', + SomeEnum.B + ).forEach { keyValue -> + test("handle string-able key type: ${keyValue::class.simpleName}") { + AvroAssertions.assertThat(GenericMapForTests(mapOf(keyValue to "something")), GenericMapForTests.serializer(keyValue::class.serializer())) + .generatesSchema( + SchemaBuilder.record("mapStringStringTest").fields() + .name("map").type(Schema.createMap(Schema.create(Schema.Type.STRING))).withDefault(mapOf()) + .endRecord() + ) + .isEncodedAs(record(mapOf(keyValue.toString() to "something"))) + } + } + + listOf( + WrappedBoolean(true) to true, + WrappedInt(1) to 1, + WrappedByte(1.toByte()) to 1.toByte(), + WrappedShort(1.toShort()) to 1.toShort(), + WrappedLong(1.toLong()) to 1.toLong(), + WrappedFloat(1.toFloat()) to 1.toFloat(), + WrappedDouble(1.toDouble()) to 1.toDouble(), + WrappedString(1.toString()) to 1.toString(), + WrappedEnum(SomeEnum.B) to "B", + WrappedChar('a') to "a" + ).forEach { (keyValue, avroValue) -> + test("handle string-able key type inside a value class: ${keyValue::class.simpleName}") { + AvroAssertions.assertThat(GenericMapForTests(mapOf(keyValue to "something")), GenericMapForTests.serializer(keyValue::class.serializer())) + .generatesSchema( + SchemaBuilder.record("mapStringStringTest").fields() + .name("map").type(Schema.createMap(Schema.create(Schema.Type.STRING))).withDefault(mapOf()) + .endRecord() + ) + .isEncodedAs(record(mapOf(avroValue to "something"))) + } + } + + test("should fail on nullable key") { + shouldThrow { + Avro.schema(GenericMapForTests.serializer(String.serializer().nullable)) + } + } + test("should fail on non-stringable key type: record") { + shouldThrow { + Avro.schema(GenericMapForTests.serializer(DataRecord.serializer())) + } + } + test("should fail on non-stringable key type: map") { + shouldThrow { + Avro.schema(GenericMapForTests.serializer(MapSerializer(String.serializer(), String.serializer()))) + } + } + test("should fail on non-stringable key type: array") { + shouldThrow { + Avro.schema(GenericMapForTests.serializer(ListSerializer(String.serializer()))) + } + } + test("should fail on non-stringable key type: bytes") { + shouldThrow { + Avro.schema(GenericMapForTests.serializer(ListSerializer(Byte.serializer()))) + } + } +}) { + @Serializable + @SerialName("mapStringStringTest") + private data class GenericMapForTests(val map: Map) + + @Serializable + @SerialName("ContextualKeyTests") + private data class ContextualKeyTests(val map: Map<@Contextual NonSerializableKey, Int>) + + private data class NonSerializableKey(val key: String) + + private object NonSerializableKeyKSerializer : KSerializer { + @OptIn(InternalSerializationApi::class) + override val descriptor = buildSerialDescriptor("NonSerializableKey", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder) = NonSerializableKey(decoder.decodeString()) + + override fun serialize( + encoder: Encoder, + value: NonSerializableKey, + ) { + encoder.encodeString(value.key) + } + } + + @Serializable + private data class DataRecord(val field: Int) + + @Serializable + private data class StringIntTest(val map: Map) + + @Serializable + private data class Nested(val goo: String) + + @Serializable + private data class StringNestedTest(val map: Map) + + @Serializable + private data class StringBooleanTest(val map: Map) + + @Serializable + private data class StringSetNestedTest(val map: Map>) + + @Serializable + @SerialName("arrayOfMapStringString") + private data class ArrayTest(val array: Array>) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ArrayTest + + return array.contentEquals(other.array) + } + + override fun hashCode(): Int { + return array.contentHashCode() + } + } + + @Serializable + @SerialName("arrayOfMapStringString") + private data class WrappedStringArrayTest(val array: Array>) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WrappedStringArrayTest + + return array.contentEquals(other.array) + } + + override fun hashCode(): Int { + return array.contentHashCode() + } + } + + @Serializable + private data class ListTest(val list: List>) + + @Serializable + private data class SetTest(val set: Set>) + + @Serializable + private data class List2Test(val ship: List>) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/NestedClassEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/NestedClassEncodingTest.kt new file mode 100644 index 00000000..aaa332c2 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/NestedClassEncodingTest.kt @@ -0,0 +1,73 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable + +internal class NestedClassEncodingTest : StringSpec({ + "decode nested class" { + AvroAssertions.assertThat(Birthplace(person = "Sammy Sam", town = Town(name = "Hardwick", population = 123))) + .isEncodedAs( + record( + "Sammy Sam", + record("Hardwick", 123) + ) + ) + } + + "decode nested list of classes" { + AvroAssertions.assertThat( + County( + name = "Bucks", + towns = listOf(Town(name = "Hardwick", population = 123), Town(name = "Weedon", population = 225)), + ceremonial = true, + lat = 12.34, + long = 0.123 + ) + ).isEncodedAs( + record( + "Bucks", + listOf( + record("Hardwick", 123), + record("Weedon", 225) + ), + true, + 12.34, + 0.123 + ) + ) + } + + "decode nested class from previous schema (new schema is back compat)" { + AvroAssertions.assertThat( + PersonV2(name = "Ryan", hasChickenPoxVaccine = true, hasCovidVaccine = null) + ).isEncodedAs( + record("Ryan", true, null) + ) + } +}) { + @Serializable + private data class County( + val name: String, + val towns: List, + val ceremonial: Boolean, + val lat: Double, + val long: Double, + ) + + @Serializable + private data class Town(val name: String, val population: Int) + + @Serializable + private data class Birthplace(val person: String, val town: Town) + + @Serializable + private data class PersonV2( + val name: String, + val hasChickenPoxVaccine: Boolean, + @AvroDefault("null") + val hasCovidVaccine: Boolean? = null, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/PrimitiveEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/PrimitiveEncodingTest.kt new file mode 100644 index 00000000..d98295ca --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/PrimitiveEncodingTest.kt @@ -0,0 +1,155 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.WrappedBoolean +import com.github.avrokotlin.avro4k.WrappedByte +import com.github.avrokotlin.avro4k.WrappedChar +import com.github.avrokotlin.avro4k.WrappedDouble +import com.github.avrokotlin.avro4k.WrappedFloat +import com.github.avrokotlin.avro4k.WrappedInt +import com.github.avrokotlin.avro4k.WrappedLong +import com.github.avrokotlin.avro4k.WrappedShort +import com.github.avrokotlin.avro4k.WrappedString +import com.github.avrokotlin.avro4k.record +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable +import java.nio.ByteBuffer + +internal class PrimitiveEncodingTest : StringSpec({ + "read write out booleans" { + AvroAssertions.assertThat(BooleanTest(true)) + .isEncodedAs(record(true)) + AvroAssertions.assertThat(BooleanTest(false)) + .isEncodedAs(record(false)) + AvroAssertions.assertThat(true) + .isEncodedAs(true) + AvroAssertions.assertThat(false) + .isEncodedAs(false) + AvroAssertions.assertThat(WrappedBoolean(true)) + .isEncodedAs(true) + AvroAssertions.assertThat(WrappedBoolean(false)) + .isEncodedAs(false) + } + + "read write out bytes" { + AvroAssertions.assertThat(ByteTest(3)) + .isEncodedAs(record(3)) + AvroAssertions.assertThat(3.toByte()) + .isEncodedAs(3) + AvroAssertions.assertThat(WrappedByte(3)) + .isEncodedAs(3) + } + + "read write out shorts" { + AvroAssertions.assertThat(ShortTest(3)) + .isEncodedAs(record(3)) + AvroAssertions.assertThat(3.toShort()) + .isEncodedAs(3) + AvroAssertions.assertThat(WrappedShort(3)) + .isEncodedAs(3) + } + + "read write out chars" { + AvroAssertions.assertThat(CharTest('A')) + .isEncodedAs(record('A'.code)) + AvroAssertions.assertThat('A') + .isEncodedAs('A'.code) + AvroAssertions.assertThat(WrappedChar('A')) + .isEncodedAs('A'.code) + } + + "read write out strings" { + AvroAssertions.assertThat(StringTest("Hello world")) + .isEncodedAs(record("Hello world")) + AvroAssertions.assertThat("Hello world") + .isEncodedAs("Hello world") + AvroAssertions.assertThat(WrappedString("Hello world")) + .isEncodedAs("Hello world") + } + + "read write out longs" { + AvroAssertions.assertThat(LongTest(65653L)) + .isEncodedAs(record(65653L)) + AvroAssertions.assertThat(65653L) + .isEncodedAs(65653L) + AvroAssertions.assertThat(WrappedLong(65653)) + .isEncodedAs(65653L) + } + + "read write out ints" { + AvroAssertions.assertThat(IntTest(44)) + .isEncodedAs(record(44)) + AvroAssertions.assertThat(44) + .isEncodedAs(44) + AvroAssertions.assertThat(WrappedInt(44)) + .isEncodedAs(44) + } + + "read write out doubles" { + AvroAssertions.assertThat(DoubleTest(3.235)) + .isEncodedAs(record(3.235)) + AvroAssertions.assertThat(3.235) + .isEncodedAs(3.235) + AvroAssertions.assertThat(WrappedDouble(3.235)) + .isEncodedAs(3.235) + } + + "read write out floats" { + AvroAssertions.assertThat(FloatTest(3.4F)) + .isEncodedAs(record(3.4F)) + AvroAssertions.assertThat(3.4F) + .isEncodedAs(3.4F) + AvroAssertions.assertThat(WrappedFloat(3.4F)) + .isEncodedAs(3.4F) + } + + "read write out byte arrays" { + AvroAssertions.assertThat(ByteArrayTest("ABC".toByteArray())) + .isEncodedAs(record(ByteBuffer.wrap("ABC".toByteArray()))) + AvroAssertions.assertThat("ABC".toByteArray()) + .isEncodedAs(ByteBuffer.wrap("ABC".toByteArray())) + } +}) { + @Serializable + data class BooleanTest(val z: Boolean) + + @Serializable + data class ByteTest(val z: Byte) + + @Serializable + data class ShortTest(val z: Short) + + @Serializable + data class CharTest(val z: Char) + + @Serializable + data class StringTest(val z: String) + + @Serializable + data class FloatTest(val z: Float) + + @Serializable + data class DoubleTest(val z: Double) + + @Serializable + data class IntTest(val z: Int) + + @Serializable + data class LongTest(val z: Long) + + @Serializable + data class ByteArrayTest(val z: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayTest + + return z.contentEquals(other.z) + } + + override fun hashCode(): Int { + return z.contentHashCode() + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt new file mode 100644 index 00000000..f07816b2 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt @@ -0,0 +1,246 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder + +internal class RecordEncodingTest : StringSpec({ + "encoding basic data class" { + val input = + Foo( + "string value", + 2.2, + true, + S(setOf(1, 2, 3)), + vc = ValueClass(ByteArray(3) { it.toByte() }) + ) + + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("Foo").fields() + .name("a").type().stringType().noDefault() + .name("b").type().doubleType().noDefault() + .name("c").type(Schema.create(Schema.Type.BOOLEAN).nullable).withDefault(null) + .name("s").type( + SchemaBuilder.record("S").fields() + .name("t").type().array().items().intType().arrayDefault(emptyList()) + .endRecord() + ).noDefault() + .name("optionalField").type().intType().noDefault() + .name("vc").type().bytesType().noDefault() + .endRecord() + ) + AvroAssertions.assertThat(input) + .isEncodedAs( + record( + "string value", + 2.2, + true, + record(setOf(1, 2, 3)), + 42, + ByteArray(3) { it.toByte() } + ) + ) + } + "encode/decode strings as UTF8" { + AvroAssertions.assertThat(StringFoo("hello")) + .isEncodedAs(record("hello")) + } + "encode/decode nullable string" { + @Serializable + data class NullableString(val a: String?) + + AvroAssertions.assertThat(NullableString("hello")) + .isEncodedAs(record("hello")) + AvroAssertions.assertThat(NullableString(null)) + .isEncodedAs(record(null)) + } + "encode/decode longs" { + @Serializable + data class LongFoo(val l: Long) + AvroAssertions.assertThat(LongFoo(123456L)) + .isEncodedAs(record(123456L)) + } + "encode/decode doubles" { + @Serializable + data class DoubleFoo(val d: Double) + AvroAssertions.assertThat(DoubleFoo(123.435)) + .isEncodedAs(record(123.435)) + } + "encode/decode booleans" { + @Serializable + data class BooleanFoo(val d: Boolean) + AvroAssertions.assertThat(BooleanFoo(false)) + .isEncodedAs(record(false)) + } + "encode/decode nullable booleans" { + @Serializable + data class NullableBoolean(val a: Boolean?) + + AvroAssertions.assertThat(NullableBoolean(true)) + .isEncodedAs(record(true)) + AvroAssertions.assertThat(NullableBoolean(null)) + .isEncodedAs(record(null)) + } + "encode/decode floats" { + @Serializable + data class FloatFoo(val d: Float) + AvroAssertions.assertThat(FloatFoo(123.435F)) + .isEncodedAs(record(123.435F)) + } + "encode/decode ints" { + @Serializable + data class IntFoo(val i: Int) + AvroAssertions.assertThat(IntFoo(123)) + .isEncodedAs(record(123)) + } + "encode/decode shorts" { + @Serializable + data class ShortFoo(val s: Short) + AvroAssertions.assertThat(ShortFoo(123)) + .isEncodedAs(record(123)) + } + "encode/decode bytes" { + @Serializable + data class ByteFoo(val b: Byte) + AvroAssertions.assertThat(ByteFoo(123)) + .isEncodedAs(record(123)) + } + "should not encode records with a different name" { + @Serializable + data class TheRecord(val v: Int) + shouldThrow { + val wrongRecordSchema = SchemaBuilder.record("AnotherRecord").fields().name("v").type().intType().noDefault().endRecord() + AvroAssertions.assertThat(TheRecord(1)) + .isEncodedAs(record(1), writerSchema = wrongRecordSchema) + } + } + "support objects" { + AvroAssertions.assertThat(ObjectClass) + .isEncodedAs(record()) + } + "support records with different field positions" { + val input = + Foo( + "string value", + 2.2, + true, + S(setOf(1, 2, 3)), + vc = ValueClass(ByteArray(3) { it.toByte() }) + ) + + val differentWriterSchema = + SchemaBuilder.record("Foo").fields() + .name("b").type().doubleType().noDefault() + .name("a").type().stringType().noDefault() + .name("c").type().nullable().booleanType().noDefault() + .name("s").type( + SchemaBuilder.record("S").fields() + .name("t").type().array().items().intType().noDefault() + .endRecord() + ).noDefault() + .name("vc").type().bytesType().noDefault() + .endRecord() + + AvroAssertions.assertThat(input) + .isEncodedAs( + record( + 2.2, + "string value", + true, + record(setOf(1, 2, 3)), + ByteArray(3) { it.toByte() } + ), + writerSchema = differentWriterSchema + ) + } + "support written records where some fields are missing from kotlin data class when decoding" { + @Serializable + @SerialName("Foo") + data class MissingFields( + val c: Boolean?, + ) + + val input = + Foo( + "string value", + 2.2, + true, + S(setOf(1, 2, 3)), + vc = ValueClass(ByteArray(3) { it.toByte() }) + ) + + AvroAssertions.assertThat(input) + .isDecodedAs(MissingFields(true)) + } + "should fail when trying to write a data class but missing the last schema field" { + @Serializable + @SerialName("Base") + data class Base( + val c: Boolean?, + val a: String, + ) + + @Serializable + @SerialName("Base") + data class Incomplete( + val c: Boolean?, + ) + + shouldThrow { + Avro.encodeToByteArray(Avro.schema(), Incomplete(true)) + } + } +}) { + @Serializable + private object ObjectClass { + val field1 = "ignored" + } + + @Serializable + private data class StringFoo(val s: String) + + @Serializable + @SerialName("Foo") + private data class Foo(val a: String, val b: Double, val c: Boolean?, val s: S, val optionalField: Int = 42, val vc: ValueClass) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Foo) return false + + if (a != other.a) return false + if (b != other.b) return false + if (c != other.c) return false + if (s != other.s) return false + if (!vc.value.contentEquals(other.vc.value)) return false + + return true + } + + override fun hashCode(): Int { + var result = a.hashCode() + result = 31 * result + b.hashCode() + result = 31 * result + c.hashCode() + result = 31 * result + s.hashCode() + result = 31 * result + vc.value.contentHashCode() + return result + } + } + + @Serializable + @SerialName("S") + private data class S(val t: Set) + + @JvmInline + @Serializable + private value class ValueClass(val value: ByteArray) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/SealedClassEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/SealedClassEncodingTest.kt new file mode 100644 index 00000000..83c20bb2 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/SealedClassEncodingTest.kt @@ -0,0 +1,65 @@ +package com.github.avrokotlin.avro4k.encoding + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.record +import com.github.avrokotlin.avro4k.recordWithSchema +import com.github.avrokotlin.avro4k.schema +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable + +internal class SealedClassEncodingTest : StringSpec({ + "encode/decode sealed classes" { + AvroAssertions.assertThat(ReferencingSealedClass(Operation.Binary.Add(1, 2))) + .isEncodedAs(record(recordWithSchema(Avro.schema(), 1, 2))) + AvroAssertions.assertThat(Operation.Binary.Add(1, 2)) + .isEncodedAs(recordWithSchema(Avro.schema(), 1, 2)) + } + "encode/decode nullable sealed classes" { + AvroAssertions.assertThat(ReferencingNullableSealedClass(Operation.Binary.Add(1, 2))) + .isEncodedAs(record(recordWithSchema(Avro.schema(), 1, 2))) + AvroAssertions.assertThat(ReferencingNullableSealedClass(null)) + .isEncodedAs(record(null)) + + AvroAssertions.assertThat(Operation.Binary.Add(1, 2)) + .isEncodedAs(recordWithSchema(Avro.schema(), 1, 2)) + AvroAssertions.assertThat(null) + .isEncodedAs(null) + } +}) { + @Serializable + private data class ReferencingSealedClass( + val notNullable: Operation, + ) + + @Serializable + private data class ReferencingNullableSealedClass( + val nullable: Operation?, + ) + + @Serializable + private sealed interface Operation { + @Serializable + object Nullary : Operation + + @Serializable + sealed class Unary : Operation { + abstract val value: Int + + @Serializable + data class Negate(override val value: Int) : Unary() + } + + @Serializable + sealed class Binary : Operation { + abstract val left: Int + abstract val right: Int + + @Serializable + data class Add(override val left: Int, override val right: Int) : Binary() + + @Serializable + data class Substract(override val left: Int, override val right: Int) : Binary() + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ArrayEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ArrayEncoderTest.kt deleted file mode 100644 index 12b75a05..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ArrayEncoderTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.WordSpec -import io.kotest.core.spec.style.wordSpec -import kotlinx.serialization.Serializable - -class ArrayEncoderTest : WordSpec({ - includeForEveryEncoder { arrayEncodingTests(it) } -}) - -@Suppress("ArrayInDataClass") -fun arrayEncodingTests(encoderToTest: EnDecoder): TestFactory { - return wordSpec { - "en-/decoder" should { - "generate GenericData.Array for an Array" { - @Serializable - data class ArrayBooleanTest(val a: Array) - - val value = ArrayBooleanTest(arrayOf(true, false, true)) - encoderToTest.testEncodeDecode(value, record(value.a.asList())) - } - "generate GenericData.Array for an Array" { - @Serializable - data class ArrayBooleanTest(val a: Array) - - val value = ArrayBooleanTest(arrayOf(true, null, false)) - encoderToTest.testEncodeDecode(value, record(value.a.asList())) - } - "support GenericData.Array for an Array with other fields" { - @Serializable - data class ArrayBooleanWithOthersTest(val a: String, val b: Array, val c: Long) - - val value = ArrayBooleanWithOthersTest("foo", arrayOf(true, false, true), 123L) - encoderToTest.testEncodeDecode( - value, record( - "foo", - listOf(true, false, true), - 123L - ) - ) - } - - "generate GenericData.Array for a List" { - @Serializable - data class ListStringTest(val a: List) - encoderToTest.testEncodeDecode( - ListStringTest(listOf("we23", "54z")), record( - listOf("we23", "54z") - ) - ) - } - "generate GenericData.Array for a Set" { - @Serializable - data class SetLongTest(val a: Set) - - val value = SetLongTest(setOf(123L, 643L, 912L)) - val record = record(listOf(123L, 643L, 912)) - encoderToTest.testEncodeDecode(value, record) - } - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroInlineEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroInlineEncoderTest.kt deleted file mode 100644 index 30da47fe..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroInlineEncoderTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.AvroInline -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.Serializable - -class AvroInlineEncoderTest : FunSpec({ - includeForEveryEncoder { - inlineEncodingTests(it) - } -}) - -fun inlineEncodingTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "encode/decode @AvroInline" { - @Serializable - @AvroInline - data class Name(val value: String) - - @Serializable - data class Product(val id: String, val name: Name) - encoderToTest.testEncodeDecode( - Product("123", Name("sneakers")), - record("123", "sneakers") - ) - } - } -} - diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt deleted file mode 100644 index 2e0c4509..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/AvroNameEncoderTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -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.Serializable - -class AvroNameEncoderTest : FunSpec({ - includeForEveryEncoder { avroNameEncodingTests(it) } -}) - -fun avroNameEncodingTests(endecoder: EnDecoder): TestFactory { - return stringSpec { - "take into account @AvroName on fields" { - @Serializable - data class Foo(@AvroName("bar") val foo: String) - endecoder.testEncodeDecode(Foo("hello"), record("hello")) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigDecimalEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigDecimalEncoderTest.kt deleted file mode 100644 index 7130b704..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigDecimalEncoderTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -@file:UseSerializers(BigDecimalSerializer::class) - -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.apache.avro.Conversions -import org.apache.avro.LogicalTypes -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData -import java.math.BigDecimal - -class BigDecimalEncoderTest : FunSpec({ - includeForEveryEncoder { bigDecimalEncoderTests(it) } -}) -fun bigDecimalEncoderTests(encoderToTest: EnDecoder): TestFactory { - @Serializable - data class BigDecimalTest(val decimal: BigDecimal) - - return stringSpec { - "use byte array for BigDecimal" { - val schema = encoderToTest.avro.schema(BigDecimalTest.serializer()) - val obj = BigDecimalTest(BigDecimal("12.34")) - val s = schema.getField("decimal").schema() - val bytes = Conversions.DecimalConversion().toBytes(obj.decimal, s, s.logicalType) - - encoderToTest.testEncodeDecode(value = obj, shouldMatch = record(bytes), schema = schema) - } - - "allow BigDecimal to be en-/decoded as strings" { - val decimalSchema = Schema.create(Schema.Type.STRING) - val schema = SchemaBuilder.record("Test").fields() - .name("decimal").type(decimalSchema).noDefault() - .endRecord() - encoderToTest.testEncodeDecode( - value = BigDecimalTest(BigDecimal("123.456")), - shouldMatch = record("123.456"), - schema = schema - ) - } - "support nullable big decimals" { - @Serializable - data class NullableBigDecimalTest(val big: BigDecimal?) - - val schema = encoderToTest.avro.schema(NullableBigDecimalTest.serializer()) - - val obj = NullableBigDecimalTest(BigDecimal("123.40")) - val bigSchema = schema.getField("big").schema().types[1] //Nullable is encoded as Union - val bytes = Conversions.DecimalConversion().toBytes(obj.big, bigSchema, bigSchema.logicalType) - encoderToTest.testEncodeDecode(obj, record(bytes)) - encoderToTest.testEncodeDecode(NullableBigDecimalTest(null), record(null)) - } - - "allow BigDecimal to be en-/decoded as generic fixed" { - //Schema needs to have the precision of 16 in order to serialize a 8 digit integer with a scale of 8 - val decimal = LogicalTypes.decimal(16, 8).addToSchema(Schema.createFixed("decimal", null, null, 8)) - - val schema = SchemaBuilder.record("Test").fields() - .name("decimal").type(decimal).noDefault() - .endRecord() - encoderToTest.testEncodeDecode( - value = BigDecimalTest(BigDecimal("12345678.00000000")), - shouldMatch = record(GenericData.Fixed(decimal, byteArrayOf(0, 4, 98, -43, 55, 43, -114, 0))), - schema = schema - ) - } - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigIntegerEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigIntegerEncoderTest.kt deleted file mode 100644 index 0596a1bc..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/BigIntegerEncoderTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -@file:UseSerializers(BigIntegerSerializer::class) - -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.serializer.BigIntegerSerializer -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.math.BigInteger - -class BigIntegerEncoderTest : FunSpec({ - includeForEveryEncoder { bigIntegerEncoderTests(it) } -}) -fun bigIntegerEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "use string for BigInteger" { - @Serializable - data class BigIntegerTest(val b: BigInteger) - - val test = BigIntegerTest(BigInteger("123123123123213213213123214325365477686789676234")) - encoderToTest.testEncodeDecode(test, record("123123123123213213213123214325365477686789676234")) - } - - "encode nullable BigInteger" { - @Serializable - data class NullableBigIntegerTest(val b: BigInteger?) - encoderToTest.testEncodeDecode( - NullableBigIntegerTest(BigInteger("12312312312321312365477686789676234")), - record("12312312312321312365477686789676234") - ) - encoderToTest.testEncodeDecode(NullableBigIntegerTest(null), record(null)) - - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ByteArrayEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ByteArrayEncoderTest.kt deleted file mode 100644 index 796219f9..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ByteArrayEncoderTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -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.Serializable -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData -import java.nio.ByteBuffer - -class ByteArrayEncoderTest : FunSpec({ - includeForEveryEncoder { byteArrayEncoderTests(it) } -}) - -fun byteArrayEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - @Serializable - data class ByteArrayTest(val z: ByteArray) - - fun avroByteArray(vararg bytes: Byte) = ByteBuffer.wrap(bytes) - "encode/decode ByteArray" { - encoderToTest.testEncodeDecode( - ByteArrayTest(byteArrayOf(1, 4, 9)), record(avroByteArray(1, 4, 9)) - ) - } - "encode/decode List" { - @Serializable - data class ListByteTest(val z: List) - encoderToTest.testEncodeDecode(ListByteTest(listOf(1, 4, 9)), record(avroByteArray(1, 4, 9))) - } - - "encode/decode Array to ByteBuffer" { - @Serializable - data class ArrayByteTest(val z: Array) - encoderToTest.testEncodeDecode(ArrayByteTest(arrayOf(1, 4, 9)), record(avroByteArray(1, 4, 9))) - } - - "encode/decode ByteArray as FIXED when schema is Type.Fixed" { - val fixedSchema = Schema.createFixed("ByteArray", null, null, 8) - val schema = - SchemaBuilder.record("ByteArrayTest").fields().name("z").type(fixedSchema).noDefault().endRecord() - val unpaddedByteArray = byteArrayOf(1, 4, 9) - val paddedByteArray = byteArrayOf(0, 0, 0, 0, 0, 1, 4, 9) - val encoded = encoderToTest.testEncodeIsEqual( - value = ByteArrayTest(unpaddedByteArray), - shouldMatch = record(GenericData.Fixed(fixedSchema, paddedByteArray)), - schema = schema - ) - encoderToTest.testDecodeIsEqual( - byteArray = encoded, value = ByteArrayTest(paddedByteArray), readSchema = schema - ) - } - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/DateEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/DateEncoderTest.kt deleted file mode 100644 index 1a4432aa..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/DateEncoderTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -@file:UseSerializers( - LocalDateTimeSerializer::class, - LocalDateSerializer::class, - LocalTimeSerializer::class, - TimestampSerializer::class, -) - -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.serializer.* -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.sql.Timestamp -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.temporal.ChronoUnit - -class DateEncoderTest : FunSpec({ - includeForEveryEncoder { dateEncoderTests(it) } -}) - -fun dateEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "encode/decode LocalTime as an Int" { - @Serializable - data class LocalTimeTest(val t: LocalTime) - encoderToTest.testEncodeDecode(LocalTimeTest(LocalTime.of(12, 50, 45)), record(46245000)) - } - - "encode/decode nullable LocalTime" { - @Serializable - data class NullableLocalTimeTest(val t: LocalTime?) - encoderToTest.testEncodeDecode(NullableLocalTimeTest(LocalTime.of(12, 50, 45)), record(46245000)) - encoderToTest.testEncodeDecode(NullableLocalTimeTest(null), record(null)) - } - - "encode/decode LocalDate as an Int" { - @Serializable - data class LocalDateTest(val d: LocalDate) - encoderToTest.testEncodeDecode(LocalDateTest(LocalDate.of(2018, 9, 10)), record(17784)) - } - - "encode/decode LocalDateTime as Long" { - @Serializable - data class LocalDateTimeTest(val dt: LocalDateTime) - encoderToTest.testEncodeDecode( - LocalDateTimeTest(LocalDateTime.of(2018, 9, 10, 11, 58, 59)), - record(1536580739000L) - ) - } - - "encode/decode Timestamp as Long" { - @Serializable - data class TimestampTest(val t: Timestamp) - encoderToTest.testEncodeDecode( - TimestampTest(Timestamp.from(Instant.ofEpochMilli(1538312231000L))), - record(1538312231000L) - ) - } - - "encode/decode nullable Timestamp as Long" { - @Serializable - data class NullableTimestampTest(val t: Timestamp?) - encoderToTest.testEncodeDecode(NullableTimestampTest(null), record(null)) - encoderToTest.testEncodeDecode( - NullableTimestampTest(Timestamp.from(Instant.ofEpochMilli(1538312231000L))), - record(1538312231000L) - ) - } - - "encode/decode Instant as Long" { - @Serializable - data class InstantMillisTest(@Serializable(with = InstantSerializer::class) val i: Instant) - encoderToTest.testEncodeDecode( - InstantMillisTest(Instant.ofEpochMilli(1538312231000L)), - record(1538312231000L) - ) - } - - "encode/decode Instant with microseconds as Long" { - @Serializable - data class InstantMicrosTest(@Serializable(with = InstantToMicroSerializer::class) val i: Instant) - - encoderToTest.testEncodeDecode( - InstantMicrosTest(Instant.ofEpochMilli(1538312231000L).plus(5, ChronoUnit.MICROS)), - record(1538312231000005L) - ) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnDecoder.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnDecoder.kt deleted file mode 100644 index a283fa52..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnDecoder.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.RecordBuilderForTest -import io.kotest.assertions.fail -import io.kotest.assertions.withClue -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.DslDrivenSpec -import io.kotest.matchers.equality.shouldBeEqualToComparingFields -import io.kotest.matchers.shouldBe -import io.kotest.mpp.newInstanceNoArgConstructorOrObjectInstance -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.serializer -import org.apache.avro.Schema -import org.apache.avro.generic.GenericDatumReader -import org.apache.avro.generic.GenericDatumWriter -import org.apache.avro.generic.GenericRecord -import org.apache.avro.io.Decoder -import org.apache.avro.io.DecoderFactory -import org.apache.avro.io.Encoder -import org.apache.avro.io.EncoderFactory -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.InputStream - - -sealed interface EnDecoder { - val name: String - var avro: Avro - fun encodeGenericRecordForComparison(value: GenericRecord, schema: Schema): ByteArray - - fun decode( - byteArray: ByteArray, - deserializer: DeserializationStrategy, - readSchema: Schema, - writeSchema: Schema - ): T - - fun encode(value: T, serializer: SerializationStrategy, schema: Schema): ByteArray -} - -class AvroLibEnDecoder : EnDecoder { - override var avro: Avro = Avro.default - override val name: String = "AvroLibrary" - override fun encode(value: T, serializer: SerializationStrategy, schema: Schema): ByteArray { - val asRecord = avro.toRecord(serializer, schema, value) - return encodeGenericRecordForComparison(asRecord, schema) - } - - override fun encodeGenericRecordForComparison(value: GenericRecord, schema: Schema): ByteArray { - val writer = GenericDatumWriter(schema) - val byteArrayOutputStream = ByteArrayOutputStream() - val encoder = avroLibEncoder(schema, byteArrayOutputStream) - writer.write(value, encoder) - encoder.flush() - return byteArrayOutputStream.toByteArray() - } - - override fun decode( - byteArray: ByteArray, - deserializer: DeserializationStrategy, - readSchema: Schema, - writeSchema: Schema - ): T { - val input = ByteArrayInputStream(byteArray) - val reader = GenericDatumReader(writeSchema, readSchema) - val genericData = reader.read(null, avroLibDecoder(writeSchema, input)) - return avro.fromRecord(deserializer,genericData) - } - - fun avroLibEncoder(schema: Schema, outputStream: ByteArrayOutputStream): Encoder = - EncoderFactory.get().jsonEncoder(schema, outputStream) - - fun avroLibDecoder(schema: Schema, inputStream: InputStream) : Decoder = - DecoderFactory.get().jsonDecoder(schema, inputStream) -} - -inline fun EnDecoder.testEncodeDecode( - value: T, - shouldMatch: RecordBuilderForTest, - serializer: KSerializer = avro.serializersModule.serializer(), - schema: Schema = avro.schema(serializer) -) { - val encoded = testEncodeIsEqual(value, shouldMatch, serializer, schema) - testDecodeIsEqual(encoded, value,serializer, schema) -} - -inline fun EnDecoder.testEncodeIsEqual( - value: T, - shouldMatch: RecordBuilderForTest, - serializer: SerializationStrategy = avro.serializersModule.serializer(), - schema: Schema = avro.schema(serializer) -) : ByteArray{ - val record = shouldMatch.createRecord(schema) - val encodedValue = encode(value, serializer, schema) - withClue("Encoded result was not equal to the encoded result of the apache avro library.") { - encodedValue shouldBe encodeGenericRecordForComparison(record, schema) - } - return encodedValue -} -inline fun EnDecoder.testDecodeIsEqual( - byteArray: ByteArray, - value: T, - serializer: KSerializer = avro.serializersModule.serializer(), - readSchema: Schema = avro.schema(serializer), - writeSchema: Schema = readSchema -) : T { - val decodedValue = decode(byteArray, serializer, readSchema, writeSchema) - withClue("Decoded result was not equal to the passed value.") { - if (decodedValue == null && value != null) { - fail("Decoded value is null but '$value' is expected.") - } else if (decodedValue != null && value != null) { - decodedValue shouldBeEqualToComparingFields value - } - } - return decodedValue -} - -fun DslDrivenSpec.includeForEveryEncoder(createFactoryToInclude: (EnDecoder) -> TestFactory) { - EnDecoder::class.sealedSubclasses.map { it.newInstanceNoArgConstructorOrObjectInstance() }.forEach { - include(it.name, createFactoryToInclude.invoke(it)) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnumEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnumEncoderTest.kt deleted file mode 100644 index 63ec5da4..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/EnumEncoderTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.schema.Wine -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericData - -class EnumEncoderTest : FunSpec({ - includeForEveryEncoder { enumEncoderTests(it) } -}) -fun enumEncoderTests(enDecoder: EnDecoder): TestFactory { - @Serializable - data class MyWine(val wine: Wine) - - @Serializable - data class NullableWine(val wine: Wine?) - return stringSpec { - "support enums" { - enDecoder.testEncodeDecode( - MyWine(Wine.Malbec), record( - GenericData.EnumSymbol( - null, Wine.Malbec - ) - ) - ) - } - - "support nullable enums" { - val enumSchema = enDecoder.avro.schema(NullableWine.serializer()).getField("wine").schema().types[1] - enDecoder.testEncodeDecode(NullableWine(Wine.Shiraz), record( - GenericData.EnumSymbol(enumSchema, Wine.Shiraz) - ) - ) - enDecoder.testEncodeDecode(NullableWine(null), record(null)) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/MapEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/MapEncoderTest.kt deleted file mode 100644 index aa568b5e..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/MapEncoderTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import java.nio.ByteBuffer - -class MapEncoderTest : StringSpec({ - includeForEveryEncoder { mapEncoderTests(it) } -}) - -fun mapEncoderTests(enDecoder: EnDecoder): TestFactory { - return stringSpec { - - "encode/decode a Map" { - @Serializable - data class StringBooleanTest(val a: Map) - enDecoder.testEncodeDecode( - StringBooleanTest(mapOf("a" to true, "b" to false, "c" to true)), - record(mapOf("a" to true, "b" to false, "c" to true)) - ) - } - - "encode/decode a Map" { - @Serializable - data class StringBooleanTest(val a: Map) - enDecoder.testEncodeDecode( - StringBooleanTest(mapOf("a" to true, "b" to null, "c" to false)), - record(mapOf("a" to true, "b" to null, "c" to false)) - ) - } - - "encode/decode a Map" { - - @Serializable - data class StringStringTest(val a: Map) - enDecoder.testEncodeDecode( - StringStringTest(mapOf("a" to "x", "b" to "y", "c" to "z")), - record(mapOf("a" to "x", "b" to "y", "c" to "z")) - ) - } - - "encode/decode a Map" { - @Serializable - data class StringByteArrayTest(val a: Map) - enDecoder.testEncodeDecode( - StringByteArrayTest( - mapOf( - "a" to "x".toByteArray(), - "b" to "y".toByteArray(), - "c" to "z".toByteArray() - ) - ), - record( - mapOf( - "a" to ByteBuffer.wrap("x".toByteArray()), - "b" to ByteBuffer.wrap("y".toByteArray()), - "c" to ByteBuffer.wrap("z".toByteArray()) - ) - ) - ) - } - - "encode/decode a Map of records" { - - @Serializable - data class Foo(val a: String, val b: Boolean) - - @Serializable - data class StringFooTest(val a: Map) - enDecoder.testEncodeDecode( - StringFooTest(mapOf("a" to Foo("x", true), "b" to Foo("y", false))), - record( - mapOf( - "a" to record("x", true), - "b" to record("y", false) - ) - ) - ) - } - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt deleted file mode 100644 index 1db07213..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/NamingStrategyEncoderTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.WordSpec -import io.kotest.core.spec.style.stringSpec -import io.kotest.matchers.shouldNotBe -import kotlinx.serialization.Serializable - -class NamingStrategyEncoderTest : WordSpec({ - includeForEveryEncoder { namingStrategyEncoderTests(it) } -}) - -fun namingStrategyEncoderTests(enDecoder: EnDecoder): TestFactory { - return stringSpec { - @Serializable - data class Foo(val fooBar: String) - - "encode/decode fields with snake_casing" { - enDecoder.avro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) - 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)) - 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/endecode/RecordEncodingTests.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/RecordEncodingTests.kt deleted file mode 100644 index b84b8a53..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/RecordEncodingTests.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.WordSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData - -class RecordEncodingTests : WordSpec({ - includeForEveryEncoder { recordEncodingTests(it) } -}) - -fun recordEncodingTests(encoderToTest: EnDecoder): TestFactory { - @Serializable - data class StringFoo(val s: String) - return stringSpec { - "encode/decode strings as UTF8" { - encoderToTest.testEncodeDecode(StringFoo("hello"), record("hello")) - } - "encode/decode strings as GenericFixed and pad bytes when schema is Type.FIXED" { - val fixedSchema = Schema.createFixed("FixedString", null, null, 7) - val schema = SchemaBuilder.record("Foo").fields().name("s").type(fixedSchema).noDefault().endRecord() - val encoded = encoderToTest.testEncodeIsEqual( - value = StringFoo("hello"), - shouldMatch = record(GenericData.Fixed(fixedSchema, byteArrayOf(104, 101, 108, 108, 111, 0, 0))), - schema = schema - ) - encoderToTest.testDecodeIsEqual(encoded, StringFoo(String("hello".toByteArray() + byteArrayOf(0, 0)))) - } - "encode/decode nullable string" { - @Serializable - data class NullableString(val a: String?) - encoderToTest.testEncodeDecode(NullableString("hello"), record("hello")) - encoderToTest.testEncodeDecode(NullableString(null), record(null)) - } - "encode/decode longs" { - @Serializable - data class LongFoo(val l: Long) - encoderToTest.testEncodeDecode(LongFoo(123456L), record(123456L)) - } - "encode/decode doubles" { - - @Serializable - data class DoubleFoo(val d: Double) - encoderToTest.testEncodeDecode(DoubleFoo(123.435), record(123.435)) - } - "encode/decode booleans" { - @Serializable - data class BooleanFoo(val d: Boolean) - encoderToTest.testEncodeDecode(BooleanFoo(false), record(false)) - } - "encode/decode nullable booleans" { - @Serializable - data class NullableBoolean(val a: Boolean?) - encoderToTest.testEncodeDecode(NullableBoolean(true), record(true)) - encoderToTest.testEncodeDecode(NullableBoolean(null), record(null)) - } - "encode/decode floats" { - @Serializable - data class FloatFoo(val d: Float) - encoderToTest.testEncodeDecode(FloatFoo(123.435F), record(123.435F)) - } - "encode/decode ints" { - @Serializable - data class IntFoo(val i: Int) - encoderToTest.testEncodeDecode(IntFoo(123), record(123)) - } - "encode/decode shorts" { - @Serializable - data class ShortFoo(val s: Short) - encoderToTest.testEncodeDecode(ShortFoo(123.toShort()), record(123.toShort())) - } - "encode/decode bytes" { - @Serializable - data class ByteFoo(val b: Byte) - encoderToTest.testEncodeDecode(ByteFoo(123.toByte()), record(123.toByte())) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/SealedClassEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/SealedClassEncoderTest.kt deleted file mode 100644 index cd6f3017..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/SealedClassEncoderTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.schema.Operation -import com.github.avrokotlin.avro4k.schema.ReferencingNullableSealedClass -import com.github.avrokotlin.avro4k.schema.ReferencingSealedClass -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.spec.style.stringSpec - - -class SealedClassEncoderTest : StringSpec({ - includeForEveryEncoder { sealedClassEncoderTests(it) } -}) - -fun sealedClassEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "encode/decode sealed classes" { - val addSchema = encoderToTest.avro.schema(Operation.Binary.Add.serializer()) - encoderToTest.testEncodeDecode( - ReferencingSealedClass(Operation.Binary.Add(1, 2)), record(record(1, 2).createRecord(addSchema)) - ) - } - "encode/decode nullable sealed classes" { - val addSchema = Avro.default.schema(Operation.Binary.Add.serializer()) - - encoderToTest.testEncodeDecode( - ReferencingNullableSealedClass( - Operation.Binary.Add(1, 2) - ), record(record(1, 2).createRecord(addSchema)) - ) - - encoderToTest.testEncodeDecode(ReferencingNullableSealedClass(null), record(null)) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/TransientEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/TransientEncoderTest.kt deleted file mode 100644 index cdf1d62e..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/TransientEncoderTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.Avro -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.Serializable -import kotlinx.serialization.Transient -import org.apache.avro.generic.GenericData - -class TransientEncoderTest : FunSpec({ - includeForEveryEncoder { transientEncoderTests(it) } -}) -fun transientEncoderTests(encoderToTest: EnDecoder): TestFactory { - @Serializable - data class Foo(val a: String, @Transient val b: String = "foo", val c: String) - return stringSpec { - "should skip @Transient fields" { - val value = Foo("a", "b", "c") - encoderToTest.testEncodeIsEqual(value, record("a", "c")) - } - "decoder should populate transient fields with default" { - val schema = Avro.default.schema(Foo.serializer()) - val record = GenericData.Record(schema) - record.put("a", "hello") - - val encoded = encoderToTest.testEncodeIsEqual(Foo("a", "b","c"), record("a", "c")) - encoderToTest.testDecodeIsEqual(encoded, Foo(a = "a", c="c")) - } - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/URLEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/URLEncoderTest.kt deleted file mode 100644 index 5037c042..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/URLEncoderTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:UseSerializers(URLSerializer::class) - -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.serializer.URLSerializer -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.net.URL - -class URLEncoderTest : FunSpec({ - includeForEveryEncoder { urlEncoderTests(it) } -}) -fun urlEncoderTests(enDecoder: EnDecoder): TestFactory { - return stringSpec { - "encode/decode URLs as string" { - @Serializable - data class UrlTest(val b: URL) - - val test = UrlTest(URL("https://www.sksamuel.com")) - enDecoder.testEncodeDecode(test, record("https://www.sksamuel.com")) - } - - "encode/decode nullable URLs" { - @Serializable - data class NullableUrlTest(val b: URL?) - enDecoder.testEncodeDecode( - NullableUrlTest(URL("https://www.sksamuel.com")), record("https://www.sksamuel.com") - ) - enDecoder.testEncodeDecode(NullableUrlTest(null), record(null)) - } - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/UUIDEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/UUIDEncoderTest.kt deleted file mode 100644 index fbe9765d..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/UUIDEncoderTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.stringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.util.* - -class UUIDEncoderTest : FunSpec({ - includeForEveryEncoder { uuidEncoderTests(it) } -}) -fun uuidEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "encode/decode UUIDs" { - @Serializable - data class UUIDTest(val uuid: UUID) - - val uuid = UUID.randomUUID() - encoderToTest.testEncodeDecode(UUIDTest(uuid), record(uuid.toString())) - } - - "encode/decode nullable UUIDs" { - @Serializable - data class NullableUUIDTest(val uuid: UUID?) - - val uuid = UUID.randomUUID() - encoderToTest.testEncodeDecode(NullableUUIDTest(uuid), record(uuid.toString())) - encoderToTest.testEncodeDecode(NullableUUIDTest(null), record(null)) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ValueClassEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ValueClassEncoderTest.kt deleted file mode 100644 index ca1f0ddb..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/endecode/ValueClassEncoderTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.avrokotlin.avro4k.endecode - -import com.github.avrokotlin.avro4k.record -import com.github.avrokotlin.avro4k.schema.ValueClassSchemaTest -import io.kotest.core.factory.TestFactory -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.spec.style.stringSpec -import java.util.* - -class ValueClassEncoderTest : StringSpec({ - includeForEveryEncoder { valueClassEncoderTests(it) } -}) -fun valueClassEncoderTests(encoderToTest: EnDecoder): TestFactory { - return stringSpec { - "encode/decode value class" { - val uuid = UUID.randomUUID() - encoderToTest.testEncodeDecode( - ValueClassSchemaTest.ContainsInlineTest( - ValueClassSchemaTest.StringWrapper("100500"), ValueClassSchemaTest.UuidWrapper(uuid) - ), - record("100500", uuid.toString()) - ) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroBinaryOutputStreamTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroBinaryOutputStreamTest.kt deleted file mode 100644 index 056807bf..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroBinaryOutputStreamTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.inspectors.forNone -import io.kotest.matchers.string.shouldContain -import kotlinx.serialization.Serializable -import java.io.ByteArrayOutputStream - -class AvroBinaryOutputStreamTest : StringSpec({ - - val ennio = Composer("ennio morricone", "rome", listOf(Work("legend of 1900", 1986), Work("ecstasy of gold", 1969))) - - val hans = Composer("hans zimmer", "frankfurt", listOf(Work("batman begins", 2007), Work("dunkirk", 2017))) - - "AvroBinaryOutputStream should not write schemas" { - - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Binary - }.to(baos).write(ennio).write(hans).close() - - // the schema should not be written in a binary stream - listOf("name", "birthplace", "works", "year").forNone { - String(baos.toByteArray()).shouldContain(it) - } - } - -}) { - @Serializable - data class Work(val name: String, val year: Int) - - @Serializable - data class Composer(val name: String, val birthplace: String, val works: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStreamCodecTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStreamCodecTest.kt deleted file mode 100644 index a9e2b2f4..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroDataOutputStreamCodecTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.should -import io.kotest.matchers.shouldNot -import io.kotest.matchers.string.contain -import kotlinx.serialization.Serializable -import org.apache.avro.file.CodecFactory -import java.io.ByteArrayOutputStream - -class AvroDataOutputStreamCodecTest : StringSpec({ - - val ennio = Composer("ennio morricone", "rome", listOf("legend of 1900", "ecstasy of gold")) - - "include schema" { - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Data() - }.to(baos).write(ennio).close() - String(baos.toByteArray()) should contain("birthplace") - String(baos.toByteArray()) should contain("compositions") - } - - "include snappy coded in metadata when serialized with snappy" { - - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Data(CodecFactory.snappyCodec()) - }.to(baos).write(ennio).close() - String(baos.toByteArray()) should contain("snappy") - String(baos.toByteArray()) shouldNot contain("bzip2") - String(baos.toByteArray()) shouldNot contain("deflate") - } - - "include deflate coded in metadata when serialized with deflate" { - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Data(CodecFactory.deflateCodec(CodecFactory.DEFAULT_DEFLATE_LEVEL)) - }.to(baos).write(ennio).close() - String(baos.toByteArray()) should contain("deflate") - String(baos.toByteArray()) shouldNot contain("bzip2") - String(baos.toByteArray()) shouldNot contain("snappy") - } - - "include bzip2 coded in metadata when serialized with bzip2" { - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Data(CodecFactory.bzip2Codec()) - }.to(baos).write(ennio).close() - String(baos.toByteArray()) should contain("bzip2") - String(baos.toByteArray()) shouldNot contain("deflate") - String(baos.toByteArray()) shouldNot contain("snappy") - } - -}) { - - @Serializable - data class Composer(val name: String, val birthplace: String, val compositions: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroJsonOutputStreamTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroJsonOutputStreamTest.kt deleted file mode 100644 index 03dc9d31..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroJsonOutputStreamTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.inspectors.forAll -import io.kotest.matchers.string.shouldContain -import kotlinx.serialization.Serializable -import java.io.ByteArrayOutputStream - -class AvroJsonOutputStreamTest : StringSpec({ - - val ennio = Composer("ennio morricone", "rome", listOf(Work("legend of 1900", 1986), Work("ecstasy of gold", 1969))) - - val hans = Composer("hans zimmer", "frankfurt", listOf(Work("batman begins", 2007), Work("dunkirk", 2017))) - - "AvroJsonOutputStream should write schemas" { - - val baos = ByteArrayOutputStream() - Avro.default.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Json - }.to(baos).write(ennio).write(hans).close() - val jsonString = String(baos.toByteArray()) - // the schema should be written in a json stream - listOf("name", "birthplace", "works", "year").forAll { - jsonString.shouldContain(it) - } - } - -}) { - - @Serializable - data class Work(val name: String, val year: Int) - - @Serializable - data class Composer(val name: String, val birthplace: String, val works: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt deleted file mode 100644 index 54acdd12..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/AvroNameIoTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -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.Serializable -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericRecord -import java.nio.file.Files - -class AvroNameIoTest : StringSpec({ - - "using @AvroName to write out a record" { - - val ennio = Composer("Ennio Morricone", "Maestro") - - // writing out using the schema derived from Compose means fullname should be used - val bytes = Avro.default.encodeToByteArray(Composer.serializer(), ennio) - - // using a custom schema to check that fullname was definitely used - val schema = SchemaBuilder.record("Composer").fields() - .name("fullname").type(Schema.create(Schema.Type.STRING)).noDefault() - .name("status").type(Schema.create(Schema.Type.STRING)).noDefault() - .endRecord() - - Avro.default.openInputStream(Composer.serializer()){ - decodeFormat = AvroDecodeFormat.Data(schema, defaultReadSchema) - }.from(bytes).nextOrThrow() shouldBe ennio - } - - "using @AvroName to read a record back in" { - - val schema1 = SchemaBuilder.record("Composer").fields() - .name("fullname").type(Schema.create(Schema.Type.STRING)).noDefault() - .name("status").type(Schema.create(Schema.Type.STRING)).noDefault() - .endRecord() - - val record = GenericData.Record(schema1) - record.put("fullname", "Ennio Morricone") - record.put("status", "Maestro") - - val file = Files.createTempFile("avroname.avro","") - - val outputStream = Files.newOutputStream(file) - AvroBinaryOutputStream(outputStream, {it}, schema1).write(record).close() - - val schema2 = Avro.default.schema(Composer.serializer()) - val input = Avro.default.openInputStream(Composer.serializer()) { - decodeFormat = AvroDecodeFormat.Binary( - writerSchema = schema1, - readerSchema = schema2 - ) - }.from(file) - - input.next() shouldBe Composer("Ennio Morricone", "Maestro") - input.close() - - Files.delete(file) - } - -}) { - - @Serializable - data class Composer(@AvroName("fullname") val name: String, val status: String) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/BasicIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/BasicIoTest.kt deleted file mode 100644 index 52ef7035..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/BasicIoTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable -import org.apache.avro.util.Utf8 - -class BasicIoTest : FunSpec() { - init { - test("read write out booleans") { - writeRead(BooleanTest(true), BooleanTest.serializer()) - writeRead(BooleanTest(false), BooleanTest.serializer()) { - it["z"] shouldBe false - } - writeRead(BooleanTest(true), BooleanTest.serializer()) { - it["z"] shouldBe true - } - } - - test("read write out strings") { - writeRead(StringTest("Hello world"), StringTest.serializer()) - writeRead(StringTest("Hello world"), StringTest.serializer()) { - it["z"] shouldBe Utf8("Hello world") - } - } - - test("read write out longs") { - writeRead(LongTest(65653L), LongTest.serializer()) - writeRead(LongTest(65653L), LongTest.serializer()) { - it["z"] shouldBe 65653L - } - } - - test("read write out ints") { - writeRead(IntTest(44), IntTest.serializer()) - } - - test("read write out doubles") { - writeRead(DoubleTest(3.235), DoubleTest.serializer()) - } - - test("read write out floats") { - writeRead(FloatTest(3.4F), FloatTest.serializer()) - } - test("read write out byte arrays") { - val expected = ByteArrayTest("ABC".toByteArray()) - writeRead(expected,ByteArrayTest.serializer()){ - val deserialized = Avro.default.fromRecord(ByteArrayTest.serializer(),it) - expected.z shouldBe deserialized.z - } - } - } - - @Serializable - data class BooleanTest(val z: Boolean) - - @Serializable - data class StringTest(val z: String) - - @Serializable - data class FloatTest(val z: Float) - - @Serializable - data class DoubleTest(val z: Double) - - @Serializable - data class IntTest(val z: Int) - - @Serializable - data class LongTest(val z: Long) - - @Suppress("ArrayInDataClass") - @Serializable - data class ByteArrayTest(val z : ByteArray) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/CollectionsIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/CollectionsIoTest.kt deleted file mode 100644 index d4513f98..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/CollectionsIoTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -@file:Suppress("UNCHECKED_CAST") - -package com.github.avrokotlin.avro4k.io - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.property.Arb -import io.kotest.property.arbitrary.double -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.long -import io.kotest.property.arbitrary.numericDouble -import io.kotest.property.checkAll -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericArray -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericRecord -import org.apache.avro.util.Utf8 - -class CollectionsIoTest : StringSpec({ - - "read / write lists of strings" { - - writeRead(StringListsTest(listOf("foo", "boo"), listOf("goo", "moo")), StringListsTest.serializer()) - writeRead(StringListsTest(listOf("foo", "boo"), listOf("goo", "moo")), StringListsTest.serializer()) { - it["a"] shouldBe listOf(Utf8("foo"), Utf8("boo")) - it["b"] shouldBe listOf(Utf8("goo"), Utf8("moo")) - } - } - - "read / write sets of booleans" { - - writeRead(BooleanSetsTest(setOf(true, false), setOf(false, true)), BooleanSetsTest.serializer()) - writeRead(BooleanSetsTest(setOf(true, false), setOf(false, true)), BooleanSetsTest.serializer()) { - it["a"] shouldBe listOf(true, false) - it["b"] shouldBe listOf(false, true) - } - } - - "read / write sets of ints" { - - writeRead(S(setOf(1, 3)), S.serializer()) - writeRead(S(setOf(1, 3)), S.serializer()) { - (it["t"] as GenericData.Array).toSet() shouldBe setOf(1, 3) - } - } - - "read / write arrays of doubles" { - - // there's a bug in avro with Double.POSINF - checkAll( - Arb.double(), - Arb.double(), - Arb.double() - ) { a, b, c -> - val data = DoubleArrayTest(arrayOf(a, b, c)) - val serializer = DoubleArrayTest.serializer() - val test : (GenericRecord) -> Unit = {it["a"] shouldBe listOf(a,b,c)} - writeReadData(data, serializer, test = test) - writeReadBinary(data, serializer, test = test) - } - } - - "read / write arrays of double in json" { - //Json does not support -inf/+inf and NaN - checkAll( - Arb.numericDouble(), - Arb.numericDouble(), - Arb.numericDouble() - ) { a, b, c -> - writeReadJson(DoubleArrayTest(arrayOf(a, b, c)), DoubleArrayTest.serializer()) { - it["a"] shouldBe listOf(a,b,c) - } - } - } - - "read / write lists of long/ints" { - - checkAll(Arb.long(), Arb.long(), Arb.int(), Arb.int()) { a, b, c, d -> - writeRead(LongListsTest(listOf(a, b), listOf(c, d)), LongListsTest.serializer()) { - it["a"] shouldBe listOf(a, b) - it["b"] shouldBe listOf(c, d) - } - } - } - - "read / write lists of records" { - - val hawaiian = Pizza("hawaiian", listOf(Ingredient("ham", 1.5, 5.6), Ingredient("pineapple", 5.2, 0.2)), false, 391) - - writeRead(hawaiian, Pizza.serializer()) - writeRead(hawaiian, Pizza.serializer()) { - it["name"] shouldBe Utf8("hawaiian") - it["vegetarian"] shouldBe false - it["kcals"] shouldBe 391 - (it["ingredients"] as GenericArray)[0]["name"] shouldBe Utf8("ham") - (it["ingredients"] as GenericArray)[1]["sugar"] shouldBe 5.2 - } - } -}) { - @Serializable - data class StringListsTest(val a: List, val b: List) - - @Serializable - data class BooleanSetsTest(val a: Set, val b: Set) - - @Serializable - data class S(val t: Set) - - @Serializable - data class LongListsTest(val a: List, val b: List) - - @Serializable - data class DoubleArrayTest(val a: Array) - - @Serializable - data class Ingredient(val name: String, val sugar: Double, val fat: Double) - - @Serializable - data class Pizza(val name: String, val ingredients: List, val vegetarian: Boolean, val kcals: Int) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/EnumIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/EnumIoTest.kt deleted file mode 100644 index faf39cc7..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/EnumIoTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.StringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericEnumSymbol - -enum class Cream { - Bruce, Baker, Clapton -} - -enum class BBM { - Bruce, Baker, Moore -} - -class EnumIoTest : StringSpec({ - - "read / write enums" { - - writeRead(EnumTest(Cream.Bruce, BBM.Moore), EnumTest.serializer()) - writeRead(EnumTest(Cream.Bruce, BBM.Moore), EnumTest.serializer()) { - (it["a"] as GenericEnumSymbol<*>).toString() shouldBe "Bruce" - (it["b"] as GenericEnumSymbol<*>).toString() shouldBe "Moore" - } - } - - "read / write list of enums" { - - writeRead(EnumListTest(listOf(Cream.Bruce, Cream.Clapton)), EnumListTest.serializer()) - writeRead(EnumListTest(listOf(Cream.Bruce, Cream.Clapton)), EnumListTest.serializer()) { record -> - (record["a"] as List<*>).map { it.toString() } shouldBe listOf("Bruce", "Clapton") - } - } - - "read / write nullable enums" { - - writeRead(NullableEnumTest(null), NullableEnumTest.serializer()) - writeRead(NullableEnumTest(Cream.Bruce), NullableEnumTest.serializer()) - writeRead(NullableEnumTest(Cream.Bruce), NullableEnumTest.serializer()) { - (it["a"] as GenericData.EnumSymbol).toString() shouldBe "Bruce" - } - } -}) { - - @Serializable - data class EnumTest(val a: Cream, val b: BBM) - - @Serializable - data class EnumListTest(val a: List) - - @Serializable - data class NullableEnumTest(val a: Cream?) - -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/MapIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/MapIoTest.kt deleted file mode 100644 index 8a307a8d..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/MapIoTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.util.Utf8 - -class MapIoTest : StringSpec({ - - "read/write primitive maps"{ - - writeRead( - Test( - mapOf("a" to true, "b" to false), - mapOf("a" to "x", "b" to "y"), - mapOf("a" to 123L, "b" to 999L) - ), - Test.serializer() - ) { - it["a"] shouldBe mapOf(Utf8("a") to true, Utf8("b") to false) - it["b"] shouldBe mapOf(Utf8("a") to Utf8("x"), Utf8("b") to Utf8("y")) - it["c"] shouldBe mapOf(Utf8("a") to 123L, Utf8("b") to 999L) - } - } - - "read/write complex maps" { - val record = ComplexTypes(mutableMapOf(Pair("0", Status.Completed(0)), Pair("1", Status.Failed(1)))) - writeRead( - record,record, - ComplexTypes.serializer() - ) - } -}) { - - @Serializable - data class Test(val a: Map, val b: Map, val c: Map) - - @Serializable - data class ComplexTypes( - val statuses: MutableMap = mutableMapOf(), - ) - - @Serializable - sealed class Status { - @Serializable - data class Completed( - val completionIndex: Int - ) : Status() - - @Serializable - data class Failed( - val failureIndex: Int - ) : Status() - } -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt deleted file mode 100644 index ef26d3e6..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/NamingStrategyIoTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -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 io.kotest.core.spec.style.StringSpec -import io.kotest.engine.spec.tempfile -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericRecord -import java.io.ByteArrayOutputStream -import java.nio.file.Files - -class NamingStrategyIoTest : StringSpec({ - - val snakeCaseAvro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) - - "using snake_case namingStrategy to write out a record" { - val ennio = Composer("Ennio Morricone", "Maestro") - - val baos = ByteArrayOutputStream() - - snakeCaseAvro.openOutputStream(Composer.serializer()) { - encodeFormat = AvroEncodeFormat.Data() - }.to(baos).use { - it.write(ennio) - } - - val schema = SchemaBuilder.record("Composer").fields() - .name("full_name").type(Schema.create(Schema.Type.STRING)).noDefault() - .name("status").type(Schema.create(Schema.Type.STRING)).noDefault() - .endRecord() - - snakeCaseAvro.openInputStream(Composer.serializer()) { - decodeFormat = AvroDecodeFormat.Data(schema, defaultReadSchema) - }.from(baos.toByteArray()).nextOrThrow() shouldBe ennio - } - - "using snake_case namingStrategy to read a record back in" { - - val schema1 = SchemaBuilder.record("Composer").fields() - .name("full_name").type(Schema.create(Schema.Type.STRING)).noDefault() - .name("status").type(Schema.create(Schema.Type.STRING)).noDefault() - .endRecord() - - val record = GenericData.Record(schema1) - record.put("full_name", "Ennio Morricone") - record.put("status", "Maestro") - - val file = tempfile("avroname.avro", "") - - AvroBinaryOutputStream(Files.newOutputStream(file.toPath()), { it }, schema1).use { - it.write(record) - } - - val schema2 = snakeCaseAvro.schema(Composer.serializer()) - - snakeCaseAvro.openInputStream(Composer.serializer()) { - decodeFormat = AvroDecodeFormat.Binary( - writerSchema = schema1, - readerSchema = schema2 - ) - }.from(file).use { - it.next() shouldBe Composer("Ennio Morricone", "Maestro") - } - } -}) { - @Serializable - data class Composer(val fullName: String, val status: String) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/PolymorphicClassIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/PolymorphicClassIoTest.kt deleted file mode 100644 index 412dd5c2..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/PolymorphicClassIoTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.schema.* -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf -import org.apache.avro.generic.GenericRecord -import org.apache.avro.util.Utf8 - -class PolymorphicClassIoTest : StringSpec({ - "read / write nested polymorphic class" { - val avro = Avro(serializersModule = polymorphicModule) - writeRead(ReferencingPolymorphicRoot(UnsealedChildOne("one")), ReferencingPolymorphicRoot.serializer(), avro) - writeRead(ReferencingPolymorphicRoot(UnsealedChildOne("one")), ReferencingPolymorphicRoot.serializer(), avro) { - val root = it["root"] as GenericRecord - root.schema shouldBe avro.schema(UnsealedChildOne.serializer()) - } - } - "read / write nested polymorphic list" { - val avro = Avro(serializersModule = polymorphicModule) - writeRead(PolymorphicRootInList(listOf(UnsealedChildOne("one"))), PolymorphicRootInList.serializer(), avro) - writeRead(PolymorphicRootInList(listOf(UnsealedChildOne("one"))), PolymorphicRootInList.serializer(), avro) { - it["listOfRoot"].shouldBeInstanceOf>() - val unsealeadChild = (it["listOfRoot"] as List<*>)[0] as GenericRecord - unsealeadChild.schema shouldBe avro.schema(UnsealedChildOne.serializer()) - } - } - "read / write nested polymorphic map" { - val avro = Avro(serializersModule = polymorphicModule) - writeRead(PolymorphicRootInMap(mapOf("a" to UnsealedChildOne("one"))), PolymorphicRootInMap.serializer(), avro) - writeRead( - PolymorphicRootInMap(mapOf("a" to UnsealedChildOne("one"))), - PolymorphicRootInMap.serializer(), - avro - ) { - it["mapOfRoot"].shouldBeInstanceOf>() - val unsealeadChild = (it["mapOfRoot"] as Map<*, *>)[Utf8("a")] as GenericRecord - unsealeadChild.schema shouldBe avro.schema(UnsealedChildOne.serializer()) - } - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/RecursiveIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/RecursiveIoTest.kt deleted file mode 100644 index 4a468800..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/RecursiveIoTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.schema.Level1 -import com.github.avrokotlin.avro4k.schema.Level2 -import com.github.avrokotlin.avro4k.schema.Level3 -import com.github.avrokotlin.avro4k.schema.Level4 -import com.github.avrokotlin.avro4k.schema.RecursiveClass -import com.github.avrokotlin.avro4k.schema.RecursiveListItem -import com.github.avrokotlin.avro4k.schema.RecursiveMapValue -import com.github.avrokotlin.avro4k.schema.RecursivePair -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.types.shouldBeInstanceOf -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericRecord -import org.apache.avro.util.Utf8 - -class RecursiveIoTest : StringSpec({ - - "read / write direct recursive class" { - writeRead(RecursiveClass(1, RecursiveClass(2, null)), RecursiveClass.serializer()) - writeRead(RecursiveClass(1, RecursiveClass(2, null)), RecursiveClass.serializer()) { - it["payload"] shouldBe 1 - it["klass"].shouldBeInstanceOf() - val klass = it["klass"] as GenericRecord - klass.schema shouldBe Avro.default.schema(RecursiveClass.serializer()) - klass["payload"] shouldBe 2 - klass["klass"] shouldBe null - } - } - - "read / write direct recursive list" { - writeRead(RecursiveListItem(1, listOf(RecursiveListItem(2, null))), RecursiveListItem.serializer()) - writeRead(RecursiveListItem(1, listOf(RecursiveListItem(2, null))), RecursiveListItem.serializer()) { - it["payload"] shouldBe 1 - it["list"].shouldBeInstanceOf>() - val list = (it["list"] as List<*>)[0] as GenericRecord - list.schema shouldBe Avro.default.schema(RecursiveListItem.serializer()) - list["payload"] shouldBe 2 - list["list"] shouldBe null - } - } - - "read / write direct recursive map" { - writeRead(RecursiveMapValue(1, mapOf("a" to RecursiveMapValue(2, null))), RecursiveMapValue.serializer()) - writeRead(RecursiveMapValue(1, mapOf("a" to RecursiveMapValue(2, null))), RecursiveMapValue.serializer()) { - it["payload"] shouldBe 1 - it["map"].shouldBeInstanceOf>() - val map = (it["map"] as Map<*, *>)[Utf8("a")]!! as GenericRecord - map.schema shouldBe Avro.default.schema(RecursiveMapValue.serializer()) - map["payload"] shouldBe 2 - map["map"] shouldBe null - } - } - - "read / write direct recursive pair" { - writeRead(RecursivePair(1, (RecursivePair(2, null) to RecursivePair(3, null))), RecursivePair.serializer()) - writeRead(RecursivePair(1, (RecursivePair(2, null) to RecursivePair(3, null))), RecursivePair.serializer()) { - it["payload"] shouldBe 1 - it["pair"].shouldBeInstanceOf() - val first = (it["pair"] as GenericData.Record)["first"] - first.shouldBeInstanceOf() - first.schema shouldBe Avro.default.schema(RecursivePair.serializer()) - first["payload"] shouldBe 2 - first["pair"] shouldBe null - val second = (it["pair"] as GenericData.Record)["second"] - second.shouldBeInstanceOf() - second.schema shouldBe Avro.default.schema(RecursivePair.serializer()) - second["payload"] shouldBe 3 - second["pair"] shouldBe null - } - } - - "read / write nested recursive classes" { - writeRead(Level1(Level2(Level3(Level4(Level1(null))))), Level1.serializer()) - writeRead(Level1(Level2(Level3(Level4(Level1(null))))), Level1.serializer()) { - it["level2"].shouldBeInstanceOf() - val level2 = it["level2"] as GenericRecord - level2["level3"].shouldBeInstanceOf() - val level3 = level2["level3"] as GenericRecord - level3["level4"].shouldBeInstanceOf() - val level4 = level3["level4"] as GenericRecord - level4["level1"].shouldBeInstanceOf() - val level1 = level4["level1"] as GenericRecord - level1["level2"] shouldBe null - } - } -}) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/SealedClassIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/SealedClassIoTest.kt deleted file mode 100644 index b685746a..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/SealedClassIoTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.schema.Operation -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericRecord - -class SealedClassIoTest : StringSpec({ - - "read / write sealed class" { - - writeRead(SealedClassTest(Operation.Unary.Negate(1)), SealedClassTest.serializer()) - writeRead(SealedClassTest(Operation.Unary.Negate(1)), SealedClassTest.serializer()) { - val operation = it["a"] as GenericRecord - operation.schema shouldBe Avro.default.schema(Operation.Unary.Negate.serializer()) - operation["value"] shouldBe 1 - } - } - - "read / write sealed class using object" { - - writeRead(SealedClassTest(Operation.Nullary), SealedClassTest.serializer()) - writeRead(SealedClassTest(Operation.Nullary), SealedClassTest.serializer()) { - val operation = it["a"] as GenericRecord - operation.schema shouldBe Avro.default.schema(Operation.Nullary.serializer()) - } - } - - "read / write list of sealed class values" { - - val test = SealedClassListTest(listOf( - Operation.Nullary, - Operation.Unary.Negate(1), - Operation.Binary.Add(3,4) - )) - writeRead(test, SealedClassListTest.serializer()) - writeRead(test, SealedClassListTest.serializer()) { - it["a"].shouldBeInstanceOf>() - @Suppress("UNCHECKED_CAST") - val operations = it["a"] as List - operations.size shouldBe 3 - operations[0].schema shouldBe Avro.default.schema(Operation.Nullary.serializer()) - operations[1].schema shouldBe Avro.default.schema(Operation.Unary.Negate.serializer()) - operations[2].schema shouldBe Avro.default.schema(Operation.Binary.Add.serializer()) - - operations[1]["value"] shouldBe 1 - operations[2]["left"] shouldBe 3 - operations[2]["right"] shouldBe 4 - } - } - - "read / write nullable sealed class" { - - writeRead(NullableSealedClassTest(null), NullableSealedClassTest.serializer()) - writeRead(NullableSealedClassTest(Operation.Nullary), NullableSealedClassTest.serializer()) - writeRead(NullableSealedClassTest(Operation.Unary.Negate(1)), NullableSealedClassTest.serializer()) - writeRead(NullableSealedClassTest(Operation.Unary.Negate(1)), NullableSealedClassTest.serializer()) { - val operation = it["a"] as GenericRecord - operation.schema shouldBe Avro.default.schema(Operation.Unary.Negate.serializer()) - operation["value"] shouldBe 1 - } - } - - "read / write sealed class directly"{ - writeRead(Operation.Nullary, Operation.serializer()) - writeRead(Operation.Unary.Negate(1), Operation.serializer()) - writeRead(Operation.Unary.Negate(1), Operation.serializer()) {operation -> - operation.schema shouldBe Avro.default.schema(Operation.Unary.Negate.serializer()) - operation["value"] shouldBe 1 - } - } -}) { - - @Serializable - data class SealedClassTest(val a: Operation) - - @Serializable - data class SealedClassListTest(val a: List) - - @Serializable - data class NullableSealedClassTest(val a: Operation?) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/StreamTests.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/StreamTests.kt deleted file mode 100644 index 4ebb194e..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/StreamTests.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.github.avrokotlin.avro4k.io - - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy -import org.apache.avro.file.SeekableByteArrayInput -import org.apache.avro.generic.GenericDatumReader -import org.apache.avro.generic.GenericRecord -import org.apache.avro.io.DecoderFactory -import java.io.ByteArrayOutputStream - -fun writeRead(t: T, serializer: KSerializer, avro: Avro = Avro.default) { - writeData(t, serializer, avro).apply { - val record = readData(this, serializer, avro) - val tt = avro.fromRecord(serializer, record) - t shouldBe tt - } - writeBinary(t, serializer, avro).apply { - val record = readBinary(this, serializer, avro) - val tt = avro.fromRecord(serializer, record) - t shouldBe tt - } - writeJson(t, serializer, avro).apply { - val record = readJson(this, serializer, avro) - val tt = avro.fromRecord(serializer, record) - t shouldBe tt - } -} - -fun writeRead(t: T, expected: T, serializer: KSerializer, avro: Avro = Avro.default) { - writeData(t, serializer, avro).apply { - val record = readData(this, serializer, avro) - val tt = avro.fromRecord(serializer, record) - tt shouldBe expected - } - writeBinary(t, serializer, avro).apply { - val record = readBinary(this, serializer, avro) - val tt = avro.fromRecord(serializer, record) - tt shouldBe expected - } -} - -fun writeRead(t: T, serializer: KSerializer, avro: Avro = Avro.default, test: (GenericRecord) -> Unit) { - writeReadData(t, serializer, avro, test) - writeReadBinary(t, serializer, avro, test) - writeReadJson(t, serializer, avro, test) -} - -fun writeReadJson(t: T, serializer: KSerializer, avro: Avro = Avro.default, test: (GenericRecord) -> Unit -) { - writeJson(t, serializer, avro).apply { - val record = readJson(this, serializer, avro) - test(record) - } -} - -fun writeReadBinary(t: T, serializer: KSerializer, avro: Avro = Avro.default , test: (GenericRecord) -> Unit) { - writeBinary(t, serializer, avro).apply { - val record = readBinary(this, serializer, avro) - test(record) - } -} - -fun writeReadData(t: T, serializer: KSerializer, avro: Avro = Avro.default, test: (GenericRecord) -> Unit) { - writeData(t, serializer, avro).apply { - val record = readData(this, serializer, avro) - test(record) - } -} - -fun writeData(t: T, serializer: SerializationStrategy, avro: Avro = Avro.default): ByteArray { - val schema = avro.schema(serializer) - val out = ByteArrayOutputStream() - val output = avro.openOutputStream(serializer) { - encodeFormat = AvroEncodeFormat.Data() - this.schema = schema - }.to(out) - output.write(t) - output.close() - return out.toByteArray() -} - -fun readJson(bytes: ByteArray, serializer: KSerializer, avro: Avro = Avro.default): GenericRecord { - val schema = avro.schema(serializer) - val datumReader = GenericDatumReader(schema) - val decoder = DecoderFactory.get().jsonDecoder(schema, SeekableByteArrayInput(bytes)) - return datumReader.read(null, decoder) -} - -fun writeJson(t: T, serializer: KSerializer, avro: Avro = Avro.default): ByteArray { - val schema = avro.schema(serializer) - val baos = ByteArrayOutputStream() - val output = avro.openOutputStream(serializer) { - encodeFormat = AvroEncodeFormat.Json - this.schema = schema - }.to(baos) - output.write(t) - output.close() - return baos.toByteArray() -} - -fun readData(bytes: ByteArray, serializer: KSerializer, avro: Avro = Avro.default): GenericRecord { - val schema = avro.schema(serializer) - val input = avro.openInputStream { - decodeFormat = AvroDecodeFormat.Data(schema) - }.from(bytes) - return input.next() as GenericRecord -} - -fun writeBinary(t: T, serializer: SerializationStrategy, avro: Avro = Avro.default): ByteArray { - val schema = avro.schema(serializer) - val out = ByteArrayOutputStream() - val output = avro.openOutputStream(serializer) { - encodeFormat = AvroEncodeFormat.Binary - this.schema = schema - }.to(out) - output.write(t) - output.close() - return out.toByteArray() -} - -fun readBinary(bytes: ByteArray, serializer: KSerializer, avro: Avro = Avro.default): GenericRecord { - val schema = avro.schema(serializer) - val datumReader = GenericDatumReader(schema) - val decoder = DecoderFactory.get().binaryDecoder(SeekableByteArrayInput(bytes), null) - return datumReader.read(null, decoder) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/io/UUIDIoTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/io/UUIDIoTest.kt deleted file mode 100644 index 58fa3e86..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/io/UUIDIoTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.avrokotlin.avro4k.io - -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.StringSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.apache.avro.LogicalTypes -import org.apache.avro.SchemaBuilder -import org.apache.avro.generic.GenericData -import org.apache.avro.util.Utf8 -import java.util.* - -class UUIDIoTest : StringSpec({ - - "read / write UUID" { - - val uuid = UUID.randomUUID() - - writeRead(UUIDTest(uuid), UUIDTest.serializer()) - writeRead(UUIDTest(uuid), UUIDTest.serializer()) { - it["a"] shouldBe Utf8(uuid.toString()) - } - } - - "read / write list of UUIDs" { - - val uuid1 = UUID.randomUUID() - val uuid2 = UUID.randomUUID() - - writeRead(UUIDListTest(listOf(uuid1, uuid2)), UUIDListTest.serializer()) - writeRead(UUIDListTest(listOf(uuid1, uuid2)), UUIDListTest.serializer()) { - val uuidSchema = SchemaBuilder.builder().stringType() - LogicalTypes.uuid().addToSchema(uuidSchema) - val schema = SchemaBuilder.array().items(uuidSchema) - it["a"] shouldBe GenericData.Array(schema, listOf(Utf8(uuid1.toString()), Utf8(uuid2.toString()))) - } - } - - "read / write nullable UUIDs" { - - val uuid = UUID.randomUUID() - - writeRead(NullableUUIDTest(uuid), NullableUUIDTest.serializer()) { - it["a"] shouldBe Utf8(uuid.toString()) - } - - writeRead(NullableUUIDTest(null), NullableUUIDTest.serializer()) { - it["a"] shouldBe null - } - } -}) { - - @Serializable - data class UUIDTest(val a: UUID) - - @Serializable - data class UUIDListTest(val a: List) - - @Serializable - data class NullableUUIDTest(val a: UUID?) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ArraySchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ArraySchemaTest.kt index 9af3683c..c2f7b0bb 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ArraySchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ArraySchemaTest.kt @@ -1,92 +1,64 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.WordSpec import kotlinx.serialization.Serializable - -class ArraySchemaTest : WordSpec({ - - "SchemaEncoder" should { - "generate array type for an Array of primitives" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array.json")) - val schema = Avro.default.schema(BooleanArrayTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for a List of primitives" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/list.json")) - val schema = Avro.default.schema(NestedListString.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for an Array of records" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/arrayrecords.json")) - val schema = Avro.default.schema(NestedArrayTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for a List of records" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/listrecords.json")) - val schema = Avro.default.schema(NestedListTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for a Set of records" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/setrecords.json")) - val schema = Avro.default.schema(NestedSet.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for a Set of strings" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/setstrings.json")) - val schema = Avro.default.schema(StringSetTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "generate array type for a Set of doubles" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/setdoubles.json")) - val schema = Avro.default.schema(NestedSetDouble.serializer()) - schema.toString(true) shouldBe expected.toString(true) +import kotlin.io.path.Path + +internal class ArraySchemaTest : WordSpec({ + + "SchemaEncoder" should { + "generate array type for an Array of primitives" { + AvroAssertions.assertThat() + .generatesSchema(Path("/array.json")) + } + "generate array type for a List of primitives" { + AvroAssertions.assertThat() + .generatesSchema(Path("/list.json")) + } + "generate array type for an Array of records" { + AvroAssertions.assertThat() + .generatesSchema(Path("/arrayrecords.json")) + } + "generate array type for a List of records" { + AvroAssertions.assertThat() + .generatesSchema(Path("/listrecords.json")) + } + "generate array type for a Set of records" { + AvroAssertions.assertThat() + .generatesSchema(Path("/setrecords.json")) + } + "generate array type for a Set of strings" { + AvroAssertions.assertThat() + .generatesSchema(Path("/setstrings.json")) + } + "generate array type for a Set of doubles" { + AvroAssertions.assertThat() + .generatesSchema(Path("/setdoubles.json")) + } } -// "support top level List[Int]" { -// @Serializable -// val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/top_level_list_int.json")) -// val schema = AvroSchema[List[Int]] -// val schema = Avro.default.schema(Test.serializer()) -// schema.toString(true) shouldBe expected.toString(true) -// } -// "support top level Set[Boolean]" { -// val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/top_level_set_boolean.json")) -// val schema = AvroSchema[Set[Boolean]] -// val schema = Avro.default.schema(Test.serializer()) -// schema.toString(true) shouldBe expected.toString(true) -// } - } - }) { - @Serializable - data class BooleanArrayTest(val array: Array) + @Serializable + private data class BooleanArrayTest(val array: Array) - @Serializable - data class Nested(val goo: String) + @Serializable + private data class Nested(val goo: String) - @Serializable - data class NestedListString(val list: List) + @Serializable + private data class NestedListString(val list: List) - @Serializable - data class NestedArrayTest(val array: Array) + @Serializable + private data class NestedArrayTest(val array: Array) - @Serializable - data class NestedListTest(val list: List) + @Serializable + private data class NestedListTest(val list: List) - @Serializable - data class NestedSet(val set: Set) + @Serializable + private data class NestedSet(val set: Set) - @Serializable - data class StringSetTest(val set: Set) + @Serializable + private data class StringSetTest(val set: Set) - @Serializable - data class NestedSetDouble(val set: Set) -} + @Serializable + private data class NestedSetDouble(val set: Set) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroAliasSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroAliasSchemaTest.kt index ec6b7e16..302a4e51 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroAliasSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroAliasSchemaTest.kt @@ -1,52 +1,55 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.WordSpec -import io.kotest.matchers.shouldBe import kotlinx.serialization.Serializable - -class AvroAliasSchemaTest : WordSpec({ - - "SchemaEncoder" should { - "support alias annotations on types" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/aliases_on_types.json")) - val schema = Avro.default.schema(TypeAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) +import kotlin.io.path.Path + +internal class AvroAliasSchemaTest : WordSpec({ + + "SchemaEncoder" should { + "support alias annotations on types" { + AvroAssertions.assertThat() + .generatesSchema(Path("/aliases_on_types.json")) + } + "support multiple alias annotations on types" { + AvroAssertions.assertThat() + .generatesSchema(Path("/aliases_on_types_multiple.json")) + } + "support alias annotations on field" { + AvroAssertions.assertThat() + .generatesSchema(Path("/aliases_on_fields.json")) + } + "support multiple alias annotations on fields" { + AvroAssertions.assertThat() + .generatesSchema(Path("/aliases_on_fields_multiple.json")) + } } - "support multiple alias annotations on types" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/aliases_on_types_multiple.json")) - val schema = Avro.default.schema(TypeAliasAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support alias annotations on field" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/aliases_on_fields.json")) - val schema = Avro.default.schema(FieldAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support multiple alias annotations on fields" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/aliases_on_fields_multiple.json")) - val schema = Avro.default.schema(FieldAliasAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - }) { @Serializable @AvroAlias("queen") - data class TypeAnnotated(val str: String) + private data class TypeAnnotated(val str: String) @AvroAlias("queen", "ledzep") @Serializable - data class TypeAliasAnnotated(val str: String) + private data class TypeAliasAnnotated(val str: String) + + @Serializable + private data class FieldAnnotated( + @AvroAlias("cold") val str: String, + @AvroAlias("kate") val long: Long, + val int: IntValue, + ) @Serializable - data class FieldAnnotated(@AvroAlias("cold") val str: String, @AvroAlias("kate") val long: Long, val int: Int) + @JvmInline + private value class IntValue( + @AvroAlias("ignoredAlias") val value: Int, + ) @Serializable - data class FieldAliasAnnotated(@AvroAlias("queen", "ledzep") val str: String) -} + private data class FieldAliasAnnotated( + @AvroAlias("queen", "ledzep") val str: String, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt new file mode 100644 index 00000000..3230ebb9 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt @@ -0,0 +1,62 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder + +internal class AvroCustomSchemaTest : StringSpec({ + "support custom schema" { + AvroAssertions.assertThat() + .generatesSchema(CustomSchemaSerializer.SCHEMA) + AvroAssertions.assertThat() + .generatesSchema(CustomSchemaSerializer.SCHEMA.nullable) + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("CustomSchemaClass") + .fields() + .name("value").type(CustomSchemaSerializer.SCHEMA).noDefault() + .name("nullableValue").type(CustomSchemaSerializer.SCHEMA.nullable).withDefault(null) + .endRecord() + ) + } +}) { + @JvmInline + @Serializable + private value class CustomSchemaValueClass( + @Serializable(with = CustomSchemaSerializer::class) val value: String, + ) + + @Serializable + @SerialName("CustomSchemaClass") + private data class CustomSchemaClass( + @Serializable(with = CustomSchemaSerializer::class) val value: String, + @Serializable(with = CustomSchemaSerializer::class) val nullableValue: String?, + ) + + private object CustomSchemaSerializer : AvroSerializer("CustomSchema") { + val SCHEMA = Schema.createUnion(Schema.createFixed("testFixed", "doc", "namespace", 10)) + + override fun getSchema(context: SchemaSupplierContext): Schema { + return SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: String, + ) { + TODO("Not yet implemented") + } + + override fun deserializeAvro(decoder: AvroDecoder): String { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDefaultSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDefaultSchemaTest.kt index be875c64..9e302a98 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDefaultSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDefaultSchemaTest.kt @@ -1,223 +1,216 @@ -@file:Suppress("unused") - package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecimal import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe import kotlinx.serialization.Serializable import org.apache.avro.AvroTypeException import java.math.BigDecimal - -@Suppress("BlockingMethodInNonBlockingContext") -class AvroDefaultSchemaTest : FunSpec() { - init { - test("schema for data class with @AvroDefault should include default value as a string") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_string.json")) - val schema = Avro.default.schema(BarString.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as an int") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_int.json")) - val schema = Avro.default.schema(BarInt.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as a float") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_float.json")) - val schema = Avro.default.schema(BarFloat.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as a BigDecimal") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_big_decimal.json")) - val schema = Avro.default.schema(BarDecimal.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as an Enum") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_enum.json")) - val schema = Avro.default.schema(BarEnum.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as a list") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_list.json")) - val schema = Avro.default.schema(BarList.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as a list with a record element type") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_list_of_records.json")) - val schema = Avro.default.schema(BarListOfElements.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as an array") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_array.json")) - val schema = Avro.default.schema(BarArray.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should include default value as an set") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_set.json")) - val schema = Avro.default.schema(BarSet.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("schema for data class with @AvroDefault should throw error when array type does not match default value type") { - shouldThrow { Avro.default.schema(BarInvalidArrayType.serializer()) } - shouldThrow { Avro.default.toRecord(BarInvalidNonArrayType.serializer(), BarInvalidNonArrayType()) } - } - } -} - - -@Serializable -data class BarString( - val a: String, - @AvroDefault("hello") - val b: String, - @AvroDefault(Avro.NULL) - val nullableString: String?, - @AvroDefault("hello") - val c: String? -) - -@Serializable -data class BarInt( - val a: String, - @AvroDefault("5") - val b: Int, - @AvroDefault(Avro.NULL) - val nullableInt: Int?, - @AvroDefault("5") - val c: Int? -) - -@Serializable -data class BarFloat( - val a: String, - @AvroDefault("3.14") - val b: Float, - @AvroDefault(Avro.NULL) - val nullableFloat: Float?, - @AvroDefault("3.14") - val c: Float? -) - -enum class FooEnum{ - A,B,C -} -@Serializable -data class BarEnum( - val a : FooEnum, - @AvroDefault("A") - val b : FooEnum, - @AvroDefault(Avro.NULL) - val nullableEnum : FooEnum?, - @AvroDefault("B") - val c : FooEnum? -) - -@Serializable -data class BarDecimal( - @Serializable(BigDecimalSerializer::class) - val a: BigDecimal, - @Serializable(BigDecimalSerializer::class) - @AvroDefault("\u0000") - val b: BigDecimal, - @Serializable(BigDecimalSerializer::class) - @AvroDefault(Avro.NULL) - val nullableString: BigDecimal?, - @Serializable(BigDecimalSerializer::class) - @AvroDefault("\u0000") - val c: BigDecimal? -) - -@Serializable -data class BarSet( - @AvroDefault("[]") - val defaultEmptySet: Set, - @AvroDefault(Avro.NULL) - val nullableDefaultEmptySet: Set?, - @AvroDefault("""["John", "Doe"]""") - val defaultStringSetWith2Defaults: Set, - @AvroDefault("""[1, 2]""") - val defaultIntSetWith2Defaults: Set, - @AvroDefault("""[3.14, 9.89]""") - val defaultFloatSetWith2Defaults: Set, - //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in - //the default - //See https://issues.apache.org/jira/browse/AVRO-2647 - @AvroDefault("""[null]""") - val defaultStringSetWithNullableTypes : Set -) -@Serializable -data class BarList( - @AvroDefault("[]") - val defaultEmptyList: List, - @AvroDefault(Avro.NULL) - val nullableDefaultEmptyList: List?, - @AvroDefault("""["John", "Doe"]""") - val defaultStringListWith2Defaults: List, - @AvroDefault("""[1, 2]""") - val defaultIntListWith2Defaults: List, - @AvroDefault("""[3.14, 9.89]""") - val defaultFloatListWith2Defaults: List, - //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in - //the default - //See https://issues.apache.org/jira/browse/AVRO-2647 - @AvroDefault("""[null]""") - val defaultStringListWithNullableTypes : List -) - -@Serializable -data class FooElement(val value: String) - -@Serializable -data class BarListOfElements( - @AvroDefault("[]") - val defaultEmptyListOfRecords: List, - @AvroDefault("""[{"value":"foo"}]""") - val defaultListWithOneValue : List -) - -@Suppress("ArrayInDataClass") -@Serializable -data class BarArray( - @AvroDefault("[]") - val defaultEmptyArray: Array, - @AvroDefault(Avro.NULL) - val nullableDefaultEmptyArray: Array?, - @AvroDefault("""["John", "Doe"]""") - val defaultStringArrayWith2Defaults: Array, - @AvroDefault("""[1, 2]""") - val defaultIntArrayWith2Defaults: Array, - @AvroDefault("""[3.14, 9.89]""") - val defaultFloatArrayWith2Defaults: Array, - //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in - //the default - //See https://issues.apache.org/jira/browse/AVRO-2647 - @AvroDefault("""[null]""") - val defaultStringArrayWithNullableTypes : Array -) - -@Serializable -data class BarInvalidArrayType( - @AvroDefault("""["foo-bar"]""") - val defaultFloatArrayWith2Defaults: List -) - -@Serializable -data class BarInvalidNonArrayType( - @AvroDefault("{}") - val defaultBarArrayWithNonWorkingDefaults: List = ArrayList() -) - - - +import kotlin.io.path.Path + +internal class AvroDefaultSchemaTest : FunSpec({ + test("schema for data class with @AvroDefault should include default value as a string") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_string.json")) + } + + test("schema for data class with @AvroDefault should include default value as an int") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_int.json")) + } + + test("schema for data class with @AvroDefault should include default value as a float") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_float.json")) + } + + test("schema for data class with @AvroDefault should include default value as a BigDecimal") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_big_decimal.json")) + } + + test("schema for data class with @AvroDefault should include default value as an Enum") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_enum.json")) + } + + test("schema for data class with @AvroDefault should include default value as a list") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_list.json")) + } + + test("schema for data class with @AvroDefault should include default value as a list with a record element type") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_list_of_records.json")) + } + + test("schema for data class with @AvroDefault should include default value as an array") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_array.json")) + } + + test("schema for data class with @AvroDefault should include default value as an set") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_default_annotation_set.json")) + } + + test("schema for data class with @AvroDefault should throw error when array type does not match default value type") { + shouldThrow { Avro.schema(BarInvalidArrayType.serializer()) } + shouldThrow { Avro.schema(BarInvalidNonArrayType.serializer()) } + } +}) { + @Serializable + private data class BarString( + val a: String, + @AvroDefault("hello") + val b: String, + @AvroDefault("null") + val nullableString: String?, + @AvroDefault("hello") + val c: String?, + ) + + @Serializable + private data class BarInt( + val a: String, + @AvroDefault("5") + val b: Int, + @AvroDefault("null") + val nullableInt: Int?, + @AvroDefault("5") + val c: Int?, + ) + + @Serializable + private data class BarFloat( + val a: String, + @AvroDefault("3.14") + val b: Float, + @AvroDefault("null") + val nullableFloat: Float?, + @AvroDefault("3.14") + val c: Float?, + ) + + private enum class FooEnum { + A, + B, + C, + } + + @Serializable + private data class BarEnum( + val a: FooEnum, + @AvroDefault("A") + val b: FooEnum, + @AvroDefault("null") + val nullableEnum: FooEnum?, + @AvroDefault("B") + val c: FooEnum?, + ) + + @Serializable + private data class BarDecimal( + @AvroDecimal(scale = 2, precision = 8) + @Serializable(BigDecimalSerializer::class) + val a: BigDecimal, + @AvroDecimal(scale = 2, precision = 8) + @Serializable(BigDecimalSerializer::class) + @AvroDefault("\u0000") + val b: BigDecimal, + @AvroDecimal(scale = 2, precision = 8) + @Serializable(BigDecimalSerializer::class) + @AvroDefault("null") + val nullableString: BigDecimal?, + @AvroDecimal(scale = 2, precision = 8) + @Serializable(BigDecimalSerializer::class) + @AvroDefault("\u0000") + val c: BigDecimal?, + ) + + @Serializable + private data class BarSet( + @AvroDefault("[]") + val defaultEmptySet: Set, + @AvroDefault("null") + val nullableDefaultEmptySet: Set?, + @AvroDefault("""["John", "Doe"]""") + val defaultStringSetWith2Defaults: Set, + @AvroDefault("""[1, 2]""") + val defaultIntSetWith2Defaults: Set, + @AvroDefault("""[3.14, 9.89]""") + val defaultFloatSetWith2Defaults: Set, + // Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in + // the default + // See https://issues.apache.org/jira/browse/AVRO-2647 + @AvroDefault("""[null]""") + val defaultStringSetWithNullableTypes: Set, + ) + + @Serializable + private data class BarList( + @AvroDefault("[]") + val defaultEmptyList: List, + @AvroDefault("null") + val nullableDefaultEmptyList: List?, + @AvroDefault("""["John", "Doe"]""") + val defaultStringListWith2Defaults: List, + @AvroDefault("""[1, 2]""") + val defaultIntListWith2Defaults: List, + @AvroDefault("""[3.14, 9.89]""") + val defaultFloatListWith2Defaults: List, + // Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in + // the default + // See https://issues.apache.org/jira/browse/AVRO-2647 + @AvroDefault("""[null]""") + val defaultStringListWithNullableTypes: List, + ) + + @Serializable + private data class FooElement(val value: String) + + @Serializable + private data class BarListOfElements( + @AvroDefault("[]") + val defaultEmptyListOfRecords: List, + @AvroDefault("""[{"value":"foo"}]""") + val defaultListWithOneValue: List, + ) + + @Suppress("ArrayInDataClass") + @Serializable + private data class BarArray( + @AvroDefault("[]") + val defaultEmptyArray: Array, + @AvroDefault("null") + val nullableDefaultEmptyArray: Array?, + @AvroDefault("""["John", "Doe"]""") + val defaultStringArrayWith2Defaults: Array, + @AvroDefault("""[1, 2]""") + val defaultIntArrayWith2Defaults: Array, + @AvroDefault("""[3.14, 9.89]""") + val defaultFloatArrayWith2Defaults: Array, + // Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in + // the default + // See https://issues.apache.org/jira/browse/AVRO-2647 + @AvroDefault("""[null]""") + val defaultStringArrayWithNullableTypes: Array, + ) + + @Serializable + private data class BarInvalidArrayType( + @AvroDefault("""["foo-bar"]""") + val defaultFloatArrayWith2Defaults: List, + ) + + @Serializable + private data class BarInvalidNonArrayType( + @AvroDefault("{}") + val defaultBarArrayWithNonWorkingDefaults: List = ArrayList(), + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDocSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDocSchemaTest.kt index da22fdb3..ded56e82 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDocSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroDocSchemaTest.kt @@ -1,45 +1,46 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions import com.github.avrokotlin.avro4k.AvroDoc -import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.WordSpec import kotlinx.serialization.Serializable - -class AvroDocSchemaTest : WordSpec({ - - "@AvroDoc" should { - "support doc annotation on class" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/doc_annotation_class.json")) - val schema = Avro.default.schema(TypeAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) +import kotlin.io.path.Path + +internal class AvroDocSchemaTest : WordSpec({ + + "@AvroDoc" should { + "support doc annotation on class" { + AvroAssertions.assertThat() + .generatesSchema(Path("/doc_annotation_class.json")) + } + "support doc annotation on field" { + AvroAssertions.assertThat() + .generatesSchema(Path("/doc_annotation_field.json")) + } + "support doc annotation on nested class" { + AvroAssertions.assertThat() + .generatesSchema(Path("/doc_annotation_field_struct.json")) + } } - "support doc annotation on field" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/doc_annotation_field.json")) - val schema = Avro.default.schema(FieldAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support doc annotation on nested class" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/doc_annotation_field_struct.json")) - val schema = Avro.default.schema(NestedAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - }) { - @AvroDoc("hello; is it me youre looking for") - @Serializable - data class TypeAnnotated(val str: String) - - @Serializable - data class FieldAnnotated(@AvroDoc("hello its me") val str: String, @AvroDoc("I am a long") val long: Long, val int: Int) - - @Serializable - data class Nested(@AvroDoc("b") val foo: String) - - @Serializable - data class NestedAnnotated(@AvroDoc("c") val nested: Nested) -} + @AvroDoc("hello; is it me youre looking for") + @Serializable + private data class TypeAnnotated(val str: String) + + @Serializable + private data class FieldAnnotated( + @AvroDoc("hello its me") val str: String, + @AvroDoc("I am a long") val long: Long, + val int: Int, + ) + + @Serializable + private data class Nested( + @AvroDoc("b") val foo: String, + ) + + @Serializable + private data class NestedAnnotated( + @AvroDoc("c") val nested: Nested, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroFixedSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroFixedSchemaTest.kt deleted file mode 100644 index 98fa36d1..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroFixedSchemaTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroFixed -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.WordSpec -import kotlinx.serialization.Serializable - -class AvroFixedSchemaTest : WordSpec({ - - "@AvroFixed" should { - - "generated fixed field schema when used on a field" { - - val schema = Avro.default.schema(FixedStringField.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/fixed_string.json")) - schema.toString(true) shouldBe expected.toString(true) - } - - "generated fixed schema when an annotated type is used as the type in a field" { - - val schema = Avro.default.schema(Foo.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/fixed_string_value_type_as_field.json")) - schema.toString(true) shouldBe expected.toString(true) - } - } -}) { - @Serializable - data class FixedStringField(@AvroFixed(7) val mystring: String) - - @AvroFixed(8) - @Serializable - data class FixedClass(val bytes: ByteArray) - - @Serializable - data class Foo(val z: FixedClass) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroInlineSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroInlineSchemaTest.kt deleted file mode 100644 index 96115249..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroInlineSchemaTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroInline -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class AvroInlineSchemaTest : FunSpec({ - - test("support @AvroInline") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/value_type.json")) - val schema = Avro.default.schema(Product.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - -}) { - - @Serializable - @AvroInline - data class Name(val value: String) - - @Serializable - data class Product(val id: String, val name: Name) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroJsonPropSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroJsonPropSchemaTest.kt deleted file mode 100644 index 2a1a6516..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroJsonPropSchemaTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroJsonProp -import io.kotest.core.spec.style.WordSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable - -class AvroJsonPropSchemaTest : WordSpec() { - - enum class Colours { - Red, Green, Blue - } - - init { - "@AvroJsonProp" should { - "support props annotation on class" { - - val expected = org.apache.avro.Schema.Parser() - .parse(javaClass.getResourceAsStream("/props_json_annotation_class.json")) - val schema = Avro.default.schema(TypeAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support props annotation on field" { - - val expected = org.apache.avro.Schema.Parser() - .parse(javaClass.getResourceAsStream("/props_json_annotation_field.json")) - val schema = Avro.default.schema(AnnotatedProperties.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support props annotations on enums" { - - val expected = org.apache.avro.Schema.Parser() - .parse(javaClass.getResourceAsStream("/props_json_annotation_scala_enum.json")) - val schema = Avro.default.schema(EnumAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - } - - @Serializable - @AvroJsonProp("guns", """["and", "roses"]""") - data class TypeAnnotated(val str: String) - - @Serializable - data class AnnotatedProperties( - @AvroJsonProp("guns", """["and", "roses"]""") val str: String, - @AvroJsonProp("jean", """["michel", "jarre"]""") val long: Long, - @AvroJsonProp( - key = "object", - jsonValue = """{ - "a": "foo", - "b": 200, - "c": true, - "d": null, - "e": { "e1": null, "e2": 429 }, - "f": ["bar", 404, false, null, {}] - }""" - ) - val int: Int - ) - - @Serializable - data class EnumAnnotated(@AvroJsonProp("guns", """["and", "roses"]""") val colours: Colours) -} 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 d857e4b9..c09d0acf 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNameTest.kt @@ -1,33 +1,21 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.io.path.Path -class AvroNameSchemaTest : FunSpec({ - - test("generate field names using @AvroName") { - - val schema = Avro.default.schema(FieldNamesFoo.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_name_field.json")) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate class names using @AvroName") { - - val schema = Avro.default.schema(ClassNameFoo.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_name_class.json")) - schema.toString(true) shouldBe expected.toString(true) - } +internal class AvroNameSchemaTest : FunSpec({ + test("Change field and class name") { + AvroAssertions.assertThat() + .generatesSchema(Path("/avro_name_field.json")) + } }) { - - @Serializable - data class FieldNamesFoo(@AvroName("foo") val wibble: String, val wobble: String) - - @AvroName("wibble") - @Serializable - data class ClassNameFoo(val a: String, val b: String) -} + @Serializable + @SerialName("anotherRecordName") + private data class FieldNamesFoo( + @SerialName("foo") val wibble: 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 deleted file mode 100644 index 1ef96fd3..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -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 io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class AvroNamespaceSchemaTest : FunSpec({ - - test("support namespace annotations on records") { - - val schema = Avro.default.schema(AnnotatedNamespace.serializer()) - schema.namespace shouldBe "com.yuval" - } - - test("support namespace annotations in nested records") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/namespace.json")) - val schema = Avro.default.schema(NestedAnnotatedNamespace.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support namespace annotations on field") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/namespace.json")) - val schema = Avro.default.schema(InternalAnnotatedNamespace.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("favour namespace annotations on field over record") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/namespace.json")) - val schema = Avro.default.schema(FieldAnnotatedNamespace.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("empty namespace") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/namespace_empty.json")) - val schema = Avro.default.schema(Foo.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - -}) { - - @AvroNamespace("com.yuval") - @Serializable - data class AnnotatedNamespace(val s: String) - - @AvroNamespace("com.yuval.internal") - @Serializable - data class InternalAnnotated(val i: Int) - - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") - @Serializable - data class NestedAnnotatedNamespace(val s: String, val internal: InternalAnnotated) - - @Serializable - @AvroName("InternalAnnotated") - data class Internal(val i: Int) - - @Serializable - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") - data class InternalAnnotatedNamespace(val s: String, - @AvroNamespace("com.yuval.internal") val internal: Internal) - - @Serializable - @AvroName("InternalAnnotated") - @AvroNamespace("ignore") - data class InternalIgnoreAnnotated(val i: Int) - - @Serializable - @AvroName("AnnotatedNamespace") - @AvroNamespace("com.yuval") - data class FieldAnnotatedNamespace(val s: String, - @AvroNamespace("com.yuval.internal") val internal: InternalIgnoreAnnotated) - - @AvroNamespace("") - @Serializable - data class Foo(val s: String) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropSchemaTest.kt deleted file mode 100644 index 8af8075d..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropSchemaTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroProp -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.WordSpec -import kotlinx.serialization.Serializable - -class AvroPropSchemaTest : WordSpec() { - - enum class Colours { - Red, Green, Blue - } - - init { - "@AvroProp" should { - "support prop annotation on class" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/props_annotation_class.json")) - val schema = Avro.default.schema(TypeAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support prop annotation on field" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/props_annotation_field.json")) - val schema = Avro.default.schema(AnnotatedProperties.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "support props annotations on enums" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/props_annotation_scala_enum.json")) - val schema = Avro.default.schema(EnumAnnotated.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - } - - @Serializable - @AvroProp("cold", "play") - data class TypeAnnotated(val str: String) - - @Serializable - data class AnnotatedProperties( - @AvroProp("cold", "play") val str: String, - @AvroProp("kate", "bush") val long: Long, - val int: Int) - - @Serializable - data class EnumAnnotated(@AvroProp("cold", "play") val colours: Colours) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt new file mode 100644 index 00000000..4cb8a1da --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt @@ -0,0 +1,157 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroEnumDefault +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.AvroProp +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder +import kotlin.io.path.Path + +internal class AvroPropsSchemaTest : StringSpec({ + "should support props annotation on class" { + AvroAssertions.assertThat() + .generatesSchema(Path("/props_json_annotation_class.json")) + } + "should add props on the contained type of a value class" { + val stringSchema = + Schema.create(Schema.Type.STRING).also { + it.addProp("cold", "play") + } + + AvroAssertions.assertThat() + .generatesSchema(stringSchema) + AvroAssertions.assertThat() + .generatesSchema(stringSchema) + AvroAssertions.assertThat() + .generatesSchema(stringSchema) + } + "props in value class is only applying to underlying type but not the enclosing record field" { + val stringSchema = + Schema.create(Schema.Type.STRING).also { + it.addProp("cold", "play") + } + + @SerialName("SimpleDataClass") + @Serializable + data class SimpleDataClass( + val customPropOnClass: CustomPropOnClass, + val customPropOnField: CustomPropOnField, + ) + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("SimpleDataClass").fields() + .name("customPropOnClass").type(stringSchema).noDefault() + .name("customPropOnField").type(stringSchema).noDefault() + .endRecord() + ) + } + "when props are added to a record using props in value class and also reusing the record but without the same props fails" { + @Serializable + data class SimpleDataClass( + val basicRecord: BasicRecord, + val basicRecordWithProps: BasicRecordWithProps, + ) + shouldThrow { + Avro.schema().toString() + } + } + "forbid adding props to a named schema in a value class" { + shouldThrow { + Avro.schema>().toString() + } + shouldThrow { + Avro.schema>().toString() + } + shouldThrow { + Avro.schema().toString() + } + } +}) { + @JvmInline + @Serializable + private value class ValueClassWithProps( + @AvroProp("key", "value") + val value: T, + ) + + @JvmInline + @Serializable + private value class FixedValueClassWithProps( + @AvroProp("key", "value") + @AvroFixed(10) + val value: ByteArray, + ) + + @Serializable + @SerialName("BasicRecord") + private data class BasicRecord( + val field: String, + ) + + @JvmInline + @Serializable + private value class BasicRecordWithProps( + @AvroProp("cold", "play") + val value: BasicRecord, + ) + + @JvmInline + @Serializable + @AvroProp("cold", "play") + private value class CustomPropOnClass( + val value: String, + ) + + @JvmInline + @Serializable + @AvroProp("cold", "ignored") + private value class CustomPropOnFieldPriorToClass( + @AvroProp("cold", "play") + val value: String, + ) + + @JvmInline + @Serializable + private value class CustomPropOnField( + @AvroProp("cold", "play") + val value: String, + ) + + @Serializable + @AvroProp("hey", "there") + @AvroProp("counting", """["one", "two"]""") + private data class TypeAnnotated( + @AvroProp("cold", "play") + @AvroProp( + key = "complexObject", + value = """{ + "a": "foo", + "b": 200, + "c": true, + "d": null, + "e": { "e1": null, "e2": 429 }, + "f": ["bar", 404, false, null, {}] + }""" + ) + val field: EnumAnnotated, + ) + + @Serializable + @AvroProp("enums", "power") + @AvroProp("countingAgain", """["three", "four"]""") + private enum class EnumAnnotated { + Red, + + @AvroEnumDefault + Green, + Blue, + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BasicSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BasicSchemaTest.kt index 30f4af86..ba2dec4d 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BasicSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BasicSchemaTest.kt @@ -1,104 +1,68 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.io.AvroDecodeFormat -import com.github.avrokotlin.avro4k.io.AvroEncodeFormat +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe import kotlinx.serialization.Serializable -import java.io.File +import kotlin.io.path.Path -fun main() { +internal class BasicSchemaTest : FunSpec({ - val veg = Pizza("veg", listOf(Ingredient("peppers", 0.1, 0.3), Ingredient("onion", 1.0, 0.4)), true, 265) - val hawaiian = Pizza("hawaiian", listOf(Ingredient("ham", 1.5, 5.6), Ingredient("pineapple", 5.2, 0.2)), false, 391) + test("schema for basic types") { + AvroAssertions.assertThat() + .generatesSchema(Path("/basic.json")) + } - val writeSchema = Avro.default.schema(Pizza.serializer()) + test("accept nested case classes") { + AvroAssertions.assertThat() + .generatesSchema(Path("/nested.json")) + } - val output = Avro.default.openOutputStream(Pizza.serializer()){ - encodeFormat = AvroEncodeFormat.Binary - schema = writeSchema - }.to(File("pizzas.avro")) - output.write(listOf(veg, hawaiian)) - output.close() + test("accept multiple nested case classes") { + AvroAssertions.assertThat() + .generatesSchema(Path("/nested_multiple.json")) + } - val input = Avro.default.openInputStream(Pizza.serializer()){ - decodeFormat = AvroDecodeFormat.Binary(writeSchema, defaultReadSchema) - }.from(File("pizzas.avro")) - input.iterator().forEach { println(it) } - input.close() -} - -@Serializable -data class Ingredient(val name: String, val sugar: Double, val fat: Double) - -@Serializable -data class Pizza(val name: String, val ingredients: List, val vegetarian: Boolean, val kcals: Int) - -@Serializable -data class Nested(val goo: String) - -@Serializable -data class Test(val foo: String, val nested: Nested) - -class BasicSchemaTest : FunSpec({ - - test("schema for basic types") { - - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/basic.json")) - val schema = Avro.default.schema(Foo.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept nested case classes") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/nested.json")) - val schema = Avro.default.schema(Test.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept multiple nested case classes") { - - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/nested_multiple.json")) - val schema = Avro.default.schema(Outer.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } + test("accept deep nested structure") { + AvroAssertions.assertThat() + .generatesSchema(Path("/deepnested.json")) + } +}) { + @Serializable + private data class Nested(val goo: String) - test("accept deep nested structure") { + @Serializable + private data class Test(val foo: String, val nested: Nested) - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/deepnested.json")) - val schema = Avro.default.schema(Level1.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) { @Serializable - data class Foo(val a: String, + private data class Foo( + val a: String, val b: Double, val c: Boolean, val d: Float, val e: Long, val f: Int, val g: Short, - val h: Byte) - + val h: Byte, + ) @Serializable - data class Inner(val goo: String) + private data class Inner(val goo: String) @Serializable - data class Middle(val inner: Inner) + private data class Middle(val inner: Inner) @Serializable - data class Outer(val middle: Middle) + private data class Outer(val middle: Middle) @Serializable - data class Level4(val str: String) + private data class Level4(val str: String) @Serializable - data class Level3(val level4: Level4) + private data class Level3(val level4: Level4) @Serializable - data class Level2(val level3: Level3) + private data class Level2(val level3: Level3) @Serializable - data class Level1(val level2: Level2) -} + private data class Level1(val level2: Level2) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt index b4a834c9..3a6aa220 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt @@ -1,43 +1,56 @@ -@file:UseSerializers(BigDecimalSerializer::class) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.ScalePrecision -import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecimal +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers +import org.apache.avro.LogicalTypes +import org.apache.avro.Schema import java.math.BigDecimal -class BigDecimalSchemaTest : FunSpec({ - - test("accept big decimal as logical type on bytes") { - - val schema = Avro.default.schema(BigDecimalTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/bigdecimal.json")) - schema shouldBe expected - } - test("accept big decimal as logical type on bytes with custom scale and precision") { - - val schema = Avro.default.schema(BigDecimalPrecisionTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/bigdecimal-scale-and-precision.json")) - schema shouldBe expected - } - test("support nullable BigDecimal as a union") { - - val schema = Avro.default.schema(NullableBigDecimalTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/bigdecimal_nullable.json")) - schema shouldBe expected - } +internal class BigDecimalSchemaTest : FunSpec({ + test("support BigDecimal logical types") { + AvroAssertions.assertThat() + .generatesSchema(LogicalTypes.decimal(8, 2).addToSchema(Schema.create(Schema.Type.BYTES))) + } + + test("support BigDecimal logical types as fixed") { + AvroAssertions.assertThat() + .generatesSchema(LogicalTypes.decimal(5, 3).addToSchema(Schema.createFixed("field", null, null, 5))) + } + + test("support nullable BigDecimal logical types") { + AvroAssertions.assertThat() + .generatesSchema(LogicalTypes.decimal(2, 1).addToSchema(Schema.create(Schema.Type.BYTES)).nullable) + } }) { - @Serializable - data class BigDecimalTest(val decimal: BigDecimal) - - @Serializable - data class BigDecimalPrecisionTest(@ScalePrecision(1, 4) val decimal: BigDecimal) - - @Serializable - data class NullableBigDecimalTest(val decimal: BigDecimal?) -} + @JvmInline + @Serializable + private value class BigDecimalTest( + @AvroDecimal(scale = 2, precision = 8) + @Contextual + val bigDecimal: BigDecimal, + ) + + @JvmInline + @Serializable + @SerialName("BigDecimalFixedTest") + private value class BigDecimalFixedTest( + @AvroDecimal(scale = 3, precision = 5) + @AvroFixed(5) + @Contextual + val field: BigDecimal, + ) + + @JvmInline + @Serializable + private value class BigDecimalNullableTest( + @AvroDecimal(scale = 1, precision = 2) + @Contextual + val bigDecimal: BigDecimal?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt index a764c190..9621246f 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt @@ -1,34 +1,33 @@ -@file:UseSerializers(BigIntegerSerializer::class) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.BigIntegerSerializer -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers +import org.apache.avro.Schema import java.math.BigInteger -class BigIntegerSchemaTest : FunSpec({ - - test("accept big integer as String") { - - val schema = Avro.default.schema(Test.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/bigint.json")) - schema shouldBe expected - } +internal class BigIntegerSchemaTest : FunSpec({ + test("support BigInteger as string") { + AvroAssertions.assertThat() + .generatesSchema(Schema.create(Schema.Type.STRING)) + } - test("accept nullable big integer as String union") { - - val schema = Avro.default.schema(NullableTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/bigint_nullable.json")) - schema shouldBe expected - } + test("support nullable BigInteger as string") { + AvroAssertions.assertThat() + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + } }) { - @Serializable - data class Test(val b: BigInteger) - - @Serializable - data class NullableTest(val b: BigInteger?) -} + @JvmInline + @Serializable + private value class BigIntegerTest( + @Contextual val bigInteger: BigInteger, + ) + + @JvmInline + @Serializable + private value class BigIntegerNullableTest( + @Contextual val bigInteger: BigInteger?, + ) +} \ 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 deleted file mode 100644 index f3423b78..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ByteArraySchemaTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroName -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class ByteArraySchemaTest : FunSpec({ - - test("encode byte arrays as BYTES type") { - - val schema = Avro.default.schema(ByteArrayTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/byte_array.json")) - schema.toString(true) shouldBe expected.toString(true) - } - - test("encode lists as BYTES type") { - - val schema = Avro.default.schema(ByteListTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/byte_array.json")) - schema.toString(true) shouldBe expected.toString(true) - } - -// test("support top level byte arrays") { -// @Serializable -// val schema = Avro.default.schema(Array[Byte.serializer())] -// val expected = new org . apache . avro . Schema . Parser ().parse(getClass.getResourceAsStream("/top_level_byte_array.json")) -// schema.toString(true) shouldBe expected.toString(true) -// } - -// test("encode ByteBuffer as BYTES type") { -// @Serializable -// data class Test(val z: ByteBuffer) -// -// val schema = Avro.default.schema(Test.serializer()) -// val expected = new org . apache . avro . Schema . Parser ().parse(getClass.getResourceAsStream("/bytebuffer.json")) -// schema.toString(true) shouldBe expected.toString(true) -// } - -// test("support top level ByteBuffers") { -// val schema = Avro.default.schema(ByteBuffer.serializer()) -// val expected = new org . apache . avro . Schema . Parser ().parse(getClass.getResourceAsStream("/top_level_bytebuffer.json")) -// schema.toString(true) shouldBe expected.toString(true) -// } - -}) { - @Serializable - data class ByteArrayTest(val z: ByteArray) - - @Serializable - @AvroName("ByteArrayTest") - data class ByteListTest(val z: List) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt new file mode 100644 index 00000000..e58f8f89 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt @@ -0,0 +1,65 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable +import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.serializer +import org.apache.avro.Schema + +internal class BytesSchemaTest : FunSpec({ + listOf( + WrappedObjectByteArray.serializer(), + serializer>(), + WrappedByteList.serializer(), + serializer>(), + WrappedByteCollection.serializer(), + serializer>(), + WrappedByteSet.serializer(), + serializer>() + ).forEach { serializer -> + test("encode ${serializer.descriptor} as ARRAY[INT] type instead of BYTES") { + AvroAssertions.assertThat(serializer) + .generatesSchema(Schema.createArray(Schema.create(Schema.Type.INT))) + } + test("encode nullable ${serializer.descriptor} as ARRAY[INT] type instead of BYTES") { + AvroAssertions.assertThat(serializer.nullable) + .generatesSchema(Schema.createArray(Schema.create(Schema.Type.INT)).nullable) + } + } + + listOf( + WrappedByteArray.serializer(), + serializer() + ).forEach { serializer -> + test("encode ${serializer.descriptor} as BYTES type") { + AvroAssertions.assertThat(serializer) + .generatesSchema(Schema.create(Schema.Type.BYTES)) + } + test("encode nullable ${serializer.descriptor} as BYTES type") { + AvroAssertions.assertThat(serializer.nullable) + .generatesSchema(Schema.create(Schema.Type.BYTES).nullable) + } + } +}) { + @JvmInline + @Serializable + private value class WrappedByteArray(val value: ByteArray) + + @JvmInline + @Serializable + private value class WrappedObjectByteArray(val value: Array) + + @JvmInline + @Serializable + private value class WrappedByteList(val value: List) + + @JvmInline + @Serializable + private value class WrappedByteCollection(val value: Collection) + + @JvmInline + @Serializable + private value class WrappedByteSet(val value: Set) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ContextualSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ContextualSchemaTest.kt index 08a1c490..fa928a9f 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ContextualSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ContextualSchemaTest.kt @@ -1,52 +1,47 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.InstantSerializer -import com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer -import io.kotest.assertions.throwables.shouldThrow +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual -import org.apache.avro.Schema -import java.lang.IllegalArgumentException import java.time.Instant +import kotlin.io.path.Path -class ContextualSchemaTest : StringSpec({ +internal class ContextualSchemaTest : StringSpec({ "schema for contextual serializer" { - - val format1 = Avro( - serializersModule = SerializersModule { - contextual(InstantSerializer()) - } - ) - - val format2 = Avro( - serializersModule = SerializersModule { - contextual(InstantToMicroSerializer()) - } - ) - - val schema1 = format1.schema(Test.serializer()) - val schema2 = format2.schema(Test.serializer()) - - shouldThrow { - Avro.default.schema(Test.serializer()) - } - val expected1 = Schema.Parser().parse(javaClass.getResourceAsStream("/contextual_1.json")) - schema1 shouldBe expected1 - val expected2 = Schema.Parser().parse(javaClass.getResourceAsStream("/contextual_2.json")) - schema2 shouldBe expected2 + AvroAssertions.assertThat() + .withConfig { serializersModule = SerializersModule { contextual(MySerializer) } } + .generatesSchema(Path("/contextual_1.json")) } }) { @Serializable - data class Test( + private data class Test( @Contextual val ts: Instant, @Contextual val withFallback: Int?, ) -} + + private object MySerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("MySerializer", PrimitiveKind.BOOLEAN) + + override fun deserialize(decoder: Decoder): Instant { + TODO("Not yet implemented") + } + + override fun serialize( + encoder: Encoder, + value: Instant, + ) { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt index 46383280..829c8cd5 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt @@ -1,96 +1,33 @@ -@file:UseSerializers( - LocalDateSerializer::class, - LocalTimeSerializer::class, - TimestampSerializer::class, - InstantSerializer::class, - LocalDateTimeSerializer::class -) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.* -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.serializer.InstantSerializer +import com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer +import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer +import com.github.avrokotlin.avro4k.serializer.LocalDateTimeSerializer +import com.github.avrokotlin.avro4k.serializer.LocalTimeSerializer import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.sql.Timestamp -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime - -class DateSchemaTest : FunSpec({ - - test("generate date logical type for LocalDate") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/localdate.json")) - val schema = Avro.default.schema(LocalDateTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate date logical type for nullable LocalDate") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/localdate_nullable.json")) - val schema = Avro.default.schema(NullableLocalDateTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate time logical type for LocalTime") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/localtime.json")) - val schema = Avro.default.schema(LocalTimeTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate time logical type for LocalDateTime") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/localdatetime.json")) - val schema = Avro.default.schema(LocalDateTimeTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate timestamp-millis logical type for Instant") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/instant.json")) - val schema = Avro.default.schema(InstantTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate timestamp-millis logical type for Timestamp") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/timestamp.json")) - val schema = Avro.default.schema(TimestampTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate timestamp-millis logical type for nullable Timestamp") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/timestamp_nullable.json")) - val schema = Avro.default.schema(Test.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) { - - @Serializable - data class LocalDateTest(val date: LocalDate) - - @Serializable - data class NullableLocalDateTest(val date: LocalDate?) - - @Serializable - data class LocalTimeTest(val time: LocalTime) - - @Serializable - data class LocalDateTimeTest(val time: LocalDateTime) - - @Serializable - data class InstantTest(val instant: Instant) - - - @Serializable - data class TimestampTest(val ts: Timestamp) - - @Serializable - data class Test(val ts: Timestamp?) -} +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.nullable +import org.apache.avro.LogicalTypes +import org.apache.avro.Schema + +internal class DateSchemaTest : FunSpec({ + listOf( + LocalDateSerializer to LogicalTypes.date().addToSchema(Schema.create(Schema.Type.INT)), + LocalTimeSerializer to LogicalTypes.timeMillis().addToSchema(Schema.create(Schema.Type.INT)), + LocalDateTimeSerializer to LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG)), + InstantSerializer to LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG)), + InstantToMicroSerializer to LogicalTypes.timestampMicros().addToSchema(Schema.create(Schema.Type.LONG)) + ).forEach { (serializer: KSerializer<*>, expected) -> + test("generate date logical type for $serializer") { + AvroAssertions.assertThat(serializer) + .generatesSchema(expected) + } + test("generate nullable date logical type for $serializer") { + AvroAssertions.assertThat(serializer.nullable) + .generatesSchema(expected.nullable) + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DefaultSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DefaultSchemaTest.kt deleted file mode 100644 index 195f9b70..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DefaultSchemaTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:UseSerializers( - TimestampSerializer::class -) - -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.TimestampSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import java.sql.Timestamp -import java.time.Instant - -class DefaultSchemaTest : FunSpec() { - init { - test("schema for data class should include fields that define a default") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/default.json")) - val schema = Avro.default.schema(Foo.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } -} - -@Serializable -data class Foo( - val a: String, - val b: String = "hello", - val c: Timestamp = Timestamp.from(Instant.now()), - val d: Timestamp -) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt index 848b5378..122943dc 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt @@ -2,103 +2,55 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAlias -import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.AvroAssertions import com.github.avrokotlin.avro4k.AvroDoc import com.github.avrokotlin.avro4k.AvroEnumDefault +import com.github.avrokotlin.avro4k.RecordWithGenericField +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.schema import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.WordSpec -import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.Serializable - -class EnumSchemaTest : WordSpec({ - - "SchemaEncoder" should { - "accept enums" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/enum.json")) - val schema = Avro.default.schema(EnumTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - "Enum with documentation and aliases" should { - - val expected = - org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/enum_with_documentation.json")) - val schema = Avro.default.schema(EnumWithDocuTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - "Enum with default values" should { - "generate schema" { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/enum_with_default.json")) - - val schema = Avro.default.schema(EnumWithDefaultTest.serializer()) - - schema.toString(true) shouldBe expected.toString(true) - } - "generate schema with default and nullable union types" { - - val expected = - org.apache.avro.Schema.Parser() - .parse(javaClass.getResourceAsStream("/enum_with_default_value_and_null.json")) - - val schema = Avro.default.schema(EnumWithAvroDefaultTest.serializer()) - - schema.toString(true) shouldBe expected.toString(true) - } - "modifying namespaces retains enum defaults" { - val schemaWithNewNameSpace = Avro.default.schema(EnumWithDefaultTest.serializer()).overrideNamespace("new") - - val expected = org.apache.avro.Schema.Parser() - .parse(javaClass.getResourceAsStream("/enum_with_default_new_namespace.json")) - - schemaWithNewNameSpace.toString(true) shouldBe expected.toString(true) - } - "fail with unknown values" { - shouldThrow { - Avro.default.schema(EnumWithUnknownDefaultTest.serializer()) - } - } - } +import kotlin.io.path.Path + +internal class EnumSchemaTest : StringSpec({ + "should generate schema with alias, enum default and doc" { + AvroAssertions.assertThat() + .generatesSchema(Path("/enum_with_default.json")) + AvroAssertions.assertThat>() + .generatesSchema(Path("/enum_with_default_record.json")) + } + "should generate nullable schema" { + AvroAssertions.assertThat() + .generatesSchema(Path("/enum_with_default.json")) { it.nullable } + } + "fail with unknown values" { + shouldThrow { + Avro.schema() + } + shouldThrow { + Avro.schema>() + } + } }) { - - @Serializable - data class EnumTest(val wine: Wine) - @Serializable - data class EnumWithDocuTest( - val value: Suit - ) - @Serializable - data class EnumWithDefaultTest( - val type: IngredientType - ) - @Serializable - data class EnumWithAvroDefaultTest( - @AvroDefault(Avro.NULL) val type: IngredientType? - ) - @Serializable - data class EnumWithUnknownDefaultTest( - val type: InvalidIngredientType - ) - -} - -enum class Wine { - Malbec, Shiraz, CabSav, Merlot -} - -@Serializable -@AvroAlias("MySuit") -@AvroDoc("documentation") -enum class Suit { - SPADES, HEARTS, DIAMONDS, CLUBS; -} - -@Serializable -@AvroEnumDefault("MEAT") -enum class IngredientType { VEGGIE, MEAT, } - -@Serializable -@AvroEnumDefault("PINEAPPLE") -enum class InvalidIngredientType { VEGGIE, MEAT, } + @Serializable + @AvroAlias("MySuit") + @AvroDoc("documentation") + private enum class Suit { + SPADES, + HEARTS, + + @AvroEnumDefault + DIAMONDS, + CLUBS, + } + + @Serializable + private enum class InvalidEnumDefault { + @AvroEnumDefault + VEGGIE, + + @AvroEnumDefault + MEAT, + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/FieldNamingStrategySchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/FieldNamingStrategySchemaTest.kt new file mode 100644 index 00000000..f4233bb7 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/FieldNamingStrategySchemaTest.kt @@ -0,0 +1,32 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.FieldNamingStrategy +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable +import kotlin.io.path.Path + +internal class FieldNamingStrategySchemaTest : StringSpec({ + "should convert schema with snake_case to camelCase" { + AvroAssertions.assertThat() + .withConfig { fieldNamingStrategy = FieldNamingStrategy.Builtins.SnakeCase } + .generatesSchema(Path("/snake_case_schema.json")) + } + + "should convert schema with PascalCase to camelCase" { + AvroAssertions.assertThat() + .withConfig { fieldNamingStrategy = FieldNamingStrategy.Builtins.PascalCase } + .generatesSchema(Path("/pascal_case_schema.json")) + } +}) { + @Serializable + private data class Interface( + val name: String, + val ipv4Address: String, + val ipv4SubnetMask: Int, + val v: InternetProtocol, + ) + + @Serializable + private enum class InternetProtocol { IPv4, IPv6 } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ImplicitConfigSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ImplicitConfigSchemaTest.kt new file mode 100644 index 00000000..227f1a5b --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ImplicitConfigSchemaTest.kt @@ -0,0 +1,180 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.WrappedInt +import com.github.avrokotlin.avro4k.decodeFromByteArray +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder + +internal class ImplicitConfigSchemaTest : FunSpec({ + test("Should set default value to null for nullable fields when implicitNulls is true (default)") { + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("ImplicitNulls").fields() + .name("string").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .name("boolean").type(Schema.create(Schema.Type.BOOLEAN).nullable).withDefault(null) + .name("booleanWrapped1").type(Schema.create(Schema.Type.BOOLEAN).nullable).withDefault(null) + .name("intWrapped").type(Schema.create(Schema.Type.INT).nullable).withDefault(null) + .name("doubleWrapped").type(Schema.create(Schema.Type.DOUBLE).nullable).withDefault(null) + .name("nested").type( + SchemaBuilder.record("Nested").fields() + .name("string").type(Schema.create(Schema.Type.STRING).nullable).withDefault(null) + .name("boolean").type(Schema.create(Schema.Type.BOOLEAN).nullable).withDefault(null) + .endRecord().nullable + ).withDefault(null) + .name("nullableList").type(Schema.createArray(Schema.create(Schema.Type.STRING)).nullable).withDefault(null) + .name("nullableMap").type(Schema.createMap(Schema.create(Schema.Type.STRING)).nullable).withDefault(null) + .name("stringWithAvroDefault").type().nullable().stringType().stringDefault("implicit nulls bypassed") + .endRecord() + ) + AvroAssertions.assertThat(EmptyType) + .isDecodedAs( + ImplicitNulls( + string = null, + boolean = null, + booleanWrapped1 = NullableBooleanWrapper(null), + intWrapped = null, + doubleWrapped = null, + nested = null, + nullableList = null, + nullableMap = null, + stringWithAvroDefault = "implicit nulls bypassed" + ) + ) + } + + test("Should fail for missing nullable fields when implicitNulls is false. Collections are still having their implicit default") { + AvroAssertions.assertThat() + .withConfig { + implicitNulls = false + } + .generatesSchema( + SchemaBuilder.record("ImplicitNulls").fields() + .name("string").type(Schema.create(Schema.Type.STRING).nullable).noDefault() + .name("boolean").type(Schema.create(Schema.Type.BOOLEAN).nullable).noDefault() + .name("booleanWrapped1").type(Schema.create(Schema.Type.BOOLEAN).nullable).noDefault() + .name("intWrapped").type(Schema.create(Schema.Type.INT).nullable).noDefault() + .name("doubleWrapped").type(Schema.create(Schema.Type.DOUBLE).nullable).noDefault() + .name("nested").type( + SchemaBuilder.record("Nested").fields() + .name("string").type(Schema.create(Schema.Type.STRING).nullable).noDefault() + .name("boolean").type(Schema.create(Schema.Type.BOOLEAN).nullable).noDefault() + .endRecord().nullable + ).noDefault() + .name("nullableList").type().nullable().array().items().stringType().arrayDefault(emptyList()) + .name("nullableMap").type().nullable().map().values().stringType().mapDefault(emptyMap()) + .name("stringWithAvroDefault").type().nullable().stringType().stringDefault("implicit nulls bypassed") + .endRecord() + ) + val avro = + Avro { + implicitNulls = false + } + val bytes = avro.encodeToByteArray(EmptyType) + shouldThrow { + avro.decodeFromByteArray(writerSchema = avro.schema(), bytes) + } + } + + test("Should set default value to empty array for Collection fields when implicitEmptyCollections is true (default)") { + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("ImplicitEmptyCollections").fields() + .name("list").type().array().items().stringType().arrayDefault(emptyList()) + .name("set").type().array().items().stringType().arrayDefault(emptyList()) + .name("collection").type().array().items().stringType().arrayDefault(emptyList()) + .name("map").type().map().values().stringType().mapDefault(emptyMap()) + .endRecord() + ) + AvroAssertions.assertThat(EmptyCollectionType) + .isDecodedAs( + ImplicitEmptyCollections( + list = emptyList(), + set = emptySet(), + collection = emptyList(), + map = emptyMap() + ) + ) + } + + test("Should not set default value for Collection fields when implicitEmptyCollections is false") { + AvroAssertions.assertThat() + .withConfig { + implicitEmptyCollections = false + } + .generatesSchema( + SchemaBuilder.record("ImplicitEmptyCollections").fields() + .name("list").type().array().items().stringType().noDefault() + .name("set").type().array().items().stringType().noDefault() + .name("collection").type().array().items().stringType().noDefault() + .name("map").type().map().values().stringType().noDefault() + .endRecord() + ) + val avro = + Avro { + implicitEmptyCollections = false + } + val bytes = avro.encodeToByteArray(EmptyCollectionType) + shouldThrow { + avro.decodeFromByteArray(writerSchema = avro.schema(), bytes) + } + } +}) { + @Serializable + @SerialName("ImplicitNulls") + private data object EmptyType + + @Serializable + @SerialName("ImplicitNulls") + private data class ImplicitNulls( + val string: String?, + val boolean: Boolean?, + val booleanWrapped1: NullableBooleanWrapper, + val intWrapped: WrappedInt?, + val doubleWrapped: NullableDoubleWrapper?, + val nested: Nested?, + val nullableList: List?, + val nullableMap: Map?, + @AvroDefault("implicit nulls bypassed") + val stringWithAvroDefault: String?, + ) + + @Serializable + @SerialName("ImplicitEmptyCollections") + private data object EmptyCollectionType + + @Serializable + @SerialName("ImplicitEmptyCollections") + private data class ImplicitEmptyCollections( + val list: List, + val set: Set, + val collection: Collection, + val map: Map, + ) + + @JvmInline + @Serializable + private value class NullableBooleanWrapper(val value: Boolean?) + + @JvmInline + @Serializable + private value class NullableDoubleWrapper(val value: Double?) + + @Serializable + @SerialName("Nested") + private data class Nested( + val string: String?, + val boolean: Boolean?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/InlineClassSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/InlineClassSchemaTest.kt deleted file mode 100644 index fa682eba..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/InlineClassSchemaTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -@file:UseSerializers(UUIDSerializer::class) - -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroInline -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers - -class InlineClassSchemaTest : FunSpec({ - - test("support inline types") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/inline_type.json")) - val schema = Avro.default.schema(Product.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - -}) { - @Serializable - @AvroInline - data class Name(val value: String) - - @Serializable - data class Product(val id: String, val name: Name) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MapSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MapSchemaTest.kt deleted file mode 100644 index 8e3caa2f..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MapSchemaTest.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -class MapSchemaTest : FunSpec({ - - test("generate map type for a Map of strings") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_string.json")) - val schema = Avro.default.schema(StringStringTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate map type for a Map of strings with value class") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_string.json")) - val schema = Avro.default.schema(WrappedStringStringTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate map type for a Map of ints") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_int.json")) - val schema = Avro.default.schema(StringIntTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate map type for a Map of records") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_record.json")) - val schema = Avro.default.schema(StringNestedTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("generate map type for map of nullable booleans") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_boolean_null.json")) - val schema = Avro.default.schema(StringBooleanTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support maps of sets of records") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_set_nested.json")) - val schema = Avro.default.schema(StringSetNestedTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support array of maps") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json")) - val schema = Avro.default.schema(ArrayTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support array of maps") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json")) - val schema = Avro.default.schema(WrappedStringArrayTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support lists of maps") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/list_of_maps.json")) - val schema = Avro.default.schema(ListTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support sets of maps") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/set_of_maps.json")) - val schema = Avro.default.schema(SetTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("support data class of list of data class with maps") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/class_of_list_of_maps.json")) - val schema = Avro.default.schema(List2Test.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - -}) { - @Serializable - @SerialName("mapStringStringTest") - data class StringStringTest(val map: Map) - - @Serializable - @SerialName("mapStringStringTest") - data class WrappedStringStringTest(val map: Map) - - @Serializable - @JvmInline - value class WrappedString(val value: String) - - @Serializable - data class StringIntTest(val map: Map) - - @Serializable - data class Nested(val goo: String) - - @Serializable - data class StringNestedTest(val map: Map) - - @Serializable - data class StringBooleanTest(val map: Map) - - @Serializable - data class StringSetNestedTest(val map: Map>) - - @Serializable - @SerialName("arrayOfMapStringString") - data class ArrayTest(val array: Array>) - - @Serializable - @SerialName("arrayOfMapStringString") - data class WrappedStringArrayTest(val array: Array>) - - @Serializable - data class ListTest(val list: List>) - - @Serializable - data class SetTest(val set: Set>) - - @Serializable - data class Ship(val map: Map) - - @Serializable - data class List2Test(val ship: List>) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MixedPolymorphicSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MixedPolymorphicSchemaTest.kt deleted file mode 100644 index 2af369ae..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/MixedPolymorphicSchemaTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroDefault -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass -import org.apache.avro.Schema - -@Serializable -sealed interface Expr -@Serializable -sealed interface UnaryExpr : Expr { - val value : Int -} -@Serializable -sealed class BinaryExpr : Expr { - abstract val left: Int - abstract val right: Int -} -@Serializable -object NullaryExpr : Expr -@Serializable -data class NegateExpr(override val value : Int) : UnaryExpr -@Serializable -data class AddExpr(override val left : Int, override val right : Int) : BinaryExpr() -@Serializable -data class SubstractExpr(override val left : Int, override val right : Int) : BinaryExpr() -@Serializable -abstract class OtherBinaryExpr : BinaryExpr() -@Serializable -data class MultiplicationExpr(override val left : Int, override val right : Int) : OtherBinaryExpr() -@Serializable -abstract class OtherUnaryExpr : UnaryExpr -@Serializable -data class ConstExpr(override val value : Int) : OtherUnaryExpr() - -@Serializable -data class ReferencingMixedPolymorphic( - val notNullable: Expr -) -@Serializable -data class ReferencingNullableMixedPolymorphic( - @AvroDefault(Avro.NULL) - val nullable : Expr? -) -class MixedPolymorphicSchemaTest : StringSpec({ - val module = SerializersModule { - polymorphic(OtherUnaryExpr::class) { - subclass(ConstExpr::class) - } - polymorphic(OtherBinaryExpr::class) { - subclass(MultiplicationExpr::class) - } - } - "referencing a mixed sealed hierarchy"{ - val schema = Avro(module).schema(ReferencingMixedPolymorphic.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/mixed_polymorphic_referenced.json")) - schema shouldBe expected - } - "referencing a mixed nullable sealed hierarchy"{ - val schema = Avro(module).schema(ReferencingNullableMixedPolymorphic.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/mixed_polymorphic_nullable_referenced.json")) - schema shouldBe expected - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamespaceTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamespaceTest.kt deleted file mode 100644 index 94c29eb6..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamespaceTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class NamespaceSchemaTest : FunSpec() { - - init { - test("use package name for top level class") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/top_level_class_namespace.json")) - val schema = Avro.default.schema(Tau.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("use namespace of object for classes defined inside an object") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/top_level_object_namespace.json")) - val schema = Avro.default.schema(Outer.Inner.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("local classes should use the namespace of their parent object package") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/local_class_namespace.json")) - val schema = Avro.default.schema(NamespaceTestFoo.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - } - - @Serializable - data class NamespaceTestFoo(val inner: String) -} - -@Serializable -data class Tau(val a: String, val b: Boolean) - -object Outer { - @Serializable - data class Inner(val a: String, val b: Boolean) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt deleted file mode 100644 index 6f296a37..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NamingStrategySchemaTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroConfiguration -import io.kotest.core.spec.style.WordSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable - -class NamingStrategySchemaTest : WordSpec({ - "NamingStrategy" should { - "convert schema with snake_case to camelCase" { - val snakeCaseAvro = Avro(AvroConfiguration(SnakeCaseNamingStrategy)) - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/snake_case_schema.json")) - - val schema = snakeCaseAvro.schema(Interface.serializer()) - - schema.toString(true) shouldBe expected.toString(true) - } - - "convert schema with PascalCase to camelCase" { - val pascalCaseAvro = Avro(AvroConfiguration(PascalCaseNamingStrategy)) - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/pascal_case_schema.json")) - - val schema = pascalCaseAvro.schema(Interface.serializer()) - - schema.toString(true) shouldBe expected.toString(true) - } - } -}) { - @Serializable - data class SubInterface(val name: String, val ipv4Address: String) - - @Serializable - data class Interface( - val name: String, - val ipv4Address: String, - val ipv4SubnetMask: Int, - val v: InternetProtocol, - val subInterface: SubInterface? - ) -} - -@Serializable -enum class InternetProtocol { IPv4, IPv6 } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableSchemaTest.kt deleted file mode 100644 index d3f33554..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableSchemaTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class NullableSchemaTest : FunSpec({ - - test("generate null as Union[T, Null]") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/nullables.json")) - val schema = Avro.default.schema(Test.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - -// test("move default option values to first schema as per avro spec") { -// val schema = AvroSchema[OptionWithDefault] -// val expected = new org . apache . avro . Schema . Parser ().parse(getClass.getResourceAsStream("/option_default_value.json")) -// schema.toString(true) shouldBe expected.toString(true) -// } -// -// test("if a field has a default value of null then define the field to be nullable") { -// val schema = AvroSchema[FieldWithNull] -// val expected = new org . apache . avro . Schema . Parser ().parse(getClass.getResourceAsStream("/option_from_null_default.json")) -// schema.toString(true) shouldBe expected.toString(true) -// } - -}) { - @Serializable - data class Test(val nullableString: String?, val nullableBoolean: Boolean?) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableWithDefaultsSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableWithDefaultsSchemaTest.kt deleted file mode 100644 index 4fc76018..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/NullableWithDefaultsSchemaTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroConfiguration -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable - -class NullableWithDefaultsSchemaTest : FunSpec({ - test("generate null as Union[T, Null] with default null") { - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/nullables-with-defaults.json")) - val schema = Avro(AvroConfiguration(implicitNulls = true)).schema(Test.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) { - @Serializable - data class Test(val nullableString: String?, val nullableBoolean: Boolean?) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PairSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PairSchemaTest.kt deleted file mode 100644 index deba7380..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PairSchemaTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.Serializable - -class PairSchemaTest : FunSpec({ - - test("!generate union:T,U for Pair[T,U] of primitives") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/pair.json")) - val schema = Avro.default.schema(StringDoubleTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - test("!generate union:T,U for Either[T,U] of records") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/pair_records.json")) - val schema = Avro.default.schema(GooFooTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -// "generate union:T,U for Either[T,U] of records using @AvroNamespace" in { -// @AvroNamespace("mm") -// data class Goo(s: String) -// @AvroNamespace("nn") -// data class Foo(b: Boolean) -// data class Test(either: Either[Goo, Foo]) -// val expected = new org.apache.avro.Schema.Parser().parse(getClass.getResourceAsStream("/either_record_with_avro_namespace.json")) -// val schema = AvroSchema[Test] -// schema.toString(true) shouldBe expected.toString(true) -// } -// test("flatten nested unions and move null to first position") { -// AvroSchema[Either[String, Option[Int]]].toString shouldBe """["null","string","int"]""" -// } - -}) { - @Serializable - data class StringDoubleTest(val p: Pair) - - @Serializable - data class Goo(val s: String) - - @Serializable - data class Foo(val b: Boolean) - - @Serializable - data class GooFooTest(val p: Pair) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicClassSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicClassSchemaTest.kt deleted file mode 100644 index 1c63f00d..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicClassSchemaTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass -import org.apache.avro.Schema - -@Serializable -abstract class UnsealedPolymorphicRoot - -@Serializable -data class UnsealedChildOne(val one: String) : UnsealedPolymorphicRoot() - -@Serializable -sealed class SealedChildTwo : UnsealedPolymorphicRoot() -@Serializable -data class UnsealedChildTwo(val two: String) : SealedChildTwo() - -@Serializable -data class ReferencingPolymorphicRoot( - val root : UnsealedPolymorphicRoot, - val nullableRoot : UnsealedPolymorphicRoot? = null -) - -@Serializable -data class PolymorphicRootInList( - val listOfRoot : List -) - -@Serializable -data class PolymorphicRootInMap( - val mapOfRoot : Map -) -val polymorphicModule = SerializersModule { - polymorphic(UnsealedPolymorphicRoot::class) { - subclass(UnsealedChildOne::class) - subclass(UnsealedChildTwo::class) - } -} - -class PolymorphicClassSchemaTest : StringSpec({ - "schema for polymorphic hierarchy" { - val module = SerializersModule { - polymorphic(UnsealedPolymorphicRoot::class) { - subclass(UnsealedChildOne::class) - subclass(SealedChildTwo::class) - } - } - val schema = Avro(serializersModule = module).schema(UnsealedPolymorphicRoot.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/polymorphic.json")) - schema shouldBe expected - } - - "supports polymorphic references / nested fields" { - val schema = Avro(serializersModule = polymorphicModule).schema(ReferencingPolymorphicRoot.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/polymorphic_reference.json")) - schema shouldBe expected - } - - "Supports polymorphic references in lists" { - val schema = Avro(serializersModule = polymorphicModule).schema(PolymorphicRootInList.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/polymorphic_reference_list.json")) - schema shouldBe expected - } - - "Supports polymorphic references in maps" { - val schema = Avro(serializersModule = polymorphicModule).schema(PolymorphicRootInMap.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/polymorphic_reference_map.json")) - schema shouldBe expected - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicSchemaTest.kt deleted file mode 100644 index 6959230b..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PolymorphicSchemaTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.io.AvroDecodeFormat -import com.github.avrokotlin.avro4k.io.AvroEncodeFormat -import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer -import kotlinx.serialization.Serializable -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.time.LocalDate - -@Serializable -data class BugFoo( - @Serializable(with = LocalDateSerializer::class) - val d: LocalDate, - val f: Float, - val s: String -) - -fun main() { - val byteArrayOutputStream = ByteArrayOutputStream() - val value = BugFoo(LocalDate.of(2002,1,1),2.3f,"34") - val serializer = BugFoo.serializer() - Avro.default.openOutputStream(serializer) { - encodeFormat = AvroEncodeFormat.Binary - }.to(byteArrayOutputStream).write(value).flush() - - val byteArrayInputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray()) - val result = Avro.default.openInputStream(serializer) { - decodeFormat = AvroDecodeFormat.Binary(Avro.default.schema(serializer)) - }.from(byteArrayInputStream).next() - - println("Decoded from array stream: $result") -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt index 76a08291..b0280510 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt @@ -1,136 +1,73 @@ -@file:UseContextualSerialization(forClasses = [UUID::class]) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.WrappedBoolean +import com.github.avrokotlin.avro4k.WrappedByte +import com.github.avrokotlin.avro4k.WrappedChar +import com.github.avrokotlin.avro4k.WrappedDouble +import com.github.avrokotlin.avro4k.WrappedFloat +import com.github.avrokotlin.avro4k.WrappedInt +import com.github.avrokotlin.avro4k.WrappedLong +import com.github.avrokotlin.avro4k.WrappedShort +import com.github.avrokotlin.avro4k.WrappedString +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseContextualSerialization -import kotlinx.serialization.builtins.ByteArraySerializer -import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.serializer -import org.apache.avro.Schema.Parser -import java.util.UUID +import org.apache.avro.LogicalType +import org.apache.avro.Schema + +@OptIn(InternalSerializationApi::class) +internal class PrimitiveSchemaTest : StringSpec({ + listOf( + WrappedBoolean::class to Schema.create(Schema.Type.BOOLEAN), + WrappedByte::class to Schema.create(Schema.Type.INT), + WrappedShort::class to Schema.create(Schema.Type.INT), + WrappedInt::class to Schema.create(Schema.Type.INT), + WrappedLong::class to Schema.create(Schema.Type.LONG), + WrappedFloat::class to Schema.create(Schema.Type.FLOAT), + WrappedDouble::class to Schema.create(Schema.Type.DOUBLE), + WrappedString::class to Schema.create(Schema.Type.STRING), + WrappedChar::class to Schema.create(Schema.Type.INT).also { LogicalType("char").addToSchema(it) } + ).forEach { (type, expectedSchema) -> + "value class ${type.simpleName} should be primitive schema $expectedSchema" { + AvroAssertions.assertThat(type.serializer()) + .generatesSchema(expectedSchema) + } + } -class PrimitiveSchemaTest : StringSpec({ + listOf( + Boolean::class to Schema.create(Schema.Type.BOOLEAN), + Byte::class to Schema.create(Schema.Type.INT), + Short::class to Schema.create(Schema.Type.INT), + Int::class to Schema.create(Schema.Type.INT), + Long::class to Schema.create(Schema.Type.LONG), + Float::class to Schema.create(Schema.Type.FLOAT), + Double::class to Schema.create(Schema.Type.DOUBLE), + String::class to Schema.create(Schema.Type.STRING), + Char::class to Schema.create(Schema.Type.INT).also { LogicalType("char").addToSchema(it) } + ).forEach { (type, expectedSchema) -> + "type ${type.simpleName} should be primitive schema $expectedSchema" { + AvroAssertions.assertThat(type.serializer()) + .generatesSchema(expectedSchema) + } + } - "boolean value class should be boolean primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_boolean.json")) - val schema = Avro.default.schema(BooleanWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "boolean type should be boolean primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_boolean.json")) - val schema = Avro.default.schema(Boolean.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "byte value class should be byte primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(ByteWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "byte type should be byte primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(Byte.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "short value class should be short primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(ShortWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "short type should be short primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(Short.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "int value class should be int primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(IntWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "int type should be int primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_int.json")) - val schema = Avro.default.schema(Int.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "long value class should be long primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_long.json")) - val schema = Avro.default.schema(LongWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "long type should be long primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_long.json")) - val schema = Avro.default.schema(Long.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "float value class should be float primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_float.json")) - val schema = Avro.default.schema(FloatWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "float type should be float primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_float.json")) - val schema = Avro.default.schema(Float.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "double value class should be double primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_double.json")) - val schema = Avro.default.schema(DoubleWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "double type should be double primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_double.json")) - val schema = Avro.default.schema(Double.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "bytes value class should be bytes primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_bytes.json")) - val schema = Avro.default.schema(ByteArrayWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "bytes type should be bytes primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_bytes.json")) - val schema = Avro.default.schema(ByteArraySerializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "string value class should be string primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_string.json")) - val schema = Avro.default.schema(StringWrapper.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - "string type should be string primitive schema" { - val expected = Parser().parse(javaClass.getResourceAsStream("/primitive_string.json")) - val schema = Avro.default.schema(String.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) { - @Serializable - @JvmInline - value class BooleanWrapper(val value: Boolean) - @Serializable - @JvmInline - value class ByteWrapper(val value: Byte) - @Serializable - @JvmInline - value class ShortWrapper(val value: Short) - @Serializable - @JvmInline - value class IntWrapper(val value: Int) - @Serializable - @JvmInline - value class LongWrapper(val value: Long) - @Serializable - @JvmInline - value class FloatWrapper(val value: Float) - @Serializable - @JvmInline - value class DoubleWrapper(val value: Double) - @Serializable - @JvmInline - value class ByteArrayWrapper(val value: ByteArray) - @Serializable - @JvmInline - value class StringWrapper(val value: String) -} + listOf( + Boolean::class to Schema.create(Schema.Type.BOOLEAN), + Byte::class to Schema.create(Schema.Type.INT), + Short::class to Schema.create(Schema.Type.INT), + Int::class to Schema.create(Schema.Type.INT), + Long::class to Schema.create(Schema.Type.LONG), + Float::class to Schema.create(Schema.Type.FLOAT), + Double::class to Schema.create(Schema.Type.DOUBLE), + String::class to Schema.create(Schema.Type.STRING), + Char::class to Schema.create(Schema.Type.INT).also { LogicalType("char").addToSchema(it) } + ).forEach { (type, expectedSchema) -> + "type ${type.simpleName}? should be nullable primitive schema $expectedSchema" { + AvroAssertions.assertThat(type.serializer().nullable) + .generatesSchema(expectedSchema.nullable) + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/RecursiveSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/RecursiveSchemaTest.kt index b4bd8eb6..f2fa59f1 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/RecursiveSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/RecursiveSchemaTest.kt @@ -1,63 +1,47 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.Serializable +import kotlin.io.path.Path -@Serializable -data class RecursiveClass(val payload: Int, val klass: RecursiveClass?) +internal class RecursiveSchemaTest : FunSpec({ -@Serializable -data class RecursiveListItem(val payload: Int, val list: List?) + test("accept direct recursive classes") { + AvroAssertions.assertThat() + .generatesSchema(Path("/recursive_class.json")) + } -@Serializable -data class RecursiveMapValue(val payload: Int, val map: Map?) + test("accept direct recursive lists") { + AvroAssertions.assertThat() + .generatesSchema(Path("/recursive_list.json")) + } -@Serializable -data class RecursivePair(val payload: Int, val pair: Pair?) + test("accept direct recursive maps") { + AvroAssertions.assertThat() + .generatesSchema(Path("/recursive_map.json")) + } -@Serializable -data class Level4(val level1: Level1) + test("accept nested recursive classes") { + AvroAssertions.assertThat() + .generatesSchema(Path("/recursive_nested.json")) + } +}) { + @Serializable + private data class RecursiveClass(val payload: Int, val klass: RecursiveClass?) -@Serializable -data class Level3(val level4: Level4?) + @Serializable + private data class RecursiveListItem(val payload: Int, val list: List?) -@Serializable -data class Level2(val level3: Level3) + @Serializable + private data class RecursiveMapValue(val payload: Int, val map: Map?) -@Serializable -data class Level1(val level2: Level2?) + @Serializable + private data class Level3(val level1: Level1?) -class RecursiveSchemaTest : FunSpec({ + @Serializable + private data class Level2(val level3: Level3) - test("accept direct recursive classes") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/recursive_class.json")) - val schema = Avro.default.schema(RecursiveClass.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept direct recursive lists") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/recursive_list.json")) - val schema = Avro.default.schema(RecursiveListItem.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept direct recursive maps") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/recursive_map.json")) - val schema = Avro.default.schema(RecursiveMapValue.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept direct recursive pairs") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/recursive_pair.json")) - val schema = Avro.default.schema(RecursivePair.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - - test("accept nested recursive classes") { - val expected = org.apache.avro.Schema.Parser().parse(this::class.java.getResourceAsStream("/recursive_nested.json")) - val schema = Avro.default.schema(Level1.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) + @Serializable + private data class Level1(val level2: Level2?) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedClassSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedClassSchemaTest.kt deleted file mode 100644 index 7da05547..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedClassSchemaTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -@file:Suppress("unused") - -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroDefault -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import org.apache.avro.Schema - -@Serializable -sealed class Operation { - @Serializable - object Nullary : Operation() - - @Serializable - sealed class Unary : Operation(){ - abstract val value : Int - @Serializable - data class Negate(override val value:Int) : Unary() - } - - @Serializable - sealed class Binary : Operation(){ - abstract val left : Int - abstract val right : Int - @Serializable - data class Add(override val left : Int, override val right : Int) : Binary() - @Serializable - data class Substract(override val left : Int, override val right : Int) : Binary() - } -} -@Serializable -data class ReferencingSealedClass( - val notNullable: Operation -) -@Serializable -data class ReferencingNullableSealedClass( - @AvroDefault(Avro.NULL) - val nullable : Operation? -) - -class SealedClassSchemaTest : StringSpec({ - - "schema for sealed hierarchy" { - val schema = Avro.default.schema(Operation.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed.json")) - schema shouldBe expected - } - "referencing a sealed hierarchy"{ - val schema = Avro.default.schema(ReferencingSealedClass.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_referenced.json")) - schema shouldBe expected - } - "referencing a nullable sealed hierarchy"{ - val schema = Avro.default.schema(ReferencingNullableSealedClass.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_nullable_referenced.json")) - schema shouldBe expected - } -}) \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedInterfaceSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedInterfaceSchemaTest.kt deleted file mode 100644 index 65ad0210..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/SealedInterfaceSchemaTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:Suppress("unused") - -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroDefault -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema - -@Serializable -sealed interface Calculable -@Serializable -sealed interface UnaryCalculable : Calculable { - val value : Int -} -@Serializable -sealed interface BinaryCalculable : Calculable { - val left: Int - val right: Int -} -@Serializable -object NullaryCalculable : Calculable -@Serializable -data class NegateCalculable(override val value : Int) : UnaryCalculable -@Serializable -data class AddCalculable(override val left : Int, override val right : Int) : BinaryCalculable -@Serializable -data class SubstractCalculable(override val left : Int, override val right : Int) : BinaryCalculable - - -@Serializable -data class ReferencingSealedInterface( - val notNullable: Calculable -) -@Serializable -data class ReferencingNullableSealedInterface( - @AvroDefault(Avro.NULL) - val nullable : Calculable? -) - -class SealedInterfaceSchemaTest : StringSpec({ - - "referencing a sealed hierarchy"{ - val schema = Avro().schema(ReferencingSealedInterface.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_hierarchy_referenced.json")) - schema shouldBe expected - } - "referencing a nullable sealed hierarchy"{ - val schema = Avro(serializersModule = SerializersModule { - - }).schema(ReferencingNullableSealedInterface.serializer()) - val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_nullable_referenced.json")) - schema shouldBe expected - } -}) - diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/TransientSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/TransientSchemaTest.kt index a68cdf2a..a5d4b3b6 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/TransientSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/TransientSchemaTest.kt @@ -1,20 +1,27 @@ package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder -class TransientSchemaTest : FunSpec({ - - test("@AvroTransient fields should be ignored") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/transient.json")) - val schema = Avro.default.schema(TransientFoo.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } - +internal class TransientSchemaTest : FunSpec({ + test("ignore fields with @Transient") { + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("TransientTest").fields() + .name("presentField").type(Schema.create(Schema.Type.STRING)).noDefault() + .endRecord() + ) + } }) { - @Serializable - data class TransientFoo(val a: String, @kotlinx.serialization.Transient val b: String = "foo", val c: String) -} + @Serializable + @SerialName("TransientTest") + private data class TransientTest( + val presentField: String, + @Transient val transientField: String = "default", + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt index 83d56f96..ab9b168e 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt @@ -1,35 +1,33 @@ -@file:UseSerializers(URLSerializer::class) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.URLSerializer -import io.kotest.matchers.shouldBe +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers +import org.apache.avro.Schema import java.net.URL -class URLSchemaTest : FunSpec({ - - test("accept URL as String") { - - val schema = Avro.default.schema(Test.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/url.json")) - schema shouldBe expected - } +internal class URLSchemaTest : FunSpec({ + test("accept URL as String") { + AvroAssertions.assertThat() + .generatesSchema(Schema.create(Schema.Type.STRING)) + } - test("accept nullable URL as String union") { - - val schema = Avro.default.schema(NullableTest.serializer()) - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/url_nullable.json")) - schema shouldBe expected - } + test("accept nullable URL as String union") { + AvroAssertions.assertThat() + .generatesSchema(Schema.create(Schema.Type.STRING).nullable) + } }) { - - @Serializable - data class Test(val b: URL) - - @Serializable - data class NullableTest(val b: URL?) -} + @JvmInline + @Serializable + private value class URLTest( + @Contextual val url: URL, + ) + + @JvmInline + @Serializable + private value class URLNullableTest( + @Contextual val url: URL?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt index cbf78fdd..67e87952 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt @@ -1,26 +1,34 @@ -@file:UseSerializers(UUIDSerializer::class) - package com.github.avrokotlin.avro4k.schema -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers +import org.apache.avro.LogicalTypes +import org.apache.avro.Schema import java.util.UUID -class UUIDSchemaTest : FunSpec({ - - test("support UUID logical types") { - - val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/uuid.json")) - val schema = Avro.default.schema(UUIDTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } +internal class UUIDSchemaTest : FunSpec({ + test("support UUID logical types") { + AvroAssertions.assertThat() + .generatesSchema(LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING))) + } + test("support nullable UUID logical types") { + AvroAssertions.assertThat() + .generatesSchema(LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING)).nullable) + } }) { + @JvmInline + @Serializable + private value class UUIDTest( + @Contextual val uuid: UUID, + ) - @Serializable - data class UUIDTest(val uuid: UUID) -} + @JvmInline + @Serializable + private value class UUIDNullableTest( + @Contextual val uuid: UUID?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaTest.kt new file mode 100644 index 00000000..277a0f7a --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UnionSchemaTest.kt @@ -0,0 +1,84 @@ +@file:Suppress("unused") + +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDefault +import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import com.github.avrokotlin.avro4k.schema +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import kotlin.io.path.Path + +internal class SealedClassSchemaTest : StringSpec({ + "should throw error when no implementation for an abstract class" { + shouldThrow { + Avro.schema(Operation.Binary.serializer()) + } + } + + val polymorphicSerializersModule = + SerializersModule { + polymorphic(Operation.Binary::class) { + subclass(Operation.Binary.Add::class) + subclass(Operation.Binary.Substract::class) + } + } + "schema for sealed hierarchy" { + AvroAssertions.assertThat() + .withConfig { serializersModule = polymorphicSerializersModule } + .generatesSchema(Path("/sealed.json")) + } + "referencing a sealed hierarchy" { + AvroAssertions.assertThat() + .withConfig { serializersModule = polymorphicSerializersModule } + .generatesSchema(Path("/sealed_referenced.json")) + } + "referencing a nullable sealed hierarchy" { + AvroAssertions.assertThat() + .withConfig { serializersModule = polymorphicSerializersModule } + .generatesSchema(Path("/sealed_nullable_referenced.json")) + } +}) { + @Serializable + private sealed interface Operation { + @Serializable + object Nullary : Operation + + @Serializable + sealed class Unary : Operation { + abstract val value: Int + + @Serializable + data class Negate(override val value: Int) : Unary() + } + + @Serializable + abstract class Binary : Operation { + abstract val left: Int + abstract val right: Int + + @Serializable + data class Add(override val left: Int, override val right: Int) : Binary() + + @Serializable + data class Substract(override val left: Int, override val right: Int) : Binary() + } + } + + @Serializable + private data class ReferencingSealedClass( + val notNullable: Operation, + ) + + @Serializable + private data class ReferencingNullableSealedClass( + @AvroDefault("null") + val nullable: Operation?, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt deleted file mode 100644 index a191a3e7..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.serializer.AvroSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder - -class UserDefinedSerializerTest : FunSpec({ - - test("schema from user-defined-serializer") { - - Avro.default.schema(Test.serializer()) shouldBe - SchemaBuilder.record("Test") - .namespace("com.github.avrokotlin.avro4k.schema.UserDefinedSerializerTest") - .fields() - .name("fixed").type(Schema.createFixed("foo", null, null, 10)).noDefault() - .endRecord() - } -}) { - @Serializer(forClass = String::class) - @OptIn(ExperimentalSerializationApi::class) - class StringAsFixedSerializer : AvroSerializer() { - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: String) { - TODO() - } - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): String { - TODO() - } - - override val descriptor: SerialDescriptor = object : AvroDescriptor("fixed-string", PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema { - return Schema.createFixed("foo", null, null, 10) - } - } - } - - @Serializable - data class Test(@Serializable(with = StringAsFixedSerializer::class) val fixed: String) -} diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ValueClassSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ValueClassSchemaTest.kt deleted file mode 100644 index 0b77c9df..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/ValueClassSchemaTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -@file:UseContextualSerialization(forClasses = [UUID::class]) - -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseContextualSerialization -import java.util.UUID - -class ValueClassSchemaTest : StringSpec({ - - "value class should be primitive in schema" { - val expected = - org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/value_class.json")) - val schema = Avro.default.schema(ContainsInlineTest.serializer()) - schema.toString(true) shouldBe expected.toString(true) - } -}) { - @Serializable - @JvmInline - value class StringWrapper(val a: String) - - @Serializable - @JvmInline - value class UuidWrapper(val uuid: UUID) - - @Serializable - data class ContainsInlineTest(val id: StringWrapper, val uuid: UuidWrapper) -} \ No newline at end of file diff --git a/src/test/resources/aliases_on_fields.json b/src/test/resources/aliases_on_fields.json index 0fc332dd..4e80edc1 100644 --- a/src/test/resources/aliases_on_fields.json +++ b/src/test/resources/aliases_on_fields.json @@ -1,25 +1,21 @@ { - "type": "record", - "name": "FieldAnnotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroAliasSchemaTest", - "fields": [ - { - "name": "str", - "type": "string", - "aliases": [ - "cold" - ] - }, - { - "name": "long", - "type": "long", - "aliases": [ - "kate" - ] - }, - { - "name": "int", - "type": "int" - } - ] + "type": "record", + "name": "FieldAnnotated", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroAliasSchemaTest", + "fields": [ + { + "name": "str", + "type": "string", + "aliases": ["cold"] + }, + { + "name": "long", + "type": "long", + "aliases": ["kate"] + }, + { + "name": "int", + "type": "int" + } + ] } diff --git a/src/test/resources/aliases_on_fields_multiple.json b/src/test/resources/aliases_on_fields_multiple.json index 0a4409e2..2f5bfc64 100644 --- a/src/test/resources/aliases_on_fields_multiple.json +++ b/src/test/resources/aliases_on_fields_multiple.json @@ -6,10 +6,7 @@ { "name": "str", "type": "string", - "aliases": [ - "queen", - "ledzep" - ] + "aliases": ["queen", "ledzep"] } ] } diff --git a/src/test/resources/aliases_on_types.json b/src/test/resources/aliases_on_types.json index a17ef511..29d489a0 100644 --- a/src/test/resources/aliases_on_types.json +++ b/src/test/resources/aliases_on_types.json @@ -2,9 +2,7 @@ "type": "record", "name": "TypeAnnotated", "namespace": "com.github.avrokotlin.avro4k.schema.AvroAliasSchemaTest", - "aliases": [ - "queen" - ], + "aliases": ["queen"], "fields": [ { "name": "str", diff --git a/src/test/resources/aliases_on_types_multiple.json b/src/test/resources/aliases_on_types_multiple.json index 8e2d1417..04f3dfe6 100644 --- a/src/test/resources/aliases_on_types_multiple.json +++ b/src/test/resources/aliases_on_types_multiple.json @@ -2,10 +2,7 @@ "type": "record", "name": "TypeAliasAnnotated", "namespace": "com.github.avrokotlin.avro4k.schema.AvroAliasSchemaTest", - "aliases": [ - "queen", - "ledzep" - ], + "aliases": ["queen", "ledzep"], "fields": [ { "name": "str", diff --git a/src/test/resources/array.json b/src/test/resources/array.json index 8ce9d77f..bf2b7f63 100644 --- a/src/test/resources/array.json +++ b/src/test/resources/array.json @@ -10,7 +10,8 @@ "items": { "type": "boolean" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/array_of_maps.json b/src/test/resources/array_of_maps.json index f75deb04..cf8b28db 100644 --- a/src/test/resources/array_of_maps.json +++ b/src/test/resources/array_of_maps.json @@ -10,7 +10,8 @@ "type": "map", "values": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/arrayrecords.json b/src/test/resources/arrayrecords.json index c196827c..9b91fcdc 100644 --- a/src/test/resources/arrayrecords.json +++ b/src/test/resources/arrayrecords.json @@ -17,7 +17,8 @@ } ] } - } + }, + "default": [] } ] } diff --git a/src/test/resources/avro_default_annotation_array.json b/src/test/resources/avro_default_annotation_array.json index 97f8567f..26fc22e9 100644 --- a/src/test/resources/avro_default_annotation_array.json +++ b/src/test/resources/avro_default_annotation_array.json @@ -1,58 +1,58 @@ { - "type": "record", - "name": "BarArray", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "defaultEmptyArray", - "type": { - "type": "array", - "items": "string" - }, - "default": [] + "type": "record", + "name": "BarArray", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "defaultEmptyArray", + "type": { + "type": "array", + "items": "string" }, - { - "name": "nullableDefaultEmptyArray", - "type": [ - "null", - { - "type": "array", - "items": "string" - } - ], - "default": null + "default": [] + }, + { + "name": "nullableDefaultEmptyArray", + "type": [ + "null", + { + "type": "array", + "items": "string" + } + ], + "default": null + }, + { + "name": "defaultStringArrayWith2Defaults", + "type": { + "type": "array", + "items": "string" }, - { - "name": "defaultStringArrayWith2Defaults", - "type": { - "type": "array", - "items": "string" - }, - "default": ["John", "Doe"] + "default": ["John", "Doe"] + }, + { + "name": "defaultIntArrayWith2Defaults", + "type": { + "type": "array", + "items": "int" }, - { - "name": "defaultIntArrayWith2Defaults", - "type": { - "type": "array", - "items": "int" - }, - "default": [1,2] + "default": [1, 2] + }, + { + "name": "defaultFloatArrayWith2Defaults", + "type": { + "type": "array", + "items": "float" }, - { - "name": "defaultFloatArrayWith2Defaults", - "type": { - "type": "array", - "items": "float" - }, - "default": [3.14, 9.89] + "default": [3.14, 9.89] + }, + { + "name": "defaultStringArrayWithNullableTypes", + "type": { + "type": "array", + "items": ["null", "string"] }, - { - "name": "defaultStringArrayWithNullableTypes", - "type": { - "type": "array", - "items": [ "null", "string" ] - }, - "default" : [ null ] - } - ] + "default": [null] + } + ] } diff --git a/src/test/resources/avro_default_annotation_big_decimal.json b/src/test/resources/avro_default_annotation_big_decimal.json index c23c32d4..bcedb5b7 100644 --- a/src/test/resources/avro_default_annotation_big_decimal.json +++ b/src/test/resources/avro_default_annotation_big_decimal.json @@ -1,51 +1,52 @@ { - "type": "record", - "name": "BarDecimal", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - } - }, - { - "name": "b", - "type": { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - }, - "default": "\u0000" - }, - { - "name": "nullableString", - "type": ["null", - { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - } - ], - "default" : null - }, - { - "name": "c", - "type": [ - { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - } - ,"null" - ], - "default": "\u0000" + "type": "record", + "name": "BarDecimal", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "a", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 8, + "scale": 2 } - ] + }, + { + "name": "b", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 8, + "scale": 2 + }, + "default": "\u0000" + }, + { + "name": "nullableString", + "type": [ + "null", + { + "type": "bytes", + "logicalType": "decimal", + "precision": 8, + "scale": 2 + } + ], + "default": null + }, + { + "name": "c", + "type": [ + { + "type": "bytes", + "logicalType": "decimal", + "precision": 8, + "scale": 2 + }, + "null" + ], + "default": "\u0000" + } + ] } diff --git a/src/test/resources/avro_default_annotation_enum.json b/src/test/resources/avro_default_annotation_enum.json index 4dbf5b4a..60328ea4 100644 --- a/src/test/resources/avro_default_annotation_enum.json +++ b/src/test/resources/avro_default_annotation_enum.json @@ -1,25 +1,30 @@ { - "type" : "record", - "name" : "BarEnum", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "fields" : [ { - "name" : "a", - "type" : { - "type" : "enum", - "name" : "FooEnum", - "symbols" : [ "A", "B", "C" ] + "type": "record", + "name": "BarEnum", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "a", + "type": { + "type": "enum", + "name": "FooEnum", + "symbols": ["A", "B", "C"] } - }, { - "name" : "b", - "type" : "FooEnum", - "default" : "A" - }, { - "name" : "nullableEnum", - "type" : [ "null", "FooEnum" ], - "default" : null - }, { - "name" : "c", - "type" : [ "FooEnum", "null" ], - "default" : "B" - } ] -} \ No newline at end of file + }, + { + "name": "b", + "type": "FooEnum", + "default": "A" + }, + { + "name": "nullableEnum", + "type": ["null", "FooEnum"], + "default": null + }, + { + "name": "c", + "type": ["FooEnum", "null"], + "default": "B" + } + ] +} diff --git a/src/test/resources/avro_default_annotation_float.json b/src/test/resources/avro_default_annotation_float.json index 561c230a..24c5d81b 100644 --- a/src/test/resources/avro_default_annotation_float.json +++ b/src/test/resources/avro_default_annotation_float.json @@ -1,26 +1,26 @@ { - "type": "record", - "name": "BarFloat", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "float", - "default": 3.14 - }, - { - "name": "nullableFloat", - "type": ["null","float"], - "default" : null - }, - { - "name": "c", - "type": ["float","null"], - "default": 3.14 - } - ] + "type": "record", + "name": "BarFloat", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "a", + "type": "string" + }, + { + "name": "b", + "type": "float", + "default": 3.14 + }, + { + "name": "nullableFloat", + "type": ["null", "float"], + "default": null + }, + { + "name": "c", + "type": ["float", "null"], + "default": 3.14 + } + ] } diff --git a/src/test/resources/avro_default_annotation_int.json b/src/test/resources/avro_default_annotation_int.json index b57d3c65..da595de0 100644 --- a/src/test/resources/avro_default_annotation_int.json +++ b/src/test/resources/avro_default_annotation_int.json @@ -1,26 +1,26 @@ { - "type": "record", - "name": "BarInt", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "int", - "default": 5 - }, - { - "name": "nullableInt", - "type": ["null","int"], - "default" : null - }, - { - "name": "c", - "type": ["int","null"], - "default" : 5 - } - ] + "type": "record", + "name": "BarInt", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "a", + "type": "string" + }, + { + "name": "b", + "type": "int", + "default": 5 + }, + { + "name": "nullableInt", + "type": ["null", "int"], + "default": null + }, + { + "name": "c", + "type": ["int", "null"], + "default": 5 + } + ] } diff --git a/src/test/resources/avro_default_annotation_list.json b/src/test/resources/avro_default_annotation_list.json index aefc3ba6..752fd755 100644 --- a/src/test/resources/avro_default_annotation_list.json +++ b/src/test/resources/avro_default_annotation_list.json @@ -1,58 +1,58 @@ { - "type": "record", - "name": "BarList", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "defaultEmptyList", - "type": { - "type": "array", - "items": "string" - }, - "default": [] + "type": "record", + "name": "BarList", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "defaultEmptyList", + "type": { + "type": "array", + "items": "string" }, - { - "name": "nullableDefaultEmptyList", - "type": [ - "null", - { - "type": "array", - "items": "string" - } - ], - "default": null + "default": [] + }, + { + "name": "nullableDefaultEmptyList", + "type": [ + "null", + { + "type": "array", + "items": "string" + } + ], + "default": null + }, + { + "name": "defaultStringListWith2Defaults", + "type": { + "type": "array", + "items": "string" }, - { - "name": "defaultStringListWith2Defaults", - "type": { - "type": "array", - "items": "string" - }, - "default": ["John", "Doe"] + "default": ["John", "Doe"] + }, + { + "name": "defaultIntListWith2Defaults", + "type": { + "type": "array", + "items": "int" }, - { - "name": "defaultIntListWith2Defaults", - "type": { - "type": "array", - "items": "int" - }, - "default": [1,2] + "default": [1, 2] + }, + { + "name": "defaultFloatListWith2Defaults", + "type": { + "type": "array", + "items": "float" }, - { - "name": "defaultFloatListWith2Defaults", - "type": { - "type": "array", - "items": "float" - }, - "default": [3.14, 9.89] + "default": [3.14, 9.89] + }, + { + "name": "defaultStringListWithNullableTypes", + "type": { + "type": "array", + "items": ["null", "string"] }, - { - "name": "defaultStringListWithNullableTypes", - "type": { - "type": "array", - "items": [ "null", "string" ] - }, - "default" : [ null ] - } - ] + "default": [null] + } + ] } diff --git a/src/test/resources/avro_default_annotation_list_of_records.json b/src/test/resources/avro_default_annotation_list_of_records.json index cb45645c..29f286ff 100644 --- a/src/test/resources/avro_default_annotation_list_of_records.json +++ b/src/test/resources/avro_default_annotation_list_of_records.json @@ -1,32 +1,36 @@ { - "type": "record", - "name": "BarListOfElements", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "defaultEmptyListOfRecords", - "type": { - "type": "array", - "items": { - "type": "record", - "name": "FooElement", - "fields": [ - { - "name": "value", - "type": "string" - } - ] + "type": "record", + "name": "BarListOfElements", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "defaultEmptyListOfRecords", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "FooElement", + "fields": [ + { + "name": "value", + "type": "string" } - }, - "default": [] - }, { - "name" : "defaultListWithOneValue", - "type" : { - "type" : "array", - "items" : "FooElement" - }, - "default" : [ { - "value" : "foo" - } ] - } ] + ] + } + }, + "default": [] + }, + { + "name": "defaultListWithOneValue", + "type": { + "type": "array", + "items": "FooElement" + }, + "default": [ + { + "value": "foo" + } + ] + } + ] } diff --git a/src/test/resources/avro_default_annotation_set.json b/src/test/resources/avro_default_annotation_set.json index 35195a83..d506667b 100644 --- a/src/test/resources/avro_default_annotation_set.json +++ b/src/test/resources/avro_default_annotation_set.json @@ -1,58 +1,58 @@ { - "type": "record", - "name": "BarSet", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "defaultEmptySet", - "type": { - "type": "array", - "items": "string" - }, - "default": [] + "type": "record", + "name": "BarSet", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "defaultEmptySet", + "type": { + "type": "array", + "items": "string" }, - { - "name": "nullableDefaultEmptySet", - "type": [ - "null", - { - "type": "array", - "items": "string" - } - ], - "default": null + "default": [] + }, + { + "name": "nullableDefaultEmptySet", + "type": [ + "null", + { + "type": "array", + "items": "string" + } + ], + "default": null + }, + { + "name": "defaultStringSetWith2Defaults", + "type": { + "type": "array", + "items": "string" }, - { - "name": "defaultStringSetWith2Defaults", - "type": { - "type": "array", - "items": "string" - }, - "default": ["John", "Doe"] + "default": ["John", "Doe"] + }, + { + "name": "defaultIntSetWith2Defaults", + "type": { + "type": "array", + "items": "int" }, - { - "name": "defaultIntSetWith2Defaults", - "type": { - "type": "array", - "items": "int" - }, - "default": [1,2] + "default": [1, 2] + }, + { + "name": "defaultFloatSetWith2Defaults", + "type": { + "type": "array", + "items": "float" }, - { - "name": "defaultFloatSetWith2Defaults", - "type": { - "type": "array", - "items": "float" - }, - "default": [3.14, 9.89] + "default": [3.14, 9.89] + }, + { + "name": "defaultStringSetWithNullableTypes", + "type": { + "type": "array", + "items": ["null", "string"] }, - { - "name": "defaultStringSetWithNullableTypes", - "type": { - "type": "array", - "items": [ "null", "string" ] - }, - "default" : [ null ] - } - ] + "default": [null] + } + ] } diff --git a/src/test/resources/avro_default_annotation_string.json b/src/test/resources/avro_default_annotation_string.json index 0f0480ad..f0243b0e 100644 --- a/src/test/resources/avro_default_annotation_string.json +++ b/src/test/resources/avro_default_annotation_string.json @@ -1,26 +1,26 @@ { - "type": "record", - "name": "BarString", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "string", - "default": "hello" - }, - { - "name": "nullableString", - "type": ["null","string"], - "default" : null - }, - { - "name": "c", - "type": ["string","null"], - "default": "hello" - } - ] + "type": "record", + "name": "BarString", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroDefaultSchemaTest", + "fields": [ + { + "name": "a", + "type": "string" + }, + { + "name": "b", + "type": "string", + "default": "hello" + }, + { + "name": "nullableString", + "type": ["null", "string"], + "default": null + }, + { + "name": "c", + "type": ["string", "null"], + "default": "hello" + } + ] } diff --git a/src/test/resources/avro_name_class.json b/src/test/resources/avro_name_class.json deleted file mode 100644 index 05c6cbc1..00000000 --- a/src/test/resources/avro_name_class.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "wibble", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroNameSchemaTest", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/avro_name_field.json b/src/test/resources/avro_name_field.json index 40c75609..040b7e90 100644 --- a/src/test/resources/avro_name_field.json +++ b/src/test/resources/avro_name_field.json @@ -1,15 +1,10 @@ { "type": "record", - "name": "FieldNamesFoo", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroNameSchemaTest", + "name": "anotherRecordName", "fields": [ { "name": "foo", "type": "string" - }, - { - "name": "wobble", - "type": "string" } ] } diff --git a/src/test/resources/basic.json b/src/test/resources/basic.json index 16553b75..9666e927 100644 --- a/src/test/resources/basic.json +++ b/src/test/resources/basic.json @@ -36,4 +36,4 @@ "type": "int" } ] -} \ No newline at end of file +} diff --git a/src/test/resources/bigdecimal-scale-and-precision.json b/src/test/resources/bigdecimal-scale-and-precision.json deleted file mode 100644 index 4cf07636..00000000 --- a/src/test/resources/bigdecimal-scale-and-precision.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "record", - "name": "BigDecimalPrecisionTest", - "namespace": "com.github.avrokotlin.avro4k.schema.BigDecimalSchemaTest", - "fields": [ - { - "name": "decimal", - "type": { - "type": "bytes", - "logicalType": "decimal", - "precision": 4, - "scale": 1 - } - } - ] -} diff --git a/src/test/resources/bigdecimal.json b/src/test/resources/bigdecimal.json deleted file mode 100644 index c3d61266..00000000 --- a/src/test/resources/bigdecimal.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "record", - "name": "BigDecimalTest", - "namespace": "com.github.avrokotlin.avro4k.schema.BigDecimalSchemaTest", - "fields": [ - { - "name": "decimal", - "type": { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - } - } - ] -} diff --git a/src/test/resources/bigdecimal_nullable.json b/src/test/resources/bigdecimal_nullable.json deleted file mode 100644 index df1fb049..00000000 --- a/src/test/resources/bigdecimal_nullable.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "record", - "name": "NullableBigDecimalTest", - "namespace": "com.github.avrokotlin.avro4k.schema.BigDecimalSchemaTest", - "fields": [ - { - "name": "decimal", - "type": [ - "null", - { - "type": "bytes", - "logicalType": "decimal", - "precision": 8, - "scale": 2 - } - ] - } - ] -} diff --git a/src/test/resources/bigint.json b/src/test/resources/bigint.json deleted file mode 100644 index ba841c8f..00000000 --- a/src/test/resources/bigint.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.BigIntegerSchemaTest", - "fields": [ - { - "name": "b", - "type": "string" - } - ] -} diff --git a/src/test/resources/bigint_nullable.json b/src/test/resources/bigint_nullable.json deleted file mode 100644 index 20539fe5..00000000 --- a/src/test/resources/bigint_nullable.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "NullableTest", - "namespace": "com.github.avrokotlin.avro4k.schema.BigIntegerSchemaTest", - "fields": [ - { - "name": "b", - "type": [ - "null", - "string" - ] - } - ] -} diff --git a/src/test/resources/byte_array.json b/src/test/resources/byte_array.json deleted file mode 100644 index 15e80c9d..00000000 --- a/src/test/resources/byte_array.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "ByteArrayTest", - "namespace": "com.github.avrokotlin.avro4k.schema.ByteArraySchemaTest", - "fields": [ - { - "name": "z", - "type": "bytes" - } - ] -} diff --git a/src/test/resources/bytebuffer.json b/src/test/resources/bytebuffer.json deleted file mode 100644 index 2794eee1..00000000 --- a/src/test/resources/bytebuffer.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.ByteArraySchemaTest", - "fields": [ - { - "name": "z", - "type": "bytes" - } - ] -} diff --git a/src/test/resources/class_of_list_of_maps.json b/src/test/resources/class_of_list_of_maps.json index ec509ca2..a812ec17 100644 --- a/src/test/resources/class_of_list_of_maps.json +++ b/src/test/resources/class_of_list_of_maps.json @@ -1,7 +1,7 @@ { "type": "record", "name": "List2Test", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "ship", @@ -11,7 +11,8 @@ "type": "map", "values": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/contextual_1.json b/src/test/resources/contextual_1.json index cb363e7a..bc0f5448 100644 --- a/src/test/resources/contextual_1.json +++ b/src/test/resources/contextual_1.json @@ -5,17 +5,12 @@ "fields": [ { "name": "ts", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } + "type": "boolean" }, { "name": "withFallback", - "type": [ - "null", - "int" - ] + "type": ["null", "int"], + "default": null } ] } diff --git a/src/test/resources/contextual_2.json b/src/test/resources/contextual_2.json deleted file mode 100644 index f5f1f113..00000000 --- a/src/test/resources/contextual_2.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.ContextualSchemaTest", - "fields": [ - { - "name": "ts", - "type": { - "type": "long", - "logicalType": "timestamp-micros" - } - }, - { - "name": "withFallback", - "type": [ - "null", - "int" - ] - } - ] -} diff --git a/src/test/resources/date.json b/src/test/resources/date.json deleted file mode 100644 index bf3bc504..00000000 --- a/src/test/resources/date.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "DateTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "date", - "type": { - "type": "int", - "logicalType": "date" - } - } - ] -} diff --git a/src/test/resources/default.json b/src/test/resources/default.json deleted file mode 100644 index c7b4925d..00000000 --- a/src/test/resources/default.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "string" - }, - { - "name": "c", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - }, - { - "name": "d", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/doc_annotation_class.json b/src/test/resources/doc_annotation_class.json index a1a6fe58..9ec15182 100644 --- a/src/test/resources/doc_annotation_class.json +++ b/src/test/resources/doc_annotation_class.json @@ -2,7 +2,7 @@ "type": "record", "name": "TypeAnnotated", "namespace": "com.github.avrokotlin.avro4k.schema.AvroDocSchemaTest", - "doc" : "hello; is it me youre looking for", + "doc": "hello; is it me youre looking for", "fields": [ { "name": "str", diff --git a/src/test/resources/doc_annotation_field.json b/src/test/resources/doc_annotation_field.json index 2e232365..b1618116 100644 --- a/src/test/resources/doc_annotation_field.json +++ b/src/test/resources/doc_annotation_field.json @@ -6,12 +6,12 @@ { "name": "str", "type": "string", - "doc" : "hello its me" + "doc": "hello its me" }, { "name": "long", "type": "long", - "doc" : "I am a long" + "doc": "I am a long" }, { "name": "int", diff --git a/src/test/resources/enum.json b/src/test/resources/enum.json deleted file mode 100644 index ad0a4f55..00000000 --- a/src/test/resources/enum.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "record", - "name": "EnumTest", - "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", - "fields": [ - { - "name": "wine", - "type": { - "type": "enum", - "name": "Wine", - "namespace": "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "Malbec", - "Shiraz", - "CabSav", - "Merlot" - ] - } - } - ] -} diff --git a/src/test/resources/enum_with_default.json b/src/test/resources/enum_with_default.json index 92ebf357..4d5c2a88 100644 --- a/src/test/resources/enum_with_default.json +++ b/src/test/resources/enum_with_default.json @@ -1,20 +1,9 @@ { - "type": "record", - "name": "EnumWithDefaultTest", - "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", - "fields": [ - { - "name": "type", - "type": { - "type": "enum", - "name": "IngredientType", - "namespace": "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "VEGGIE", - "MEAT" - ], - "default": "MEAT" - } - } - ] + "type": "enum", + "name": "Suit", + "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", + "aliases": ["MySuit"], + "doc": "documentation", + "symbols": ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"], + "default": "DIAMONDS" } diff --git a/src/test/resources/enum_with_default_new_namespace.json b/src/test/resources/enum_with_default_new_namespace.json deleted file mode 100644 index 98b9374a..00000000 --- a/src/test/resources/enum_with_default_new_namespace.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "record", - "name": "EnumWithDefaultTest", - "namespace": "new", - "fields": [ - { - "name": "type", - "type": { - "type": "enum", - "name": "IngredientType", - "symbols": [ - "VEGGIE", - "MEAT" - ], - "default": "MEAT" - } - } - ] -} diff --git a/src/test/resources/enum_with_default_record.json b/src/test/resources/enum_with_default_record.json new file mode 100644 index 00000000..c0616ddd --- /dev/null +++ b/src/test/resources/enum_with_default_record.json @@ -0,0 +1,18 @@ +{ + "type": "record", + "name": "RecordWithGenericField", + "fields": [ + { + "name": "value", + "type": { + "type": "enum", + "name": "Suit", + "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", + "doc": "documentation", + "symbols": ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"], + "default": "DIAMONDS", + "aliases": ["MySuit"] + } + } + ] +} diff --git a/src/test/resources/enum_with_default_value_and_null.json b/src/test/resources/enum_with_default_value_and_null.json deleted file mode 100644 index 49710c23..00000000 --- a/src/test/resources/enum_with_default_value_and_null.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "record", - "name": "EnumWithAvroDefaultTest", - "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", - "fields": [ - { - "name": "type", - "type": [ - "null", - { - "type": "enum", - "name": "IngredientType", - "namespace": "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "VEGGIE", - "MEAT" - ], - "default": "MEAT" - } - ], - "default": null - } - ] -} diff --git a/src/test/resources/enum_with_documentation.json b/src/test/resources/enum_with_documentation.json deleted file mode 100644 index 68b1403b..00000000 --- a/src/test/resources/enum_with_documentation.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "record", - "name": "EnumWithDocuTest", - "namespace": "com.github.avrokotlin.avro4k.schema.EnumSchemaTest", - "fields": [ - { - "name": "value", - "type": { - "type": "enum", - "name": "Suit", - "aliases" : ["MySuit"], - "doc" : "documentation", - "namespace": "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "SPADES", - "HEARTS", - "DIAMONDS", - "CLUBS" - ] - } - } - ] -} diff --git a/src/test/resources/fixed_string.json b/src/test/resources/fixed_string.json index 5b976288..94522457 100644 --- a/src/test/resources/fixed_string.json +++ b/src/test/resources/fixed_string.json @@ -1,7 +1,6 @@ { "type": "record", - "name": "FixedStringField", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroFixedSchemaTest", + "name": "Fixed", "fields": [ { "name": "mystring", @@ -12,4 +11,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/test/resources/fixed_string_5.json b/src/test/resources/fixed_string_5.json new file mode 100644 index 00000000..b0144ce2 --- /dev/null +++ b/src/test/resources/fixed_string_5.json @@ -0,0 +1,14 @@ +{ + "type": "record", + "name": "Fixed", + "fields": [ + { + "name": "mystring", + "type": { + "type": "fixed", + "name": "mystring", + "size": 5 + } + } + ] +} diff --git a/src/test/resources/fixed_string_top_level_value_type.json b/src/test/resources/fixed_string_top_level_value_type.json deleted file mode 100644 index a8cd891f..00000000 --- a/src/test/resources/fixed_string_top_level_value_type.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "fixed", - "name": "FixedValueClass", - "namespace": "com.sksamuel.avro4s.schema", - "size": 8 -} \ No newline at end of file diff --git a/src/test/resources/fixed_string_value_type_as_field.json b/src/test/resources/fixed_string_value_type_as_field.json deleted file mode 100644 index 59f0cbbc..00000000 --- a/src/test/resources/fixed_string_value_type_as_field.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "Foo", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroFixedSchemaTest", - "fields": [ - { - "name": "z", - "type": { - "type": "fixed", - "name": "FixedClass", - "size": 8 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/inline_type.json b/src/test/resources/inline_type.json deleted file mode 100644 index 6db26dab..00000000 --- a/src/test/resources/inline_type.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "Product", - "namespace": "com.github.avrokotlin.avro4k.schema.InlineClassSchemaTest", - "fields": [ - { - "name": "id", - "type": "string" - }, - { - "name": "name", - "type": "string" - } - ] -} diff --git a/src/test/resources/instant.json b/src/test/resources/instant.json deleted file mode 100644 index 6fbd20da..00000000 --- a/src/test/resources/instant.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "InstantTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "instant", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - } - ] -} diff --git a/src/test/resources/list.json b/src/test/resources/list.json index 0990901f..e51b7810 100644 --- a/src/test/resources/list.json +++ b/src/test/resources/list.json @@ -10,7 +10,8 @@ "items": { "type": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/list_of_maps.json b/src/test/resources/list_of_maps.json index c9fae030..2d0c8098 100644 --- a/src/test/resources/list_of_maps.json +++ b/src/test/resources/list_of_maps.json @@ -1,7 +1,7 @@ { "type": "record", "name": "ListTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "list", @@ -11,7 +11,8 @@ "type": "map", "values": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/listrecords.json b/src/test/resources/listrecords.json index 64907f99..13edf685 100644 --- a/src/test/resources/listrecords.json +++ b/src/test/resources/listrecords.json @@ -17,7 +17,8 @@ } ] } - } + }, + "default": [] } ] } diff --git a/src/test/resources/local_class_namespace.json b/src/test/resources/local_class_namespace.json deleted file mode 100644 index fa112ba4..00000000 --- a/src/test/resources/local_class_namespace.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "NamespaceTestFoo", - "namespace": "com.github.avrokotlin.avro4k.schema.NamespaceSchemaTest", - "fields": [ - { - "name": "inner", - "type": "string" - } - ] -} diff --git a/src/test/resources/localdate.json b/src/test/resources/localdate.json deleted file mode 100644 index 51659118..00000000 --- a/src/test/resources/localdate.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "LocalDateTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "date", - "type": { - "type": "int", - "logicalType": "date" - } - } - ] -} diff --git a/src/test/resources/localdate_nullable.json b/src/test/resources/localdate_nullable.json deleted file mode 100644 index 759e340e..00000000 --- a/src/test/resources/localdate_nullable.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "record", - "name": "NullableLocalDateTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "date", - "type": [ - "null", - { - "type": "int", - "logicalType": "date" - } - ] - } - ] -} diff --git a/src/test/resources/localdatetime.json b/src/test/resources/localdatetime.json deleted file mode 100644 index f928854b..00000000 --- a/src/test/resources/localdatetime.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "LocalDateTimeTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "time", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - } - ] -} diff --git a/src/test/resources/localtime.json b/src/test/resources/localtime.json deleted file mode 100644 index ac21d4a8..00000000 --- a/src/test/resources/localtime.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "LocalTimeTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "time", - "type": { - "type": "int", - "logicalType": "time-millis" - } - } - ] -} diff --git a/src/test/resources/map_boolean_null.json b/src/test/resources/map_boolean_null.json index 91bef6d2..5467a7d1 100644 --- a/src/test/resources/map_boolean_null.json +++ b/src/test/resources/map_boolean_null.json @@ -1,17 +1,15 @@ { "type": "record", "name": "StringBooleanTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "map", "type": { "type": "map", - "values": [ - "null", - "boolean" - ] - } + "values": ["null", "boolean"] + }, + "default": {} } ] } diff --git a/src/test/resources/map_int.json b/src/test/resources/map_int.json index 03ccba4a..60e1f2f6 100644 --- a/src/test/resources/map_int.json +++ b/src/test/resources/map_int.json @@ -1,14 +1,15 @@ { "type": "record", "name": "StringIntTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "map", "type": { "type": "map", "values": "int" - } + }, + "default": {} } ] } diff --git a/src/test/resources/map_record.json b/src/test/resources/map_record.json index 406c16c6..ba003f9a 100644 --- a/src/test/resources/map_record.json +++ b/src/test/resources/map_record.json @@ -1,7 +1,7 @@ { "type": "record", "name": "StringNestedTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "map", @@ -17,7 +17,8 @@ } ] } - } + }, + "default": {} } ] } diff --git a/src/test/resources/map_set_nested.json b/src/test/resources/map_set_nested.json index ecaebd43..a13814ee 100644 --- a/src/test/resources/map_set_nested.json +++ b/src/test/resources/map_set_nested.json @@ -1,7 +1,7 @@ { "type": "record", "name": "StringSetNestedTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "map", @@ -20,7 +20,8 @@ ] } } - } + }, + "default": {} } ] } diff --git a/src/test/resources/map_string.json b/src/test/resources/map_string.json deleted file mode 100644 index e336861a..00000000 --- a/src/test/resources/map_string.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "record", - "name": "mapStringStringTest", - "fields": [ - { - "name": "map", - "type": { - "type": "map", - "values": "string" - } - } - ] -} diff --git a/src/test/resources/mixed_polymorphic_nullable_referenced.json b/src/test/resources/mixed_polymorphic_nullable_referenced.json deleted file mode 100644 index 655021a3..00000000 --- a/src/test/resources/mixed_polymorphic_nullable_referenced.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "type":"record", - "name":"ReferencingNullableMixedPolymorphic", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name":"nullable", - "type":[ - "null", - { - "type":"record", - "name":"AddExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"ConstExpr", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"MultiplicationExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NegateExpr", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NullaryExpr", - "fields":[] - }, - { - "type":"record", - "name":"SubstractExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - } - ], - "default" : null - } - ] -} diff --git a/src/test/resources/mixed_polymorphic_referenced.json b/src/test/resources/mixed_polymorphic_referenced.json deleted file mode 100644 index c80e0380..00000000 --- a/src/test/resources/mixed_polymorphic_referenced.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "type":"record", - "name":"ReferencingMixedPolymorphic", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name":"notNullable", - "type":[ - { - "type":"record", - "name":"AddExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"ConstExpr", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"MultiplicationExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NegateExpr", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NullaryExpr", - "fields":[] - }, - { - "type":"record", - "name":"SubstractExpr", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - } - ] - } - ] -} diff --git a/src/test/resources/namespace.json b/src/test/resources/namespace.json deleted file mode 100644 index e7ad9519..00000000 --- a/src/test/resources/namespace.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "type": "record", - "name": "AnnotatedNamespace", - "namespace": "com.yuval", - "fields": [ - { - "name": "s", - "type": "string" - }, - { - "name": "internal", - "type": { - "type": "record", - "name": "InternalAnnotated", - "namespace": "com.yuval.internal", - "fields": [ - { - "name": "i", - "type": "int" - } - ] - } - } - ] -} diff --git a/src/test/resources/namespace_empty.json b/src/test/resources/namespace_empty.json deleted file mode 100644 index 8d879a32..00000000 --- a/src/test/resources/namespace_empty.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "record", - "name": "Foo", - "fields": [ - { - "name": "s", - "type": "string" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/nested.json b/src/test/resources/nested.json index f20f36c8..bfd66a27 100644 --- a/src/test/resources/nested.json +++ b/src/test/resources/nested.json @@ -1,7 +1,7 @@ { "type": "record", "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema", + "namespace": "com.github.avrokotlin.avro4k.schema.BasicSchemaTest", "fields": [ { "name": "foo", diff --git a/src/test/resources/nullable_enum.json b/src/test/resources/nullable_enum.json deleted file mode 100644 index 51585a79..00000000 --- a/src/test/resources/nullable_enum.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.sksamuel.avro4s.schema.EnumSchemaTest", - "fields": [ - { - "name": "wine", - "type": { - "type": "enum", - "name": "Wine", - "namespace": "com.sksamuel.avro4s.schema", - "symbols": [ - "Malbec", - "Shiraz", - "CabSav", - "Merlot" - ] - } - } - ] -} diff --git a/src/test/resources/nullables-with-defaults.json b/src/test/resources/nullables-with-defaults.json deleted file mode 100644 index c8499d37..00000000 --- a/src/test/resources/nullables-with-defaults.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.NullableWithDefaultsSchemaTest", - "fields": [ - { - "name": "nullableString", - "type": [ - "null", - "string" - ], - "default": null - }, - { - "name": "nullableBoolean", - "type": [ - "null", - "boolean" - ], - "default": null - } - ] -} diff --git a/src/test/resources/nullables.json b/src/test/resources/nullables.json deleted file mode 100644 index 29e65294..00000000 --- a/src/test/resources/nullables.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.NullableSchemaTest", - "fields": [ - { - "name": "nullableString", - "type": [ - "null", - "string" - ] - }, - { - "name": "nullableBoolean", - "type": [ - "null", - "boolean" - ] - } - ] -} diff --git a/src/test/resources/pair.json b/src/test/resources/pair.json deleted file mode 100644 index fbb75895..00000000 --- a/src/test/resources/pair.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "Annotated", - "namespace": "com.github.avrokotlin.avro4k.schema.PairSchemaTest", - "fields": [ - { - "name": "p", - "type": "string" - } - ] -} diff --git a/src/test/resources/pair_records.json b/src/test/resources/pair_records.json deleted file mode 100644 index ce6ef1bd..00000000 --- a/src/test/resources/pair_records.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "Annotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroAliasSchemaTest", - "fields": [ - { - "name": "str", - "type": "string", - "aliases": [ - "cold" - ] - } - ] -} diff --git a/src/test/resources/pascal_case_schema.json b/src/test/resources/pascal_case_schema.json index 2942f4cd..a7539fb1 100644 --- a/src/test/resources/pascal_case_schema.json +++ b/src/test/resources/pascal_case_schema.json @@ -1,51 +1,27 @@ { - "type": "record", - "name": "Interface", - "namespace": "com.github.avrokotlin.avro4k.schema.NamingStrategySchemaTest", - "fields": [ - { - "name": "Name", - "type": "string" - }, - { - "name": "Ipv4Address", - "type": "string" - }, - { - "name": "Ipv4SubnetMask", - "type": "int" - }, - { - "name": "V", - "type": { - "type": "enum", - "name": "InternetProtocol", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "IPv4", - "IPv6" - ] - } - }, - { - "name": "SubInterface", - "type": [ - "null", - { - "type": "record", - "name": "SubInterface", - "fields": [ - { - "name": "Name", - "type": "string" - }, - { - "name": "Ipv4Address", - "type": "string" - } - ] - } - ] + "type": "record", + "name": "Interface", + "namespace": "com.github.avrokotlin.avro4k.schema.FieldNamingStrategySchemaTest", + "fields": [ + { + "name": "Name", + "type": "string" + }, + { + "name": "Ipv4Address", + "type": "string" + }, + { + "name": "Ipv4SubnetMask", + "type": "int" + }, + { + "name": "V", + "type": { + "type": "enum", + "name": "InternetProtocol", + "symbols": ["IPv4", "IPv6"] } - ] + } + ] } diff --git a/src/test/resources/polymorphic.json b/src/test/resources/polymorphic.json deleted file mode 100644 index b5d718ad..00000000 --- a/src/test/resources/polymorphic.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "type": "record", - "name": "UnsealedChildOne", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "one", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "UnsealedChildTwo", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "two", - "type": "string" - } - ] - } -] diff --git a/src/test/resources/polymorphic_reference.json b/src/test/resources/polymorphic_reference.json deleted file mode 100644 index 75f6f140..00000000 --- a/src/test/resources/polymorphic_reference.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "record", - "name": "ReferencingPolymorphicRoot", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "root", - "type": [ - { - "type": "record", - "name": "UnsealedChildOne", - "fields": [ - { - "name": "one", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "UnsealedChildTwo", - "fields": [ - { - "name": "two", - "type": "string" - } - ] - } - ] - }, - { - "name": "nullableRoot", - "type": [ - "null", - "UnsealedChildOne", - "UnsealedChildTwo" - ] - } - ] -} - diff --git a/src/test/resources/polymorphic_reference_list.json b/src/test/resources/polymorphic_reference_list.json deleted file mode 100644 index 45013ed8..00000000 --- a/src/test/resources/polymorphic_reference_list.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "type": "record", - "name": "PolymorphicRootInList", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "listOfRoot", - "type": { - "type": "array", - "items": [ - { - "type": "record", - "name": "UnsealedChildOne", - "fields": [ - { - "name": "one", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "UnsealedChildTwo", - "fields": [ - { - "name": "two", - "type": "string" - } - ] - } - ] - - } - } - ] -} - diff --git a/src/test/resources/polymorphic_reference_map.json b/src/test/resources/polymorphic_reference_map.json deleted file mode 100644 index 6a5de62a..00000000 --- a/src/test/resources/polymorphic_reference_map.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "type": "record", - "name": "PolymorphicRootInMap", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "mapOfRoot", - "type": { - "type": "map", - "values": [ - { - "type": "record", - "name": "UnsealedChildOne", - "fields": [ - { - "name": "one", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "UnsealedChildTwo", - "fields": [ - { - "name": "two", - "type": "string" - } - ] - } - ] - - } - } - ] -} - diff --git a/src/test/resources/primitive_boolean.json b/src/test/resources/primitive_boolean.json deleted file mode 100644 index ccaf553a..00000000 --- a/src/test/resources/primitive_boolean.json +++ /dev/null @@ -1 +0,0 @@ -"boolean" \ No newline at end of file diff --git a/src/test/resources/primitive_bytes.json b/src/test/resources/primitive_bytes.json deleted file mode 100644 index 346cb6af..00000000 --- a/src/test/resources/primitive_bytes.json +++ /dev/null @@ -1 +0,0 @@ -"bytes" \ No newline at end of file diff --git a/src/test/resources/primitive_double.json b/src/test/resources/primitive_double.json deleted file mode 100644 index 352b8cbc..00000000 --- a/src/test/resources/primitive_double.json +++ /dev/null @@ -1 +0,0 @@ -"double" \ No newline at end of file diff --git a/src/test/resources/primitive_float.json b/src/test/resources/primitive_float.json deleted file mode 100644 index 61628143..00000000 --- a/src/test/resources/primitive_float.json +++ /dev/null @@ -1 +0,0 @@ -"float" \ No newline at end of file diff --git a/src/test/resources/primitive_int.json b/src/test/resources/primitive_int.json deleted file mode 100644 index f08c4dfd..00000000 --- a/src/test/resources/primitive_int.json +++ /dev/null @@ -1 +0,0 @@ -"int" \ No newline at end of file diff --git a/src/test/resources/primitive_long.json b/src/test/resources/primitive_long.json deleted file mode 100644 index 9f4e86f3..00000000 --- a/src/test/resources/primitive_long.json +++ /dev/null @@ -1 +0,0 @@ -"long" \ No newline at end of file diff --git a/src/test/resources/primitive_string.json b/src/test/resources/primitive_string.json deleted file mode 100644 index 1f13d5d4..00000000 --- a/src/test/resources/primitive_string.json +++ /dev/null @@ -1 +0,0 @@ -"string" \ No newline at end of file diff --git a/src/test/resources/props_annotation_class.json b/src/test/resources/props_annotation_class.json deleted file mode 100644 index 06abad6a..00000000 --- a/src/test/resources/props_annotation_class.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "record", - "name": "TypeAnnotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroPropSchemaTest", - "fields": [ - { - "name": "str", - "type": "string" - } - ], - "cold": "play" -} diff --git a/src/test/resources/props_annotation_field.json b/src/test/resources/props_annotation_field.json deleted file mode 100644 index 0fb57646..00000000 --- a/src/test/resources/props_annotation_field.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "record", - "name": "AnnotatedProperties", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroPropSchemaTest", - "fields": [ - { - "name": "str", - "type": "string", - "cold": "play" - }, - { - "name": "long", - "type": "long", - "kate": "bush" - }, - { - "name": "int", - "type": "int" - } - ] -} diff --git a/src/test/resources/props_annotation_scala_enum.json b/src/test/resources/props_annotation_scala_enum.json deleted file mode 100644 index af07e5bd..00000000 --- a/src/test/resources/props_annotation_scala_enum.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "record", - "name": "EnumAnnotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroPropSchemaTest", - "fields": [ - { - "name": "colours", - "type": { - "type": "enum", - "name": "Colours", - "symbols": [ - "Red", - "Green", - "Blue" - ] - }, - "cold": "play" - } - ] -} diff --git a/src/test/resources/props_json_annotation_class.json b/src/test/resources/props_json_annotation_class.json index aff1f874..75dadb7d 100644 --- a/src/test/resources/props_json_annotation_class.json +++ b/src/test/resources/props_json_annotation_class.json @@ -1,12 +1,32 @@ { "type": "record", "name": "TypeAnnotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroJsonPropSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.schema.AvroPropsSchemaTest", "fields": [ { - "name": "str", - "type": "string" + "name": "field", + "type": { + "type": "enum", + "name": "EnumAnnotated", + "symbols": ["Red", "Green", "Blue"], + "default": "Green", + "enums": "power", + "countingAgain": ["three", "four"] + }, + "cold": "play", + "complexObject": { + "a": "foo", + "b": 200, + "c": true, + "d": null, + "e": { + "e1": null, + "e2": 429 + }, + "f": ["bar", 404, false, null, {}] + } } ], - "guns": ["and", "roses"] + "hey": "there", + "counting": ["one", "two"] } diff --git a/src/test/resources/props_json_annotation_field.json b/src/test/resources/props_json_annotation_field.json deleted file mode 100644 index 88db680c..00000000 --- a/src/test/resources/props_json_annotation_field.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "record", - "name": "AnnotatedProperties", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroJsonPropSchemaTest", - "fields": [ - { - "name": "str", - "type": "string", - "guns": ["and", "roses"] - }, - { - "name": "long", - "type": "long", - "jean": ["michel", "jarre"] - }, - { - "name": "int", - "type": "int", - "object": { - "a": "foo", - "b": 200, - "c": true, - "d": null, - "e": { "e1": null, "e2": 429 }, - "f": [ "bar", 404, false, null, {} ] - } - } - ] -} diff --git a/src/test/resources/props_json_annotation_scala_enum.json b/src/test/resources/props_json_annotation_scala_enum.json deleted file mode 100644 index 57f84e44..00000000 --- a/src/test/resources/props_json_annotation_scala_enum.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "record", - "name": "EnumAnnotated", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroJsonPropSchemaTest", - "fields": [ - { - "name": "colours", - "type": { - "type": "enum", - "name": "Colours", - "symbols": [ - "Red", - "Green", - "Blue" - ] - }, - "guns": ["and", "roses"] - } - ] -} diff --git a/src/test/resources/recursive_class.json b/src/test/resources/recursive_class.json index 51e23cc1..8cb6d777 100644 --- a/src/test/resources/recursive_class.json +++ b/src/test/resources/recursive_class.json @@ -1,12 +1,16 @@ { - "type" : "record", - "name" : "RecursiveClass", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "fields" : [ { - "name" : "payload", - "type" : "int" - }, { - "name" : "klass", - "type" : [ "null", "RecursiveClass" ] - } ] + "type": "record", + "name": "RecursiveClass", + "namespace": "com.github.avrokotlin.avro4k.schema.RecursiveSchemaTest", + "fields": [ + { + "name": "payload", + "type": "int" + }, + { + "name": "klass", + "type": ["null", "RecursiveClass"], + "default": null + } + ] } diff --git a/src/test/resources/recursive_list.json b/src/test/resources/recursive_list.json index afb7bcf1..695fbba8 100644 --- a/src/test/resources/recursive_list.json +++ b/src/test/resources/recursive_list.json @@ -1,15 +1,22 @@ { - "type" : "record", - "name" : "RecursiveListItem", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "fields" : [ { - "name" : "payload", - "type" : "int" - }, { - "name" : "list", - "type" : [ "null", { - "type" : "array", - "items" : "RecursiveListItem" - } ] - } ] + "type": "record", + "name": "RecursiveListItem", + "namespace": "com.github.avrokotlin.avro4k.schema.RecursiveSchemaTest", + "fields": [ + { + "name": "payload", + "type": "int" + }, + { + "name": "list", + "type": [ + "null", + { + "type": "array", + "items": "RecursiveListItem" + } + ], + "default": null + } + ] } diff --git a/src/test/resources/recursive_map.json b/src/test/resources/recursive_map.json index 89934b85..d3e45577 100644 --- a/src/test/resources/recursive_map.json +++ b/src/test/resources/recursive_map.json @@ -1,15 +1,22 @@ { - "type" : "record", - "name" : "RecursiveMapValue", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "fields" : [ { - "name" : "payload", - "type" : "int" - }, { - "name" : "map", - "type" : [ "null", { - "type" : "map", - "values" : "RecursiveMapValue" - } ] - } ] + "type": "record", + "name": "RecursiveMapValue", + "namespace": "com.github.avrokotlin.avro4k.schema.RecursiveSchemaTest", + "fields": [ + { + "name": "payload", + "type": "int" + }, + { + "name": "map", + "type": [ + "null", + { + "type": "map", + "values": "RecursiveMapValue" + } + ], + "default": null + } + ] } diff --git a/src/test/resources/recursive_nested.json b/src/test/resources/recursive_nested.json index df961211..f7ce13b3 100644 --- a/src/test/resources/recursive_nested.json +++ b/src/test/resources/recursive_nested.json @@ -1,44 +1,34 @@ { - "type": "record", - "name": "Level1", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "level2", - "type": [ - "null", + "type": "record", + "name": "Level1", + "namespace": "com.github.avrokotlin.avro4k.schema.RecursiveSchemaTest", + "fields": [ + { + "name": "level2", + "type": [ + "null", + { + "type": "record", + "name": "Level2", + "fields": [ { - "type": "record", - "name": "Level2", - "fields": [ + "name": "level3", + "type": { + "type": "record", + "name": "Level3", + "fields": [ { - "name": "level3", - "type": { - "type": "record", - "name": "Level3", - "fields": [ - { - "name": "level4", - "type": [ - "null", - { - "type": "record", - "name": "Level4", - "fields": [ - { - "name": "level1", - "type": "Level1" - } - ] - } - ] - } - ] - } + "name": "level1", + "type": ["null", "Level1"], + "default": null } - ] + ] + } } - ] - } - ] + ] + } + ], + "default": null + } + ] } diff --git a/src/test/resources/recursive_pair.json b/src/test/resources/recursive_pair.json deleted file mode 100644 index b5e6b994..00000000 --- a/src/test/resources/recursive_pair.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type" : "record", - "name" : "RecursivePair", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "fields" : [ { - "name" : "payload", - "type" : "int" - }, { - "name" : "pair", - "type" : [ "null", { - "type" : "record", - "name" : "Pair", - "namespace" : "kotlin", - "fields" : [ { - "name" : "first", - "type" : "com.github.avrokotlin.avro4k.schema.RecursivePair" - }, { - "name" : "second", - "type" : "com.github.avrokotlin.avro4k.schema.RecursivePair" - } ] - } ] - } ] -} diff --git a/src/test/resources/sealed.json b/src/test/resources/sealed.json index bcd900a4..7f222075 100644 --- a/src/test/resources/sealed.json +++ b/src/test/resources/sealed.json @@ -1,50 +1,49 @@ [ - { - "type":"record", - "name":"Add", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"Substract", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"Nullary", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation", - "fields":[] - }, - { - "type":"record", - "name":"Negate", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Unary", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - } - + { + "type": "record", + "name": "Add", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ + { + "name": "left", + "type": "int" + }, + { + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Substract", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ + { + "name": "left", + "type": "int" + }, + { + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Nullary", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation", + "fields": [] + }, + { + "type": "record", + "name": "Negate", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Unary", + "fields": [ + { + "name": "value", + "type": "int" + } + ] + } ] diff --git a/src/test/resources/sealed_interface_hierarchy_referenced.json b/src/test/resources/sealed_interface_hierarchy_referenced.json deleted file mode 100644 index fe60b98b..00000000 --- a/src/test/resources/sealed_interface_hierarchy_referenced.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "type":"record", - "name":"ReferencingSealedInterface", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields":[ - { - "name":"notNullable", - "type":[ - { - "type":"record", - "name":"AddCalculable", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NegateCalculable", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NullaryCalculable", - "fields":[] - }, - { - "type":"record", - "name":"SubstractCalculable", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - } - ] - } - ] -} diff --git a/src/test/resources/sealed_interface_nullable_referenced.json b/src/test/resources/sealed_interface_nullable_referenced.json deleted file mode 100644 index 4d282e97..00000000 --- a/src/test/resources/sealed_interface_nullable_referenced.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "type":"record", - "name":"ReferencingNullableSealedInterface", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields":[ - { - "name":"nullable", - "type":[ - "null", - { - "type":"record", - "name":"AddCalculable", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NegateCalculable", - "fields":[ - { - "name":"value", - "type":"int" - } - ] - }, - { - "type":"record", - "name":"NullaryCalculable", - "fields":[] - }, - { - "type":"record", - "name":"SubstractCalculable", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - } - ], - "default":null - } - ] -} diff --git a/src/test/resources/sealed_nullable_referenced.json b/src/test/resources/sealed_nullable_referenced.json index 0db58602..4011adf4 100644 --- a/src/test/resources/sealed_nullable_referenced.json +++ b/src/test/resources/sealed_nullable_referenced.json @@ -1,61 +1,61 @@ { - "type":"record", - "name":"ReferencingNullableSealedClass", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields":[ - { - "name":"nullable", - "type":[ - "null", + "type": "record", + "name": "ReferencingNullableSealedClass", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest", + "fields": [ + { + "name": "nullable", + "type": [ + "null", + { + "type": "record", + "name": "Add", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ { - "type":"record", - "name":"Add", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] + "name": "left", + "type": "int" }, { - "type":"record", - "name":"Substract", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Substract", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ { - "type":"record", - "name":"Nullary", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation", - "fields":[] + "name": "left", + "type": "int" }, { - "type":"record", - "name":"Negate", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Unary", - "fields":[ - { - "name":"value", - "type":"int" - } - ] + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Nullary", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation", + "fields": [] + }, + { + "type": "record", + "name": "Negate", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Unary", + "fields": [ + { + "name": "value", + "type": "int" } - ], - "default" : null - } - ] + ] + } + ], + "default": null + } + ] } diff --git a/src/test/resources/sealed_referenced.json b/src/test/resources/sealed_referenced.json index aca68550..63847590 100644 --- a/src/test/resources/sealed_referenced.json +++ b/src/test/resources/sealed_referenced.json @@ -1,59 +1,59 @@ { - "type":"record", - "name":"ReferencingSealedClass", - "namespace":"com.github.avrokotlin.avro4k.schema", - "fields":[ - { - "name":"notNullable", - "type":[ + "type": "record", + "name": "ReferencingSealedClass", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest", + "fields": [ + { + "name": "notNullable", + "type": [ + { + "type": "record", + "name": "Add", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ { - "type":"record", - "name":"Add", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] + "name": "left", + "type": "int" }, { - "type":"record", - "name":"Substract", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Binary", - "fields":[ - { - "name":"left", - "type":"int" - }, - { - "name":"right", - "type":"int" - } - ] - }, + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Substract", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Binary", + "fields": [ { - "type":"record", - "name":"Nullary", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation", - "fields":[] + "name": "left", + "type": "int" }, { - "type":"record", - "name":"Negate", - "namespace":"com.github.avrokotlin.avro4k.schema.Operation.Unary", - "fields":[ - { - "name":"value", - "type":"int" - } - ] + "name": "right", + "type": "int" + } + ] + }, + { + "type": "record", + "name": "Nullary", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation", + "fields": [] + }, + { + "type": "record", + "name": "Negate", + "namespace": "com.github.avrokotlin.avro4k.schema.SealedClassSchemaTest.Operation.Unary", + "fields": [ + { + "name": "value", + "type": "int" } - ] - } - ] + ] + } + ] + } + ] } diff --git a/src/test/resources/set_of_maps.json b/src/test/resources/set_of_maps.json index 141788a9..e657d999 100644 --- a/src/test/resources/set_of_maps.json +++ b/src/test/resources/set_of_maps.json @@ -1,7 +1,7 @@ { "type": "record", "name": "SetTest", - "namespace": "com.github.avrokotlin.avro4k.schema.MapSchemaTest", + "namespace": "com.github.avrokotlin.avro4k.encoding.MapEncodingTest", "fields": [ { "name": "set", @@ -11,7 +11,8 @@ "type": "map", "values": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/setdoubles.json b/src/test/resources/setdoubles.json index 962b1fb6..da4e979a 100644 --- a/src/test/resources/setdoubles.json +++ b/src/test/resources/setdoubles.json @@ -10,7 +10,8 @@ "items": { "type": "double" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/setrecords.json b/src/test/resources/setrecords.json index 0bc04264..9016f3d2 100644 --- a/src/test/resources/setrecords.json +++ b/src/test/resources/setrecords.json @@ -17,7 +17,8 @@ } ] } - } + }, + "default": [] } ] } diff --git a/src/test/resources/setstrings.json b/src/test/resources/setstrings.json index 11bb9e8f..2c32d345 100644 --- a/src/test/resources/setstrings.json +++ b/src/test/resources/setstrings.json @@ -10,7 +10,8 @@ "items": { "type": "string" } - } + }, + "default": [] } ] } diff --git a/src/test/resources/snake_case_schema.json b/src/test/resources/snake_case_schema.json index bb7c42a9..8271e6b9 100644 --- a/src/test/resources/snake_case_schema.json +++ b/src/test/resources/snake_case_schema.json @@ -1,51 +1,27 @@ { - "type": "record", - "name": "Interface", - "namespace": "com.github.avrokotlin.avro4k.schema.NamingStrategySchemaTest", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "ipv4_address", - "type": "string" - }, - { - "name": "ipv4_subnet_mask", - "type": "int" - }, - { - "name": "v", - "type": { - "type": "enum", - "name": "InternetProtocol", - "namespace" : "com.github.avrokotlin.avro4k.schema", - "symbols": [ - "IPv4", - "IPv6" - ] - } - }, - { - "name": "sub_interface", - "type": [ - "null", - { - "type": "record", - "name": "SubInterface", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "ipv4_address", - "type": "string" - } - ] - } - ] + "type": "record", + "name": "Interface", + "namespace": "com.github.avrokotlin.avro4k.schema.FieldNamingStrategySchemaTest", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "ipv4_address", + "type": "string" + }, + { + "name": "ipv4_subnet_mask", + "type": "int" + }, + { + "name": "v", + "type": { + "type": "enum", + "name": "InternetProtocol", + "symbols": ["IPv4", "IPv6"] } - ] + } + ] } diff --git a/src/test/resources/timestamp.json b/src/test/resources/timestamp.json deleted file mode 100644 index 72e690d2..00000000 --- a/src/test/resources/timestamp.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "TimestampTest", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "ts", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - } - ] -} diff --git a/src/test/resources/timestamp_nullable.json b/src/test/resources/timestamp_nullable.json deleted file mode 100644 index 3bada0b4..00000000 --- a/src/test/resources/timestamp_nullable.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.DateSchemaTest", - "fields": [ - { - "name": "ts", - "type": [ - "null", - { - "type": "long", - "logicalType": "timestamp-millis" - } - ] - } - ] -} diff --git a/src/test/resources/top_level_class_namespace.json b/src/test/resources/top_level_class_namespace.json deleted file mode 100644 index fdf5904c..00000000 --- a/src/test/resources/top_level_class_namespace.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "Tau", - "namespace": "com.github.avrokotlin.avro4k.schema", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "boolean" - } - ] -} diff --git a/src/test/resources/top_level_object_namespace.json b/src/test/resources/top_level_object_namespace.json deleted file mode 100644 index 31e5d930..00000000 --- a/src/test/resources/top_level_object_namespace.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "Inner", - "namespace": "com.github.avrokotlin.avro4k.schema.Outer", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "b", - "type": "boolean" - } - ] -} diff --git a/src/test/resources/transient.json b/src/test/resources/transient.json deleted file mode 100644 index 81ec5dd4..00000000 --- a/src/test/resources/transient.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "TransientFoo", - "namespace": "com.github.avrokotlin.avro4k.schema.TransientSchemaTest", - "fields": [ - { - "name": "a", - "type": "string" - }, - { - "name": "c", - "type": "string" - } - ] -} diff --git a/src/test/resources/url.json b/src/test/resources/url.json deleted file mode 100644 index df71c8c7..00000000 --- a/src/test/resources/url.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "record", - "name": "Test", - "namespace": "com.github.avrokotlin.avro4k.schema.URLSchemaTest", - "fields": [ - { - "name": "b", - "type": "string" - } - ] -} diff --git a/src/test/resources/url_nullable.json b/src/test/resources/url_nullable.json deleted file mode 100644 index 14efac57..00000000 --- a/src/test/resources/url_nullable.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "NullableTest", - "namespace": "com.github.avrokotlin.avro4k.schema.URLSchemaTest", - "fields": [ - { - "name": "b", - "type": [ - "null", - "string" - ] - } - ] -} diff --git a/src/test/resources/uuid.json b/src/test/resources/uuid.json deleted file mode 100644 index 419bdbbe..00000000 --- a/src/test/resources/uuid.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "name": "UUIDTest", - "namespace": "com.github.avrokotlin.avro4k.schema.UUIDSchemaTest", - "fields": [ - { - "name": "uuid", - "type": { - "type": "string", - "logicalType": "uuid" - } - } - ] -} diff --git a/src/test/resources/value_class.json b/src/test/resources/value_class.json deleted file mode 100644 index 1d08277f..00000000 --- a/src/test/resources/value_class.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "record", - "name": "ContainsInlineTest", - "namespace": "com.github.avrokotlin.avro4k.schema.ValueClassSchemaTest", - "fields": [ - { - "name": "id", - "type": "string" - }, { - "name" : "uuid", - "type" : { - "type" : "string", - "logicalType" : "uuid" - } - } - ] -} diff --git a/src/test/resources/value_type.json b/src/test/resources/value_type.json deleted file mode 100644 index 2558ef70..00000000 --- a/src/test/resources/value_type.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "record", - "name": "Product", - "namespace": "com.github.avrokotlin.avro4k.schema.AvroInlineSchemaTest", - "fields": [ - { - "name": "id", - "type": "string" - }, - { - "name": "name", - "type": "string" - } - ] -}