├── .editorconfig ├── .github └── workflows │ ├── ci.yaml │ ├── gradle_task.yml │ └── release.yaml ├── .gitignore ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build-logic ├── build.gradle.kts ├── repositories.gradle.kts ├── settings.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── ks3 │ │ └── conventions │ │ ├── Ks3BuildLogicSettings.kt │ │ ├── api-validation.gradle.kts │ │ ├── base.gradle.kts │ │ ├── lang │ │ ├── accessors.kt │ │ ├── kotlin-multiplatform-base.gradle.kts │ │ ├── kotlin-multiplatform-js.gradle.kts │ │ ├── kotlin-multiplatform-jvm.gradle.kts │ │ ├── kotlin-multiplatform-native.gradle.kts │ │ └── kotlinJsExtensions.kt │ │ └── publishing │ │ ├── accessors.kt │ │ └── maven-publish.gradle.kts │ └── test │ └── kotlin │ └── IsReleaseVersionTest.kt ├── build.gradle.kts ├── doc ├── README.md ├── builders.md ├── jdk.md └── standard.md ├── dokka └── custom-styles.css ├── gradle.properties ├── gradle ├── kotlin-js-store │ └── yarn.lock ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ks3-core ├── api │ ├── ks3-core.api │ └── ks3-core.klib.api ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── ks3 │ └── core │ ├── ExperimentalKs3.kt │ └── Ks3Internal.kt ├── ks3-jdk ├── Module.md ├── README.md ├── api │ ├── ks3-jdk.api │ └── ks3-jdk.klib.api ├── build.gradle.kts └── src │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── ks3 │ │ └── java │ │ ├── io │ │ └── FilePathSerializer.kt │ │ ├── math │ │ ├── BigDecimalSerializers.kt │ │ └── BigIntegerSerializers.kt │ │ ├── net │ │ ├── UriSerializer.kt │ │ └── UrlSerializer.kt │ │ ├── time │ │ ├── InstantAsStringSerializer.kt │ │ ├── LocalDateAsStringSerializer.kt │ │ ├── LocalDateTimeAsStringSerializer.kt │ │ ├── LocalTimeAsStringSerializer.kt │ │ ├── OffsetDateTimeAsStringSerializer.kt │ │ ├── YearMonthAsStringSerializer.kt │ │ └── YearSerializers.kt │ │ ├── typealiases │ │ └── Typealiases.kt │ │ └── util │ │ ├── AtomicSerializers.kt │ │ ├── CurrencySerializer.kt │ │ ├── LocaleSerializer.kt │ │ └── UuidSerializer.kt │ └── jvmTest │ └── kotlin │ └── io │ └── ks3 │ └── java │ ├── io │ └── FileSerializerTests.kt │ ├── math │ ├── BigDecimalSerializerTests.kt │ └── BigIntegerSerializerTests.kt │ ├── net │ ├── UriSerializerTests.kt │ └── UrlSerializerTests.kt │ ├── time │ ├── InstantSerializersTests.kt │ ├── LocalDateAsStringSerializerTests.kt │ ├── LocalDateTimeAsStringSerializerTests.kt │ ├── LocalTimeAsStringSerializerTests.kt │ ├── OffsetDateTimeAsStringSerializerTests.kt │ ├── YearMonthAsStringSerializerTests.kt │ └── YearSerializersTests.kt │ └── util │ ├── AtomicSerializersTests.kt │ ├── CurrencySerializerTest.kt │ ├── LocaleSerializerTests.kt │ └── UuidSerializerTests.kt ├── ks3-standard ├── Module.md ├── README.md ├── api │ ├── ks3-standard.api │ └── ks3-standard.klib.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── ks3 │ │ └── standard │ │ ├── ByteArrayAsBase64StringSerializer.kt │ │ ├── DoubleSerializer.kt │ │ ├── EnumAsOrdinalSerializer.kt │ │ ├── EnumDecodingException.kt │ │ ├── IntSerializer.kt │ │ ├── LenientJsonArraySerializer.kt │ │ ├── LongSerializer.kt │ │ ├── ObjectWrappingSerializer.kt │ │ ├── SortedCollectionSerializer.kt │ │ └── StringSerializer.kt │ ├── commonTest │ └── kotlin │ │ └── io │ │ └── ks3 │ │ └── standard │ │ ├── ByteArrayAsBase64StringSerializerTest.kt │ │ ├── EnumAsOrdinalSerializerTest.kt │ │ ├── LenientJsonArraySerializerTest.kt │ │ └── SortedCollectionSerializerTest.kt │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── ks3 │ │ └── standard │ │ ├── TupleDescriptor.kt │ │ └── TupleSerializer.kt │ └── jvmTest │ └── kotlin │ └── io │ └── ks3 │ └── standard │ └── TupleSerializerTest.kt ├── ks3-test ├── Module.md ├── api │ ├── ks3-test.api │ └── ks3-test.klib.api ├── build.gradle.kts └── src │ └── commonMain │ ├── kotlin │ └── io.ks3.test │ │ ├── SerializerTestFactory.kt │ │ └── TestHelpers.kt │ └── resources │ └── kotest.properties ├── renovate.json └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | indent_style=space 5 | indent_size=3 6 | max_line_length=140 7 | 8 | TRIM_TRAILING_WHITESPACE=true 9 | insert_final_newline=true 10 | 11 | end_of_line=lf 12 | 13 | [*.md] 14 | indent_size=2 15 | 16 | [*.{kt,kts}] 17 | ij_kotlin_import_nested_classes=false 18 | ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ 19 | ij_kotlin_name_count_to_use_star_import=2147483647 20 | ij_kotlin_name_count_to_use_star_import_for_members=2147483647 21 | ij_kotlin_allow_trailing_comma = true 22 | ij_kotlin_allow_trailing_comma_on_call_site = true 23 | 24 | # Disabled for now. io.ks3.java.typealias -> io.ks3.java.typealiases slated for 1.0.0 25 | ktlint_standard_package-name = disabled 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | paths-ignore: 8 | - "doc/**" 9 | - "documentation/**" 10 | - "*.yml" 11 | branches: 12 | - "main" 13 | - "release/*" 14 | tags: 15 | - "v**" 16 | 17 | workflow_dispatch: 18 | 19 | release: 20 | types: [ published ] 21 | 22 | 23 | concurrency: 24 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 25 | cancel-in-progress: true 26 | 27 | 28 | jobs: 29 | 30 | linux-tests: 31 | name: "./gradlew ${{ matrix.target }} @ ${{ matrix.os }}" 32 | strategy: 33 | matrix: 34 | os: [ ubuntu-latest ] 35 | target: 36 | - jvmTest 37 | - jsTest 38 | - linuxX64Test 39 | # - jsLegacyBrowserTest 40 | # - jsLegacyNodeTest 41 | # - jsLegacyTest 42 | # - jsIrBrowserTest 43 | # - jsIrNodeTest 44 | uses: ./.github/workflows/gradle_task.yml 45 | with: 46 | runs-on: ${{ matrix.os }} 47 | gradle-task: ${{ matrix.target }} --stacktrace 48 | 49 | 50 | macos-tests: 51 | name: "./gradlew ${{ matrix.target }} @ ${{ matrix.os }}" 52 | strategy: 53 | matrix: 54 | os: [ macos-latest ] 55 | target: 56 | - apiCheck -Pks3_enableKotlinNative=true -Pks3_enableKotlinJs=true -DenableKlibValidation 57 | - iosSimulatorArm64Test 58 | - iosX64Test 59 | - macosArm64Test 60 | - macosX64Test 61 | - tvosSimulatorArm64Test 62 | - tvosX64Test 63 | - watchosSimulatorArm64Test 64 | - watchosX64Test 65 | uses: ./.github/workflows/gradle_task.yml 66 | with: 67 | runs-on: ${{ matrix.os }} 68 | gradle-task: ${{ matrix.target }} --stacktrace 69 | 70 | 71 | windows-tests: 72 | name: "./gradlew ${{ matrix.target }} @ ${{ matrix.os }}" 73 | strategy: 74 | matrix: 75 | os: [ windows-latest ] 76 | target: 77 | - mingwX64Test 78 | uses: ./.github/workflows/gradle_task.yml 79 | with: 80 | runs-on: ${{ matrix.os }} 81 | gradle-task: ${{ matrix.target }} --stacktrace 82 | 83 | 84 | gradle-check: 85 | name: "./gradlew check @ ${{ matrix.os }}" 86 | needs: [ linux-tests, macos-tests, windows-tests ] 87 | # Run 'check' task for each OS. 88 | # To improve speed, only enable Kotlin/JVM (Kotlin/Native is particularly slow). 89 | # The other targets are tested individually above. 90 | strategy: 91 | matrix: 92 | os: [ ubuntu-latest, macos-latest, windows-latest ] 93 | uses: ./.github/workflows/gradle_task.yml 94 | with: 95 | runs-on: ${{ matrix.os }} 96 | gradle-task: check --stacktrace -Pks3_enableKotlinNative=false -Pks3_enableKotlinJs=false 97 | 98 | 99 | docs: 100 | runs-on: ubuntu-latest 101 | steps: 102 | - name: "Checkout code" 103 | uses: actions/checkout@v4 104 | with: 105 | fetch-depth: 1 # Checkout the code. No need of tags and whole history. 106 | 107 | - name: Setup JDK 108 | uses: actions/setup-java@v4 109 | with: 110 | distribution: 'temurin' 111 | java-version: '11' 112 | 113 | - name: Setup Gradle 114 | uses: gradle/gradle-build-action@v3 115 | 116 | - name: Build docs 117 | run: ./gradlew dokkatooGeneratePublicationHtml --no-configuration-cache --no-build-cache --rerun-tasks 118 | 119 | - name: Deploy docs 120 | uses: peaceiris/actions-gh-pages@v4 121 | if: github.ref == 'refs/heads/main' 122 | with: 123 | github_token: ${{ secrets.GITHUB_TOKEN }} 124 | publish_dir: ./build/dokka/html/ 125 | -------------------------------------------------------------------------------- /.github/workflows/gradle_task.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Task 2 | run-name: "Gradle Task ${{ inputs.gradle-task }} @ ${{ inputs.runs-on }}" 3 | 4 | # Reusable Workflow for running a Gradle task 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | workflow_call: 10 | inputs: 11 | ref: 12 | description: "The git branch, tag or SHA to checkout" 13 | required: false 14 | type: string 15 | gradle-task: 16 | description: "The Gradle task to run, including any flags" 17 | required: true 18 | type: string 19 | runs-on: 20 | description: "OSes to run the task on" 21 | required: true 22 | type: string 23 | 24 | 25 | concurrency: 26 | # note: the Workflow inputs are also included in the concurrency group 27 | group: "${{ github.workflow }} ${{ join(inputs.*) }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 28 | cancel-in-progress: true 29 | 30 | 31 | permissions: 32 | contents: read 33 | 34 | 35 | jobs: 36 | 37 | run-task: 38 | runs-on: ${{ inputs.runs-on }} 39 | name: "./gradlew ${{ inputs.gradle-task}} @ ${{ inputs.runs-on }}" 40 | timeout-minutes: 60 41 | steps: 42 | - name: Checkout the repo 43 | uses: actions/checkout@v4 44 | with: 45 | ref: ${{inputs.ref }} 46 | fetch-depth: 0 47 | 48 | - name: Calculate version 49 | id: version 50 | uses: Kantis/semantic-version@master 51 | with: 52 | version_format: "${major}.${minor}.${patch}.${increment}-SNAPSHOT" 53 | 54 | - name: Setup JDK 55 | uses: actions/setup-java@v4 56 | with: 57 | distribution: temurin 58 | java-version: 11 59 | 60 | - uses: gradle/actions/setup-gradle@v4 61 | 62 | - id: cache-kotlin-konan 63 | uses: actions/cache@v4 64 | with: 65 | path: ~/.konan 66 | key: ${{ runner.os }}-kotlin-konan 67 | 68 | - id: stable-version 69 | if: steps.version.outputs.is_tagged == 'true' 70 | run: > 71 | echo "version=${{ steps.version.outputs.major }}.${{steps.version.outputs.minor }}.${{steps.version.outputs.patch }}" >> $GITHUB_OUTPUT 72 | 73 | - name: Run ${{ inputs.gradle-task }} 74 | env: 75 | ORG_GRADLE_PROJECT_ks3_enableKotlinJvm: true 76 | ORG_GRADLE_PROJECT_ks3_enableKotlinJs: true 77 | ORG_GRADLE_PROJECT_ks3_enableKotlinNative: true 78 | 79 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} 80 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 81 | ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRH_USERNAME }} 82 | ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRH_PASSWORD }} 83 | run: >- 84 | ./gradlew ${{ inputs.gradle-task }} -Pversion="${{ steps.stable-version.outputs.version || steps.version.outputs.version }}" 85 | 86 | 87 | - name: Upload build reports 88 | if: failure() 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: build-report-${{ runner.os }}-${{ github.action }} 92 | path: "**/build/reports/" 93 | if-no-files-found: ignore 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: "The git branch, tag or SHA to checkout" 8 | required: false 9 | default: '' 10 | type: string 11 | 12 | workflow_run: 13 | workflows: [ "build" ] 14 | types: 15 | - completed 16 | 17 | 18 | concurrency: 19 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 20 | cancel-in-progress: true 21 | 22 | 23 | permissions: 24 | contents: read 25 | 26 | 27 | jobs: 28 | 29 | publish: 30 | # only publish when 31 | # * on the main repo, 32 | # * if triggered by a workflow, the workflow was successful 33 | if: >- 34 | github.repository == 'Kantis/ks3' 35 | && 36 | (!github.event.workflow_run || github.event.workflow_run.conclusion == 'success') 37 | strategy: 38 | max-parallel: 1 # Sonatype doesn't like parallel uploads, so disable it where possible 39 | fail-fast: false 40 | matrix: 41 | include: 42 | # Publish 'common' components (KotlinMultiplatform,jvm,js) only on Linux, to avoid duplicate publications 43 | - os: ubuntu-latest 44 | args: -P"ks3_enabledPublicationNamePrefixes=KotlinMultiplatform,jvm,js,linux" 45 | 46 | # Windows: MinGW 47 | - os: windows-latest 48 | args: -P"ks3_enabledPublicationNamePrefixes=mingw" 49 | 50 | # Apple: macOS, iOS, tvOS, watchOS 51 | - os: macos-latest 52 | args: -P"ks3_enabledPublicationNamePrefixes=macOS,iOS,tvOS,watchOS" 53 | 54 | uses: ./.github/workflows/gradle_task.yml 55 | secrets: inherit 56 | with: 57 | ref: ${{ inputs.ref }} 58 | gradle-task: publishAllPublicationsToSonatypeReleaseRepository ${{ matrix.args }} --no-parallel --stacktrace --no-configuration-cache 59 | runs-on: ${{ matrix.os }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .idea 4 | *.iml 5 | build 6 | out -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | www.ks3.io -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@junit.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Adding features 2 | Please raise an issue to discuss and assess support for new features before starting work on them. 3 | 4 | ## Code style 5 | 6 | This project uses Ktlint to ensure that code is consistently formatted and doesn't change based on automated formatting settings of 7 | individual contributors. 8 | 9 | Please follow [the official recommended Ktlint setup](https://pinterest.github.io/ktlint/latest/install/setup/]) to setup your environment. 10 | 11 | ## API Changes 12 | 13 | This project uses Kotlin Binary Compatibity Validator to ensure that changes to the public API are done in a backwards compatible way. 14 | 15 | Breaking changes are reserved for major version updates, and must be preceded by deprecations. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [2016] [sksamuel] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KotlinX Serialization Standard Serializers (KS3) 2 | ![Stability Status - Stable](https://kotl.in/badges/stable.svg) 3 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/kantis/ks3/ci.yaml?branch=main) 4 | 5 | This project solves two things: 6 | * Provides [out-of-the-box serializers](https://github.com/Kantis/ks3/blob/main/doc/jdk.md) for a lof of commonly used Java types. 7 | * Provides [utilities](https://github.com/Kantis/ks3/blob/main/doc/builders.md) for more conveniently defining your own serializers by composition. 8 | 9 | This is accomplished without any additional footprint (the project only depends on KotlinX serialization itself). 10 | 11 | ## Contributing 12 | See the contribution guide [here](CONTRIBUTING.md). 13 | 14 | ## Compatibility 15 | 16 | | Ks3 | Kotlin | KotlinX serialization | 17 | |-----|--------|-----------------------| 18 | | 1.0 | 1.9+ | 1.6+ | 19 | 20 | ## Getting started 21 | 22 | Add the dependency. With Gradle: 23 | 24 | ```kotlin 25 | dependencies { 26 | implementation("io.ks3:ks3-jdk:1.0.0") 27 | } 28 | ``` 29 | 30 | Maven: 31 | 32 | ```xml 33 | 34 | io.ks3 35 | ks3-jdk-jvm 36 | 1.0.0 37 | 38 | ``` 39 | 40 | > Note that when using Maven dependencies must specify the multiplatform variant. For example, append `-jvm` to specify the JVM variant of `ks3-jdk`. 41 | 42 | Now you can start using the provided serializers. There's several possible ways to do this. 43 | 44 | * Using [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#contextual-serialization) 45 | * [Passing a serializer manually](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#passing-a-serializer-manually) 46 | * [Specifying serializer on a property](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#specifying-serializer-on-a-property) 47 | * [Specifying serializers for a file](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#specifying-serializers-for-a-file) 48 | * [Specifying serializer using a typealias](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#specifying-serializer-globally-using-typealias) 49 | 50 | 51 | ### Using `typealias` 52 | 53 | ```kotlin 54 | typealias LocalDateTimeAsString = @Serializable(LocalDateTimeAsStringSerializer::class) LocalDateTime 55 | 56 | data class Appointment( 57 | val datetime: LocalDateTimeAsString 58 | ) 59 | ``` 60 | 61 | > For details, see the [kotlinx.serialization guide](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#specifying-serializer-globally-using-typealias) 62 | 63 | This method is most useful when you want to use different serial formats for the same type, or when you can't configure the serializer itself. 64 | 65 | ### Using `@Contextual` 66 | 67 | > See the details in [kotlinx.serialization guide](https://github.com/Kotlin/kotlinx.serialization/blob/1.4.1-release/docs/serializers.md#contextual-serialization) 68 | 69 | ```kotlin 70 | @Serializable 71 | class ProgrammingLanguage( 72 | val name: String, 73 | @Contextual 74 | val stableReleaseDate: Date 75 | ) 76 | 77 | private val module = SerializersModule { 78 | contextual(DateAsLongSerializer) 79 | } 80 | 81 | val format = Json { serializersModule = module } 82 | ``` 83 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | kotlin("jvm") version embeddedKotlinVersion 6 | } 7 | 8 | dependencies { 9 | implementation(libs.gradlePlugin.kotlin) 10 | implementation(libs.gradlePlugin.kotlinSerialization) 11 | implementation(libs.gradlePlugin.testlogger) 12 | implementation(libs.gradlePlugin.kotest) 13 | implementation(libs.gradlePlugin.dokkatoo) 14 | implementation(libs.gradlePlugin.ktlint) 15 | implementation(libs.gradlePlugin.kotlinBinaryCompatibilityValidator) 16 | 17 | testImplementation(libs.kotest.runnerJunit5) 18 | } 19 | 20 | val buildLogicJvmTarget = "11" 21 | 22 | kotlin { 23 | jvmToolchain { 24 | languageVersion.set(JavaLanguageVersion.of(buildLogicJvmTarget)) 25 | } 26 | } 27 | 28 | tasks.withType().configureEach { 29 | kotlinOptions { 30 | jvmTarget = buildLogicJvmTarget 31 | } 32 | } 33 | 34 | tasks.withType().configureEach { 35 | useJUnitPlatform() 36 | } 37 | 38 | kotlinDslPluginOptions { 39 | jvmTarget.set(buildLogicJvmTarget) 40 | } 41 | -------------------------------------------------------------------------------- /build-logic/repositories.gradle.kts: -------------------------------------------------------------------------------- 1 | // Shared repository config, for use in both build-logic and the root project 2 | 3 | 4 | @Suppress("UnstableApiUsage") // Central declaration of repositories is an incubating feature 5 | dependencyResolutionManagement { 6 | 7 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 8 | 9 | repositories { 10 | mavenCentral() 11 | 12 | // Declare the Node.js & Yarn download repositories 13 | exclusiveContent { 14 | forRepository { 15 | ivy("https://nodejs.org/dist/") { 16 | name = "Node Distributions at $url" 17 | patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") } 18 | metadataSources { artifact() } 19 | content { includeModule("org.nodejs", "node") } 20 | } 21 | } 22 | filter { includeGroup("org.nodejs") } 23 | } 24 | 25 | exclusiveContent { 26 | forRepository { 27 | ivy("https://github.com/yarnpkg/yarn/releases/download") { 28 | name = "Yarn Distributions at $url" 29 | patternLayout { artifact("v[revision]/[artifact](-v[revision]).[ext]") } 30 | metadataSources { artifact() } 31 | content { includeModule("com.yarnpkg", "yarn") } 32 | } 33 | } 34 | filter { includeGroup("com.yarnpkg") } 35 | } 36 | 37 | // workaround for https://youtrack.jetbrains.com/issue/KT-51379 38 | exclusiveContent { 39 | forRepository { 40 | ivy("https://download.jetbrains.com/kotlin/native/builds") { 41 | name = "Kotlin Native" 42 | patternLayout { 43 | 44 | // example download URLs: 45 | // https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/linux-x86_64/kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz 46 | // https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/windows-x86_64/kotlin-native-prebuilt-windows-x86_64-1.7.20.zip 47 | // https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/macos-x86_64/kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz 48 | listOf( 49 | "macos-x86_64", 50 | "macos-aarch64", 51 | "osx-x86_64", 52 | "osx-aarch64", 53 | "linux-x86_64", 54 | "windows-x86_64", 55 | ).forEach { os -> 56 | listOf("dev", "releases").forEach { stage -> 57 | artifact("$stage/[revision]/$os/[artifact]-[revision].[ext]") 58 | } 59 | } 60 | } 61 | metadataSources { artifact() } 62 | } 63 | } 64 | filter { includeModuleByRegex(".*", ".*kotlin-native-prebuilt.*") } 65 | } 66 | 67 | sonatypeSnapshots() 68 | google() 69 | gradlePluginPortal() // tvOS builds need to be able to fetch a Kotlin Gradle plugin 70 | } 71 | 72 | pluginManagement { 73 | repositories { 74 | gradlePluginPortal() 75 | mavenCentral() 76 | google() 77 | sonatypeSnapshots() 78 | } 79 | } 80 | } 81 | 82 | fun RepositoryHandler.sonatypeSnapshots() { 83 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") { 84 | name = "SonatypeSnapshotS01" 85 | mavenContent { snapshotsOnly() } 86 | } 87 | maven("https://oss.sonatype.org/content/repositories/snapshots/") { 88 | name = "SonatypeSnapshotsOSS" 89 | mavenContent { snapshotsOnly() } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "build-logic" 2 | 3 | apply(from = "repositories.gradle.kts") 4 | 5 | dependencyResolutionManagement { 6 | @Suppress("UnstableApiUsage") 7 | versionCatalogs { 8 | create("libs") { 9 | from(files("../gradle/libs.versions.toml")) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/Ks3BuildLogicSettings.kt: -------------------------------------------------------------------------------- 1 | package ks3.conventions 2 | 3 | import org.gradle.api.provider.Provider 4 | import org.gradle.api.provider.ProviderFactory 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Common settings for configuring ks3's build logic. 9 | * 10 | * The settings need to be accessible during configuration, so they should come from Gradle 11 | * properties or environment variables. 12 | */ 13 | abstract class Ks3BuildLogicSettings @Inject constructor( 14 | private val providers: ProviderFactory, 15 | ) { 16 | 17 | val kotlinTarget: Provider = ks3Setting("kotlinTarget", "1.8") 18 | val jvmTarget: Provider = ks3Setting("jvmTarget", "11") 19 | 20 | /** Controls whether Kotlin Multiplatform JVM is enabled */ 21 | val enableKotlinJvm: Provider = ks3Flag("enableKotlinJvm", true) 22 | /** Controls whether Kotlin Multiplatform JS is enabled */ 23 | val enableKotlinJs: Provider = ks3Flag("enableKotlinJs", true) 24 | /** Controls whether Kotlin Multiplatform Native is enabled */ 25 | val enableKotlinNative: Provider = ks3Flag("enableKotlinNative", false) 26 | 27 | /** 28 | * Comma separated list of MavenPublication names that will have the publishing task enabled. 29 | * The provided names will be matched ignoring case, and by prefix, so `iOS` will match 30 | * `iosArm64`, `iosX64`, and `iosSimulatorArm64`. 31 | * 32 | * This is used to avoid duplicate publications, which can occur when a Kotlin Multiplatform 33 | * project is published in CI/CD on different host machines (Linux, Windows, and macOS). 34 | * 35 | * For example, by including `jvm` in the values when publishing on Linux, but omitting `jvm` on 36 | * Windows and macOS, this results in any Kotlin/JVM publications only being published once. 37 | */ 38 | val enabledPublicationNamePrefixes: Provider> = 39 | ks3Setting("enabledPublicationNamePrefixes", "KotlinMultiplatform,Jvm,Js,iOS,macOS,watchOS,tvOS,mingw") 40 | .map { enabledPlatforms -> 41 | enabledPlatforms 42 | .split(",") 43 | .map { it.trim() } 44 | .filter { it.isNotBlank() } 45 | .toSet() 46 | } 47 | 48 | private fun ks3Setting(name: String, default: String? = null) = 49 | providers.gradleProperty("ks3_$name") 50 | .orElse(providers.provider { default }) // workaround for https://github.com/gradle/gradle/issues/12388 51 | 52 | private fun ks3Flag(name: String, default: Boolean) = 53 | providers.gradleProperty("ks3_$name").map { it.toBoolean() }.orElse(default) 54 | 55 | companion object { 56 | const val EXTENSION_NAME = "ks3Settings" 57 | 58 | /** 59 | * Regex for matching the release version. 60 | * 61 | * If a version does not match this code it should be treated as a SNAPSHOT version. 62 | */ 63 | val releaseVersionRegex = Regex("\\d+.\\d+.\\d+") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/api-validation.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions 2 | 3 | import org.gradle.kotlin.dsl.configure 4 | 5 | plugins { 6 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 7 | } 8 | 9 | apiValidation { 10 | @OptIn(kotlinx.validation.ExperimentalBCVApi::class) 11 | klib { 12 | enabled = System.getProperty("enableKlibValidation") != null 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/base.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions 2 | 3 | plugins { 4 | base 5 | id("com.adarshr.test-logger") 6 | id("dev.adamko.dokkatoo-html") 7 | } 8 | 9 | // common config for all subprojects 10 | 11 | if (project != rootProject) { 12 | project.version = rootProject.version 13 | project.group = rootProject.group 14 | } 15 | 16 | extensions.create(Ks3BuildLogicSettings.EXTENSION_NAME, Ks3BuildLogicSettings::class) 17 | 18 | //testlogger { 19 | // showPassed = false 20 | //} 21 | 22 | tasks.withType().configureEach { 23 | // https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives 24 | isPreserveFileTimestamps = false 25 | isReproducibleFileOrder = true 26 | } 27 | 28 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/accessors.kt: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.kotlin.dsl.configure 5 | import org.gradle.kotlin.dsl.getByType 6 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 7 | 8 | 9 | // hacks because IntelliJ is refusing to index generated accessors 10 | 11 | internal val Project.kotlin: KotlinMultiplatformExtension get() = extensions.getByType() 12 | internal fun Project.kotlin(configure: KotlinMultiplatformExtension.() -> Unit) = extensions.configure(configure) 13 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/kotlin-multiplatform-base.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | import org.gradle.configurationcache.extensions.capitalized 5 | import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType 6 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget 7 | import org.jetbrains.kotlin.gradle.testing.KotlinTaskTestRun 8 | 9 | plugins { 10 | id("ks3.conventions.base") 11 | kotlin("multiplatform") 12 | kotlin("plugin.serialization") 13 | id("io.kotest.multiplatform") 14 | id("org.jlleitschuh.gradle.ktlint") 15 | } 16 | 17 | // Base configuration for all Kotlin/Multiplatform conventions. 18 | // This plugin does not enable any Kotlin target. To enable a target in a subproject, prefer 19 | // applying specific Kotlin target convention plugins. 20 | 21 | val ks3Settings = extensions.getByType() 22 | 23 | kotlin { 24 | jvmToolchain { 25 | languageVersion.set(JavaLanguageVersion.of(ks3Settings.jvmTarget.get())) 26 | } 27 | 28 | sourceSets { 29 | all { 30 | languageSettings.optIn("io.ks3.core.Ks3Internal") 31 | languageSettings.optIn("kotlin.RequiresOptIn") 32 | } 33 | } 34 | 35 | targets.configureEach { 36 | compilations.configureEach { 37 | kotlinOptions { 38 | apiVersion = ks3Settings.kotlinTarget.get() 39 | languageVersion = ks3Settings.kotlinTarget.get() 40 | } 41 | } 42 | } 43 | 44 | // configure all Kotlin/JVM Tests to use JUnit 45 | targets.withType().configureEach { 46 | testRuns.configureEach { 47 | executionTask.configure { 48 | useJUnitPlatform() 49 | } 50 | } 51 | } 52 | 53 | sourceSets.configureEach { 54 | languageSettings { 55 | languageVersion = ks3Settings.kotlinTarget.get() 56 | apiVersion = ks3Settings.kotlinTarget.get() 57 | optIn("kotlin.RequiresOptIn") 58 | } 59 | } 60 | } 61 | 62 | // create lifecycle task for each Kotlin Platform, that will run all tests 63 | KotlinPlatformType.values().forEach { kotlinPlatform -> 64 | val kotlinPlatformName = kotlinPlatform.name.capitalized() 65 | 66 | val testKotlinTargetLifecycleTask = 67 | tasks.create("allKotlin${kotlinPlatformName}Tests") { 68 | group = LifecycleBasePlugin.VERIFICATION_GROUP 69 | description = "Run all Kotlin/$kotlinPlatformName tests" 70 | } 71 | 72 | kotlin.testableTargets.matching { 73 | it.platformType == kotlinPlatform 74 | }.configureEach { 75 | testRuns.configureEach { 76 | if (this is KotlinTaskTestRun<*, *>) { 77 | testKotlinTargetLifecycleTask.dependsOn(executionTask) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/kotlin-multiplatform-js.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | 5 | plugins { 6 | id("ks3.conventions.lang.kotlin-multiplatform-base") 7 | } 8 | 9 | val ks3Settings = extensions.getByType() 10 | 11 | if (ks3Settings.enableKotlinJs.get()) { 12 | kotlin { 13 | js(IR) { 14 | browser() 15 | nodejs() 16 | } 17 | } 18 | 19 | relocateKotlinJsStore() 20 | } 21 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/kotlin-multiplatform-jvm.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | 5 | plugins { 6 | id("ks3.conventions.lang.kotlin-multiplatform-base") 7 | } 8 | 9 | val ks3Settings = extensions.getByType() 10 | 11 | if (ks3Settings.enableKotlinJvm.get()) { 12 | kotlin { 13 | jvm { 14 | withJava() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/kotlin-multiplatform-native.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | 5 | // conventions for a Kotlin/Native subproject 6 | 7 | plugins { 8 | id("ks3.conventions.lang.kotlin-multiplatform-base") 9 | } 10 | 11 | val ks3Settings = extensions.getByType() 12 | 13 | if (ks3Settings.enableKotlinNative.get()) { 14 | kotlin { 15 | 16 | // Native targets all extend commonMain and commonTest. 17 | // 18 | // Some targets (ios, tvos, watchos) are shortcuts provided by the Kotlin DSL, that 19 | // provide additional targets, except for 'simulators' which must be defined manually. 20 | // https://kotlinlang.org/docs/multiplatform-share-on-platforms.html#use-target-shortcuts 21 | // 22 | // common/ 23 | // └── native/ 24 | // ├── linuxX64 25 | // ├── mingwX64 26 | // ├── macosX64 27 | // ├── macosArm64 28 | // ├── ios/ (shortcut) 29 | // │ ├── iosArm64 30 | // │ ├── iosX64 31 | // │ └── iosSimulatorArm64 32 | // ├── tvos/ (shortcut) 33 | // │ ├── tvosArm64 34 | // │ ├── tvosX64 35 | // │ └── tvosSimulatorArm64Main 36 | // └── watchos/ (shortcut) 37 | // ├── watchosArm32 38 | // ├── watchosArm64 39 | // ├── watchosX64 40 | // └── watchosSimulatorArm64Main 41 | 42 | linuxX64() 43 | 44 | mingwX64() 45 | 46 | macosX64() 47 | macosArm64() 48 | 49 | iosX64() 50 | iosArm64() 51 | iosSimulatorArm64() 52 | 53 | watchosX64() 54 | watchosArm32() 55 | watchosArm64() 56 | watchosSimulatorArm64() 57 | 58 | tvosX64() 59 | tvosArm64() 60 | tvosSimulatorArm64() 61 | 62 | // Targets inherit in a natural way since Kotlin 1.9 63 | // https://kotlinlang.org/docs/multiplatform-hierarchy.html#default-hierarchy-template 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/lang/kotlinJsExtensions.kt: -------------------------------------------------------------------------------- 1 | package ks3.conventions.lang 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.kotlin.dsl.configure 5 | import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension 6 | 7 | /** 8 | * `kotlin-js` and `kotlin-multiplatform` plugins adds a directory in the root-dir for the Yarn 9 | * lockfile. That's a bit annoying. It's a little neater if it's in the Gradle dir, next to the 10 | * version catalog. 11 | */ 12 | internal fun Project.relocateKotlinJsStore() { 13 | afterEvaluate { 14 | rootProject.extensions.configure { 15 | lockFileDirectory = project.rootDir.resolve("gradle/kotlin-js-store") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/publishing/accessors.kt: -------------------------------------------------------------------------------- 1 | package ks3.conventions.publishing 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.publish.PublishingExtension 5 | import org.gradle.kotlin.dsl.configure 6 | import org.gradle.kotlin.dsl.getByType 7 | import org.gradle.plugins.signing.SigningExtension 8 | 9 | // Hacks, because IntelliJ is refusing to load the generated Gradle Kotlin DSL accessors. 10 | // These aren't necessary for building, only so auto-complete works. 11 | 12 | internal val Project.signing get() = extensions.getByType() 13 | internal fun Project.signing(action: SigningExtension.() -> Unit) = extensions.configure(action) 14 | internal val Project.publishing get() = extensions.getByType() 15 | internal fun Project.publishing(action: PublishingExtension.() -> Unit) = extensions.configure(action) 16 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/ks3/conventions/publishing/maven-publish.gradle.kts: -------------------------------------------------------------------------------- 1 | package ks3.conventions.publishing 2 | 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper 5 | 6 | plugins { 7 | signing 8 | `maven-publish` 9 | } 10 | 11 | 12 | val ks3Settings = extensions.getByType() 13 | 14 | 15 | val javadocJarStub by tasks.registering(Jar::class) { 16 | group = JavaBasePlugin.DOCUMENTATION_GROUP 17 | description = "Empty Javadoc Jar (required by Maven Central)" 18 | archiveClassifier.set("javadoc") 19 | } 20 | 21 | // can be set with environment variables: ORG_GRADLE_PROJECT_ossrhUsername and ORG_GRADLE_PROJECT_ossrhPassword 22 | val ossrhUsername: Provider = providers.gradleProperty("ossrhUsername") 23 | val ossrhPassword: Provider = providers.gradleProperty("ossrhPassword") 24 | 25 | val isReleaseVersion = provider { version.toString().matches(Ks3BuildLogicSettings.releaseVersionRegex) } 26 | 27 | val sonatypeReleaseUrl: Provider = isReleaseVersion.map { isRelease -> 28 | if (isRelease) { 29 | "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 30 | } else { 31 | "https://s01.oss.sonatype.org/content/repositories/snapshots/" 32 | } 33 | } 34 | 35 | publishing { 36 | repositories { 37 | if (ossrhUsername.isPresent && ossrhPassword.isPresent) { 38 | maven(sonatypeReleaseUrl) { 39 | name = "SonatypeRelease" 40 | credentials { 41 | username = ossrhUsername.get() 42 | password = ossrhPassword.get() 43 | } 44 | } 45 | } 46 | 47 | // Publish to a project-local Maven directory, for verification. To test, run: 48 | // ./gradlew publishAllPublicationsToMavenProjectLocalRepository 49 | // and check $rootDir/build/maven-project-local 50 | maven(rootProject.layout.buildDirectory.dir("maven-project-local")) { 51 | name = "MavenProjectLocal" 52 | } 53 | } 54 | 55 | publications.withType().forEach { 56 | it.apply { 57 | 58 | artifact(javadocJarStub) 59 | 60 | pom { 61 | name.set("Ks3") 62 | description.set("KotlinX Serialization standard serializers") 63 | url.set("https://github.com/Kantis/ks3") 64 | 65 | scm { 66 | connection.set("scm:git:https://github.com/Kantis/ks3/") 67 | developerConnection.set("scm:git:https://github.com/Kantis/") 68 | url.set("https://github.com/Kantis/ks3") 69 | } 70 | 71 | licenses { 72 | license { 73 | name.set("Apache-2.0") 74 | url.set("https://opensource.org/licenses/Apache-2.0") 75 | } 76 | } 77 | 78 | developers { 79 | developer { 80 | id.set("Kantis") 81 | name.set("Emil Kantis") 82 | email.set("emil.kantis@protonmail.com") 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | signing { 91 | val signingKey: String? by project 92 | val signingPassword: String? by project 93 | 94 | logger.lifecycle("[maven-publish convention] signing is enabled for ${project.path}") 95 | useGpgCmd() 96 | useInMemoryPgpKeys(signingKey, signingPassword) 97 | sign(publishing.publications) 98 | } 99 | 100 | // https://youtrack.jetbrains.com/issue/KT-46466 101 | val signingTasks = tasks.withType() 102 | 103 | tasks.withType().configureEach { 104 | // Gradle warns about some signing tasks using publishing task outputs without explicit dependencies. 105 | // Here's a quick fix. 106 | dependsOn(signingTasks) 107 | mustRunAfter(signingTasks) 108 | 109 | // use a val for the GAV to avoid Gradle Configuration Cache issues 110 | val publicationGAV = publication?.run { "$group:$artifactId:$version" } 111 | 112 | doLast { 113 | if (publicationGAV != null) { 114 | logger.lifecycle("[task: ${path}] $publicationGAV") 115 | } 116 | } 117 | } 118 | 119 | tasks.withType().configureEach { 120 | // use vals - improves Gradle Config Cache compatibility 121 | val publicationName = publication.name 122 | val enabledPublicationNamePrefixes = ks3Settings.enabledPublicationNamePrefixes 123 | 124 | val isPublicationEnabled = enabledPublicationNamePrefixes.map { prefixes -> 125 | prefixes.any { prefix -> publicationName.startsWith(prefix, ignoreCase = true) } 126 | } 127 | 128 | // register an input so Gradle can do up-to-date checks 129 | inputs.property("publicationEnabled", isPublicationEnabled) 130 | 131 | onlyIf { 132 | val enabled = isPublicationEnabled.get() 133 | if (!enabled) { 134 | logger.lifecycle("[task: $path] publishing for $publicationName is disabled") 135 | } 136 | enabled 137 | } 138 | } 139 | 140 | // Kotlin Multiplatform specific publishing configuration 141 | plugins.withType().configureEach { 142 | // nothing yet! 143 | } 144 | -------------------------------------------------------------------------------- /build-logic/src/test/kotlin/IsReleaseVersionTest.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.spec.style.FreeSpec 2 | import io.kotest.matchers.shouldBe 3 | import ks3.conventions.Ks3BuildLogicSettings 4 | 5 | class IsReleaseVersionTest : FreeSpec ( 6 | { 7 | "isReleaseVersion" - { 8 | fun isReleaseVersion(version: String): Boolean = version.matches(Ks3BuildLogicSettings.releaseVersionRegex) 9 | 10 | "should return true for a release version" { 11 | isReleaseVersion("1.2.3") shouldBe true 12 | } 13 | "should return false for a snapshot version" { 14 | isReleaseVersion("1.2.3-SNAPSHOT") shouldBe false 15 | } 16 | "should return false for a milestone version" { 17 | isReleaseVersion("1.2.3-M1") shouldBe false 18 | } 19 | "should return false for a release candidate version" { 20 | isReleaseVersion("1.2.3-RC1") shouldBe false 21 | } 22 | "should return false for a pre-release version" { 23 | isReleaseVersion("1.2.3-alpha1") shouldBe false 24 | } 25 | } 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.adamko.dokkatoo.dokka.plugins.DokkaHtmlPluginParameters 2 | 3 | plugins { 4 | id("ks3.conventions.base") 5 | id("ks3.conventions.api-validation") 6 | idea 7 | } 8 | 9 | group = "io.ks3" 10 | 11 | dependencies { 12 | dokkatoo(projects.ks3Jdk) 13 | dokkatoo(projects.ks3Standard) 14 | dokkatoo(projects.ks3Test) 15 | dokkatooPluginHtml(libs.dokka.templating) 16 | dokkatooPluginHtml(libs.dokka.allModulesPage) 17 | } 18 | 19 | dokkatoo { 20 | moduleName.set("KS3") 21 | 22 | dokkatooPublications.configureEach { 23 | includes.from("README.md") 24 | } 25 | 26 | pluginsConfiguration.named("html") { 27 | customStyleSheets.from( 28 | "./dokka/custom-styles.css", 29 | ) 30 | } 31 | } 32 | 33 | idea { 34 | module { 35 | isDownloadSources = true 36 | isDownloadJavadoc = false 37 | excludeDirs = excludeDirs + 38 | layout.files( 39 | ".idea", 40 | "gradle/kotlin-js-store", // location of the lock file, overridden by Kotlin/JS convention 41 | "gradle/wrapper", 42 | ) 43 | 44 | // exclude generated Gradle code, so it doesn't clog up search results 45 | excludeDirs.addAll( 46 | layout.files( 47 | "build-logic/build/generated-sources/kotlin-dsl-accessors/kotlin/gradle", 48 | "build-logic/build/generated-sources/kotlin-dsl-external-plugin-spec-builders/kotlin/gradle", 49 | "build-logic/build/generated-sources/kotlin-dsl-plugins/kotlin", 50 | "build-logic/build/pluginUnderTestMetadata", 51 | ), 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | Serializers for: 2 | * [Java types](jdk.md) 3 | * [Generic Kotlin stuff](standard.md) 4 | -------------------------------------------------------------------------------- /doc/builders.md: -------------------------------------------------------------------------------- 1 | # Builders 2 | 3 | Builders provide a convenient way of creating a serializer that turns your type into a primitive by simply providing two conversion methods. 4 | 5 | ### Example: 6 | 7 | ```kotlin 8 | object UriSerializer : KSerializer by stringSerializer( 9 | ::URI, 10 | URI::toASCIIString, 11 | ) 12 | ``` 13 | 14 | Here we create a serializer for `URI`. Our object implements `KSerializer` by delegating to the result of `stringSerializer(..)`. 15 | 16 | The first parameter, `::URI`, is the _decoding_ method which takes our primitive (`String`) and returns a `URI`. 17 | 18 | The second parameter is the _encoding_ method, which takes our type and returns the primitive. 19 | 20 | Since the methods to go back and forth from String to URI already exists in Java, this serializer is easily created using `stringSerializer`. 21 | 22 | 23 | ### Available builders are: 24 | 25 | * `intSerializer` 26 | * `longSerializer` 27 | * `stringSerializer` 28 | -------------------------------------------------------------------------------- /doc/jdk.md: -------------------------------------------------------------------------------- 1 | ## `java.time` 2 | 3 | | Type | Serializer | Typealias | Example | 4 | |----------------|---------------------------------|------------------------|-------------------------------| 5 | | Instant | InstantAsStringSerializer | InstantAsString | `2023-04-27T21:24:37.476555Z` | 6 | | Instant | InstantAsLongSerializer | InstantAsLong | `1682630695852` | 7 | | LocalDate | LocalDateAsStringSerializer | LocalDateAsString | `"2022-10-23"` | 8 | | LocalDateTime | LocalDateTimeAsStringSerializer | LocalDateTimeAsString | `"2022-10-23T21:44:00"` | 9 | | LocalTime | LocalTimeAsStringSerializer | LocalTimeAsString | `"21:44:00"` | 10 | | OffsetDateTime | OffsetDateTimeSerializer | OffsetDateTimeAsString | `2022` | 11 | | YearMonth | YearMonthAsStringSerializer | YearMonthAsString | `"2022-10"` | 12 | | Year | YearAsStringSerializer | not defined | `"2022"` | 13 | | Year | YearAsIntSerializer | not defined | `2022` | 14 | 15 | ## `java.io` 16 | 17 | | Type | Serializer | Typealias | Example | 18 | |------|--------------------|--------------|-------------------------| 19 | | File | FilePathSerializer | FileAsString | `"/home/emil/file.txt"` | 20 | 21 | ## `java.math` 22 | 23 | | Type | Serializer | Typealias | Example | 24 | |------------|------------------------------|-------------------------|----------------------------| 25 | | BigDecimal | BigDecimalAsStringSerializer | BigDecimalAsString | `"3.12345678901234567890"` | 26 | | BigDecimal | BigDecimalAsDouble | BigDecimalAsDouble | `3.1234567890123457` | 27 | | BigDecimal | BigDecimalAsJsonLiteral | BigDecimalAsJsonLiteral | `3.12345678901234567890` | 28 | | BigInteger | BigIntegerAsString | BigIntegerAsString | `"9223372036854775808"` | 29 | | BigInteger | BigIntegerAsJsonLiteral | BigIntegerAsJsonLiteral | `9223372036854775808` | 30 | 31 | ## `java.net` 32 | 33 | | Type | Serializer | Example | 34 | |------|---------------|-----------------------------| 35 | | URL | UrlSerializer | `"https://www.github.com"` | 36 | | URI | UriSerializer | `"telnet://192.0.2.16:80/"` | 37 | 38 | ## `java.util` 39 | 40 | | Type | Serializer | Example | 41 | |---------------|-------------------------|------------------------------------------| 42 | | UUID | UuidSerializer | `"8d44ac12-394b-4a50-ad6a-eff2cff129c4"` | 43 | | AtomicInteger | AtomicIntegerSerializer | `5` | 44 | | AtomicBoolean | AtomicBooleanSerializer | `true` | 45 | | AtomicLong | AtomicLongSerializer | `22` | 46 | | Locale | LocaleSerializer | `"en_US"` | 47 | | Currency | CurrencySerializer | `"AUD"` | 48 | -------------------------------------------------------------------------------- /doc/standard.md: -------------------------------------------------------------------------------- 1 | | Type | Serializer | Example | 2 | |---------|-------------------------|---------| 3 | | Enum<*> | EnumAsOrdinalSerializer | `0` | 4 | 5 | ### EnumAsOrdinalSerializer 6 | 7 | Encodes/decodes an enum to its ordinal int value. 8 | 9 | Example usage: 10 | 11 | ```kotlin 12 | enum class Shape { 13 | SQUARE, CIRCLE; 14 | } 15 | 16 | object ShapeSerializer : KSerializer by enumAsOrdinalSerializer() 17 | 18 | println(Json.encodeToString(ShapeSerializer, Shape.CIRCLE)) // "1" 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /dokka/custom-styles.css: -------------------------------------------------------------------------------- 1 | .theme-dark .quotation { 2 | background-color: #2b2b2b; 3 | color: #d4d4d4; 4 | padding: 0.5em 10px; 5 | margin: 0.5em 10px; 6 | } 7 | 8 | .theme-dark .quotation code { 9 | 10 | } 11 | 12 | :root { 13 | --default-gray: rgb(66, 66, 66); 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ### Gradle build settings ### 2 | 3 | org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=756m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 4 | systemProp.org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=756m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 5 | 6 | org.gradle.daemon=true 7 | org.gradle.parallel=true 8 | org.gradle.caching=true 9 | org.gradle.configureondemand=false 10 | 11 | # overriden by GitHub action 12 | version = 0.1.0-SNAPSHOT 13 | 14 | # cache accessors - defaults to 'true' in Gradle 8.0 https://github.com/gradle/gradle/issues/20416 15 | org.gradle.kotlin.dsl.precompiled.accessors.strict=true 16 | 17 | org.gradle.unsafe.configuration-cache=true 18 | org.gradle.unsafe.configuration-cache-problems=warn 19 | # signing & Kotlin Multiplatform plugins triggers the default max-problems limit of 512 - so increase it to the max 20 | org.gradle.unsafe.configuration-cache.max-problems=2147483647 21 | 22 | #https://github.com/gradle/gradle/issues/11308 23 | org.gradle.internal.publish.checksums.insecure=true 24 | systemProp.org.gradle.internal.publish.checksums.insecure=true 25 | 26 | 27 | ### Kotlin config ### 28 | 29 | kotlin.code.style=official 30 | 31 | kotlin.native.ignoreIncorrectDependencies=true 32 | kotlin.native.disableCompilerDaemon=true 33 | kotlin.native.ignoreDisabledTargets=true 34 | kotlin.mpp.stability.nowarn=true 35 | 36 | kotlin.incremental=true 37 | kotlin.incremental.js=true 38 | systemProp.kotlin.native.disableCompilerDaemon=true 39 | 40 | 41 | ### Android config ### 42 | 43 | android.useAndroidX=true 44 | android.enableJetifier=true 45 | 46 | 47 | ### Ks3BuildLogicSettings ### 48 | 49 | # comma separated list of MavenPublication name prefixes, each of which will have its publishing task enabled 50 | ks3_enabledPublicationNamePrefixes=KotlinMultiplatform,Jvm,Js,iOS,macOS,watchOS,tvOS,mingw 51 | 52 | # enabled/disable Kotlin targets, for improving local dev & CI/CD performance 53 | ks3_enableKotlinJvm=true 54 | ks3_enableKotlinJs=true 55 | ks3_enableKotlinNative=false 56 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | dokka = "2.0.0" 3 | dokkatoo = "2.4.0" 4 | 5 | kotlin = "1.9.25" 6 | kotlinx-serialization = "1.6.3" 7 | 8 | kotest = "5.9.1" 9 | kotest-propertyArbsExtension = "2.1.2" 10 | 11 | okio = "3.9.1" 12 | 13 | gradlePlugin-ktlint = "12.1.2" 14 | gradlePlugin-testlogger = "4.0.0" 15 | 16 | kotlinBinaryCompatibilityValidator = "0.17.0" 17 | 18 | [libraries] 19 | 20 | ## KotlinX Serialization ## 21 | kotlinxSerialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } 22 | kotlinxSerialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" } 23 | kotlinxSerialization-hocon = { module = "org.jetbrains.kotlinx:kotlinx-serialization-hocon", version.ref = "kotlinx-serialization" } 24 | kotlinxSerialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 25 | kotlinxSerialization-properties = { module = "org.jetbrains.kotlinx:kotlinx-serialization-properties", version.ref = "kotlinx-serialization" } 26 | kotlinxSerialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" } 27 | kotlinxSerialization-jsonOkio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" } 28 | 29 | 30 | ## Kotest ## 31 | kotest-assertionsCore = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } 32 | kotest-assertionsJson = { module = "io.kotest:kotest-assertions-json", version.ref = "kotest" } 33 | kotest-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" } 34 | kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } 35 | kotest-frameworkApi = { module = "io.kotest:kotest-framework-api", version.ref = "kotest" } 36 | kotest-frameworkEngine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } # Not included in the BOM 37 | kotest-frameworkDatatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" } 38 | kotest-runnerJunit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } 39 | 40 | kotest-propertyArbsExtension = { module = "io.kotest.extensions:kotest-property-arbs", version.ref = "kotest-propertyArbsExtension" } 41 | 42 | kotestPlugin-embeddedCompiler = { module = "io.kotest:kotest-framework-multiplatform-plugin-embeddable-compiler" } 43 | kotestPlugin-multiplatform = { module = "io.kotest:kotest-framework-multiplatform-plugin-gradle" } 44 | 45 | ## Dokka plugins ## 46 | dokka-allModulesPage = { module = "org.jetbrains.dokka:all-modules-page-plugin", version.ref = "dokka" } 47 | dokka-templating = { module = "org.jetbrains.dokka:templating-plugin", version.ref = "dokka" } 48 | 49 | 50 | ## Okio ## 51 | okio-core = { module = "com.squareup.okio:okio", version.ref = "okio" } 52 | 53 | 54 | ### Gradle plugins ### 55 | # Maven coordinates of Gradle plugins. Used in ./build-logic/build.gradle.kts. 56 | gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 57 | gradlePlugin-kotlinAllOpen = { module = "org.jetbrains.kotlin:kotlin-allopen", version.ref = "kotlin" } 58 | gradlePlugin-kotlinNoArg = { module = "org.jetbrains.kotlin:kotlin-noarg", version.ref = "kotlin" } 59 | gradlePlugin-kotlinSerialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } 60 | 61 | gradlePlugin-testlogger = { module = "com.adarshr:gradle-test-logger-plugin", version.ref = "gradlePlugin-testlogger" } 62 | 63 | gradlePlugin-kotest = { module = "io.kotest:kotest-framework-multiplatform-plugin-gradle", version.ref = "kotest" } 64 | 65 | gradlePlugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 66 | gradlePlugin-dokkatoo = { module = "dev.adamko.dokkatoo:dokkatoo-plugin", version.ref = "dokkatoo" } 67 | 68 | gradlePlugin-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "gradlePlugin-ktlint" } 69 | gradlePlugin-kotlinBinaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "kotlinBinaryCompatibilityValidator" } 70 | [bundles] 71 | 72 | 73 | [plugins] 74 | 75 | # If plugin will be used by conventions, define plugins in ./build-logic/build.gradle.kts to avoid error: 76 | # The Kotlin Gradle plugin was loaded multiple times in different subprojects, which is not supported and may break the build. 77 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kantis/ks3/c80f89080aba6744f396c72394ac6a8b089829b4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /ks3-core/api/ks3-core.api: -------------------------------------------------------------------------------- 1 | public abstract interface annotation class io/ks3/core/ExperimentalKs3 : java/lang/annotation/Annotation { 2 | } 3 | 4 | public abstract interface annotation class io/ks3/core/Ks3Internal : java/lang/annotation/Annotation { 5 | } 6 | 7 | -------------------------------------------------------------------------------- /ks3-core/api/ks3-core.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | open annotation class io.ks3.core/ExperimentalKs3 : kotlin/Annotation { // io.ks3.core/ExperimentalKs3|null[0] 10 | constructor () // io.ks3.core/ExperimentalKs3.|(){}[0] 11 | } 12 | 13 | open annotation class io.ks3.core/Ks3Internal : kotlin/Annotation { // io.ks3.core/Ks3Internal|null[0] 14 | constructor () // io.ks3.core/Ks3Internal.|(){}[0] 15 | } 16 | -------------------------------------------------------------------------------- /ks3-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("ks3.conventions.lang.kotlin-multiplatform-js") 3 | id("ks3.conventions.lang.kotlin-multiplatform-jvm") 4 | id("ks3.conventions.lang.kotlin-multiplatform-native") 5 | id("ks3.conventions.publishing.maven-publish") 6 | } 7 | -------------------------------------------------------------------------------- /ks3-core/src/commonMain/kotlin/io/ks3/core/ExperimentalKs3.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.core 2 | 3 | @RequiresOptIn(level = RequiresOptIn.Level.WARNING, message = "This API is experimental and is prone to change in the future.") 4 | @MustBeDocumented 5 | @Retention(value = AnnotationRetention.BINARY) 6 | annotation class ExperimentalKs3 7 | -------------------------------------------------------------------------------- /ks3-core/src/commonMain/kotlin/io/ks3/core/Ks3Internal.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.core 2 | 3 | /** 4 | * An internal Ks3 feature that is public for operational reasons but should not be used by end users. 5 | */ 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS) 7 | @MustBeDocumented 8 | @Retention(value = AnnotationRetention.BINARY) 9 | @RequiresOptIn(level = RequiresOptIn.Level.WARNING) 10 | annotation class Ks3Internal 11 | -------------------------------------------------------------------------------- /ks3-jdk/Module.md: -------------------------------------------------------------------------------- 1 | # Module ks3-jdk 2 | 3 | Serializers for Java types. Available for JVM targets. 4 | 5 | # Package io.ks3.java.io 6 | 7 | Contains serializers for types in the `java.io`-package, such as `java.io.File`. 8 | 9 | ## Typealiases 10 | 11 | | Type | Typealias | Example | 12 | |------|--------------|---------------------------------------| 13 | | File | FileAsString | `"/Users/emil/Documents/theFile.txt"` | 14 | 15 | # Package io.ks3.java.net 16 | 17 | Contains serializers for networking-related types, such as `java.net.URL`. 18 | 19 | ## Typealiases 20 | 21 | | Type | Typealias | Example | 22 | |------|-------------|-----------------------------| 23 | | URL | UrlAsString | `"https://www.github.com"` | 24 | | URI | UriAsString | `"telnet://192.0.2.16:80/"` | 25 | 26 | # Package io.ks3.java.time 27 | 28 | Contains serializers for time-related types, such as `java.time.Instant`. 29 | 30 | ## Typealiases 31 | 32 | | Type | Typealias | Example | 33 | |----------------|------------------------|---------------------------------| 34 | | Instant | InstantAsString | `"2023-04-27T21:24:37.476555Z"` | 35 | | Instant | InstantAsLong | `1682630695852` | 36 | | LocalDate | LocalDateAsString | `"2022-10-23"` | 37 | | LocalDateTime | LocalDateTimeAsString | `"2022-10-23T21:44:00"` | 38 | | LocalTime | LocalTimeAsString | `"21:44:00"` | 39 | | OffsetDateTime | OffsetDateTimeAsString | `"2022-10-23T21:44:00+02:00"` | 40 | | YearMonth | YearMonthAsString | `"2022-10"` | 41 | 42 | # Package io.ks3.java.typealias 43 | 44 | Contains `typealias`es for easily defining types where the Java type uses a pre-determined `Serializer`. 45 | 46 | See each package for available `typealias`es. 47 | 48 | # Package io.ks3.java.util 49 | 50 | Contains serializers for types in `java.util`, such as `UUID` or `AtomicLong`. 51 | -------------------------------------------------------------------------------- /ks3-jdk/README.md: -------------------------------------------------------------------------------- 1 | Serializers for Java types, available for JVM targets. 2 | -------------------------------------------------------------------------------- /ks3-jdk/api/ks3-jdk.api: -------------------------------------------------------------------------------- 1 | public final class io/ks3/java/io/FilePathSerializer : kotlinx/serialization/KSerializer { 2 | public static final field INSTANCE Lio/ks3/java/io/FilePathSerializer; 3 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/io/File; 4 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 5 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 6 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/io/File;)V 7 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 8 | } 9 | 10 | public final class io/ks3/java/math/BigDecimalAsDoubleSerializer : kotlinx/serialization/KSerializer { 11 | public static final field INSTANCE Lio/ks3/java/math/BigDecimalAsDoubleSerializer; 12 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 13 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; 14 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 15 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 16 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigDecimal;)V 17 | } 18 | 19 | public final class io/ks3/java/math/BigDecimalAsJsonNumberSerializer : kotlinx/serialization/KSerializer { 20 | public static final field INSTANCE Lio/ks3/java/math/BigDecimalAsJsonNumberSerializer; 21 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 22 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; 23 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 24 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 25 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigDecimal;)V 26 | } 27 | 28 | public final class io/ks3/java/math/BigDecimalAsStringSerializer : kotlinx/serialization/KSerializer { 29 | public static final field INSTANCE Lio/ks3/java/math/BigDecimalAsStringSerializer; 30 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 31 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; 32 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 33 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 34 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigDecimal;)V 35 | } 36 | 37 | public final class io/ks3/java/math/BigIntegerAsJsonLiteralSerializer : kotlinx/serialization/KSerializer { 38 | public static final field INSTANCE Lio/ks3/java/math/BigIntegerAsJsonLiteralSerializer; 39 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 40 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigInteger; 41 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 42 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 43 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigInteger;)V 44 | } 45 | 46 | public final class io/ks3/java/math/BigIntegerAsStringSerializer : kotlinx/serialization/KSerializer { 47 | public static final field INSTANCE Lio/ks3/java/math/BigIntegerAsStringSerializer; 48 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 49 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigInteger; 50 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 51 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 52 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigInteger;)V 53 | } 54 | 55 | public final class io/ks3/java/net/UriSerializer : kotlinx/serialization/KSerializer { 56 | public static final field INSTANCE Lio/ks3/java/net/UriSerializer; 57 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 58 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/net/URI; 59 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 60 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 61 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/net/URI;)V 62 | } 63 | 64 | public final class io/ks3/java/net/UrlSerializer : kotlinx/serialization/KSerializer { 65 | public static final field INSTANCE Lio/ks3/java/net/UrlSerializer; 66 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 67 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/net/URL; 68 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 69 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 70 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/net/URL;)V 71 | } 72 | 73 | public final class io/ks3/java/time/InstantAsLongSerializer : kotlinx/serialization/KSerializer { 74 | public static final field INSTANCE Lio/ks3/java/time/InstantAsLongSerializer; 75 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 76 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; 77 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 78 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 79 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Instant;)V 80 | } 81 | 82 | public final class io/ks3/java/time/InstantAsStringSerializer : kotlinx/serialization/KSerializer { 83 | public static final field INSTANCE Lio/ks3/java/time/InstantAsStringSerializer; 84 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 85 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; 86 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 87 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 88 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Instant;)V 89 | } 90 | 91 | public final class io/ks3/java/time/LocalDateAsStringSerializer : kotlinx/serialization/KSerializer { 92 | public static final field INSTANCE Lio/ks3/java/time/LocalDateAsStringSerializer; 93 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 94 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDate; 95 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 96 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 97 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalDate;)V 98 | } 99 | 100 | public final class io/ks3/java/time/LocalDateTimeAsStringSerializer : kotlinx/serialization/KSerializer { 101 | public static final field INSTANCE Lio/ks3/java/time/LocalDateTimeAsStringSerializer; 102 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 103 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDateTime; 104 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 105 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 106 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalDateTime;)V 107 | } 108 | 109 | public final class io/ks3/java/time/LocalTimeAsStringSerializer : kotlinx/serialization/KSerializer { 110 | public static final field INSTANCE Lio/ks3/java/time/LocalTimeAsStringSerializer; 111 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 112 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalTime; 113 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 114 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 115 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalTime;)V 116 | } 117 | 118 | public final class io/ks3/java/time/OffsetDateTimeAsStringSerializer : kotlinx/serialization/KSerializer { 119 | public static final field INSTANCE Lio/ks3/java/time/OffsetDateTimeAsStringSerializer; 120 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 121 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/OffsetDateTime; 122 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 123 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 124 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/OffsetDateTime;)V 125 | } 126 | 127 | public final class io/ks3/java/time/YearAsIntSerializer : kotlinx/serialization/KSerializer { 128 | public static final field INSTANCE Lio/ks3/java/time/YearAsIntSerializer; 129 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 130 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Year; 131 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 132 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 133 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Year;)V 134 | } 135 | 136 | public final class io/ks3/java/time/YearAsStringSerializer : kotlinx/serialization/KSerializer { 137 | public static final field INSTANCE Lio/ks3/java/time/YearAsStringSerializer; 138 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 139 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Year; 140 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 141 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 142 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Year;)V 143 | } 144 | 145 | public final class io/ks3/java/time/YearMonthAsStringSerializer : kotlinx/serialization/KSerializer { 146 | public static final field INSTANCE Lio/ks3/java/time/YearMonthAsStringSerializer; 147 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 148 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/YearMonth; 149 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 150 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 151 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/YearMonth;)V 152 | } 153 | 154 | public final class io/ks3/java/util/AtomicBooleanSerializer : kotlinx/serialization/KSerializer { 155 | public static final field INSTANCE Lio/ks3/java/util/AtomicBooleanSerializer; 156 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 157 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/concurrent/atomic/AtomicBoolean; 158 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 159 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 160 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/concurrent/atomic/AtomicBoolean;)V 161 | } 162 | 163 | public final class io/ks3/java/util/AtomicIntegerSerializer : kotlinx/serialization/KSerializer { 164 | public static final field INSTANCE Lio/ks3/java/util/AtomicIntegerSerializer; 165 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 166 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/concurrent/atomic/AtomicInteger; 167 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 168 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 169 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/concurrent/atomic/AtomicInteger;)V 170 | } 171 | 172 | public final class io/ks3/java/util/AtomicLongSerializer : kotlinx/serialization/KSerializer { 173 | public static final field INSTANCE Lio/ks3/java/util/AtomicLongSerializer; 174 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 175 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/concurrent/atomic/AtomicLong; 176 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 177 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 178 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/concurrent/atomic/AtomicLong;)V 179 | } 180 | 181 | public final class io/ks3/java/util/CurrencySerializer : kotlinx/serialization/KSerializer { 182 | public static final field INSTANCE Lio/ks3/java/util/CurrencySerializer; 183 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 184 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/Currency; 185 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 186 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 187 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/Currency;)V 188 | } 189 | 190 | public final class io/ks3/java/util/LocaleSerializer : kotlinx/serialization/KSerializer { 191 | public static final field INSTANCE Lio/ks3/java/util/LocaleSerializer; 192 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 193 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/Locale; 194 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 195 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 196 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/Locale;)V 197 | } 198 | 199 | public final class io/ks3/java/util/UuidSerializer : kotlinx/serialization/KSerializer { 200 | public static final field INSTANCE Lio/ks3/java/util/UuidSerializer; 201 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 202 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/UUID; 203 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 204 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 205 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/UUID;)V 206 | } 207 | 208 | -------------------------------------------------------------------------------- /ks3-jdk/api/ks3-jdk.klib.api: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kantis/ks3/c80f89080aba6744f396c72394ac6a8b089829b4/ks3-jdk/api/ks3-jdk.klib.api -------------------------------------------------------------------------------- /ks3-jdk/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("ks3.conventions.lang.kotlin-multiplatform-jvm") // Kotlin/JVM only 3 | id("ks3.conventions.publishing.maven-publish") 4 | } 5 | 6 | dokkatoo { 7 | dokkatooSourceSets.configureEach { 8 | includes.from("Module.md") 9 | } 10 | 11 | dokkatooSourceSets.named("jvmMain") { 12 | sourceLink { 13 | localDirectory.set(file("src/jvmMain/kotlin")) 14 | remoteUrl("https://github.com/Kantis/ks3/blob/main/ks3-jdk/src/jvmMain/kotlin") 15 | remoteLineSuffix.set("#L") 16 | } 17 | } 18 | 19 | modulePath.set("ks3-jdk") // match the original dokka default 20 | } 21 | 22 | tasks.withType().configureEach { 23 | generator.dokkaSourceSets.configureEach { 24 | sourceSetScope.set(":ks3-jdk:dokkaHtmlPartial") 25 | } 26 | } 27 | 28 | kotlin { 29 | sourceSets { 30 | commonMain { 31 | dependencies { 32 | implementation(projects.ks3Core) 33 | implementation(projects.ks3Standard) 34 | implementation(libs.kotlinxSerialization.core) 35 | implementation(libs.kotlinxSerialization.json) 36 | } 37 | } 38 | 39 | commonTest { 40 | dependencies { 41 | implementation(projects.ks3Test) 42 | 43 | implementation(libs.kotest.frameworkEngine) 44 | implementation(libs.kotest.assertionsCore) 45 | implementation(libs.kotest.assertionsJson) 46 | implementation(libs.kotest.property) 47 | 48 | implementation(libs.kotlinxSerialization.json) 49 | } 50 | } 51 | 52 | if (ks3Settings.enableKotlinJvm.get()) { 53 | jvmTest { 54 | dependencies { 55 | implementation(kotlin("reflect")) 56 | implementation(libs.kotest.runnerJunit5) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/io/FilePathSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.io 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.io.File 6 | 7 | object FilePathSerializer : KSerializer by stringSerializer(::File, File::invariantSeparatorsPath) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/math/BigDecimalSerializers.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.math 2 | 3 | import io.ks3.standard.doubleSerializer 4 | import io.ks3.standard.stringSerializer 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.descriptors.PrimitiveKind 9 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 10 | import kotlinx.serialization.encoding.Decoder 11 | import kotlinx.serialization.encoding.Encoder 12 | import kotlinx.serialization.json.JsonDecoder 13 | import kotlinx.serialization.json.JsonEncoder 14 | import kotlinx.serialization.json.JsonUnquotedLiteral 15 | import kotlinx.serialization.json.jsonPrimitive 16 | import java.math.BigDecimal 17 | 18 | /** 19 | * Encodes a [BigDecimal] as a string, preserving the full precision of the number. Wraps the number in quotes. 20 | */ 21 | typealias BigDecimalAsString = 22 | @Serializable(with = BigDecimalAsStringSerializer::class) 23 | BigDecimal 24 | 25 | /** 26 | * Encodes a [BigDecimal] to it's double representation. [Double] have limited precision, so the number might not be 27 | * exactly the same as the original [BigDecimal]. 28 | */ 29 | typealias BigDecimalAsDouble = 30 | @Serializable(with = BigDecimalAsDoubleSerializer::class) 31 | BigDecimal 32 | 33 | /** 34 | * Encodes a [BigDecimal] as an unquoted JSON literal, preserving the full precision of the number while being encoded as a number. 35 | * 36 | * Note that this typealias is primarily meant for JSON, other formats will have the [BigDecimal] encoded as a string. 37 | */ 38 | @ExperimentalSerializationApi 39 | typealias BigDecimalAsJsonNumber = 40 | @Serializable(with = BigDecimalAsJsonNumberSerializer::class) 41 | BigDecimal 42 | 43 | object BigDecimalAsStringSerializer : KSerializer by stringSerializer(::BigDecimal, BigDecimal::toPlainString) 44 | 45 | object BigDecimalAsDoubleSerializer : KSerializer by doubleSerializer({ it.toString().toBigDecimal() }, BigDecimal::toDouble) 46 | 47 | /** 48 | * Encodes a [BigDecimal] as an exact numeric value, preserving the full precision of the number. 49 | * 50 | * Note that this serializer is primarily meant for JSON, other formats will have the [BigDecimal] encoded as a string. 51 | */ 52 | @ExperimentalSerializationApi 53 | object BigDecimalAsJsonNumberSerializer : KSerializer { 54 | override val descriptor = PrimitiveSerialDescriptor(BigDecimal::class.qualifiedName!!, PrimitiveKind.STRING) 55 | 56 | override fun deserialize(decoder: Decoder): BigDecimal { 57 | return if (decoder is JsonDecoder) { 58 | BigDecimal(decoder.decodeJsonElement().jsonPrimitive.content) 59 | } else { 60 | BigDecimal(decoder.decodeString()) 61 | } 62 | } 63 | 64 | override fun serialize( 65 | encoder: Encoder, 66 | value: BigDecimal, 67 | ) { 68 | val bdString = value.toPlainString() 69 | 70 | if (encoder is JsonEncoder) { 71 | encoder.encodeJsonElement(JsonUnquotedLiteral(bdString)) 72 | } else { 73 | encoder.encodeString(bdString) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/math/BigIntegerSerializers.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.math 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.descriptors.PrimitiveKind 8 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | import kotlinx.serialization.json.JsonDecoder 12 | import kotlinx.serialization.json.JsonEncoder 13 | import kotlinx.serialization.json.JsonUnquotedLiteral 14 | import kotlinx.serialization.json.jsonPrimitive 15 | import java.math.BigDecimal 16 | import java.math.BigInteger 17 | 18 | /** 19 | * Encodes a [BigDecimal] as a string, preserving the full precision of the number. Wraps the number in quotes. 20 | */ 21 | typealias BigIntegerAsString = 22 | @Serializable(with = BigIntegerAsStringSerializer::class) 23 | BigInteger 24 | 25 | /** 26 | * Encodes a [BigInteger] as an unquoted JSON literal, preserving the full precision of the number while being encoded as a number. 27 | * 28 | * Note that this typealias is primarily meant for JSON, other formats will have the [BigInteger] encoded as a string. 29 | */ 30 | @ExperimentalSerializationApi 31 | typealias BigIntegerAsJsonLiteral = 32 | @Serializable(with = BigIntegerAsJsonLiteralSerializer::class) 33 | BigInteger 34 | 35 | object BigIntegerAsStringSerializer : KSerializer by stringSerializer(::BigInteger, BigInteger::toString) 36 | 37 | /** 38 | * Encodes a [BigInteger] as an exact numeric value, preserving the full precision of the number. 39 | * 40 | * Note that this serializer is primarily meant for JSON, other formats will have the [BigInteger] encoded as a string. 41 | */ 42 | @ExperimentalSerializationApi 43 | object BigIntegerAsJsonLiteralSerializer : KSerializer { 44 | override val descriptor = PrimitiveSerialDescriptor(BigInteger::class.qualifiedName!!, PrimitiveKind.DOUBLE) 45 | 46 | override fun deserialize(decoder: Decoder): BigInteger { 47 | return if (decoder is JsonDecoder) { 48 | BigInteger(decoder.decodeJsonElement().jsonPrimitive.content) 49 | } else { 50 | BigInteger(decoder.decodeString()) 51 | } 52 | } 53 | 54 | override fun serialize( 55 | encoder: Encoder, 56 | value: BigInteger, 57 | ) { 58 | val bdString = value.toString() 59 | 60 | if (encoder is JsonEncoder) { 61 | encoder.encodeJsonElement(JsonUnquotedLiteral(bdString)) 62 | } else { 63 | encoder.encodeString(bdString) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/net/UriSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.net 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.net.URI 6 | 7 | object UriSerializer : KSerializer by stringSerializer(::URI, URI::toASCIIString) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/net/UrlSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.net 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.net.URL 6 | 7 | object UrlSerializer : KSerializer by stringSerializer(::URL, URL::toExternalForm) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/InstantAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.longSerializer 4 | import io.ks3.standard.stringSerializer 5 | import kotlinx.serialization.KSerializer 6 | import java.time.Instant 7 | 8 | /** 9 | * Serializes an [Instant] to its' [String] representation. Example: `"2023-01-01T18:49:00.123456Z"` 10 | */ 11 | object InstantAsStringSerializer : KSerializer by stringSerializer( 12 | Instant::parse, 13 | Instant::toString, 14 | ) 15 | 16 | /** 17 | * Serializes an [Instant] to its' epoch millis. 18 | * WARNING!️ Nanosecond precision is lost when encoding/decoding [Instant]s with this serializer. 19 | * Considering using [InstantAsStringSerializer] instead. 20 | */ 21 | object InstantAsLongSerializer : KSerializer by longSerializer( 22 | Instant::ofEpochMilli, 23 | Instant::toEpochMilli, 24 | ) 25 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/LocalDateAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.time.LocalDate 6 | 7 | /** 8 | * Uses [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format 9 | * Example: `"2020-01-01"` 10 | */ 11 | object LocalDateAsStringSerializer : KSerializer by stringSerializer( 12 | { if (it.endsWith("T00:00:00")) LocalDate.parse(it.split("T")[0]) else LocalDate.parse(it) }, 13 | LocalDate::toString, 14 | ) 15 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/LocalDateTimeAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.time.LocalDateTime 6 | 7 | /** 8 | * Uses [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format, with no timezone information 9 | * Example: `"2020-01-01T18:49:00"` 10 | */ 11 | object LocalDateTimeAsStringSerializer : KSerializer by stringSerializer(LocalDateTime::parse) 12 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/LocalTimeAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.time.LocalTime 6 | 7 | /** 8 | * Uses [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format, with no timezone information 9 | * Example: `"12:00:00"` means 12 'o clock at noon. 10 | */ 11 | object LocalTimeAsStringSerializer : KSerializer by stringSerializer(LocalTime::parse) 12 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/OffsetDateTimeAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.time.OffsetDateTime 6 | 7 | /** 8 | * Serializes an [OffsetDateTime] to a string, including offset. Example: `"2020-01-01T18:49:00+01:00"` 9 | */ 10 | object OffsetDateTimeAsStringSerializer : KSerializer by stringSerializer(OffsetDateTime::parse) 11 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/YearMonthAsStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.time.YearMonth 6 | 7 | /** 8 | * Serializes a [YearMonth] to a string. Example: `"2020-01"` 9 | */ 10 | object YearMonthAsStringSerializer : KSerializer by stringSerializer(YearMonth::parse) 11 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/time/YearSerializers.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.ks3.standard.intSerializer 4 | import io.ks3.standard.stringSerializer 5 | import kotlinx.serialization.KSerializer 6 | import java.time.Year 7 | 8 | /** 9 | * Serializes a [Year] to a string. Example: `"2020"` 10 | */ 11 | object YearAsStringSerializer : KSerializer by stringSerializer( 12 | { Year.of(it.toInt()) }, 13 | ) 14 | 15 | /** 16 | * Serializes a [Year] to an integer. Example: `2020` 17 | */ 18 | object YearAsIntSerializer : KSerializer by intSerializer( 19 | Year::of, 20 | Year::getValue, 21 | ) 22 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/typealiases/Typealiases.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.ks3.java.typealiases 4 | 5 | import io.ks3.java.io.FilePathSerializer 6 | import io.ks3.java.net.UriSerializer 7 | import io.ks3.java.net.UrlSerializer 8 | import io.ks3.java.time.InstantAsLongSerializer 9 | import io.ks3.java.time.InstantAsStringSerializer 10 | import io.ks3.java.time.LocalDateAsStringSerializer 11 | import io.ks3.java.time.LocalDateTimeAsStringSerializer 12 | import io.ks3.java.time.LocalTimeAsStringSerializer 13 | import io.ks3.java.time.OffsetDateTimeAsStringSerializer 14 | import io.ks3.java.time.YearMonthAsStringSerializer 15 | import io.ks3.java.util.CurrencySerializer 16 | import io.ks3.java.util.LocaleSerializer 17 | import io.ks3.java.util.UuidSerializer 18 | import kotlinx.serialization.Serializable 19 | import java.io.File 20 | import java.net.URI 21 | import java.net.URL 22 | import java.time.Instant 23 | import java.time.LocalDate 24 | import java.time.LocalDateTime 25 | import java.time.LocalTime 26 | import java.time.OffsetDateTime 27 | import java.time.YearMonth 28 | import java.util.Currency 29 | import java.util.Locale 30 | import java.util.UUID 31 | 32 | // -- IO -- 33 | typealias FileAsString = 34 | @Serializable(with = FilePathSerializer::class) 35 | File 36 | 37 | // -- NET -- 38 | typealias UrlAsString = 39 | @Serializable(with = UrlSerializer::class) 40 | URL 41 | 42 | typealias UriAsString = 43 | @Serializable(with = UriSerializer::class) 44 | URI 45 | 46 | // -- TIME -- 47 | typealias YearMonthAsString = 48 | @Serializable(with = YearMonthAsStringSerializer::class) 49 | YearMonth 50 | 51 | typealias InstantAsString = 52 | @Serializable(with = InstantAsStringSerializer::class) 53 | Instant 54 | 55 | typealias InstantAsLong = 56 | @Serializable(with = InstantAsLongSerializer::class) 57 | Instant 58 | 59 | typealias LocalDateAsString = 60 | @Serializable(with = LocalDateAsStringSerializer::class) 61 | LocalDate 62 | 63 | typealias LocalDateTimeAsString = 64 | @Serializable(with = LocalDateTimeAsStringSerializer::class) 65 | LocalDateTime 66 | 67 | typealias LocalTimeAsString = 68 | @Serializable(with = LocalTimeAsStringSerializer::class) 69 | LocalTime 70 | 71 | typealias OffsetDateTimeAsString = 72 | @Serializable(with = OffsetDateTimeAsStringSerializer::class) 73 | OffsetDateTime 74 | 75 | // -- UTIL -- 76 | typealias LocaleAsString = 77 | @Serializable(with = LocaleSerializer::class) 78 | Locale 79 | 80 | typealias UuidAsString = 81 | @Serializable(with = UuidSerializer::class) 82 | UUID 83 | 84 | typealias CurrencyAsString = 85 | @Serializable(with = CurrencySerializer::class) 86 | Currency 87 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/util/AtomicSerializers.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.ks3.standard.intSerializer 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | import java.util.concurrent.atomic.AtomicInteger 11 | import java.util.concurrent.atomic.AtomicLong 12 | 13 | // From https://github.com/Kotlin/kotlinx.serialization/blob/15c6b59d2175485f8f5f4563ff17610003459113/runtime/jvm/src/main/kotlin/kotlinx/serialization/java/AtomicSerializers.kt 14 | 15 | object AtomicIntegerSerializer : KSerializer by intSerializer( 16 | ::AtomicInteger, 17 | AtomicInteger::get, 18 | ) 19 | 20 | object AtomicLongSerializer : KSerializer { 21 | override fun serialize( 22 | encoder: Encoder, 23 | value: AtomicLong, 24 | ) = encoder.encodeLong(value.get()) 25 | 26 | override fun deserialize(decoder: Decoder): AtomicLong = AtomicLong(decoder.decodeLong()) 27 | 28 | override val descriptor = PrimitiveSerialDescriptor(AtomicLongSerializer::class.qualifiedName!!, PrimitiveKind.LONG) 29 | } 30 | 31 | object AtomicBooleanSerializer : KSerializer { 32 | override fun serialize( 33 | encoder: Encoder, 34 | value: AtomicBoolean, 35 | ) = encoder.encodeBoolean(value.get()) 36 | 37 | override fun deserialize(decoder: Decoder): AtomicBoolean = AtomicBoolean(decoder.decodeBoolean()) 38 | 39 | override val descriptor = PrimitiveSerialDescriptor(AtomicBooleanSerializer::class.qualifiedName!!, PrimitiveKind.BOOLEAN) 40 | } 41 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/util/CurrencySerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.util.Currency 6 | 7 | object CurrencySerializer : KSerializer by stringSerializer(Currency::getInstance) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/util/LocaleSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.util.Locale 6 | 7 | object LocaleSerializer : KSerializer by stringSerializer(::Locale) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmMain/kotlin/io/ks3/java/util/UuidSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.ks3.standard.stringSerializer 4 | import kotlinx.serialization.KSerializer 5 | import java.util.UUID 6 | 7 | object UuidSerializer : KSerializer by stringSerializer(UUID::fromString) 8 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/io/FileSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.io 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.exhaustive.exhaustive 5 | import io.ks3.test.generateSerializerTests 6 | import java.io.File 7 | 8 | class FileSerializerTests : FunSpec( 9 | { 10 | val someFiles = 11 | listOf( 12 | "/", 13 | "/home/kotlin", 14 | "relative/to/linux", 15 | "C:", 16 | "C:\\Windows\\System32", 17 | "relative\\to\\windows", 18 | ).map(::File) 19 | 20 | include(generateSerializerTests(FilePathSerializer, someFiles.exhaustive())) 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/math/BigDecimalSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.math 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.Arb 5 | import io.kotest.property.arbitrary.bigDecimal 6 | import io.kotest.property.arbitrary.double 7 | import io.kotest.property.arbitrary.map 8 | import io.kotest.property.arbitrary.withEdgecases 9 | import io.ks3.test.generateSerializerTests 10 | import kotlinx.serialization.ExperimentalSerializationApi 11 | import java.math.BigDecimal 12 | 13 | @OptIn(ExperimentalSerializationApi::class) 14 | class BigDecimalSerializerTests : FunSpec( 15 | { 16 | val someNumbers = 17 | listOf( 18 | "0", 19 | "-1E307", 20 | "-1E-307", 21 | // Very many decimal digits.. 22 | "1.234349584529824359834295834958345892374892173498721349872398" + 23 | "457234985798234758927349872341908273978174239823475982743982734988", 24 | // Very many digits 25 | "98213749824375892743987239847298347982734598245982437598217394" + 26 | "8721398479817223987129483745782347823461982379812739817239873450.1", 27 | ).map(::BigDecimal) 28 | .map(BigDecimal::toPlainString) // Get rid of scientific notation 29 | .map(::BigDecimal) 30 | 31 | include( 32 | generateSerializerTests( 33 | BigDecimalAsStringSerializer, 34 | Arb.bigDecimal().withEdgecases(someNumbers), 35 | { "BigDecimalAsStringSerializer performs round-trip serialization" }, 36 | ), 37 | ) 38 | 39 | include( 40 | generateSerializerTests( 41 | BigDecimalAsJsonNumberSerializer, 42 | Arb.bigDecimal().withEdgecases(someNumbers), 43 | { "BigDecimalAsHighPrecisionNumberSerializer performs round-trip serialization" }, 44 | ), 45 | ) 46 | 47 | include( 48 | generateSerializerTests( 49 | BigDecimalAsDoubleSerializer, 50 | Arb.double() 51 | .withEdgecases(emptyList()) // get rid of NaN and Infinity 52 | .map { BigDecimal(it.toString()) }, 53 | { "BigDecimalAsDoubleSerializer performs round-trip serialization" }, 54 | ), 55 | ) 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/math/BigIntegerSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.math 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.Arb 5 | import io.kotest.property.arbitrary.bigInt 6 | import io.ks3.test.generateSerializerTests 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | 9 | @OptIn(ExperimentalSerializationApi::class) 10 | class BigIntegerSerializerTests : FunSpec( 11 | { 12 | 13 | include( 14 | generateSerializerTests( 15 | BigIntegerAsStringSerializer, 16 | Arb.bigInt(maxNumBits = 4096), 17 | { "BigIntegerAsStringSerializer performs round-trip serialization" }, 18 | ), 19 | ) 20 | 21 | include( 22 | generateSerializerTests( 23 | BigIntegerAsJsonLiteralSerializer, 24 | Arb.bigInt(maxNumBits = 4096), 25 | { "BigIntegerAsJsonLiteralSerializer performs round-trip serialization" }, 26 | ), 27 | ) 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/net/UriSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.net 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.exhaustive.exhaustive 5 | import io.ks3.test.generateSerializerTests 6 | import java.net.URI 7 | 8 | class UriSerializerTests : FunSpec( 9 | { 10 | val someUris = 11 | listOf( 12 | "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top", 13 | "ldap://[2001:db8::7]/c=GB?objectClass?one", 14 | "mailto:John.Doe@example.com", 15 | "news:comp.infosystems.www.servers.unix", 16 | "tel:+1-816-555-1212", 17 | "telnet://192.0.2.16:80/", 18 | "urn:oasis:names:specification:docbook:dtd:xml:4.1.2", 19 | ).map(::URI) 20 | 21 | include(generateSerializerTests(UriSerializer, someUris.exhaustive())) 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/net/UrlSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.net 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.exhaustive.exhaustive 5 | import io.ks3.test.generateSerializerTests 6 | import java.net.URL 7 | 8 | class UrlSerializerTests : FunSpec( 9 | { 10 | val someUrls = 11 | listOf( 12 | "http://localhost:1234/path?param=7", 13 | "https://www.google.com:1234", 14 | "ftp://www.dn.se", 15 | ).map(::URL) 16 | 17 | include(generateSerializerTests(UrlSerializer, someUrls.exhaustive())) 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/InstantSerializersTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FreeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.instant 7 | import io.ks3.test.generateSerializerTests 8 | import kotlinx.serialization.json.Json 9 | import java.time.Instant 10 | import java.time.Instant.ofEpochMilli 11 | import java.time.temporal.ChronoUnit.MILLIS 12 | 13 | class InstantSerializersTests : FreeSpec( 14 | { 15 | include( 16 | generateSerializerTests( 17 | InstantAsLongSerializer, 18 | // Only values that can be represented by a Long 19 | Arb.instant(ofEpochMilli(Long.MIN_VALUE)..ofEpochMilli(Long.MAX_VALUE)), 20 | { "Encodes Instant as epoch millis and back again" }, 21 | ) { originalValue -> 22 | truncatedTo(MILLIS) shouldBe originalValue.truncatedTo(MILLIS) // We lose nanosecond parts when encoding to epoch millis 23 | }, 24 | ) 25 | 26 | include(generateSerializerTests(InstantAsStringSerializer, Arb.instant(), { "Encodes Instant as String and back again" })) 27 | 28 | "samples" { 29 | Json.encodeToString(InstantAsLongSerializer, Instant.ofEpochMilli(1666528056)) shouldBe "1666528056" 30 | Json.encodeToString(InstantAsStringSerializer, Instant.ofEpochMilli(1666528056)) shouldBe "\"1970-01-20T06:55:28.056Z\"" 31 | } 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/LocalDateAsStringSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.property.Arb 7 | import io.kotest.property.arbitrary.localDate 8 | import io.kotest.property.checkAll 9 | import io.kotest.property.exhaustive.exhaustive 10 | import io.ks3.test.generateEncoders 11 | import io.ks3.test.generateSerializerTests 12 | import kotlinx.serialization.Serializable 13 | import kotlinx.serialization.decodeFromString 14 | import kotlinx.serialization.json.Json 15 | import java.time.LocalDate 16 | import java.time.format.DateTimeParseException 17 | 18 | class LocalDateAsStringSerializerTests : FunSpec( 19 | { 20 | val format = Json 21 | val (_, decoders) = format.generateEncoders(LocalDateAsStringSerializer) 22 | 23 | include(generateSerializerTests(LocalDateAsStringSerializer, Arb.localDate())) 24 | 25 | test("sample") { 26 | format.encodeToString(LocalDateAsStringSerializer, LocalDate.of(2022, 10, 25)) shouldBe 27 | "\"2022-10-25\"" 28 | } 29 | 30 | test("handles timestamps with time included if, and only if, it contains no information") { 31 | format.decodeFromString( 32 | """ 33 | { 34 | "date": "2021-01-01T00:00:00" 35 | } 36 | """.trimIndent(), 37 | ) shouldBe Sample(LocalDate.of(2021, 1, 1)) 38 | } 39 | 40 | test("timestamps cause error") { 41 | checkAll(decoders.exhaustive()) { decode -> 42 | shouldThrow { 43 | "\"2021-01-01T12:30:45.000Z\"".decode() 44 | } 45 | } 46 | } 47 | 48 | test("Just date is fine as well") { 49 | checkAll(decoders.exhaustive()) { decode -> 50 | "\"2021-01-01\"".decode() shouldBe LocalDate.of(2021, 1, 1) 51 | } 52 | } 53 | }, 54 | ) 55 | 56 | @Serializable 57 | private data class Sample( 58 | @Serializable(with = LocalDateAsStringSerializer::class) 59 | val date: LocalDate? = null, 60 | ) 61 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/LocalDateTimeAsStringSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.localDateTime 7 | import io.ks3.test.generateSerializerTests 8 | import kotlinx.serialization.json.Json 9 | import java.time.LocalDateTime 10 | 11 | class LocalDateTimeAsStringSerializerTests : FunSpec( 12 | { 13 | include(generateSerializerTests(LocalDateTimeAsStringSerializer, Arb.localDateTime())) 14 | 15 | test("sample") { 16 | Json.encodeToString(LocalDateTimeAsStringSerializer, LocalDateTime.of(2022, 10, 25, 18, 59, 13)) shouldBe 17 | "\"2022-10-25T18:59:13\"" 18 | } 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/LocalTimeAsStringSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FreeSpec 4 | import io.kotest.property.Arb 5 | import io.kotest.property.arbitrary.localTime 6 | import io.ks3.test.generateSerializerTests 7 | 8 | class LocalTimeAsStringSerializerTests : FreeSpec( 9 | { 10 | include(generateSerializerTests(LocalTimeAsStringSerializer, Arb.localTime())) 11 | }, 12 | ) 13 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/OffsetDateTimeAsStringSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FreeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.localTime 7 | import io.ks3.test.generateSerializerTests 8 | import kotlinx.serialization.json.Json 9 | import java.time.OffsetDateTime 10 | import java.time.ZoneOffset 11 | 12 | class OffsetDateTimeAsStringSerializerTests : FreeSpec( 13 | { 14 | include(generateSerializerTests(LocalTimeAsStringSerializer, Arb.localTime())) 15 | 16 | "Serialized form is expected" { 17 | Json.encodeToString( 18 | OffsetDateTimeAsStringSerializer, 19 | OffsetDateTime.of( 20 | 2022, 10, 25, 18, 59, 13, 0, ZoneOffset.ofHours(1), 21 | ), 22 | ) shouldBe "\"2022-10-25T18:59:13+01:00\"" 23 | } 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/YearMonthAsStringSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FreeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.yearMonth 7 | import io.ks3.java.typealiases.YearMonthAsString 8 | import io.ks3.test.generateSerializerTests 9 | import kotlinx.serialization.Serializable 10 | import kotlinx.serialization.json.Json 11 | import java.time.YearMonth 12 | 13 | class YearMonthAsStringSerializerTests : FreeSpec( 14 | { 15 | include(generateSerializerTests(YearMonthAsStringSerializer, Arb.yearMonth())) 16 | 17 | "sample" { 18 | Json.encodeToString(YearMonthAsStringSerializer, YearMonth.of(2022, 10)) shouldBe "\"2022-10\"" 19 | } 20 | 21 | "nullability with typealias" { 22 | @Serializable 23 | data class Appointment( 24 | val yearMonth: YearMonthAsString?, 25 | ) 26 | 27 | Json.decodeFromString("""{ "yearMonth": null }""") shouldBe Appointment(null) 28 | Json.decodeFromString("""{ "yearMonth": "2022-10" }""") shouldBe Appointment(YearMonth.of(2022, 10)) 29 | } 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/time/YearSerializersTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.time 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.int 7 | import io.kotest.property.arbitrary.map 8 | import io.ks3.test.generateSerializerTests 9 | import kotlinx.serialization.json.Json 10 | import java.time.Year 11 | 12 | class YearSerializersTests : FunSpec( 13 | { 14 | val generator = Arb.int(min = -999999999, max = 999999999).map(Year::of) 15 | 16 | include(generateSerializerTests(YearAsIntSerializer, generator, { "Encodes Year as Int and back again" })) 17 | include(generateSerializerTests(YearAsStringSerializer, generator, { "Encodes Year as String and back again" })) 18 | 19 | test("Sample") { 20 | Json.encodeToString(YearAsStringSerializer, Year.of(2022)) shouldBe "\"2022\"" 21 | Json.encodeToString(YearAsIntSerializer, Year.of(2022)) shouldBe "2022" 22 | } 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/util/AtomicSerializersTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.kotest.assertions.json.shouldEqualJson 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.types.shouldBeInstanceOf 7 | import io.kotest.property.Arb 8 | import io.kotest.property.Exhaustive 9 | import io.kotest.property.arbitrary.int 10 | import io.kotest.property.arbitrary.long 11 | import io.kotest.property.arbitrary.map 12 | import io.kotest.property.exhaustive.boolean 13 | import io.kotest.property.exhaustive.map 14 | import io.ks3.test.generateSerializerTests 15 | import kotlinx.serialization.Serializable 16 | import kotlinx.serialization.json.Json 17 | import java.util.concurrent.atomic.AtomicBoolean 18 | import java.util.concurrent.atomic.AtomicInteger 19 | import java.util.concurrent.atomic.AtomicLong 20 | 21 | class AtomicSerializersTests : FunSpec( 22 | { 23 | include( 24 | generateSerializerTests(AtomicBooleanSerializer, Exhaustive.boolean().map(::AtomicBoolean)) { originalValue -> 25 | this 26 | .shouldBeInstanceOf() 27 | .get() shouldBe originalValue.get() 28 | }, 29 | ) 30 | 31 | include( 32 | generateSerializerTests(AtomicIntegerSerializer, Arb.int().map(::AtomicInteger)) { originalValue -> 33 | this 34 | .shouldBeInstanceOf() 35 | .get() shouldBe originalValue.get() 36 | }, 37 | ) 38 | 39 | include( 40 | generateSerializerTests(AtomicLongSerializer, Arb.long().map(::AtomicLong)) { originalValue -> 41 | this 42 | .shouldBeInstanceOf() 43 | .get() shouldBe originalValue.get() 44 | }, 45 | ) 46 | 47 | @Serializable 48 | data class Sample( 49 | @Serializable(with = AtomicBooleanSerializer::class) 50 | val bool: AtomicBoolean, 51 | @Serializable(with = AtomicIntegerSerializer::class) 52 | val int: AtomicInteger, 53 | @Serializable(with = AtomicLongSerializer::class) 54 | val long: AtomicLong, 55 | ) 56 | 57 | test("sample") { 58 | Json.encodeToString(Sample.serializer(), Sample(AtomicBoolean(false), AtomicInteger(5), AtomicLong(22))) shouldEqualJson 59 | """ 60 | { 61 | "bool": false, 62 | "int": 5, 63 | "long": 22 64 | } 65 | """.trimIndent() 66 | } 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/util/CurrencySerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.kotest.core.spec.style.FreeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.map 7 | import io.kotest.property.arbitrary.of 8 | import io.ks3.test.generateSerializerTests 9 | import kotlinx.serialization.json.Json 10 | import java.util.Currency 11 | 12 | class CurrencySerializerTest : 13 | FreeSpec( 14 | { 15 | include( 16 | generateSerializerTests( 17 | CurrencySerializer, 18 | Arb.of(Currency.getAvailableCurrencies()).map { it }, 19 | ), 20 | ) 21 | "sample" { 22 | Json.encodeToString(CurrencySerializer, Currency.getInstance("AUD")) shouldBe "\"AUD\"" 23 | } 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/util/LocaleSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.Arb 5 | import io.kotest.property.arbitrary.locale 6 | import io.kotest.property.arbitrary.map 7 | import io.ks3.test.generateSerializerTests 8 | import java.util.Locale 9 | 10 | class LocaleSerializerTests : FunSpec( 11 | { 12 | include( 13 | generateSerializerTests( 14 | LocaleSerializer, 15 | Arb.locale().map { Locale(it) }, 16 | ), 17 | ) 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /ks3-jdk/src/jvmTest/kotlin/io/ks3/java/util/UuidSerializerTests.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.java.util 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.property.Arb 5 | import io.kotest.property.arbitrary.UUIDVersion 6 | import io.kotest.property.arbitrary.uuid 7 | import io.ks3.test.generateSerializerTests 8 | 9 | class UuidSerializerTests : FunSpec( 10 | { 11 | UUIDVersion.values().forEach { uuidVersion -> 12 | include( 13 | generateSerializerTests( 14 | UuidSerializer, 15 | Arb.uuid(uuidVersion), 16 | nameFn = { "Encode and decode UUID${uuidVersion.name}" }, 17 | ), 18 | ) 19 | } 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /ks3-standard/Module.md: -------------------------------------------------------------------------------- 1 | # Module ks3-standard 2 | * Serializers for standard Kotlin types, available on all (or at least, most) platforms. 3 | * Functions for more conveniently creating serializers. 4 | 5 | ## Serializer builders 6 | 7 | Serializer builders provide a convenient way of creating a serializer that turns `T` into a primitive, by simply providing the two 8 | conversion methods `(T) -> ` and `() -> T`. 9 | 10 | The return value of serializer builders is an anonymous object implementing `KSerializer`. 11 | 12 | ### Example: 13 | 14 | ```kotlin 15 | // Delegate the implementation to the return value of `stringSerializer(..)` 16 | object UriSerializer : KSerializer by stringSerializer( 17 | ::URI, 18 | URI::toASCIIString, 19 | ) 20 | ``` 21 | 22 | Here we create a serializer for `URI`. Our object implements `KSerializer` by delegating to the result of `stringSerializer(..)`. 23 | 24 | The first parameter, `::URI`, is the _decoding_ method which takes our primitive (`String`) and returns a `URI`. 25 | 26 | The second parameter is the _encoding_ method, which takes our type and returns the primitive. 27 | 28 | Since the methods to go back and forth from String to URI already exists in Java, this serializer is easily created using `stringSerializer`. 29 | 30 | 31 | ### Available builders are: 32 | 33 | * `intSerializer` 34 | * `longSerializer` 35 | * `stringSerializer` 36 | -------------------------------------------------------------------------------- /ks3-standard/README.md: -------------------------------------------------------------------------------- 1 | Serializers for standard Kotlin types, available on all (or at least, most) platforms. 2 | 3 | 4 | ## Serializers 5 | 6 | ### EnumAsOrdinalSerializer 7 | 8 | Used to serialize/deserialize enums to their ordinal value. 9 | 10 | Example usage: 11 | 12 | ```kotlin 13 | enum class Shape { 14 | SQUARE, CIRCLE; 15 | } 16 | 17 | object ShapeSerializer: KSerializer by enumAsOrdinalSerializer() 18 | ``` 19 | 20 | Now you can use the `ShapeSerializer` like you would any other. 21 | -------------------------------------------------------------------------------- /ks3-standard/api/ks3-standard.api: -------------------------------------------------------------------------------- 1 | public final class io/ks3/standard/ByteArrayAsBase64StringSerializer : kotlinx/serialization/KSerializer { 2 | public static final field INSTANCE Lio/ks3/standard/ByteArrayAsBase64StringSerializer; 3 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 4 | public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)[B 5 | public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 6 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 7 | public fun serialize (Lkotlinx/serialization/encoding/Encoder;[B)V 8 | } 9 | 10 | public final class io/ks3/standard/EnumDecodingException : kotlinx/serialization/SerializationException { 11 | public fun ()V 12 | public fun (Ljava/lang/String;Ljava/lang/Throwable;)V 13 | public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 14 | } 15 | 16 | public final class io/ks3/standard/TupleDescriptor : kotlinx/serialization/descriptors/SerialDescriptor { 17 | public fun (Ljava/lang/String;[Lkotlinx/serialization/descriptors/SerialDescriptor;)V 18 | public fun equals (Ljava/lang/Object;)Z 19 | public fun getAnnotations ()Ljava/util/List; 20 | public fun getElementAnnotations (I)Ljava/util/List; 21 | public fun getElementDescriptor (I)Lkotlinx/serialization/descriptors/SerialDescriptor; 22 | public final fun getElementDescriptors ()[Lkotlinx/serialization/descriptors/SerialDescriptor; 23 | public fun getElementIndex (Ljava/lang/String;)I 24 | public fun getElementName (I)Ljava/lang/String; 25 | public fun getElementsCount ()I 26 | public fun getKind ()Lkotlinx/serialization/descriptors/SerialKind; 27 | public fun getSerialName ()Ljava/lang/String; 28 | public fun hashCode ()I 29 | public fun isElementOptional (I)Z 30 | public fun isInline ()Z 31 | public fun isNullable ()Z 32 | public fun toString ()Ljava/lang/String; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /ks3-standard/api/ks3-standard.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final class io.ks3.standard/EnumDecodingException : kotlinx.serialization/SerializationException { // io.ks3.standard/EnumDecodingException|null[0] 10 | constructor (kotlin/String? =..., kotlin/Throwable? =...) // io.ks3.standard/EnumDecodingException.|(kotlin.String?;kotlin.Throwable?){}[0] 11 | } 12 | 13 | final object io.ks3.standard/ByteArrayAsBase64StringSerializer : kotlinx.serialization/KSerializer { // io.ks3.standard/ByteArrayAsBase64StringSerializer|null[0] 14 | final val descriptor // io.ks3.standard/ByteArrayAsBase64StringSerializer.descriptor|{}descriptor[0] 15 | final fun (): kotlinx.serialization.descriptors/SerialDescriptor // io.ks3.standard/ByteArrayAsBase64StringSerializer.descriptor.|(){}[0] 16 | 17 | final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlin/ByteArray // io.ks3.standard/ByteArrayAsBase64StringSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] 18 | final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin/ByteArray) // io.ks3.standard/ByteArrayAsBase64StringSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.ByteArray){}[0] 19 | } 20 | 21 | final inline fun <#A: reified kotlin/Any> io.ks3.standard/objectWrappingSerializer(kotlin/String): kotlinx.serialization.json/JsonTransformingSerializer<#A> // io.ks3.standard/objectWrappingSerializer|objectWrappingSerializer(kotlin.String){0§}[0] 22 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/doubleSerializer(crossinline kotlin/Function1, crossinline kotlin/Function1<#A, kotlin/Double>, kotlin/String? =...): kotlinx.serialization/KSerializer<#A> // io.ks3.standard/doubleSerializer|doubleSerializer(kotlin.Function1;kotlin.Function1<0:0,kotlin.Double>;kotlin.String?){0§}[0] 23 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/intSerializer(crossinline kotlin/Function1, crossinline kotlin/Function1<#A, kotlin/Int>, kotlin/String? =...): kotlinx.serialization/KSerializer<#A> // io.ks3.standard/intSerializer|intSerializer(kotlin.Function1;kotlin.Function1<0:0,kotlin.Int>;kotlin.String?){0§}[0] 24 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/lenientJsonArraySerializer(kotlinx.serialization/KSerializer<#A>? =...): kotlinx.serialization/KSerializer> // io.ks3.standard/lenientJsonArraySerializer|lenientJsonArraySerializer(kotlinx.serialization.KSerializer<0:0>?){0§}[0] 25 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/longSerializer(crossinline kotlin/Function1, crossinline kotlin/Function1<#A, kotlin/Long>, kotlin/String? =...): kotlinx.serialization/KSerializer<#A> // io.ks3.standard/longSerializer|longSerializer(kotlin.Function1;kotlin.Function1<0:0,kotlin.Long>;kotlin.String?){0§}[0] 26 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/sortedCollectionSerializer(kotlin/Comparator): kotlinx.serialization/KSerializer> // io.ks3.standard/sortedCollectionSerializer|sortedCollectionSerializer(kotlin.Comparator){0§}[0] 27 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/sortedListSerializer(kotlin/Comparator): kotlinx.serialization/KSerializer> // io.ks3.standard/sortedListSerializer|sortedListSerializer(kotlin.Comparator){0§}[0] 28 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/sortedSetSerializer(kotlin/Comparator): kotlinx.serialization/KSerializer> // io.ks3.standard/sortedSetSerializer|sortedSetSerializer(kotlin.Comparator){0§}[0] 29 | final inline fun <#A: reified kotlin/Any?> io.ks3.standard/stringSerializer(crossinline kotlin/Function1, crossinline kotlin/Function1<#A, kotlin/String> =..., kotlin/String? =...): kotlinx.serialization/KSerializer<#A> // io.ks3.standard/stringSerializer|stringSerializer(kotlin.Function1;kotlin.Function1<0:0,kotlin.String>;kotlin.String?){0§}[0] 30 | final inline fun <#A: reified kotlin/Enum<#A>> io.ks3.standard/enumAsOrdinalSerializer(): kotlinx.serialization/KSerializer<#A> // io.ks3.standard/enumAsOrdinalSerializer|enumAsOrdinalSerializer(){0§>}[0] 31 | -------------------------------------------------------------------------------- /ks3-standard/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("ks3.conventions.lang.kotlin-multiplatform-js") 3 | id("ks3.conventions.lang.kotlin-multiplatform-jvm") 4 | id("ks3.conventions.lang.kotlin-multiplatform-native") 5 | id("ks3.conventions.publishing.maven-publish") 6 | } 7 | 8 | dokkatoo { 9 | dokkatooSourceSets.configureEach { 10 | includes.from("Module.md") 11 | } 12 | 13 | dokkatooSourceSets.named("commonMain") { 14 | sourceLink { 15 | localDirectory.set(file("src/commonMain/kotlin")) 16 | remoteUrl("https://github.com/Kantis/ks3/blob/main/ks3-standard/src/commonMain/kotlin") 17 | remoteLineSuffix.set("#L") 18 | } 19 | } 20 | 21 | dokkatooSourceSets.named("jvmMain") { 22 | sourceLink { 23 | localDirectory.set(file("src/jvmMain/kotlin")) 24 | remoteUrl("https://github.com/Kantis/ks3/blob/main/ks3-standard/src/jvmMain/kotlin") 25 | remoteLineSuffix.set("#L") 26 | } 27 | } 28 | 29 | modulePath.set("ks3-standard") // match the original dokka default 30 | } 31 | 32 | tasks.withType().configureEach { 33 | generator.dokkaSourceSets.configureEach { 34 | sourceSetScope.set(":ks3-standard:dokkaHtmlPartial") 35 | } 36 | } 37 | 38 | kotlin { 39 | sourceSets { 40 | commonMain { 41 | dependencies { 42 | implementation(projects.ks3Core) 43 | implementation(libs.kotlinxSerialization.core) 44 | implementation(libs.kotlinxSerialization.json) 45 | } 46 | } 47 | 48 | commonTest { 49 | dependencies { 50 | implementation(libs.kotest.frameworkEngine) 51 | implementation(libs.kotest.assertionsCore) 52 | implementation(libs.kotest.assertionsJson) 53 | implementation(libs.kotest.property) 54 | 55 | implementation(libs.okio.core) 56 | 57 | implementation(libs.kotlinxSerialization.json) 58 | } 59 | } 60 | 61 | if (ks3Settings.enableKotlinJvm.get()) { 62 | jvmMain { 63 | dependencies { 64 | implementation(kotlin("reflect")) 65 | implementation(projects.ks3Core) 66 | } 67 | } 68 | 69 | jvmTest { 70 | dependencies { 71 | implementation(libs.kotest.runnerJunit5) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/ByteArrayAsBase64StringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | import kotlin.io.encoding.Base64 11 | import kotlin.io.encoding.ExperimentalEncodingApi 12 | 13 | typealias ByteArrayAsBase64String = 14 | @Serializable(with = ByteArrayAsBase64StringSerializer::class) 15 | ByteArray 16 | 17 | @OptIn(ExperimentalEncodingApi::class) 18 | object ByteArrayAsBase64StringSerializer : KSerializer { 19 | override val descriptor: SerialDescriptor = 20 | PrimitiveSerialDescriptor( 21 | "io.ks3.standard.ByteArrayAsBase64StringSerializer", 22 | PrimitiveKind.STRING, 23 | ) 24 | 25 | override fun deserialize(decoder: Decoder): ByteArray { 26 | return Base64.decode(decoder.decodeString()) 27 | } 28 | 29 | override fun serialize( 30 | encoder: Encoder, 31 | value: ByteArray, 32 | ) { 33 | encoder.encodeString(Base64.encode(value)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/DoubleSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | inline fun doubleSerializer( 10 | crossinline decode: (Double) -> T, 11 | crossinline encode: (T) -> Double, 12 | nameOverride: String? = null, 13 | ): KSerializer = 14 | object : KSerializer { 15 | override val descriptor = PrimitiveSerialDescriptor(nameOverride ?: T::class.simpleName!!, PrimitiveKind.DOUBLE) 16 | 17 | override fun deserialize(decoder: Decoder) = decode(decoder.decodeDouble()) 18 | 19 | override fun serialize( 20 | encoder: Encoder, 21 | value: T, 22 | ) = encoder.encodeDouble(encode(value)) 23 | } 24 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/EnumAsOrdinalSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | inline fun > enumAsOrdinalSerializer() = 10 | object : KSerializer { 11 | val values = enumValues() 12 | val name = T::class.simpleName 13 | 14 | override val descriptor = PrimitiveSerialDescriptor("io.ks3.standard.EnumAsOrdinal", PrimitiveKind.INT) 15 | 16 | override fun deserialize(decoder: Decoder): T { 17 | val i = decoder.decodeInt() 18 | if (i !in values.indices) { 19 | throw EnumDecodingException("Invalid ordinal value $i for $name, must be in ${values.indices}") 20 | } 21 | return values[i] 22 | } 23 | 24 | override fun serialize( 25 | encoder: Encoder, 26 | value: T, 27 | ) { 28 | return encoder.encodeInt(values.indexOf(value)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/EnumDecodingException.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.SerializationException 4 | 5 | class EnumDecodingException(message: String? = null, cause: Throwable? = null) : SerializationException(message, cause) 6 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/IntSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | inline fun intSerializer( 10 | crossinline decode: (Int) -> T, 11 | crossinline encode: (T) -> Int, 12 | nameOverride: String? = null, 13 | ): KSerializer = 14 | object : KSerializer { 15 | override val descriptor = PrimitiveSerialDescriptor(nameOverride ?: T::class.simpleName!!, PrimitiveKind.LONG) 16 | 17 | override fun deserialize(decoder: Decoder) = decode(decoder.decodeInt()) 18 | 19 | override fun serialize( 20 | encoder: Encoder, 21 | value: T, 22 | ) = encoder.encodeInt(encode(value)) 23 | } 24 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/LenientJsonArraySerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.ks3.core.ExperimentalKs3 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.SerializationException 6 | import kotlinx.serialization.SerializationStrategy 7 | import kotlinx.serialization.builtins.ListSerializer 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | import kotlinx.serialization.json.JsonDecoder 12 | import kotlinx.serialization.json.jsonArray 13 | import kotlinx.serialization.serializer 14 | 15 | /** 16 | * Creates a serializer that will parse a JSON array into a list of [T] elements, ignoring any elements that fail to parse. 17 | * 18 | * Example: 19 | * ``` 20 | * val lenientIntListSerializer = lenientJsonArraySerializer() 21 | * val numbers = Json.decodeFromString(lenientIntListSerializer, "[1,2,3, \"foo\",5]") 22 | * 23 | * println(numbers) // > [1, 2, 3, 5] 24 | * ``` 25 | */ 26 | @ExperimentalKs3 27 | inline fun lenientJsonArraySerializer(explicitElementSerializer: KSerializer? = null) = 28 | object : KSerializer> { 29 | private val elementSerializer = explicitElementSerializer ?: serializer() 30 | private val serializer: SerializationStrategy> = ListSerializer(elementSerializer) 31 | override val descriptor: SerialDescriptor = serializer.descriptor 32 | 33 | override fun deserialize(decoder: Decoder): List { 34 | require(decoder is JsonDecoder) { "This deserializer can only be used with Json" } 35 | val json = decoder.json 36 | val list = decoder.decodeJsonElement() 37 | return list 38 | .jsonArray 39 | .mapNotNull { 40 | try { 41 | json.decodeFromJsonElement(elementSerializer, it) 42 | } catch (e: SerializationException) { 43 | null 44 | } catch (e: IllegalArgumentException) { 45 | null 46 | } 47 | } 48 | } 49 | 50 | override fun serialize( 51 | encoder: Encoder, 52 | value: List, 53 | ): Unit = serializer.serialize(encoder, value) 54 | } 55 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/LongSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | inline fun longSerializer( 10 | crossinline decode: (Long) -> T, 11 | crossinline encode: (T) -> Long, 12 | nameOverride: String? = null, 13 | ): KSerializer = 14 | object : KSerializer { 15 | override val descriptor = PrimitiveSerialDescriptor(nameOverride ?: T::class.simpleName!!, PrimitiveKind.LONG) 16 | 17 | override fun deserialize(decoder: Decoder) = decode(decoder.decodeLong()) 18 | 19 | override fun serialize( 20 | encoder: Encoder, 21 | value: T, 22 | ) = encoder.encodeLong(encode(value)) 23 | } 24 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/ObjectWrappingSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.json.JsonElement 4 | import kotlinx.serialization.json.JsonTransformingSerializer 5 | import kotlinx.serialization.json.buildJsonObject 6 | import kotlinx.serialization.json.jsonObject 7 | import kotlinx.serialization.serializer 8 | 9 | /** 10 | * Wraps the inner serializer by putting the entire structure into a JSON object, keyed by [fieldName]. 11 | * 12 | * Example: 13 | * ```kotlin 14 | * val serializer = objectWrappingSerializer("someFieldName") 15 | * data class Person(val name: String, val age: Int) 16 | * 17 | * println(Json.encodeToString(serializer, Person("John", 42))) 18 | * // > { "someFieldName": { "name" : "John" , "age" : 42 } } 19 | * ``` 20 | * 21 | * Can be used to wrap a value class in a JSON object, for instance if an API requires you to send ` { "id": 42 } ` instead of just `42`. 22 | */ 23 | inline fun objectWrappingSerializer(fieldName: String) = 24 | object : JsonTransformingSerializer(serializer()) { 25 | override fun transformSerialize(element: JsonElement): JsonElement { 26 | return buildJsonObject { 27 | put(fieldName, element) 28 | } 29 | } 30 | 31 | override fun transformDeserialize(element: JsonElement): JsonElement { 32 | return element.jsonObject[fieldName]!! 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/SortedCollectionSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.ks3.core.ExperimentalKs3 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.encoding.Encoder 6 | import kotlinx.serialization.serializer 7 | 8 | /** 9 | * Serializers that sort collections / lists / sets with the given [comparator] upon serialization. 10 | * 11 | * Example: 12 | * ```kotlin 13 | * class SortedStringListSerializer : 14 | * KSerializer> by sortedListSerializer(compareBy { it }) 15 | * 16 | * @Serializable 17 | * data class Friends( 18 | * @Serializable(SortedStringListSerializer::class) 19 | * val names: List 20 | * ) 21 | * ``` 22 | */ 23 | @ExperimentalKs3 24 | inline fun sortedCollectionSerializer(comparator: Comparator): KSerializer> { 25 | val delegate = serializer>() 26 | return object : KSerializer> by delegate { 27 | override fun serialize( 28 | encoder: Encoder, 29 | value: Collection, 30 | ) = delegate.serialize(encoder, value.sortedWith(comparator)) 31 | } 32 | } 33 | 34 | @ExperimentalKs3 35 | inline fun sortedListSerializer(comparator: Comparator): KSerializer> { 36 | val delegate = serializer>() 37 | return object : KSerializer> by delegate { 38 | override fun serialize( 39 | encoder: Encoder, 40 | value: List, 41 | ) = delegate.serialize(encoder, value.sortedWith(comparator)) 42 | } 43 | } 44 | 45 | @ExperimentalKs3 46 | inline fun sortedSetSerializer(comparator: Comparator): KSerializer> { 47 | val delegate = serializer>() 48 | return object : KSerializer> by delegate { 49 | override fun serialize( 50 | encoder: Encoder, 51 | value: Set, 52 | ) = delegate.serialize(encoder, value.sortedWith(comparator).toSet()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ks3-standard/src/commonMain/kotlin/io/ks3/standard/StringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | inline fun stringSerializer( 10 | crossinline decode: (String) -> T, 11 | crossinline encode: (T) -> String = { it.toString() }, 12 | nameOverride: String? = null, 13 | ): KSerializer = 14 | object : KSerializer { 15 | override val descriptor = PrimitiveSerialDescriptor(nameOverride ?: T::class.simpleName!!, PrimitiveKind.STRING) 16 | 17 | override fun deserialize(decoder: Decoder) = decode(decoder.decodeString()) 18 | 19 | override fun serialize( 20 | encoder: Encoder, 21 | value: T, 22 | ) = encoder.encodeString(encode(value)) 23 | } 24 | -------------------------------------------------------------------------------- /ks3-standard/src/commonTest/kotlin/io/ks3/standard/ByteArrayAsBase64StringSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.kotest.assertions.throwables.shouldThrowWithMessage 4 | import io.kotest.core.spec.style.FreeSpec 5 | import io.kotest.matchers.shouldBe 6 | import kotlinx.serialization.json.Json 7 | 8 | class ByteArrayAsBase64StringSerializerTest : FreeSpec( 9 | { 10 | val format = Json 11 | 12 | fun ByteArray.serializeBase64() = format.encodeToString(ByteArrayAsBase64StringSerializer, this) 13 | 14 | fun String.deserializeBase64() = format.decodeFromString(ByteArrayAsBase64StringSerializer, this) 15 | 16 | "Encode to base64 string" { 17 | "test".encodeToByteArray().serializeBase64() shouldBe "\"dGVzdA==\"" 18 | } 19 | 20 | "Decode from base64 string" { 21 | "\"dGVzdA==\"".deserializeBase64() shouldBe "test".encodeToByteArray() 22 | } 23 | 24 | "Non-base64 content" { 25 | shouldThrowWithMessage("Invalid symbol '['(133) at index 0") { 26 | "\"[][]\"".deserializeBase64() 27 | } 28 | } 29 | 30 | "Invalid base64" { 31 | shouldThrowWithMessage("The last unit of input does not have enough bits") { 32 | "\"A===\"".deserializeBase64() 33 | } 34 | } 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /ks3-standard/src/commonTest/kotlin/io/ks3/standard/EnumAsOrdinalSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.kotest.assertions.json.shouldEqualJson 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.FreeSpec 6 | import io.kotest.matchers.shouldBe 7 | import kotlinx.serialization.KSerializer 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.encodeToString 11 | import kotlinx.serialization.json.Json 12 | 13 | object ShapeSerializer : KSerializer by enumAsOrdinalSerializer() 14 | 15 | class EnumAsOrdinalSerializerTest : FreeSpec( 16 | { 17 | 18 | val serializer = ShapeSerializer 19 | val format = Json 20 | 21 | @Serializable 22 | data class EnumWrapper( 23 | @Serializable(with = ShapeSerializer::class) 24 | val shape: Shape?, 25 | ) 26 | 27 | "Given a nullable enum property, Then null should be encoded" { 28 | format.encodeToString(EnumWrapper(null)) shouldEqualJson 29 | """ 30 | { 31 | "shape": null 32 | } 33 | """.trimIndent() 34 | } 35 | 36 | "Given a nullable enum property, Then non-null should be encoded" { 37 | format.encodeToString(EnumWrapper(Shape.SQUARE)) shouldEqualJson 38 | """ 39 | { 40 | "shape": 1 41 | } 42 | """.trimIndent() 43 | } 44 | 45 | "Given a nullable enum property, Then null should be decoded" { 46 | format.decodeFromString("{ \"shape\": null }") shouldBe EnumWrapper(null) 47 | } 48 | 49 | "Given a nullable enum property, Then non-null value should be decoded" { 50 | format.decodeFromString("{ \"shape\": 1 }") shouldBe EnumWrapper(Shape.SQUARE) 51 | } 52 | 53 | "Encodes to ordinal value" { 54 | format.encodeToString(serializer, Shape.SQUARE) shouldBe "1" 55 | format.encodeToString(serializer, Shape.CIRCLE) shouldBe "0" 56 | } 57 | 58 | "Decodes from ordinal value" { 59 | format.decodeFromString(serializer, "1") shouldBe Shape.SQUARE 60 | format.decodeFromString(serializer, "0") shouldBe Shape.CIRCLE 61 | } 62 | 63 | "Decodes quoted value" { 64 | format.decodeFromString(serializer, "\"1\"") shouldBe Shape.SQUARE 65 | } 66 | 67 | "Throws exception on invalid ordinal" { 68 | shouldThrow { 69 | format.decodeFromString(serializer, "-1") 70 | }.message shouldBe "Invalid ordinal value -1 for Shape, must be in 0..1" 71 | 72 | shouldThrow { 73 | format.decodeFromString(serializer, "2") 74 | }.message shouldBe "Invalid ordinal value 2 for Shape, must be in 0..1" 75 | } 76 | }, 77 | ) 78 | 79 | private enum class Shape { 80 | CIRCLE, 81 | SQUARE, 82 | } 83 | -------------------------------------------------------------------------------- /ks3-standard/src/commonTest/kotlin/io/ks3/standard/LenientJsonArraySerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.json.Json 7 | 8 | class LenientJsonArraySerializerTest : StringSpec( 9 | { 10 | val json = Json 11 | 12 | "Single bad element is discarded" { 13 | json.decodeFromString(lenientJsonArraySerializer(), "[1,2,3, \"foo\",5]") shouldBe listOf(1, 2, 3, 5) 14 | } 15 | 16 | "Entire list can be discarded" { 17 | json.decodeFromString(lenientJsonArraySerializer(), "[\"foo\",\"bar\"]") shouldBe listOf() 18 | } 19 | 20 | "Should work with nested lists" { 21 | json.decodeFromString( 22 | // We need to pass the inner serializer so resilience will be applied within the nested list deserialization 23 | lenientJsonArraySerializer>( 24 | lenientJsonArraySerializer(), 25 | ), 26 | "[[1,2,3], [4,\"foo\",6], [7,8,9]]", 27 | ) shouldBe 28 | listOf( 29 | listOf(1, 2, 3), 30 | listOf(4, 6), 31 | listOf(7, 8, 9), 32 | ) 33 | } 34 | 35 | "Should omit data classes when they fail to deserialize" { 36 | @Serializable 37 | data class Foo( 38 | val bar: String, 39 | val baz: Int, 40 | ) 41 | 42 | json.decodeFromString( 43 | lenientJsonArraySerializer(), 44 | """ 45 | [ 46 | {"bar": "bar", "baz":1}, 47 | {"bar": "bar"}, 48 | {"bar": "bar", "baz": "x"} 49 | ] 50 | """.trimIndent(), 51 | ) shouldBe listOf(Foo("bar", 1)) 52 | } 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /ks3-standard/src/commonTest/kotlin/io/ks3/standard/SortedCollectionSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.ks3.core.ExperimentalKs3 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.Json 8 | 9 | @OptIn(ExperimentalKs3::class) 10 | class SortedCollectionSerializerTest : StringSpec( 11 | { 12 | val json = Json 13 | 14 | "A list of strings can be serialized by natural ordering" { 15 | json.encodeToString( 16 | sortedListSerializer( 17 | compareBy { it }, 18 | ), 19 | listOf("c", "b", "a"), 20 | ) shouldBe 21 | """ 22 | ["a","b","c"] 23 | """.trimIndent() 24 | } 25 | 26 | "A list of non-comparable objects can be serialized by custom ordering" { 27 | @Serializable 28 | data class Friend(val name: String, val age: Int) 29 | 30 | val friends = 31 | listOf( 32 | Friend("Alice", 20), 33 | Friend("Bob", 30), 34 | Friend("Charles", 40), 35 | ) 36 | 37 | json.encodeToString(sortedListSerializer(compareByDescending { it.age }), friends) shouldBe 38 | """ 39 | [{"name":"Charles","age":40},{"name":"Bob","age":30},{"name":"Alice","age":20}] 40 | """.trimIndent() 41 | } 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /ks3-standard/src/jvmMain/kotlin/io/ks3/standard/TupleDescriptor.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.ks3.core.Ks3Internal 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialKind 7 | import kotlinx.serialization.descriptors.StructureKind 8 | 9 | @Ks3Internal 10 | @ExperimentalSerializationApi 11 | class TupleDescriptor(override val serialName: String, vararg val elementDescriptors: SerialDescriptor) : 12 | SerialDescriptor { 13 | override val kind: SerialKind get() = StructureKind.LIST 14 | override val elementsCount: Int = elementDescriptors.size 15 | 16 | override fun getElementName(index: Int): String = index.toString() 17 | 18 | override fun getElementIndex(name: String): Int = name.toIntOrNull() ?: throw IllegalArgumentException("$name is not a valid list index") 19 | 20 | override fun isElementOptional(index: Int): Boolean { 21 | require(index >= 0) { "Illegal index $index, $serialName expects only non-negative indices" } 22 | return false 23 | } 24 | 25 | override fun getElementAnnotations(index: Int): List { 26 | require(index >= 0) { "Illegal index $index, $serialName expects only non-negative indices" } 27 | return emptyList() 28 | } 29 | 30 | override fun getElementDescriptor(index: Int): SerialDescriptor { 31 | require(index >= 0) { "Illegal index $index, $serialName expects only non-negative indices" } 32 | return elementDescriptors[index] 33 | } 34 | 35 | override fun equals(other: Any?): Boolean { 36 | if (this === other) return true 37 | if (other !is TupleDescriptor) return false 38 | return elementDescriptors.contentEquals(other.elementDescriptors) && serialName == other.serialName 39 | } 40 | 41 | override fun hashCode(): Int { 42 | return elementDescriptors.hashCode() * 31 + serialName.hashCode() 43 | } 44 | 45 | override fun toString(): String = "$serialName(${elementDescriptors.joinToString()})" 46 | } 47 | -------------------------------------------------------------------------------- /ks3-standard/src/jvmMain/kotlin/io/ks3/standard/TupleSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.encoding.Decoder 6 | import kotlinx.serialization.encoding.Encoder 7 | import kotlinx.serialization.encoding.decodeStructure 8 | import kotlinx.serialization.encoding.encodeStructure 9 | import kotlinx.serialization.serializer 10 | import kotlin.reflect.KParameter 11 | import kotlin.reflect.KProperty1 12 | import kotlin.reflect.full.isSubtypeOf 13 | 14 | /** 15 | * Creates a serializer that encodes select properties of [T] as a tuple. 16 | * 17 | * For deserialization to work, the following conditions must be fulfilled: 18 | * * There must be a constructor where only the serialized properties are required. 19 | * * The constructor _can_ have other parameters, but they must have a default value. 20 | * * The properties must match parameters of the constructor by name and type. 21 | * 22 | * 23 | * Example: 24 | * ```kotlin 25 | * 26 | * @Serializable(with = PersonSerializer::class) 27 | * data class Person(val name: String, val age: Int, val profession: String) 28 | * 29 | * object PersonSerializer : KSerializer by tupleSerializer( 30 | * Person::name, 31 | * Person::age, 32 | * Person::profession 33 | * ) 34 | * 35 | * println(Json.encodeToString(PersonSerializer, Person("Kaylee", 21, "Mechanic"))) 36 | * // > ["Kaylee", 21, "Mechanic"] 37 | * ``` 38 | */ 39 | @ExperimentalSerializationApi 40 | inline fun tupleSerializer(vararg properties: KProperty1): KSerializer = 41 | object : KSerializer { 42 | override val descriptor = 43 | TupleDescriptor( 44 | "io.ks3.Tuple(${T::class.qualifiedName})", 45 | *properties.map { serializer(it.returnType).descriptor }.toTypedArray(), 46 | ) 47 | 48 | private val constructorPropertyMap = 49 | T::class.constructors 50 | .mapNotNull { constructor -> 51 | val mapping = 52 | constructor.parameters 53 | .filterNot(KParameter::isOptional) 54 | .associateWith { param -> 55 | properties.singleOrNull { prop -> 56 | prop.returnType.isSubtypeOf(param.type) && prop.name == param.name 57 | } 58 | } 59 | 60 | if (mapping.any { (_, key) -> key == null }) { 61 | null 62 | } else { 63 | constructor to mapping 64 | } 65 | } 66 | .single() 67 | 68 | override fun deserialize(decoder: Decoder): T { 69 | return decoder.decodeStructure(descriptor) { 70 | val args = mutableListOf() 71 | while (true) { 72 | val index = decodeElementIndex(descriptor) 73 | if (index == -1) break 74 | decodeSerializableElement(descriptor, index, serializer(properties[index].returnType)).let { args.add(it) } 75 | } 76 | 77 | val mappedArgs = constructorPropertyMap.second.mapValues { (_, prop) -> args[properties.indexOf(prop)] } 78 | 79 | constructorPropertyMap.first.callBy(mappedArgs) 80 | } 81 | } 82 | 83 | override fun serialize( 84 | encoder: Encoder, 85 | value: T, 86 | ) { 87 | encoder.encodeStructure(descriptor) { 88 | for (i in properties.indices) { 89 | encodeSerializableElement(descriptor, i, serializer(properties[i].returnType), properties[i].get(value)) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ks3-standard/src/jvmTest/kotlin/io/ks3/standard/TupleSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.standard 2 | 3 | import io.kotest.assertions.json.shouldEqualJson 4 | import io.kotest.core.spec.style.FreeSpec 5 | import io.kotest.matchers.shouldBe 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.KSerializer 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.encodeToString 11 | import kotlinx.serialization.json.Json 12 | 13 | @Serializable(with = PointAsTupleSerializer::class) 14 | data class Point(val x: Int, val y: Int) 15 | 16 | @OptIn(ExperimentalSerializationApi::class) 17 | object PointAsTupleSerializer : KSerializer by tupleSerializer(Point::x, Point::y) 18 | 19 | @Serializable(with = PersonAsTupleSerializer::class) 20 | data class Person(val name: String, val age: Int, val origin: Point) 21 | 22 | @OptIn(ExperimentalSerializationApi::class) 23 | object PersonAsTupleSerializer : KSerializer by tupleSerializer(Person::name, Person::age, Person::origin) 24 | 25 | @Serializable(with = CoordinatesDetailsSerializer::class) 26 | data class CoordinatesDetails( 27 | val x: Int, 28 | val y: Int, 29 | private val colour: String, 30 | private val active: Boolean, 31 | ) { 32 | // Required for decoding partially encoded tuple 33 | constructor(x: Int, y: Int) : this(x, y, "red", true) 34 | } 35 | 36 | @OptIn(ExperimentalSerializationApi::class) 37 | object CoordinatesDetailsSerializer : KSerializer by tupleSerializer(CoordinatesDetails::x, CoordinatesDetails::y) 38 | 39 | @Serializable(with = CoordinatesDetailsWithDefaultsSerializer::class) 40 | data class CoordinatesDetailsWithDefaults( 41 | val x: Int, 42 | val y: Int, 43 | val colour: String = "red", 44 | val active: Boolean = true, 45 | ) 46 | 47 | @OptIn(ExperimentalSerializationApi::class) 48 | object CoordinatesDetailsWithDefaultsSerializer : 49 | KSerializer by tupleSerializer(CoordinatesDetailsWithDefaults::x, CoordinatesDetailsWithDefaults::y) 50 | 51 | class TupleSerializerTest : FreeSpec( 52 | { 53 | val format = Json 54 | 55 | "should serialize" { 56 | format.encodeToString(Point(1, 2)) shouldEqualJson "[1,2]" 57 | } 58 | 59 | "should deserialize" { 60 | format.decodeFromString("[1,2]") shouldBe Point(1, 2) 61 | } 62 | 63 | "Tuple in tuple" - { 64 | val person = Person("Emil", 35, Point(1, 2)) 65 | val serialForm = """["Emil", 35, [1,2]]""" 66 | 67 | "should serialize" { 68 | format.encodeToString(person) shouldEqualJson serialForm 69 | } 70 | 71 | "should deserialize" { 72 | format.decodeFromString(serialForm) shouldBe person 73 | } 74 | } 75 | 76 | "Partially encoded tuple can be decoded using matching constructor" { 77 | format.encodeToString(CoordinatesDetails(1, 2, "red", true)) shouldEqualJson "[1,2]" 78 | format.decodeFromString("[1,2]") shouldBe CoordinatesDetails(1, 2, "red", true) 79 | } 80 | 81 | "Partially encoded tuple can be decoded using default values" { 82 | format.encodeToString(CoordinatesDetailsWithDefaults(1, 2, "red", true)) shouldEqualJson "[1,2]" 83 | format.decodeFromString("[1,2]") shouldBe CoordinatesDetailsWithDefaults(1, 2, "red", true) 84 | } 85 | }, 86 | ) 87 | -------------------------------------------------------------------------------- /ks3-test/Module.md: -------------------------------------------------------------------------------- 1 | # Module ks3-test 2 | Utilities for testing serializers using [Kotest](https://kotest.io). 3 | 4 | `generateSerializerTests` returns a Kotest [test factory](https://kotest.io/docs/framework/test-factories.html) for testing a serializer. 5 | 6 | It uses property testing to test round-trip serialization of random inputs. 7 | -------------------------------------------------------------------------------- /ks3-test/api/ks3-test.api: -------------------------------------------------------------------------------- 1 | public final class io/ks3/test/Encoders { 2 | public fun (Ljava/util/List;Ljava/util/List;)V 3 | public final fun component1 ()Ljava/util/List; 4 | public final fun component2 ()Ljava/util/List; 5 | public final fun copy (Ljava/util/List;Ljava/util/List;)Lio/ks3/test/Encoders; 6 | public static synthetic fun copy$default (Lio/ks3/test/Encoders;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lio/ks3/test/Encoders; 7 | public fun equals (Ljava/lang/Object;)Z 8 | public final fun getDecoders ()Ljava/util/List; 9 | public final fun getEncoders ()Ljava/util/List; 10 | public fun hashCode ()I 11 | public fun toString ()Ljava/lang/String; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /ks3-test/api/ks3-test.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final class <#A: kotlin/Any?> io.ks3.test/Encoders { // io.ks3.test/Encoders|null[0] 10 | constructor (kotlin.collections/List>, kotlin.collections/List>) // io.ks3.test/Encoders.|(kotlin.collections.List>;kotlin.collections.List>){}[0] 11 | 12 | final val decoders // io.ks3.test/Encoders.decoders|{}decoders[0] 13 | final fun (): kotlin.collections/List> // io.ks3.test/Encoders.decoders.|(){}[0] 14 | final val encoders // io.ks3.test/Encoders.encoders|{}encoders[0] 15 | final fun (): kotlin.collections/List> // io.ks3.test/Encoders.encoders.|(){}[0] 16 | 17 | final fun component1(): kotlin.collections/List> // io.ks3.test/Encoders.component1|component1(){}[0] 18 | final fun component2(): kotlin.collections/List> // io.ks3.test/Encoders.component2|component2(){}[0] 19 | final fun copy(kotlin.collections/List> =..., kotlin.collections/List> =...): io.ks3.test/Encoders<#A> // io.ks3.test/Encoders.copy|copy(kotlin.collections.List>;kotlin.collections.List>){}[0] 20 | final fun equals(kotlin/Any?): kotlin/Boolean // io.ks3.test/Encoders.equals|equals(kotlin.Any?){}[0] 21 | final fun hashCode(): kotlin/Int // io.ks3.test/Encoders.hashCode|hashCode(){}[0] 22 | final fun toString(): kotlin/String // io.ks3.test/Encoders.toString|toString(){}[0] 23 | } 24 | 25 | final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).io.ks3.test/decoders(kotlinx.serialization/KSerializer<#A>): kotlin.collections/List> // io.ks3.test/decoders|decoders@kotlinx.serialization.json.Json(kotlinx.serialization.KSerializer<0:0>){0§}[0] 26 | final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).io.ks3.test/encoders(kotlinx.serialization/KSerializer<#A>): kotlin.collections/List> // io.ks3.test/encoders|encoders@kotlinx.serialization.json.Json(kotlinx.serialization.KSerializer<0:0>){0§}[0] 27 | final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.json/Json).io.ks3.test/generateEncoders(kotlinx.serialization/KSerializer<#A>): io.ks3.test/Encoders<#A> // io.ks3.test/generateEncoders|generateEncoders@kotlinx.serialization.json.Json(kotlinx.serialization.KSerializer<0:0>){0§}[0] 28 | final inline fun <#A: reified kotlin/Any?> io.ks3.test/generateSerializerTests(kotlinx.serialization/KSerializer<#A>, io.kotest.property/Gen<#A>, crossinline kotlin/Function0 =..., crossinline kotlin/Function2<#A, #A, kotlin/Unit> =...): io.kotest.core.factory/TestFactory // io.ks3.test/generateSerializerTests|generateSerializerTests(kotlinx.serialization.KSerializer<0:0>;io.kotest.property.Gen<0:0>;kotlin.Function0;kotlin.Function2<0:0,0:0,kotlin.Unit>){0§}[0] 29 | -------------------------------------------------------------------------------- /ks3-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("ks3.conventions.lang.kotlin-multiplatform-js") 3 | id("ks3.conventions.lang.kotlin-multiplatform-jvm") 4 | id("ks3.conventions.lang.kotlin-multiplatform-native") 5 | id("ks3.conventions.publishing.maven-publish") 6 | } 7 | 8 | dokkatoo { 9 | dokkatooSourceSets.configureEach { 10 | includes.from("Module.md") 11 | } 12 | modulePath.set("ks3-test") // match the original dokka default 13 | } 14 | 15 | tasks.withType().configureEach { 16 | generator.dokkaSourceSets.configureEach { 17 | sourceSetScope.set(":ks3-test:dokkaHtmlPartial") 18 | } 19 | } 20 | 21 | kotlin { 22 | targets.configureEach { 23 | compilations.all { 24 | kotlinOptions { 25 | freeCompilerArgs += listOf("-opt-in=kotlinx.serialization.json.internal.JsonFriendModuleApi") 26 | } 27 | } 28 | } 29 | 30 | sourceSets { 31 | commonMain { 32 | dependencies { 33 | api(libs.okio.core) 34 | api(libs.kotlinxSerialization.jsonOkio) 35 | 36 | implementation(projects.ks3Core) 37 | 38 | implementation(libs.kotest.property) 39 | implementation(libs.kotest.frameworkEngine) 40 | 41 | implementation(libs.kotlinxSerialization.core) 42 | implementation(libs.kotlinxSerialization.json) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ks3-test/src/commonMain/kotlin/io.ks3.test/SerializerTestFactory.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.test 2 | 3 | import io.kotest.core.spec.style.funSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.property.Exhaustive 6 | import io.kotest.property.Gen 7 | import io.kotest.property.checkAll 8 | import io.kotest.property.exhaustive.cartesianPairs 9 | import io.kotest.property.exhaustive.exhaustive 10 | import kotlinx.serialization.KSerializer 11 | import kotlinx.serialization.json.Json 12 | 13 | /** 14 | * Creates a test factory for [serializer]. 15 | */ 16 | inline fun generateSerializerTests( 17 | serializer: KSerializer, 18 | generator: Gen, 19 | crossinline nameFn: () -> String = { "Encodes and decodes values back to original form" }, 20 | crossinline assertion: T.(T) -> Unit = { original -> this shouldBe original }, 21 | ) = funSpec { 22 | val (encoders, decoders) = Json.generateEncoders(serializer) 23 | 24 | test(nameFn()) { 25 | checkAll(generator, Exhaustive.cartesianPairs(encoders.exhaustive(), decoders.exhaustive())) { value, (encode, decode) -> 26 | value.encode().decode().assertion(value) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ks3-test/src/commonMain/kotlin/io.ks3.test/TestHelpers.kt: -------------------------------------------------------------------------------- 1 | package io.ks3.test 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.InternalSerializationApi 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | import kotlinx.serialization.json.internal.decodeStringToJsonTree 9 | import kotlinx.serialization.json.internal.readJson 10 | import kotlinx.serialization.json.okio.decodeFromBufferedSource 11 | import kotlinx.serialization.json.okio.encodeToBufferedSink 12 | import okio.Buffer 13 | 14 | typealias Encoder = T.() -> String 15 | typealias Decoder = String.() -> T 16 | 17 | data class Encoders( 18 | val encoders: List>, 19 | val decoders: List>, 20 | ) 21 | 22 | inline fun Json.generateEncoders(serializer: KSerializer) = 23 | Encoders( 24 | encoders(serializer), 25 | decoders(serializer), 26 | ) 27 | 28 | @OptIn(ExperimentalSerializationApi::class) 29 | inline fun Json.encoders(serializer: KSerializer): List> = 30 | listOf( 31 | { encodeToString(serializer, this) }, 32 | { encodeToString(encodeToJsonElement(serializer, this)) }, 33 | { 34 | val buffer = Buffer() 35 | encodeToBufferedSink(serializer, this, buffer) 36 | buffer.readUtf8() 37 | }, 38 | ) 39 | 40 | @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) 41 | inline fun Json.decoders(serializer: KSerializer): List> = 42 | listOf( 43 | { decodeFromString(serializer, this) }, 44 | { 45 | val tree = decodeStringToJsonTree(this@decoders, serializer, this) 46 | readJson(this@decoders, tree, serializer) 47 | }, 48 | { 49 | val buffer = Buffer() 50 | buffer.writeUtf8(this) 51 | decodeFromBufferedSource(serializer, buffer) 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /ks3-test/src/commonMain/resources/kotest.properties: -------------------------------------------------------------------------------- 1 | kotest.framework.classpath.scanning.autoscan.disable=true 2 | kotest.framework.classpath.scanning.config.disable=true 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ks3" 2 | 3 | plugins { 4 | id("com.gradle.enterprise") version "3.19" 5 | } 6 | 7 | apply(from = "build-logic/repositories.gradle.kts") 8 | 9 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 10 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 11 | 12 | includeBuild("build-logic") 13 | 14 | include( 15 | ":ks3-jdk", 16 | ":ks3-standard", 17 | ":ks3-test", 18 | ":ks3-core", 19 | ) 20 | 21 | 22 | develocity { 23 | 24 | buildScan { 25 | val isCI = providers.environmentVariable("CI").orNull.toBoolean() 26 | 27 | tag(if (isCI) "CI" else "local") 28 | tag(providers.systemProperty("os.name").orNull) 29 | tag(providers.systemProperty("os.arch").orNull) 30 | 31 | if (isCI) { 32 | // only automatically enable build scan on CI 33 | termsOfUseUrl = "https://gradle.com/terms-of-service" 34 | termsOfUseAgree = "yes" 35 | uploadInBackground.set(false) 36 | 37 | val ghServer = providers.environmentVariable("GITHUB_SERVER_URL").orNull 38 | val ghRepo = providers.environmentVariable("GITHUB_REPOSITORY").orNull 39 | val giRunId = providers.environmentVariable("GITHUB_RUN_ID").orNull 40 | link("GitHub Workflow run", "$ghServer/$ghRepo/actions/runs/$giRunId") 41 | } 42 | } 43 | } 44 | --------------------------------------------------------------------------------