├── .codecov.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── stale.yml └── workflows │ ├── build.yml │ ├── pr-title-validation.yml │ └── release │ ├── .gitignore │ ├── package-lock.json │ └── package.json ├── .gitignore ├── .releaserc.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RELEASING.md ├── ROADMAP.md ├── SECURITY.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── charleskorn │ └── kaml │ └── build │ ├── ConfigureAssemble.kt │ ├── ConfigurePublishing.kt │ ├── ConfigureTesting.kt │ ├── ConfigureVersioning.kt │ ├── ConfigureWrapper.kt │ ├── SpotlessConfiguration.kt │ └── Utils.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── renovate.json ├── settings.gradle.kts └── src ├── commonMain └── kotlin │ └── com │ └── charleskorn │ └── kaml │ ├── Annotations.kt │ ├── Location.kt │ ├── Yaml.kt │ ├── YamlConfiguration.kt │ ├── YamlContentPolymorphicSerializer.kt │ ├── YamlContextualInput.kt │ ├── YamlException.kt │ ├── YamlInput.kt │ ├── YamlListInput.kt │ ├── YamlMapInput.kt │ ├── YamlMapLikeInputBase.kt │ ├── YamlNamingStrategy.kt │ ├── YamlNode.kt │ ├── YamlNodeReader.kt │ ├── YamlNodeSerializer.kt │ ├── YamlNullInput.kt │ ├── YamlObjectInput.kt │ ├── YamlOutput.kt │ ├── YamlParser.kt │ ├── YamlPath.kt │ ├── YamlPolymorphicInput.kt │ ├── YamlScalarInput.kt │ └── internal │ └── OkioUtils.kt ├── commonTest └── kotlin │ └── com │ └── charleskorn │ └── kaml │ ├── FlatFunSpec.kt │ ├── TestSerializers.kt │ ├── TestUtils.kt │ ├── YamlContentPolymorphicSerializerTest.kt │ ├── YamlExceptionTest.kt │ ├── YamlListTest.kt │ ├── YamlMapTest.kt │ ├── YamlNodeTest.kt │ ├── YamlNullReadingTest.kt │ ├── YamlNullTest.kt │ ├── YamlPathTest.kt │ ├── YamlReadingTest.kt │ ├── YamlScalarReadingTest.kt │ ├── YamlScalarTest.kt │ ├── YamlTaggedNodeTest.kt │ ├── YamlWritingTest.kt │ └── testobjects │ ├── PolymorphicTestObjects.kt │ └── TestObjects.kt ├── jsTest └── kotlin │ └── com │ └── charleskorn │ └── kaml │ └── TestUtils.kt ├── jvmMain └── kotlin │ └── com │ └── charleskorn │ └── kaml │ ├── JvmYamlReading.kt │ └── JvmYamlWriting.kt ├── jvmTest └── kotlin │ └── com │ └── charleskorn │ └── kaml │ ├── JvmYamlReadingTest.kt │ ├── JvmYamlWritingTest.kt │ ├── TestUtils.kt │ └── YamlNodeReaderTest.kt ├── nativeTest └── kotlin │ └── com │ └── charleskorn │ └── kaml │ └── TestUtils.kt └── wasmJsTest └── kotlin └── com └── charleskorn └── kaml └── TestUtils.kt /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5 7 | base: auto 8 | informational: true 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 5 13 | base: auto 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{kt,kts,sh}] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{md,yml,yaml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.bat] 16 | end_of_line = crlf 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report an issue in kaml 3 | labels: 4 | - is:bug 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Describe the bug 9 | description: Describe the problem in a few sentences. Please include exact error messages (if any). 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Reproduction repo 15 | description: While not required, if you can provide a simple sample project that demonstrates the problem, this makes it much easier to understand and investigate the issue and validate that it has been fixed. 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: Steps to reproduce 21 | description: A clear set of steps that triggers the issue. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Expected behaviour 27 | description: A description of what you expected to happen. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Actual behaviour 33 | description: A description of what actually happened. Include any error messages and other surrounding output. 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Version information 39 | description: What version of kaml are you using? (Now is also a good time to double-check you're using the [most recent version](https://github.com/charleskorn/kaml/releases/latest) of kaml.) 40 | render: plain text 41 | validations: 42 | required: true 43 | - type: textarea 44 | attributes: 45 | label: Any other information 46 | description: Is there anything else we need to know? 47 | validations: 48 | required: false 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Questions 5 | about: Have a question about kaml? Discussions is the best place for that. 6 | url: https://github.com/charleskorn/kaml/discussions 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for kaml 3 | labels: 4 | - is:suggestion 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Describe the problem you'd like to solve 9 | description: A clear and concise description of what the problem is. For example, [...] is really time consuming and error prone. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like 15 | description: A clear and concise description of what you want to happen. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Describe alternatives you've considered 21 | description: A clear and concise description of any alternative solutions or features you've considered. 22 | validations: 23 | required: false 24 | - type: textarea 25 | attributes: 26 | label: Additional context 27 | description: Add any other context about the feature request here. 28 | validations: 29 | required: false 30 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - frozen 5 | staleLabel: stale 6 | markComment: > 7 | This issue has been automatically marked as stale because it has not had any activity in the last 60 days. 8 | It will automatically be closed if no further activity occurs in the next seven days to enable maintainers to 9 | focus on the most important issues. 10 | 11 | If this issue is still affecting you, please comment below within the next seven days. 12 | 13 | Thank you for your contributions. 14 | closeComment: > 15 | This issue has been automatically closed because it has not had any recent activity. 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' # Run on all branches, ignore tags. 7 | pull_request: 8 | 9 | env: 10 | GRADLE_OPTS: "-Dorg.gradle.internal.launcher.welcomeMessageEnabled=false" 11 | 12 | concurrency: 13 | cancel-in-progress: true 14 | group: build-${{ github.event.pull_request.number || github.event.after }} 15 | 16 | jobs: 17 | test-matrix: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | tasks: check 24 | - os: windows-latest 25 | tasks: mingwX64Test 26 | - os: macos-13 27 | tasks: macosX64Test iosX64Test tvosX64Test watchosX64Test 28 | - os: macos-latest 29 | tasks: macosArm64Test iosSimulatorArm64Test tvosSimulatorArm64Test watchosSimulatorArm64Test 30 | runs-on: ${{ matrix.os }} 31 | name: Test on ${{ matrix.os }} 32 | steps: 33 | - name: Check out code 34 | uses: actions/checkout@v4.2.2 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Validate Gradle wrapper 39 | uses: gradle/actions/wrapper-validation@v4.4.0 40 | 41 | - name: Set up JDK 42 | uses: actions/setup-java@v4.7.1 43 | with: 44 | java-version: 17 45 | distribution: temurin 46 | 47 | - name: Setup Gradle 48 | uses: gradle/actions/setup-gradle@v4.4.0 49 | 50 | - name: Cache konan dependencies 51 | uses: actions/cache@v4 52 | with: 53 | path: ~/.konan 54 | key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} 55 | restore-keys: | 56 | ${{ runner.os }}-gradle- 57 | 58 | - name: Build 59 | run: ./gradlew ${{ matrix.tasks }} 60 | 61 | publish: 62 | name: "Build and Publish" 63 | needs: 64 | - test-matrix 65 | runs-on: macos-latest 66 | env: 67 | TERM: xterm-256color 68 | GPG_KEY_ID: 6D76AD03 # Run `gpg -K` to get this, take last eight characters 69 | 70 | permissions: 71 | contents: write # Required to be able to publish releases, see https://docs.github.com/en/rest/reference/permissions-required-for-github-apps#permission-on-contents 72 | issues: write 73 | pull-requests: write 74 | 75 | steps: 76 | - name: Check out code 77 | uses: actions/checkout@v4.2.2 78 | with: 79 | fetch-depth: 0 80 | 81 | - name: Validate Gradle wrapper 82 | uses: gradle/actions/wrapper-validation@v4.4.0 83 | 84 | - name: Set up JDK 85 | uses: actions/setup-java@v4.7.1 86 | with: 87 | java-version: 17 88 | distribution: temurin 89 | 90 | - name: Setup Node.js 91 | uses: actions/setup-node@v4.4.0 92 | with: 93 | node-version: 20 94 | 95 | - name: Setup Gradle 96 | uses: gradle/actions/setup-gradle@v4.4.0 97 | 98 | - name: Cache konan dependencies 99 | uses: actions/cache@v4 100 | with: 101 | path: ~/.konan 102 | key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} 103 | restore-keys: | 104 | ${{ runner.os }}-gradle- 105 | 106 | - name: Install release tooling 107 | run: npm --prefix=.github/workflows/release clean-install 108 | 109 | - name: Create release 110 | run: npx --prefix=.github/workflows/release semantic-release 111 | env: 112 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | if: github.repository == 'charleskorn/kaml' && github.ref == 'refs/heads/main' && github.event_name == 'push' 114 | 115 | - name: Build 116 | run: ./gradlew assemble 117 | 118 | - name: Get tag 119 | id: get_tag 120 | run: | 121 | echo "Local changes, if any:" 122 | git status 123 | 124 | if git describe --tags --abbrev=0 --exact-match >/dev/null; then 125 | tag=$(git describe --tags --abbrev=0 --exact-match) 126 | echo "Found tag $tag." 127 | echo "tag=$tag" >> $GITHUB_OUTPUT 128 | else 129 | echo "git describe failed, skipping release:" 130 | git describe --tags --abbrev=0 --exact-match 131 | echo "skip=true" >> $GITHUB_OUTPUT 132 | fi 133 | if: github.repository == 'charleskorn/kaml' && github.ref == 'refs/heads/main' && github.event_name == 'push' 134 | 135 | - name: Assemble release 136 | run: ./gradlew assembleRelease 137 | env: 138 | GPG_KEY_RING: ${{ secrets.GPG_KEY_RING }} # Run `gpg --export-secret-keys "" | base64` to get this 139 | GPG_KEY_PASSPHRASE: ${{ secrets.GPG_KEY_PASSPHRASE }} 140 | if: github.repository == 'charleskorn/kaml' && github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.get_tag.outputs.skip != 'true' 141 | 142 | - name: Publish release 143 | run: ./gradlew publishRelease 144 | env: 145 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 146 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 147 | GPG_KEY_RING: ${{ secrets.GPG_KEY_RING }} # Run `gpg --export-secret-keys "" | base64` to get this 148 | GPG_KEY_PASSPHRASE: ${{ secrets.GPG_KEY_PASSPHRASE }} 149 | if: github.repository == 'charleskorn/kaml' && github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.get_tag.outputs.skip != 'true' 150 | 151 | - name: Add artifacts to GitHub release 152 | uses: softprops/action-gh-release@v2.2.2 153 | with: 154 | tag_name: ${{ steps.get_tag.outputs.tag }} 155 | files: build/release/* 156 | fail_on_unmatched_files: true 157 | draft: false 158 | env: 159 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 160 | if: github.repository == 'charleskorn/kaml' && github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.get_tag.outputs.skip != 'true' 161 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate PR title" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | main: 16 | name: Require PR title in conventional commit format 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | types: | 24 | build 25 | chore 26 | ci 27 | deps 28 | docs 29 | feat 30 | fix 31 | perf 32 | refactor 33 | revert 34 | style 35 | test 36 | -------------------------------------------------------------------------------- /.github/workflows/release/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.github/workflows/release/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "conventional-changelog-conventionalcommits": "^9.0.0", 4 | "semantic-release": "^24.2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | 5 | build/ 6 | !buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ 7 | 8 | # Ignore Gradle GUI config 9 | gradle-app.setting 10 | 11 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 12 | !gradle-wrapper.jar 13 | 14 | # Cache of project 15 | .gradletasknamecache 16 | 17 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 18 | # gradle/wrapper/gradle-wrapper.properties 19 | 20 | ### IntelliJ 21 | /.idea/ 22 | out 23 | *.iml 24 | 25 | **/.kotlin/ 26 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | - main 3 | plugins: 4 | - - "@semantic-release/commit-analyzer" 5 | - preset: conventionalcommits 6 | releaseRules: 7 | - type: feat 8 | release: minor 9 | - type: feature 10 | release: minor 11 | - type: fix 12 | release: patch 13 | - type: perf 14 | release: patch 15 | - type: revert 16 | release: minor 17 | - type: docs 18 | release: patch 19 | - type: style 20 | release: minor 21 | - type: chore 22 | release: minor 23 | - type: refactor 24 | release: patch 25 | - type: deps 26 | release: minor 27 | - type: deps 28 | scope: internal 29 | release: false 30 | - type: test 31 | release: false 32 | - - "@semantic-release/release-notes-generator" 33 | - preset: conventionalcommits 34 | presetConfig: 35 | types: 36 | - type: feat 37 | section: Features 38 | hidden: false 39 | - type: feature 40 | section: Features 41 | hidden: false 42 | - type: fix 43 | section: Bug Fixes 44 | hidden: false 45 | - type: perf 46 | section: Performance Improvements 47 | hidden: false 48 | - type: revert 49 | section: Reverts 50 | hidden: false 51 | - type: docs 52 | section: Documentation 53 | hidden: false 54 | - type: style 55 | section: Styles 56 | hidden: false 57 | - type: chore 58 | section: Chores 59 | hidden: false 60 | - type: deps 61 | section: Dependency Updates 62 | hidden: false 63 | - type: deps 64 | scope: internal 65 | hidden: true 66 | - type: refactor 67 | section: Refactoring 68 | hidden: false 69 | - type: test 70 | hidden: true 71 | - type: build 72 | section: Build System 73 | hidden: false 74 | - type: ci 75 | section: Continuous Integration 76 | hidden: false 77 | - - "@semantic-release/github" 78 | - releasedLabels: false 79 | tagFormat: ${version} 80 | -------------------------------------------------------------------------------- /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 me@charleskorn.com. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kaml 2 | 3 | [![Pipeline](https://github.com/charleskorn/kaml/actions/workflows/build.yml/badge.svg)](https://github.com/charleskorn/kaml/actions/workflows/build.yml) 4 | [![Coverage](https://img.shields.io/codecov/c/github/charleskorn/kaml.svg)](https://codecov.io/gh/charleskorn/kaml) 5 | [![License](https://img.shields.io/github/license/charleskorn/kaml.svg)](https://opensource.org/licenses/Apache-2.0) 6 | [![Maven Central](https://img.shields.io/maven-central/v/com.charleskorn.kaml/kaml.svg?label=maven%20central)](https://search.maven.org/search?q=g:%22com.charleskorn.kaml%22%20AND%20a:%22kaml%22) 7 | 8 | ## What is this? 9 | 10 | This library adds YAML support to [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization/). 11 | 12 | Currently, only Kotlin/JVM is fully supported. 13 | 14 | Kotlin/JS and Kotlin/Wasm support are considered highly experimental. It is not yet fully functional, and may be removed or modified at any time. 15 | 16 | (Follow [this issue](https://github.com/charleskorn/kaml/issues/232) for a discussion of adding support for other targets.) 17 | 18 | YAML version 1.2 is supported. 19 | 20 | ## Usage samples 21 | 22 | ### Parsing from YAML to a Kotlin object 23 | 24 | ```kotlin 25 | @Serializable 26 | data class Team( 27 | val leader: String, 28 | val members: List 29 | ) 30 | 31 | val input = """ 32 | leader: Amy 33 | members: 34 | - Bob 35 | - Cindy 36 | - Dan 37 | """.trimIndent() 38 | 39 | val result = Yaml.default.decodeFromString(Team.serializer(), input) 40 | 41 | println(result) 42 | ``` 43 | 44 | ### Serializing from a Kotlin object to YAML 45 | 46 | ```kotlin 47 | @Serializable 48 | data class Team( 49 | val leader: String, 50 | val members: List 51 | ) 52 | 53 | val input = Team("Amy", listOf("Bob", "Cindy", "Dan")) 54 | 55 | val result = Yaml.default.encodeToString(Team.serializer(), input) 56 | 57 | println(result) 58 | ``` 59 | 60 | ### Parsing into YamlNode 61 | 62 | It is possible to parse a string or an InputStream directly into a YamlNode, for example 63 | the following code prints `Cindy`. 64 | ```kotlin 65 | val input = """ 66 | leader: Amy 67 | members: 68 | - Bob 69 | - Cindy 70 | - Dan 71 | """.trimIndent() 72 | 73 | val result = Yaml.default.parseToYamlNode(input) 74 | 75 | println( 76 | result 77 | .yamlMap.get("members")!![1] 78 | .yamlScalar 79 | .content 80 | ) 81 | ``` 82 | 83 | ## Referencing kaml 84 | 85 | Add the following to your Gradle build script: 86 | 87 | **Groovy DSL** 88 | 89 | ```groovy 90 | plugins { 91 | id 'org.jetbrains.kotlin.jvm' version '1.4.20' 92 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.20' 93 | } 94 | 95 | dependencies { 96 | implementation "com.charleskorn.kaml:kaml:" // Get the latest version number from https://github.com/charleskorn/kaml/releases/latest 97 | } 98 | ``` 99 | 100 | **Kotlin DSL** 101 | 102 | ```kotlin 103 | plugins { 104 | kotlin("jvm") version "1.4.20" 105 | kotlin("plugin.serialization") version "1.4.20" 106 | } 107 | 108 | dependencies { 109 | implementation("com.charleskorn.kaml:kaml:") // Get the latest version number from https://github.com/charleskorn/kaml/releases/latest 110 | } 111 | ``` 112 | 113 | Check the [releases page](https://github.com/charleskorn/kaml/releases) for the latest release information, 114 | and the [Maven Central page](https://search.maven.org/artifact/com.charleskorn.kaml/kaml) for examples of how 115 | to reference the library in other build systems. 116 | 117 | ## Features 118 | 119 | * Supports most major YAML features: 120 | * Scalars, including strings, booleans, integers and floats 121 | * [Sequences (lists)](https://yaml.org/type/seq.html) 122 | * [Maps](https://yaml.org/type/map.html) 123 | * [Nulls](https://yaml.org/type/null.html) 124 | * [Aliases and anchors](https://yaml.org/spec/1.2/spec.html#id2765878), including [merging aliases to form one map](https://yaml.org/type/merge.html) 125 | 126 | * Supports parsing YAML to Kotlin objects (deserializing) and writing Kotlin objects as YAML (serializing) 127 | 128 | * Supports [kotlinx.serialization's polymorphism](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md) for sealed and unsealed types 129 | 130 | Two styles are available (set `YamlConfiguration.polymorphismStyle` when creating an instance of `Yaml`): 131 | 132 | * using [YAML tags](https://yaml.org/spec/1.2/spec.html#id2761292) to specify the type: 133 | 134 | ```yaml 135 | servers: 136 | - ! 137 | hostname: a.mycompany.com 138 | - ! 139 | database: db-1 140 | ``` 141 | 142 | * using a `type` property to specify the type: 143 | 144 | ```yaml 145 | servers: 146 | - type: frontend 147 | hostname: a.mycompany.com 148 | - type: backend 149 | database: db-1 150 | ``` 151 | 152 | The fragments above could be generated with: 153 | 154 | ```kotlin 155 | @Serializable 156 | sealed class Server { 157 | @SerialName("frontend") 158 | @Serializable 159 | data class Frontend(val hostname: String) : Server() 160 | 161 | @SerialName("backend") 162 | @Serializable 163 | data class Backend(val database: String) : Server() 164 | } 165 | 166 | @Serializable 167 | data class Config(val servers: List) 168 | 169 | val config = Config(listOf( 170 | Frontend("a.mycompany.com"), 171 | Backend("db-1") 172 | )) 173 | 174 | val result = Yaml.default.encodeToString(Config.serializer(), config) 175 | 176 | println(result) 177 | ``` 178 | 179 | * Supports [Docker Compose-style extension fields](https://medium.com/@kinghuang/docker-compose-anchors-aliases-extensions-a1e4105d70bd) 180 | 181 | ```yaml 182 | x-common-labels: &common-labels 183 | labels: 184 | owned-by: myteam@mycompany.com 185 | cost-centre: myteam 186 | 187 | servers: 188 | server-a: 189 | <<: *common-labels 190 | kind: frontend 191 | 192 | server-b: 193 | <<: *common-labels 194 | kind: backend 195 | 196 | # server-b and server-c are equivalent 197 | server-c: 198 | labels: 199 | owned-by: myteam@mycompany.com 200 | cost-centre: myteam 201 | kind: backend 202 | ``` 203 | 204 | Specify the extension prefix by setting `YamlConfiguration.extensionDefinitionPrefix` when creating an instance of `Yaml` (eg. `"x-"` for the example above). 205 | 206 | Extensions can only be defined at the top level of a document, and only if the top level element is a map or object. Any key starting with the extension prefix must have an anchor defined (`&...`) and will not be included in the deserialised value. 207 | 208 | ## Contributing to kaml 209 | 210 | Pull requests and bug reports are always welcome! 211 | 212 | kaml uses Gradle for builds and testing: 213 | 214 | * To build the library: `./gradlew assemble` 215 | * To run the tests and static analysis tools: `./gradlew check` 216 | * To run the tests and static analysis tools continuously: `./gradlew --continuous check` 217 | 218 | ## Reference links 219 | 220 | * [YAML 1.2 Specification](http://yaml.org/spec/1.2/spec.html) 221 | * [snakeyaml-engine-kmp](https://github.com/krzema12/snakeyaml-engine-kmp), a Kotlin Multiplatform port of [snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine/wiki/Home), the YAML parser this library is based on 222 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | 1. Commit any remaining changes and push. Wait for Travis build to come back green. 4 | 2. Create Git tag with next version number: `git tag -s ` 5 | 3. Push tag. Travis will automatically create GitHub release with binaries and push them to OSSRH 6 | (which will push them to Maven Central). 7 | 4. Go to GitHub and add release notes / changelog to release. 8 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | * Switch to multiplatform project layout once this supports recent versions of Gradle 2 | * Kotlin/Native support 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the most recent version of kaml is supported with security fixes. Older versions are not supported. 6 | 7 | Sample projects are provided as-is. While I will endeavour to fix any issues reported, they are intended as educational examples, not production-ready code. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover or suspect you have discovered a vulnerability, please report it through [GitHub's security vulnerability reporting tool](https://github.com/charleskorn/kaml/security/advisories). 12 | Please include a short description of the issue and steps on how to reproduce it. 13 | 14 | The issue will be investigated and fixed privately, then disclosed publicly once a fix is available. 15 | 16 | Anyone who reports a vulnerability will be acknowledged in the release notes and security advisory. 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | import com.charleskorn.kaml.build.configureAssemble 20 | import com.charleskorn.kaml.build.configurePublishing 21 | import com.charleskorn.kaml.build.configureSpotless 22 | import com.charleskorn.kaml.build.configureTesting 23 | import com.charleskorn.kaml.build.configureVersioning 24 | import com.charleskorn.kaml.build.configureWrapper 25 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 26 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink 27 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 28 | 29 | plugins { 30 | kotlin("multiplatform") 31 | kotlin("plugin.serialization") 32 | id("io.kotest.multiplatform") version "6.0.0.M3" 33 | } 34 | 35 | group = "com.charleskorn.kaml" 36 | 37 | repositories { 38 | mavenCentral() 39 | } 40 | 41 | kotlin { 42 | explicitApi() 43 | 44 | jvm {} 45 | 46 | js(IR) { 47 | browser() 48 | nodejs() 49 | binaries.executable() 50 | } 51 | 52 | wasmJs { 53 | binaries.library() 54 | browser() 55 | nodejs() 56 | } 57 | 58 | // According to https://kotlinlang.org/docs/native-target-support.html 59 | // Tier 1 60 | macosX64() 61 | macosArm64() 62 | iosSimulatorArm64() 63 | iosX64() 64 | 65 | // Tier 2 66 | linuxX64() 67 | linuxArm64() 68 | iosArm64() 69 | watchosSimulatorArm64() 70 | watchosX64() 71 | watchosArm32() 72 | watchosArm64() 73 | tvosSimulatorArm64() 74 | tvosX64() 75 | tvosArm64() 76 | 77 | // Tier 3 78 | mingwX64() 79 | 80 | sourceSets { 81 | commonMain { 82 | dependencies { 83 | api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3") 84 | implementation("it.krzeminski:snakeyaml-engine-kmp:3.1.1") 85 | implementation("com.squareup.okio:okio:3.12.0") 86 | } 87 | } 88 | 89 | commonTest { 90 | dependencies { 91 | implementation("io.kotest:kotest-assertions-core:6.0.0.M1") 92 | implementation("io.kotest:kotest-framework-api:6.0.0.M1") 93 | implementation("io.kotest:kotest-framework-engine:6.0.0.M1") 94 | // Overriding coroutines' version to solve a problem with WASM JS tests. 95 | // See https://kotlinlang.slack.com/archives/CDFP59223/p1736191408326039?thread_ts=1734964013.996149&cid=CDFP59223 96 | runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") 97 | } 98 | } 99 | 100 | jvmTest { 101 | dependencies { 102 | implementation("io.kotest:kotest-runner-junit5:6.0.0.M1") 103 | } 104 | } 105 | } 106 | } 107 | 108 | tasks.withType().configureEach { 109 | compilerOptions { 110 | jvmTarget.set(JvmTarget.JVM_11) 111 | } 112 | } 113 | 114 | tasks.withType().configureEach { 115 | compilerOptions { 116 | // Catching IndexOutOfBoundsException in Kotlin/Wasm is impossible by default, 117 | // unless we enable "-Xwasm-enable-array-range-checks" compiler flag. 118 | // We rely on it in the tests, see https://github.com/charleskorn/kaml/blob/108b48fb560559f0d0724559bb8c7fff631503f9/src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt#L79 119 | // See https://youtrack.jetbrains.com/issue/KT-59081/ 120 | freeCompilerArgs.add("-Xwasm-enable-array-range-checks") 121 | } 122 | } 123 | 124 | java { 125 | sourceCompatibility = JavaVersion.VERSION_11 126 | targetCompatibility = JavaVersion.VERSION_11 127 | } 128 | 129 | configureAssemble() 130 | configurePublishing() 131 | configureSpotless() 132 | configureTesting() 133 | configureVersioning() 134 | configureWrapper() 135 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 20 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 21 | 22 | plugins { 23 | `kotlin-dsl` 24 | } 25 | 26 | repositories { 27 | maven("https://plugins.gradle.org/m2/") 28 | } 29 | 30 | dependencies { 31 | implementation(group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version = "7.0.4") 32 | implementation(group = "io.github.gradle-nexus", name = "publish-plugin", version = "2.0.0") 33 | implementation(group = "org.ajoberstar.reckon", name = "reckon-gradle", version = "0.19.2") 34 | } 35 | 36 | java { 37 | sourceCompatibility = JavaVersion.VERSION_11 38 | targetCompatibility = JavaVersion.VERSION_11 39 | } 40 | 41 | tasks.withType { 42 | compilerOptions { 43 | jvmTarget = JvmTarget.JVM_11 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ConfigureAssemble.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import org.gradle.api.Project 22 | import org.gradle.api.file.DuplicatesStrategy 23 | import org.gradle.api.publish.PublishingExtension 24 | import org.gradle.api.tasks.Copy 25 | import org.gradle.kotlin.dsl.getByType 26 | import org.gradle.kotlin.dsl.register 27 | 28 | private val TARGETS_WITH_JAR_TASK = setOf("jvm", "js", "wasmJs") 29 | 30 | fun Project.configureAssemble() { 31 | tasks.register("assembleRelease") { 32 | description = "Prepares files for release." 33 | group = "Distribution" 34 | 35 | project.extensions.getByType().publications.names 36 | .filter { it != "kotlinMultiplatform" } 37 | .forEach { publicationName -> 38 | if (publicationName in TARGETS_WITH_JAR_TASK) { 39 | from(tasks.named("${publicationName}Jar")) 40 | } 41 | from(tasks.named("${publicationName}JavadocJar")) 42 | from(tasks.named("${publicationName}SourcesJar")) 43 | 44 | with( 45 | copySpec() 46 | .from(tasks.named("sign${publicationName.capitalize()}Publication")) 47 | .rename { fileName -> 48 | when (fileName) { 49 | "module.json.asc" -> "${project.name}-$publicationName-${project.version}.module.json.asc" 50 | "pom-default.xml.asc" -> "${project.name}-$publicationName-${project.version}.pom.asc" 51 | "${project.name}.klib.asc" -> "${project.name}-$publicationName-${project.version}.klib.asc" 52 | else -> fileName 53 | } 54 | }, 55 | ) 56 | 57 | with( 58 | copySpec() 59 | .from(tasks.named("generatePomFileFor${publicationName.capitalize()}Publication")) 60 | .rename { fileName -> if (fileName == "pom-default.xml") "${project.name}-$publicationName-${project.version}.pom" else fileName }, 61 | ) 62 | 63 | with( 64 | copySpec() 65 | .from(tasks.named("generateMetadataFileFor${publicationName.capitalize()}Publication")) 66 | .rename { fileName -> if (fileName == "module.json") "${project.name}-$publicationName-${project.version}.module.json" else fileName }, 67 | ) 68 | } 69 | 70 | into(layout.buildDirectory.dir("release")) 71 | 72 | duplicatesStrategy = DuplicatesStrategy.FAIL 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ConfigurePublishing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import io.github.gradlenexus.publishplugin.NexusPublishExtension 22 | import io.github.gradlenexus.publishplugin.NexusPublishPlugin 23 | import org.gradle.api.Project 24 | import org.gradle.api.Task 25 | import org.gradle.api.publish.PublishingExtension 26 | import org.gradle.api.publish.maven.MavenPublication 27 | import org.gradle.api.publish.maven.plugins.MavenPublishPlugin 28 | import org.gradle.api.tasks.SourceSetContainer 29 | import org.gradle.api.tasks.TaskProvider 30 | import org.gradle.jvm.tasks.Jar 31 | import org.gradle.kotlin.dsl.apply 32 | import org.gradle.kotlin.dsl.configure 33 | import org.gradle.kotlin.dsl.extra 34 | import org.gradle.kotlin.dsl.getByType 35 | import org.gradle.kotlin.dsl.register 36 | import org.gradle.kotlin.dsl.withType 37 | import org.gradle.plugins.signing.Sign 38 | import org.gradle.plugins.signing.SigningExtension 39 | import org.gradle.plugins.signing.SigningPlugin 40 | import java.nio.file.Files 41 | import java.util.Base64 42 | 43 | fun Project.configurePublishing() { 44 | apply() 45 | apply() 46 | apply() 47 | 48 | val usernameEnvironmentVariableName = "OSSRH_USERNAME" 49 | val passwordEnvironmentVariableName = "OSSRH_PASSWORD" 50 | val repoUsername = System.getenv(usernameEnvironmentVariableName) 51 | val repoPassword = System.getenv(passwordEnvironmentVariableName) 52 | 53 | val validateCredentialsTask = tasks.register("validateMavenRepositoryCredentials") { 54 | doFirst { 55 | if (repoUsername.isNullOrBlank()) { 56 | throw RuntimeException("Environment variable '$usernameEnvironmentVariableName' not set.") 57 | } 58 | 59 | if (repoPassword.isNullOrBlank()) { 60 | throw RuntimeException("Environment variable '$passwordEnvironmentVariableName' not set.") 61 | } 62 | } 63 | } 64 | 65 | createPublishingTasks(repoUsername, repoPassword, validateCredentialsTask) 66 | createSigningTasks() 67 | createReleaseTasks(validateCredentialsTask) 68 | } 69 | 70 | private fun Project.createPublishingTasks(repoUsername: String?, repoPassword: String?, validateCredentialsTask: TaskProvider) { 71 | configure { 72 | publications.withType { 73 | // HACK: this is a workaround while we're waiting to get Dokka set up correctly 74 | // (see https://kotlinlang.slack.com/archives/C0F4UNJET/p1616470404031100?thread_ts=1616198351.029900&cid=C0F4UNJET) 75 | // This creates an empty JavaDoc JAR to make Maven Central happy. 76 | val publicationName = this.name 77 | val javadocTask = tasks.register(this.name + "JavadocJar") { 78 | archiveClassifier.set("javadoc") 79 | archiveBaseName.set("kaml-$publicationName") 80 | } 81 | 82 | artifact(javadocTask) 83 | 84 | pom { 85 | name.set("kaml") 86 | description.set("YAML support for kotlinx.serialization") 87 | url.set("https://github.com/charleskorn/kaml") 88 | 89 | licenses { 90 | license { 91 | name.set("The Apache License, Version 2.0") 92 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 93 | } 94 | } 95 | 96 | developers { 97 | developer { 98 | id.set("charleskorn") 99 | name.set("Charles Korn") 100 | email.set("me@charleskorn.com") 101 | } 102 | } 103 | 104 | scm { 105 | connection.set("scm:git:git://github.com/charleskorn/kaml.git") 106 | developerConnection.set("scm:git:ssh://github.com:charleskorn/kaml.git") 107 | url.set("https://github.com/charleskorn/kaml") 108 | } 109 | } 110 | } 111 | } 112 | 113 | configure { 114 | repositories { 115 | sonatype { 116 | username.set(repoUsername) 117 | password.set(repoPassword) 118 | } 119 | } 120 | 121 | transitionCheckOptions { 122 | maxRetries.set(100) 123 | } 124 | } 125 | 126 | afterEvaluate { 127 | publishing.publications.names.forEach { publication -> 128 | tasks.named("publish${publication.capitalize()}PublicationToSonatypeRepository").configure { 129 | dependsOn(validateCredentialsTask) 130 | } 131 | } 132 | } 133 | } 134 | 135 | private fun Project.createSigningTasks() { 136 | configure { 137 | sign(publishing.publications) 138 | } 139 | 140 | tasks.withType().configureEach { 141 | doFirst { 142 | val keyId = getEnvironmentVariableOrThrow("GPG_KEY_ID") 143 | val keyRing = getEnvironmentVariableOrThrow("GPG_KEY_RING") 144 | val keyPassphrase = getEnvironmentVariableOrThrow("GPG_KEY_PASSPHRASE") 145 | 146 | val keyRingFilePath = Files.createTempFile("kaml-signing", ".gpg") 147 | keyRingFilePath.toFile().deleteOnExit() 148 | 149 | Files.write(keyRingFilePath, Base64.getDecoder().decode(keyRing)) 150 | 151 | project.extra["signing.keyId"] = keyId 152 | project.extra["signing.secretKeyRingFile"] = keyRingFilePath.toString() 153 | project.extra["signing.password"] = keyPassphrase 154 | } 155 | } 156 | } 157 | 158 | private fun Project.createReleaseTasks( 159 | validateCredentialsTask: TaskProvider, 160 | ) { 161 | setOf("closeSonatypeStagingRepository", "releaseSonatypeStagingRepository").forEach { taskName -> 162 | tasks.named(taskName).configure { 163 | dependsOn(validateCredentialsTask) 164 | } 165 | } 166 | 167 | val validateReleaseTask = tasks.register("validateRelease") { 168 | doFirst { 169 | if (version.toString().contains("-")) { 170 | throw RuntimeException("Attempting to publish a release of an untagged commit.") 171 | } 172 | } 173 | } 174 | 175 | tasks.register("publishSnapshot") { 176 | dependsOn("publishAllPublicationsToSonatypeRepository") 177 | } 178 | 179 | tasks.named("closeSonatypeStagingRepository") { 180 | mustRunAfter("publishAllPublicationsToSonatypeRepository") 181 | } 182 | 183 | tasks.register("publishRelease") { 184 | dependsOn(validateReleaseTask) 185 | dependsOn("publishAllPublicationsToSonatypeRepository") 186 | dependsOn("closeAndReleaseSonatypeStagingRepository") 187 | } 188 | } 189 | 190 | private val Project.sourceSets: SourceSetContainer 191 | get() = extensions.getByName("sourceSets") as SourceSetContainer 192 | 193 | private val Project.publishing: PublishingExtension 194 | get() = extensions.getByType() 195 | 196 | private fun getEnvironmentVariableOrThrow(name: String): String = System.getenv().getOrElse(name) { 197 | throw RuntimeException("Environment variable '$name' not set.") 198 | } 199 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ConfigureTesting.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import org.gradle.api.Project 22 | import org.gradle.api.tasks.testing.Test 23 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 24 | import org.gradle.kotlin.dsl.withType 25 | 26 | fun Project.configureTesting() { 27 | tasks.withType { 28 | useJUnitPlatform() 29 | 30 | testLogging { 31 | events("failed") 32 | events("skipped") 33 | events("standard_out") 34 | events("standard_error") 35 | 36 | showExceptions = true 37 | showStackTraces = true 38 | showCauses = true 39 | exceptionFormat = TestExceptionFormat.FULL 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ConfigureVersioning.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import org.ajoberstar.reckon.gradle.ReckonExtension 22 | import org.ajoberstar.reckon.gradle.ReckonPlugin 23 | import org.gradle.api.Project 24 | import org.gradle.kotlin.dsl.apply 25 | import org.gradle.kotlin.dsl.configure 26 | 27 | fun Project.configureVersioning() { 28 | apply() 29 | 30 | configure { 31 | setDefaultInferredScope("patch") 32 | snapshots() 33 | setStageCalc(calcStageFromProp()) 34 | setScopeCalc(calcScopeFromProp()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/ConfigureWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import org.gradle.api.Project 22 | import org.gradle.api.tasks.wrapper.Wrapper 23 | import org.gradle.kotlin.dsl.named 24 | 25 | fun Project.configureWrapper() { 26 | tasks.named("wrapper") { 27 | distributionType = Wrapper.DistributionType.ALL 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/SpotlessConfiguration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | import com.diffplug.gradle.spotless.SpotlessExtension 22 | import com.diffplug.gradle.spotless.SpotlessPlugin 23 | import org.gradle.api.Project 24 | import org.gradle.kotlin.dsl.apply 25 | import org.gradle.kotlin.dsl.configure 26 | 27 | val licenseText = """ 28 | Copyright 2018-2023 Charles Korn. 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | https://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | """ 42 | 43 | val kotlinLicenseHeader = """/* 44 | $licenseText 45 | */ 46 | 47 | """ 48 | 49 | fun Project.configureSpotless() { 50 | apply() 51 | 52 | configure() { 53 | format("misc") { 54 | target( 55 | fileTree( 56 | mapOf( 57 | "dir" to ".", 58 | "include" to listOf("**/*.md", "**/.gitignore", "**/*.yaml", "**/*.yml", "**/*.sh", "**/Dockerfile"), 59 | "exclude" to listOf(".gradle/**", ".gradle-cache/**", "build/**"), 60 | ), 61 | ), 62 | ) 63 | 64 | trimTrailingWhitespace() 65 | leadingTabsToSpaces() 66 | endWithNewline() 67 | } 68 | 69 | kotlinGradle { 70 | target("*.gradle.kts", "gradle/*.gradle.kts", "buildSrc/*.gradle.kts") 71 | ktlint("0.50.0") 72 | 73 | @Suppress("INACCESSIBLE_TYPE") 74 | licenseHeader(kotlinLicenseHeader, "import|tasks|apply|plugins|rootProject") 75 | 76 | trimTrailingWhitespace() 77 | leadingTabsToSpaces() 78 | endWithNewline() 79 | } 80 | 81 | kotlin { 82 | target("src/**/*.kt", "buildSrc/**/*.kt") 83 | ktlint("0.50.0") 84 | 85 | @Suppress("INACCESSIBLE_TYPE") 86 | licenseHeader(kotlinLicenseHeader) 87 | 88 | trimTrailingWhitespace() 89 | leadingTabsToSpaces() 90 | endWithNewline() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/charleskorn/kaml/build/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.build 20 | 21 | fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.daemon.jvmargs=-Xmx1g 3 | org.gradle.jvmargs=-Xmx1g 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charleskorn/kaml/81f4e675d1723d4d0f2fb0667f75163c77efb123/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 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="\\\"\\\"" 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 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "docker:enableMajor", 5 | ":prHourlyLimitNone", 6 | ":prConcurrentLimitNone", 7 | ":switchToGradleLite", 8 | ":disableDependencyDashboard", 9 | ":semanticCommits", 10 | ":semanticCommitTypeAll(deps)", 11 | ":semanticCommitScopeDisabled" 12 | ], 13 | "reviewers": [ 14 | "charleskorn" 15 | ], 16 | "automerge": true, 17 | "labels": [ 18 | "is:dependency-update" 19 | ], 20 | "rebaseWhen": "behind-base-branch", 21 | "digest": { 22 | "enabled": false 23 | }, 24 | "packageRules": [ 25 | { 26 | "groupName": "Kotest", 27 | "matchPackagePatterns": [ 28 | "^io\\.kotest:", 29 | "^io\\.kotest\\." 30 | ], 31 | "matchDatasources": [ 32 | "maven" 33 | ], 34 | "semanticCommitScope": "internal" 35 | }, 36 | { 37 | "groupName": "Kotlin", 38 | "matchManagers": [ 39 | "gradle-lite" 40 | ], 41 | "matchPackagePatterns": [ 42 | "^org\\.jetbrains\\.kotlin\\." 43 | ], 44 | "matchDepTypes": [ 45 | "plugin" 46 | ] 47 | }, 48 | { 49 | "matchDatasources": [ 50 | "docker" 51 | ], 52 | "matchPackageNames": [ 53 | "openjdk" 54 | ], 55 | "versioning": "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)(-\\d+)?-(?.*)$" 56 | }, 57 | { 58 | "groupName": "Spotless", 59 | "matchManagers": [ 60 | "gradle-lite" 61 | ], 62 | "matchPackagePatterns": [ 63 | "^com\\.diffplug\\.spotless$", 64 | "^com\\.diffplug\\.spotless:", 65 | "^com\\.pinterest:ktlint$" 66 | ], 67 | "semanticCommitScope": "internal" 68 | }, 69 | { 70 | "matchPaths": [ 71 | ".github/workflows/**/*", 72 | ".github/workflows/*.yml", 73 | "+(batect)", 74 | "+(batect.cmd)", 75 | "+(batect.yml)" 76 | ], 77 | "semanticCommitScope": "internal" 78 | }, 79 | { 80 | "matchManagers": [ 81 | "github-actions", 82 | "gradle-wrapper" 83 | ], 84 | "semanticCommitScope": "internal" 85 | } 86 | ], 87 | "regexManagers": [ 88 | { 89 | "fileMatch": [ 90 | ".groovy$", 91 | ".gradle$", 92 | ".gradle.kts$", 93 | ".kt$" 94 | ], 95 | "matchStrings": [ 96 | "ktlint\\(\"(?[\\d.]*?)\"\\)" 97 | ], 98 | "datasourceTemplate": "maven", 99 | "depNameTemplate": "com.pinterest:ktlint" 100 | } 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | rootProject.name = "kaml" 20 | 21 | pluginManagement { 22 | plugins { 23 | kotlin("multiplatform") version "2.1.21" 24 | kotlin("plugin.serialization") version "2.1.21" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/Annotations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.SerialInfo 23 | 24 | /** 25 | * Adds a comment block before property on serialization 26 | * @property lines comment lines to add 27 | */ 28 | @OptIn(ExperimentalSerializationApi::class) 29 | @Target(AnnotationTarget.PROPERTY) 30 | @Retention(AnnotationRetention.BINARY) 31 | @SerialInfo 32 | public annotation class YamlComment( 33 | vararg val lines: String, 34 | ) 35 | 36 | /** 37 | * Write a String value if it is a single line in the specified ScalarStyle. 38 | * This overrides the value specified in the [YamlConfiguration]. 39 | */ 40 | @OptIn(ExperimentalSerializationApi::class) 41 | @Target(AnnotationTarget.PROPERTY) 42 | @Retention(AnnotationRetention.BINARY) 43 | @SerialInfo 44 | public annotation class YamlSingleLineStringStyle( 45 | val singleLineStringStyle: SingleLineStringStyle, 46 | ) 47 | 48 | /** 49 | * Write a String value if it is a multiline in the specified ScalarStyle. 50 | * This overrides the value specified in the [YamlConfiguration]. 51 | */ 52 | @OptIn(ExperimentalSerializationApi::class) 53 | @Target(AnnotationTarget.PROPERTY) 54 | @Retention(AnnotationRetention.BINARY) 55 | @SerialInfo 56 | public annotation class YamlMultiLineStringStyle( 57 | val multiLineStringStyle: MultiLineStringStyle, 58 | ) 59 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/Location.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | public data class Location(val line: Int, val column: Int) 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/Yaml.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import com.charleskorn.kaml.internal.bufferedSource 22 | import it.krzeminski.snakeyaml.engine.kmp.api.StreamDataWriter 23 | import kotlinx.serialization.DeserializationStrategy 24 | import kotlinx.serialization.SerializationStrategy 25 | import kotlinx.serialization.StringFormat 26 | import kotlinx.serialization.modules.EmptySerializersModule 27 | import kotlinx.serialization.modules.SerializersModule 28 | import kotlinx.serialization.serializer 29 | import okio.Buffer 30 | import okio.BufferedSink 31 | import okio.Sink 32 | import okio.Source 33 | import okio.buffer 34 | 35 | public class Yaml( 36 | override val serializersModule: SerializersModule = EmptySerializersModule(), 37 | public val configuration: YamlConfiguration = YamlConfiguration(), 38 | ) : StringFormat { 39 | 40 | public companion object { 41 | public val default: Yaml = Yaml() 42 | } 43 | 44 | public inline fun decodeFromYamlNode(node: YamlNode): T = 45 | decodeFromYamlNode(serializersModule.serializer(), node) 46 | 47 | public fun decodeFromYamlNode( 48 | deserializer: DeserializationStrategy, 49 | node: YamlNode, 50 | ): T { 51 | val input = YamlInput.createFor(node, this, serializersModule, configuration, deserializer.descriptor) 52 | return input.decodeSerializableValue(deserializer) 53 | } 54 | 55 | override fun decodeFromString( 56 | deserializer: DeserializationStrategy, 57 | string: String, 58 | ): T { 59 | return decodeFromSource(deserializer, string.bufferedSource()) 60 | } 61 | 62 | public inline fun decodeFromSource(source: Source): T = 63 | decodeFromSource(serializersModule.serializer(), source) 64 | 65 | public fun decodeFromSource( 66 | deserializer: DeserializationStrategy, 67 | source: Source, 68 | ): T { 69 | val rootNode = parseToYamlNode(source) 70 | 71 | val input = YamlInput.createFor(rootNode, this, serializersModule, configuration, deserializer.descriptor) 72 | return input.decodeSerializableValue(deserializer) 73 | } 74 | 75 | public fun parseToYamlNode(string: String): YamlNode = 76 | parseToYamlNode(string.bufferedSource()) 77 | 78 | internal fun parseToYamlNode(source: Source): YamlNode { 79 | val parser = YamlParser(source, configuration.codePointLimit) 80 | val reader = YamlNodeReader( 81 | parser, 82 | configuration.extensionDefinitionPrefix, 83 | configuration.anchorsAndAliases.maxAliasCount, 84 | ) 85 | val node = reader.read() 86 | parser.ensureEndOfStreamReached() 87 | return node 88 | } 89 | 90 | public inline fun encodeToSink(value: T, sink: Sink): Unit = 91 | encodeToSink(serializersModule.serializer(), value, sink) 92 | 93 | public fun encodeToSink( 94 | serializer: SerializationStrategy, 95 | value: T, 96 | sink: Sink, 97 | ) { 98 | encodeToBufferedSink(serializer, value, sink.buffer()) 99 | } 100 | 101 | override fun encodeToString( 102 | serializer: SerializationStrategy, 103 | value: T, 104 | ): String { 105 | val buffer = Buffer() 106 | encodeToBufferedSink(serializer, value, buffer) 107 | return buffer.readUtf8().trimEnd() 108 | } 109 | 110 | public inline fun encodeToBufferedSink(value: T, sink: BufferedSink): Unit = 111 | encodeToBufferedSink(serializersModule.serializer(), value, sink) 112 | 113 | @PublishedApi 114 | internal fun encodeToBufferedSink( 115 | serializer: SerializationStrategy, 116 | value: T, 117 | sink: BufferedSink, 118 | ) { 119 | BufferedSinkDataWriter(sink).use { writer -> 120 | YamlOutput(writer, serializersModule, configuration).use { output -> 121 | output.encodeSerializableValue(serializer, value) 122 | } 123 | } 124 | } 125 | } 126 | 127 | private class BufferedSinkDataWriter( 128 | val sink: BufferedSink, 129 | ) : StreamDataWriter, AutoCloseable { 130 | override fun flush(): Unit = sink.flush() 131 | 132 | override fun write(str: String) { 133 | sink.writeUtf8(str) 134 | } 135 | 136 | override fun write(str: String, off: Int, len: Int) { 137 | sink.writeUtf8(string = str, beginIndex = off, endIndex = off + len) 138 | } 139 | 140 | override fun close() { 141 | flush() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | 23 | /** 24 | * Configuration options for parsing YAML to objects and serialising objects to YAML. 25 | * 26 | * * [encodeDefaults]: set to `false` to not write default property values to YAML (defaults to `true`) 27 | * * [strictMode]: set to true to throw an exception when reading an object that has an unknown property, or false to ignore unknown properties (defaults to `true`) 28 | * * [extensionDefinitionPrefix]: prefix used on root-level keys (where document root is an object) to define extensions that can later be merged (defaults to `null`, which disables extensions altogether). See https://batect.dev/docs/reference/config#anchors-aliases-extensions-and-merging for example. 29 | * * [polymorphismStyle]: how to read or write the type of a polymorphic object: 30 | * * [PolymorphismStyle.Tag]: use a YAML tag (eg. `! { property: value }`) 31 | * * [PolymorphismStyle.Property]: use a property (eg. `{ type: typeOfThing, property: value }`) 32 | * * [polymorphismPropertyName]: property name to use when [polymorphismStyle] is [PolymorphismStyle.Property] 33 | * * [encodingIndentationSize]: number of spaces to use as indentation when encoding objects as YAML 34 | * * [breakScalarsAt]: maximum length of scalars when encoding objects as YAML (scalars exceeding this length will be split into multiple lines) 35 | * * [sequenceStyle]: how sequences (aka lists and arrays) should be formatted. See [SequenceStyle] for an example of each 36 | * * [singleLineStringStyle]: the style in which a single line String value is written. Can be overruled for a specific field with the [YamlSingleLineStringStyle] annotation. 37 | * * [multiLineStringStyle]: the style in which a multi line String value is written. Can be overruled for a specific field with the [YamlMultiLineStringStyle] annotation. 38 | * * [ambiguousQuoteStyle]: how strings should be escaped when [singleLineStringStyle] is [SingleLineStringStyle.PlainExceptAmbiguous] and the value is ambiguous 39 | * * [sequenceBlockIndent]: number of spaces to use as indentation for sequences, if [sequenceStyle] set to [SequenceStyle.Block] 40 | * * [anchorsAndAliases]: set to [AnchorsAndAliases.Permitted] to allow anchors and aliases when decoding YAML (defaults to [AnchorsAndAliases.Forbidden]) 41 | * * [yamlNamingStrategy]: The system that converts the field names in to the names used in the Yaml. 42 | * * [codePointLimit]: the maximum amount of code points allowed in the input YAML document (defaults to 3 MB) 43 | * * [decodeEnumCaseInsensitive]: set to true to allow case-insensitive decoding of enums (defaults to `false`) 44 | */ 45 | public data class YamlConfiguration( 46 | internal val encodeDefaults: Boolean = true, 47 | internal val strictMode: Boolean = true, 48 | internal val extensionDefinitionPrefix: String? = null, 49 | internal val polymorphismStyle: PolymorphismStyle = PolymorphismStyle.Tag, 50 | internal val polymorphismPropertyName: String = "type", 51 | internal val encodingIndentationSize: Int = 2, 52 | internal val breakScalarsAt: Int = 80, 53 | internal val sequenceStyle: SequenceStyle = SequenceStyle.Block, 54 | internal val singleLineStringStyle: SingleLineStringStyle = SingleLineStringStyle.DoubleQuoted, 55 | internal val multiLineStringStyle: MultiLineStringStyle = singleLineStringStyle.multiLineStringStyle, 56 | internal val ambiguousQuoteStyle: AmbiguousQuoteStyle = AmbiguousQuoteStyle.DoubleQuoted, 57 | internal val sequenceBlockIndent: Int = 0, 58 | internal val anchorsAndAliases: AnchorsAndAliases = AnchorsAndAliases.Forbidden, 59 | internal val yamlNamingStrategy: YamlNamingStrategy? = null, 60 | internal val codePointLimit: Int? = null, 61 | @ExperimentalSerializationApi 62 | internal val decodeEnumCaseInsensitive: Boolean = false, 63 | ) 64 | 65 | public enum class PolymorphismStyle { 66 | Tag, 67 | Property, 68 | None, 69 | } 70 | 71 | public enum class SequenceStyle { 72 | /** 73 | * The block form, eg 74 | * ```yaml 75 | * - 1 76 | * - 2 77 | * - 3 78 | * ``` 79 | */ 80 | Block, 81 | 82 | /** 83 | * The flow form, eg 84 | * ```yaml 85 | * [1, 2, 3] 86 | * ``` 87 | */ 88 | Flow, 89 | } 90 | 91 | public enum class MultiLineStringStyle { 92 | Literal, 93 | Folded, 94 | DoubleQuoted, 95 | SingleQuoted, 96 | Plain, 97 | } 98 | 99 | public enum class SingleLineStringStyle { 100 | DoubleQuoted, 101 | SingleQuoted, 102 | Plain, 103 | 104 | /** 105 | * This is the same as [SingleLineStringStyle.Plain], except strings that could be misinterpreted as other types 106 | * will be quoted with the escape style defined in [AmbiguousQuoteStyle]. 107 | * 108 | * For example, the strings "True", "0xAB", "1" and "1.2" would all be quoted, 109 | * while "1.2.3" and "abc" would not be quoted. 110 | */ 111 | PlainExceptAmbiguous, 112 | ; 113 | 114 | public val multiLineStringStyle: MultiLineStringStyle 115 | get() = when (this) { 116 | DoubleQuoted -> MultiLineStringStyle.DoubleQuoted 117 | SingleQuoted -> MultiLineStringStyle.SingleQuoted 118 | Plain -> MultiLineStringStyle.Plain 119 | PlainExceptAmbiguous -> MultiLineStringStyle.Plain 120 | } 121 | } 122 | 123 | public enum class AmbiguousQuoteStyle { 124 | DoubleQuoted, 125 | SingleQuoted, 126 | } 127 | 128 | public sealed class AnchorsAndAliases { 129 | internal abstract val maxAliasCount: UInt? 130 | 131 | public data object Forbidden : AnchorsAndAliases() { 132 | override val maxAliasCount: UInt = 0u 133 | } 134 | 135 | /** 136 | * [maxAliasCount]: the maximum amount of aliases allowed in the input YAML document if allowed at all, `null` allows any amount (defaults to `100`) 137 | */ 138 | public data class Permitted(override val maxAliasCount: UInt? = 100u) : AnchorsAndAliases() 139 | } 140 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | import kotlinx.serialization.InternalSerializationApi 24 | import kotlinx.serialization.KSerializer 25 | import kotlinx.serialization.SerialInfo 26 | import kotlinx.serialization.SerializationException 27 | import kotlinx.serialization.descriptors.PolymorphicKind 28 | import kotlinx.serialization.descriptors.SerialDescriptor 29 | import kotlinx.serialization.descriptors.buildSerialDescriptor 30 | import kotlinx.serialization.encoding.Decoder 31 | import kotlinx.serialization.encoding.Encoder 32 | import kotlinx.serialization.serializerOrNull 33 | import kotlin.reflect.KClass 34 | 35 | @OptIn(ExperimentalSerializationApi::class) 36 | public abstract class YamlContentPolymorphicSerializer(private val baseClass: KClass) : KSerializer { 37 | @OptIn(InternalSerializationApi::class) 38 | override val descriptor: SerialDescriptor = buildSerialDescriptor( 39 | "${YamlContentPolymorphicSerializer::class.simpleName}<${baseClass.simpleName}>", 40 | PolymorphicKind.SEALED, 41 | ) { 42 | annotations += Marker() 43 | } 44 | 45 | @SerialInfo 46 | internal annotation class Marker 47 | 48 | @OptIn(InternalSerializationApi::class) 49 | override fun serialize(encoder: Encoder, value: T) { 50 | val actualSerializer = encoder.serializersModule.getPolymorphic(baseClass, value) 51 | ?: value::class.serializerOrNull() 52 | ?: throwSubtypeNotRegistered(value::class, baseClass) 53 | @Suppress("UNCHECKED_CAST") 54 | (actualSerializer as KSerializer).serialize(encoder, value) 55 | } 56 | 57 | override fun deserialize(decoder: Decoder): T { 58 | return decoder.decodeSerializableValue(selectDeserializer((decoder as YamlInput).node)) 59 | } 60 | 61 | public abstract fun selectDeserializer(node: YamlNode): DeserializationStrategy 62 | 63 | private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing { 64 | val subClassName = subClass.simpleName ?: "$subClass" 65 | throw SerializationException( 66 | """ 67 | Class '$subClassName' is not registered for polymorphic serialization in the scope of '${baseClass.simpleName}'. 68 | Mark the base class as 'sealed' or register the serializer explicitly. 69 | """.trimIndent(), 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlContextualInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.descriptors.SerialDescriptor 22 | import kotlinx.serialization.encoding.CompositeDecoder 23 | import kotlinx.serialization.modules.SerializersModule 24 | 25 | internal class YamlContextualInput(node: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(node, yaml, context, configuration) { 26 | override fun decodeString(): String = delegateToYamlScalarInput { decodeString() } 27 | override fun decodeInt(): Int = delegateToYamlScalarInput { decodeInt() } 28 | override fun decodeLong(): Long = delegateToYamlScalarInput { decodeLong() } 29 | override fun decodeShort(): Short = delegateToYamlScalarInput { decodeShort() } 30 | override fun decodeByte(): Byte = delegateToYamlScalarInput { decodeByte() } 31 | override fun decodeDouble(): Double = delegateToYamlScalarInput { decodeDouble() } 32 | override fun decodeFloat(): Float = delegateToYamlScalarInput { decodeFloat() } 33 | override fun decodeBoolean(): Boolean = delegateToYamlScalarInput { decodeBoolean() } 34 | override fun decodeChar(): Char = delegateToYamlScalarInput { decodeChar() } 35 | override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = delegateToYamlScalarInput { decodeEnum(enumDescriptor) } 36 | 37 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int = throw IllegalStateException("Must call beginStructure() and use returned Decoder") 38 | 39 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = 40 | createFor(node, yaml, serializersModule, configuration, descriptor) 41 | 42 | override fun getCurrentLocation(): Location = node.location 43 | override fun getCurrentPath(): YamlPath = node.path 44 | 45 | private inline fun delegateToYamlScalarInput(block: YamlScalarInput.() -> T): T { 46 | return when (node) { 47 | is YamlScalar -> YamlScalarInput(node, yaml, serializersModule, configuration).block() 48 | is YamlNull -> throw UnexpectedNullValueException(node.path) 49 | else -> throw IllegalStateException("Must call beginStructure() and use returned Decoder") 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.SerializationException 22 | 23 | public open class YamlException( 24 | override val message: String, 25 | public val path: YamlPath, 26 | override val cause: Throwable? = null, 27 | ) : SerializationException(message, cause) { 28 | public val location: Location = path.endLocation 29 | public val line: Int = location.line 30 | public val column: Int = location.column 31 | 32 | override fun toString(): String = "${this::class.simpleName} at ${path.toHumanReadableString()} on line $line, column $column: $message" 33 | } 34 | 35 | public class DuplicateKeyException( 36 | public val originalPath: YamlPath, 37 | public val duplicatePath: YamlPath, 38 | public val key: String, 39 | ) : 40 | YamlException("Duplicate key $key. It was previously given at line ${originalPath.endLocation.line}, column ${originalPath.endLocation.column}.", duplicatePath) { 41 | 42 | public val originalLocation: Location = originalPath.endLocation 43 | public val duplicateLocation: Location = duplicatePath.endLocation 44 | } 45 | 46 | public class EmptyYamlDocumentException(message: String, path: YamlPath) : YamlException(message, path) 47 | 48 | public class InvalidPropertyValueException( 49 | public val propertyName: String, 50 | public val reason: String, 51 | path: YamlPath, 52 | cause: Throwable? = null, 53 | ) : YamlException("Value for '$propertyName' is invalid: $reason", path, cause) 54 | 55 | public class MalformedYamlException(message: String, path: YamlPath) : YamlException(message, path) 56 | 57 | public class UnexpectedNullValueException(path: YamlPath) : YamlException("Unexpected null or empty value for non-null field.", path) 58 | 59 | public class MissingRequiredPropertyException( 60 | public val propertyName: String, 61 | path: YamlPath, 62 | cause: Throwable? = null, 63 | ) : 64 | YamlException("Property '$propertyName' is required but it is missing.", path, cause) 65 | 66 | public class UnknownPropertyException( 67 | public val propertyName: String, 68 | public val validPropertyNames: Set, 69 | path: YamlPath, 70 | ) : 71 | YamlException("Unknown property '$propertyName'. Known properties are: ${validPropertyNames.sorted().joinToString(", ")}", path) 72 | 73 | public class UnknownPolymorphicTypeException( 74 | public val typeName: String, 75 | public val validTypeNames: Set, 76 | path: YamlPath, 77 | cause: Throwable? = null, 78 | ) : 79 | YamlException("Unknown type '$typeName'. Known types are: ${validTypeNames.sorted().joinToString(", ")}", path, cause) 80 | 81 | public class YamlScalarFormatException( 82 | message: String, 83 | path: YamlPath, 84 | public val originalValue: String, 85 | ) : YamlException(message, path) 86 | 87 | public open class IncorrectTypeException(message: String, path: YamlPath) : YamlException(message, path) 88 | 89 | public class MissingTypeTagException(path: YamlPath) : 90 | IncorrectTypeException("Value is missing a type tag (eg. !)", path) 91 | 92 | public class UnknownAnchorException(public val anchorName: String, path: YamlPath) : 93 | YamlException("Unknown anchor '$anchorName'.", path) 94 | 95 | public class NoAnchorForExtensionException( 96 | public val key: String, 97 | public val extensionDefinitionPrefix: String, 98 | path: YamlPath, 99 | ) : 100 | YamlException("The key '$key' starts with the extension definition prefix '$extensionDefinitionPrefix' but does not define an anchor.", path) 101 | 102 | public class ForbiddenAnchorOrAliasException(message: String, path: YamlPath) : YamlException(message, path) 103 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | import kotlinx.serialization.SerializationException 24 | import kotlinx.serialization.descriptors.PolymorphicKind 25 | import kotlinx.serialization.descriptors.PrimitiveKind 26 | import kotlinx.serialization.descriptors.SerialDescriptor 27 | import kotlinx.serialization.descriptors.SerialKind 28 | import kotlinx.serialization.descriptors.StructureKind 29 | import kotlinx.serialization.descriptors.getContextualDescriptor 30 | import kotlinx.serialization.encoding.AbstractDecoder 31 | import kotlinx.serialization.modules.SerializersModule 32 | 33 | @OptIn(ExperimentalSerializationApi::class) 34 | public sealed class YamlInput( 35 | public val node: YamlNode, 36 | public val yaml: Yaml, 37 | override var serializersModule: SerializersModule, 38 | public val configuration: YamlConfiguration, 39 | ) : AbstractDecoder() { 40 | internal companion object { 41 | private val missingFieldExceptionMessage: Regex = """^Field '(.*)' is required for type with serial name '.*', but it was missing$""".toRegex() 42 | 43 | internal fun createFor(node: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration, descriptor: SerialDescriptor): YamlInput = when (node) { 44 | is YamlNull -> when { 45 | descriptor.kind is PolymorphicKind && !descriptor.isNullable -> throw MissingTypeTagException(node.path) 46 | else -> YamlNullInput(node, yaml, context, configuration) 47 | } 48 | 49 | is YamlScalar -> when { 50 | descriptor.kind is PrimitiveKind || descriptor.kind is SerialKind.ENUM || descriptor.isInline -> YamlScalarInput(node, yaml, context, configuration) 51 | descriptor.kind is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) 52 | descriptor.kind is PolymorphicKind -> { 53 | if (descriptor.isContentBasedPolymorphic) { 54 | createContextual(node, yaml, context, configuration, descriptor) 55 | } else { 56 | throw MissingTypeTagException(node.path) 57 | } 58 | } 59 | else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a scalar value", node.path) 60 | } 61 | 62 | is YamlList -> when (descriptor.kind) { 63 | is StructureKind.LIST -> YamlListInput(node, yaml, context, configuration) 64 | is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) 65 | is PolymorphicKind -> { 66 | if (descriptor.isContentBasedPolymorphic) { 67 | createContextual(node, yaml, context, configuration, descriptor) 68 | } else { 69 | throw MissingTypeTagException(node.path) 70 | } 71 | } 72 | else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a list", node.path) 73 | } 74 | 75 | is YamlMap -> when (descriptor.kind) { 76 | is StructureKind.CLASS, StructureKind.OBJECT -> YamlObjectInput(node, yaml, context, configuration) 77 | is StructureKind.MAP -> YamlMapInput(node, yaml, context, configuration) 78 | is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) 79 | is PolymorphicKind -> { 80 | if (descriptor.isContentBasedPolymorphic) { 81 | createContextual(node, yaml, context, configuration, descriptor) 82 | } else { 83 | when (configuration.polymorphismStyle) { 84 | PolymorphismStyle.None -> 85 | throw IncorrectTypeException("Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'", node.path) 86 | 87 | PolymorphismStyle.Tag -> throw MissingTypeTagException(node.path) 88 | PolymorphismStyle.Property -> createPolymorphicMapDeserializer(node, yaml, context, configuration) 89 | } 90 | } 91 | } 92 | else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a map", node.path) 93 | } 94 | 95 | is YamlTaggedNode -> when { 96 | descriptor.kind is PolymorphicKind && configuration.polymorphismStyle == PolymorphismStyle.None -> { 97 | throw IncorrectTypeException("Encountered a tagged polymorphic descriptor but PolymorphismStyle is 'None'", node.path) 98 | } 99 | descriptor.kind is PolymorphicKind && configuration.polymorphismStyle == PolymorphismStyle.Tag -> { 100 | YamlPolymorphicInput(node.tag, node.path, node.innerNode, yaml, context, configuration) 101 | } 102 | else -> createFor(node.innerNode, yaml, context, configuration, descriptor) 103 | } 104 | } 105 | 106 | private fun createContextual( 107 | node: YamlNode, 108 | yaml: Yaml, 109 | context: SerializersModule, 110 | configuration: YamlConfiguration, 111 | descriptor: SerialDescriptor, 112 | ): YamlInput = context.getContextualDescriptor(descriptor) 113 | ?.let { createFor(node, yaml, context, configuration, it) } 114 | ?: YamlContextualInput(node, yaml, context, configuration) 115 | 116 | private fun createPolymorphicMapDeserializer(node: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration): YamlPolymorphicInput { 117 | val desiredKey = configuration.polymorphismPropertyName 118 | when (val typeName = node.getValue(desiredKey)) { 119 | is YamlList -> throw InvalidPropertyValueException(desiredKey, "expected a string, but got a list", typeName.path) 120 | is YamlMap -> throw InvalidPropertyValueException(desiredKey, "expected a string, but got a map", typeName.path) 121 | is YamlNull -> throw InvalidPropertyValueException(desiredKey, "expected a string, but got a null value", typeName.path) 122 | is YamlTaggedNode -> throw InvalidPropertyValueException(desiredKey, "expected a string, but got a tagged value", typeName.path) 123 | is YamlScalar -> { 124 | val remainingProperties = node.withoutKey(desiredKey) 125 | 126 | return YamlPolymorphicInput(typeName.content, typeName.path, remainingProperties, yaml, context, configuration) 127 | } 128 | } 129 | } 130 | 131 | private fun YamlMap.getValue(desiredKey: String): YamlNode { 132 | return this.get(desiredKey) ?: throw MissingRequiredPropertyException(desiredKey, this.path) 133 | } 134 | 135 | private fun YamlMap.withoutKey(key: String): YamlMap { 136 | return this.copy(entries = entries.filterKeys { it.content != key }) 137 | } 138 | 139 | private val SerialDescriptor.isContentBasedPolymorphic get() = annotations.any { it is YamlContentPolymorphicSerializer.Marker } 140 | } 141 | 142 | override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { 143 | try { 144 | return super.decodeSerializableValue(deserializer) 145 | } catch (e: SerializationException) { 146 | throwIfMissingRequiredPropertyException(e) 147 | 148 | throw e 149 | } 150 | } 151 | 152 | private fun throwIfMissingRequiredPropertyException(e: SerializationException) { 153 | val match = missingFieldExceptionMessage.matchEntire(e.message!!) ?: return 154 | 155 | throw MissingRequiredPropertyException(match.groupValues[1], node.path, e) 156 | } 157 | 158 | public abstract fun getCurrentLocation(): Location 159 | public abstract fun getCurrentPath(): YamlPath 160 | } 161 | 162 | @OptIn(ExperimentalSerializationApi::class) 163 | private val SerialKind.friendlyDescription: String 164 | get() { 165 | return when (this) { 166 | is StructureKind.MAP -> "a map" 167 | is StructureKind.CLASS -> "an object" 168 | is StructureKind.OBJECT -> "an object" 169 | is StructureKind.LIST -> "a list" 170 | is PrimitiveKind.STRING -> "a string" 171 | is PrimitiveKind.BOOLEAN -> "a boolean" 172 | is PrimitiveKind.BYTE -> "a byte" 173 | is PrimitiveKind.CHAR -> "a character" 174 | is PrimitiveKind.DOUBLE -> "a double" 175 | is PrimitiveKind.FLOAT -> "a float" 176 | is PrimitiveKind.INT -> "an integer" 177 | is PrimitiveKind.SHORT -> "a short" 178 | is PrimitiveKind.LONG -> "a long" 179 | is SerialKind.ENUM -> "an enumeration value" 180 | else -> "a $this value" 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | import kotlinx.serialization.descriptors.SerialDescriptor 24 | import kotlinx.serialization.encoding.CompositeDecoder 25 | import kotlinx.serialization.modules.SerializersModule 26 | 27 | @OptIn(ExperimentalSerializationApi::class) 28 | internal class YamlListInput(val list: YamlList, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(list, yaml, context, configuration) { 29 | private var nextElementIndex = 0 30 | private lateinit var currentElementDecoder: YamlInput 31 | 32 | override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = list.items.size 33 | 34 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int { 35 | if (nextElementIndex == list.items.size) { 36 | return CompositeDecoder.DECODE_DONE 37 | } 38 | 39 | currentElementDecoder = createFor( 40 | list.items[nextElementIndex], 41 | yaml, 42 | serializersModule, 43 | configuration, 44 | descriptor.getElementDescriptor(0), 45 | ) 46 | 47 | return nextElementIndex++ 48 | } 49 | 50 | override fun decodeNotNullMark(): Boolean { 51 | if (!haveStartedReadingElements) { 52 | return true 53 | } 54 | 55 | return currentElementDecoder.decodeNotNullMark() 56 | } 57 | 58 | override fun decodeString(): String = currentElementDecoder.decodeString() 59 | override fun decodeInt(): Int = currentElementDecoder.decodeInt() 60 | override fun decodeLong(): Long = currentElementDecoder.decodeLong() 61 | override fun decodeShort(): Short = currentElementDecoder.decodeShort() 62 | override fun decodeByte(): Byte = currentElementDecoder.decodeByte() 63 | override fun decodeDouble(): Double = currentElementDecoder.decodeDouble() 64 | override fun decodeFloat(): Float = currentElementDecoder.decodeFloat() 65 | override fun decodeBoolean(): Boolean = currentElementDecoder.decodeBoolean() 66 | override fun decodeChar(): Char = currentElementDecoder.decodeChar() 67 | override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = currentElementDecoder.decodeEnum(enumDescriptor) 68 | 69 | override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { 70 | if (!haveStartedReadingElements) { 71 | return super.decodeSerializableValue(deserializer) 72 | } 73 | return currentElementDecoder.decodeSerializableValue(deserializer) 74 | } 75 | 76 | private val haveStartedReadingElements: Boolean 77 | get() = nextElementIndex > 0 78 | 79 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { 80 | if (haveStartedReadingElements) { 81 | return currentElementDecoder 82 | } 83 | 84 | return super.beginStructure(descriptor) 85 | } 86 | 87 | override fun getCurrentPath(): YamlPath { 88 | return if (haveStartedReadingElements) { 89 | currentElementDecoder.node.path 90 | } else { 91 | list.path 92 | } 93 | } 94 | 95 | override fun getCurrentLocation(): Location = getCurrentPath().endLocation 96 | } 97 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlMapInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.descriptors.SerialDescriptor 23 | import kotlinx.serialization.encoding.CompositeDecoder 24 | import kotlinx.serialization.modules.SerializersModule 25 | 26 | @OptIn(ExperimentalSerializationApi::class) 27 | internal class YamlMapInput(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, yaml, context, configuration) { 28 | private val entriesList = map.entries.entries.toList() 29 | private var nextIndex = 0 30 | private lateinit var currentEntry: Map.Entry 31 | 32 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int { 33 | if (nextIndex == entriesList.size * 2) { 34 | return CompositeDecoder.DECODE_DONE 35 | } 36 | 37 | val entryIndex = nextIndex / 2 38 | currentEntry = entriesList[entryIndex] 39 | currentKey = currentEntry.key 40 | currentlyReadingValue = nextIndex % 2 != 0 41 | 42 | currentValueDecoder = when (currentlyReadingValue) { 43 | true -> 44 | try { 45 | createFor(currentEntry.value, yaml, serializersModule, configuration, descriptor.getElementDescriptor(1)) 46 | } catch (e: IncorrectTypeException) { 47 | throw InvalidPropertyValueException(propertyName, e.message, e.path, e) 48 | } 49 | 50 | false -> createFor(currentKey, yaml, serializersModule, configuration, descriptor.getElementDescriptor(0)) 51 | } 52 | 53 | return nextIndex++ 54 | } 55 | 56 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { 57 | if (haveStartedReadingEntries) { 58 | return fromCurrentValue { beginStructure(descriptor) } 59 | } 60 | 61 | return super.beginStructure(descriptor) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlMapLikeInputBase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.descriptors.SerialDescriptor 23 | import kotlinx.serialization.modules.SerializersModule 24 | 25 | internal sealed class YamlMapLikeInputBase(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(map, yaml, context, configuration) { 26 | protected lateinit var currentValueDecoder: YamlInput 27 | protected lateinit var currentKey: YamlScalar 28 | protected var currentlyReadingValue = false 29 | 30 | override fun decodeNotNullMark(): Boolean { 31 | if (!haveStartedReadingEntries) { 32 | return true 33 | } 34 | 35 | return fromCurrentValue { decodeNotNullMark() } 36 | } 37 | 38 | override fun decodeString(): String = fromCurrentValue { decodeString() } 39 | override fun decodeInt(): Int = fromCurrentValue { decodeInt() } 40 | override fun decodeLong(): Long = fromCurrentValue { decodeLong() } 41 | override fun decodeShort(): Short = fromCurrentValue { decodeShort() } 42 | override fun decodeByte(): Byte = fromCurrentValue { decodeByte() } 43 | override fun decodeDouble(): Double = fromCurrentValue { decodeDouble() } 44 | override fun decodeFloat(): Float = fromCurrentValue { decodeFloat() } 45 | override fun decodeBoolean(): Boolean = fromCurrentValue { decodeBoolean() } 46 | override fun decodeChar(): Char = fromCurrentValue { decodeChar() } 47 | override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = fromCurrentValue { decodeEnum(enumDescriptor) } 48 | 49 | override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { 50 | if (!haveStartedReadingEntries) { 51 | return super.decodeSerializableValue(deserializer) 52 | } 53 | return fromCurrentValue { 54 | decodeSerializableValue(deserializer) 55 | } 56 | } 57 | 58 | protected fun fromCurrentValue(action: YamlInput.() -> T): T { 59 | try { 60 | return action(currentValueDecoder) 61 | } catch (e: YamlException) { 62 | if (currentlyReadingValue) { 63 | throw InvalidPropertyValueException(propertyName, e.message, e.path, e) 64 | } else { 65 | throw e 66 | } 67 | } 68 | } 69 | 70 | protected val haveStartedReadingEntries: Boolean 71 | get() = this::currentValueDecoder.isInitialized 72 | 73 | override fun getCurrentPath(): YamlPath { 74 | return if (haveStartedReadingEntries) { 75 | currentValueDecoder.node.path 76 | } else { 77 | node.path 78 | } 79 | } 80 | 81 | override fun getCurrentLocation(): Location = getCurrentPath().endLocation 82 | 83 | protected val propertyName: String 84 | get() = currentKey.content 85 | } 86 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlNamingStrategy.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | public fun interface YamlNamingStrategy { 22 | public fun serialNameForYaml(serialName: String): String 23 | 24 | public companion object Builtins { 25 | /** 26 | * A [YamlNamingStrategy] that converts property names to snake_case (lowercase words separated by underscores). 27 | */ 28 | public val SnakeCase: YamlNamingStrategy = object : YamlNamingStrategy { 29 | override fun serialNameForYaml(serialName: String): String = serialName.toDelimitedCase('_') 30 | 31 | override fun toString(): String = "com.charleskorn.kaml.YamlNamingStrategy.SnakeCase" 32 | } 33 | 34 | /** 35 | * A [YamlNamingStrategy] that converts property names to kebab-case (lowercase words separated by dashes). 36 | */ 37 | public val KebabCase: YamlNamingStrategy = object : YamlNamingStrategy { 38 | override fun serialNameForYaml(serialName: String): String = serialName.toDelimitedCase('-') 39 | 40 | override fun toString(): String = "com.charleskorn.kaml.YamlNamingStrategy.KebabCase" 41 | } 42 | 43 | /** 44 | * A [YamlNamingStrategy] that converts property names to PascalCase (capitalized words concatenated together). 45 | */ 46 | public val PascalCase: YamlNamingStrategy = object : YamlNamingStrategy { 47 | override fun serialNameForYaml(serialName: String): String = serialName 48 | .split(Regex("[^a-zA-Z0-9]+")) 49 | .joinToString("") { it.replaceFirstChar(Char::titlecaseChar) } 50 | 51 | override fun toString(): String = "com.charleskorn.kaml.YamlNamingStrategy.PascalCase" 52 | } 53 | 54 | /** 55 | * A [YamlNamingStrategy] that converts property names to camelCase (like [PascalCase] but with the first letter lowercase). 56 | */ 57 | public val CamelCase: YamlNamingStrategy = object : YamlNamingStrategy { 58 | override fun serialNameForYaml(serialName: String): String = PascalCase 59 | .serialNameForYaml(serialName) 60 | .replaceFirstChar(Char::lowercaseChar) 61 | 62 | override fun toString(): String = "com.charleskorn.kaml.YamlNamingStrategy.CamelCase" 63 | } 64 | 65 | private fun String.toDelimitedCase(delimiter: Char): String = buildString(length * 2) { 66 | var bufferedChar: Char? = null 67 | var previousCaseCharsCount = 0 68 | 69 | for (character in this@toDelimitedCase) { 70 | if (character.isUpperCase()) { 71 | if (previousCaseCharsCount == 0 && isNotEmpty() && last() != delimiter) { 72 | append(delimiter) 73 | } 74 | 75 | bufferedChar?.let(::append) 76 | 77 | previousCaseCharsCount++ 78 | bufferedChar = character.lowercaseChar() 79 | } else { 80 | if (bufferedChar != null) { 81 | if (previousCaseCharsCount > 1 && character.isLetter()) { 82 | append(delimiter) 83 | } 84 | append(bufferedChar) 85 | previousCaseCharsCount = 0 86 | bufferedChar = null 87 | } 88 | append(character) 89 | } 90 | } 91 | 92 | if (bufferedChar != null) { 93 | append(bufferedChar) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import it.krzeminski.snakeyaml.engine.kmp.common.Anchor 22 | import it.krzeminski.snakeyaml.engine.kmp.events.AliasEvent 23 | import it.krzeminski.snakeyaml.engine.kmp.events.Event 24 | import it.krzeminski.snakeyaml.engine.kmp.events.MappingStartEvent 25 | import it.krzeminski.snakeyaml.engine.kmp.events.NodeEvent 26 | import it.krzeminski.snakeyaml.engine.kmp.events.ScalarEvent 27 | import it.krzeminski.snakeyaml.engine.kmp.events.SequenceStartEvent 28 | 29 | internal class YamlNodeReader( 30 | private val parser: YamlParser, 31 | private val extensionDefinitionPrefix: String? = null, 32 | private val maxAliasCount: UInt? = 0u, 33 | ) { 34 | private val aliases = mutableMapOf() 35 | private var aliasCount = 0u 36 | 37 | fun read(): YamlNode = readNode(YamlPath.root).node 38 | 39 | private fun readNode(path: YamlPath): WeightedNode = readNodeAndAnchor(path).first 40 | 41 | private fun readNodeAndAnchor(path: YamlPath): Pair { 42 | val event = parser.consumeEvent(path) 43 | val (node, weight) = readFromEvent(event, path) 44 | 45 | if (event is NodeEvent) { 46 | if (event !is AliasEvent) { 47 | event.anchor?.let { 48 | if (maxAliasCount == 0u) { 49 | throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path) 50 | } 51 | 52 | val anchor = node.withPath(YamlPath.forAliasDefinition(it.value, event.location)) 53 | aliases[it] = WeightedNode(anchor, weight) 54 | } 55 | } 56 | 57 | return WeightedNode(node, weight) to event.anchor 58 | } 59 | 60 | return WeightedNode(node, weight = 0u) to null 61 | } 62 | 63 | private fun readFromEvent(event: Event, path: YamlPath): WeightedNode = when (event) { 64 | is ScalarEvent -> WeightedNode(readScalarOrNull(event, path).maybeToTaggedNode(event.tag), weight = 0u) 65 | is SequenceStartEvent -> readSequence(path).let { it.copy(node = it.node.maybeToTaggedNode(event.tag)) } 66 | is MappingStartEvent -> readMapping(path).let { it.copy(node = it.node.maybeToTaggedNode(event.tag)) } 67 | is AliasEvent -> readAlias(event, path) 68 | else -> throw MalformedYamlException("Unexpected ${event.eventId}", path.withError(event.location)) 69 | } 70 | 71 | private fun readScalarOrNull(event: ScalarEvent, path: YamlPath): YamlNode { 72 | if ((event.value == "null" || event.value == "" || event.value == "~") && event.plain) { 73 | return YamlNull(path) 74 | } else { 75 | return YamlScalar(event.value, path) 76 | } 77 | } 78 | 79 | private fun readSequence(path: YamlPath): WeightedNode { 80 | val items = mutableListOf() 81 | var sequenceWeight = 0u 82 | 83 | while (true) { 84 | val event = parser.peekEvent(path) 85 | 86 | when (event.eventId) { 87 | Event.ID.SequenceEnd -> { 88 | parser.consumeEventOfType(Event.ID.SequenceEnd, path) 89 | return WeightedNode(YamlList(items, path), sequenceWeight) 90 | } 91 | 92 | else -> { 93 | val (node, weight) = readNode(path.withListEntry(items.size, event.location)) 94 | sequenceWeight += weight 95 | items += node 96 | } 97 | } 98 | } 99 | } 100 | 101 | private fun readMapping(path: YamlPath): WeightedNode { 102 | val items = mutableMapOf() 103 | var mapWeight = 0u 104 | 105 | while (true) { 106 | val event = parser.peekEvent(path) 107 | 108 | when (event.eventId) { 109 | Event.ID.MappingEnd -> { 110 | parser.consumeEventOfType(Event.ID.MappingEnd, path) 111 | return WeightedNode(YamlMap(doMerges(items), path), mapWeight) 112 | } 113 | 114 | else -> { 115 | val keyLocation = parser.peekEvent(path).location 116 | val key = readMapKey(path) 117 | val keyNode = YamlScalar(key, path.withMapElementKey(key, keyLocation)) 118 | 119 | val valueLocation = parser.peekEvent(keyNode.path).location 120 | val valuePath = if (isMerge(keyNode)) path.withMerge(valueLocation) else keyNode.path.withMapElementValue(valueLocation) 121 | val (weightedNode, anchor) = readNodeAndAnchor(valuePath) 122 | mapWeight += weightedNode.weight 123 | 124 | if (path == YamlPath.root && extensionDefinitionPrefix != null && key.startsWith(extensionDefinitionPrefix)) { 125 | if (anchor == null) { 126 | throw NoAnchorForExtensionException(key, extensionDefinitionPrefix, path.withError(event.location)) 127 | } 128 | } else { 129 | items += (keyNode to weightedNode.node) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | private fun readMapKey(path: YamlPath): String { 137 | val event = parser.peekEvent(path) 138 | 139 | when (event.eventId) { 140 | Event.ID.Scalar -> { 141 | parser.consumeEventOfType(Event.ID.Scalar, path) 142 | val scalarEvent = event as ScalarEvent 143 | val isNullKey = (scalarEvent.value == "null" || scalarEvent.value == "~") && scalarEvent.plain 144 | 145 | if (scalarEvent.tag != null || isNullKey) { 146 | throw nonScalarMapKeyException(path, event) 147 | } 148 | 149 | return scalarEvent.value 150 | } 151 | else -> throw nonScalarMapKeyException(path, event) 152 | } 153 | } 154 | 155 | private fun nonScalarMapKeyException(path: YamlPath, event: Event) = MalformedYamlException("Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)", path.withError(event.location)) 156 | 157 | private fun YamlNode.maybeToTaggedNode(tag: String?): YamlNode = 158 | tag?.let { YamlTaggedNode(it, this) } ?: this 159 | 160 | private fun doMerges(items: Map): Map { 161 | val mergeEntries = items.entries.filter { (key, _) -> isMerge(key) } 162 | 163 | when (mergeEntries.count()) { 164 | 0 -> return items 165 | 1 -> when (val mappingsToMerge = mergeEntries.single().value) { 166 | is YamlList -> return doMerges(items, mappingsToMerge.items) 167 | else -> return doMerges(items, listOf(mappingsToMerge)) 168 | } 169 | else -> throw MalformedYamlException("Cannot perform multiple '<<' merges into a map. Instead, combine all merges into a single '<<' entry.", mergeEntries.second().key.path) 170 | } 171 | } 172 | 173 | private fun isMerge(key: YamlNode): Boolean = key is YamlScalar && key.content == "<<" 174 | 175 | private fun doMerges(original: Map, others: List): Map { 176 | val merged = mutableMapOf() 177 | 178 | original 179 | .filterNot { (key, _) -> isMerge(key) } 180 | .forEach { (key, value) -> merged.put(key, value) } 181 | 182 | others 183 | .forEach { other -> 184 | when (other) { 185 | is YamlNull -> throw MalformedYamlException("Cannot merge a null value into a map.", other.path) 186 | is YamlScalar -> throw MalformedYamlException("Cannot merge a scalar value into a map.", other.path) 187 | is YamlList -> throw MalformedYamlException("Cannot merge a list value into a map.", other.path) 188 | is YamlTaggedNode -> throw MalformedYamlException("Cannot merge a tagged value into a map.", other.path) 189 | is YamlMap -> 190 | other.entries.forEach { (key, value) -> 191 | val existingEntry = merged.entries.singleOrNull { it.key.equivalentContentTo(key) } 192 | 193 | if (existingEntry == null) { 194 | merged.put(key, value) 195 | } 196 | } 197 | } 198 | } 199 | 200 | return merged 201 | } 202 | 203 | private fun readAlias(event: AliasEvent, path: YamlPath): WeightedNode { 204 | if (maxAliasCount == 0u) { 205 | throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path) 206 | } 207 | 208 | val anchor = event.alias 209 | 210 | val (resolvedNode, resolvedNodeWeight) = aliases.getOrElse(anchor) { 211 | throw UnknownAnchorException(anchor.value, path.withError(event.location)) 212 | } 213 | 214 | val resultWeight = resolvedNodeWeight + 1u 215 | aliasCount += resultWeight 216 | 217 | if ((maxAliasCount != null) && (aliasCount > maxAliasCount)) { 218 | throw ForbiddenAnchorOrAliasException( 219 | "Maximum number of aliases has been reached.", 220 | path, 221 | ) 222 | } 223 | 224 | return WeightedNode( 225 | node = resolvedNode.withPath( 226 | path.withAliasReference(anchor.value, event.location) 227 | .withAliasDefinition(anchor.value, resolvedNode.location), 228 | ), 229 | weight = resultWeight, 230 | ) 231 | } 232 | 233 | private fun Iterable.second(): T = this.drop(1).first() 234 | 235 | private val Event.location: Location 236 | get() = Location(startMark!!.line + 1, startMark!!.column + 1) 237 | } 238 | 239 | private data class WeightedNode( 240 | val node: YamlNode, 241 | val weight: UInt, 242 | ) 243 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlNodeSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | @file:OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) 20 | 21 | package com.charleskorn.kaml 22 | 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | import kotlinx.serialization.InternalSerializationApi 25 | import kotlinx.serialization.KSerializer 26 | import kotlinx.serialization.builtins.ListSerializer 27 | import kotlinx.serialization.builtins.MapSerializer 28 | import kotlinx.serialization.builtins.serializer 29 | import kotlinx.serialization.descriptors.PolymorphicKind 30 | import kotlinx.serialization.descriptors.PrimitiveKind 31 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 32 | import kotlinx.serialization.descriptors.SerialDescriptor 33 | import kotlinx.serialization.descriptors.SerialKind 34 | import kotlinx.serialization.descriptors.buildSerialDescriptor 35 | import kotlinx.serialization.descriptors.nullable 36 | import kotlinx.serialization.encoding.Decoder 37 | import kotlinx.serialization.encoding.Encoder 38 | import kotlinx.serialization.encoding.encodeStructure 39 | 40 | internal object YamlNodeSerializer : KSerializer { 41 | override val descriptor: SerialDescriptor = 42 | buildSerialDescriptor("com.charleskorn.kaml.YamlNode", PolymorphicKind.SEALED) { 43 | annotations += YamlContentPolymorphicSerializer.Marker() 44 | }.nullable 45 | 46 | override fun serialize(encoder: Encoder, value: YamlNode) { 47 | encoder.asYamlOutput() 48 | when (value) { 49 | is YamlList -> encoder.encodeSerializableValue(YamlListSerializer, value) 50 | is YamlMap -> encoder.encodeSerializableValue(YamlMapSerializer, value) 51 | is YamlNull -> encoder.encodeSerializableValue(YamlNullSerializer, value) 52 | is YamlScalar -> encoder.encodeSerializableValue(YamlScalarSerializer, value) 53 | is YamlTaggedNode -> encoder.encodeSerializableValue(YamlTaggedNodeSerializer, value) 54 | } 55 | } 56 | 57 | override fun deserialize(decoder: Decoder): YamlNode { 58 | val input = decoder.asYamlInput() 59 | return if (input is YamlPolymorphicInput) YamlTaggedNode(input.typeName, input.node) else input.node 60 | } 61 | } 62 | 63 | internal object YamlScalarSerializer : KSerializer { 64 | override val descriptor: SerialDescriptor = 65 | PrimitiveSerialDescriptor("com.charleskorn.kaml.YamlScalar", PrimitiveKind.STRING) 66 | 67 | override fun serialize(encoder: Encoder, value: YamlScalar) { 68 | encoder.asYamlOutput() 69 | value.toBooleanOrNull()?.also { return encoder.encodeBoolean(it) } 70 | value.toLongOrNull()?.also { return encoder.encodeLong(it) } 71 | value.toDoubleOrNull()?.also { return encoder.encodeDouble(it) } 72 | value.toCharOrNull()?.also { return encoder.encodeChar(it) } 73 | encoder.encodeString(value.content) 74 | } 75 | 76 | override fun deserialize(decoder: Decoder): YamlScalar { 77 | val result = decoder.asYamlInput() 78 | return result.scalar 79 | } 80 | } 81 | 82 | @OptIn(ExperimentalSerializationApi::class) 83 | internal object YamlNullSerializer : KSerializer { 84 | override val descriptor: SerialDescriptor = buildSerialDescriptor("com.charleskorn.kaml.YamlNull", SerialKind.ENUM) 85 | 86 | override fun serialize(encoder: Encoder, value: YamlNull) { 87 | encoder.asYamlOutput().encodeNull() 88 | } 89 | 90 | override fun deserialize(decoder: Decoder): YamlNull { 91 | val input = decoder.asYamlInput() 92 | return input.nullValue 93 | } 94 | } 95 | 96 | internal object YamlTaggedNodeSerializer : KSerializer { 97 | 98 | override val descriptor: SerialDescriptor = 99 | buildSerialDescriptor("com.charleskorn.kaml.YamlTaggedNode", PolymorphicKind.OPEN) { 100 | element("tag", String.serializer().descriptor) 101 | element("node", YamlNodeSerializer.descriptor) 102 | } 103 | 104 | override fun serialize(encoder: Encoder, value: YamlTaggedNode) { 105 | encoder.asYamlOutput().encodeStructure(descriptor) { 106 | encodeStringElement(descriptor, 0, value.tag) 107 | encodeSerializableElement(descriptor, 1, YamlNodeSerializer, value.innerNode) 108 | } 109 | } 110 | 111 | override fun deserialize(decoder: Decoder): YamlTaggedNode { 112 | val input = decoder.asYamlInput() 113 | return YamlTaggedNode(input.typeName, input.contentNode) 114 | } 115 | } 116 | 117 | internal object YamlMapSerializer : KSerializer { 118 | 119 | private object YamlMapDescriptor : 120 | SerialDescriptor by MapSerializer(YamlScalarSerializer, YamlNodeSerializer).descriptor { 121 | override val serialName: String = "com.charleskorn.kaml.YamlMap" 122 | } 123 | 124 | override val descriptor: SerialDescriptor = YamlMapDescriptor 125 | 126 | override fun serialize(encoder: Encoder, value: YamlMap) { 127 | encoder.asYamlOutput() 128 | MapSerializer(YamlScalarSerializer, YamlNodeSerializer).serialize(encoder, value.entries) 129 | } 130 | 131 | override fun deserialize(decoder: Decoder): YamlMap { 132 | val input = decoder.asYamlInput() 133 | return input.node as YamlMap 134 | } 135 | } 136 | 137 | internal object YamlListSerializer : KSerializer { 138 | 139 | private object YamlListDescriptor : SerialDescriptor by ListSerializer(YamlNodeSerializer).descriptor { 140 | override val serialName: String = "com.charleskorn.kaml.YamlList" 141 | } 142 | 143 | override val descriptor: SerialDescriptor = YamlListDescriptor 144 | 145 | override fun serialize(encoder: Encoder, value: YamlList) { 146 | encoder.asYamlOutput() 147 | ListSerializer(YamlNodeSerializer).serialize(encoder, value.items) 148 | } 149 | 150 | override fun deserialize(decoder: Decoder): YamlList { 151 | val input = decoder.asYamlInput() 152 | return input.list 153 | } 154 | } 155 | 156 | private inline fun Decoder.asYamlInput(): I = checkNotNull(this as? I) { 157 | "This serializer can be used only with Yaml format. Expected Decoder to be ${I::class.simpleName}, got ${this::class}" 158 | } 159 | 160 | private fun Encoder.asYamlOutput() = checkNotNull(this as? YamlOutput) { 161 | "This serializer can be used only with Yaml format. Expected Encoder to be YamlOutput, got ${this::class}" 162 | } 163 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlNullInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.descriptors.SerialDescriptor 22 | import kotlinx.serialization.encoding.CompositeDecoder 23 | import kotlinx.serialization.modules.SerializersModule 24 | 25 | internal class YamlNullInput(val nullValue: YamlNull, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(nullValue, yaml, context, configuration) { 26 | override fun decodeNotNullMark(): Boolean = false 27 | 28 | override fun decodeValue(): Any = throw UnexpectedNullValueException(nullValue.path) 29 | override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = throw UnexpectedNullValueException(nullValue.path) 30 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = throw UnexpectedNullValueException( 31 | nullValue.path, 32 | ) 33 | 34 | override fun getCurrentLocation(): Location = nullValue.location 35 | override fun getCurrentPath(): YamlPath = nullValue.path 36 | 37 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int = CompositeDecoder.DECODE_DONE 38 | } 39 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlObjectInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.descriptors.SerialDescriptor 23 | import kotlinx.serialization.encoding.CompositeDecoder 24 | import kotlinx.serialization.modules.SerializersModule 25 | 26 | @OptIn(ExperimentalSerializationApi::class) 27 | internal class YamlObjectInput(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, yaml, context, configuration) { 28 | private val entriesList = map.entries.entries.toList() 29 | private var nextIndex = 0 30 | private lateinit var pairedPropertyNames: Map 31 | 32 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int { 33 | if (!::pairedPropertyNames.isInitialized) { 34 | pairedPropertyNames = (0 until descriptor.elementsCount).associateBy { index -> 35 | val elementName = descriptor.getElementName(index) 36 | configuration.yamlNamingStrategy?.serialNameForYaml(elementName) ?: elementName 37 | } 38 | } 39 | 40 | while (true) { 41 | if (nextIndex == entriesList.size) { 42 | return CompositeDecoder.DECODE_DONE 43 | } 44 | 45 | val currentEntry = entriesList[nextIndex] 46 | currentKey = currentEntry.key 47 | 48 | val fieldDescriptorIndex = pairedPropertyNames[propertyName] ?: CompositeDecoder.UNKNOWN_NAME 49 | 50 | if (fieldDescriptorIndex == CompositeDecoder.UNKNOWN_NAME) { 51 | if (configuration.strictMode) { 52 | throw UnknownPropertyException( 53 | propertyName, 54 | pairedPropertyNames.keys, 55 | currentKey.path, 56 | ) 57 | } else { 58 | nextIndex++ 59 | continue 60 | } 61 | } 62 | 63 | try { 64 | currentValueDecoder = createFor( 65 | entriesList[nextIndex].value, 66 | yaml, 67 | serializersModule, 68 | configuration, 69 | descriptor.getElementDescriptor(fieldDescriptorIndex), 70 | ) 71 | } catch (e: IncorrectTypeException) { 72 | throw InvalidPropertyValueException(propertyName, e.message, e.path, e) 73 | } 74 | 75 | currentlyReadingValue = true 76 | nextIndex++ 77 | 78 | return fieldDescriptorIndex 79 | } 80 | } 81 | 82 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { 83 | if (haveStartedReadingEntries) { 84 | return fromCurrentValue { beginStructure(descriptor) } 85 | } 86 | 87 | return super.beginStructure(descriptor) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import com.charleskorn.kaml.internal.bufferedSource 22 | import it.krzeminski.snakeyaml.engine.kmp.api.LoadSettings 23 | import it.krzeminski.snakeyaml.engine.kmp.events.Event 24 | import it.krzeminski.snakeyaml.engine.kmp.exceptions.MarkedYamlEngineException 25 | import it.krzeminski.snakeyaml.engine.kmp.parser.ParserImpl 26 | import it.krzeminski.snakeyaml.engine.kmp.scanner.StreamReader 27 | import okio.Source 28 | 29 | internal class YamlParser(reader: Source, codePointLimit: Int? = null) { 30 | internal constructor(source: String) : this(source.bufferedSource()) 31 | 32 | private val dummyFileName = "DUMMY_FILE_NAME" 33 | private val loadSettings = LoadSettings.builder().apply { 34 | if (codePointLimit != null) setCodePointLimit(codePointLimit) 35 | setLabel(dummyFileName) 36 | }.build() 37 | private val streamReader = StreamReader(loadSettings, reader) 38 | private val events = ParserImpl(loadSettings, streamReader) 39 | 40 | init { 41 | consumeEventOfType(Event.ID.StreamStart, YamlPath.root) 42 | 43 | if (peekEvent(YamlPath.root).eventId == Event.ID.StreamEnd) { 44 | throw EmptyYamlDocumentException("The YAML document is empty.", YamlPath.root) 45 | } 46 | 47 | consumeEventOfType(Event.ID.DocumentStart, YamlPath.root) 48 | } 49 | 50 | fun ensureEndOfStreamReached() { 51 | consumeEventOfType(Event.ID.DocumentEnd, YamlPath.root) 52 | consumeEventOfType(Event.ID.StreamEnd, YamlPath.root) 53 | } 54 | 55 | fun consumeEvent(path: YamlPath): Event = checkEvent(path) { events.next() } 56 | fun peekEvent(path: YamlPath): Event = checkEvent(path) { events.peekEvent() } 57 | 58 | fun consumeEventOfType(type: Event.ID, path: YamlPath) { 59 | val event = consumeEvent(path) 60 | 61 | if (event.eventId != type) { 62 | throw MalformedYamlException( 63 | "Unexpected ${event.eventId}, expected $type", 64 | path.withError(Location(event.startMark!!.line, event.startMark!!.column)), 65 | ) 66 | } 67 | } 68 | 69 | private fun checkEvent(path: YamlPath, retrieve: () -> Event): Event { 70 | try { 71 | return retrieve() 72 | } catch (e: MarkedYamlEngineException) { 73 | throw translateYamlEngineException(e, path) 74 | } 75 | } 76 | 77 | private fun translateYamlEngineException(e: MarkedYamlEngineException, path: YamlPath): MalformedYamlException { 78 | val updatedMessage = StringBuilder() 79 | 80 | val context = e.context 81 | val contextMark = e.contextMark 82 | 83 | if (context != null && contextMark != null) { 84 | val snippet = contextMark.createSnippet(4, Int.MAX_VALUE) 85 | updatedMessage.append( 86 | """ 87 | |$context 88 | | at line ${contextMark.line + 1}, column ${contextMark.column + 1}: 89 | |$snippet 90 | | 91 | """.trimMargin(), 92 | ) 93 | } 94 | 95 | val problemMark = e.problemMark 96 | if (problemMark != null) { 97 | val problem = translateYamlEngineExceptionMessage(e.problem) 98 | val snippet = problemMark.createSnippet(4, Int.MAX_VALUE) 99 | updatedMessage.append( 100 | """ 101 | |$problem 102 | | at line ${problemMark.line + 1}, column ${problemMark.column + 1}: 103 | |$snippet 104 | """.trimMargin(), 105 | ) 106 | } 107 | 108 | val updatedPath = 109 | if (problemMark != null) { 110 | path.withError(Location(problemMark.line + 1, problemMark.column + 1)) 111 | } else { 112 | path 113 | } 114 | 115 | return MalformedYamlException( 116 | message = updatedMessage.toString(), 117 | path = updatedPath, 118 | ) 119 | } 120 | 121 | private fun translateYamlEngineExceptionMessage(message: String): String = when (message) { 122 | "mapping values are not allowed here", 123 | "expected , but found ''", 124 | "expected , but found ''", 125 | -> 126 | "$message (is the indentation level of this line or a line nearby incorrect?)" 127 | else -> message 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlPath.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | public data class YamlPath(val segments: List) { 22 | public constructor(vararg segments: YamlPathSegment) : this(segments.toList()) 23 | 24 | init { 25 | if (segments.isEmpty()) { 26 | throw IllegalArgumentException("Path must contain at least one segment.") 27 | } 28 | 29 | if (segments.first() !is YamlPathSegment.Root && segments.first() !is YamlPathSegment.AliasDefinition) { 30 | throw IllegalArgumentException("First element of path must be root segment or alias definition.") 31 | } 32 | 33 | if (segments.drop(1).contains(YamlPathSegment.Root)) { 34 | throw IllegalArgumentException("Root segment can only be first element of path.") 35 | } 36 | } 37 | 38 | val endLocation: Location = segments.last().location 39 | 40 | public fun withError(location: Location): YamlPath = withSegment(YamlPathSegment.Error(location)) 41 | public fun withListEntry(index: Int, location: Location): YamlPath = withSegment(YamlPathSegment.ListEntry(index, location)) 42 | public fun withMapElementKey(key: String, location: Location): YamlPath = withSegment(YamlPathSegment.MapElementKey(key, location)) 43 | public fun withMapElementValue(location: Location): YamlPath = withSegment(YamlPathSegment.MapElementValue(location)) 44 | public fun withAliasReference(name: String, location: Location): YamlPath = withSegment(YamlPathSegment.AliasReference(name, location)) 45 | public fun withAliasDefinition(name: String, location: Location): YamlPath = withSegment(YamlPathSegment.AliasDefinition(name, location)) 46 | public fun withMerge(location: Location): YamlPath = withSegment(YamlPathSegment.Merge(location)) 47 | private fun withSegment(segment: YamlPathSegment): YamlPath = YamlPath(segments + segment) 48 | 49 | public fun toHumanReadableString(): String { 50 | val builder = StringBuilder() 51 | 52 | var nextSegmentIndex = 1 53 | 54 | while (nextSegmentIndex <= segments.lastIndex) { 55 | val segmentIndex = nextSegmentIndex 56 | nextSegmentIndex++ 57 | 58 | when (val segment = segments[segmentIndex]) { 59 | is YamlPathSegment.ListEntry -> { 60 | builder.append('[') 61 | builder.append(segment.index) 62 | builder.append(']') 63 | } 64 | is YamlPathSegment.MapElementKey -> { 65 | if (builder.isNotEmpty()) { 66 | builder.append('.') 67 | } 68 | 69 | builder.append(segment.key) 70 | } 71 | is YamlPathSegment.AliasReference -> { 72 | builder.append("->&") 73 | builder.append(segment.name) 74 | } 75 | is YamlPathSegment.Merge -> { 76 | builder.append(">>(merged") 77 | 78 | if (nextSegmentIndex <= segments.lastIndex && segments[nextSegmentIndex] is YamlPathSegment.ListEntry) { 79 | builder.append(" entry ") 80 | builder.append((segments[nextSegmentIndex] as YamlPathSegment.ListEntry).index) 81 | nextSegmentIndex++ 82 | } 83 | 84 | if (nextSegmentIndex <= segments.lastIndex && segments[nextSegmentIndex] is YamlPathSegment.AliasReference) { 85 | builder.append(" &") 86 | builder.append((segments[nextSegmentIndex] as YamlPathSegment.AliasReference).name) 87 | nextSegmentIndex++ 88 | } 89 | 90 | builder.append(")") 91 | } 92 | is YamlPathSegment.Root, is YamlPathSegment.Error, is YamlPathSegment.MapElementValue, is YamlPathSegment.AliasDefinition -> { 93 | // Nothing to do. 94 | } 95 | } 96 | } 97 | 98 | if (builder.isNotEmpty()) { 99 | return builder.toString() 100 | } 101 | 102 | return "" 103 | } 104 | 105 | public companion object { 106 | public val root: YamlPath = YamlPath(YamlPathSegment.Root) 107 | public fun forAliasDefinition(name: String, location: Location): YamlPath = YamlPath(YamlPathSegment.AliasDefinition(name, location)) 108 | } 109 | } 110 | 111 | public sealed class YamlPathSegment(public open val location: Location) { 112 | public object Root : YamlPathSegment(Location(1, 1)) 113 | public data class ListEntry(val index: Int, override val location: Location) : YamlPathSegment(location) 114 | public data class MapElementKey(val key: String, override val location: Location) : YamlPathSegment(location) 115 | public data class MapElementValue(override val location: Location) : YamlPathSegment(location) 116 | public data class AliasReference(val name: String, override val location: Location) : YamlPathSegment(location) 117 | public data class AliasDefinition(val name: String, override val location: Location) : YamlPathSegment(location) 118 | public data class Merge(override val location: Location) : YamlPathSegment(location) 119 | public data class Error(override val location: Location) : YamlPathSegment(location) 120 | } 121 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlPolymorphicInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | import kotlinx.serialization.KSerializer 24 | import kotlinx.serialization.SerializationException 25 | import kotlinx.serialization.SerializationStrategy 26 | import kotlinx.serialization.descriptors.PolymorphicKind 27 | import kotlinx.serialization.descriptors.SerialDescriptor 28 | import kotlinx.serialization.descriptors.elementNames 29 | import kotlinx.serialization.encoding.CompositeDecoder 30 | import kotlinx.serialization.modules.SerializersModule 31 | import kotlinx.serialization.modules.SerializersModuleCollector 32 | import kotlin.reflect.KClass 33 | 34 | @OptIn(ExperimentalSerializationApi::class) 35 | internal class YamlPolymorphicInput(val typeName: String, private val typeNamePath: YamlPath, val contentNode: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(contentNode, yaml, context, configuration) { 36 | private var currentField = CurrentField.NotStarted 37 | private lateinit var contentDecoder: YamlInput 38 | 39 | override fun getCurrentLocation(): Location = contentNode.location 40 | override fun getCurrentPath(): YamlPath = contentNode.path 41 | 42 | override fun decodeElementIndex(descriptor: SerialDescriptor): Int { 43 | return when (currentField) { 44 | CurrentField.NotStarted -> { 45 | currentField = CurrentField.Type 46 | 0 47 | } 48 | CurrentField.Type -> { 49 | when (contentNode) { 50 | is YamlScalar -> contentDecoder = YamlScalarInput(contentNode, yaml, serializersModule, configuration) 51 | is YamlNull -> contentDecoder = YamlNullInput(contentNode, yaml, serializersModule, configuration) 52 | else -> { 53 | // Nothing to do here - contentDecoder is set in beginStructure() for non-scalar values. 54 | } 55 | } 56 | 57 | currentField = CurrentField.Content 58 | 1 59 | } 60 | CurrentField.Content -> CompositeDecoder.DECODE_DONE 61 | } 62 | } 63 | 64 | override fun decodeNotNullMark(): Boolean = maybeCallOnContent(blockOnType = { true }, blockOnContent = YamlInput::decodeNotNullMark) 65 | override fun decodeNull(): Nothing? = maybeCallOnContent("decodeNull", blockOnContent = YamlInput::decodeNull) 66 | override fun decodeBoolean(): Boolean = maybeCallOnContent("decodeBoolean", blockOnContent = YamlInput::decodeBoolean) 67 | override fun decodeByte(): Byte = maybeCallOnContent("decodeByte", blockOnContent = YamlInput::decodeByte) 68 | override fun decodeShort(): Short = maybeCallOnContent("decodeShort", blockOnContent = YamlInput::decodeShort) 69 | override fun decodeInt(): Int = maybeCallOnContent("decodeInt", blockOnContent = YamlInput::decodeInt) 70 | override fun decodeLong(): Long = maybeCallOnContent("decodeLong", blockOnContent = YamlInput::decodeLong) 71 | override fun decodeFloat(): Float = maybeCallOnContent("decodeFloat", blockOnContent = YamlInput::decodeFloat) 72 | override fun decodeDouble(): Double = maybeCallOnContent("decodeDouble", blockOnContent = YamlInput::decodeDouble) 73 | override fun decodeChar(): Char = maybeCallOnContent("decodeChar", blockOnContent = YamlInput::decodeChar) 74 | override fun decodeString(): String = maybeCallOnContent(blockOnType = { typeName }, blockOnContent = YamlInput::decodeString) 75 | override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = maybeCallOnContent("decodeEnum") { decodeEnum(enumDescriptor) } 76 | 77 | override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { 78 | return when (currentField) { 79 | CurrentField.NotStarted, CurrentField.Type -> super.beginStructure(descriptor) 80 | CurrentField.Content -> { 81 | contentDecoder = createFor(contentNode, yaml, serializersModule, configuration, descriptor) 82 | 83 | return contentDecoder 84 | } 85 | } 86 | } 87 | 88 | private inline fun maybeCallOnContent(functionName: String, blockOnContent: YamlInput.() -> T): T = 89 | maybeCallOnContent(blockOnType = { throw UnsupportedOperationException("Can't call $functionName() on type field") }, blockOnContent = blockOnContent) 90 | 91 | private inline fun maybeCallOnContent(blockOnType: () -> T, blockOnContent: YamlInput.() -> T): T { 92 | return when (currentField) { 93 | CurrentField.NotStarted, CurrentField.Type -> blockOnType() 94 | CurrentField.Content -> contentDecoder.blockOnContent() 95 | } 96 | } 97 | 98 | override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { 99 | try { 100 | return super.decodeSerializableValue(deserializer) 101 | } catch (e: SerializationException) { 102 | throwIfUnknownPolymorphicTypeException(e, deserializer) 103 | 104 | throw e 105 | } 106 | } 107 | 108 | private fun throwIfUnknownPolymorphicTypeException(e: Exception, deserializer: DeserializationStrategy<*>) { 109 | val message = e.message ?: return 110 | val match = unknownPolymorphicTypeExceptionMessage.matchAt(message, 0) ?: return 111 | val unknownType = match.groupValues[1] 112 | val className = match.groupValues[2] 113 | 114 | val knownTypes = when (deserializer.descriptor.kind) { 115 | PolymorphicKind.SEALED -> getKnownTypesForSealedType(deserializer) 116 | PolymorphicKind.OPEN -> getKnownTypesForOpenType(className) 117 | else -> throw IllegalArgumentException("Can't get known types for descriptor of kind ${deserializer.descriptor.kind}") 118 | } 119 | 120 | throw UnknownPolymorphicTypeException(unknownType, knownTypes, typeNamePath, e) 121 | } 122 | 123 | private fun getKnownTypesForSealedType(deserializer: DeserializationStrategy<*>): Set { 124 | val typesDescriptor = deserializer.descriptor.getElementDescriptor(1) 125 | 126 | return typesDescriptor.elementNames.toSet() 127 | } 128 | 129 | private fun getKnownTypesForOpenType(className: String): Set { 130 | val knownTypes = mutableSetOf() 131 | 132 | serializersModule.dumpTo(object : SerializersModuleCollector { 133 | override fun contextual(kClass: KClass, provider: (typeArgumentsSerializers: List>) -> KSerializer<*>) {} 134 | 135 | // FIXME: ideally we'd be able to get the name as used by the SerialModule (eg. the values in 'polyBase2NamedSerializers' in SerialModuleImpl, but these aren't exposed. 136 | // The serializer's descriptor's name seems to be the same value. 137 | override fun polymorphic(baseClass: KClass, actualClass: KClass, actualSerializer: KSerializer) { 138 | if (baseClass.simpleName == className) { 139 | knownTypes.add(actualSerializer.descriptor.serialName) 140 | } 141 | } 142 | 143 | @ExperimentalSerializationApi 144 | override fun polymorphicDefaultSerializer(baseClass: KClass, defaultSerializerProvider: (value: Base) -> SerializationStrategy?) { 145 | throw UnsupportedOperationException("This method should never be called.") 146 | } 147 | 148 | @ExperimentalSerializationApi 149 | override fun polymorphicDefaultDeserializer(baseClass: KClass, defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?) { 150 | throw UnsupportedOperationException("This method should never be called") 151 | } 152 | }) 153 | 154 | return knownTypes 155 | } 156 | 157 | private enum class CurrentField { 158 | NotStarted, 159 | Type, 160 | Content, 161 | } 162 | 163 | companion object { 164 | private val unknownPolymorphicTypeExceptionMessage: Regex = """^Serializer for subclass '(.*)' is not found in the polymorphic scope of '(.*)'.\n.*""".toRegex() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/charleskorn/kaml/YamlScalarInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.descriptors.SerialDescriptor 23 | import kotlinx.serialization.encoding.CompositeDecoder 24 | import kotlinx.serialization.modules.SerializersModule 25 | 26 | @OptIn(ExperimentalSerializationApi::class) 27 | internal class YamlScalarInput(val scalar: YamlScalar, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(scalar, yaml, context, configuration) { 28 | override fun decodeString(): String = scalar.content 29 | override fun decodeInt(): Int = scalar.toInt() 30 | override fun decodeLong(): Long = scalar.toLong() 31 | override fun decodeShort(): Short = scalar.toShort() 32 | override fun decodeByte(): Byte = scalar.toByte() 33 | override fun decodeDouble(): Double = scalar.toDouble() 34 | override fun decodeFloat(): Float = scalar.toFloat() 35 | override fun decodeBoolean(): Boolean = scalar.toBoolean() 36 | override fun decodeChar(): Char = scalar.toChar() 37 | 38 | override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { 39 | val index = enumDescriptor.getElementIndex(scalar.content) 40 | 41 | if (index != CompositeDecoder.UNKNOWN_NAME) { 42 | return index 43 | } 44 | 45 | val choices = (0.. Unit = {}) : this() { 48 | body() 49 | } 50 | 51 | // Overload context functions with non-nested versions 52 | @JvmName("context\$FlatSpec") 53 | fun context(name: String, test: FlatSpecContainerScope.() -> Unit): Unit = 54 | test(FlatSpecContainerScope(this, name.toContainerPrefix(), false)) 55 | 56 | @JvmName("xcontext\$FlatSpec") 57 | fun xcontext(name: String, test: FlatSpecContainerScope.() -> Unit): Unit = 58 | test(FlatSpecContainerScope(this, name.toContainerPrefix(), true)) 59 | 60 | // Suppress FunSpec's context functions, so they can't be used 61 | @Deprecated("Unsupported", level = DeprecationLevel.HIDDEN) 62 | override fun context(name: String, test: suspend FunSpecContainerScope.() -> Unit): Nothing = error("Unsupported") 63 | 64 | @Deprecated("Unsupported", level = DeprecationLevel.HIDDEN) 65 | override fun xcontext(name: String, test: suspend FunSpecContainerScope.() -> Unit): Nothing = error("Unsupported") 66 | 67 | @ExperimentalKotest 68 | @Deprecated("Unsupported", level = DeprecationLevel.HIDDEN) 69 | override fun context(name: String): Nothing = error("Unsupported") 70 | 71 | @ExperimentalKotest 72 | @Deprecated("Unsupported", level = DeprecationLevel.HIDDEN) 73 | override fun xcontext(name: String): Nothing = error("Unsupported") 74 | } 75 | 76 | @Suppress("unused") 77 | @KotestTestScope 78 | class FlatSpecContainerScope( 79 | private val spec: FlatFunSpec, 80 | private val prefix: String, 81 | private val ignored: Boolean, 82 | ) { 83 | fun test(name: String): RootTestWithConfigBuilder = if (ignored) { 84 | spec.xtest(prefix + name) 85 | } else { 86 | spec.test(prefix + name) 87 | } 88 | 89 | fun test(name: String, test: suspend TestScope.() -> Unit): Unit = if (ignored) { 90 | spec.xtest(prefix + name, test) 91 | } else { 92 | spec.test(prefix + name, test) 93 | } 94 | 95 | fun xtest(name: String): RootTestWithConfigBuilder = spec.xtest(prefix + name) 96 | 97 | fun xtest(name: String, test: suspend TestScope.() -> Unit): Unit = spec.xtest(prefix + name, test) 98 | 99 | fun context(name: String, test: FlatSpecContainerScope.() -> Unit): Unit = if (ignored) { 100 | spec.xcontext(prefix + name, test) 101 | } else { 102 | spec.context(prefix + name, test) 103 | } 104 | 105 | fun xcontext(name: String, test: FlatSpecContainerScope.() -> Unit): Unit = spec.xcontext(prefix + name, test) 106 | } 107 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/TestSerializers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.InternalSerializationApi 23 | import kotlinx.serialization.KSerializer 24 | import kotlinx.serialization.builtins.MapSerializer 25 | import kotlinx.serialization.builtins.serializer 26 | import kotlinx.serialization.descriptors.SerialDescriptor 27 | import kotlinx.serialization.descriptors.SerialKind 28 | import kotlinx.serialization.descriptors.buildSerialDescriptor 29 | import kotlinx.serialization.encoding.Decoder 30 | import kotlinx.serialization.encoding.Encoder 31 | 32 | @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) 33 | internal object LocationThrowingSerializer : KSerializer { 34 | override val descriptor = buildSerialDescriptor(LocationThrowingSerializer::class.simpleName!!, SerialKind.CONTEXTUAL) 35 | 36 | override fun deserialize(decoder: Decoder): Any { 37 | val location = (decoder as YamlInput).getCurrentLocation() 38 | val path = decoder.getCurrentPath() 39 | 40 | throw LocationInformationException("Serializer called with location (${location.line}, ${location.column}) and path: ${path.toHumanReadableString()}") 41 | } 42 | 43 | override fun serialize(encoder: Encoder, value: Any) = throw UnsupportedOperationException() 44 | } 45 | 46 | internal object LocationThrowingMapSerializer : KSerializer { 47 | override val descriptor: SerialDescriptor = MapSerializer(String.serializer(), String.serializer()).descriptor 48 | 49 | override fun deserialize(decoder: Decoder): Any { 50 | val location = (decoder as YamlInput).getCurrentLocation() 51 | val path = decoder.getCurrentPath() 52 | 53 | throw LocationInformationException("Serializer called with location (${location.line}, ${location.column}) and path: ${path.toHumanReadableString()}") 54 | } 55 | 56 | override fun serialize(encoder: Encoder, value: Any) = throw UnsupportedOperationException() 57 | } 58 | 59 | internal class LocationInformationException(message: String) : RuntimeException(message) 60 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | enum class KotlinTarget { 22 | JVM, 23 | JS, 24 | WASM, 25 | NATIVE, 26 | } 27 | 28 | expect val kotlinTarget: KotlinTarget 29 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import com.charleskorn.kaml.testobjects.TestSealedStructure 22 | import com.charleskorn.kaml.testobjects.polymorphicModule 23 | import io.kotest.assertions.asClue 24 | import io.kotest.assertions.throwables.shouldThrow 25 | import io.kotest.matchers.shouldBe 26 | import kotlinx.serialization.DeserializationStrategy 27 | import kotlinx.serialization.SerializationException 28 | import kotlinx.serialization.builtins.ListSerializer 29 | import kotlinx.serialization.builtins.nullable 30 | 31 | class YamlContentPolymorphicSerializerTest : FlatFunSpec({ 32 | context("a YAML parser") { 33 | context("parsing polymorphic values with PolymorphismStyle.None") { 34 | val polymorphicYaml = Yaml( 35 | serializersModule = polymorphicModule, 36 | configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None), 37 | ) 38 | 39 | context("given some input where the value should be a sealed class") { 40 | val input = """ 41 | value: "asdfg" 42 | """.trimIndent() 43 | 44 | val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) 45 | 46 | test("deserializes it to a Kotlin object") { 47 | result shouldBe TestSealedStructure.SimpleSealedString("asdfg") 48 | } 49 | } 50 | 51 | context("given some input where the value should be a sealed class (inline)") { 52 | val input = """ 53 | "abcdef" 54 | """.trimIndent() 55 | 56 | val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) 57 | 58 | test("deserializes it to a Kotlin object") { 59 | result shouldBe TestSealedStructure.InlineSealedString("abcdef") 60 | } 61 | } 62 | 63 | context("given some input missing without the serializer") { 64 | val input = """ 65 | value: "asdfg" 66 | """.trimIndent() 67 | 68 | test("throws an exception with the correct location information") { 69 | val exception = shouldThrow { 70 | polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) 71 | } 72 | 73 | exception.asClue { 74 | it.message shouldBe "Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'" 75 | it.line shouldBe 1 76 | it.column shouldBe 1 77 | it.path shouldBe YamlPath.root 78 | } 79 | } 80 | } 81 | 82 | context("given some input representing a list of polymorphic objects") { 83 | val input = """ 84 | - value: null 85 | - value: -987 86 | - value: 654 87 | - "testing" 88 | - value: "tests" 89 | """.trimIndent() 90 | 91 | val result = polymorphicYaml.decodeFromString( 92 | ListSerializer(TestSealedStructureBasedOnContentSerializer), 93 | input, 94 | ) 95 | 96 | test("deserializes it to a Kotlin object") { 97 | result shouldBe listOf( 98 | TestSealedStructure.SimpleSealedString(null), 99 | TestSealedStructure.SimpleSealedInt(-987), 100 | TestSealedStructure.SimpleSealedInt(654), 101 | TestSealedStructure.InlineSealedString("testing"), 102 | TestSealedStructure.SimpleSealedString("tests"), 103 | ) 104 | } 105 | } 106 | 107 | context("given some input with a tag and a type property") { 108 | val input = """ 109 | ! 110 | kind: sealedString 111 | value: "asdfg" 112 | """.trimIndent() 113 | 114 | test("throws an exception with the correct location information") { 115 | val exception = shouldThrow { 116 | polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) 117 | } 118 | 119 | exception.asClue { 120 | it.message shouldBe "Encountered a tagged polymorphic descriptor but PolymorphismStyle is 'None'" 121 | it.line shouldBe 1 122 | it.column shouldBe 1 123 | it.path shouldBe YamlPath.root 124 | } 125 | } 126 | } 127 | } 128 | } 129 | context("a YAML serializer") { 130 | context("serializing polymorphic values with custom serializer") { 131 | val polymorphicYaml = Yaml( 132 | serializersModule = polymorphicModule, 133 | configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag), 134 | ) 135 | 136 | context("serializing a sealed type") { 137 | val input = TestSealedStructure.SimpleSealedInt(5) 138 | val output = polymorphicYaml.encodeToString(TestSealedStructureBasedOnContentSerializer, input) 139 | val expectedYaml = """ 140 | value: 5 141 | """.trimIndent() 142 | 143 | test("returns the value serialized in the expected YAML form") { 144 | output shouldBe expectedYaml 145 | } 146 | } 147 | 148 | context("serializing a list of polymorphic values") { 149 | val input = listOf( 150 | TestSealedStructure.SimpleSealedInt(5), 151 | TestSealedStructure.SimpleSealedString("some test"), 152 | TestSealedStructure.SimpleSealedInt(-20), 153 | TestSealedStructure.InlineSealedString("testing"), 154 | TestSealedStructure.SimpleSealedString(null), 155 | null, 156 | ) 157 | 158 | val output = polymorphicYaml.encodeToString( 159 | ListSerializer(TestSealedStructureBasedOnContentSerializer.nullable), 160 | input, 161 | ) 162 | 163 | val expectedYaml = """ 164 | - value: 5 165 | - value: "some test" 166 | - value: -20 167 | - "testing" 168 | - value: null 169 | - null 170 | """.trimIndent() 171 | 172 | test("returns the value serialized in the expected YAML form") { 173 | output shouldBe expectedYaml 174 | } 175 | } 176 | } 177 | } 178 | }) 179 | 180 | object TestSealedStructureBasedOnContentSerializer : YamlContentPolymorphicSerializer( 181 | TestSealedStructure::class, 182 | ) { 183 | override fun selectDeserializer(node: YamlNode): DeserializationStrategy = when (node) { 184 | is YamlScalar -> TestSealedStructure.InlineSealedString.serializer() 185 | is YamlMap -> when (val value: YamlNode? = node["value"]) { 186 | is YamlScalar -> when { 187 | value.content.toIntOrNull() == null -> TestSealedStructure.SimpleSealedString.serializer() 188 | else -> TestSealedStructure.SimpleSealedInt.serializer() 189 | } 190 | is YamlNull -> TestSealedStructure.SimpleSealedString.serializer() 191 | else -> throw SerializationException("Unsupported property type for TestSealedStructure.value: ${value?.let { it::class.simpleName}}") 192 | } 193 | else -> throw SerializationException("Unsupported node type for TestSealedStructure: ${node::class.simpleName}") 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlExceptionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.core.spec.style.FunSpec 22 | import io.kotest.matchers.shouldBe 23 | 24 | class YamlExceptionTest : FunSpec({ 25 | test("Formatting a YAML exception as a string") { 26 | val path = YamlPath.root 27 | .withMapElementKey("colours", Location(3, 4)) 28 | .withMapElementValue(Location(4, 1)) 29 | .withListEntry(2, Location(123, 456)) 30 | val exception = YamlException("Something went wrong", path) 31 | 32 | exception.toString() shouldBe "YamlException at colours[2] on line 123, column 456: Something went wrong" 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.assertions.throwables.shouldThrow 22 | import io.kotest.core.spec.style.FunSpec 23 | import io.kotest.matchers.shouldBe 24 | 25 | class YamlListTest : FunSpec({ 26 | 27 | val list = YamlList( 28 | listOf( 29 | YamlScalar("item 1", YamlPath.root.withListEntry(0, Location(4, 5))), 30 | YamlScalar("item 2", YamlPath.root.withListEntry(1, Location(6, 7))), 31 | ), 32 | YamlPath.root, 33 | ) 34 | 35 | test("list equivalence with same instance") { 36 | list.equivalentContentTo(list) shouldBe true 37 | } 38 | 39 | test("list equivalence with same items but different path") { 40 | list.equivalentContentTo(YamlList(list.items, YamlPath.root.withMapElementValue(Location(5, 6)))) shouldBe true 41 | } 42 | 43 | test("list equivalence with same items in different order") { 44 | list.equivalentContentTo(YamlList(list.items.reversed(), list.path)) shouldBe false 45 | } 46 | 47 | test("list equivalence with different items") { 48 | list.equivalentContentTo(YamlList(emptyList(), list.path)) shouldBe false 49 | } 50 | 51 | test("list equivalence with scalar value") { 52 | list.equivalentContentTo(YamlScalar("some content", list.path)) shouldBe false 53 | } 54 | 55 | test("list equivalence with null value") { 56 | list.equivalentContentTo(YamlNull(list.path)) shouldBe false 57 | } 58 | 59 | test("list equivalence with map") { 60 | list.equivalentContentTo(YamlMap(emptyMap(), list.path)) shouldBe false 61 | } 62 | 63 | val firstItemPath = YamlPath.root.withListEntry(0, Location(4, 5)) 64 | val secondItemPath = YamlPath.root.withListEntry(1, Location(6, 7)) 65 | 66 | val listElements = YamlList( 67 | listOf( 68 | YamlScalar("item 1", firstItemPath), 69 | YamlScalar("item 2", secondItemPath), 70 | ), 71 | YamlPath.root, 72 | ) 73 | 74 | test("getting element in bounds from list") { 75 | listElements[0] shouldBe YamlScalar("item 1", firstItemPath) 76 | listElements[1] shouldBe YamlScalar("item 2", secondItemPath) 77 | } 78 | 79 | test("getting element out of bounds from list") { 80 | shouldThrow { listElements[2] } 81 | shouldThrow { listElements[10] } 82 | } 83 | 84 | test("converting content of an empty list to a human-readable string") { 85 | val listEmpty = YamlList(emptyList(), YamlPath.root) 86 | listEmpty.contentToString() shouldBe "[]" 87 | } 88 | 89 | test("converting content of a list with a single entry to a human-readable string") { 90 | val listSingleEntry = YamlList(listOf(YamlScalar("hello", YamlPath.root.withListEntry(0, Location(1, 1)))), YamlPath.root) 91 | listSingleEntry.contentToString() shouldBe "['hello']" 92 | } 93 | 94 | test("converting content of a list with multiple entries to a human-readable string") { 95 | val listMultipleEntries = YamlList( 96 | listOf( 97 | YamlScalar("hello", YamlPath.root.withListEntry(0, Location(1, 1))), 98 | YamlScalar("world", YamlPath.root.withListEntry(1, Location(2, 1))), 99 | ), 100 | YamlPath.root, 101 | ) 102 | listMultipleEntries.contentToString() shouldBe "['hello', 'world']" 103 | } 104 | 105 | test("replacing list's path") { 106 | val original = YamlList( 107 | listOf( 108 | YamlScalar("hello", YamlPath.root.withListEntry(0, Location(1, 1))), 109 | YamlScalar("world", YamlPath.root.withListEntry(1, Location(2, 1))), 110 | ), 111 | YamlPath.root, 112 | ) 113 | 114 | val newPath = YamlPath.forAliasDefinition("blah", Location(2, 3)) 115 | 116 | val expected = YamlList( 117 | listOf( 118 | YamlScalar("hello", newPath.withListEntry(0, Location(1, 1))), 119 | YamlScalar("world", newPath.withListEntry(1, Location(2, 1))), 120 | ), 121 | newPath, 122 | ) 123 | 124 | original.withPath(newPath) shouldBe expected 125 | } 126 | 127 | test("converting list to a string") { 128 | val path = YamlPath.root.withMapElementKey("test", Location(2, 1)).withMapElementValue(Location(2, 7)) 129 | val elementPath = path.withListEntry(0, Location(3, 3)) 130 | val value = YamlList(listOf(YamlScalar("hello", elementPath)), path) 131 | 132 | value.toString() shouldBe 133 | """ 134 | list @ $path (size: 1) 135 | - item 0: 136 | scalar @ $elementPath : hello 137 | """.trimIndent() 138 | } 139 | }) 140 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlNodeTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.assertions.asClue 22 | import io.kotest.assertions.throwables.shouldNotThrowAny 23 | import io.kotest.assertions.throwables.shouldThrow 24 | import io.kotest.core.spec.style.FunSpec 25 | import io.kotest.datatest.withData 26 | import io.kotest.matchers.shouldBe 27 | import kotlin.reflect.KProperty1 28 | 29 | class YamlNodeTest : FunSpec({ 30 | data class TestCase(val name: String, val method: KProperty1, val value: YamlNode) 31 | 32 | val path = YamlPath.root 33 | val testScalar = YamlScalar("test", path) 34 | val testNull = YamlNull(path) 35 | val testList = YamlList(emptyList(), path) 36 | val testMap = YamlMap(emptyMap(), path) 37 | val testTaggedNode = YamlTaggedNode("tag", YamlScalar("tagged_scalar", path)) 38 | 39 | withData( 40 | { it.name }, 41 | TestCase("scalar", YamlNode::yamlScalar, testScalar), 42 | TestCase("null", YamlNode::yamlNull, testNull), 43 | TestCase("list", YamlNode::yamlList, testList), 44 | TestCase("map", YamlNode::yamlMap, testMap), 45 | TestCase("tagged node", YamlNode::yamlTaggedNode, testTaggedNode), 46 | ) { testCase -> 47 | shouldNotThrowAny { testCase.method(testCase.value) } 48 | testCase.method(testCase.value) shouldBe testCase.value 49 | } 50 | 51 | withData( 52 | { it.name }, 53 | TestCase("retrieving a scalar from a null", YamlNode::yamlScalar, testNull), 54 | TestCase("retrieving a scalar from a list", YamlNode::yamlScalar, testList), 55 | TestCase("retrieving a scalar from a map", YamlNode::yamlScalar, testMap), 56 | TestCase("retrieving a scalar from a tagged node", YamlNode::yamlScalar, testTaggedNode), 57 | TestCase("retrieving a null from a scalar", YamlNode::yamlNull, testScalar), 58 | TestCase("retrieving a null from a list", YamlNode::yamlNull, testList), 59 | TestCase("retrieving a null from a map", YamlNode::yamlNull, testMap), 60 | TestCase("retrieving a null from a tagged node", YamlNode::yamlNull, testTaggedNode), 61 | TestCase("retrieving a list from a scalar", YamlNode::yamlList, testScalar), 62 | TestCase("retrieving a list from a null", YamlNode::yamlList, testNull), 63 | TestCase("retrieving a list from a map", YamlNode::yamlList, testMap), 64 | TestCase("retrieving a list from a tagged node", YamlNode::yamlList, testTaggedNode), 65 | TestCase("retrieving a map from a scalar", YamlNode::yamlMap, testScalar), 66 | TestCase("retrieving a map from a null", YamlNode::yamlMap, testNull), 67 | TestCase("retrieving a map from a list", YamlNode::yamlMap, testList), 68 | TestCase("retrieving a map from a tagged node", YamlNode::yamlMap, testTaggedNode), 69 | TestCase("retrieving a tagged node from a scalar", YamlNode::yamlTaggedNode, testScalar), 70 | TestCase("retrieving a tagged node from a null", YamlNode::yamlTaggedNode, testNull), 71 | TestCase("retrieving a tagged node from a list", YamlNode::yamlTaggedNode, testList), 72 | TestCase("retrieving a tagged node from a map", YamlNode::yamlTaggedNode, testMap), 73 | ) { testCase -> 74 | val type = testCase.method.name.replaceFirstChar(Char::titlecase) 75 | val fromType = testCase.value::class.simpleName 76 | val exception = shouldThrow { testCase.method(testCase.value) } 77 | exception.asClue { 78 | it.message shouldBe "Expected element to be $type but is $fromType" 79 | it.path shouldBe path 80 | } 81 | } 82 | }) 83 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlNullTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.core.spec.style.FunSpec 22 | import io.kotest.matchers.shouldBe 23 | 24 | class YamlNullTest : FunSpec({ 25 | val nullValue = YamlNull(YamlPath.root) 26 | val path = YamlPath.root.withListEntry(0, Location(2, 4)) 27 | val original = YamlNull(YamlPath.root) 28 | val newPath = YamlPath.forAliasDefinition("blah", Location(2, 3)) 29 | val value = YamlNull(path) 30 | 31 | test("Null value should be equivalent to same instance") { 32 | nullValue.equivalentContentTo(nullValue) shouldBe true 33 | } 34 | 35 | test("Null value should be equivalent to another null value with same path") { 36 | nullValue.equivalentContentTo(YamlNull(nullValue.path)) shouldBe true 37 | } 38 | 39 | test("Null value should be equivalent to another null value with different path") { 40 | nullValue.equivalentContentTo(YamlNull(path)) shouldBe true 41 | } 42 | 43 | test("Null value should not be equivalent to a scalar value") { 44 | nullValue.equivalentContentTo(YamlScalar("some content", nullValue.path)) shouldBe false 45 | } 46 | 47 | test("Null value should not be equivalent to a list") { 48 | nullValue.equivalentContentTo(YamlList(emptyList(), nullValue.path)) shouldBe false 49 | } 50 | 51 | test("Null value should not be equivalent to a map") { 52 | nullValue.equivalentContentTo(YamlMap(emptyMap(), nullValue.path)) shouldBe false 53 | } 54 | 55 | test("Converting content to human-readable string should return 'null'") { 56 | YamlNull(YamlPath.root).contentToString() shouldBe "null" 57 | } 58 | 59 | test("Replacing its path should return null value with the provided path") { 60 | original.withPath(newPath) shouldBe YamlNull(newPath) 61 | } 62 | 63 | test("Converting to string should return a human-readable description") { 64 | value.toString() shouldBe "null @ $path" 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/YamlTaggedNodeTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.core.spec.style.FunSpec 22 | import io.kotest.matchers.shouldBe 23 | 24 | class YamlTaggedNodeTest : FunSpec({ 25 | val tagged = YamlTaggedNode("tag", YamlScalar("test", YamlPath.root)) 26 | val original = YamlTaggedNode("tag", YamlScalar("value", YamlPath.root)) 27 | val newPath = YamlPath.forAliasDefinition("blah", Location(2, 3)) 28 | val value = YamlTaggedNode("some tag", YamlScalar("some value", YamlPath.root.withListEntry(2, Location(3, 4)))) 29 | val map = YamlTaggedNode("tag", YamlScalar("test", YamlPath.root)) 30 | 31 | test("Tagged node should be equivalent to the same instance") { 32 | tagged.equivalentContentTo(tagged) shouldBe true 33 | } 34 | 35 | test("Tagged node should not be equivalent to a non-tagged node") { 36 | tagged.equivalentContentTo(YamlScalar("test", YamlPath.root)) shouldBe false 37 | } 38 | 39 | test("Tagged node should not be equivalent to a tagged node with different tag") { 40 | tagged.equivalentContentTo(YamlTaggedNode("tag2", YamlScalar("test", YamlPath.root))) shouldBe false 41 | } 42 | 43 | test("Tagged node should not be equivalent to a tagged node with different child node") { 44 | tagged.equivalentContentTo(YamlTaggedNode("tag", YamlScalar("test2", YamlPath.root))) shouldBe false 45 | } 46 | 47 | test("Converting tagged scalar content to human-readable string should return tag and child") { 48 | map.contentToString() shouldBe "!tag 'test'" 49 | } 50 | 51 | test("Replacing its path should return a tagged node with the inner node updated with the provided path") { 52 | original.withPath(newPath) shouldBe YamlTaggedNode("tag", YamlScalar("value", newPath)) 53 | } 54 | 55 | test("Converting to string should return a human-readable description") { 56 | value.toString() shouldBe "tagged 'some tag': scalar @ ${value.path} : some value" 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/testobjects/PolymorphicTestObjects.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.testobjects 20 | 21 | import kotlinx.serialization.ExperimentalSerializationApi 22 | import kotlinx.serialization.KSerializer 23 | import kotlinx.serialization.SerialName 24 | import kotlinx.serialization.Serializable 25 | import kotlinx.serialization.builtins.nullable 26 | import kotlinx.serialization.builtins.serializer 27 | import kotlinx.serialization.descriptors.SerialDescriptor 28 | import kotlinx.serialization.encoding.Decoder 29 | import kotlinx.serialization.encoding.Encoder 30 | import kotlinx.serialization.modules.SerializersModule 31 | import kotlin.jvm.JvmInline 32 | 33 | @Serializable 34 | sealed interface TestSealedStructure { 35 | @Serializable 36 | @SerialName("sealedInt") 37 | data class SimpleSealedInt(val value: Int) : TestSealedStructure 38 | 39 | @Serializable 40 | @SerialName("sealedString") 41 | data class SimpleSealedString(val value: String?) : TestSealedStructure 42 | 43 | @Serializable 44 | @SerialName("inlineString") 45 | @JvmInline 46 | value class InlineSealedString(val value: String) : TestSealedStructure 47 | } 48 | 49 | @Serializable 50 | data class SealedWrapper(val element: TestSealedStructure?) 51 | 52 | open class UnsealedClass 53 | 54 | @Serializable 55 | @SerialName("unsealedBoolean") 56 | data class UnsealedBoolean(val value: Boolean) : UnsealedClass() 57 | 58 | @Serializable 59 | @SerialName("unsealedString") 60 | data class UnsealedString(val value: String) : UnsealedClass() 61 | 62 | interface UnwrappedInterface 63 | 64 | object UnwrappedNull : UnwrappedInterface { 65 | val kSerializer: KSerializer = UnwrappedValueSerializer(Unit.serializer().nullable, "simpleNull", { UnwrappedNull }, { null }) 66 | } 67 | 68 | data class UnwrappedUnit(val data: Unit) : UnwrappedInterface { 69 | companion object { 70 | val kSerializer = UnwrappedValueSerializer(Unit.serializer(), "simpleUnit", ::UnwrappedUnit, UnwrappedUnit::data) 71 | } 72 | } 73 | 74 | data class UnwrappedBoolean(val data: Boolean) : UnwrappedInterface { 75 | companion object { 76 | val kSerializer = UnwrappedValueSerializer(Boolean.serializer(), "simpleBoolean", ::UnwrappedBoolean, UnwrappedBoolean::data) 77 | } 78 | } 79 | 80 | data class UnwrappedByte(val data: Byte) : UnwrappedInterface { 81 | companion object { 82 | val kSerializer = UnwrappedValueSerializer(Byte.serializer(), "simpleByte", ::UnwrappedByte, UnwrappedByte::data) 83 | } 84 | } 85 | 86 | data class UnwrappedShort(val data: Short) : UnwrappedInterface { 87 | companion object { 88 | val kSerializer = UnwrappedValueSerializer(Short.serializer(), "simpleShort", ::UnwrappedShort, UnwrappedShort::data) 89 | } 90 | } 91 | 92 | data class UnwrappedInt(val data: Int) : UnwrappedInterface { 93 | companion object { 94 | val kSerializer = UnwrappedValueSerializer(Int.serializer(), "simpleInt", ::UnwrappedInt, UnwrappedInt::data) 95 | } 96 | } 97 | 98 | data class UnwrappedNullableInt(val data: Int?) : UnwrappedInterface { 99 | companion object { 100 | val kSerializer = UnwrappedValueSerializer(Int.serializer().nullable, "simpleNullableInt", ::UnwrappedNullableInt, UnwrappedNullableInt::data) 101 | } 102 | } 103 | 104 | data class UnwrappedLong(val data: Long) : UnwrappedInterface { 105 | companion object { 106 | val kSerializer = UnwrappedValueSerializer(Long.serializer(), "simpleLong", ::UnwrappedLong, UnwrappedLong::data) 107 | } 108 | } 109 | 110 | data class UnwrappedFloat(val data: Float) : UnwrappedInterface { 111 | companion object { 112 | val kSerializer = UnwrappedValueSerializer(Float.serializer(), "simpleFloat", ::UnwrappedFloat, UnwrappedFloat::data) 113 | } 114 | } 115 | 116 | data class UnwrappedDouble(val data: Double) : UnwrappedInterface { 117 | companion object { 118 | val kSerializer = UnwrappedValueSerializer(Double.serializer(), "simpleDouble", ::UnwrappedDouble, UnwrappedDouble::data) 119 | } 120 | } 121 | 122 | data class UnwrappedChar(val data: Char) : UnwrappedInterface { 123 | companion object { 124 | val kSerializer = UnwrappedValueSerializer(Char.serializer(), "simpleChar", ::UnwrappedChar, UnwrappedChar::data) 125 | } 126 | } 127 | 128 | data class UnwrappedString(val data: String) : UnwrappedInterface { 129 | companion object { 130 | val kSerializer = UnwrappedValueSerializer(String.serializer(), "simpleString", ::UnwrappedString, UnwrappedString::data) 131 | } 132 | } 133 | 134 | @Serializable 135 | @SerialName("simpleClass") 136 | data class UnwrappedClass(val value: String, val otherValue: String) : UnwrappedInterface 137 | 138 | @Serializable 139 | @SerialName("simpleEnum") 140 | enum class UnwrappedEnum : UnwrappedInterface { 141 | TEST, TEST2 142 | } 143 | 144 | @OptIn(ExperimentalSerializationApi::class) 145 | fun SerialDescriptor.withName(newName: String): SerialDescriptor { 146 | return object : SerialDescriptor by this { 147 | override val serialName: String = newName 148 | } 149 | } 150 | 151 | class UnwrappedValueSerializer(private val valueSerializer: KSerializer, descriptorName: String, private val fromSource: (S) -> T, private val toSource: (T) -> S) : KSerializer { 152 | override val descriptor: SerialDescriptor = valueSerializer.descriptor.withName(descriptorName) 153 | 154 | override fun deserialize(decoder: Decoder): T { 155 | return fromSource(valueSerializer.deserialize(decoder)) 156 | } 157 | 158 | override fun serialize(encoder: Encoder, value: T) { 159 | valueSerializer.serialize(encoder, toSource(value)) 160 | } 161 | } 162 | 163 | @Serializable 164 | data class PolymorphicWrapper(val test: UnwrappedInterface) 165 | 166 | val polymorphicModule = SerializersModule { 167 | polymorphic(UnwrappedInterface::class, UnwrappedNull::class, UnwrappedNull.kSerializer) 168 | polymorphic(UnwrappedInterface::class, UnwrappedUnit::class, UnwrappedUnit.kSerializer) 169 | polymorphic(UnwrappedInterface::class, UnwrappedBoolean::class, UnwrappedBoolean.kSerializer) 170 | polymorphic(UnwrappedInterface::class, UnwrappedByte::class, UnwrappedByte.kSerializer) 171 | polymorphic(UnwrappedInterface::class, UnwrappedShort::class, UnwrappedShort.kSerializer) 172 | polymorphic(UnwrappedInterface::class, UnwrappedInt::class, UnwrappedInt.kSerializer) 173 | polymorphic(UnwrappedInterface::class, UnwrappedLong::class, UnwrappedLong.kSerializer) 174 | polymorphic(UnwrappedInterface::class, UnwrappedFloat::class, UnwrappedFloat.kSerializer) 175 | polymorphic(UnwrappedInterface::class, UnwrappedDouble::class, UnwrappedDouble.kSerializer) 176 | polymorphic(UnwrappedInterface::class, UnwrappedChar::class, UnwrappedChar.kSerializer) 177 | polymorphic(UnwrappedInterface::class, UnwrappedString::class, UnwrappedString.kSerializer) 178 | polymorphic(UnwrappedInterface::class, UnwrappedEnum::class, UnwrappedEnum.serializer()) 179 | polymorphic(UnwrappedInterface::class, UnwrappedNullableInt::class, UnwrappedNullableInt.kSerializer) 180 | polymorphic(UnwrappedInterface::class, UnwrappedClass::class, UnwrappedClass.serializer()) 181 | 182 | polymorphic(UnsealedClass::class, UnsealedBoolean::class, UnsealedBoolean.serializer()) 183 | polymorphic(UnsealedClass::class, UnsealedString::class, UnsealedString.serializer()) 184 | } 185 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/charleskorn/kaml/testobjects/TestObjects.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml.testobjects 20 | 21 | import com.charleskorn.kaml.YamlList 22 | import com.charleskorn.kaml.YamlMap 23 | import com.charleskorn.kaml.YamlNode 24 | import com.charleskorn.kaml.YamlNull 25 | import com.charleskorn.kaml.YamlScalar 26 | import com.charleskorn.kaml.YamlTaggedNode 27 | import kotlinx.serialization.SerialName 28 | import kotlinx.serialization.Serializable 29 | 30 | @Serializable 31 | data class SimpleStructure( 32 | val name: String, 33 | ) 34 | 35 | @Serializable 36 | data class Team( 37 | val members: List, 38 | ) 39 | 40 | @Serializable 41 | data class NestedObjects( 42 | val firstPerson: SimpleStructure, 43 | val secondPerson: SimpleStructure, 44 | ) 45 | 46 | @Serializable 47 | enum class TestEnum { 48 | Value1, 49 | Value2, 50 | } 51 | 52 | @Serializable 53 | enum class TestEnumWithExplicitNames { 54 | @SerialName("A") 55 | Alpha, 56 | 57 | @SerialName("B") 58 | Beta, 59 | 60 | @SerialName("With space") 61 | WithSpace, 62 | } 63 | 64 | @Serializable 65 | data class TestClassWithNestedNode( 66 | val text: String, 67 | val node: YamlNode, 68 | ) 69 | 70 | @Serializable 71 | data class TestClassWithNestedScalar( 72 | val text: String, 73 | val node: YamlScalar, 74 | ) 75 | 76 | @Serializable 77 | data class TestClassWithNestedNull( 78 | val text: String, 79 | val node: YamlNull, 80 | ) 81 | 82 | @Serializable 83 | data class TestClassWithNestedMap( 84 | val text: String, 85 | val node: YamlMap, 86 | ) 87 | 88 | @Serializable 89 | data class TestClassWithNestedList( 90 | val text: String, 91 | val node: YamlList, 92 | ) 93 | 94 | @Serializable 95 | data class TestClassWithNestedTaggedNode( 96 | val text: String, 97 | val node: YamlTaggedNode, 98 | ) 99 | -------------------------------------------------------------------------------- /src/jsTest/kotlin/com/charleskorn/kaml/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | actual val kotlinTarget = KotlinTarget.JS 22 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/charleskorn/kaml/JvmYamlReading.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.DeserializationStrategy 22 | import kotlinx.serialization.serializer 23 | import okio.source 24 | import java.io.InputStream 25 | 26 | /** 27 | * Decode a YAML value [T] from an [InputStream]. 28 | * 29 | * [InputStream] must be encoded with UTF-8. 30 | */ 31 | // The character encoding is not configurable, because we use Okio which doesn't support converting 32 | // between UTF-8 and other encodings. 33 | public fun Yaml.decodeFromStream( 34 | deserializer: DeserializationStrategy, 35 | source: InputStream, 36 | ): T = decodeFromSource( 37 | deserializer = deserializer, 38 | source = source.source(), 39 | ) 40 | 41 | /** 42 | * Decode a YAML value [T] from an [InputStream]. 43 | * 44 | * [InputStream] must be encoded with UTF-8. 45 | */ 46 | public inline fun Yaml.decodeFromStream( 47 | stream: InputStream, 48 | ): T = decodeFromSource( 49 | deserializer = serializersModule.serializer(), 50 | source = stream.source(), 51 | ) 52 | 53 | /** 54 | * 55 | * Decode a [YamlNode] from an [InputStream]. 56 | * 57 | * [InputStream] must be encoded with UTF-8. 58 | */ 59 | public fun Yaml.parseToYamlNode( 60 | source: InputStream, 61 | ): YamlNode = 62 | parseToYamlNode(source.source()) 63 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/charleskorn/kaml/JvmYamlWriting.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import kotlinx.serialization.SerializationStrategy 22 | import kotlinx.serialization.serializer 23 | import okio.sink 24 | import java.io.OutputStream 25 | 26 | /** 27 | * Convert [value] to YAML, and output the result into an [OutputStream]. 28 | * 29 | * The character encoding is UTF-8. 30 | */ 31 | // The character encoding is not configurable, because we use Okio which doesn't support converting 32 | // between UTF-8 and other encodings. 33 | public fun Yaml.encodeToStream( 34 | serializer: SerializationStrategy, 35 | value: T, 36 | stream: OutputStream, 37 | ): Unit = encodeToSink(serializer, value, stream.sink()) 38 | 39 | /** 40 | * Convert [value] to YAML, and output the result into an [OutputStream]. 41 | * 42 | * The character encoding is UTF-8. 43 | */ 44 | public inline fun Yaml.encodeToStream( 45 | value: T, 46 | stream: OutputStream, 47 | ): Unit = encodeToStream(serializersModule.serializer(), value, stream) 48 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | import io.kotest.core.spec.style.DescribeSpec 22 | import io.kotest.matchers.shouldBe 23 | import kotlinx.serialization.builtins.serializer 24 | 25 | class JvmYamlReadingTest : DescribeSpec({ 26 | describe("JVM-specific extensions for YAML reading") { 27 | describe("parsing from a stream") { 28 | val input = "123" 29 | val result = Yaml.default.decodeFromStream(Int.serializer(), input.byteInputStream()) 30 | 31 | it("successfully deserializes values from a stream") { 32 | result shouldBe 123 33 | } 34 | } 35 | 36 | describe("parsing from a stream via generic extension function") { 37 | val input = "123" 38 | val result = Yaml.default.decodeFromStream(input.byteInputStream()) 39 | 40 | it("successfully deserializes values from a stream") { 41 | result shouldBe 123 42 | } 43 | } 44 | 45 | describe("parsing into a YamlNode from a string") { 46 | val input = "123" 47 | val result = Yaml.default.parseToYamlNode(input) 48 | 49 | it("successfully deserializes values from a string") { 50 | result shouldBe YamlScalar("123", YamlPath.root) 51 | } 52 | } 53 | 54 | describe("parsing into a YamlNode from a stream") { 55 | val input = "123" 56 | val result = Yaml.default.parseToYamlNode(input.byteInputStream()) 57 | 58 | it("successfully deserializes values from a stream") { 59 | result shouldBe YamlScalar("123", YamlPath.root) 60 | } 61 | } 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/charleskorn/kaml/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | actual val kotlinTarget = KotlinTarget.JVM 22 | -------------------------------------------------------------------------------- /src/nativeTest/kotlin/com/charleskorn/kaml/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | actual val kotlinTarget: KotlinTarget = KotlinTarget.NATIVE 22 | -------------------------------------------------------------------------------- /src/wasmJsTest/kotlin/com/charleskorn/kaml/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2018-2023 Charles Korn. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package com.charleskorn.kaml 20 | 21 | actual val kotlinTarget = KotlinTarget.WASM 22 | --------------------------------------------------------------------------------